improved vertical split gated behind 9:16
This commit is contained in:
@@ -39,7 +39,7 @@ import { WEBCAM_LAYOUT_PRESETS } from "@/lib/compositeLayout";
|
||||
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
|
||||
import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type AspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { getTestId } from "@/utils/getTestId";
|
||||
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
|
||||
import { CropControl } from "./CropControl";
|
||||
@@ -609,7 +609,11 @@ export function SettingsPanel({
|
||||
<SelectValue placeholder={t("layout.selectPreset")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WEBCAM_LAYOUT_PRESETS.map((preset) => (
|
||||
{WEBCAM_LAYOUT_PRESETS.filter(
|
||||
(preset) =>
|
||||
preset.value === "picture-in-picture" ||
|
||||
isPortraitAspectRatio(aspectRatio),
|
||||
).map((preset) => (
|
||||
<SelectItem key={preset.value} value={preset.value} className="text-xs">
|
||||
{preset.value === "picture-in-picture"
|
||||
? t("layout.pictureInPicture")
|
||||
@@ -700,20 +704,25 @@ export function SettingsPanel({
|
||||
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
|
||||
<div
|
||||
className={`p-2 rounded-lg bg-white/5 border border-white/5 ${webcamLayoutPreset === "vertical-stack" ? "opacity-40 pointer-events-none" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="text-[10px] font-medium text-slate-300">
|
||||
{t("effects.padding")}
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500 font-mono">{padding}%</span>
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
{webcamLayoutPreset === "vertical-stack" ? "—" : `${padding}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[padding]}
|
||||
value={[webcamLayoutPreset === "vertical-stack" ? 0 : padding]}
|
||||
onValueChange={(values) => onPaddingChange?.(values[0])}
|
||||
onValueCommit={() => onPaddingCommit?.()}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
disabled={webcamLayoutPreset === "vertical-stack"}
|
||||
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
} from "@/lib/exporter";
|
||||
import type { ProjectMedia } from "@/lib/recordingSession";
|
||||
import { matchesShortcut } from "@/lib/shortcuts";
|
||||
import { getAspectRatioValue, getNativeAspectRatioValue } from "@/utils/aspectRatioUtils";
|
||||
import {
|
||||
getAspectRatioValue,
|
||||
getNativeAspectRatioValue,
|
||||
isPortraitAspectRatio,
|
||||
} from "@/utils/aspectRatioUtils";
|
||||
import { ExportDialog } from "./ExportDialog";
|
||||
import PlaybackControls from "./PlaybackControls";
|
||||
import {
|
||||
@@ -1529,7 +1533,15 @@ export default function VideoEditor() {
|
||||
selectedAnnotationId={selectedAnnotationId}
|
||||
onSelectAnnotation={handleSelectAnnotation}
|
||||
aspectRatio={aspectRatio}
|
||||
onAspectRatioChange={(ar) => pushState({ aspectRatio: ar })}
|
||||
onAspectRatioChange={(ar) =>
|
||||
pushState({
|
||||
aspectRatio: ar,
|
||||
webcamLayoutPreset:
|
||||
!isPortraitAspectRatio(ar) && webcamLayoutPreset === "vertical-stack"
|
||||
? "picture-in-picture"
|
||||
: webcamLayoutPreset,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
@@ -81,7 +81,9 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
|
||||
// Calculate scale to fit the cropped area in the viewport
|
||||
// Padding is a percentage (0-100), where 50 matches the original VIEWPORT_SCALE of 0.8
|
||||
const paddingScale = 1.0 - (padding / 100) * 0.4;
|
||||
// Vertical stack ignores padding — it's full-bleed
|
||||
const effectivePadding = webcamLayoutPreset === "vertical-stack" ? 0 : padding;
|
||||
const paddingScale = 1.0 - (effectivePadding / 100) * 0.4;
|
||||
const maxDisplayWidth = width * paddingScale;
|
||||
const maxDisplayHeight = height * paddingScale;
|
||||
|
||||
@@ -98,7 +100,15 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
videoSprite.scale.set(scale);
|
||||
|
||||
@@ -106,25 +116,24 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const fullVideoDisplayWidth = videoWidth * scale;
|
||||
const fullVideoDisplayHeight = videoHeight * scale;
|
||||
|
||||
// Calculate display size of just the cropped region
|
||||
// Position the full video sprite so that when we apply the mask,
|
||||
// the cropped region appears centered
|
||||
// The crop starts at (crop.x * videoWidth, crop.y * videoHeight) in video coordinates
|
||||
// In display coordinates, that's (crop.x * fullVideoDisplayWidth, crop.y * fullVideoDisplayHeight)
|
||||
// We want that point to be at screenRect.x, screenRect.y
|
||||
const spriteX = compositeLayout.screenRect.x - crop.x * fullVideoDisplayWidth;
|
||||
const spriteY = compositeLayout.screenRect.y - crop.y * fullVideoDisplayHeight;
|
||||
// Position the video so the cropped region is centered within the screenRect
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
const offsetX = screenRect.x + (screenRect.width - croppedDisplayWidth) / 2;
|
||||
const offsetY = screenRect.y + (screenRect.height - croppedDisplayHeight) / 2;
|
||||
const spriteX = offsetX - crop.x * fullVideoDisplayWidth;
|
||||
const spriteY = offsetY - crop.y * fullVideoDisplayHeight;
|
||||
|
||||
videoSprite.position.set(spriteX, spriteY);
|
||||
|
||||
// Apply border radius
|
||||
// Apply border radius — mask clips the video to the screenRect
|
||||
maskGraphics.clear();
|
||||
maskGraphics.roundRect(
|
||||
compositeLayout.screenRect.x,
|
||||
compositeLayout.screenRect.y,
|
||||
compositeLayout.screenRect.width,
|
||||
compositeLayout.screenRect.height,
|
||||
borderRadius,
|
||||
screenRect.x,
|
||||
screenRect.y,
|
||||
screenRect.width,
|
||||
screenRect.height,
|
||||
compositeLayout.screenCover ? 0 : borderRadius,
|
||||
);
|
||||
maskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user