wizz-method 2.16.0 → 2.17.1

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/THIRD-PARTY-NOTICES.md +6 -0
  2. package/package.json +1 -1
  3. package/skills-registry.yaml +18 -6
  4. package/src/modules/wizz/_shared/model-ladder.md +7 -5
  5. package/src/modules/wizz/_shared/video-pipeline.md +1 -0
  6. package/src/modules/wizz/subagents/codex/wizz-exec-mechanical.toml +2 -2
  7. package/src/modules/wizz/subagents/codex/wizz-exec-review.toml +2 -2
  8. package/src/modules/wizz/subagents/codex/wizz-exec-standard.toml +2 -2
  9. package/src/modules/wizz/subagents/codex/wizz-exec-strong.toml +2 -2
  10. package/src/modules/wizz/subagents/spec.yaml +7 -4
  11. package/src/skills-lib/wizz-router/references/routing-table-flat.md +1 -0
  12. package/src/skills-lib/wizz-video-styles/SKILL.md +66 -0
  13. package/src/skills-lib/wizz-video-styles/assets/audio/CREDITS.md +54 -0
  14. package/src/skills-lib/wizz-video-styles/assets/audio/LIBRARIES.md +31 -0
  15. package/src/skills-lib/wizz-video-styles/assets/audio/kit.json +182 -0
  16. package/src/skills-lib/wizz-video-styles/assets/audio/music/acoustic-lifestyle.mp3 +0 -0
  17. package/src/skills-lib/wizz-video-styles/assets/audio/music/cinematic-tense.mp3 +0 -0
  18. package/src/skills-lib/wizz-video-styles/assets/audio/music/classical-light.mp3 +0 -0
  19. package/src/skills-lib/wizz-video-styles/assets/audio/music/corporate-light.mp3 +0 -0
  20. package/src/skills-lib/wizz-video-styles/assets/audio/music/lofi-chill.mp3 +0 -0
  21. package/src/skills-lib/wizz-video-styles/assets/audio/music/meme-beat.mp3 +0 -0
  22. package/src/skills-lib/wizz-video-styles/assets/audio/music/piano-editorial.mp3 +0 -0
  23. package/src/skills-lib/wizz-video-styles/assets/audio/music/tech-pulse.mp3 +0 -0
  24. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/boom-impact.mp3 +0 -0
  25. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/click-mouse.mp3 +0 -0
  26. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ding-notify.mp3 +0 -0
  27. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/glitch-short.mp3 +0 -0
  28. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ka-ching.mp3 +0 -0
  29. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/pop-bubble.mp3 +0 -0
  30. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/record-scratch.mp3 +0 -0
  31. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/riser-short.mp3 +0 -0
  32. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/swoosh-text.mp3 +0 -0
  33. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/typing-short.mp3 +0 -0
  34. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-fast.mp3 +0 -0
  35. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-soft.mp3 +0 -0
  36. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/EditorialReel.tsx +111 -0
  37. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/README.md +77 -0
  38. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/captions.tsx +56 -0
  39. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/example-words.json +285 -0
  40. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/primitives.tsx +241 -0
  41. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/scenes.tsx +113 -0
  42. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/theme.ts +68 -0
  43. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/timeline.ts +65 -0
  44. package/src/skills-lib/wizz-video-styles/data/recipes.json +341 -0
  45. package/src/skills-lib/wizz-video-styles/data/references.json +3397 -0
  46. package/src/skills-lib/wizz-video-styles/data/styles.json +726 -0
  47. package/src/skills-lib/wizz-video-styles/references/audio.md +54 -0
  48. package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +38 -0
  49. package/src/skills-lib/wizz-video-styles/references/research-method.md +33 -0
  50. package/src/skills-lib/wizz-video-styles/references/style-picker.md +51 -0
  51. package/src/skills-lib/wizz-video-styles/references/styles/camera-no-setup.md +60 -0
  52. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +60 -0
  53. package/src/skills-lib/wizz-video-styles/references/styles/esquete.md +57 -0
  54. package/src/skills-lib/wizz-video-styles/references/styles/experimento.md +56 -0
  55. package/src/skills-lib/wizz-video-styles/references/styles/explicador-animado.md +62 -0
  56. package/src/skills-lib/wizz-video-styles/references/styles/macro-asmr.md +61 -0
  57. package/src/skills-lib/wizz-video-styles/references/styles/meme-rapido.md +65 -0
  58. package/src/skills-lib/wizz-video-styles/references/styles/motion-editorial.md +69 -0
  59. package/src/skills-lib/wizz-video-styles/references/styles/podcast-clip.md +61 -0
  60. package/src/skills-lib/wizz-video-styles/references/styles/pov-vlog.md +57 -0
  61. package/src/skills-lib/wizz-video-styles/references/styles/quadro-branco.md +60 -0
  62. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-cards.md +62 -0
  63. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-legenda.md +62 -0
  64. package/src/skills-lib/wizz-video-styles/references/styles/tour.md +59 -0
  65. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +66 -0
  66. package/src/skills-lib/wizz-video-styles/scripts/build_catalog.py +128 -0
  67. package/tools/installer/core/project-instructions.js +2 -1
  68. package/tools/installer/core/subagent-models.js +112 -0
  69. package/tools/installer/ide/_config-driven.js +8 -3
@@ -0,0 +1,241 @@
1
+ import React, { useId } from "react";
2
+ import { AbsoluteFill, Easing, Img, interpolate, spring, staticFile, useCurrentFrame } from "remotion";
3
+ import { FPS, H, W, useTheme } from "./theme";
4
+
5
+ // Vocabulário do estilo: fundo claro com grade, cards escuros com sombra funda,
6
+ // serifa itálica bicolor, pílula digitada, conector tracejado, raios, contador.
7
+
8
+ export const clamp = { extrapolateLeft: "clamp", extrapolateRight: "clamp" } as const;
9
+ export const ease = Easing.bezier(0.22, 1, 0.36, 1);
10
+ export const useSeconds = (): number => useCurrentFrame() / FPS;
11
+ export const pop = (frame: number, delay = 0): number =>
12
+ spring({ frame: frame - Math.round(delay * FPS), fps: FPS, config: { damping: 15, mass: 0.7 } });
13
+
14
+ const CX = W / 2;
15
+
16
+ // Envelope de cena: entra subindo com mola, sai subindo e sumindo (sem corte seco).
17
+ export const Stage: React.FC<{ dur: number; children: React.ReactNode }> = ({ dur, children }) => {
18
+ const frame = useCurrentFrame();
19
+ const t = frame / FPS;
20
+ const s = pop(frame);
21
+ const out = interpolate(t, [dur - 0.3, dur], [0, 1], { ...clamp, easing: Easing.in(Easing.cubic) });
22
+ const cam = interpolate(t, [0, dur], [1, 1.035], clamp);
23
+ return (
24
+ <AbsoluteFill
25
+ style={{
26
+ opacity: Math.min(1, s * 1.4) * (1 - out),
27
+ transform: `translateY(${(1 - s) * 70 - out * 60}px) scale(${(0.94 + 0.06 * s) * cam})`,
28
+ }}
29
+ >
30
+ {children}
31
+ </AbsoluteFill>
32
+ );
33
+ };
34
+
35
+ export const Backdrop: React.FC = () => {
36
+ const c = useTheme();
37
+ return (
38
+ <AbsoluteFill
39
+ style={{
40
+ background: c.bg,
41
+ backgroundImage: `linear-gradient(${c.grid} 2px, transparent 2px), linear-gradient(90deg, ${c.grid} 2px, transparent 2px)`,
42
+ backgroundSize: "72px 72px",
43
+ }}
44
+ >
45
+ <AbsoluteFill style={{ background: `radial-gradient(ellipse at 50% 45%, ${c.glow}, rgba(0,0,0,0) 60%, ${c.vignette})` }} />
46
+ </AbsoluteFill>
47
+ );
48
+ };
49
+
50
+ // Título serifado em duas linhas: a primeira na cor do texto, a segunda no destaque.
51
+ export const Headline: React.FC<{ a: string; b: string; top?: number; delay?: number; size?: number }> = ({
52
+ a, b, top = 300, delay = 0, size = 104,
53
+ }) => {
54
+ const c = useTheme();
55
+ const frame = useCurrentFrame();
56
+ const s1 = pop(frame, delay);
57
+ const s2 = pop(frame, delay + 0.18);
58
+ const base = { fontFamily: c.serif, fontWeight: 700, fontSize: size, lineHeight: 1.0, letterSpacing: -2 } as const;
59
+ return (
60
+ <div style={{ position: "absolute", top, left: 0, right: 0, textAlign: "center" }}>
61
+ <div style={{ ...base, color: c.ink, opacity: s1, transform: `translateY(${(1 - s1) * 30}px)` }}>{a}</div>
62
+ <div style={{ ...base, color: c.accent, opacity: s2, transform: `translateY(${(1 - s2) * 30}px)` }}>{b}</div>
63
+ </div>
64
+ );
65
+ };
66
+
67
+ // Raios que "acendem" abaixo de uma pílula (o agente trabalhando). grow: 0..1.
68
+ export const Rays: React.FC<{ grow: number; y: number; size?: number }> = ({ grow, y, size = 300 }) => {
69
+ const c = useTheme();
70
+ const frame = useCurrentFrame();
71
+ const n = 11;
72
+ return (
73
+ <div style={{ position: "absolute", left: CX - size, top: y, width: size * 2, height: size, overflow: "hidden" }}>
74
+ {Array.from({ length: n }, (_, i) => {
75
+ const angle = -80 + (160 / (n - 1)) * i;
76
+ const wobble = Math.sin(frame / 9 + i) * 0.06;
77
+ const len = size * (0.62 + (0.3 * ((i * 7) % 5)) / 4) * grow * (1 + wobble);
78
+ return (
79
+ <div
80
+ key={i}
81
+ style={{
82
+ position: "absolute", left: size - 17, top: 0, width: 34, height: len, borderRadius: 17,
83
+ background: `linear-gradient(${c.accentWarm}, ${c.accentLight})`,
84
+ transformOrigin: "50% 0%", transform: `rotate(${angle}deg)`,
85
+ boxShadow: `0 10px 18px ${c.accentShadow}`,
86
+ }}
87
+ />
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ // Anel girando: objeto-símbolo de abertura.
95
+ export const Ring: React.FC<{ y?: number; size?: number }> = ({ y = 760, size = 420 }) => {
96
+ const c = useTheme();
97
+ const frame = useCurrentFrame();
98
+ const s = pop(frame, 0.1);
99
+ return (
100
+ <div style={{ position: "absolute", left: CX - size / 2, top: y, width: size, height: size, transform: `scale(${s}) rotate(${frame * 0.8}deg)` }}>
101
+ <div style={{ position: "absolute", inset: 0, borderRadius: "50%", border: `${size * 0.186}px solid ${c.accent}`, borderRightColor: "transparent", boxShadow: `0 50px 80px ${c.accentShadow}` }} />
102
+ </div>
103
+ );
104
+ };
105
+
106
+ // Pílula escura com texto digitado (input de prompt). Tempos em segundos da cena.
107
+ export const Pill: React.FC<{
108
+ text: string; typeFrom: number; typeDur: number; y: number; w?: number; icon?: string; serif?: boolean;
109
+ }> = ({ text, typeFrom, typeDur, y, w = 820, icon = "✦", serif = true }) => {
110
+ const c = useTheme();
111
+ const t = useSeconds();
112
+ const n = Math.round(interpolate(t, [typeFrom, typeFrom + typeDur], [0, text.length], clamp));
113
+ const caret = n < text.length && Math.floor(t * 3) % 2 === 0;
114
+ return (
115
+ <div style={{ position: "absolute", left: CX - w / 2, top: y, width: w, height: 150, borderRadius: 40, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, display: "flex", alignItems: "center", padding: "0 44px", gap: 28 }}>
116
+ <div style={{ width: 62, height: 62, borderRadius: 18, background: c.accent, color: c.onAccent, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 34 }}>{icon}</div>
117
+ <div style={{ fontFamily: serif ? c.serif : c.sans, fontWeight: 600, fontSize: 56, color: c.line, whiteSpace: "nowrap" }}>
118
+ {text.slice(0, n)}
119
+ <span style={{ color: c.accentLight, opacity: caret ? 1 : 0 }}>|</span>
120
+ </div>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ // Chip pequeno, centrado em (x, y). Entra com mola em `delay` (s).
126
+ export const Chip: React.FC<{
127
+ label: string; x: number; y: number; delay: number; accent?: boolean; big?: boolean; icon?: string;
128
+ }> = ({ label, x, y, delay, accent, big, icon = "▤" }) => {
129
+ const c = useTheme();
130
+ const s = pop(useCurrentFrame(), delay);
131
+ return (
132
+ <div style={{ position: "absolute", left: x, top: y, transform: `translate(-50%,-50%) scale(${s})`, opacity: s > 0.02 ? 1 : 0, background: accent ? c.accent : `linear-gradient(${c.cardHi}, ${c.card})`, color: accent ? c.onAccent : c.line, borderRadius: 26, padding: big ? "26px 40px" : "20px 32px", boxShadow: c.shadow, display: "flex", alignItems: "center", gap: 18, fontFamily: c.serif, fontWeight: 600, fontSize: big ? 52 : 44, whiteSpace: "nowrap" }}>
133
+ {icon ? <span style={{ fontFamily: c.sans, fontSize: big ? 34 : 28, opacity: 0.8 }}>{icon}</span> : null}
134
+ {label}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ // Conector tracejado que se desenha de (x1,y1) a (x2,y2) a partir de `from` (s).
140
+ export const Dash: React.FC<{ x1: number; y1: number; x2: number; y2: number; from: number; dur?: number }> = ({
141
+ x1, y1, x2, y2, from, dur = 0.45,
142
+ }) => {
143
+ const c = useTheme();
144
+ const t = useSeconds();
145
+ const id = `dash${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
146
+ const p = interpolate(t, [from, from + dur], [0, 1], { ...clamp, easing: ease });
147
+ const len = Math.hypot(x2 - x1, y2 - y1);
148
+ const d = `M ${x1} ${y1} C ${x1} ${(y1 + y2) / 2}, ${(x1 + x2) / 2} ${y2}, ${x2} ${y2}`;
149
+ return (
150
+ <svg style={{ position: "absolute", inset: 0 }} width={W} height={H}>
151
+ <defs>
152
+ <mask id={id}>
153
+ <path d={d} stroke="#fff" strokeWidth={10} fill="none" strokeDasharray={len * 1.5} strokeDashoffset={len * 1.5 * (1 - p)} />
154
+ </mask>
155
+ </defs>
156
+ <path d={d} stroke={c.dash} strokeWidth={4} fill="none" strokeDasharray="10 12" mask={`url(#${id})`} />
157
+ </svg>
158
+ );
159
+ };
160
+
161
+ const resolveSrc = (src: string): string => (/^(https?:|data:)/.test(src) ? src : staticFile(src));
162
+
163
+ // Celular-card com screenshot em proporção real. `ratio` = altura/largura da imagem
164
+ // (iPhone 1206x2622 = 2.174). Sem `src`, desenha uma tela de exemplo (o molde roda sem assets).
165
+ export const PhoneCard: React.FC<{
166
+ src?: string; y?: number; scale?: number; focusY?: number; maxH?: number; ratio?: number;
167
+ }> = ({ src, y = 250, scale = 1, focusY = 0, maxH = 1180, ratio = 2622 / 1206 }) => {
168
+ const c = useTheme();
169
+ const w = 600 * scale;
170
+ const h = w * ratio;
171
+ const viewH = Math.min(h, maxH);
172
+ return (
173
+ <div style={{ position: "absolute", left: CX - w / 2 - 16, top: y, width: w + 32, height: viewH + 32, borderRadius: 74 * scale, background: c.card, padding: 16, boxShadow: c.shadow }}>
174
+ <div style={{ width: w, height: viewH, borderRadius: 60 * scale, overflow: "hidden", background: c.bg }}>
175
+ {src ? (
176
+ <Img src={resolveSrc(src)} style={{ width: w, height: h, transform: `translateY(${-focusY * scale}px)` }} />
177
+ ) : (
178
+ <div style={{ padding: `${60 * scale}px ${40 * scale}px`, display: "flex", flexDirection: "column", gap: 28 * scale }}>
179
+ <div style={{ height: 70 * scale, borderRadius: 20 * scale, background: c.accent }} />
180
+ {Array.from({ length: 6 }, (_, i) => (
181
+ <div key={i} style={{ height: 120 * scale, borderRadius: 24 * scale, background: c.line, opacity: 1 - i * 0.1 }} />
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ );
188
+ };
189
+
190
+ // Card com número que sobe até `to` a partir de `delay` (s).
191
+ export const Counter: React.FC<{ to: number; label: string; y: number; delay: number }> = ({ to, label, y, delay }) => {
192
+ const c = useTheme();
193
+ const frame = useCurrentFrame();
194
+ const s = pop(frame, delay);
195
+ const v = Math.round(interpolate(frame / FPS, [delay, delay + 1.0], [0, to], { ...clamp, easing: Easing.out(Easing.cubic) }));
196
+ return (
197
+ <div style={{ position: "absolute", left: 140, right: 140, top: y, height: 330, borderRadius: 44, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, transform: `scale(${s})`, opacity: s > 0.02 ? 1 : 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
198
+ <div style={{ fontFamily: c.serif, fontWeight: 700, fontSize: 180, color: c.accentLight, lineHeight: 1 }}>{v}</div>
199
+ <div style={{ fontFamily: c.serif, fontWeight: 600, fontSize: 52, color: c.line, marginTop: 6 }}>{label}</div>
200
+ </div>
201
+ );
202
+ };
203
+
204
+ export type DocPage = { lines: string[]; at: number; mono?: boolean }; // `at` em s da cena
205
+ const REVIEW_SPOTS = [
206
+ { x: 175, y: 560 }, { x: 905, y: 700 }, { x: 175, y: 1020 }, { x: 905, y: 1160 },
207
+ ] as const;
208
+
209
+ // Card alto escuro (documento). Cada página troca o conteúdo e digita as linhas;
210
+ // `reviewers` pendura até 4 chips conectados por tracejado ao redor do card.
211
+ export const DocCard: React.FC<{
212
+ pages: DocPage[]; title: string; altTitle?: string; reviewers?: { labels: string[]; at: number };
213
+ }> = ({ pages, title, altTitle, reviewers }) => {
214
+ const c = useTheme();
215
+ const t = useSeconds();
216
+ const page = [...pages].reverse().find((p) => t >= p.at) ?? pages[0];
217
+ const isAlt = page !== pages[0];
218
+ const shown = page.lines.slice(0, Math.round(interpolate(t, [page.at + 0.2, page.at + 1.2], [0, page.lines.length], clamp)));
219
+ return (
220
+ <>
221
+ <div style={{ position: "absolute", left: 220, top: 380, width: 640, height: 980, borderRadius: 48, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, padding: "130px 48px 0", overflow: "hidden" }}>
222
+ {shown.map((l, i) => (
223
+ <div key={`${page.at}-${i}`} style={{ fontFamily: page.mono ? c.mono : c.sans, fontSize: 25, lineHeight: page.mono ? "46px" : "42px", color: i === 0 && !page.mono ? c.accentLight : c.line, whiteSpace: "nowrap", overflow: "hidden", opacity: 0.92 }}>
224
+ {l}
225
+ </div>
226
+ ))}
227
+ </div>
228
+ <Chip label={isAlt && altTitle ? altTitle : title} x={CX} y={380} delay={0.05} accent={isAlt} />
229
+ {(reviewers?.labels ?? []).slice(0, 4).map((label, i) => {
230
+ const spot = REVIEW_SPOTS[i];
231
+ const from = (reviewers?.at ?? 0) + i * 0.22;
232
+ return (
233
+ <React.Fragment key={label}>
234
+ <Dash x1={spot.x < CX ? 230 : 850} y1={spot.y} x2={spot.x} y2={spot.y} from={from} dur={0.2} />
235
+ <Chip label={label} x={spot.x} y={spot.y} delay={from} accent />
236
+ </React.Fragment>
237
+ );
238
+ })}
239
+ </>
240
+ );
241
+ };
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { interpolate } from "remotion";
3
+ import {
4
+ Chip, Counter, Dash, DocCard, Headline, PhoneCard, Pill, Rays, Ring, clamp, ease, useSeconds,
5
+ type DocPage,
6
+ } from "./primitives";
7
+ import { useTheme } from "./theme";
8
+ import type { Anchored } from "./timeline";
9
+
10
+ // Cenas como DADOS: cada objeto do array vira um componente. `at` é a palavra-âncora
11
+ // onde a cena começa; `marks` nomeia palavras internas (ex.: quando o contador sobe).
12
+
13
+ export type SceneDef = Anchored &
14
+ (
15
+ | { kind: "hook"; a: string; b: string }
16
+ | { kind: "prompt"; a: string; b: string; prompt: string; typedMark?: string }
17
+ | { kind: "phone"; src?: string; ratio?: number; scale?: number; y?: number; focusY?: number }
18
+ | { kind: "numbers"; chip: string; counters: { to: number; label: string; mark: string }[] }
19
+ | { kind: "steps"; prompt: string; items: [string, string, string]; typedMark?: string }
20
+ | { kind: "doc"; title: string; altTitle?: string; pages: (Omit<DocPage, "at"> & { mark?: string })[]; reviewers?: { labels: string[]; mark: string } }
21
+ | { kind: "cta"; a: string; b: string; keyword: string; footer: string; hideCaptions?: boolean }
22
+ );
23
+
24
+ export type SceneCtx = { dur: number; mark: (name: string | undefined, fallback?: number) => number };
25
+
26
+ const Hook: React.FC<{ a: string; b: string }> = ({ a, b }) => (
27
+ <>
28
+ <Headline a={a} b={b} />
29
+ <Ring />
30
+ </>
31
+ );
32
+
33
+ const Prompt: React.FC<{ a: string; b: string; prompt: string; typed: number; dur: number }> = ({ a, b, prompt, typed, dur }) => {
34
+ const t = useSeconds();
35
+ return (
36
+ <>
37
+ <Headline a={a} b={b} />
38
+ <Pill text={prompt} typeFrom={0.2} typeDur={Math.min(typed, dur * 0.6)} y={820} />
39
+ <Rays grow={interpolate(t, [0.4, 1.0], [0, 1], { ...clamp, easing: ease })} y={970} />
40
+ </>
41
+ );
42
+ };
43
+
44
+ const Numbers: React.FC<{ chip: string; counters: { to: number; label: string; delay: number }[] }> = ({ chip, counters }) => (
45
+ <>
46
+ <Chip label={chip} x={540} y={330} delay={0.05} accent />
47
+ {counters.slice(0, 2).map((c, i) => (
48
+ <Counter key={c.label} to={c.to} label={c.label} y={470 + i * 390} delay={c.delay} />
49
+ ))}
50
+ </>
51
+ );
52
+
53
+ const Steps: React.FC<{ prompt: string; items: [string, string, string]; typed: number }> = ({ prompt, items, typed }) => {
54
+ const t = useSeconds();
55
+ const spots = [
56
+ { x: 230, y: 1000, from: [300, 760] as const, delay: 0.25 },
57
+ { x: 540, y: 1100, from: [540, 760] as const, delay: 0.55 },
58
+ { x: 850, y: 1000, from: [780, 760] as const, delay: 0.85 },
59
+ ];
60
+ return (
61
+ <>
62
+ <Pill text={prompt} typeFrom={0.1} typeDur={Math.max(0.6, typed - 0.1)} y={330} w={700} />
63
+ <Rays grow={interpolate(t, [0.1, 0.6], [0, 1], { ...clamp, easing: ease })} y={480} size={260} />
64
+ {spots.map((s, i) => (
65
+ <React.Fragment key={items[i]}>
66
+ <Dash x1={s.from[0]} y1={s.from[1]} x2={s.x} y2={s.y - 40} from={s.delay - 0.05} />
67
+ <Chip label={items[i]} x={s.x} y={s.y} delay={s.delay} />
68
+ </React.Fragment>
69
+ ))}
70
+ </>
71
+ );
72
+ };
73
+
74
+ const Cta: React.FC<{ a: string; b: string; keyword: string; footer: string }> = ({ a, b, keyword, footer }) => {
75
+ const c = useTheme();
76
+ const t = useSeconds();
77
+ return (
78
+ <>
79
+ <Headline a={a} b={b} top={360} />
80
+ <Pill text={keyword} typeFrom={0.35} typeDur={0.5} y={760} w={520} />
81
+ <Rays grow={interpolate(t, [0.6, 1.1], [0, 1], { ...clamp, easing: ease })} y={910} />
82
+ <div style={{ position: "absolute", top: 1290, left: 120, right: 120, textAlign: "center", fontFamily: c.serif, fontWeight: 600, fontSize: 54, color: c.ink, lineHeight: 1.2 }}>
83
+ {footer}
84
+ </div>
85
+ </>
86
+ );
87
+ };
88
+
89
+ export const SceneView: React.FC<{ scene: SceneDef; ctx: SceneCtx }> = ({ scene, ctx }) => {
90
+ switch (scene.kind) {
91
+ case "hook":
92
+ return <Hook a={scene.a} b={scene.b} />;
93
+ case "prompt":
94
+ return <Prompt a={scene.a} b={scene.b} prompt={scene.prompt} typed={ctx.mark(scene.typedMark, 1.1)} dur={ctx.dur} />;
95
+ case "phone":
96
+ return <PhoneCard src={scene.src} ratio={scene.ratio} scale={scene.scale ?? 1} y={scene.y ?? 260} focusY={scene.focusY} maxH={1200} />;
97
+ case "numbers":
98
+ return <Numbers chip={scene.chip} counters={scene.counters.map((c) => ({ ...c, delay: ctx.mark(c.mark, 0.3) }))} />;
99
+ case "steps":
100
+ return <Steps prompt={scene.prompt} items={scene.items} typed={ctx.mark(scene.typedMark, 0.8)} />;
101
+ case "doc":
102
+ return (
103
+ <DocCard
104
+ title={scene.title}
105
+ altTitle={scene.altTitle}
106
+ pages={scene.pages.map((p, i) => ({ lines: p.lines, mono: p.mono, at: i === 0 ? 0.2 : ctx.mark(p.mark, 1) }))}
107
+ reviewers={scene.reviewers ? { labels: scene.reviewers.labels, at: ctx.mark(scene.reviewers.mark, 2) } : undefined}
108
+ />
109
+ );
110
+ case "cta":
111
+ return <Cta a={scene.a} b={scene.b} keyword={scene.keyword} footer={scene.footer} />;
112
+ }
113
+ };
@@ -0,0 +1,68 @@
1
+ import { createContext, useContext } from "react";
2
+ import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
3
+ import { loadFont as loadJetBrainsMono } from "@remotion/google-fonts/JetBrainsMono";
4
+ import { loadFont as loadPlayfair } from "@remotion/google-fonts/PlayfairDisplay";
5
+
6
+ // Tema do estilo "motion editorial". Tudo que é cor, fonte e sombra mora aqui;
7
+ // as peças leem via useTheme() e a composição aceita um tema parcial por props.
8
+
9
+ export const W = 1080;
10
+ export const H = 1920;
11
+ export const FPS = 30;
12
+
13
+ const playfair = loadPlayfair("italic", { weights: ["600", "700"], subsets: ["latin"] });
14
+ const inter = loadInter("normal", { weights: ["400", "700", "800"], subsets: ["latin"] });
15
+ const mono = loadJetBrainsMono("normal", { weights: ["500", "700"], subsets: ["latin"] });
16
+
17
+ export const waitForFonts = (): Promise<unknown[]> =>
18
+ Promise.all([playfair.waitUntilDone(), inter.waitUntilDone(), mono.waitUntilDone()]);
19
+
20
+ export type EditorialTheme = {
21
+ bg: string; // fundo claro
22
+ grid: string; // linhas da grade (rgba bem sutil)
23
+ glow: string; // luz central do fundo
24
+ vignette: string; // escurecimento das bordas
25
+ ink: string; // texto principal sobre o fundo
26
+ card: string; // base dos cards escuros
27
+ cardHi: string; // topo do gradiente dos cards
28
+ line: string; // texto claro sobre card escuro
29
+ onAccent: string; // texto sobre a cor de destaque
30
+ accent: string; // cor de destaque (palavra-chave, chips, anel)
31
+ accentWarm: string; // início do gradiente dos raios
32
+ accentLight: string; // fim do gradiente, contadores, cursor
33
+ accentShadow: string; // sombra dos raios e do anel
34
+ dash: string; // conector tracejado
35
+ shadow: string; // sombra funda dos cards
36
+ serif: string;
37
+ sans: string;
38
+ mono: string;
39
+ };
40
+
41
+ export const defaultTheme: EditorialTheme = {
42
+ bg: "#EEE7DA",
43
+ grid: "rgba(60,40,20,.045)",
44
+ glow: "rgba(255,255,255,.55)",
45
+ vignette: "rgba(80,50,20,.10)",
46
+ ink: "#1A1A1A",
47
+ card: "#1C1916",
48
+ cardHi: "#2A2521",
49
+ line: "#DFD8C6",
50
+ onAccent: "#FFFFFF",
51
+ accent: "#B5541C",
52
+ accentWarm: "#C9622A",
53
+ accentLight: "#E39A6C",
54
+ accentShadow: "rgba(150,70,20,.25)",
55
+ dash: "#6B5A4A",
56
+ shadow: "0 50px 90px rgba(70,40,15,.32), 0 12px 24px rgba(40,20,5,.22)",
57
+ serif: playfair.fontFamily,
58
+ sans: inter.fontFamily,
59
+ mono: mono.fontFamily,
60
+ };
61
+
62
+ export const mergeTheme = (overrides?: Partial<EditorialTheme>): EditorialTheme => ({
63
+ ...defaultTheme,
64
+ ...overrides,
65
+ });
66
+
67
+ export const ThemeContext = createContext<EditorialTheme>(defaultTheme);
68
+ export const useTheme = (): EditorialTheme => useContext(ThemeContext);
@@ -0,0 +1,65 @@
1
+ import { FPS } from "./theme";
2
+
3
+ // Timing vem do ctc-align: { words: [{ text, start, end }], total } em segundos.
4
+ // Trocar a voz = realinhar o JSON; as cenas seguem as palavras-âncora, não segundos.
5
+
6
+ export type Word = { text: string; start: number; end: number };
7
+ export type WordsData = { words: Word[]; total: number };
8
+
9
+ export const norm = (s: string): string => s.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
10
+
11
+ // Início (s) da primeira palavra `text` que começa em `after` ou depois.
12
+ // A busca é monotônica: sempre passe o tempo da âncora anterior como `after`,
13
+ // assim palavras repetidas ("e", "o") não casam com uma ocorrência antiga.
14
+ export const at = (words: readonly Word[], text: string, after = 0): number => {
15
+ const target = norm(text);
16
+ const hit = words.find((w) => w.start >= after - 0.001 && norm(w.text) === target);
17
+ if (!hit) throw new Error(`palavra não encontrada depois de ${after}s: "${text}"`);
18
+ return hit.start;
19
+ };
20
+
21
+ // Resolve um mapa nome -> palavra em tempos absolutos (monotônico na ordem de declaração).
22
+ export const resolveMarks = (
23
+ words: readonly Word[],
24
+ marks: Readonly<Record<string, string>> | undefined,
25
+ from: number,
26
+ ): Record<string, number> =>
27
+ Object.entries(marks ?? {}).reduce<{ cursor: number; out: Record<string, number> }>(
28
+ (acc, [name, text]) => {
29
+ const t = at(words, text, acc.cursor);
30
+ return { cursor: t, out: { ...acc.out, [name]: t } };
31
+ },
32
+ { cursor: from, out: {} },
33
+ ).out;
34
+
35
+ export type Anchored = { at: string; marks?: Record<string, string> };
36
+
37
+ export type TimedScene<T extends Anchored> = {
38
+ def: T;
39
+ from: number; // s, absoluto
40
+ dur: number; // s
41
+ marks: Record<string, number>; // s, RELATIVO ao início da cena (usa direto dentro da Sequence)
42
+ };
43
+
44
+ // Monta as cenas por âncora: cada cena começa na sua palavra e vai até a âncora da
45
+ // próxima. `overlap` faz a cena seguinte entrar um pouco antes enquanto a anterior sobe.
46
+ export const buildScenes = <T extends Anchored>(
47
+ words: readonly Word[],
48
+ defs: readonly T[],
49
+ opts: { end: number; overlap?: number },
50
+ ): TimedScene<T>[] => {
51
+ const overlap = opts.overlap ?? 0.12;
52
+ const starts = defs.reduce<number[]>((acc, d) => [...acc, at(words, d.at, acc[acc.length - 1] ?? 0)], []);
53
+ return defs.map((def, i) => {
54
+ const from = Math.max(0, starts[i] - (i ? overlap : 0));
55
+ const to = starts[i + 1] ?? opts.end;
56
+ const abs = resolveMarks(words, def.marks, starts[i]);
57
+ const marks = Object.fromEntries(Object.entries(abs).map(([k, v]) => [k, v - from]));
58
+ return { def, from, dur: to - from + overlap, marks };
59
+ });
60
+ };
61
+
62
+ export const toFrames = (seconds: number): number => Math.round(seconds * FPS);
63
+
64
+ // Duração total do filme: fim da fala + respiro final.
65
+ export const totalSeconds = (data: WordsData, tail = 1.4): number => data.total + tail;