improved vertical split gated behind 9:16
This commit is contained in:
+27
-42
@@ -52,11 +52,12 @@ export interface WebcamLayoutPresetDefinition {
|
||||
export interface WebcamCompositeLayout {
|
||||
screenRect: RenderRect;
|
||||
webcamRect: StyledRenderRect | null;
|
||||
/** When true, the video should be scaled to cover screenRect (cropping overflow). */
|
||||
screenCover?: boolean;
|
||||
}
|
||||
|
||||
const MAX_STAGE_FRACTION = 0.18;
|
||||
const MARGIN_FRACTION = 0.02;
|
||||
const MIN_SIZE = 96;
|
||||
const MAX_BORDER_RADIUS = 24;
|
||||
const WEBCAM_LAYOUT_PRESET_MAP: Record<WebcamLayoutPreset, WebcamLayoutPresetDefinition> = {
|
||||
"picture-in-picture": {
|
||||
@@ -65,8 +66,8 @@ const WEBCAM_LAYOUT_PRESET_MAP: Record<WebcamLayoutPreset, WebcamLayoutPresetDef
|
||||
type: "overlay",
|
||||
maxStageFraction: MAX_STAGE_FRACTION,
|
||||
marginFraction: MARGIN_FRACTION,
|
||||
minMargin: 12,
|
||||
minSize: MIN_SIZE,
|
||||
minMargin: 0,
|
||||
minSize: 0,
|
||||
},
|
||||
borderRadius: {
|
||||
max: MAX_BORDER_RADIUS,
|
||||
@@ -134,7 +135,6 @@ export function computeCompositeLayout(params: {
|
||||
webcamPosition,
|
||||
} = params;
|
||||
const { width: canvasWidth, height: canvasHeight } = canvasSize;
|
||||
const { width: maxContentWidth, height: maxContentHeight } = maxContentSize;
|
||||
const { width: screenWidth, height: screenHeight } = screenSize;
|
||||
const webcamWidth = webcamSize?.width;
|
||||
const webcamHeight = webcamSize?.height;
|
||||
@@ -146,52 +146,37 @@ export function computeCompositeLayout(params: {
|
||||
|
||||
if (preset.transform.type === "stack") {
|
||||
if (!webcamWidth || !webcamHeight || webcamWidth <= 0 || webcamHeight <= 0) {
|
||||
// No webcam — screen fills the entire canvas (cover mode)
|
||||
return {
|
||||
screenRect: centerRect({
|
||||
canvasSize,
|
||||
size: screenSize,
|
||||
maxSize: maxContentSize,
|
||||
}),
|
||||
screenRect: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
|
||||
webcamRect: null,
|
||||
screenCover: true,
|
||||
};
|
||||
}
|
||||
|
||||
const gap = preset.transform.gap;
|
||||
const normalizedWebcamHeight = webcamHeight * (screenWidth / webcamWidth);
|
||||
const combinedHeight = screenHeight + gap + normalizedWebcamHeight;
|
||||
const scale = Math.min(maxContentWidth / screenWidth, maxContentHeight / combinedHeight, 1);
|
||||
const clampedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||
const resolvedScreenHeight = Math.round(screenHeight * clampedScale);
|
||||
const resolvedScreenWidth = Math.round(screenWidth * clampedScale);
|
||||
const resolvedWebcamHeight = Math.round(normalizedWebcamHeight * clampedScale);
|
||||
const resolvedGap = Math.round(gap * clampedScale);
|
||||
const totalHeight = resolvedScreenHeight + resolvedGap + resolvedWebcamHeight;
|
||||
const top = Math.max(0, Math.floor((canvasHeight - totalHeight) / 2));
|
||||
const left = Math.max(0, Math.floor((canvasWidth - resolvedScreenWidth) / 2));
|
||||
const screenRect = {
|
||||
x: left,
|
||||
y: top,
|
||||
width: resolvedScreenWidth,
|
||||
height: resolvedScreenHeight,
|
||||
};
|
||||
// Webcam: full width at the bottom, maintaining its aspect ratio
|
||||
const webcamAspect = webcamWidth / webcamHeight;
|
||||
const resolvedWebcamWidth = canvasWidth;
|
||||
const resolvedWebcamHeight = Math.round(canvasWidth / webcamAspect);
|
||||
|
||||
// Screen: fills remaining space at the top (cover mode — may crop sides)
|
||||
const screenRectHeight = canvasHeight - resolvedWebcamHeight;
|
||||
|
||||
return {
|
||||
screenRect,
|
||||
webcamRect: {
|
||||
x: left,
|
||||
y: top + resolvedScreenHeight + resolvedGap,
|
||||
width: resolvedScreenWidth,
|
||||
height: resolvedWebcamHeight,
|
||||
borderRadius: Math.min(
|
||||
preset.borderRadius.max,
|
||||
Math.max(
|
||||
preset.borderRadius.min,
|
||||
Math.round(
|
||||
Math.min(resolvedScreenWidth, resolvedWebcamHeight) * preset.borderRadius.fraction,
|
||||
),
|
||||
),
|
||||
),
|
||||
screenRect: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: canvasWidth,
|
||||
height: Math.max(0, screenRectHeight),
|
||||
},
|
||||
webcamRect: {
|
||||
x: 0,
|
||||
y: Math.max(0, screenRectHeight),
|
||||
width: resolvedWebcamWidth,
|
||||
height: resolvedWebcamHeight,
|
||||
borderRadius: 0,
|
||||
},
|
||||
screenCover: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -429,7 +429,9 @@ export class FrameRenderer {
|
||||
|
||||
// Calculate scale to fit in viewport
|
||||
// Padding is a percentage (0-100), where 50% ~ 0.8 scale
|
||||
const paddingScale = 1.0 - (padding / 100) * 0.4;
|
||||
// Vertical stack ignores padding — it's full-bleed
|
||||
const effectivePadding = this.config.webcamLayoutPreset === "vertical-stack" ? 0 : padding;
|
||||
const paddingScale = 1.0 - (effectivePadding / 100) * 0.4;
|
||||
const viewportWidth = width * paddingScale;
|
||||
const viewportHeight = height * paddingScale;
|
||||
const compositeLayout = computeCompositeLayout({
|
||||
@@ -442,37 +444,46 @@ export class FrameRenderer {
|
||||
});
|
||||
if (!compositeLayout) return;
|
||||
|
||||
const scale = compositeLayout.screenRect.width / croppedVideoWidth;
|
||||
const screenRect = compositeLayout.screenRect;
|
||||
|
||||
// Cover mode: scale to fill the rect (may crop), otherwise fit-to-width
|
||||
let scale: number;
|
||||
if (compositeLayout.screenCover) {
|
||||
scale = Math.max(
|
||||
screenRect.width / croppedVideoWidth,
|
||||
screenRect.height / croppedVideoHeight,
|
||||
);
|
||||
} else {
|
||||
scale = screenRect.width / croppedVideoWidth;
|
||||
}
|
||||
|
||||
// Position video sprite
|
||||
this.videoSprite.width = videoWidth * scale;
|
||||
this.videoSprite.height = videoHeight * scale;
|
||||
|
||||
// Center the cropped region within the screenRect
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
const coverOffsetX = (screenRect.width - croppedDisplayWidth) / 2;
|
||||
const coverOffsetY = (screenRect.height - croppedDisplayHeight) / 2;
|
||||
|
||||
const cropPixelX = cropStartX * videoWidth * scale;
|
||||
const cropPixelY = cropStartY * videoHeight * scale;
|
||||
this.videoSprite.x = -cropPixelX;
|
||||
this.videoSprite.y = -cropPixelY;
|
||||
this.videoSprite.x = -cropPixelX + coverOffsetX;
|
||||
this.videoSprite.y = -cropPixelY + coverOffsetY;
|
||||
|
||||
// Position video container
|
||||
const croppedDisplayWidth = compositeLayout.screenRect.width;
|
||||
const croppedDisplayHeight = compositeLayout.screenRect.height;
|
||||
this.videoContainer.x = compositeLayout.screenRect.x;
|
||||
this.videoContainer.y = compositeLayout.screenRect.y;
|
||||
this.videoContainer.x = screenRect.x;
|
||||
this.videoContainer.y = screenRect.y;
|
||||
|
||||
// scale border radius by export/preview canvas ratio
|
||||
const previewWidth = this.config.previewWidth || 1920;
|
||||
const previewHeight = this.config.previewHeight || 1080;
|
||||
const canvasScaleFactor = Math.min(width / previewWidth, height / previewHeight);
|
||||
const scaledBorderRadius = borderRadius * canvasScaleFactor;
|
||||
const scaledBorderRadius = compositeLayout.screenCover ? 0 : borderRadius * canvasScaleFactor;
|
||||
|
||||
this.maskGraphics.clear();
|
||||
this.maskGraphics.roundRect(
|
||||
0,
|
||||
0,
|
||||
croppedDisplayWidth,
|
||||
croppedDisplayHeight,
|
||||
scaledBorderRadius,
|
||||
);
|
||||
this.maskGraphics.roundRect(0, 0, screenRect.width, screenRect.height, scaledBorderRadius);
|
||||
this.maskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
// Cache layout info
|
||||
|
||||
Reference in New Issue
Block a user