mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(studio): let an embedder draw the preview edge to edge with fillBox (#4408)
NLEPreview always drew its picture 8px in from each side on a dark band, so an embedder that sizes the box to the film saw a smaller picture in a black frame. fillBox drops that inset from both the layout and the stage-size math. Default look is unchanged.
This commit is contained in:
@@ -80,6 +80,10 @@ function renderPreview(
|
||||
previewSlots: Array<{ gen: number; role: "live" | "shadow"; url?: string }> = [
|
||||
{ gen: 0, role: "live" },
|
||||
],
|
||||
{
|
||||
box = { width: 800, height: 600 },
|
||||
fillBox,
|
||||
}: { box?: { width: number; height: number }; fillBox?: boolean } = {},
|
||||
) {
|
||||
resizeCallbacks = [];
|
||||
const host = document.createElement("div");
|
||||
@@ -99,6 +103,7 @@ function renderPreview(
|
||||
onShadowError: () => {},
|
||||
setShadowIframeNode: () => {},
|
||||
resetPreviewSlots: () => {},
|
||||
fillBox,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -108,7 +113,7 @@ function renderPreview(
|
||||
expect(viewport).toBeTruthy();
|
||||
expect(stage).toBeTruthy();
|
||||
|
||||
setRect(viewport, { width: 800, height: 600 });
|
||||
setRect(viewport, box);
|
||||
act(() => {
|
||||
for (const fire of resizeCallbacks) fire();
|
||||
});
|
||||
@@ -251,6 +256,19 @@ describe("NLEPreview", () => {
|
||||
view.cleanup();
|
||||
});
|
||||
|
||||
it("insets the picture by default and fills a same-shape box when fillBox is on", () => {
|
||||
const box = { width: 640, height: 360 };
|
||||
const inset = renderPreview(undefined, { box });
|
||||
expect([inset.stage.style.width, inset.stage.style.height]).toEqual(["611.5556px", "344px"]);
|
||||
expect(parseFloat(inset.stage.parentElement!.style.inset)).toBe(8);
|
||||
inset.cleanup();
|
||||
|
||||
const filled = renderPreview(undefined, { box, fillBox: true });
|
||||
expect([filled.stage.style.width, filled.stage.style.height]).toEqual(["640px", "360px"]);
|
||||
expect(parseFloat(filled.stage.parentElement!.style.inset)).toBe(0);
|
||||
filled.cleanup();
|
||||
});
|
||||
|
||||
it("clips a shadow reload so its own loading overlay cannot paint over the live frame", () => {
|
||||
const view = renderPreview([
|
||||
{ gen: 0, role: "live" },
|
||||
|
||||
@@ -36,6 +36,8 @@ interface NLEPreviewProps {
|
||||
onStageRef?: (ref: React.RefObject<HTMLDivElement | null>) => void;
|
||||
/** Reports the authored composition size measured from the loaded preview. */
|
||||
onCompositionSizeChange?: (size: PreviewCompositionSize | null) => void;
|
||||
/** Draws the picture edge to edge in this box, without Studio's inset band. */
|
||||
fillBox?: boolean;
|
||||
}
|
||||
|
||||
export function getPreviewPlayerKey({
|
||||
@@ -50,7 +52,7 @@ export function getPreviewPlayerKey({
|
||||
|
||||
const ZOOM_HUD_TIMEOUT_MS = 1200;
|
||||
const ZOOM_SETTLE_MS = 200;
|
||||
const PREVIEW_STAGE_INSET_PX = 16;
|
||||
const PREVIEW_STAGE_INSET_PX = 8;
|
||||
|
||||
// clip-path as well as visibility: the player's loading overlay sets its own
|
||||
// visibility:visible and would otherwise paint over the live frame.
|
||||
@@ -87,9 +89,10 @@ export function resolvePreviewStageSize(
|
||||
compositionSize: PreviewCompositionSize | null,
|
||||
portrait: boolean | undefined,
|
||||
gutterPx = 0,
|
||||
insetPx = PREVIEW_STAGE_INSET_PX,
|
||||
): { width: number; height: number } {
|
||||
const availableWidth = Math.max(0, viewportWidth - PREVIEW_STAGE_INSET_PX - 2 * gutterPx);
|
||||
const availableHeight = Math.max(0, viewportHeight - PREVIEW_STAGE_INSET_PX - 2 * gutterPx);
|
||||
const availableWidth = Math.max(0, viewportWidth - 2 * (insetPx + gutterPx));
|
||||
const availableHeight = Math.max(0, viewportHeight - 2 * (insetPx + gutterPx));
|
||||
const aspectRatio =
|
||||
compositionSize && compositionSize.width > 0 && compositionSize.height > 0
|
||||
? compositionSize.width / compositionSize.height
|
||||
@@ -130,6 +133,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
suppressLoadingOverlay,
|
||||
onStageRef,
|
||||
onCompositionSizeChange,
|
||||
fillBox,
|
||||
}: NLEPreviewProps) {
|
||||
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
@@ -151,6 +155,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
const reportPreviewFirstFrame = usePreviewFirstFrameTelemetry(previewSlots);
|
||||
const [compositionSize, setCompositionSize] = useState<PreviewCompositionSize | null>(null);
|
||||
const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
|
||||
const insetPx = fillBox ? 0 : PREVIEW_STAGE_INSET_PX;
|
||||
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
|
||||
|
||||
const zoomRef = useRef<PreviewZoomState>(loadInitialZoom());
|
||||
@@ -181,7 +186,14 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
const updateStageSize = () => {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
setStageSize(
|
||||
resolvePreviewStageSize(rect.width, rect.height, compositionSize, portrait, gutterPx),
|
||||
resolvePreviewStageSize(
|
||||
rect.width,
|
||||
rect.height,
|
||||
compositionSize,
|
||||
portrait,
|
||||
gutterPx,
|
||||
insetPx,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -189,7 +201,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
const observer = new ResizeObserver(updateStageSize);
|
||||
observer.observe(viewport);
|
||||
return () => observer.disconnect();
|
||||
}, [compositionSize, portrait, gutterPx]);
|
||||
}, [compositionSize, portrait, gutterPx, insetPx]);
|
||||
|
||||
const onCompositionSizeChangeRef = useRef(onCompositionSizeChange);
|
||||
onCompositionSizeChangeRef.current = onCompositionSizeChange;
|
||||
@@ -467,11 +479,14 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div
|
||||
ref={viewportRef}
|
||||
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-hidden focus:ring-1 focus:ring-studio-accent/40 bg-[var(--studio-preview-bg,var(--color-neutral-950))]"
|
||||
className="relative flex-1 flex items-center justify-center overflow-hidden min-h-0 outline-hidden focus:ring-1 focus:ring-studio-accent/40 bg-[var(--studio-preview-bg,var(--color-neutral-950))]"
|
||||
tabIndex={0}
|
||||
aria-label="Composition preview"
|
||||
>
|
||||
<div className="absolute inset-2 flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
className="absolute flex items-center justify-center pointer-events-none"
|
||||
style={{ inset: insetPx }}
|
||||
>
|
||||
<div
|
||||
ref={stageRef}
|
||||
className="relative shrink-0 pointer-events-auto"
|
||||
|
||||
Reference in New Issue
Block a user