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.
- package/README.md +15 -0
- package/bin/init.mjs +72 -0
- package/package.json +18 -0
- package/template/CLAUDE.md +341 -0
- package/template/STYLE.md +165 -0
- package/template/package.json +45 -0
- package/template/remotion.config.ts +7 -0
- package/template/requirements.txt +17 -0
- package/template/scripts/align_script.py +106 -0
- package/template/scripts/assemble.mjs +364 -0
- package/template/scripts/beats.mjs +273 -0
- package/template/scripts/blur_regions.py +40 -0
- package/template/scripts/check_pair.py +90 -0
- package/template/scripts/doctor.mjs +141 -0
- package/template/scripts/find_cuts.py +45 -0
- package/template/scripts/grade.py +202 -0
- package/template/scripts/lib/media.mjs +228 -0
- package/template/scripts/lib/media.py +99 -0
- package/template/scripts/make_cutout.py +68 -0
- package/template/scripts/motion-check.mjs +268 -0
- package/template/scripts/motion-lint.mjs +122 -0
- package/template/scripts/new-video.mjs +49 -0
- package/template/scripts/prep.mjs +287 -0
- package/template/scripts/ramp.mjs +149 -0
- package/template/scripts/refs.mjs +72 -0
- package/template/scripts/refstyle.mjs +177 -0
- package/template/scripts/sounddesign.mjs +332 -0
- package/template/scripts/stills.mjs +64 -0
- package/template/scripts/sync-audio.mjs +134 -0
- package/template/scripts/transcribe.py +103 -0
- package/template/scripts/trim.mjs +374 -0
- package/template/src/Root.tsx +42 -0
- package/template/src/components/BrandBackground.tsx +22 -0
- package/template/src/components/Captions.tsx +141 -0
- package/template/src/components/Cursor.tsx +93 -0
- package/template/src/components/DrawnCircle.tsx +55 -0
- package/template/src/components/FilmOverlay.tsx +139 -0
- package/template/src/components/Hero.tsx +114 -0
- package/template/src/components/HeroTag.tsx +92 -0
- package/template/src/components/HighlightMark.tsx +34 -0
- package/template/src/components/LogoIcon.tsx +45 -0
- package/template/src/components/MacWindow.tsx +94 -0
- package/template/src/components/MaskReveal.tsx +34 -0
- package/template/src/components/MotionBlur.tsx +34 -0
- package/template/src/components/PlatformTile.tsx +153 -0
- package/template/src/components/Presence.tsx +27 -0
- package/template/src/components/RollingNumber.tsx +89 -0
- package/template/src/components/ScreenView.tsx +52 -0
- package/template/src/components/Stage.tsx +103 -0
- package/template/src/components/Transition.tsx +100 -0
- package/template/src/components/VelocityBlur.tsx +53 -0
- package/template/src/compositions/FilmTest.tsx +67 -0
- package/template/src/compositions/PresetTest.tsx +52 -0
- package/template/src/design/color.ts +6 -0
- package/template/src/design/glow.ts +11 -0
- package/template/src/design/logos.json +18 -0
- package/template/src/design/motion.ts +90 -0
- package/template/src/design/overlays.ts +121 -0
- package/template/src/design/presets.ts +65 -0
- package/template/src/design/tokens.ts +200 -0
- package/template/src/index.ts +4 -0
- package/template/src/lib/animate.ts +194 -0
- package/template/src/lib/camera.ts +58 -0
- package/template/src/lib/continuity.ts +86 -0
- package/template/src/lib/project.ts +13 -0
- package/template/src/lib/spine.ts +37 -0
- package/template/src/videos/sample/Sample.tsx +58 -0
- package/template/templates/BRIEF.md +88 -0
- 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
|
+
};
|