mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
Merge commit 'refs/tmp/pr-independent-padding-controls'
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Palette, Trash as Trash2, UploadSimple as Upload, X } from "@phosphor-icons/react";
|
||||
import { Link, LinkBreak, Palette, Trash as Trash2, UploadSimple as Upload, X } from "@phosphor-icons/react";
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -43,6 +43,7 @@ import type {
|
||||
CursorStyle,
|
||||
EditorEffectSection,
|
||||
FigureData,
|
||||
Padding,
|
||||
PlaybackSpeed,
|
||||
WebcamOverlaySettings,
|
||||
WebcamPositionPreset,
|
||||
@@ -50,6 +51,9 @@ import type {
|
||||
ZoomMode,
|
||||
ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
import {
|
||||
isZeroPadding,
|
||||
} from "./videoPlayback/layoutUtils";
|
||||
import {
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
DEFAULT_CROP_REGION,
|
||||
@@ -60,6 +64,7 @@ import {
|
||||
DEFAULT_CURSOR_SMOOTHING,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SWAY,
|
||||
DEFAULT_PADDING,
|
||||
DEFAULT_WEBCAM_CORNER_RADIUS,
|
||||
DEFAULT_WEBCAM_MARGIN,
|
||||
DEFAULT_WEBCAM_POSITION_PRESET,
|
||||
@@ -394,8 +399,8 @@ interface SettingsPanelProps {
|
||||
onWebcamChange?: (webcam: WebcamOverlaySettings) => void;
|
||||
onUploadWebcam?: () => void;
|
||||
onClearWebcam?: () => void;
|
||||
padding?: number;
|
||||
onPaddingChange?: (padding: number) => void;
|
||||
padding?: Padding;
|
||||
onPaddingChange?: (padding: Padding) => void;
|
||||
frame?: string | null;
|
||||
onFrameChange?: (frameId: string | null) => void;
|
||||
cropRegion?: CropRegion;
|
||||
@@ -734,7 +739,7 @@ export function SettingsPanel({
|
||||
onWebcamChange,
|
||||
onUploadWebcam,
|
||||
onClearWebcam,
|
||||
padding = 50,
|
||||
padding = DEFAULT_PADDING,
|
||||
onPaddingChange,
|
||||
frame = null,
|
||||
onFrameChange,
|
||||
@@ -787,7 +792,7 @@ export function SettingsPanel({
|
||||
);
|
||||
const removeBackgroundStateRef = useRef<{
|
||||
aspectRatio: AspectRatio;
|
||||
padding: number;
|
||||
padding: Padding;
|
||||
} | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const builtInWallpaperPaths = useMemo(
|
||||
@@ -915,7 +920,7 @@ export function SettingsPanel({
|
||||
const [gradient, setGradient] = useState<string>(
|
||||
GRADIENTS.includes(selected) ? selected : GRADIENTS[0],
|
||||
);
|
||||
const removeBackgroundEnabled = aspectRatio === "native" && padding === 0;
|
||||
const removeBackgroundEnabled = aspectRatio === "native" && isZeroPadding(padding);
|
||||
|
||||
// Device frames from extension system
|
||||
const [availableFrames, setAvailableFrames] = useState<FrameInstance[]>([]);
|
||||
@@ -1126,14 +1131,60 @@ export function SettingsPanel({
|
||||
padding,
|
||||
};
|
||||
onAspectRatioChange?.("native");
|
||||
onPaddingChange?.(0);
|
||||
onPaddingChange?.({ top: 0, bottom: 0, left: 0, right: 0, linked: padding.linked });
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeBackgroundStateRef.current) {
|
||||
onAspectRatioChange?.(removeBackgroundStateRef.current.aspectRatio);
|
||||
onPaddingChange?.(removeBackgroundStateRef.current.padding);
|
||||
const previousState = removeBackgroundStateRef.current;
|
||||
if (previousState) {
|
||||
onAspectRatioChange?.(previousState.aspectRatio);
|
||||
onPaddingChange?.(previousState.padding);
|
||||
removeBackgroundStateRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback if the project loaded in a "background removed" state already
|
||||
onAspectRatioChange?.(initialEditorPreferences.aspectRatio);
|
||||
onPaddingChange?.({ ...DEFAULT_PADDING });
|
||||
};
|
||||
|
||||
const togglePaddingLink = () => {
|
||||
const isLinked = padding.linked !== false;
|
||||
const nextLinked = !isLinked;
|
||||
if (nextLinked) {
|
||||
// Compute average for relinking to avoid sudden shifts
|
||||
const avg = Math.round(
|
||||
(padding.top + padding.bottom + padding.left + padding.right) / 4,
|
||||
);
|
||||
onPaddingChange?.({
|
||||
top: avg,
|
||||
bottom: avg,
|
||||
left: avg,
|
||||
right: avg,
|
||||
linked: true,
|
||||
});
|
||||
} else {
|
||||
onPaddingChange?.({
|
||||
...padding,
|
||||
linked: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaddingSideChange = (side: keyof Padding, value: number) => {
|
||||
if (padding.linked !== false) {
|
||||
onPaddingChange?.({
|
||||
top: value,
|
||||
bottom: value,
|
||||
left: value,
|
||||
right: value,
|
||||
linked: true,
|
||||
});
|
||||
} else {
|
||||
onPaddingChange?.({
|
||||
...padding,
|
||||
[side]: value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1289,7 +1340,7 @@ export function SettingsPanel({
|
||||
const resetFrameSection = () => {
|
||||
onShadowChange?.(initialEditorPreferences.shadowIntensity);
|
||||
onBorderRadiusChange?.(initialEditorPreferences.borderRadius);
|
||||
onPaddingChange?.(initialEditorPreferences.padding);
|
||||
onPaddingChange?.(DEFAULT_PADDING);
|
||||
onFrameChange?.(null);
|
||||
onAspectRatioChange?.(initialEditorPreferences.aspectRatio);
|
||||
removeBackgroundStateRef.current = null;
|
||||
@@ -1779,17 +1830,95 @@ export function SettingsPanel({
|
||||
formatValue={(v) => `${v}px`}
|
||||
parseInput={(text) => parseFloat(text.replace(/px$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.padding")}
|
||||
value={padding}
|
||||
defaultValue={initialEditorPreferences.padding}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => onPaddingChange?.(v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5 pt-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{tSettings("effects.padding")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePaddingLink}
|
||||
className={cn(
|
||||
"p-1 rounded-md transition-colors",
|
||||
padding.linked !== false
|
||||
? "text-[#2563EB] bg-[#2563EB]/10"
|
||||
: "text-muted-foreground hover:bg-foreground/[0.05]",
|
||||
)}
|
||||
title={
|
||||
padding.linked !== false
|
||||
? tSettings("effects.paddingLinked", "Linked (Uniform)")
|
||||
: tSettings("effects.paddingUnlinked", "Unlinked (Asymmetrical)")
|
||||
}
|
||||
>
|
||||
{padding.linked !== false ? (
|
||||
<Link size={12} weight="bold" />
|
||||
) : (
|
||||
<LinkBreak size={12} weight="bold" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{padding.linked !== false ? (
|
||||
<SliderControl
|
||||
label=""
|
||||
value={padding.top}
|
||||
defaultValue={DEFAULT_PADDING.top}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => handlePaddingSideChange("top", v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5">
|
||||
<SliderControl
|
||||
label={tSettings("effects.paddingTop", "Top")}
|
||||
value={padding.top}
|
||||
defaultValue={DEFAULT_PADDING.top}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => handlePaddingSideChange("top", v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.paddingBottom", "Bottom")}
|
||||
value={padding.bottom}
|
||||
defaultValue={DEFAULT_PADDING.bottom}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => handlePaddingSideChange("bottom", v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.paddingLeft", "Left")}
|
||||
value={padding.left}
|
||||
defaultValue={DEFAULT_PADDING.left}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => handlePaddingSideChange("left", v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.paddingRight", "Right")}
|
||||
value={padding.right}
|
||||
defaultValue={DEFAULT_PADDING.right}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(v) => handlePaddingSideChange("right", v)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{tSettings("effects.removeBackground")}
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
type AutoCaptionSettings,
|
||||
type CaptionCue,
|
||||
type CursorStyle,
|
||||
type CursorTelemetryPoint,
|
||||
type Padding,
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
@@ -110,6 +110,7 @@ import {
|
||||
DEFAULT_ZOOM_IN_OVERLAP_MS,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
DEFAULT_PADDING,
|
||||
getDefaultCaptionFontFamily,
|
||||
} from "./types";
|
||||
import {
|
||||
@@ -241,7 +242,7 @@ interface VideoPlaybackProps {
|
||||
zoomOutEasing?: ZoomTransitionEasing;
|
||||
connectedZoomEasing?: ZoomTransitionEasing;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
padding?: Padding | number;
|
||||
frame?: string | null;
|
||||
cropRegion?: import("./types").CropRegion;
|
||||
webcam?: WebcamOverlaySettings;
|
||||
@@ -311,7 +312,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
zoomOutEasing = DEFAULT_ZOOM_OUT_EASING,
|
||||
connectedZoomEasing = DEFAULT_CONNECTED_ZOOM_EASING,
|
||||
borderRadius = 0,
|
||||
padding = 50,
|
||||
padding = DEFAULT_PADDING,
|
||||
frame = null,
|
||||
cropRegion,
|
||||
webcam,
|
||||
|
||||
@@ -56,11 +56,13 @@ import {
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
getDefaultCaptionFontFamily,
|
||||
type Padding,
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
DEFAULT_PADDING,
|
||||
} from "./types";
|
||||
|
||||
export const PROJECT_VERSION = 1;
|
||||
@@ -91,7 +93,7 @@ export interface ProjectEditorState {
|
||||
cursorClickBounceDuration: number;
|
||||
cursorSway: number;
|
||||
borderRadius: number;
|
||||
padding: number;
|
||||
padding: Padding;
|
||||
/** Selected frame ID (e.g. "recordly.frames/browser-dark"), or null for none */
|
||||
frame: string | null;
|
||||
cropRegion: CropRegion;
|
||||
@@ -758,7 +760,30 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorSway as number, 0, 2)
|
||||
: DEFAULT_CURSOR_SWAY,
|
||||
borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 12.5,
|
||||
padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 20,
|
||||
padding: (() => {
|
||||
const p = editor.padding;
|
||||
if (p && typeof p === "object") {
|
||||
const linked = typeof p.linked === "boolean" ? p.linked : true;
|
||||
const top = isFiniteNumber(p.top) ? clamp(p.top, 0, 100) : DEFAULT_PADDING.top;
|
||||
if (linked) {
|
||||
return { top, bottom: top, left: top, right: top, linked: true };
|
||||
}
|
||||
return {
|
||||
top,
|
||||
bottom: isFiniteNumber(p.bottom)
|
||||
? clamp(p.bottom, 0, 100)
|
||||
: DEFAULT_PADDING.bottom,
|
||||
left: isFiniteNumber(p.left) ? clamp(p.left, 0, 100) : DEFAULT_PADDING.left,
|
||||
right: isFiniteNumber(p.right) ? clamp(p.right, 0, 100) : DEFAULT_PADDING.right,
|
||||
linked: false,
|
||||
};
|
||||
}
|
||||
if (typeof p === "number" && isFiniteNumber(p)) {
|
||||
const val = clamp(p, 0, 100);
|
||||
return { top: val, bottom: val, left: val, right: val, linked: true };
|
||||
}
|
||||
return { ...DEFAULT_PADDING };
|
||||
})(),
|
||||
frame: typeof editor.frame === "string" ? editor.frame : null,
|
||||
cropRegion: {
|
||||
x: cropX,
|
||||
|
||||
@@ -312,6 +312,22 @@ export const DEFAULT_CROP_REGION: CropRegion = {
|
||||
height: 1,
|
||||
};
|
||||
|
||||
export interface Padding {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
linked?: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PADDING: Padding = {
|
||||
top: 50,
|
||||
bottom: 50,
|
||||
left: 50,
|
||||
right: 50,
|
||||
linked: true,
|
||||
};
|
||||
|
||||
export interface AudioRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
|
||||
@@ -1,6 +1,124 @@
|
||||
import { Application, Graphics, Sprite } from "pixi.js";
|
||||
import { drawSquircleOnGraphics } from "@/lib/geometry/squircle";
|
||||
import type { CropRegion } from "../types";
|
||||
import type { CropRegion, Padding } from "../types";
|
||||
|
||||
export const PADDING_SCALE_FACTOR = 0.2;
|
||||
|
||||
export function isZeroPadding(padding: Padding | number): boolean {
|
||||
if (typeof padding === "number") {
|
||||
return padding === 0;
|
||||
}
|
||||
return (
|
||||
padding.top === 0 &&
|
||||
padding.bottom === 0 &&
|
||||
padding.left === 0 &&
|
||||
padding.right === 0
|
||||
);
|
||||
}
|
||||
|
||||
export interface PaddedLayoutResult {
|
||||
scale: number;
|
||||
centerOffsetX: number;
|
||||
centerOffsetY: number;
|
||||
spriteX: number;
|
||||
spriteY: number;
|
||||
fullFrameDisplayW: number;
|
||||
fullFrameDisplayH: number;
|
||||
fullVideoDisplayWidth: number;
|
||||
fullVideoDisplayHeight: number;
|
||||
croppedDisplayWidth: number;
|
||||
croppedDisplayHeight: number;
|
||||
cropStartX: number;
|
||||
cropStartY: number;
|
||||
}
|
||||
|
||||
export function computePaddedLayout(params: {
|
||||
width: number;
|
||||
height: number;
|
||||
padding: Padding | number;
|
||||
frameInsets?: { top: number; right: number; bottom: number; left: number } | null;
|
||||
cropRegion: CropRegion;
|
||||
videoWidth: number;
|
||||
videoHeight: number;
|
||||
}): PaddedLayoutResult {
|
||||
const { width, height, padding, frameInsets, cropRegion, videoWidth, videoHeight } = params;
|
||||
|
||||
// Apply asymmetrical padding
|
||||
const p =
|
||||
typeof padding === "number"
|
||||
? { top: padding, bottom: padding, left: padding, right: padding }
|
||||
: padding;
|
||||
|
||||
// Padding is a percentage (0-100)
|
||||
// Clamp to ensure we don't have overlapping padding that exceeds 100% of a dimension
|
||||
const clampPercent = (v: number) => Math.min(100, Math.max(0, v));
|
||||
const leftPadFrac = (clampPercent(p.left) / 100) * PADDING_SCALE_FACTOR;
|
||||
const rightPadFrac = (clampPercent(p.right) / 100) * PADDING_SCALE_FACTOR;
|
||||
const topPadFrac = (clampPercent(p.top) / 100) * PADDING_SCALE_FACTOR;
|
||||
const bottomPadFrac = (clampPercent(p.bottom) / 100) * PADDING_SCALE_FACTOR;
|
||||
|
||||
const availableFracW = Math.max(0, 1.0 - leftPadFrac - rightPadFrac);
|
||||
const availableFracH = Math.max(0, 1.0 - topPadFrac - bottomPadFrac);
|
||||
|
||||
const maxDisplayWidth = width * availableFracW;
|
||||
const maxDisplayHeight = height * availableFracH;
|
||||
|
||||
const crop = cropRegion;
|
||||
const croppedVideoWidth = videoWidth * crop.width;
|
||||
const croppedVideoHeight = videoHeight * crop.height;
|
||||
|
||||
const insets = frameInsets;
|
||||
const screenFracW = insets ? 1 - insets.left - insets.right : 1;
|
||||
const screenFracH = insets ? 1 - insets.top - insets.bottom : 1;
|
||||
|
||||
const fullFrameVideoW = croppedVideoWidth / screenFracW;
|
||||
const fullFrameVideoH = croppedVideoHeight / screenFracH;
|
||||
|
||||
const scale = Math.min(
|
||||
fullFrameVideoW > 0 ? maxDisplayWidth / fullFrameVideoW : 0,
|
||||
fullFrameVideoH > 0 ? maxDisplayHeight / fullFrameVideoH : 0,
|
||||
);
|
||||
|
||||
const fullVideoDisplayWidth = videoWidth * scale;
|
||||
const fullVideoDisplayHeight = videoHeight * scale;
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
|
||||
const fullFrameDisplayW = fullFrameVideoW * scale;
|
||||
const fullFrameDisplayH = fullFrameVideoH * scale;
|
||||
|
||||
const availableCenterX = leftPadFrac * width + maxDisplayWidth / 2;
|
||||
const availableCenterY = topPadFrac * height + maxDisplayHeight / 2;
|
||||
|
||||
const frameCenterX = availableCenterX - fullFrameDisplayW / 2;
|
||||
const frameCenterY = availableCenterY - fullFrameDisplayH / 2;
|
||||
|
||||
const centerOffsetX = insets
|
||||
? frameCenterX + insets.left * fullFrameDisplayW
|
||||
: frameCenterX;
|
||||
const centerOffsetY = insets
|
||||
? frameCenterY + insets.top * fullFrameDisplayH
|
||||
: frameCenterY;
|
||||
|
||||
const spriteX = centerOffsetX - crop.x * fullVideoDisplayWidth;
|
||||
const spriteY = centerOffsetY - crop.y * fullVideoDisplayHeight;
|
||||
|
||||
return {
|
||||
scale,
|
||||
centerOffsetX,
|
||||
centerOffsetY,
|
||||
spriteX,
|
||||
spriteY,
|
||||
fullFrameDisplayW,
|
||||
fullFrameDisplayH,
|
||||
fullVideoDisplayWidth,
|
||||
fullVideoDisplayHeight,
|
||||
croppedDisplayWidth,
|
||||
croppedDisplayHeight,
|
||||
cropStartX: crop.x * videoWidth,
|
||||
cropStartY: crop.y * videoHeight,
|
||||
};
|
||||
}
|
||||
|
||||
interface LayoutParams {
|
||||
container: HTMLDivElement;
|
||||
@@ -11,7 +129,7 @@ interface LayoutParams {
|
||||
cropRegion?: CropRegion;
|
||||
lockedVideoDimensions?: { width: number; height: number } | null;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
padding?: Padding | number;
|
||||
/** Screen insets from the active device frame, used to scale/center the full frame */
|
||||
frameInsets?: { top: number; right: number; bottom: number; left: number } | null;
|
||||
}
|
||||
@@ -63,99 +181,47 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
app.canvas.style.width = "100%";
|
||||
app.canvas.style.height = "100%";
|
||||
|
||||
// Apply crop region
|
||||
const crop = cropRegion || { x: 0, y: 0, width: 1, height: 1 };
|
||||
const layout = computePaddedLayout({
|
||||
width,
|
||||
height,
|
||||
padding,
|
||||
frameInsets,
|
||||
cropRegion: crop,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
});
|
||||
|
||||
// Calculate the cropped dimensions
|
||||
const croppedVideoWidth = videoWidth * crop.width;
|
||||
const croppedVideoHeight = videoHeight * crop.height;
|
||||
videoSprite.scale.set(layout.scale);
|
||||
videoSprite.position.set(layout.spriteX, layout.spriteY);
|
||||
|
||||
const cropStartX = crop.x * videoWidth;
|
||||
const cropStartY = crop.y * videoHeight;
|
||||
const cropEndX = cropStartX + croppedVideoWidth;
|
||||
const cropEndY = cropStartY + croppedVideoHeight;
|
||||
|
||||
// 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;
|
||||
const maxDisplayWidth = width * paddingScale;
|
||||
const maxDisplayHeight = height * paddingScale;
|
||||
|
||||
// When a device frame is active, the frame extends beyond the video area.
|
||||
// We need to scale so the ENTIRE frame (video + bezels) fits in the viewport,
|
||||
// then center the full frame, not just the video content.
|
||||
const insets = frameInsets;
|
||||
// Fraction of the full frame occupied by the screen area
|
||||
const screenFracW = insets ? 1 - insets.left - insets.right : 1;
|
||||
const screenFracH = insets ? 1 - insets.top - insets.bottom : 1;
|
||||
// Full frame dimensions in video pixels (the frame image is this large relative to the screen)
|
||||
const fullFrameVideoW = croppedVideoWidth / screenFracW;
|
||||
const fullFrameVideoH = croppedVideoHeight / screenFracH;
|
||||
|
||||
const scale = Math.min(maxDisplayWidth / fullFrameVideoW, maxDisplayHeight / fullFrameVideoH);
|
||||
|
||||
videoSprite.scale.set(scale);
|
||||
|
||||
// Calculate display size of the full video at this scale
|
||||
const fullVideoDisplayWidth = videoWidth * scale;
|
||||
const fullVideoDisplayHeight = videoHeight * scale;
|
||||
|
||||
// Calculate display size of just the cropped region
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
|
||||
// Center the full frame (or just the video if no frame) in the container
|
||||
// Full frame display dimensions
|
||||
const fullFrameDisplayW = fullFrameVideoW * scale;
|
||||
const fullFrameDisplayH = fullFrameVideoH * scale;
|
||||
// The full frame's top-left, centered in the viewport
|
||||
const frameCenterX = (width - fullFrameDisplayW) / 2;
|
||||
const frameCenterY = (height - fullFrameDisplayH) / 2;
|
||||
// The screen area starts at frameCenterX + insets.left * fullFrameDisplayW
|
||||
const centerOffsetX = insets
|
||||
? frameCenterX + insets.left * fullFrameDisplayW
|
||||
: (width - croppedDisplayWidth) / 2;
|
||||
const centerOffsetY = insets
|
||||
? frameCenterY + insets.top * fullFrameDisplayH
|
||||
: (height - croppedDisplayHeight) / 2;
|
||||
|
||||
// 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 centerOffsetX, centerOffsetY
|
||||
const spriteX = centerOffsetX - crop.x * fullVideoDisplayWidth;
|
||||
const spriteY = centerOffsetY - crop.y * fullVideoDisplayHeight;
|
||||
|
||||
videoSprite.position.set(spriteX, spriteY);
|
||||
|
||||
// Create a mask that only shows the cropped region (centered in container)
|
||||
const maskX = centerOffsetX;
|
||||
const maskY = centerOffsetY;
|
||||
|
||||
// Apply border radius
|
||||
maskGraphics.clear();
|
||||
drawSquircleOnGraphics(maskGraphics, {
|
||||
x: maskX,
|
||||
y: maskY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
radius: borderRadius,
|
||||
});
|
||||
maskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
return {
|
||||
stageSize: { width, height },
|
||||
videoSize: { width: croppedVideoWidth, height: croppedVideoHeight },
|
||||
baseScale: scale,
|
||||
baseOffset: { x: spriteX, y: spriteY },
|
||||
videoSize: { width: videoWidth * crop.width, height: videoHeight * crop.height },
|
||||
baseScale: layout.scale,
|
||||
baseOffset: { x: layout.spriteX, y: layout.spriteY },
|
||||
maskRect: {
|
||||
x: maskX,
|
||||
y: maskY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
sourceCrop: crop,
|
||||
},
|
||||
cropBounds: { startX: cropStartX, endX: cropEndX, startY: cropStartY, endY: cropEndY },
|
||||
cropBounds: {
|
||||
startX: layout.cropStartX,
|
||||
endX: layout.cropStartX + videoWidth * crop.width,
|
||||
startY: layout.cropStartY,
|
||||
endY: layout.cropStartY + videoHeight * crop.height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
"radius": "Radius",
|
||||
"roundness": "Roundness",
|
||||
"padding": "Padding",
|
||||
"paddingLinked": "Linked (Uniform)",
|
||||
"paddingUnlinked": "Unlinked (Asymmetrical)",
|
||||
"paddingTop": "Top",
|
||||
"paddingBottom": "Bottom",
|
||||
"paddingLeft": "Left",
|
||||
"paddingRight": "Right",
|
||||
"removeBackground": "Remove background"
|
||||
},
|
||||
"sections": {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CropRegion,
|
||||
CursorStyle,
|
||||
CursorTelemetryPoint,
|
||||
Padding,
|
||||
SpeedRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomRegion,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
BASE_PREVIEW_WIDTH,
|
||||
ZOOM_DEPTH_SCALES,
|
||||
} from "@/components/video-editor/types";
|
||||
import { computePaddedLayout } from "@/components/video-editor/videoPlayback/layoutUtils";
|
||||
import { DEFAULT_FOCUS } from "@/components/video-editor/videoPlayback/constants";
|
||||
import {
|
||||
type CursorFollowCameraState,
|
||||
@@ -88,7 +90,7 @@ interface FrameRenderConfig {
|
||||
zoomOutEasing?: ZoomTransitionEasing;
|
||||
connectedZoomEasing?: ZoomTransitionEasing;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
padding?: Padding | number;
|
||||
cropRegion: CropRegion;
|
||||
webcam?: WebcamOverlaySettings;
|
||||
webcamUrl?: string | null;
|
||||
@@ -1179,55 +1181,20 @@ export class FrameRenderer {
|
||||
private updateLayout(): void {
|
||||
if (!this.app || !this.videoSprite || !this.maskGraphics || !this.videoContainer) return;
|
||||
|
||||
const { width, height } = this.config;
|
||||
const { cropRegion, borderRadius = 0, padding = 0 } = this.config;
|
||||
const videoWidth = this.config.videoWidth;
|
||||
const videoHeight = this.config.videoHeight;
|
||||
const { width, height, cropRegion, borderRadius = 0, padding = 0, videoWidth, videoHeight } = this.config;
|
||||
|
||||
// Calculate cropped video dimensions
|
||||
const cropStartX = cropRegion.x;
|
||||
const cropStartY = cropRegion.y;
|
||||
const cropEndX = cropRegion.x + cropRegion.width;
|
||||
const cropEndY = cropRegion.y + cropRegion.height;
|
||||
const layout = computePaddedLayout({
|
||||
width,
|
||||
height,
|
||||
padding,
|
||||
frameInsets: this.frameInsets,
|
||||
cropRegion,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
});
|
||||
|
||||
const croppedVideoWidth = videoWidth * (cropEndX - cropStartX);
|
||||
const croppedVideoHeight = videoHeight * (cropEndY - cropStartY);
|
||||
|
||||
const paddingScale = 1.0 - (padding / 100) * 0.4;
|
||||
const viewportWidth = width * paddingScale;
|
||||
const viewportHeight = height * paddingScale;
|
||||
|
||||
// When a device frame is active, scale to fit the ENTIRE frame (video + bezels)
|
||||
const insets = this.frameInsets;
|
||||
const screenFracW = insets ? 1 - insets.left - insets.right : 1;
|
||||
const screenFracH = insets ? 1 - insets.top - insets.bottom : 1;
|
||||
const fullFrameVideoW = croppedVideoWidth / screenFracW;
|
||||
const fullFrameVideoH = croppedVideoHeight / screenFracH;
|
||||
|
||||
const scale = Math.min(viewportWidth / fullFrameVideoW, viewportHeight / fullFrameVideoH);
|
||||
|
||||
this.videoSprite.scale.set(scale);
|
||||
|
||||
const fullVideoDisplayWidth = videoWidth * scale;
|
||||
const fullVideoDisplayHeight = videoHeight * scale;
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
|
||||
// Center the full frame (video + bezels) in the output canvas
|
||||
const fullFrameDisplayW = fullFrameVideoW * scale;
|
||||
const fullFrameDisplayH = fullFrameVideoH * scale;
|
||||
const frameCenterX = (width - fullFrameDisplayW) / 2;
|
||||
const frameCenterY = (height - fullFrameDisplayH) / 2;
|
||||
const centerOffsetX = insets
|
||||
? frameCenterX + insets.left * fullFrameDisplayW
|
||||
: (width - croppedDisplayWidth) / 2;
|
||||
const centerOffsetY = insets
|
||||
? frameCenterY + insets.top * fullFrameDisplayH
|
||||
: (height - croppedDisplayHeight) / 2;
|
||||
|
||||
const spriteX = centerOffsetX - cropRegion.x * fullVideoDisplayWidth;
|
||||
const spriteY = centerOffsetY - cropRegion.y * fullVideoDisplayHeight;
|
||||
this.videoSprite.position.set(spriteX, spriteY);
|
||||
this.videoSprite.scale.set(layout.scale);
|
||||
this.videoSprite.position.set(layout.spriteX, layout.spriteY);
|
||||
|
||||
this.videoContainer.position.set(0, 0);
|
||||
|
||||
@@ -1240,10 +1207,10 @@ export class FrameRenderer {
|
||||
|
||||
this.maskGraphics.clear();
|
||||
drawSquircleOnGraphics(this.maskGraphics, {
|
||||
x: centerOffsetX,
|
||||
y: centerOffsetY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
radius: scaledBorderRadius,
|
||||
});
|
||||
this.maskGraphics.fill({ color: 0xffffff });
|
||||
@@ -1251,14 +1218,17 @@ export class FrameRenderer {
|
||||
// Cache layout info
|
||||
this.layoutCache = {
|
||||
stageSize: { width, height },
|
||||
videoSize: { width: croppedVideoWidth, height: croppedVideoHeight },
|
||||
baseScale: scale,
|
||||
baseOffset: { x: spriteX, y: spriteY },
|
||||
videoSize: {
|
||||
width: videoWidth * cropRegion.width,
|
||||
height: videoHeight * cropRegion.height,
|
||||
},
|
||||
baseScale: layout.scale,
|
||||
baseOffset: { x: layout.spriteX, y: layout.spriteY },
|
||||
maskRect: {
|
||||
x: centerOffsetX,
|
||||
y: centerOffsetY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
sourceCrop: cropRegion,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_WEBCAM_OVERLAY } from "@/components/video-editor/types";
|
||||
import { DEFAULT_WEBCAM_OVERLAY } from "../../components/video-editor/types";
|
||||
|
||||
vi.mock("pixi.js", () => ({
|
||||
Application: class {},
|
||||
@@ -94,6 +94,7 @@ function createMockContext() {
|
||||
drawImage: vi.fn(),
|
||||
save: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
getImageData: vi.fn(() => ({ data: new Uint8ClampedArray(0) })),
|
||||
globalAlpha: 1,
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: "high",
|
||||
@@ -182,6 +183,6 @@ describe("ModernFrameRenderer blur export path", () => {
|
||||
|
||||
expect(renderAnnotations).toHaveBeenCalledTimes(1);
|
||||
expect(renderer.getCanvas()).not.toBe(sourceCanvas);
|
||||
expect(renderer.capturePixelsForNativeExport()).toBeNull();
|
||||
expect(renderer.capturePixelsForNativeExport()).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,12 +17,14 @@ import type {
|
||||
CropRegion,
|
||||
CursorStyle,
|
||||
CursorTelemetryPoint,
|
||||
Padding,
|
||||
SpeedRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
import { getDefaultCaptionFontFamily, ZOOM_DEPTH_SCALES } from "@/components/video-editor/types";
|
||||
import { computePaddedLayout } from "@/components/video-editor/videoPlayback/layoutUtils";
|
||||
import { DEFAULT_FOCUS } from "@/components/video-editor/videoPlayback/constants";
|
||||
import {
|
||||
type CursorFollowCameraState,
|
||||
@@ -104,7 +106,7 @@ interface FrameRenderConfig {
|
||||
zoomOutEasing?: ZoomTransitionEasing;
|
||||
connectedZoomEasing?: ZoomTransitionEasing;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
padding?: Padding | number;
|
||||
cropRegion: CropRegion;
|
||||
webcam?: WebcamOverlaySettings;
|
||||
webcamUrl?: string | null;
|
||||
@@ -128,6 +130,7 @@ interface FrameRenderConfig {
|
||||
zoomSmoothness?: number;
|
||||
zoomClassicMode?: boolean;
|
||||
frame?: string | null;
|
||||
nativeReadbackMode?: "pixels" | "canvas";
|
||||
}
|
||||
|
||||
interface AnimationState {
|
||||
@@ -2434,43 +2437,29 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
private updateLayout(): void {
|
||||
if (!this.videoSprite || !this.videoMaskGraphics || !this.videoContainer) {
|
||||
return;
|
||||
}
|
||||
if (!this.app || !this.videoSprite || !this.videoMaskGraphics) return;
|
||||
|
||||
const { width, height } = this.config;
|
||||
const { cropRegion, borderRadius = 0, padding = 0 } = this.config;
|
||||
const videoWidth = this.config.videoWidth;
|
||||
const videoHeight = this.config.videoHeight;
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
cropRegion,
|
||||
borderRadius = 0,
|
||||
padding = 0,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
} = this.config;
|
||||
|
||||
const cropStartX = cropRegion.x;
|
||||
const cropStartY = cropRegion.y;
|
||||
const cropEndX = cropRegion.x + cropRegion.width;
|
||||
const cropEndY = cropRegion.y + cropRegion.height;
|
||||
const layout = computePaddedLayout({
|
||||
width,
|
||||
height,
|
||||
padding,
|
||||
cropRegion,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
});
|
||||
|
||||
const croppedVideoWidth = videoWidth * (cropEndX - cropStartX);
|
||||
const croppedVideoHeight = videoHeight * (cropEndY - cropStartY);
|
||||
|
||||
const paddingScale = 1.0 - (padding / 100) * 0.4;
|
||||
const viewportWidth = width * paddingScale;
|
||||
const viewportHeight = height * paddingScale;
|
||||
const scale = Math.min(
|
||||
viewportWidth / croppedVideoWidth,
|
||||
viewportHeight / croppedVideoHeight,
|
||||
);
|
||||
|
||||
this.videoSprite.scale.set(scale);
|
||||
|
||||
const fullVideoDisplayWidth = videoWidth * scale;
|
||||
const fullVideoDisplayHeight = videoHeight * scale;
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
const centerOffsetX = (width - croppedDisplayWidth) / 2;
|
||||
const centerOffsetY = (height - croppedDisplayHeight) / 2;
|
||||
|
||||
const spriteX = centerOffsetX - cropRegion.x * fullVideoDisplayWidth;
|
||||
const spriteY = centerOffsetY - cropRegion.y * fullVideoDisplayHeight;
|
||||
this.videoSprite.position.set(spriteX, spriteY);
|
||||
this.videoSprite.scale.set(layout.scale);
|
||||
this.videoSprite.position.set(layout.spriteX, layout.spriteY);
|
||||
|
||||
const previewWidth = this.config.previewWidth || 1920;
|
||||
const previewHeight = this.config.previewHeight || 1080;
|
||||
@@ -2479,32 +2468,35 @@ export class FrameRenderer {
|
||||
|
||||
this.videoMaskGraphics.clear();
|
||||
drawSquircleOnGraphics(this.videoMaskGraphics, {
|
||||
x: centerOffsetX,
|
||||
y: centerOffsetY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
radius: scaledBorderRadius,
|
||||
});
|
||||
this.videoMaskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
this.updateVideoShadowLayout({
|
||||
maskX: centerOffsetX,
|
||||
maskY: centerOffsetY,
|
||||
maskWidth: croppedDisplayWidth,
|
||||
maskHeight: croppedDisplayHeight,
|
||||
maskX: layout.centerOffsetX,
|
||||
maskY: layout.centerOffsetY,
|
||||
maskWidth: layout.croppedDisplayWidth,
|
||||
maskHeight: layout.croppedDisplayHeight,
|
||||
maskRadius: scaledBorderRadius,
|
||||
});
|
||||
|
||||
this.layoutCache = {
|
||||
stageSize: { width, height },
|
||||
videoSize: { width: croppedVideoWidth, height: croppedVideoHeight },
|
||||
baseScale: scale,
|
||||
baseOffset: { x: spriteX, y: spriteY },
|
||||
videoSize: {
|
||||
width: videoWidth * cropRegion.width,
|
||||
height: videoHeight * cropRegion.height,
|
||||
},
|
||||
baseScale: layout.scale,
|
||||
baseOffset: { x: layout.spriteX, y: layout.spriteY },
|
||||
maskRect: {
|
||||
x: centerOffsetX,
|
||||
y: centerOffsetY,
|
||||
width: croppedDisplayWidth,
|
||||
height: croppedDisplayHeight,
|
||||
x: layout.centerOffsetX,
|
||||
y: layout.centerOffsetY,
|
||||
width: layout.croppedDisplayWidth,
|
||||
height: layout.croppedDisplayHeight,
|
||||
sourceCrop: cropRegion,
|
||||
},
|
||||
};
|
||||
@@ -2707,6 +2699,28 @@ export class FrameRenderer {
|
||||
return this.outputCanvasOverride ?? (this.app.canvas as HTMLCanvasElement);
|
||||
}
|
||||
|
||||
capturePixelsForNativeExport(): Uint8ClampedArray | null {
|
||||
if (!this.app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const finalCanvas =
|
||||
this.outputCanvasOverride ??
|
||||
(this.shouldCompositeExtensionFrame() ? this.compositeCanvas : null);
|
||||
|
||||
if (finalCanvas) {
|
||||
const context = finalCanvas.getContext("2d");
|
||||
return context
|
||||
? context.getImageData(0, 0, finalCanvas.width, finalCanvas.height).data
|
||||
: null;
|
||||
}
|
||||
|
||||
const result = this.app.renderer.extract.pixels(this.app.stage);
|
||||
const pixels = result.pixels;
|
||||
|
||||
return pixels instanceof Uint8ClampedArray ? pixels : new Uint8ClampedArray(pixels);
|
||||
}
|
||||
|
||||
getRendererBackend(): ExportRenderBackend {
|
||||
return this.rendererBackend;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
CropRegion,
|
||||
CursorStyle,
|
||||
CursorTelemetryPoint,
|
||||
Padding,
|
||||
SpeedRegion,
|
||||
TrimRegion,
|
||||
WebcamOverlaySettings,
|
||||
@@ -55,7 +56,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
zoomOutEasing?: ZoomTransitionEasing;
|
||||
connectedZoomEasing?: ZoomTransitionEasing;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
padding?: Padding | number;
|
||||
videoPadding?: number;
|
||||
cropRegion: CropRegion;
|
||||
webcam?: WebcamOverlaySettings;
|
||||
|
||||
@@ -152,7 +152,7 @@ export class ExtensionHost {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
borderRadius: number;
|
||||
padding: number;
|
||||
padding: number | { top: number; right: number; bottom: number; left: number };
|
||||
} | null = null;
|
||||
private _zoomState: { scale: number; focusX: number; focusY: number; progress: number } | null =
|
||||
null;
|
||||
@@ -441,10 +441,33 @@ export class ExtensionHost {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
borderRadius: number;
|
||||
padding: number;
|
||||
padding: number | { top: number; right: number; bottom: number; left: number };
|
||||
} | null,
|
||||
): void {
|
||||
this._videoLayout = layout;
|
||||
if (!layout) {
|
||||
this._videoLayout = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize and deep clone padding to exclude UI-only fields like 'linked'
|
||||
const p = layout.padding;
|
||||
const normalizedPadding =
|
||||
typeof p === "number"
|
||||
? p
|
||||
: {
|
||||
top: Number(p.top) || 0,
|
||||
right: Number(p.right) || 0,
|
||||
bottom: Number(p.bottom) || 0,
|
||||
left: Number(p.left) || 0,
|
||||
};
|
||||
|
||||
this._videoLayout = {
|
||||
maskRect: { ...layout.maskRect },
|
||||
canvasWidth: layout.canvasWidth,
|
||||
canvasHeight: layout.canvasHeight,
|
||||
borderRadius: layout.borderRadius,
|
||||
padding: normalizedPadding,
|
||||
};
|
||||
}
|
||||
|
||||
setZoomState(
|
||||
@@ -841,12 +864,13 @@ export class ExtensionHost {
|
||||
|
||||
getVideoLayout() {
|
||||
if (!host._videoLayout) return null;
|
||||
const p = host._videoLayout.padding;
|
||||
return {
|
||||
maskRect: { ...host._videoLayout.maskRect },
|
||||
canvasWidth: host._videoLayout.canvasWidth,
|
||||
canvasHeight: host._videoLayout.canvasHeight,
|
||||
borderRadius: host._videoLayout.borderRadius,
|
||||
padding: host._videoLayout.padding,
|
||||
padding: typeof p === "number" ? p : { ...p },
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -241,8 +241,8 @@ export interface RenderHookContext {
|
||||
maskRect: { x: number; y: number; width: number; height: number };
|
||||
/** Border radius applied to the video (in canvas pixels) */
|
||||
borderRadius: number;
|
||||
/** Padding around the video (in canvas pixels) */
|
||||
padding: number;
|
||||
/** Padding around the video (in canvas pixels). Can be a number (global) or an object with individual sides. */
|
||||
padding: number | { top: number; right: number; bottom: number; left: number };
|
||||
};
|
||||
/** Current zoom state */
|
||||
zoom?: {
|
||||
@@ -350,7 +350,7 @@ export interface CursorEffectContext {
|
||||
videoLayout?: {
|
||||
maskRect: { x: number; y: number; width: number; height: number };
|
||||
borderRadius: number;
|
||||
padding: number;
|
||||
padding: number | { top: number; right: number; bottom: number; left: number };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -474,7 +474,7 @@ export interface RecordlyExtensionAPI {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
borderRadius: number;
|
||||
padding: number;
|
||||
padding: number | { top: number; right: number; bottom: number; left: number };
|
||||
} | null;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user