/ proof-of-concept / takes
Sign in
proof-of-concept / takes
takes / shared / layout.ts
86 lines · 3.2 KBCodeBlameHistory
f3253d0Takes: record, organize, and edit explainer videosJeremy Magland 1// Output geometry for a take, in output pixels. Used both by the ffmpeg export and by the
2// browser preview, so that what you see while editing is what gets rendered.
4import type { BackgroundMode, CameraShape, Layout, OutputSettings } from './types.ts';
5import { backgroundOf } from './types.ts';
7export interface Box {
8 x: number;
9 y: number;
10 w: number;
11 h: number;
14export interface Geometry {
15 W: number;
16 H: number;
17 screen: Box | null; // screen is always fit (letterboxed) inside its box
18 camera: Camera | null;
21export interface Camera {
22 box: Box;
23 fit: 'cover' | 'contain';
24 shape: CameraShape;
25 radius: number;
26 // Effective background: 'cutout' (no box, just the person) only applies to picture-in-picture;
27 // elsewhere it falls back to 'color'.
28 background: BackgroundMode;
29 color: string;
30 blurSigma: number; // in output pixels
33export const FPS = 30;
35const even = (n: number) => Math.max(2, Math.round(n / 2) * 2);
37export function outputSize(output: OutputSettings) {
38 const H = output.height;
39 return { W: even((H * 16) / 9), H };
42export function computeGeometry(layout: Layout, output: OutputSettings, hasScreen: boolean, hasCamera: boolean): Geometry {
43 const { W, H } = outputSize(output);
44 const full: Box = { x: 0, y: 0, w: W, h: H };
45 let mode = layout.mode;
46 if (!hasCamera) mode = 'screen-only';
47 else if (!hasScreen) mode = 'camera-only';
49 const bg = backgroundOf(layout);
50 const background: BackgroundMode = bg.mode === 'cutout' && mode !== 'pip' ? 'color' : bg.mode;
51 const camera = (box: Box, fit: 'cover' | 'contain', shape: CameraShape, radius: number): Camera => ({
52 box,
53 fit,
54 shape,
55 radius,
56 background,
57 color: bg.color,
58 blurSigma: Math.round((bg.blur * H) / 1080 * 10) / 10,
59 });
61 if (mode === 'screen-only') return { W, H, screen: full, camera: null };
62 if (mode === 'camera-only') return { W, H, screen: null, camera: camera(full, 'contain', 'rect', 0) };
64 const left = layout.corner === 'tl' || layout.corner === 'bl';
65 if (mode === 'side-by-side') {
66 const cw = even(W * 0.3);
67 const camBox: Box = { x: left ? 0 : W - cw, y: 0, w: cw, h: H };
68 const scrBox: Box = { x: left ? cw : 0, y: 0, w: W - cw, h: H };
69 return { W, H, screen: scrBox, camera: camera(camBox, 'cover', 'rect', 0) };
70 }
72 // picture-in-picture
73 const margin = even(W * 0.025);
74 const cw = even(W * Math.min(0.5, Math.max(0.08, layout.size)));
75 const top = layout.corner === 'tl' || layout.corner === 'tr';
76 if (background === 'cutout') {
77 // The person is cut off at the bottom of the camera frame, so sit that edge on the bottom of the video.
78 const ch = even((cw * 9) / 16);
79 const box: Box = { x: left ? margin : W - margin - cw, y: top ? margin : H - ch, w: cw, h: ch };
80 return { W, H, screen: full, camera: camera(box, 'cover', 'rect', 0) };
81 }
82 const ch = layout.shape === 'circle' ? cw : even((cw * 9) / 16);
83 const box: Box = { x: left ? margin : W - margin - cw, y: top ? margin : H - margin - ch, w: cw, h: ch };
84 const radius = layout.shape === 'circle' ? cw / 2 : layout.shape === 'rounded' ? Math.round(Math.min(cw, ch) * 0.12) : 0;
85 return { W, H, screen: full, camera: camera(box, 'cover', layout.shape, radius) };
↑↓ move↵openescclose