// Composite preview of the timeline, laid out in output pixels and scaled to fit, so it matches the export. import { useEffect, useRef, useState, type CSSProperties } from 'react'; import type { Take, Video } from '../../shared/types.ts'; import { computeGeometry, outputSize, type Box, type Camera } from '../../shared/layout.ts'; import { takeFileUrl } from '../api.ts'; import { usePlayerActiveTake, type TimelinePlayer } from '../lib/player.ts'; const boxStyle = (b: Box): CSSProperties => ({ left: b.x, top: b.y, width: b.w, height: b.h }); export function Stage({ video, player }: { video: Video; player: TimelinePlayer }) { const active = usePlayerActiveTake(player); const outerRef = useRef(null); const [scale, setScale] = useState(0.4); const { W, H } = outputSize(video.output); useEffect(() => { const el = outerRef.current; if (!el) return; const ro = new ResizeObserver(() => setScale(el.clientWidth / W)); ro.observe(el); return () => ro.disconnect(); }, [W]); const takes = video.takes.filter((t) => t.status === 'ready'); return (
player.toggle()}>
{takes.map((t) => { const g = computeGeometry(video.layout, video.output, t.tracks.screen, t.tracks.camera); return (
{t.tracks.screen && (
); })}
{!active &&
Nothing in the timeline yet
}
); } // The camera box: the original camera, or (once background removal has run) the chosen background // with the cutout on top. The camera element stays mounted even when hidden, since it carries the audio. function CameraLayer({ videoId, take, cam, player }: { videoId: string; take: Take; cam: Camera | null; player: TimelinePlayer }) { const camSrc = takeFileUrl(videoId, take.id, 'camera'); const muted = take.audio !== 'camera'; if (!cam) return