primocode 10.0.0 → 10.0.9

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 (55) hide show
  1. package/README.md +2 -2
  2. package/bin/primocode.js +69 -1
  3. package/lib/act.js +118 -19
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +1 -1
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/entrada.js +73 -9
  9. package/lib/estudio/cinema.js +27 -9
  10. package/lib/estudio/diretor.js +13 -1
  11. package/lib/estudio/ende.js +16 -5
  12. package/lib/estudio/estilo.js +23 -8
  13. package/lib/estudio/gerativo.js +1453 -0
  14. package/lib/estudio/hibrido.js +165 -0
  15. package/lib/estudio/index.js +244 -36
  16. package/lib/estudio/kit/gerativo.jsx +980 -0
  17. package/lib/estudio/kit/index.jsx +3 -1
  18. package/lib/estudio/kit/tres.jsx +308 -0
  19. package/lib/estudio/kit.js +7 -1
  20. package/lib/estudio/lancamento.js +28 -0
  21. package/lib/estudio/referencias.js +105 -0
  22. package/lib/estudio/remotion.js +53 -9
  23. package/lib/estudio/ritmo.js +2 -2
  24. package/lib/estudio/sons-leves.js +38 -1
  25. package/lib/estudio/variedade.js +105 -0
  26. package/lib/estudio/vox.js +37 -8
  27. package/lib/mcp.js +156 -0
  28. package/lib/personas.js +67 -0
  29. package/lib/skills.js +36 -0
  30. package/lib/tools.js +22 -3
  31. package/lib/ui.js +41 -6
  32. package/lib/video/auto.js +329 -0
  33. package/lib/video/camadas.js +207 -0
  34. package/lib/video/cli.js +1 -1
  35. package/lib/video/filters.js +39 -6
  36. package/lib/video/fx3d/Camadas.jsx +64 -0
  37. package/lib/video/fx3d/Objetos.jsx +128 -0
  38. package/lib/video/fx3d/Root.jsx +18 -0
  39. package/lib/video/fx3d/modelos.jsx +304 -0
  40. package/lib/video/fx3d.js +108 -0
  41. package/lib/video/gerados.js +119 -0
  42. package/lib/video/objetos.js +111 -0
  43. package/lib/video/ops-fx.js +453 -0
  44. package/lib/video/ops.js +30 -5
  45. package/lib/video/paralelo.js +50 -0
  46. package/lib/video/pedido.js +191 -0
  47. package/lib/video/rastrear.js +95 -0
  48. package/lib/video/rastro.js +276 -0
  49. package/lib/video/render.js +67 -14
  50. package/lib/video/texto.js +6 -4
  51. package/lib/video/timeline.js +9 -1
  52. package/package.json +2 -2
  53. package/skills/motion-de-primeira/SKILL.md +5 -4
  54. package/studio/narrar.py +10 -7
  55. package/studio/rastrear.py +399 -0
@@ -0,0 +1,980 @@
1
+ /* kit/gerativo.jsx — o motion gerativo: UM componente que desenha a
2
+ * partitura inteira (lib/estudio/gerativo.js), quadro a quadro.
3
+ *
4
+ * Não há cena, não há template: há a linha do tempo. Em cada quadro este
5
+ * componente pergunta à partitura que batidas estão no ar, onde está a
6
+ * câmera, em que forma está o acento — e desenha. Tudo é função do tempo:
7
+ * nada de Math.random, Date.now nem transição CSS.
8
+ *
9
+ * A letra é a unidade: cada uma é um <span> com o próprio transform e os
10
+ * próprios EIXOS da fonte variável (font-variation-settings) — o peso que vai
11
+ * de 100 a 900, a largura que estica, o grau que respira. Antes do primeiro
12
+ * quadro as fontes carregam e cada linha é MEDIDA no navegador (delayRender):
13
+ * o que não cabe encolhe, e o acento sabe onde fica cada palavra. */
14
+ import React, { useState, useEffect, useMemo } from 'react';
15
+ import { delayRender, continueRender } from 'remotion';
16
+ import { SNAPPY, CURVAS_KIT, acaso, suave, alfa, mistura } from './base';
17
+ import { Produto3D, Icone3D } from './tres';
18
+
19
+ const ed = CURVAS_KIT.editorial, curvaCam = CURVAS_KIT.camera, arrasto = CURVAS_KIT.arrasto, acelera = CURVAS_KIT.acelera;
20
+ const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
21
+ const lerp = (a, b, k) => a + (b - a) * k;
22
+ const entradaC = (t) => t * t * t;
23
+ const saidaC = (t) => 1 - Math.pow(1 - t, 3);
24
+
25
+ /* A mola SNAPPY do guia em forma fechada (o spring() do Remotion simula
26
+ quadro a quadro; aqui são milhares de letras por quadro). Amortecimento
27
+ 26 com massa 0,65: um pouco acima do crítico — ataque rápido, sem quique. */
28
+ const MOLA = (() => {
29
+ const { damping: c, stiffness: k, mass: m } = SNAPPY;
30
+ const w0 = Math.sqrt(k / m), z = c / (2 * Math.sqrt(k * m));
31
+ if (z > 1.0001) {
32
+ const s = Math.sqrt(z * z - 1), r1 = -w0 * (z - s), r2 = -w0 * (z + s);
33
+ const A = r2 / (r1 - r2), B = -r1 / (r1 - r2);
34
+ return (t) => (t <= 0 ? 0 : 1 + A * Math.exp(r1 * t) + B * Math.exp(r2 * t));
35
+ }
36
+ return (t) => (t <= 0 ? 0 : 1 - Math.exp(-w0 * t) * (1 + w0 * t));
37
+ })();
38
+
39
+ const EMBARALHA = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&*+=/<>';
40
+ const embaralha = (n) => EMBARALHA[Math.floor(acaso(n) * EMBARALHA.length) % EMBARALHA.length];
41
+
42
+ // ── os eixos ────────────────────────────────────────────────────────────
43
+ function fvs(eixosFonte, valores) {
44
+ const partes = [];
45
+ for (const tag of Object.keys(valores)) {
46
+ const faixa = eixosFonte && eixosFonte[tag];
47
+ if (!faixa) continue;
48
+ const v = Math.max(faixa[0], Math.min(faixa[1], valores[tag]));
49
+ partes.push('"' + tag + '" ' + Math.round(v * 10) / 10);
50
+ }
51
+ return partes.join(', ') || 'normal';
52
+ }
53
+ const textoDaLinha = (l) => (l.maiusc ? l.texto.toUpperCase() : l.texto);
54
+ const palavrasDaLinha = (l) => {
55
+ const ws = textoDaLinha(l).split(/\s+/).filter(Boolean);
56
+ return l.palavras.map((p, j) => ({ ...p, texto: ws[j] != null ? ws[j] : p.texto }));
57
+ };
58
+ const pesoDaPalavra = (l, p) => (p.wght != null ? p.wght : l.eixos.wght);
59
+ const familia = (f) => '"' + f + '", system-ui, sans-serif';
60
+
61
+ /* A marcação de uma linha: a MESMA na medida e no desenho — é assim que a
62
+ medida vale para o que aparece. */
63
+ function Marcacao({ l, eixosFonte, estiloLetra, estiloPalavra, extraLetra }) {
64
+ const ps = palavrasDaLinha(l);
65
+ let k = 0;
66
+ return ps.map((p, j) => {
67
+ const letras = Array.from(p.texto);
68
+ const ep = estiloPalavra ? estiloPalavra(p, j) : {};
69
+ const out = (
70
+ <span key={j} style={{ display: 'inline-block', fontSize: (p.escala || 1) + 'em',
71
+ fontVariationSettings: fvs(eixosFonte, { ...l.eixos, wght: pesoDaPalavra(l, p) }), ...ep }}>
72
+ {letras.map((ch, i) => {
73
+ const idx = k++;
74
+ const e = estiloLetra ? estiloLetra(p, j, ch, i, idx) : null;
75
+ return (
76
+ <span key={i} style={{ display: 'inline-block', position: 'relative', ...(e && e.estilo) }}>
77
+ {e && e.char != null ? e.char : ch}
78
+ {extraLetra ? extraLetra(p, j, i, idx) : null}
79
+ </span>
80
+ );
81
+ })}
82
+ </span>
83
+ );
84
+ return j < ps.length - 1 ? [out, <span key={'s' + j}>{' '}</span>] : out;
85
+ });
86
+ }
87
+
88
+ // ── a medida (uma vez por aba do render) ─────────────────────────────────
89
+ function medir(P) {
90
+ const caixa = document.createElement('div');
91
+ caixa.style.cssText = 'position:absolute;left:-99999px;top:0;visibility:hidden;';
92
+ document.body.appendChild(caixa);
93
+ const out = {};
94
+ const linha = (id, l, fonteForcada) => {
95
+ const d = document.createElement('div');
96
+ d.style.cssText = 'position:absolute;white-space:pre;line-height:1;font-size:100px;';
97
+ d.style.fontFamily = familia(fonteForcada || l.fonte);
98
+ d.style.letterSpacing = (l.tracking || 0) + 'em';
99
+ d.style.fontFeatureSettings = '"tnum" 1';
100
+ const eixos = P.eixos[l.fonte] || {};
101
+ const ps = palavrasDaLinha(l);
102
+ ps.forEach((p, j) => {
103
+ const w = document.createElement('span');
104
+ w.style.display = 'inline-block';
105
+ w.style.fontSize = (p.escala || 1) + 'em';
106
+ w.style.fontVariationSettings = fvs(eixos, { ...l.eixos, wght: pesoDaPalavra(l, p) });
107
+ for (const ch of Array.from(p.texto)) {
108
+ const s = document.createElement('span');
109
+ s.style.display = 'inline-block';
110
+ s.textContent = ch;
111
+ w.appendChild(s);
112
+ }
113
+ d.appendChild(w);
114
+ if (j < ps.length - 1) d.appendChild(document.createTextNode(' '));
115
+ });
116
+ caixa.appendChild(d);
117
+ const r0 = d.getBoundingClientRect();
118
+ const palavras = [], letras = [];
119
+ for (const w of d.querySelectorAll(':scope > span')) {
120
+ const r = w.getBoundingClientRect();
121
+ palavras.push({ x: (r.left - r0.left) / 100, w: r.width / 100 });
122
+ for (const s of w.querySelectorAll('span')) { const q = s.getBoundingClientRect(); letras.push({ x: (q.left - r0.left) / 100, w: q.width / 100 }); }
123
+ }
124
+ out[id] = { w: r0.width / 100, h: r0.height / 100, palavras, letras };
125
+ };
126
+ for (const b of P.batidas) {
127
+ for (const l of b.linhas) linha(l.id, l);
128
+ if (b.textura) linha(b.id + '-textura', { ...b.linhas[0], texto: b.textura.texto + ' · ', maiusc: false, palavras: (b.textura.texto + ' ·').split(/\s+/).map((t) => ({ texto: t })) });
129
+ }
130
+ document.body.removeChild(caixa);
131
+ return out;
132
+ }
133
+
134
+ function useMedidas(P) {
135
+ const [medidas, setMedidas] = useState(null);
136
+ const [alca] = useState(() => delayRender('gerativo: carregar as fontes e medir as linhas'));
137
+ useEffect(() => {
138
+ let vivo = true;
139
+ const texto = Array.from(new Set(P.batidas.flatMap((b) => b.linhas.map((l) => textoDaLinha(l) + l.texto)).join('') + '0123456789 ·—“”')).join('');
140
+ const fams = Array.from(new Set(Object.values(P.fontes)));
141
+ const carregar = () => Promise.all(fams.flatMap((f) => [100, 400, 900].map((w) => document.fonts.load(w + ' 100px "' + f + '"', texto).catch(() => null))));
142
+ carregar().then(() => document.fonts.ready).then(() => { if (vivo) setMedidas(medir(P)); })
143
+ .catch(() => { if (vivo) setMedidas(medir(P)); });
144
+ return () => { vivo = false; };
145
+ }, []);
146
+ useEffect(() => { if (medidas) continueRender(alca); }, [medidas]);
147
+ return medidas;
148
+ }
149
+
150
+ // ── o leiaute: da medida às caixas no mundo ─────────────────────────────
151
+ function leiaute(P, med) {
152
+ const W = P.largura, H = P.altura;
153
+ const porBatida = {};
154
+ for (const b of P.batidas) {
155
+ const bl = b.bloco;
156
+ const caixas = {};
157
+ /* O tamanho final: o que não cabe encolhe; o grupo que ENCHE cresce até a
158
+ largura do bloco (todas as linhas dele no mesmo tamanho), com teto. */
159
+ const grupo = {};
160
+ for (const l of b.linhas) {
161
+ if (!l.encher || !med[l.id]) continue;
162
+ const k = l.encher + (l.coluna != null ? l.coluna : '');
163
+ // O grid manda: a tese ocupa até ~74% da largura do bloco; só a sangria vai a 97%.
164
+ const s = Math.min(l.max || l.tamanho, (bl.largura * (b.cheio ? 0.97 : 0.74)) / med[l.id].w);
165
+ grupo[k] = grupo[k] == null ? s : Math.min(grupo[k], s);
166
+ }
167
+ const tam = (l) => {
168
+ const m = med[l.id];
169
+ if (l.encher && grupo[l.encher + (l.coluna != null ? l.coluna : '')] != null) return grupo[l.encher + (l.coluna != null ? l.coluna : '')];
170
+ let s = l.tamanho;
171
+ if (m && m.w * s > bl.largura) s = (bl.largura / m.w) * 0.985;
172
+ return s;
173
+ };
174
+ const gapEntre = (a, c) => {
175
+ if (c.continua) return 0.1 * tam(a);
176
+ if (a.papel === c.papel) return a.papel === 'item' ? bl.gap * tam(a) : a.papel === 'tese' || a.papel === 'numero' ? 0.02 * tam(a) : 0.14 * tam(a);
177
+ return Math.max(0.32 * Math.min(tam(a), tam(c)), 0.12 * Math.max(tam(a), tam(c)));
178
+ };
179
+ const pilha = (ls, x0, yCentro, alinha) => {
180
+ const vis = ls.filter((l) => l.papel !== 'aspas');
181
+ let total = 0;
182
+ vis.forEach((l, i) => { total += (med[l.id] ? med[l.id].h : 1) * tam(l); if (i) total += gapEntre(vis[i - 1], l); });
183
+ let y = yCentro - (bl.vert === 'meio' ? total / 2 : 0);
184
+ vis.forEach((l, i) => {
185
+ if (i) y += gapEntre(vis[i - 1], l);
186
+ const s = tam(l), m = med[l.id] || { w: 1, h: 1, palavras: [], letras: [] };
187
+ const w = m.w * s, h = m.h * s;
188
+ const x = alinha === 'centro' ? x0 - w / 2 : x0;
189
+ caixas[l.id] = { x, y, w, h, s, palavras: m.palavras.map((p) => ({ x: x + p.x * s, w: p.w * s })), letras: m.letras.map((q) => ({ x: x + q.x * s, w: q.w * s })) };
190
+ y += h;
191
+ });
192
+ return total;
193
+ };
194
+ if (bl.colunas) {
195
+ pilha(b.linhas.filter((l) => l.coluna === 0), bl.colunas[0], bl.y, 'esq');
196
+ pilha(b.linhas.filter((l) => l.coluna === 1), bl.colunas[1], bl.y, 'esq');
197
+ } else if (bl.blocos === 2) {
198
+ const a = b.linhas.filter((l) => l.coluna === 0), c = b.linhas.filter((l) => l.coluna === 1);
199
+ const alt = (ls) => ls.reduce((n, l, i) => n + (med[l.id] ? med[l.id].h : 1) * tam(l) + (i ? gapEntre(ls[i - 1], l) : 0), 0);
200
+ const hA = alt(a), hC = alt(c), vao = 0.9 * tam(a[0] || c[0]);
201
+ const topo = bl.y - (hA + vao + hC) / 2;
202
+ pilha(a, bl.x, topo + hA / 2, 'esq');
203
+ pilha(c, bl.x, topo + hA + vao + hC / 2, 'esq');
204
+ } else pilha(b.linhas, bl.x, bl.y, bl.alinha);
205
+ // As aspas penduradas acima da primeira linha da citação.
206
+ const aspas = b.linhas.find((l) => l.papel === 'aspas');
207
+ if (aspas) {
208
+ const prim = b.linhas.find((l) => l.papel !== 'aspas');
209
+ const cp = caixas[prim.id];
210
+ const s = aspas.tamanho;
211
+ caixas[aspas.id] = { x: cp.x - s * 0.04, y: cp.y - s * 0.62, w: s * 0.5, h: s, s, palavras: [{ x: cp.x, w: s * 0.5 }], letras: [{ x: cp.x, w: s * 0.5 }] };
212
+ }
213
+ // O bloco inteiro (para a barra e a moldura).
214
+ const cs = Object.values(caixas);
215
+ const bx = { x: Math.min(...cs.map((c) => c.x)), y: Math.min(...cs.map((c) => c.y)), x1: Math.max(...cs.map((c) => c.x + c.w)), y1: Math.max(...cs.map((c) => c.y + c.h)) };
216
+ porBatida[b.id] = { caixas, bloco: bx, textura: b.textura && med[b.id + '-textura'] ? med[b.id + '-textura'].w * b.textura.tamanho : 0 };
217
+ }
218
+ return porBatida;
219
+ }
220
+
221
+ // ── o acento: um traço de N pontos que muda de forma ─────────────────────
222
+ const N = 36;
223
+ function geometria(a, b, L, P) {
224
+ const lay = L[b.id];
225
+ const base = Math.min(P.largura, P.altura) / 1080;
226
+ const cx = lay.caixas[a.alvo && a.alvo.linha] || null;
227
+ const alvoPalavra = () => {
228
+ if (!cx) return { x: lay.bloco.x, w: lay.bloco.x1 - lay.bloco.x };
229
+ const i = a.alvo.palavra, f = a.alvo.ate != null ? a.alvo.ate : i;
230
+ if (i == null || i < 0 || !cx.palavras[i]) return { x: cx.x, w: cx.w };
231
+ const p0 = cx.palavras[i], p1 = cx.palavras[Math.max(i, Math.min(f, cx.palavras.length - 1))];
232
+ return { x: p0.x, w: p1.x + p1.w - p0.x };
233
+ };
234
+ const pts = [];
235
+ const f = a.forma;
236
+ if (f === 'ponto' || f === 'bala' || f === 'viagem') {
237
+ let x, y, r;
238
+ if (f === 'viagem') { x = a.x; y = a.y; r = a.r; }
239
+ else if (f === 'bala' && cx) { r = Math.max(7 * base, cx.s * 0.13); x = cx.x - r * 2.6; y = cx.y + cx.h * 0.52; }
240
+ else if (cx && a.alvo.antes) { r = cx.s * 0.2; x = cx.x - r * 2.2; y = cx.y + cx.h * 0.5; }
241
+ else if (cx) { r = Math.max(8 * base, cx.s * 0.085); x = cx.x + cx.w + r * 1.6; y = cx.y + cx.h * 0.74; }
242
+ else { r = 14 * base; x = P.largura / 2; y = P.altura / 2; }
243
+ for (let i = 0; i < N; i++) { const q = (i / (N - 1)) * Math.PI * 2; pts.push([x + Math.cos(q) * r * 0.35, y + Math.sin(q) * r * 0.35]); }
244
+ return { pts, larg: r * 1.35, alfa: 1, frente: true, cx: x, cy: y };
245
+ }
246
+ if (!cx && f !== 'barra') return geometria({ forma: 'ponto', alvo: {} }, b, L, P);
247
+ if (f === 'sublinha' || f === 'risco' || f === 'marca') {
248
+ const pw = alvoPalavra();
249
+ const s = cx.s;
250
+ const y = f === 'sublinha' ? cx.y + cx.h * 0.96 : f === 'risco' ? cx.y + cx.h * 0.56 : cx.y + cx.h * 0.6;
251
+ const x0 = pw.x - (f === 'marca' ? s * 0.06 : 0), x1 = pw.x + pw.w + (f === 'marca' ? s * 0.06 : 0);
252
+ for (let i = 0; i < N; i++) { const k = i / (N - 1); pts.push([lerp(x0, x1, k), y + Math.sin(k * 3.1 + 1) * s * 0.012 - (f === 'risco' ? (k - 0.5) * s * 0.08 : k * k * s * 0.02)]); }
253
+ return { pts, larg: f === 'marca' ? s * 0.6 : f === 'risco' ? Math.max(5 * base, s * 0.07) : Math.max(5 * base, s * 0.055), alfa: f === 'marca' ? 0.34 : 1, frente: f !== 'marca', tampa: f === 'marca' ? 'butt' : 'round', cx: (x0 + x1) / 2, cy: y };
254
+ }
255
+ if (f === 'anel') {
256
+ const pw = alvoPalavra();
257
+ const ccx = pw.x + pw.w / 2, ccy = cx.y + cx.h * 0.52;
258
+ const rx = Math.min(pw.w / 2 + cx.s * 0.14, P.largura / 2 - 24 * base), ry = cx.h * 0.6;
259
+ for (let i = 0; i < N; i++) { const q = -2.3 + (i / (N - 1)) * Math.PI * 2 * 1.1; const e = 1 + (i / N) * 0.07; pts.push([ccx + Math.cos(q) * rx * e, ccy + Math.sin(q) * ry * e]); }
260
+ return { pts, larg: Math.max(4 * base, cx.s * 0.03), alfa: 1, frente: true, cx: ccx, cy: ccy };
261
+ }
262
+ if (f === 'barra') {
263
+ const x = lay.bloco.x - Math.max(18 * base, (lay.bloco.y1 - lay.bloco.y) * 0.08);
264
+ for (let i = 0; i < N; i++) { const k = i / (N - 1); pts.push([x, lerp(lay.bloco.y + 4, lay.bloco.y1 - 4, k)]); }
265
+ return { pts, larg: Math.max(8 * base, 10 * base), alfa: 1, frente: true, cx: x, cy: (lay.bloco.y + lay.bloco.y1) / 2 };
266
+ }
267
+ if (f === 'moldura') {
268
+ const pad = cx.s * 0.14;
269
+ const x0 = cx.x - pad, y0 = cx.y - pad * 0.5, x1 = cx.x + cx.w + pad, y1 = cx.y + cx.h + pad * 0.5;
270
+ const per = 2 * (x1 - x0) + 2 * (y1 - y0);
271
+ for (let i = 0; i < N; i++) {
272
+ let d = (i / (N - 1)) * per;
273
+ if (d < x1 - x0) { pts.push([x0 + d, y0]); continue; } d -= x1 - x0;
274
+ if (d < y1 - y0) { pts.push([x1, y0 + d]); continue; } d -= y1 - y0;
275
+ if (d < x1 - x0) { pts.push([x1 - d, y1]); continue; } d -= x1 - x0;
276
+ pts.push([x0, y1 - d]);
277
+ }
278
+ return { pts, larg: Math.max(3 * base, cx.s * 0.02), alfa: 1, frente: true, tampa: 'square', cx: (x0 + x1) / 2, cy: (y0 + y1) / 2 };
279
+ }
280
+ return geometria({ forma: 'ponto', alvo: a.alvo }, b, L, P);
281
+ }
282
+
283
+ /* A linha do tempo do acento: a semente, e em cada batida a VIAGEM (vira
284
+ ponto e anda até onde o próximo traço começa) e os estados dela. */
285
+ function estadosDoAcento(P, L) {
286
+ const base = Math.min(P.largura, P.altura) / 1080;
287
+ const out = [{ t: 0, dur: 0.35, g: (() => { const r = 16 * base; const pts = []; for (let i = 0; i < N; i++) { const q = (i / (N - 1)) * Math.PI * 2; pts.push([P.largura / 2 + Math.cos(q) * r * 0.35, P.altura / 2 + Math.sin(q) * r * 0.35]); } return { pts, larg: r * 1.35, alfa: 1, frente: true, cx: P.largura / 2, cy: P.altura / 2 }; })(), cor: P.batidas[0].cores.acento, batida: 0 }];
288
+ P.batidas.forEach((b, bi) => {
289
+ b.acento.forEach((a, k) => {
290
+ const g = geometria(a, b, L, P);
291
+ if (k === 0 && a.forma !== 'bala' && a.forma !== 'ponto') {
292
+ const r = Math.max(7 * base, g.larg * 0.8);
293
+ const tv = Math.min(b.de + 0.05, a.t - 0.12);
294
+ out.push({ t: tv, dur: Math.max(0.12, Math.min(0.5, a.t - tv)), g: geometria({ forma: 'viagem', x: g.pts[0][0], y: g.pts[0][1], r }, b, L, P), cor: b.cores.acento, batida: bi });
295
+ }
296
+ out.push({ t: a.t, dur: a.dur, g, cor: b.cores.acento, batida: bi, forma: a.forma });
297
+ });
298
+ });
299
+ out.sort((a, b) => a.t - b.t);
300
+ return out;
301
+ }
302
+
303
+ function acentoEm(t, estados) {
304
+ let k = 0;
305
+ while (k < estados.length - 1 && estados[k + 1].t <= t) k++;
306
+ const cur = estados[k], prev = estados[Math.max(0, k - 1)];
307
+ const p = k === 0 ? saidaC(c01(t / cur.dur)) : arrasto(c01((t - cur.t) / Math.max(0.01, cur.dur)));
308
+ if (k === 0) {
309
+ const g = cur.g;
310
+ return { pts: g.pts, larg: g.larg * p, alfa: 1, frente: true, cor: cur.cor, cx: g.cx, cy: g.cy };
311
+ }
312
+ const a = prev.g, b = cur.g;
313
+ return {
314
+ pts: b.pts.map((q, i) => [lerp(a.pts[i][0], q[0], p), lerp(a.pts[i][1], q[1], p)]),
315
+ larg: lerp(a.larg, b.larg, p), alfa: lerp(a.alfa, b.alfa, p), frente: p > 0.5 ? b.frente : a.frente, tampa: p > 0.5 ? b.tampa : a.tampa,
316
+ cor: p > 0.5 ? cur.cor : prev.cor, cx: lerp(a.cx, b.cx, p), cy: lerp(a.cy, b.cy, p),
317
+ };
318
+ }
319
+
320
+ // ── a câmera ────────────────────────────────────────────────────────────
321
+ function cameraEm(t, ks) {
322
+ if (t <= ks[0].t) return ks[0];
323
+ for (let i = 0; i < ks.length - 1; i++) {
324
+ const a = ks[i], b = ks[i + 1];
325
+ if (t >= a.t && t < b.t) {
326
+ if (b.e === 'degrau' || b.t - a.t < 0.001) return b;
327
+ const u = (t - a.t) / (b.t - a.t);
328
+ const k = b.e === 'acelera' ? acelera(u) : b.e === 'editorial' ? ed(u) : curvaCam(u);
329
+ return { x: lerp(a.x, b.x, k), y: lerp(a.y, b.y, k), zoom: lerp(a.zoom, b.zoom, k), rot: lerp(a.rot, b.rot, k) };
330
+ }
331
+ }
332
+ return ks[ks.length - 1];
333
+ }
334
+
335
+ // ── as saídas: quanto tempo cada uma leva ───────────────────────────────
336
+ const DUR_SAIDA = { pontos: 0.5, evapora: 0.42, atravessa: 0.36, desliza: 0.36, comprime: 0.42, explode: 0.46, chicote: 0.3, inunda: 0.24, corte: 0, fim: 0 };
337
+
338
+ // ── a letra: entrada, vida, saída ───────────────────────────────────────
339
+ function letra({ t, fps, l, p, j, ch, i, idx, n, b, cx, cores, eixosF, alvoSaida, frame, folga, centroBloco }) {
340
+ const est = {};
341
+ let char = null;
342
+ const c = l.comportamento;
343
+ const passo = { carimbo: 0, desfoque: 0, tracking: 0, peso: 1.6, largura: 1.2, onda: 1.3, decodifica: 1, flap: 1.5, mascara: 1, pontos: 1.2, contorno: 1.6, monta: 0.8 }[c] || 1;
344
+ const t0 = p.t + (passo * idx) / fps * (l.papel === 'item' || l.papel === 'apoio' || l.papel === 'rotulo' ? 0.6 : 1);
345
+ const tl = t - t0;
346
+ if (tl < 0 && c !== 'decodifica' && c !== 'flap') return { estilo: { opacity: 0 } };
347
+ const m = MOLA(tl);
348
+ const tr = [];
349
+ let op = 1, blur = 0;
350
+ const eixos = { ...l.eixos, wght: pesoDaPalavra(l, p) };
351
+ const wMin = eixosF.wght ? eixosF.wght[0] : eixos.wght;
352
+ const semente = (l.id.length * 31 + idx * 7 + j * 13) % 997;
353
+ const caixaL = cx && cx.letras[idx];
354
+ const centroL = caixaL ? caixaL.x + caixaL.w / 2 : 0;
355
+ const centroLinha = cx ? cx.x + cx.w / 2 : 0;
356
+ switch (c) {
357
+ case 'carimbo': {
358
+ const r0 = (acaso(semente) - 0.5) * 8;
359
+ tr.push('rotate(' + (r0 * (1 - m)).toFixed(2) + 'deg)', 'scale(' + (1 + 0.32 * (1 - m)).toFixed(4) + ')');
360
+ const f = tl * fps;
361
+ blur = f < 2 ? 1.4 : Math.max(0, 0.7 * (1 - (f - 2) / 7));
362
+ break;
363
+ }
364
+ case 'mascara': tr.push('translateY(' + ((1 - m) * 105).toFixed(1) + '%)'); break;
365
+ case 'onda':
366
+ tr.push('translateY(' + ((1 - m) * 0.55 + Math.sin(tl * 15) * 0.08 * (1 - m)).toFixed(3) + 'em)', 'rotate(' + ((1 - m) * 10).toFixed(2) + 'deg)');
367
+ op = c01(tl * fps / 4); break;
368
+ case 'decodifica': {
369
+ if (t < p.t + (idx * 1) / fps - 0.001) return { estilo: { opacity: 0 } };
370
+ const resolve = p.t + 0.26 + (idx % 4) * 0.035 + idx * 0.012;
371
+ if (t < resolve && ch.trim()) { char = embaralha(semente + Math.floor(frame / 2) * 17); est.color = cores.fraco; }
372
+ else tr.push('scale(' + (1 + 0.1 * (1 - MOLA(t - resolve))).toFixed(3) + ')');
373
+ break;
374
+ }
375
+ case 'flap': {
376
+ if (t < p.t + (idx * 1.2) / fps) return { estilo: { opacity: 0 } };
377
+ const nf = 3 + (idx % 3), dur = (nf * 2) / fps, tf = t - (p.t + (idx * 1.2) / fps);
378
+ if (tf < dur && ch.trim()) { char = embaralha(semente + Math.floor(tf * fps / 2) * 29); const fase = (tf * fps / 2) % 1; tr.push('rotateX(' + (fase * 80).toFixed(1) + 'deg)'); est.color = cores.fraco; }
379
+ else tr.push('rotateX(' + ((1 - MOLA(tf - dur)) * 50).toFixed(1) + 'deg)');
380
+ break;
381
+ }
382
+ case 'peso': {
383
+ /* O peso BALANÇA: do fio (wght mínimo) ao mais pesado que a fonte tem,
384
+ e assenta no peso dele — a própria fonte se mexe. */
385
+ const u = c01(tl / 0.8), wMax = eixosF.wght ? eixosF.wght[1] : eixos.wght, alvo = eixos.wght;
386
+ eixos.wght = u < 0.5 ? lerp(wMin, wMax, ed(u / 0.5)) : lerp(wMax, alvo, ed((u - 0.5) / 0.5));
387
+ tr.push('scaleY(' + (0.9 + 0.1 * ed(c01(tl / 0.4))).toFixed(3) + ')');
388
+ op = c01(tl * fps / 3); break;
389
+ }
390
+ case 'largura': {
391
+ // A largura estica até onde o quadro deixa e volta (wdth 25 → pico → normal).
392
+ const u = c01(tl / 0.8), base0 = l.eixos.wdth || 100;
393
+ if (eixosF.wdth) {
394
+ const pico = Math.min(eixosF.wdth[1], base0 + (folga || 0) * 100 * 0.9);
395
+ eixos.wdth = u < 0.5 ? lerp(eixosF.wdth[0], pico, ed(u / 0.5)) : lerp(pico, base0, ed((u - 0.5) / 0.5));
396
+ }
397
+ eixos.wght = lerp(Math.max(wMin, eixos.wght - 450), eixos.wght, ed(u));
398
+ op = c01(tl * fps / 3); break;
399
+ }
400
+ case 'monta': {
401
+ // As letras chegam dos estilhaços da palavra que explodiu e se montam.
402
+ // A letra volta numa curva (arco), assenta com a mola, sem pião.
403
+ const k = MOLA(tl * 0.85);
404
+ const a = acaso(semente + 3) * Math.PI * 2, d = (160 + acaso(semente + 5) * 260) * (1 - k);
405
+ tr.push('translate(' + (Math.cos(a) * d).toFixed(1) + 'px,' + (Math.sin(a) * d - 120 * (1 - k) * k * 4).toFixed(1) + 'px)',
406
+ 'rotate(' + ((acaso(semente + 9) - 0.5) * 60 * (1 - k)).toFixed(1) + 'deg)', 'scale(' + (0.6 + 0.4 * k).toFixed(3) + ')');
407
+ blur = (1 - k) * 10; op = c01(tl * fps / 2);
408
+ eixos.wght = lerp(wMin, eixos.wght, k);
409
+ break;
410
+ }
411
+ case 'tracking': tr.push('translateX(' + ((centroLinha - centroL) * (1 - m)).toFixed(1) + 'px)'); op = c01(m * 1.4); break;
412
+ case 'desfoque': blur = (1 - m) * 16; tr.push('translateY(' + ((1 - m) * 0.3).toFixed(3) + 'em)', 'scale(' + (1 + 0.05 * (1 - m)).toFixed(3) + ')'); op = c01(m * 1.2); break;
413
+ case 'pontos': {
414
+ const k = MOLA(tl - 4 / fps);
415
+ tr.push('scale(' + k.toFixed(3) + ')');
416
+ break;
417
+ }
418
+ case 'contorno': {
419
+ // O contorno se desenha, e a tinta enche por dentro.
420
+ const k = c01((tl - 0.3) / 0.4);
421
+ est.WebkitTextStroke = (0.018 * (1 - k) + 0.0001).toFixed(4) + 'em currentColor';
422
+ if (k < 1) est.WebkitTextFillColor = alfaCor(cores, l, p, k);
423
+ op = c01(tl * fps / 3); break;
424
+ }
425
+ default: break;
426
+ }
427
+ // A VIDA no repouso: a fonte continua se mexendo enquanto a gente lê.
428
+ const principal = l.papel === 'tese' || l.papel === 'numero';
429
+ const repouso = principal ? c01((t - (l.t + 0.7)) / 0.6) : 0;
430
+ if (repouso > 0 && !l.riscado) {
431
+ const fase = t * (Math.PI * 2) / 2.8 - idx * 0.45;
432
+ if (b.vida === 'respira') {
433
+ if (eixosF.GRAD) eixos.GRAD = (l.eixos.GRAD || 0) - 200 * (0.5 + 0.5 * Math.sin(fase)) * repouso;
434
+ else eixos.wght = eixos.wght - 280 * (0.5 + 0.5 * Math.sin(fase)) * repouso;
435
+ } else if (b.vida === 'onda') tr.push('translateY(' + (-0.06 * Math.sin(fase) * repouso).toFixed(4) + 'em)');
436
+ else if (b.vida === 'deriva') tr.push('translateX(' + ((centroL - centroLinha) * 0.03 * repouso * c01((t - l.t) / 3)).toFixed(2) + 'px)');
437
+ else if (b.vida === 'estica' && eixosF.wdth) eixos.wdth = (l.eixos.wdth || 100) - 30 * (0.5 + 0.5 * Math.sin(fase)) * repouso;
438
+ }
439
+ // A letra treme de leve no pulso (a cada meio segundo), e nunca fica parada de vez.
440
+ /* (O tremor de letra no pulso saiu: lia como defeito, não como vida.) */
441
+ // A SAÍDA: a letra se transforma no que vem.
442
+ const ds = DUR_SAIDA[b.saida] || 0;
443
+ if (ds > 0) {
444
+ const ordem = idx / Math.max(1, n);
445
+ const te = c01((t - (b.ate - ds) - ordem * ds * 0.35) / (ds * 0.65));
446
+ if (te > 0) {
447
+ if (b.saida === 'pontos') {
448
+ tr.push('scale(' + (1 - saidaC(c01(te / 0.4))).toFixed(3) + ')');
449
+ } else if (b.saida === 'evapora') {
450
+ eixos.wght = lerp(eixos.wght, wMin, ed(te));
451
+ tr.push('translate(' + ((centroL - centroLinha) * 0.25 * te).toFixed(1) + 'px,' + (-0.15 * te).toFixed(3) + 'em)');
452
+ op *= 1 - te; blur += te * 6;
453
+ } else if (b.saida === 'explode') {
454
+ // A palavra EXPLODE: cada letra voa para fora do centro do bloco, girando.
455
+ const lay = cx && cx.letras[idx];
456
+ const ox = lay ? lay.x + lay.w / 2 - centroBloco.x : 0, oy = cx ? cx.y + cx.h / 2 - centroBloco.y : 0;
457
+ // Em arco (a gravidade puxa um pouco para baixo), com giro contido — física, não glitch.
458
+ const a = Math.atan2(oy, ox || 0.001) + (acaso(semente + 11) - 0.5) * 0.7;
459
+ const e2 = entradaC(te), d = (220 + acaso(semente + 13) * 360) * e2;
460
+ tr.push('translate(' + (Math.cos(a) * d).toFixed(1) + 'px,' + (Math.sin(a) * d + 180 * e2 * e2).toFixed(1) + 'px)',
461
+ 'rotate(' + ((acaso(semente + 17) - 0.5) * 90 * e2).toFixed(1) + 'deg)', 'scale(' + (1 + 0.25 * e2).toFixed(3) + ')');
462
+ op *= 1 - te * te; blur += te * 9;
463
+ if (eixosF.wght) eixos.wght = lerp(eixos.wght, eixosF.wght[1], te);
464
+ } else if (b.saida === 'comprime') {
465
+ tr.push('translateX(' + ((centroLinha - centroL) * ed(te)).toFixed(1) + 'px)', 'scaleX(' + (1 - 0.9 * te).toFixed(3) + ')');
466
+ if (eixosF.wdth) eixos.wdth = lerp(eixos.wdth || 100, eixosF.wdth[0], te);
467
+ op *= 1 - c01((te - 0.7) / 0.3);
468
+ }
469
+ }
470
+ }
471
+ if (tr.length) est.transform = tr.join(' ');
472
+ if (op < 1) est.opacity = op;
473
+ if (blur > 0.05) est.filter = 'blur(' + blur.toFixed(2) + 'px)';
474
+ est.fontVariationSettings = fvs(eixosF, eixos);
475
+ est.transformOrigin = '50% 60%';
476
+ return { estilo: est, char };
477
+ }
478
+ function alfaCor(cores, l, p, k) {
479
+ const base = p.cor === 'acento' ? cores.acento : l.cor === 'acento' ? cores.acento : l.cor === 'fraco' ? cores.fraco : cores.texto;
480
+ return alfa(base, k);
481
+ }
482
+
483
+ /* O ponto que nasce ou em que a letra vira (entrada e saída "pontos"). */
484
+ function pontoDaLetra({ t, fps, l, p, idx, n, b, cx, cores, alvoSaida }) {
485
+ const tamPonto = 0.17;
486
+ const c = l.comportamento;
487
+ const t0 = p.t + (1.2 * idx) / fps;
488
+ let escala = 0, dx = 0, dy = 0, op = 1;
489
+ if (c === 'pontos' && t >= t0 && t < t0 + 0.5) {
490
+ const tl = t - t0;
491
+ escala = tl < 4 / fps ? saidaC(tl * fps / 4) : 1 - MOLA(tl - 4 / fps);
492
+ }
493
+ const ds = DUR_SAIDA[b.saida] || 0;
494
+ if (b.saida === 'pontos' && ds > 0 && cx && cx.letras[idx]) {
495
+ const ordem = idx / Math.max(1, n);
496
+ const te = c01((t - (b.ate - ds) - ordem * ds * 0.35) / (ds * 0.65));
497
+ if (te > 0) {
498
+ escala = Math.max(escala, saidaC(c01(te / 0.3)));
499
+ if (alvoSaida) {
500
+ const q = cx.letras[idx];
501
+ const k = entradaC(c01((te - 0.3) / 0.7));
502
+ dx = (alvoSaida.x - (q.x + q.w / 2)) * k;
503
+ dy = (alvoSaida.y - (cx.y + cx.h * 0.55)) * k;
504
+ escala *= 1 - 0.5 * k;
505
+ op = 1 - c01((te - 0.85) / 0.15);
506
+ }
507
+ }
508
+ }
509
+ if (escala <= 0.01) return null;
510
+ const cor = p.cor === 'acento' ? cores.acento : cores.texto;
511
+ return (
512
+ <span style={{ position: 'absolute', left: '50%', top: '55%', width: tamPonto + 'em', height: tamPonto + 'em', marginLeft: -tamPonto / 2 + 'em', marginTop: -tamPonto / 2 + 'em',
513
+ borderRadius: '50%', background: cor, opacity: op,
514
+ transform: 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px) scale(' + escala.toFixed(3) + ')' }} />
515
+ );
516
+ }
517
+
518
+ // ── uma linha ──────────────────────────────────────────────────────────
519
+ function Linha({ l, b, cx, t, fps, P, frame, alvoSaida, centroBloco }) {
520
+ if (!cx) return null;
521
+ const folga = Math.max(0, (P.largura * 0.94) / Math.max(1, cx.w) - 1);
522
+ // O GOLPE: a tese soca (escala que bate e assenta).
523
+ let soco = 1;
524
+ for (const g of b.golpes || []) if (g.linha === l.id && t >= g.t) soco += 0.05 * g.forca * Math.exp(-(t - g.t) * 14);
525
+ const cores = b.cores;
526
+ const eixosF = P.eixos[l.fonte] || {};
527
+ const ps = palavrasDaLinha(l);
528
+ const n = ps.reduce((q, p) => q + Array.from(p.texto).length, 0);
529
+ // A cascata envelhece: o item anterior perde a cor quando o próximo entra.
530
+ const velho = l.envelhece != null ? c01((t - l.envelhece) / (6 / fps)) : 0;
531
+ let corL = l.cor === 'acento' ? cores.acento : l.cor === 'fraco' ? cores.fraco : cores.texto;
532
+ if (velho > 0) corL = mistura(corL, cores.fraco, velho);
533
+ let texto = null;
534
+ // O CONTADOR: o número conta, e o peso cresce com ele.
535
+ const ct = b.contador && b.contador.linha === l.id ? b.contador : null;
536
+ let pesoContador = null;
537
+ if (ct && ct.n != null) {
538
+ const k = ed(c01((t - ct.t) / ct.dur));
539
+ const v = ct.n * k;
540
+ let s = v.toFixed(ct.casas || 0);
541
+ if (ct.milhar) s = s.replace(/\B(?=(\d{3})+(?!\d))/g, ct.milhar);
542
+ if (ct.virgula) s = s.replace('.', ',');
543
+ texto = (ct.antes || '') + s + (ct.depois || '');
544
+ if (l.maiusc) texto = texto.toUpperCase();
545
+ const faixa = eixosF.wght || [300, 900];
546
+ pesoContador = lerp(faixa[0], l.eixos.wght, k);
547
+ }
548
+ const estiloPalavra = (p, j) => {
549
+ const e = {};
550
+ if (p.cor === 'acento' && l.cor !== 'acento') e.color = velho > 0 ? mistura(cores.acento, cores.fraco, velho) : cores.acento;
551
+ if (l.comportamento === 'mascara') e.clipPath = 'inset(-0.25em -0.1em -0.3em -0.1em)';
552
+ return e;
553
+ };
554
+ const conteudo = texto != null ? (
555
+ <span style={{ display: 'inline-block', fontVariationSettings: fvs(eixosF, { ...l.eixos, wght: pesoContador }), fontFeatureSettings: '"tnum" 1',
556
+ opacity: c01((t - ct.t) * fps / 3), transform: 'scaleY(' + (0.9 + 0.1 * ed(c01((t - ct.t) / ct.dur))).toFixed(3) + ')' }}>{texto}</span>
557
+ ) : (
558
+ <Marcacao l={l} eixosFonte={eixosF} estiloPalavra={estiloPalavra}
559
+ estiloLetra={(p, j, ch, i, idx) => letra({ t, fps, l, p, j, ch, i, idx, n, b, cx, cores, eixosF, alvoSaida, frame, folga, centroBloco })}
560
+ extraLetra={(p, j, i, idx) => pontoDaLetra({ t, fps, l, p, idx, n, b, cx, cores, alvoSaida })} />
561
+ );
562
+ // As ALÇAS de vetor da marca que se constrói: cantos e guias enquanto a letra se desenha.
563
+ const alcas = l.marca && t >= l.t && t < l.t + 1.3 ? (() => {
564
+ const k = c01((t - l.t) / 0.25) * (1 - c01((t - l.t - 0.9) / 0.4));
565
+ const lado = Math.max(5, cx.s * 0.05);
566
+ const cor = alfa(cores.texto, 0.75 * k);
567
+ return (
568
+ <>
569
+ {[0.2, 0.78, 1.0].map((f, q) => <div key={'g' + q} style={{ position: 'absolute', left: -cx.s * 0.3, width: cx.w + cx.s * 0.6, top: cx.h * f, height: 1, background: alfa(cores.acento, 0.55 * k) }} />)}
570
+ {cx.letras.map((q, qi) => {
571
+ const k2 = c01((t - l.t - qi * 1.6 / fps) / 0.15) * k;
572
+ return [0, 1].map((lx) => [0.2, 1.0].map((ly) => (
573
+ <div key={qi + '-' + lx + '-' + ly} style={{ position: 'absolute', left: q.x - cx.x + lx * q.w - lado / 2, top: cx.h * ly - lado / 2, width: lado, height: lado,
574
+ border: '1.5px solid ' + cor, background: alfa(cores.fundo, 0.9 * k2), opacity: k2 }} />
575
+ )));
576
+ })}
577
+ </>
578
+ );
579
+ })() : null;
580
+ return (
581
+ <div style={{ position: 'absolute', left: cx.x, top: cx.y, width: cx.w + 2, height: cx.h, whiteSpace: 'pre', lineHeight: 1,
582
+ fontFamily: familia(l.fonte), fontSize: cx.s, letterSpacing: (l.tracking || 0) + 'em', color: corL, perspective: '600px',
583
+ textAlign: 'left', fontFeatureSettings: '"tnum" 1', transform: soco !== 1 ? 'scale(' + soco.toFixed(4) + ')' : undefined, transformOrigin: '50% 55%' }}>
584
+ {alcas}
585
+ {conteudo}
586
+ </div>
587
+ );
588
+ }
589
+
590
+ // ── a parede: a linha repetida vira textura ─────────────────────────────
591
+ function Textura({ b, cx, t, P, periodo }) {
592
+ if (!cx || !b.textura || !periodo) return null;
593
+ const tx = b.textura;
594
+ const linhas = [];
595
+ const n = Math.ceil(tx.linhas / 2);
596
+ const reps = Math.ceil((P.largura * 3) / periodo) + 2;
597
+ const unidade = (tx.texto + ' · ').repeat(reps);
598
+ const saiu = DUR_SAIDA[b.saida] ? c01((t - (b.ate - DUR_SAIDA[b.saida])) / DUR_SAIDA[b.saida]) : 0;
599
+ const l0 = b.linhas[0];
600
+ for (let k = -n; k <= n; k++) {
601
+ if (k === 0) continue;
602
+ const aparece = c01((t - (l0.t + Math.abs(k) * 2 / P.fps)) / (6 / P.fps));
603
+ if (aparece <= 0) continue;
604
+ const dir = k % 2 ? 1 : -1;
605
+ const x = -((t * tx.velocidade * (1 + Math.abs(k) * 0.08) + Math.abs(k) * periodo * 0.37) % periodo) * dir - periodo * (dir > 0 ? 1 : 0) - P.largura;
606
+ linhas.push(
607
+ <div key={k} style={{ position: 'absolute', left: cx.x + x - cx.x, top: cx.y + k * cx.h * 1.02, whiteSpace: 'pre', lineHeight: 1, fontFamily: familia(tx.fonte),
608
+ fontSize: cx.s, fontVariationSettings: fvs(P.eixos[tx.fonte] || {}, l0.eixos), color: 'rgba(0,0,0,0)',
609
+ WebkitTextStroke: Math.max(1, cx.s * 0.008) + 'px ' + alfa(b.cores.texto, 0.32 * aparece * (1 - saiu)) }}>{unidade}</div>,
610
+ );
611
+ }
612
+ return <>{linhas}</>;
613
+ }
614
+
615
+ // ── o fundo vivo ────────────────────────────────────────────────────────
616
+ const GRAO = 'url("data:image/svg+xml;utf8,' + encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>") + '")';
617
+
618
+ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
619
+ const W = P.largura, H = P.altura;
620
+ const f = P.fundo;
621
+ // A inundação: o acento cresce e vira o fundo da próxima batida.
622
+ let corBase = b.cores.fundo, circulo = null;
623
+ /* O CAMPO troca: por inundação (o acento cresce e vira o fundo), por cortina
624
+ (a cor nova varre de um lado) ou por corte seco no quadro da batida. */
625
+ if (prevB && prevB.cores.fundo !== b.cores.fundo && b.troca === 'inunda') {
626
+ const k = entradaC(c01((t - (b.de - 0.2)) / 0.42));
627
+ if (k < 1) {
628
+ corBase = prevB.cores.fundo;
629
+ const R = Math.hypot(W, H) * 1.05 * k;
630
+ circulo = <div style={{ position: 'absolute', left: origem.x - R, top: origem.y - R, width: 2 * R, height: 2 * R, borderRadius: '50%', background: b.cores.fundo }} />;
631
+ }
632
+ } else if (prevB && prevB.cores.fundo !== b.cores.fundo && b.troca === 'cortina') {
633
+ const k = ed(c01((t - (b.de - 0.1)) / 0.34));
634
+ if (k < 1) {
635
+ corBase = prevB.cores.fundo;
636
+ const lado = (b.campo + Math.round(b.de * 10)) % 4;
637
+ const ins = lado === 0 ? `inset(0 ${100 - k * 100}% 0 0)` : lado === 1 ? `inset(0 0 0 ${100 - k * 100}%)` : lado === 2 ? `inset(0 0 ${100 - k * 100}% 0)` : `inset(${100 - k * 100}% 0 0 0)`;
638
+ circulo = <div style={{ position: 'absolute', inset: 0, background: b.cores.fundo, clipPath: ins }} />;
639
+ }
640
+ }
641
+ const claro = (() => { const n = parseInt(corBase.slice(1), 16); return ((n >> 16 & 255) * 0.299 + (n >> 8 & 255) * 0.587 + (n & 255) * 0.114) / 255 > 0.5; })();
642
+ const linhaCor = alfa(b.cores.texto, claro ? 0.07 : 0.075);
643
+ const passo = f.passo * (1 + (cam.zoom - 1) * 0.35);
644
+ const px = (-cam.x * 0.35 + t * 6) % passo, py = (-cam.y * 0.35 + t * 3) % passo;
645
+ const textura = f.textura === 'grade'
646
+ ? { backgroundImage: 'linear-gradient(' + linhaCor + ' 1px, transparent 1px), linear-gradient(90deg, ' + linhaCor + ' 1px, transparent 1px)' }
647
+ : f.textura === 'pontos' ? { backgroundImage: 'radial-gradient(' + alfa(b.cores.texto, 0.16) + ' 1.2px, transparent 1.6px)' }
648
+ : { backgroundImage: 'linear-gradient(' + linhaCor + ' 1px, transparent 1px)' };
649
+ const bx = (f.brilho.x + 0.12 * Math.sin(t * 0.23)) * 100, by = (f.brilho.y + 0.1 * Math.cos(t * 0.19)) * 100;
650
+ return (
651
+ <div style={{ position: 'absolute', inset: 0, background: corBase, overflow: 'hidden' }}>
652
+ {circulo}
653
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />
654
+ {(f.particulas || []).map((q, i) => {
655
+ const x = ((q.x - cam.x * q.z * 1.6 + t * q.v * (i % 2 ? 1 : -0.6)) % W + W) % W;
656
+ const y = ((q.y - cam.y * q.z * 1.6 - t * q.v * 0.7) % H + H) % H;
657
+ const sc = 1 + (cam.zoom - 1) * q.z * 3;
658
+ return <div key={'q' + i} style={{ position: 'absolute', left: x, top: y, width: q.r * 2 * sc, height: q.r * 2 * sc, borderRadius: '50%',
659
+ background: i % 5 === 0 ? b.cores.acento : b.cores.texto, opacity: (0.1 + q.z * 0.25), filter: q.z < 0.3 ? 'blur(1.5px)' : undefined }} />;
660
+ })}
661
+ <div style={{ position: 'absolute', inset: 0, ...textura, backgroundSize: passo + 'px ' + passo + 'px', backgroundPosition: px + 'px ' + py + 'px',
662
+ WebkitMaskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)', maskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)' }} />
663
+ </div>
664
+ );
665
+ }
666
+
667
+ // ── o HERÓI GRÁFICO: a ideia visual grande de cada batida ───────────────
668
+ const TAU = Math.PI * 2;
669
+ function formaPoligono(n, estrela, R, cx, cy, giro, N = 60) {
670
+ const v = [];
671
+ const lados = estrela ? n * 2 : n;
672
+ for (let i = 0; i < lados; i++) { const a = giro + (i / lados) * TAU - Math.PI / 2, r = estrela && i % 2 ? R * 0.45 : R; v.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); }
673
+ const pts = [];
674
+ for (let k = 0; k < N; k++) {
675
+ const u = (k / N) * lados, i = Math.floor(u), f = u - i, a = v[i % lados], b2 = v[(i + 1) % lados];
676
+ pts.push([lerp(a[0], b2[0], f), lerp(a[1], b2[1], f)]);
677
+ }
678
+ return pts;
679
+ }
680
+ const caminhoFechado = (pts) => 'M' + pts.map((q) => q[0].toFixed(1) + ' ' + q[1].toFixed(1)).join('L') + 'Z';
681
+
682
+ function Heroi({ b, t, P, slot }) {
683
+ const h = b.heroi;
684
+ if (!h) return null;
685
+ const W = P.largura, H = P.altura, base = Math.min(W, H) / 1080;
686
+ const tl = t - b.de, dur = Math.max(0.8, b.ate - b.de);
687
+ const k = ed(c01(tl / 0.7));
688
+ const sem = h.semente;
689
+ const cor = h.cor, cor2 = h.cor2;
690
+ const { cx, cy, R, fraco } = slot;
691
+ const op = fraco ? 0.32 : 1;
692
+ const els = [];
693
+ if (h.tipo === 'explosao') {
694
+ // A explosão radial: nasce de um ponto, os raios saem em mola e giram devagar; anéis de régua em volta.
695
+ const n = 10 + (sem % 7), giro = tl * 0.25 + (sem % 10) * 0.3;
696
+ for (let i = 0; i < n; i++) {
697
+ const ki = MOLA(tl - 0.12 - i * 0.025), a = giro + (i / n) * TAU, r1 = R * (0.62 + 0.38 * acaso(sem + i)) * ki * (1 + 0.04 * Math.sin(t * 3 + i));
698
+ els.push(<line key={'r' + i} x1={cx + Math.cos(a) * R * 0.14} y1={cy + Math.sin(a) * R * 0.14} x2={cx + Math.cos(a) * r1} y2={cy + Math.sin(a) * r1} stroke={cor} strokeWidth={R * 0.1} strokeLinecap="round" />);
699
+ }
700
+ els.push(<circle key="c" cx={cx} cy={cy} r={R * 0.16 * Math.min(1, tl * 6)} fill={cor} />);
701
+ els.push(<circle key="a1" cx={cx} cy={cy} r={R * 1.25 * k} fill="none" stroke={cor2} strokeOpacity={0.28} strokeWidth={1.5 * base} />);
702
+ els.push(<circle key="a2" cx={cx} cy={cy} r={R * 1.55 * ed(c01((tl - 0.15) / 0.8))} fill="none" stroke={cor2} strokeOpacity={0.22} strokeWidth={1.2 * base} strokeDasharray={`${2 * base} ${9 * base}`} transform={`rotate(${-tl * 12} ${cx} ${cy})`} />);
703
+ } else if (h.tipo === 'poligono') {
704
+ // O polígono que se transforma: triângulo → estrela → quadrado, com partículas em órbita.
705
+ const seq = [[3, false], [5, true], [4, false], [6, true]];
706
+ const u = c01(tl / dur) * 3, i = Math.min(2, Math.floor(u)), f = ed(c01((u - i) * 2.2 - 0.6));
707
+ const giro = tl * 0.35;
708
+ const A = formaPoligono(seq[i][0], seq[i][1], R * 0.62 * k, cx, cy, giro), B2 = formaPoligono(seq[i + 1][0], seq[i + 1][1], R * 0.62 * k, cx, cy, giro);
709
+ const pts = A.map((q, j) => [lerp(q[0], B2[j][0], f), lerp(q[1], B2[j][1], f)]);
710
+ els.push(<path key="g" d={caminhoFechado(formaPoligono(seq[i + 1][0], seq[i + 1][1], R * 0.7 * k, cx, cy, -giro * 1.4))} fill="none" stroke={cor} strokeOpacity={0.55} strokeWidth={1.4 * base} />);
711
+ els.push(<path key="p" d={caminhoFechado(pts)} fill={cor2} />);
712
+ const n = 12;
713
+ for (let j = 0; j < n; j++) {
714
+ const a = (j / n) * TAU + tl * 0.6, r = R * 1.18 * k;
715
+ const x = cx + Math.cos(a) * r, y = cy + Math.sin(a) * r, s2 = R * 0.045;
716
+ els.push(j % 3 ? <circle key={'o' + j} cx={x} cy={y} r={s2} fill={cor2} /> : <path key={'o' + j} d={caminhoFechado(formaPoligono(3, false, s2 * 1.6, x, y, a))} fill={cor} />);
717
+ }
718
+ } else if (h.tipo === 'esfera' || h.tipo === 'toro') {
719
+ // A malha de pontos em 3D: esfera ou toro girando, o ponto mais perto maior e mais forte.
720
+ const pts = [];
721
+ const ry = tl * 0.7 + sem, rx = 0.45 + Math.sin(tl * 0.4) * 0.2;
722
+ const add = (x, y, z, ac) => {
723
+ const x1 = x * Math.cos(ry) + z * Math.sin(ry), z1 = -x * Math.sin(ry) + z * Math.cos(ry);
724
+ const y2 = y * Math.cos(rx) - z1 * Math.sin(rx), z2 = y * Math.sin(rx) + z1 * Math.cos(rx);
725
+ const f = 3.2 / (3.2 + z2);
726
+ pts.push({ x: cx + x1 * R * f * k, y: cy + y2 * R * f * k, z: z2, f, ac });
727
+ };
728
+ if (h.tipo === 'esfera') { for (let i = 1; i < 16; i++) for (let j = 0; j < 26; j++) { const th = (i / 16) * Math.PI, ph = (j / 26) * TAU; add(Math.sin(th) * Math.cos(ph), Math.cos(th), Math.sin(th) * Math.sin(ph), (i * 26 + j) % 11 === 0); } }
729
+ else { for (let i = 0; i < 30; i++) for (let j = 0; j < 14; j++) { const u = (i / 30) * TAU, v = (j / 14) * TAU; add((0.72 + 0.3 * Math.cos(v)) * Math.cos(u), 0.3 * Math.sin(v), (0.72 + 0.3 * Math.cos(v)) * Math.sin(u), (i + j) % 9 === 0); } }
730
+ pts.sort((a, b2) => b2.z - a.z);
731
+ pts.forEach((q, j) => els.push(<circle key={j} cx={q.x} cy={q.y} r={Math.max(0.6, 2.6 * base * q.f)} fill={q.ac ? cor : cor2} opacity={0.25 + 0.75 * c01((1 - q.z) / 2)} />));
732
+ } else if (h.tipo === 'truchet') {
733
+ // O campo de truchet: arcos em azulejo e uma onda de cor que atravessa na diagonal.
734
+ const T = Math.round(Math.min(W, H) / 7.5), cols = Math.ceil(W / T) + 1, rows = Math.ceil(H / T) + 1;
735
+ const onda = (tl * 7) % (cols + rows + 8) - 4;
736
+ let dBase = '', dOnda = '';
737
+ for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
738
+ if ((i + j) * 0.9 > tl * 22) continue;
739
+ const x = i * T, y = j * T, r = T / 2, o = acaso(sem + i * 31 + j * 17) > 0.5;
740
+ const d = o ? `M${x + r} ${y}A${r} ${r} 0 0 1 ${x} ${y + r}M${x + T} ${y + r}A${r} ${r} 0 0 0 ${x + r} ${y + T}`
741
+ : `M${x + r} ${y}A${r} ${r} 0 0 0 ${x + T} ${y + r}M${x} ${y + r}A${r} ${r} 0 0 1 ${x + r} ${y + T}`;
742
+ if (Math.abs(i + j - onda) < 2.2) dOnda += d; else dBase += d;
743
+ }
744
+ els.push(<path key="b" d={dBase} fill="none" stroke={cor2} strokeOpacity={0.16} strokeWidth={T * 0.16} strokeLinecap="round" />);
745
+ els.push(<path key="o" d={dOnda} fill="none" stroke={cor} strokeOpacity={0.8} strokeWidth={T * 0.16} strokeLinecap="round" />);
746
+ } else if (h.tipo === 'aneis') {
747
+ // Anéis de órbita inclinados, cada um com o seu satélite.
748
+ for (let j = 0; j < 3; j++) {
749
+ const rx2 = R * (1.05 + j * 0.38) * k, ry2 = rx2 * (0.3 + 0.08 * j), ang = (sem % 40) - 20 + j * 28 + tl * (6 + j * 3);
750
+ const a = tl * (0.9 - j * 0.2) + j * 2;
751
+ els.push(<g key={'a' + j} transform={`rotate(${ang.toFixed(2)} ${cx} ${cy})`}>
752
+ <ellipse cx={cx} cy={cy} rx={rx2} ry={ry2} fill="none" stroke={cor2} strokeOpacity={0.3} strokeWidth={1.6 * base} />
753
+ <circle cx={cx + Math.cos(a) * rx2} cy={cy + Math.sin(a) * ry2} r={6 * base} fill={j === 1 ? cor : cor2} />
754
+ </g>);
755
+ }
756
+ } else if (h.tipo === 'grade') {
757
+ // A grade que deforma: uma lente passa e entorta as linhas.
758
+ const G = Math.round(Math.min(W, H) / 12), bx = lerp(-0.2 * W, 1.2 * W, (tl / dur) % 1), by = H * (0.35 + 0.3 * Math.sin(tl * 0.7));
759
+ const warp = (x, y) => { const d2 = ((x - bx) ** 2 + (y - by) ** 2) / (0.09 * W * W); const e = Math.exp(-d2) * 60 * base; return [x + (x - bx) * e / 300, y + (y - by) * e / 300]; };
760
+ let d = '';
761
+ for (let x = 0; x <= W + G; x += G) { d += 'M'; for (let y = 0; y <= H; y += 40) { const q = warp(x, y); d += q[0].toFixed(1) + ' ' + q[1].toFixed(1) + (y + 40 <= H ? 'L' : ''); } }
762
+ for (let y = 0; y <= H + G; y += G) { d += 'M'; for (let x = 0; x <= W; x += 40) { const q = warp(x, y); d += q[0].toFixed(1) + ' ' + q[1].toFixed(1) + (x + 40 <= W ? 'L' : ''); } }
763
+ els.push(<path key="g" d={d} fill="none" stroke={cor2} strokeOpacity={0.14 * k} strokeWidth={1.2 * base} />);
764
+ els.push(<circle key="l" cx={bx} cy={by} r={7 * base} fill={cor} opacity={k} />);
765
+ } else if (h.tipo === 'rastro') {
766
+ // Rastros de velocidade cruzando o quadro.
767
+ for (let j = 0; j < 16; j++) {
768
+ const y = H * acaso(sem + j * 7), len = W * (0.15 + 0.45 * acaso(sem + j * 11)), v = W * (1.2 + acaso(j * 13 + 1) * 1.6);
769
+ const x = ((tl * v + acaso(sem + j * 3) * W * 2) % (W * 2 + len)) - len;
770
+ els.push(<rect key={j} x={x} y={y} width={len} height={(1.5 + 4 * acaso(j * 5 + sem)) * base} rx={2} fill={j % 4 === 0 ? cor : cor2} opacity={(j % 4 === 0 ? 0.7 : 0.18) * k} />);
771
+ }
772
+ }
773
+ return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', opacity: op }}>{els}</svg>;
774
+ }
775
+
776
+ /* O HUD do motion designer, em toda batida: cantos de enquadramento, a marca,
777
+ a seção, o timecode, os quadros por segundo e a régua do vídeo. */
778
+ const SECOES = { gigante: 'TESE', pilha: 'ARGUMENTO', divisao: 'CONTRASTE', contador: 'NÚMERO', cascata: 'LISTA', parede: 'TEXTURA', citacao: 'CITAÇÃO', marca: 'MARCA', palavras: 'FALA', produto: 'PRODUTO' };
779
+ function HudG({ P, b, bi, t, fps }) {
780
+ const W = P.largura, H = P.altura, m = Math.round(Math.min(W, H) * 0.035), tam = Math.max(11, Math.round(Math.min(W, H) * 0.0125));
781
+ const cor = alfa(b.cores.texto, 0.72);
782
+ const f = Math.round(t * fps);
783
+ const tc = [Math.floor(t / 60), Math.floor(t) % 60, f % fps].map((n) => String(n).padStart(2, '0')).join(':');
784
+ const rot = { position: 'absolute', fontFamily: familia(P.fontes.mono), fontSize: tam, letterSpacing: '0.14em', color: cor, whiteSpace: 'nowrap', textTransform: 'uppercase' };
785
+ const canto = (x, y, sx, sy) => <div key={x + '-' + y} style={{ position: 'absolute', left: x - (sx < 0 ? m * 0.7 : 0), top: y - (sy < 0 ? m * 0.7 : 0), width: m * 0.7, height: m * 0.7,
786
+ borderLeft: sx > 0 ? '1.5px solid ' + cor : undefined, borderRight: sx < 0 ? '1.5px solid ' + cor : undefined, borderTop: sy > 0 ? '1.5px solid ' + cor : undefined, borderBottom: sy < 0 ? '1.5px solid ' + cor : undefined }} />;
787
+ return (
788
+ <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
789
+ {canto(m, m, 1, 1)}{canto(W - m, m, -1, 1)}{canto(m, H - m, 1, -1)}{canto(W - m, H - m, -1, -1)}
790
+ <div style={{ ...rot, left: m * 1.5, top: m * 1.1 }}><b>{P.hud.marca}</b><span style={{ opacity: 0.6 }}>{' · MOTION · ' + P.hud.ano}</span></div>
791
+ <div style={{ ...rot, right: m * 1.5, top: m * 1.1 }}>{String(bi + 1).padStart(2, '0') + ' — ' + (SECOES[b.comp] || b.comp.toUpperCase())}</div>
792
+ <div style={{ ...rot, left: m * 1.5, bottom: m * 1.1 }}>{tc}<span style={{ opacity: 0.55 }}>{' ' + fps + ' FPS'}</span></div>
793
+ <div style={{ ...rot, right: m * 1.5, bottom: m * 1.1 }}>{W + '×' + H}</div>
794
+ <div style={{ position: 'absolute', left: m * 1.5, bottom: m * 0.62, height: 1.5, width: (W - m * 3) * c01(t / P.duracao), background: cor }} />
795
+ </div>
796
+ );
797
+ }
798
+
799
+ // ── o componente ────────────────────────────────────────────────────────
800
+ export function Gerativo({ el, t, fps }) {
801
+ const P = el.partitura;
802
+ const med = useMedidas(P);
803
+ const L = useMemo(() => (med ? leiaute(P, med) : null), [med]);
804
+ const estados = useMemo(() => (L ? estadosDoAcento(P, L) : null), [L]);
805
+ const W = P.largura, H = P.altura;
806
+ if (!L) return <div style={{ position: 'absolute', inset: 0, background: P.paleta.fundo }} />;
807
+ const frame = Math.round(t * fps);
808
+ const bs = P.batidas;
809
+ let bi = 0;
810
+ while (bi < bs.length - 1 && t >= bs[bi + 1].de) bi++;
811
+ const b = bs[bi];
812
+ const cam0 = cameraEm(t, P.camera);
813
+ /* A câmera NUNCA para: por cima das chaves, uma deriva de mão; e nos
814
+ golpes (a tese que bate, o corte) ela treme e soca. */
815
+ const base = Math.min(W, H) / 1080;
816
+ // A deriva é lenta e longa (inércia); o tremor só no golpe, e dura no máximo 6 quadros.
817
+ let sx = Math.sin(t * 0.43) * 0.006 * W, sy = Math.cos(t * 0.37) * 0.005 * H, sr = Math.sin(t * 0.29) * 0.25, sz = 0;
818
+ for (const g of b.golpes || []) {
819
+ const d = t - g.t;
820
+ if (d < 0 || d > 6 / fps) continue;
821
+ const k = g.forca * (1 - d / (6 / fps));
822
+ sx += Math.sin(d * 70) * 9 * base * k; sy += Math.cos(d * 60) * 6 * base * k; sz += 0.02 * k;
823
+ }
824
+ /* O PULSO: nada assenta de vez — o mundo respira (escala lenta) e bate de
825
+ leve a cada meio segundo, como um bumbo. */
826
+ // A respiração lenta do mundo (sem bumbo: o pulso batendo lia como tranco).
827
+ sz += 0.008 * Math.sin((t * Math.PI * 2) / 3.4);
828
+ const cam = { x: cam0.x + sx, y: cam0.y + sy, zoom: cam0.zoom * (1 + sz), rot: cam0.rot + sr };
829
+ const ac0 = acentoEm(t, estados);
830
+ // O acento também vive: treme de leve e respira na espessura.
831
+ const ac = { ...ac0, pts: ac0.pts.map((q, i) => [q[0] + Math.sin(t * 3.1 + i * 0.5) * 1.6 * base, q[1] + Math.cos(t * 2.7 + i * 0.37) * 1.6 * base]), larg: ac0.larg * (1 + 0.08 * Math.sin(t * 4.2)) };
832
+ // O ponto de onde a inundação nasce: o acento, visto pela câmera.
833
+ const tela = (x, y) => {
834
+ const r = (cam.rot * Math.PI) / 180, dx = x - W / 2 - cam.x, dy = y - H / 2 - cam.y;
835
+ return { x: W / 2 + cam.zoom * (dx * Math.cos(r) - dy * Math.sin(r)), y: H / 2 + cam.zoom * (dx * Math.sin(r) + dy * Math.cos(r)) };
836
+ };
837
+ const origem = tela(ac.cx, ac.cy);
838
+ /* O ESPAÇO 3D: o mundo tem perspectiva, a câmera também inclina (rotateX/Y)
839
+ e as camadas estão em profundidades diferentes — o texto não é adesivo. */
840
+ const rx = Math.cos(t * 0.27) * 1.4, ry = Math.sin(t * 0.31) * 2 + cam.rot * 0.6;
841
+ const mundo = 'translate(' + W / 2 + 'px,' + H / 2 + 'px) rotate(' + cam.rot.toFixed(3) + 'deg) rotateX(' + rx.toFixed(3) + 'deg) rotateY(' + ry.toFixed(3) + 'deg) scale(' + cam.zoom.toFixed(4) + ') translate(' + (-W / 2 - cam.x).toFixed(1) + 'px,' + (-H / 2 - cam.y).toFixed(1) + 'px)';
842
+ // No ar: a batida atual, a anterior enquanto sai.
843
+ const noAr = bs.filter((x, i) => i === bi || (i === bi - 1 && t < x.ate + 0.14 && x.saida !== 'corte'));
844
+ const traco = (
845
+ <svg key="acento" width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible' }}>
846
+ <path d={suave(ac.pts)} fill="none" stroke={ac.cor} strokeWidth={Math.max(0.1, ac.larg)} strokeLinecap={ac.tampa || 'round'} strokeLinejoin="round" opacity={ac.alfa} />
847
+ </svg>
848
+ );
849
+ // Onde as letras que viram pontos vão parar: a viagem do acento na próxima batida.
850
+ const alvoDe = (x) => {
851
+ const prox = estados.find((e) => e.batida === bs.indexOf(x) + 1);
852
+ return prox ? { x: prox.g.cx, y: prox.g.cy } : null;
853
+ };
854
+ const produtos = (P.produtos || []).filter((q) => t >= q.de - 0.1 && t <= q.ate + 0.35);
855
+ return (
856
+ <div style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
857
+ <Fundo P={P} b={b} prevB={bs[bi - 1]} t={t} cam={cam} frame={frame} origem={origem} />
858
+ <div style={{ position: 'absolute', inset: 0, perspective: Math.round(Math.max(W, H) * 0.85) + 'px', perspectiveOrigin: '50% 48%' }}>
859
+ <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transformOrigin: '0 0', transform: mundo, transformStyle: 'preserve-3d' }}>
860
+ {produtos.map((q, i) => {
861
+ const sai = c01((t - q.ate) / 0.3), entra = ed(c01((t - q.de) / 0.6));
862
+ return (
863
+ <div key={'p' + i} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: 1 - sai, transform: 'scale(' + (0.94 + 0.06 * entra + 0.1 * sai) + ')', transformOrigin: (q.x + q.largura / 2) + 'px ' + (q.y + q.altura / 2) + 'px' }}>
864
+ <Produto3D el={{ x: q.x, y: q.y, largura: q.largura, altura: q.altura, modelo: q.modelo, cor: q.cor, corAcento: q.corAcento, t: q.de + 0.05, giro: 0.45, escala: q.escala || 1, semChao: true }} t={t} />
865
+ </div>
866
+ );
867
+ })}
868
+ {!ac.frente && traco}
869
+ {noAr.map((x) => {
870
+ const lay = L[x.id];
871
+ const ds = DUR_SAIDA[x.saida] || 0;
872
+ const te = ds ? c01((t - (x.ate - ds)) / ds) : 0;
873
+ let estilo = {};
874
+ if (x.saida === 'atravessa' && te > 0) {
875
+ estilo = { transform: 'scale(' + (1 + 4.5 * entradaC(te)).toFixed(3) + ')', opacity: 1 - c01((te - 0.45) / 0.55), filter: 'blur(' + (te * 10).toFixed(1) + 'px)' };
876
+ } else if (x.saida === 'chicote' && te > 0) {
877
+ estilo = { transform: 'translateX(' + (-0.25 * W * entradaC(te)).toFixed(1) + 'px) skewX(' + (-6 * te).toFixed(2) + 'deg)', filter: 'blur(' + (te * 12).toFixed(1) + 'px)', opacity: 1 - c01((te - 0.5) / 0.5) };
878
+ } else if (x.saida === 'inunda' && te > 0) estilo = { opacity: 1 - te, transform: 'scale(' + (1 - 0.04 * te) + ')' };
879
+ else if (x.saida === 'corte' && t >= x.ate) return null;
880
+ if (x.entrada === 'chicote' && t < x.de + 0.32) {
881
+ const k = ed(c01((t - x.de) / 0.32));
882
+ estilo = { ...estilo, transform: 'translateX(' + (0.2 * W * (1 - k)).toFixed(1) + 'px) skewX(' + (5 * (1 - k)).toFixed(2) + 'deg)', filter: 'blur(' + ((1 - k) * 10).toFixed(1) + 'px)' };
883
+ }
884
+ if (x.entrada === 'atravessa' && t < x.de + 0.4) {
885
+ const k = ed(c01((t - x.de) / 0.4));
886
+ estilo = { ...estilo, transform: 'scale(' + (0.82 + 0.18 * k).toFixed(4) + ')', filter: 'blur(' + ((1 - k) * 8).toFixed(1) + 'px)' };
887
+ }
888
+ const alvo = x.saida === 'pontos' ? alvoDe(x) : null;
889
+ /* O DOLLY: a batida chega do fundo do espaço, a câmera avança devagar
890
+ nela, e na saída ela vem para cima da lente e passa — atravessamos. */
891
+ const zEntra = x.entrada === 'corte' ? 0 : -340 * (1 - ed(c01((t - x.de) / 0.65)));
892
+ const zSai = ds && x.saida !== 'corte' && x.saida !== 'inunda' ? 560 * entradaC(te) : 0;
893
+ const zB = zEntra + 110 * c01((t - x.de) / Math.max(0.6, x.ate - x.de)) + zSai;
894
+ const tz = 'translateZ(' + zB.toFixed(1) + 'px)';
895
+ return (
896
+ <div key={x.id} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transformOrigin: (W / 2) + 'px ' + (H / 2) + 'px', transformStyle: estilo.filter ? undefined : 'preserve-3d',
897
+ ...estilo, transform: [tz, estilo.transform].filter(Boolean).join(' '), opacity: (estilo.opacity != null ? estilo.opacity : 1) * (zSai ? 1 - c01((te - 0.55) / 0.45) : 1) }}>
898
+ {x.heroi && (() => {
899
+ // O lugar do herói: o vazio do bloco (como o ícone); sem vazio, atrás do texto e mais fraco.
900
+ const bl = lay.bloco, M = P.margem;
901
+ if (x.heroi.modo === 'campo') return <Heroi b={x} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.32 }} />;
902
+ const R = Math.min(W, H) * (W >= H ? 0.2 : 0.17);
903
+ let slot = null;
904
+ if (W - M - bl.x1 >= R * 2.3) slot = { cx: bl.x1 + (W - M - bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R };
905
+ else if (bl.y - M * 1.4 >= R * 2.2) slot = { cx: W / 2, cy: M * 1.4 + (bl.y - M * 1.4) / 2, R };
906
+ else if (H - M * 1.4 - bl.y1 >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + (H - M * 1.4 - bl.y1) / 2, R };
907
+ else return <Heroi b={{ ...x, heroi: { ...x.heroi, tipo: 'aneis' } }} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.3 }} />;
908
+ return <Heroi b={x} t={t} P={P} slot={slot} />;
909
+ })()}
910
+ <Textura b={x} cx={lay.caixas[x.linhas[0].id]} t={t} P={P} periodo={lay.textura} />
911
+ {x.icone3d && (() => {
912
+ /* O ícone 3D mora no vazio do bloco: à direita (deitado) ou acima
913
+ (em pé); sem espaço, não entra — nunca por cima do texto. */
914
+ const ic = x.icone3d, tam = ic.tamanho, M = P.margem, bl = lay.bloco;
915
+ let px = null, py = null;
916
+ if (ic.lado === 'direita' && W - M - bl.x1 >= tam * 0.9) { px = Math.min(W - M - tam, bl.x1 + (W - M - bl.x1 - tam) / 2); py = (bl.y + bl.y1) / 2 - tam / 2; }
917
+ else if (bl.y - M * 1.2 >= tam * 0.85) { px = x.bloco.alinha === 'centro' ? (bl.x + bl.x1) / 2 - tam / 2 : bl.x - tam * 0.08; py = Math.max(M, bl.y - tam - 0.02 * H); }
918
+ if (px == null) return null;
919
+ return <div style={{ position: 'absolute', inset: 0, transform: 'translateZ(150px)' }}><Icone3D el={{ ...ic, x: Math.round(px), y: Math.round(py) }} t={t} /></div>;
920
+ })()}
921
+ {x.linhas.map((l, li) => {
922
+ const cx = lay.caixas[l.id];
923
+ if (t < l.t - 0.2) return null;
924
+ let eLinha = {};
925
+ if (x.saida === 'desliza') {
926
+ const tl = c01((t - (x.ate - DUR_SAIDA.desliza) - li * 3 / fps) / (DUR_SAIDA.desliza * 0.7));
927
+ if (tl > 0) eLinha = { transform: 'translateY(' + (-0.5 * ed(tl) * cx.s).toFixed(1) + 'px)', opacity: 1 - tl, filter: 'blur(' + (tl * 4).toFixed(1) + 'px)' };
928
+ }
929
+ /* Cada linha numa PROFUNDIDADE: o rótulo perto da lente, o apoio
930
+ atrás, a tese no foco — e a lente desfoca o que não é a tese.
931
+ A linha entra deitada (rotateX) e balança no eixo Y. */
932
+ const zL = { rotulo: 120, aspas: 170, apoio: -120, fraco: -150, numero: 40, item: -35 * (li % 3) }[l.papel] || 0;
933
+ const deita = (1 - ed(c01((t - l.t) / 0.55))) * 32;
934
+ const balanco = Math.sin(t * 0.42 + li * 0.9) * 1.4;
935
+ const dof = Math.min(1.1, Math.abs(zL) / 140) * 0.9;
936
+ const origemL = (cx.x + cx.w / 2).toFixed(0) + 'px ' + (cx.y + cx.h / 2).toFixed(0) + 'px';
937
+ const t3 = 'translateZ(' + zL + 'px) rotateX(' + deita.toFixed(2) + 'deg) rotateY(' + balanco.toFixed(2) + 'deg)';
938
+ eLinha = { ...eLinha, transform: [t3, eLinha.transform].filter(Boolean).join(' '), transformOrigin: origemL,
939
+ filter: [dof > 0.05 ? 'blur(' + dof.toFixed(2) + 'px)' : '', eLinha.filter || ''].filter(Boolean).join(' ') || undefined };
940
+ return (
941
+ <div key={l.id} style={{ position: 'absolute', inset: 0, ...eLinha }}>
942
+ <Linha l={l} b={x} cx={cx} t={t} fps={fps} P={P} frame={frame} alvoSaida={alvo} centroBloco={{ x: (lay.bloco.x + lay.bloco.x1) / 2, y: (lay.bloco.y + lay.bloco.y1) / 2 }} />
943
+ </div>
944
+ );
945
+ })}
946
+ </div>
947
+ );
948
+ })}
949
+ {ac.frente && traco}
950
+ </div>
951
+ </div>
952
+ {/* A FRENTE: manchas de luz que passam perto da lente (bokeh) e o feixe
953
+ de luz que varre o quadro quando uma batida chega. */}
954
+ {[0, 1, 2, 3].map((k) => {
955
+ const per = 5 + acaso(P.semente % 997 + k * 7) * 4, fase = acaso(P.semente % 991 + k * 13) * per;
956
+ const u = ((t + fase) % per) / per;
957
+ const tam = Math.min(W, H) * (0.28 + acaso(k * 17 + 3) * 0.3);
958
+ const x = lerp(-0.35 * W, 1.35 * W, u) - cam.x * 1.8, y = H * (0.15 + acaso(k * 29 + 5) * 0.7) - cam.y * 1.8;
959
+ const cor = k % 2 ? b.cores.acento : b.cores.texto;
960
+ return <div key={'bk' + k} style={{ position: 'absolute', left: x - tam / 2, top: y - tam / 2, width: tam, height: tam, borderRadius: '50%', pointerEvents: 'none',
961
+ background: 'radial-gradient(circle, ' + alfa(cor, k % 2 ? 0.16 : 0.08) + ' 0%, transparent 66%)', opacity: Math.sin(Math.PI * u) }} />;
962
+ })}
963
+ {(() => {
964
+ const k = c01((t - b.de) / 0.95);
965
+ if (k <= 0 || k >= 1) return null;
966
+ const x = lerp(-0.7 * W, 1.2 * W, ed(k));
967
+ const claro = parseInt(b.cores.fundo.slice(1, 3), 16) > 160;
968
+ return <div style={{ position: 'absolute', top: -H * 0.2, height: H * 1.4, left: x, width: W * 0.45, pointerEvents: 'none', transform: 'rotate(14deg)',
969
+ background: 'linear-gradient(90deg, transparent, ' + (claro ? 'rgba(255,255,255,0.55)' : 'rgba(255,255,255,0.12)') + ', transparent)', mixBlendMode: claro ? 'soft-light' : 'screen', opacity: Math.sin(Math.PI * k) }} />;
970
+ })()}
971
+ {P.hud && <HudG P={P} b={b} bi={bi} t={t} fps={fps} />}
972
+ {/* O acabamento: grão que muda a cada quadro e vinheta suave. */}
973
+ <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', backgroundImage: GRAO, backgroundSize: '220px 220px',
974
+ backgroundPosition: Math.floor(acaso(frame) * 220) + 'px ' + Math.floor(acaso(frame + 7) * 220) + 'px', opacity: P.fundo.grao * 1.6, mixBlendMode: 'overlay' }} />
975
+ <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', background: 'radial-gradient(ellipse at 50% 45%, transparent 55%, rgba(0,0,0,' + (parseInt(b.cores.fundo.slice(1, 3), 16) > 160 ? 0.06 : 0.18) + ') 100%)' }} />
976
+ </div>
977
+ );
978
+ }
979
+
980
+ export const GERATIVO = { gerativo: Gerativo };