creator-editing-studio 1.0.20260930

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +15 -0
  2. package/bin/init.mjs +72 -0
  3. package/package.json +18 -0
  4. package/template/CLAUDE.md +341 -0
  5. package/template/STYLE.md +165 -0
  6. package/template/package.json +45 -0
  7. package/template/remotion.config.ts +7 -0
  8. package/template/requirements.txt +17 -0
  9. package/template/scripts/align_script.py +106 -0
  10. package/template/scripts/assemble.mjs +364 -0
  11. package/template/scripts/beats.mjs +273 -0
  12. package/template/scripts/blur_regions.py +40 -0
  13. package/template/scripts/check_pair.py +90 -0
  14. package/template/scripts/doctor.mjs +141 -0
  15. package/template/scripts/find_cuts.py +45 -0
  16. package/template/scripts/grade.py +202 -0
  17. package/template/scripts/lib/media.mjs +228 -0
  18. package/template/scripts/lib/media.py +99 -0
  19. package/template/scripts/make_cutout.py +68 -0
  20. package/template/scripts/motion-check.mjs +268 -0
  21. package/template/scripts/motion-lint.mjs +122 -0
  22. package/template/scripts/new-video.mjs +49 -0
  23. package/template/scripts/prep.mjs +287 -0
  24. package/template/scripts/ramp.mjs +149 -0
  25. package/template/scripts/refs.mjs +72 -0
  26. package/template/scripts/refstyle.mjs +177 -0
  27. package/template/scripts/sounddesign.mjs +332 -0
  28. package/template/scripts/stills.mjs +64 -0
  29. package/template/scripts/sync-audio.mjs +134 -0
  30. package/template/scripts/transcribe.py +103 -0
  31. package/template/scripts/trim.mjs +374 -0
  32. package/template/src/Root.tsx +42 -0
  33. package/template/src/components/BrandBackground.tsx +22 -0
  34. package/template/src/components/Captions.tsx +141 -0
  35. package/template/src/components/Cursor.tsx +93 -0
  36. package/template/src/components/DrawnCircle.tsx +55 -0
  37. package/template/src/components/FilmOverlay.tsx +139 -0
  38. package/template/src/components/Hero.tsx +114 -0
  39. package/template/src/components/HeroTag.tsx +92 -0
  40. package/template/src/components/HighlightMark.tsx +34 -0
  41. package/template/src/components/LogoIcon.tsx +45 -0
  42. package/template/src/components/MacWindow.tsx +94 -0
  43. package/template/src/components/MaskReveal.tsx +34 -0
  44. package/template/src/components/MotionBlur.tsx +34 -0
  45. package/template/src/components/PlatformTile.tsx +153 -0
  46. package/template/src/components/Presence.tsx +27 -0
  47. package/template/src/components/RollingNumber.tsx +89 -0
  48. package/template/src/components/ScreenView.tsx +52 -0
  49. package/template/src/components/Stage.tsx +103 -0
  50. package/template/src/components/Transition.tsx +100 -0
  51. package/template/src/components/VelocityBlur.tsx +53 -0
  52. package/template/src/compositions/FilmTest.tsx +67 -0
  53. package/template/src/compositions/PresetTest.tsx +52 -0
  54. package/template/src/design/color.ts +6 -0
  55. package/template/src/design/glow.ts +11 -0
  56. package/template/src/design/logos.json +18 -0
  57. package/template/src/design/motion.ts +90 -0
  58. package/template/src/design/overlays.ts +121 -0
  59. package/template/src/design/presets.ts +65 -0
  60. package/template/src/design/tokens.ts +200 -0
  61. package/template/src/index.ts +4 -0
  62. package/template/src/lib/animate.ts +194 -0
  63. package/template/src/lib/camera.ts +58 -0
  64. package/template/src/lib/continuity.ts +86 -0
  65. package/template/src/lib/project.ts +13 -0
  66. package/template/src/lib/spine.ts +37 -0
  67. package/template/src/videos/sample/Sample.tsx +58 -0
  68. package/template/templates/BRIEF.md +88 -0
  69. package/template/tsconfig.json +17 -0
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
3
+ import {EASE} from '../design/motion';
4
+ import {TRANSITION_PRESETS, type TransitionDirection, type TransitionKind} from '../design/presets';
5
+ import {durFrames, perFrame, progress} from '../lib/animate';
6
+ import {VelocityBlur} from './VelocityBlur';
7
+
8
+ /**
9
+ * A named transition between two scenes, built from the motion tokens.
10
+ *
11
+ * <Transition kind="whipPan" at={cut} direction="left" from={<SceneA />} to={<SceneB />} />
12
+ *
13
+ * The outgoing scene is shown before `at`, the incoming after, and the named preset decides
14
+ * how they hand over. Anything that moves fast carries motion blur, so no hard edge ever
15
+ * jumps across the frame unblurred (law 10).
16
+ */
17
+ export const Transition: React.FC<{
18
+ kind: TransitionKind;
19
+ /** Frame the cut lands on. */
20
+ at: number;
21
+ direction?: TransitionDirection;
22
+ from: React.ReactNode;
23
+ to: React.ReactNode;
24
+ }> = ({kind, at, direction = 'left', from, to}) => {
25
+ const frame = useCurrentFrame();
26
+ const {fps, width, height} = useVideoConfig();
27
+ const preset = TRANSITION_PRESETS[kind];
28
+ const dur = durFrames(preset.dur, fps);
29
+
30
+ // The move is centred on the cut: half before, half after.
31
+ const start = at - Math.round(dur / 2);
32
+ const p = progress(frame, start, dur, EASE.inOut);
33
+ const velocity = perFrame((f) => progress(f, start, dur, EASE.inOut), frame);
34
+
35
+ const axis = direction === 'left' || direction === 'right' ? 'x' : 'y';
36
+ const sign = direction === 'left' || direction === 'up' ? -1 : 1;
37
+ const span = (axis === 'x' ? width : height) * preset.travel;
38
+
39
+ const shift = (amount: number) =>
40
+ axis === 'x' ? `translateX(${amount.toFixed(2)}px)` : `translateY(${amount.toFixed(2)}px)`;
41
+
42
+ const before = p < 1;
43
+ const after = p > 0;
44
+
45
+ /* ---------------------------------------------------------------- flash-based presets */
46
+
47
+ if (preset.flash) {
48
+ // colourFlip / dip: a hard cut through a solid frame, no sliding.
49
+ const half = dur / 2;
50
+ const showFlash = frame >= start && frame < start + dur;
51
+ const showTo = frame >= start + half;
52
+ return (
53
+ <AbsoluteFill>
54
+ {showTo ? to : from}
55
+ {showFlash ? <AbsoluteFill style={{backgroundColor: preset.flash}} /> : null}
56
+ </AbsoluteFill>
57
+ );
58
+ }
59
+
60
+ /* ---------------------------------------------------------------- moving presets */
61
+
62
+ // motion-ok: #000 is mask opacity here, not a design colour.
63
+ const maskEdge = `linear-gradient(${axis === 'x' ? (sign < 0 ? 'to left' : 'to right') : sign < 0 ? 'to top' : 'to bottom'}, #000 ${(p * 100).toFixed(1)}%, transparent ${(p * 100 + 12).toFixed(1)}%)`;
64
+
65
+ const outStyle: React.CSSProperties =
66
+ kind === 'maskWipe'
67
+ ? {opacity: 1}
68
+ : kind === 'matchCut'
69
+ ? {transform: `scale(${(1 + (preset.scale! - 1) * p).toFixed(4)})`, opacity: 1 - p}
70
+ : {transform: shift(sign * span * p)};
71
+
72
+ const inStyle: React.CSSProperties =
73
+ kind === 'maskWipe'
74
+ ? {
75
+ // A soft-edged wipe: the leading edge softens instead of cutting hard.
76
+ // motion-ok: #000 here is mask opacity, not a design colour.
77
+ WebkitMaskImage: maskEdge,
78
+ maskImage: maskEdge,
79
+ }
80
+ : kind === 'matchCut'
81
+ ? {transform: `scale(${(1 + (preset.scale! - 1) * (1 - p)).toFixed(4)})`, opacity: p}
82
+ : {transform: shift(-sign * span * (1 - p))};
83
+
84
+ const speed = velocity * span;
85
+ const wrap = (node: React.ReactNode, style: React.CSSProperties) =>
86
+ preset.blur ? (
87
+ <VelocityBlur vx={axis === 'x' ? speed : 0} vy={axis === 'y' ? speed : 0}>
88
+ <AbsoluteFill style={style}>{node}</AbsoluteFill>
89
+ </VelocityBlur>
90
+ ) : (
91
+ <AbsoluteFill style={style}>{node}</AbsoluteFill>
92
+ );
93
+
94
+ return (
95
+ <AbsoluteFill>
96
+ {before ? wrap(from, outStyle) : null}
97
+ {after ? wrap(to, inStyle) : null}
98
+ </AbsoluteFill>
99
+ );
100
+ };
@@ -0,0 +1,53 @@
1
+ import React, {useId, type CSSProperties} from 'react';
2
+ import {BLUR} from '../design/motion';
3
+
4
+ /** Below this Gaussian sigma (px) the blur is invisible, so it is skipped. */
5
+ const MIN_SIGMA = 0.25;
6
+
7
+ /**
8
+ * Directional motion blur sized to how far the element moves this frame, using the
9
+ * same 180-degree shutter as film / After Effects — but analytic, so it costs almost
10
+ * nothing to render. It fades to zero as the element settles.
11
+ *
12
+ * vx / vy: the element's movement in px per frame (use perFrame()).
13
+ * bleed: how far the blur may spread beyond the element, as a fraction of its size.
14
+ * Use 0 on full-frame wrappers that clip their content.
15
+ */
16
+ export const VelocityBlur: React.FC<{
17
+ vx?: number;
18
+ vy?: number;
19
+ bleed?: number;
20
+ as?: 'div' | 'span';
21
+ style?: CSSProperties;
22
+ children: React.ReactNode;
23
+ }> = ({vx = 0, vy = 0, bleed = 0.5, as: Tag = 'div', style, children}) => {
24
+ const id = `vblur${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
25
+ const shutter = BLUR.shutterAngle / 360;
26
+ // A box blur of length L spreads like a Gaussian with sigma ~ 0.29 L.
27
+ const sx = Math.abs(vx) * shutter * 0.29;
28
+ const sy = Math.abs(vy) * shutter * 0.29;
29
+ const active = sx >= MIN_SIGMA || sy >= MIN_SIGMA;
30
+ const filter = [active ? `url(#${id})` : undefined, style?.filter].filter(Boolean).join(' ') || undefined;
31
+
32
+ return (
33
+ <Tag style={{...style, filter}}>
34
+ {active ? (
35
+ <svg width={0} height={0} style={{position: 'absolute'}} aria-hidden>
36
+ <defs>
37
+ <filter
38
+ id={id}
39
+ x={`${-bleed * 100}%`}
40
+ y={`${-bleed * 100}%`}
41
+ width={`${100 + bleed * 200}%`}
42
+ height={`${100 + bleed * 200}%`}
43
+ colorInterpolationFilters="sRGB"
44
+ >
45
+ <feGaussianBlur stdDeviation={`${sx.toFixed(3)} ${sy.toFixed(3)}`} />
46
+ </filter>
47
+ </defs>
48
+ </svg>
49
+ ) : null}
50
+ {children}
51
+ </Tag>
52
+ );
53
+ };
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import {AbsoluteFill, Sequence, useVideoConfig} from 'remotion';
3
+ import {FilmOverlay} from '../components/FilmOverlay';
4
+ import {FILM_PRESETS, type FilmLook} from '../design/overlays';
5
+ import {BACKGROUND, COLOR, FONT, TYPE} from '../design/tokens';
6
+
7
+ /**
8
+ * Every film look, back to back, over the same plate — so they can be judged against
9
+ * each other and against no overlay at all. Reference composition, not a video.
10
+ *
11
+ * The plate deliberately has a bright area (for halation), a flat mid tone (where grain
12
+ * shows worst) and a hard edge (where gate weave would be visible if it were too strong).
13
+ */
14
+
15
+ const Plate: React.FC<{label: string; note: string}> = ({label, note}) => (
16
+ <AbsoluteFill style={{background: BACKGROUND.creamGlow}}>
17
+ <AbsoluteFill style={{backgroundColor: COLOR.surfaceDark, top: '55%'}} />
18
+ <AbsoluteFill style={{justifyContent: 'center', alignItems: 'center', padding: 96, textAlign: 'center'}}>
19
+ <span
20
+ style={{
21
+ fontFamily: FONT.sans,
22
+ fontSize: TYPE.h1.size,
23
+ fontWeight: TYPE.h1.weight,
24
+ letterSpacing: TYPE.h1.tracking,
25
+ color: COLOR.fg,
26
+ }}
27
+ >
28
+ {label}
29
+ </span>
30
+ <span
31
+ style={{
32
+ fontFamily: FONT.sans,
33
+ fontSize: TYPE.caption.size,
34
+ fontWeight: TYPE.caption.weight,
35
+ color: COLOR.fgSecondary,
36
+ marginTop: 24,
37
+ maxWidth: 820,
38
+ lineHeight: TYPE.caption.lineHeight,
39
+ }}
40
+ >
41
+ {note}
42
+ </span>
43
+ </AbsoluteFill>
44
+ </AbsoluteFill>
45
+ );
46
+
47
+ const LOOKS = Object.keys(FILM_PRESETS) as FilmLook[];
48
+ const HOLD_SECONDS = 2;
49
+
50
+ export const FilmTest: React.FC = () => {
51
+ const {fps} = useVideoConfig();
52
+ const hold = Math.round(HOLD_SECONDS * fps);
53
+
54
+ return (
55
+ <AbsoluteFill style={{backgroundColor: COLOR.surfaceAlt}}>
56
+ {LOOKS.map((look, i) => (
57
+ <Sequence key={look} from={i * hold} durationInFrames={hold} layout="none">
58
+ <FilmOverlay look={look} seed={i}>
59
+ <Plate label={look} note={FILM_PRESETS[look].note} />
60
+ </FilmOverlay>
61
+ </Sequence>
62
+ ))}
63
+ </AbsoluteFill>
64
+ );
65
+ };
66
+
67
+ export const FILM_TEST_FRAMES = LOOKS.length * Math.round(HOLD_SECONDS * 30);
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import {AbsoluteFill, Sequence, useVideoConfig} from 'remotion';
3
+ import {Transition} from '../components/Transition';
4
+ import {COLOR, FONT, TYPE} from '../design/tokens';
5
+ import {TRANSITION_PRESETS, type TransitionKind} from '../design/presets';
6
+
7
+ /**
8
+ * Every transition preset, back to back, so they can be judged side by side
9
+ * (and so motion-check sees them). Reference composition — not a video.
10
+ */
11
+
12
+ const Panel: React.FC<{label: string; bg: string; fg: string}> = ({label, bg, fg}) => (
13
+ <AbsoluteFill style={{backgroundColor: bg, justifyContent: 'center', alignItems: 'center'}}>
14
+ <span
15
+ style={{
16
+ fontFamily: FONT.sans,
17
+ fontSize: TYPE.display.size,
18
+ fontWeight: TYPE.display.weight,
19
+ letterSpacing: TYPE.display.tracking,
20
+ color: fg,
21
+ }}
22
+ >
23
+ {label}
24
+ </span>
25
+ </AbsoluteFill>
26
+ );
27
+
28
+ const KINDS = Object.keys(TRANSITION_PRESETS) as TransitionKind[];
29
+ const HOLD_SECONDS = 1.6;
30
+
31
+ export const PresetTest: React.FC = () => {
32
+ const {fps} = useVideoConfig();
33
+ const hold = Math.round(HOLD_SECONDS * fps);
34
+
35
+ return (
36
+ <AbsoluteFill style={{backgroundColor: COLOR.surfaceAlt}}>
37
+ {KINDS.map((kind, i) => (
38
+ <Sequence key={kind} from={i * hold} durationInFrames={hold} layout="none">
39
+ <Transition
40
+ kind={kind}
41
+ at={Math.round(hold / 2)}
42
+ direction={i % 2 ? 'right' : 'left'}
43
+ from={<Panel label={kind} bg={COLOR.surfaceDark} fg={COLOR.accent} />}
44
+ to={<Panel label="→" bg={COLOR.accent} fg={COLOR.onAccent} />}
45
+ />
46
+ </Sequence>
47
+ ))}
48
+ </AbsoluteFill>
49
+ );
50
+ };
51
+
52
+ export const PRESET_TEST_FRAMES = KINDS.length * Math.round(HOLD_SECONDS * 30);
@@ -0,0 +1,6 @@
1
+ /** `#rrggbb` + alpha (0-1) -> `rgba()` string, for fading token colours. */
2
+ export const withAlpha = (hex: string, alpha: number): string => {
3
+ const n = parseInt(hex.replace('#', ''), 16);
4
+ const a = Math.min(1, Math.max(0, alpha));
5
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a.toFixed(4)})`;
6
+ };
@@ -0,0 +1,11 @@
1
+ import {HERO} from './tokens';
2
+
3
+ /** Accent glow for hero text; `strength` 0-1 fades it (e.g. when the text shrinks into a pill). */
4
+ export const heroGlow = (strength = 1): string => {
5
+ const s = Math.max(0, Math.min(1, strength));
6
+ return [
7
+ `0 0 26px rgba(${HERO.glowRgb},${(0.55 * s).toFixed(3)})`,
8
+ `0 0 90px rgba(${HERO.glowRgb},${(0.35 * s).toFixed(3)})`,
9
+ `0 8px 30px rgba(${HERO.liftRgb},${(0.3 * s).toFixed(3)})`,
10
+ ].join(', ');
11
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "width": 256,
3
+ "height": 256,
4
+ "icons": {
5
+ "google-icon": {
6
+ "body": "<path fill=\"#4285f4\" d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"/><path fill=\"#34a853\" d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"/><path fill=\"#fbbc05\" d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"/><path fill=\"#eb4335\" d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"/>",
7
+ "height": 262
8
+ },
9
+ "meta-icon": {
10
+ "body": "<defs><linearGradient id=\"SVGYeLhubCc\" x1=\"13.878%\" x2=\"89.144%\" y1=\"55.934%\" y2=\"58.694%\"><stop offset=\"0%\" stop-color=\"#0064e1\"/><stop offset=\"40%\" stop-color=\"#0064e1\"/><stop offset=\"83%\" stop-color=\"#0073ee\"/><stop offset=\"100%\" stop-color=\"#0082fb\"/></linearGradient><linearGradient id=\"SVGll66Sdsg\" x1=\"54.315%\" x2=\"54.315%\" y1=\"82.782%\" y2=\"39.307%\"><stop offset=\"0%\" stop-color=\"#0082fb\"/><stop offset=\"100%\" stop-color=\"#0064e0\"/></linearGradient></defs><path fill=\"#0081fb\" d=\"M27.651 112.136c0 9.775 2.146 17.28 4.95 21.82c3.677 5.947 9.16 8.466 14.751 8.466c7.211 0 13.808-1.79 26.52-19.372c10.185-14.092 22.186-33.874 30.26-46.275l13.675-21.01c9.499-14.591 20.493-30.811 33.1-41.806C161.196 4.985 172.298 0 183.47 0c18.758 0 36.625 10.87 50.3 31.257C248.735 53.584 256 81.707 256 110.729c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363v-27.616c15.695 0 19.612-14.422 19.612-30.927c0-23.52-5.484-49.623-17.564-68.273c-8.574-13.23-19.684-21.313-31.907-21.313c-13.22 0-23.859 9.97-35.815 27.75c-6.356 9.445-12.882 20.956-20.208 33.944l-8.066 14.289c-16.203 28.728-20.307 35.271-28.408 46.07c-14.2 18.91-26.324 26.076-42.287 26.076c-18.935 0-30.91-8.2-38.325-20.556C2.973 139.413 0 126.202 0 111.148z\"/><path fill=\"url(#SVGYeLhubCc)\" d=\"M21.802 33.206C34.48 13.666 52.774 0 73.757 0C85.91 0 97.99 3.597 110.605 13.897c13.798 11.261 28.505 29.805 46.853 60.368l6.58 10.967c15.881 26.459 24.917 40.07 30.205 46.49c6.802 8.243 11.565 10.7 17.752 10.7c15.695 0 19.612-14.422 19.612-30.927l24.393-.766c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363c-11.395 0-21.49-2.475-32.654-13.007c-8.582-8.083-18.615-22.443-26.334-35.352l-22.96-38.352C118.528 64.08 107.96 49.73 101.845 43.23c-6.578-6.988-15.036-15.428-28.532-15.428c-10.923 0-20.2 7.666-27.963 19.39z\"/><path fill=\"url(#SVGll66Sdsg)\" d=\"M73.312 27.802c-10.923 0-20.2 7.666-27.963 19.39c-10.976 16.568-17.698 41.245-17.698 64.944c0 9.775 2.146 17.28 4.95 21.82L9.027 149.482C2.973 139.413 0 126.202 0 111.148C0 83.772 7.514 55.24 21.802 33.206C34.48 13.666 52.774 0 73.757 0z\"/>",
11
+ "height": 171
12
+ },
13
+ "shopify": {
14
+ "body": "<path fill=\"#95bf46\" d=\"M223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357a19614 19614 0 0 0-23.383-1.743s-15.507-15.395-17.209-17.099c-1.703-1.703-5.029-1.185-6.32-.805c-.19.056-3.388 1.043-8.678 2.68c-5.18-14.906-14.322-28.604-30.405-28.604c-.444 0-.901.018-1.358.044C129.31 3.407 123.644.779 118.75.779c-37.465 0-55.364 46.835-60.976 70.635c-14.558 4.511-24.9 7.718-26.221 8.133c-8.126 2.549-8.383 2.805-9.45 10.462C21.3 95.806.038 260.235.038 260.235l165.678 31.042l89.77-19.42S223.973 58.8 223.775 57.34M156.49 40.848l-14.019 4.339c.005-.988.01-1.96.01-3.023c0-9.264-1.286-16.723-3.349-22.636c8.287 1.04 13.806 10.469 17.358 21.32m-27.638-19.483c2.304 5.773 3.802 14.058 3.802 25.238c0 .572-.005 1.095-.01 1.624c-9.117 2.824-19.024 5.89-28.953 8.966c5.575-21.516 16.025-31.908 25.161-35.828m-11.131-10.537c1.617 0 3.246.549 4.805 1.622c-12.007 5.65-24.877 19.88-30.312 48.297l-22.886 7.088C75.694 46.16 90.81 10.828 117.72 10.828\"/><path fill=\"#5e8e3e\" d=\"M221.237 54.983a19614 19614 0 0 0-23.383-1.743s-15.507-15.395-17.209-17.099c-.637-.634-1.496-.959-2.394-1.099l-12.527 256.233l89.762-19.418S223.972 58.8 223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357\"/><path fill=\"#fff\" d=\"m135.242 104.585l-11.069 32.926s-9.698-5.176-21.586-5.176c-17.428 0-18.305 10.937-18.305 13.693c0 15.038 39.2 20.8 39.2 56.024c0 27.713-17.577 45.558-41.277 45.558c-28.44 0-42.984-17.7-42.984-17.7l7.615-25.16s14.95 12.835 27.565 12.835c8.243 0 11.596-6.49 11.596-11.232c0-19.616-32.16-20.491-32.16-52.724c0-27.129 19.472-53.382 58.778-53.382c15.145 0 22.627 4.338 22.627 4.338\"/>",
15
+ "height": 292
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,90 @@
1
+ import {Easing} from 'remotion';
2
+
3
+ /**
4
+ * MOTION TOKENS
5
+ * The ONLY file where timing, curves and springs are defined.
6
+ * Durations are milliseconds so the system works at 30 or 60 fps —
7
+ * convert with msToFrames()/durFrames() from src/lib/animate.ts.
8
+ */
9
+
10
+ export const DUR = {
11
+ instant: 133, // micro state changes
12
+ fast: 267, // small elements
13
+ base: 400, // standard element entrance
14
+ slow: 600, // hero elements, long travel, line draws
15
+ scene: 800, // camera moves, scene handoffs
16
+ drift: 2400, // slow push-ins on the footage
17
+ } as const;
18
+ export type Dur = keyof typeof DUR;
19
+
20
+ /** Raw cubic-bezier control points (also usable for After Effects handoff). */
21
+ export const CURVE = {
22
+ out: [0.16, 1, 0.3, 1], // entrances — brand --ease-out-expo
23
+ outDecisive: [0.22, 1, 0.36, 1], // slightly softer landing than expo
24
+ outSoft: [0.33, 1, 0.68, 1], // gentle entrances, opacity
25
+ in: [0.64, 0, 0.78, 0], // exits
26
+ inOut: [0.65, 0, 0.35, 1], // A-to-B moves while on screen
27
+ emphasis: [0.2, 0, 0, 1], // hero moments
28
+ overshoot: [0.34, 1.56, 0.64, 1], // brand --ease-spring; scale only, sparingly
29
+ } as const;
30
+ export type Curve = keyof typeof CURVE;
31
+
32
+ export const EASE: Record<Curve, (t: number) => number> = {
33
+ out: Easing.bezier(...CURVE.out),
34
+ outDecisive: Easing.bezier(...CURVE.outDecisive),
35
+ outSoft: Easing.bezier(...CURVE.outSoft),
36
+ in: Easing.bezier(...CURVE.in),
37
+ inOut: Easing.bezier(...CURVE.inOut),
38
+ emphasis: Easing.bezier(...CURVE.emphasis),
39
+ overshoot: Easing.bezier(...CURVE.overshoot),
40
+ };
41
+
42
+ export const SPRING = {
43
+ snap: {damping: 200, mass: 0.5, stiffness: 200}, // settles, no bounce
44
+ soft: {damping: 26, mass: 1, stiffness: 120}, // slight overshoot
45
+ } as const;
46
+ export type SpringKind = keyof typeof SPRING;
47
+
48
+ /** Delay between items in a group. */
49
+ export const STAGGER = {
50
+ tight: 67, // ~2 frames @30fps
51
+ normal: 100, // ~3 frames @30fps
52
+ dramatic: 167, // ~5 frames @30fps
53
+ } as const;
54
+ export type StaggerKind = keyof typeof STAGGER;
55
+
56
+ /** Exits run at this fraction of the matching entrance duration (law 3). */
57
+ export const EXIT_RATIO = 0.65;
58
+
59
+ /** Opacity completes at this fraction of the transform duration (law 5). */
60
+ export const OPACITY_LEAD = 0.6;
61
+
62
+ export const LIMITS = {
63
+ minScaleFrom: 0.9, // never scale from below this (law 6)
64
+ maxBlurPx: 12, // blur-in cap (law 7)
65
+ defaultTravelPx: 16, // default entrance offset
66
+ maxTravelPx: 48, // element entrances, not camera moves
67
+ referenceTravelPx: 200, // distance that maps to DUR.base (law 9)
68
+ motionBlurFrameWidthShare: 0.15, // needs motion blur above this travel within DUR.fast (law 10)
69
+ } as const;
70
+
71
+ /** Minimum on-screen time for readable text (law 11). */
72
+ export const READ = {
73
+ minHoldMs: 900,
74
+ baseMs: 250,
75
+ perWordMs: 55,
76
+ } as const;
77
+
78
+ /** Film-style motion blur for camera moves and fast elements (law 10). */
79
+ export const BLUR = {
80
+ shutterAngle: 180, // classic film look: blur spans half the frame interval
81
+ samples: 10, // render cost multiplier while active
82
+ } as const;
83
+
84
+ /** Scene handoffs overlap, never gap (continuity rules). */
85
+ export const TRANSITION = {
86
+ minOverlapMs: 133,
87
+ idealOverlapMs: 200,
88
+ sfxLeadMs: 133, // whoosh starts this early
89
+ sfxTailMs: 267, // and tails this long after the change
90
+ } as const;
@@ -0,0 +1,121 @@
1
+ import {PALETTE} from './tokens';
2
+
3
+ /**
4
+ * FILM OVERLAYS — grain, halation, light leaks, gate weave, letterbox.
5
+ *
6
+ * These exist to add the texture a camera and a lens would have added, not to put a
7
+ * filter on top of a video. Every strength is a token, and the defaults are deliberately
8
+ * low: the test is that someone notices the footage looks richer, never that it looks
9
+ * "filtered". If a viewer can name the effect, it is too strong.
10
+ *
11
+ * Add a look here, add its name to templates/BRIEF.md, and the user can ask for it by name.
12
+ */
13
+
14
+ export type OverlayStrength = 'off' | 'subtle' | 'medium' | 'strong';
15
+
16
+ const SCALE: Record<OverlayStrength, number> = {off: 0, subtle: 0.5, medium: 1, strong: 1.6};
17
+ export const strengthScale = (s: OverlayStrength): number => SCALE[s];
18
+
19
+ /**
20
+ * Grain lives at low opacity and a coarse-ish cell, so it survives platform compression.
21
+ *
22
+ * It composites normally rather than with `overlay`/`soft-light`. Those blend modes fade
23
+ * to nothing at both ends of the range, and this palette is cream and ink with almost no
24
+ * midtones — measured on a flat cream area, overlay grain came out at a standard deviation
25
+ * of 0.000, i.e. the layer did nothing at all. Normal blending costs a barely-perceptible
26
+ * pull toward grey and works the same on any footage.
27
+ */
28
+ export const GRAIN = {
29
+ /**
30
+ * Opacity at 'medium'. Multiplied by the strength scale.
31
+ *
32
+ * Measured at this value: ~0.7 Y stddev on a flat area at 'medium', ~1.0 at 'strong' —
33
+ * enough to stop flat areas banding, which is the job. It is deliberately not louder:
34
+ * normal blending pulls the frame toward mid grey, so grain is paid for in black level
35
+ * (blacks measured 18 → 25 at 'medium'). Turn it up per shot with the `grain` prop if a
36
+ * nostalgia moment is worth the milkier blacks; do not raise this default to get there.
37
+ */
38
+ opacity: 0.045,
39
+ /** feTurbulence base frequency — higher is finer. Fine grain is eaten by the encoder. */
40
+ frequency: 0.72,
41
+ /** The noise plate renders at this fraction of the frame, then scales up. Keeps it cheap. */
42
+ renderScale: 0.28,
43
+ /**
44
+ * New seed every N frames. Real film holds its grain for an exposure, but anything above
45
+ * 1 means the frame is byte-identical to the one before it, which motion-check reports as
46
+ * a STUTTER on every other frame — so every video using a grainy look would fail its own
47
+ * verification gate. At this opacity a fresh seed each frame is invisible anyway.
48
+ */
49
+ framesPerSeed: 1,
50
+ } as const;
51
+
52
+ /**
53
+ * Halation: highlights bleeding into what is next to them, warm, never the whole frame.
54
+ *
55
+ * The bloom plate is a copy of the scene, crushed so that everything but the brightest
56
+ * areas goes to black, then blurred and screened back on. The crush is the whole trick —
57
+ * screen blending with black is a no-op, so the darks stay exactly where they were. Skip
58
+ * it and a cream-heavy frame blooms end to end and the blacks lift to brown, which reads
59
+ * as a filter rather than as a lens.
60
+ */
61
+ export const HALATION = {
62
+ /** Blur radius of the bloom plate, in px at 1080 wide. */
63
+ blurPx: 12,
64
+ /** Crush: contrast pushes the mids down, brightness sets how much highlight survives. */
65
+ contrast: 2.6,
66
+ brightness: 0.72,
67
+ /** Warmth of the bleed — film halation is red/orange, never blue. */
68
+ sepia: 0.45,
69
+ saturate: 1.8,
70
+ /** Opacity at 'medium'. */
71
+ opacity: 0.28,
72
+ } as const;
73
+
74
+ /**
75
+ * A slow wash of light across the frame, as if it crept past the gate. A real leak enters
76
+ * from an edge, so this one is biased to the top and kept small — a glow in the middle of
77
+ * the frame is not a leak, it is a smudge.
78
+ */
79
+ export const LEAK = {
80
+ opacity: 0.1,
81
+ /** How long one drift cycle takes, in seconds. Slow enough that it is never a "move". */
82
+ cycleSeconds: 9,
83
+ warm: 'rgba(255, 186, 120, 1)',
84
+ cool: 'rgba(255, 238, 198, 1)',
85
+ } as const;
86
+
87
+ /** Gate weave: the sub-pixel drift of film through a projector. Continuous, never random. */
88
+ export const WEAVE = {
89
+ /** Maximum translation in px at 'medium'. Anything above ~3px reads as a shake. */
90
+ translatePx: 1.6,
91
+ /** Maximum rotation in degrees. */
92
+ rotateDeg: 0.06,
93
+ /** Two slow frequencies, so the drift never repeats on an obvious loop. */
94
+ hz: [0.19, 0.31] as const,
95
+ } as const;
96
+
97
+ export const LETTERBOX = {
98
+ /** Bar height as a fraction of frame height, per named ratio on a 9:16 frame. */
99
+ ratio: {cinema: 0.11, wide: 0.07},
100
+ color: PALETTE.ink900,
101
+ } as const;
102
+
103
+ export type FilmLook = 'none' | 'clean' | 'film' | 'super8' | 'cinema';
104
+
105
+ export const FILM_PRESETS: Record<
106
+ FilmLook,
107
+ {
108
+ grain: OverlayStrength;
109
+ halation: OverlayStrength;
110
+ leak: OverlayStrength;
111
+ weave: OverlayStrength;
112
+ letterbox: keyof typeof LETTERBOX.ratio | 'off';
113
+ note: string;
114
+ }
115
+ > = {
116
+ none: {grain: 'off', halation: 'off', leak: 'off', weave: 'off', letterbox: 'off', note: 'Graphics, screen recordings, anything with text as the subject.'},
117
+ clean: {grain: 'subtle', halation: 'off', leak: 'off', weave: 'off', letterbox: 'off', note: 'Default for talking head. Just enough grain to stop flat areas banding.'},
118
+ film: {grain: 'medium', halation: 'subtle', leak: 'off', weave: 'subtle', letterbox: 'off', note: 'B-roll and travel. The one to reach for first.'},
119
+ super8: {grain: 'strong', halation: 'medium', leak: 'medium', weave: 'medium', letterbox: 'off', note: 'Memory / nostalgia moments only. Never a whole video.'},
120
+ cinema: {grain: 'subtle', halation: 'medium', leak: 'off', weave: 'off', letterbox: 'cinema', note: 'A held, wide, quiet shot. Bars plus bloom, nothing that moves.'},
121
+ };
@@ -0,0 +1,65 @@
1
+ import type {Dur} from './motion';
2
+ import {COLOR, RADIUS} from './tokens';
3
+
4
+ /**
5
+ * PRESETS — named looks and transitions, so a brief can say "whip pan here" or
6
+ * "block captions" instead of describing it every time.
7
+ *
8
+ * Every preset is built from the motion tokens, so they all obey the same laws.
9
+ * Add one here, add its name to templates/BRIEF.md, and it becomes something the
10
+ * user can ask for by name.
11
+ */
12
+
13
+ export type TransitionKind =
14
+ /** Fast pan with motion blur — energy, no meaning. Good between unrelated shots. */
15
+ | 'whipPan'
16
+ /** The new scene pushes the old one out. Direction carries the story forward. */
17
+ | 'push'
18
+ /** A soft-edged wipe reveals the next scene. Calm, premium. */
19
+ | 'maskWipe'
20
+ /** A hard cut to a solid accent screen, then to the next scene. A burst, used on a phrase. */
21
+ | 'colourFlip'
22
+ /** Both scenes share a shape or motion; the cut lands inside the movement. */
23
+ | 'matchCut'
24
+ /** Quick dip through the background colour. The quietest way to change subject. */
25
+ | 'dip';
26
+
27
+ export type TransitionDirection = 'left' | 'right' | 'up' | 'down';
28
+
29
+ export const TRANSITION_PRESETS: Record<
30
+ TransitionKind,
31
+ {dur: Dur; travel: number; blur: boolean; scale?: number; flash?: string; note: string}
32
+ > = {
33
+ whipPan: {dur: 'fast', travel: 1.0, blur: true, note: 'Between unrelated shots. Never twice in a row.'},
34
+ push: {dur: 'base', travel: 1.0, blur: true, note: 'Moving forward through a list or a sequence.'},
35
+ maskWipe: {dur: 'slow', travel: 0.35, blur: false, note: 'Calm reveal. Pairs with a soft sound.'},
36
+ colourFlip: {dur: 'instant', travel: 0, blur: false, flash: COLOR.accent, note: 'A burst on a phrase. Two or three, then back to smooth.'},
37
+ matchCut: {dur: 'base', travel: 0.12, blur: true, scale: 1.08, note: 'Only when both shots share a shape or a direction.'},
38
+ dip: {dur: 'fast', travel: 0, blur: false, flash: COLOR.surfaceAlt, note: 'Changing subject without drama.'},
39
+ };
40
+
41
+ export type CaptionVariant =
42
+ /** Crisp white with a soft shadow. The default. */
43
+ | 'clean'
44
+ /** Each word on an accent block. Loud, good on busy footage. */
45
+ | 'block'
46
+ /** The spoken word is accent-coloured, the rest stays white. */
47
+ | 'highlight'
48
+ /** Words arrive with a small scale pop. Use on fast, energetic cuts. */
49
+ | 'pop';
50
+
51
+ export const CAPTION_PRESETS: Record<
52
+ CaptionVariant,
53
+ {activeColor?: string; background?: string; padding?: string; radius?: number; scaleFrom?: number; note: string}
54
+ > = {
55
+ clean: {note: 'Default. Works over almost anything.'},
56
+ block: {
57
+ background: COLOR.accent,
58
+ activeColor: COLOR.fg,
59
+ padding: '0.06em 0.22em',
60
+ radius: RADIUS.sm,
61
+ note: 'Busy or bright footage, where white alone would get lost.',
62
+ },
63
+ highlight: {activeColor: COLOR.accent, note: 'Keeps the eye on the word being said right now.'},
64
+ pop: {scaleFrom: 0.94, note: 'Fast edits. Do not combine with heavy motion behind the text.'},
65
+ };