primocode 10.0.9 → 10.2.0

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 (46) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +25 -28
  3. package/lib/act.js +5 -2
  4. package/lib/catalogo.js +7 -4
  5. package/lib/config.js +10 -2
  6. package/lib/estudio/assets.js +86 -6
  7. package/lib/estudio/batida.js +184 -0
  8. package/lib/estudio/biblioteca.js +214 -0
  9. package/lib/estudio/cinema.js +37 -17
  10. package/lib/estudio/composicao.js +2 -1
  11. package/lib/estudio/decisao.js +106 -0
  12. package/lib/estudio/edicao.js +36 -17
  13. package/lib/estudio/estilo.js +35 -8
  14. package/lib/estudio/fluido.js +352 -0
  15. package/lib/estudio/gerativo.js +21 -3
  16. package/lib/estudio/hibrido.js +16 -12
  17. package/lib/estudio/impacto.js +66 -31
  18. package/lib/estudio/index.js +245 -42
  19. package/lib/estudio/kit/fluido.jsx +614 -0
  20. package/lib/estudio/kit/gerativo.jsx +15 -5
  21. package/lib/estudio/kit/index.jsx +2 -1
  22. package/lib/estudio/kit/tres.jsx +88 -21
  23. package/lib/estudio/kit.js +3 -1
  24. package/lib/estudio/kokoro.js +111 -0
  25. package/lib/estudio/lancamento.js +3 -4
  26. package/lib/estudio/legibilidade.js +20 -0
  27. package/lib/estudio/referencias.js +36 -25
  28. package/lib/estudio/remotion.js +53 -6
  29. package/lib/estudio/revisor.js +117 -0
  30. package/lib/estudio/variacao3d.js +46 -0
  31. package/lib/estudio/variedade.js +140 -8
  32. package/lib/portao-skill.js +150 -0
  33. package/lib/skills.js +15 -1
  34. package/lib/tools.js +31 -5
  35. package/lib/video/filters.js +6 -1
  36. package/lib/video/ops.js +4 -3
  37. package/lib/video/timeline.js +1 -1
  38. package/lib/video/zoom.js +275 -0
  39. package/package.json +2 -2
  40. package/skills/escrever-em-portugues/SKILL.md +3 -0
  41. package/skills/interface-revisada/SKILL.md +7 -1
  42. package/skills/motion-de-primeira/SKILL.md +3 -5
  43. package/skills/projeto-no-github/SKILL.md +6 -0
  44. package/skills/publicar-na-internet/SKILL.md +3 -0
  45. package/skills/site-com-motion/SKILL.md +2 -0
  46. package/studio/voz_kokoro.py +98 -0
@@ -0,0 +1,614 @@
1
+ /* kit/fluido.jsx — o estilo fluido (lib/estudio/fluido.js), quadro a quadro.
2
+ *
3
+ * Uma peça só desenha a partitura inteira. Antes do primeiro quadro as fontes
4
+ * carregam e cada palavra é MEDIDA (canvas): é daí que sai o tamanho da
5
+ * pílula, do anel e do cartão. Depois, tudo é função do tempo:
6
+ *
7
+ * o PORTADOR uma forma que vive o vídeo todo e muda de lugar, tamanho,
8
+ * raio e preenchimento entre as batidas (ponto → pílula → anel
9
+ * → linha → cartão → marca). É ele que tira o corte.
10
+ * a PALAVRA entra no instante em que a voz a diz (partitura: w.em), sobe
11
+ * meio corpo e sai do desfoque, na curva editorial. Uma por
12
+ * frase em serifa itálica, na cor de acento.
13
+ * o CARTÃO a interface de produto que flutua do outro lado do texto, em
14
+ * perspectiva: terminal, chat ou painel — de forma e texto.
15
+ * o FUNDO gradiente grande e macio; o brilho segue o portador devagar.
16
+ *
17
+ * Nada fica parado: a câmera avança e deriva, o portador respira, o cartão
18
+ * flutua. Sem HUD, sem cantoneira, sem timecode, sem partícula, sem caixa alta. */
19
+ import React, { useState, useEffect, useMemo } from 'react';
20
+ import { delayRender, continueRender, Img, staticFile } from 'remotion';
21
+ import { CURVAS_KIT, alfa } from './base';
22
+
23
+ const ed = CURVAS_KIT.editorial, cam = CURVAS_KIT.camera;
24
+ const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
25
+ const lerp = (a, b, k) => a + (b - a) * k;
26
+ const lum = (hex) => { const n = parseInt(String(hex).slice(1), 16); return (0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255)) / 255; };
27
+ const entra = (t) => t * t;
28
+ const SANS = (f) => '"' + f + '", system-ui, sans-serif';
29
+ const SERIF = (f) => '"' + f + '", Georgia, serif';
30
+ const MONO = (f) => '"' + f + '", ui-monospace, monospace';
31
+ const BRILHO4 = 'M 0 -1 C 0.12 -0.12 0.12 -0.12 1 0 C 0.12 0.12 0.12 0.12 0 1 C -0.12 0.12 -0.12 0.12 -1 0 C -0.12 -0.12 -0.12 -0.12 0 -1 Z';
32
+
33
+ // ── a medida ─────────────────────────────────────────────────────────────
34
+ function useFontes(P) {
35
+ const [pronto, setPronto] = useState(false);
36
+ const [alca] = useState(() => delayRender('fluido: carregar as fontes'));
37
+ useEffect(() => {
38
+ const txt = P.batidas.flatMap((b) => b.frases.flatMap((f) => f.palavras.map((w) => w.t))).join(' ') + ' 0123456789—$✓';
39
+ Promise.all([300, 400, 500, 600].map((w) => document.fonts.load(w + ' 40px ' + SANS(P.fontes.texto), txt).catch(() => null))
40
+ .concat([document.fonts.load('italic 400 40px ' + SERIF(P.fontes.serifa), txt).catch(() => null),
41
+ document.fonts.load('400 40px ' + MONO(P.fontes.mono), txt).catch(() => null)]))
42
+ .then(() => document.fonts.ready).then(() => setPronto(true), () => setPronto(true));
43
+ }, []);
44
+ useEffect(() => { if (pronto) continueRender(alca); }, [pronto]);
45
+ return pronto;
46
+ }
47
+ let ctx = null;
48
+ function medir(t, tam, peso, serifa, P, mono) {
49
+ if (!ctx) ctx = document.createElement('canvas').getContext('2d');
50
+ ctx.font = serifa ? 'italic 400 ' + tam + 'px ' + SERIF(P.fontes.serifa) : peso + ' ' + tam + 'px ' + (mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto));
51
+ return ctx.measureText(t).width;
52
+ }
53
+
54
+ /* Um bloco de texto: quebra palavra a palavra (largura medida), cada linha
55
+ centrada em cx (ou alinhada à esquerda/direita), o bloco centrado em cy. */
56
+ function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, corAcento = null, mono = false }) {
57
+ const esp = medir(' ', tam, peso, false, P, mono);
58
+ const ws = f.palavras.map((w) => ({ ...w, w: medir(w.t, w.acento ? tam * 1.14 : tam, peso, w.acento, P, mono) }));
59
+ const linhas = [[]];
60
+ let lw = 0;
61
+ for (const w of ws) {
62
+ const cur = linhas[linhas.length - 1];
63
+ if (cur.length && lw + esp + w.w > max) { linhas.push([w]); lw = w.w; } else { lw += (cur.length ? esp : 0) + w.w; cur.push(w); }
64
+ }
65
+ const lh = tam * 1.2;
66
+ const larg = (l) => l.reduce((s, w) => s + w.w, 0) + esp * (l.length - 1);
67
+ const w = Math.max(...linhas.map(larg)), h = linhas.length * lh;
68
+ const top = cy - h / 2;
69
+ const x0 = alinha === 'centro' ? cx - w / 2 : alinha === 'esq' ? cx : cx - w;
70
+ return {
71
+ f, tam, peso, cor, corAcento, esp, lh, w, h, top, cx, cy, mono, left: x0,
72
+ linhas: linhas.map((l, i) => ({ palavras: l, y: top + i * lh, x: alinha === 'centro' ? cx - larg(l) / 2 : alinha === 'esq' ? cx : cx - larg(l) })),
73
+ };
74
+ }
75
+
76
+ // ── o leiaute de cada batida: os blocos, o cartão e as chaves do portador ─
77
+ function leiaute(P) {
78
+ const W = P.largura, H = P.altura, u = Math.min(W, H) / 1080, deitado = W >= H;
79
+ const cx = W / 2, cy = H / 2;
80
+ const pal = P.paleta, T = pal.tinta, S = pal.suave;
81
+ /* O estado do portador. A cor cheia vai PRÉ-MULTIPLICADA (sa: quanto de
82
+ acento, si: quanto de tinta): ponto de acento que vira pílula clara
83
+ cruza as duas camadas, sem passar por um cinza no meio. */
84
+ const forma = (x, y, w, h, o = {}) => {
85
+ const solid = o.solid ?? 0, ac = o.ac ?? 0;
86
+ return { x, y, w, h, r: o.r ?? Math.min(w, h) / 2, fill: o.fill ?? 0, sa: solid * ac, si: solid * (1 - ac), stroke: o.stroke ?? 0, op: o.op ?? 1 };
87
+ };
88
+ const chaves = []; // [{a, b, s}]: de a até b o portador vai para s
89
+ const vai = (a, b, s) => chaves.push({ a, b, s });
90
+ const batidas = P.batidas.map((b, bi) => {
91
+ const fr = (papel) => b.frases.filter((f) => f.papel === papel);
92
+ const [pr] = fr('principal');
93
+ const out = { b, blocos: [], extra: null };
94
+ /* A COMPOSIÇÃO: com cartão, o texto vai para um lado (ou para cima, em
95
+ pé) e o cartão para o outro; sem cartão, o centro. */
96
+ const lado = b.lado || 'centro';
97
+ const tx = !deitado || lado === 'centro' ? cx : lado === 'esq' ? cx - W * 0.21 : cx + W * 0.21;
98
+ const ty = deitado || lado === 'centro' ? cy : cy - H * 0.2;
99
+ const larg = lado === 'centro' ? W * (deitado ? 0.72 : 0.86) : deitado ? W * 0.36 : W * 0.86;
100
+ if (b.ui) {
101
+ const cw = deitado ? W * 0.34 : W * 0.8, ch = cw * (b.ui.tipo === 'terminal' ? 0.6 : 0.7);
102
+ out.cartao = { x: deitado ? (lado === 'esq' ? cx + W * 0.2 : cx - W * 0.2) : cx, y: deitado ? cy : cy + H * 0.15, w: cw, h: ch, gira: deitado ? (lado === 'esq' ? -14 : 14) : 0 };
103
+ }
104
+ let ent = b.transicao === 'corte' ? [b.de, b.de] : [Math.max(0, b.de - 0.22), b.de + 0.45];
105
+ // A primeira batida (sem ser abertura) nasce de um ponto, que então vira a forma dela.
106
+ if (bi === 0 && b.tipo !== 'abertura') {
107
+ vai(0, 0.01, forma(tx, ty, 0, 0, { solid: 1, ac: 1, op: 0 }));
108
+ vai(0.02, 0.3, forma(tx, ty, 16 * u, 16 * u, { solid: 1, ac: 1 }));
109
+ ent = [0.3, 0.8];
110
+ }
111
+ const poe = (bl) => { out.blocos.push(bl); return bl; };
112
+ /* A IMAGEM (fluido.js escolhe a batida e o jeito): o texto se arruma em
113
+ volta dela, e o portador vira um ponto discreto enquanto a foto manda. */
114
+ if (b.imagem && pr) {
115
+ const j = b.imagem.jeito;
116
+ const t0 = Math.max(b.de + 0.05, Math.min(...b.frases.map((f) => f.palavras[0].em)));
117
+ const [sb] = fr('sub');
118
+ const branco = '#ffffff';
119
+ let foto, tY;
120
+ if (j === 'cartao') {
121
+ const cw = deitado ? W * 0.36 : W * 0.74, ch = deitado ? H * 0.62 : H * 0.42;
122
+ foto = { x: deitado ? cx + W * 0.2 : cx, y: deitado ? cy : cy + H * 0.12, w: cw, h: ch };
123
+ tY = deitado ? cy : cy - H * 0.24;
124
+ } else if (j === 'faixa') {
125
+ foto = { x: cx, y: deitado ? cy - H * 0.08 : cy - H * 0.06, w: W, h: deitado ? H * 0.46 : H * 0.34 };
126
+ tY = foto.y + foto.h / 2 + 130 * u;
127
+ } else { foto = { x: cx, y: cy, w: W, h: H }; tY = j === 'vazado' ? cy : cy + H * (deitado ? 0.22 : 0.2); }
128
+ const tx2 = j === 'cartao' && deitado ? cx - W * 0.2 : cx;
129
+ const sobreFoto = j === 'janela' || j === 'circulo';
130
+ let tam = (j === 'vazado' ? 200 : sobreFoto ? 92 : 76) * u;
131
+ if (j === 'vazado') {
132
+ const l100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, 100, 700, false, P), 0) + medir(' ', 100, 700, false, P) * (pr.palavras.length - 1);
133
+ tam = Math.min(360 * u, (W * 0.84 / (1 + 0.022 * (bi + 1))) / Math.max(1, l100) * 100);
134
+ }
135
+ const larg2 = j === 'cartao' && deitado ? W * 0.34 : W * 0.84;
136
+ const tb = poe(bloco(P, pr, { tam, peso: j === 'vazado' ? 700 : 400, max: larg2, cx: tx2, cy: tY, cor: sobreFoto ? branco : T, corAcento: sobreFoto ? branco : null }));
137
+ if (sb) poe(bloco(P, sb, { tam: 34 * u, max: larg2, cx: tx2, cy: tY + tb.h / 2 + 56 * u, cor: sobreFoto ? alfa(branco, 0.82) : S }));
138
+ out.imagem = { jeito: j, src: b.imagem.src, foto, t0, texto: tb };
139
+ // O portador: no círculo ele É a foto (cresce); nos outros, vira um ponto e some.
140
+ if (j === 'circulo') vai(Math.max(0, t0 - 0.5), t0 + 0.2, forma(cx, cy, 0, 0, { solid: 1, ac: 1, op: 0 }));
141
+ else vai(ent[0], ent[1], forma(tx2, tY - tb.h / 2 - 60 * u, 10 * u, 10 * u, { solid: 1, ac: 1, op: j === 'vazado' ? 1 : 0 }));
142
+ return out;
143
+ }
144
+ /* A PALAVRA GIGANTE (fluido.js marca): a frase curta enche a largura,
145
+ sem pílula, peso fino. Na VIRADA o portador cresce até cobrir a tela
146
+ de acento, e o texto troca para a cor que lê sobre ele. */
147
+ if (b.gigante && pr) {
148
+ const base = 100;
149
+ const larguraA100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, w.acento ? base * 1.14 : base, 300, w.acento, P), 0) + medir(' ', base, 300, false, P) * (pr.palavras.length - 1);
150
+ // Cabe já descontando o zoom que a câmera terá nesta batida (1 + 0,022 por batida) e a deriva.
151
+ const zoomAqui = 1 + 0.022 * (bi + 1);
152
+ const tam = Math.min(330 * u, Math.max(96 * u, (W * 0.8 / zoomAqui) / Math.max(1, larguraA100) * base));
153
+ const sobre = b.virada ? (lum(pal.acentos[0]) > 0.55 ? pal.fundo[0] : '#ffffff') : T;
154
+ const acentoSobre = b.virada ? sobre : pal.acentos[0];
155
+ const g = poe(bloco(P, pr, { tam, peso: 300, max: W * 0.84 / zoomAqui, cx, cy, cor: sobre, corAcento: acentoSobre }));
156
+ const [sb] = fr('sub');
157
+ if (sb) poe(bloco(P, sb, { tam: 34 * u, max: W * 0.7, cx, cy: cy + g.h / 2 + 70 * u, cor: b.virada ? sobre : S }));
158
+ // A virada começa com a PRIMEIRA frase que aparece (a voz pode dizer o sub antes do título).
159
+ const t0 = Math.max(b.de + 0.1, Math.min(...b.frases.map((f) => f.palavras[0].em)));
160
+ if (b.virada) {
161
+ const D = Math.hypot(W, H) * 1.08;
162
+ vai(Math.max(0, t0 - 0.55), t0 - 0.05, forma(cx, cy, D, D, { solid: 1, ac: 1 }));
163
+ } else vai(ent[0], ent[1], forma(cx, cy - g.h / 2 - 70 * u, 14 * u, 14 * u, { solid: 1, ac: 1 }));
164
+ return out;
165
+ }
166
+ switch (b.tipo) {
167
+ case 'abertura': {
168
+ const t = pr && poe(bloco(P, pr, { tam: (lado === 'centro' ? 96 : 80) * u, max: larg, cx: tx, cy: ty, cor: T }));
169
+ const [sb] = fr('sub');
170
+ if (sb) poe(bloco(P, sb, { tam: 36 * u, max: larg * 0.8, cx: tx, cy: ty + (t ? t.h / 2 : 0) + 64 * u, cor: S }));
171
+ const y = ty - (t ? t.h / 2 : 0) - 80 * u;
172
+ vai(0, 0.01, forma(tx, y, 0, 0, { solid: 1, ac: 1, op: 0 }));
173
+ vai(0.02, 0.45, forma(tx, y, 18 * u, 18 * u, { solid: 1, ac: 1 }));
174
+ break;
175
+ }
176
+ case 'numero': {
177
+ const [nb] = fr('numero'), [rot] = fr('rotulo');
178
+ const n = nb && poe(bloco(P, nb, { tam: 280 * u, peso: 300, cx: tx, cy: ty - 40 * u, cor: T }));
179
+ const d = Math.max(n ? n.w : 0, 260 * u) + 140 * u;
180
+ if (rot) poe(bloco(P, rot, { tam: 40 * u, max: larg, cx: tx, cy: ty - 40 * u + d / 2 + 64 * u, cor: S }));
181
+ vai(ent[0], ent[1], forma(tx, ty - 40 * u, d, d, { stroke: 0.55 }));
182
+ break;
183
+ }
184
+ case 'lista': {
185
+ const its = fr('item');
186
+ const tt = pr && poe(bloco(P, pr, { tam: 60 * u, max: W * 0.8, cx, cy: cy - (deitado ? 200 : 380) * u, cor: T }));
187
+ const tam = 42 * u, padX = 36 * u, ph = tam * 2.2, ic = 30 * u;
188
+ const ws = its.map((f) => medir(f.palavras.map((w) => w.t).join(' '), tam, 400, false, P) + padX * 2 + ic + 14 * u);
189
+ const gap = 70 * u;
190
+ const total = ws.reduce((s, w) => s + w, 0) + gap * (ws.length - 1);
191
+ const linha = deitado && total < W * 0.94;
192
+ let x = cx - total / 2;
193
+ const pilulas = its.map((f, j) => {
194
+ let pcx, pcy;
195
+ if (linha) { pcx = x + ws[j] / 2; pcy = cy + 30 * u + (j % 2 ? 90 : -10) * u; x += ws[j] + gap; }
196
+ else { pcx = cx + (j % 2 ? 1 : -1) * Math.min(W * 0.14, 150 * u); pcy = cy - 160 * u + j * (ph + 90 * u); }
197
+ const bl = bloco(P, f, { tam, cx: pcx + (ic + 14 * u) / 2, cy: pcy, cor: T });
198
+ return { f, x: pcx, y: pcy, w: ws[j], h: ph, bl, ic };
199
+ });
200
+ out.blocos.push(...pilulas.map((p) => p.bl));
201
+ out.extra = { pilulas, tt };
202
+ // O portador vira a PRIMEIRA pílula; as outras nascem na ponta da curva.
203
+ const p0 = pilulas[0];
204
+ // A primeira pílula nasce com o primeiro item (antes, ficava vazia e enorme esperando a voz).
205
+ const i0 = p0 ? p0.f.palavras[0].em : b.de;
206
+ if (p0) vai(Math.max(ent[0], i0 - 0.4), Math.max(ent[1], i0 + 0.1), forma(p0.x, p0.y, p0.w, p0.h, { fill: 1, stroke: 0.35 }));
207
+ else vai(ent[0], ent[1], forma(cx, cy, 12 * u, 12 * u, { solid: 1, ac: 1 }));
208
+ break;
209
+ }
210
+ case 'passos': {
211
+ const its = fr('item');
212
+ const tam = 68 * u;
213
+ const tY = deitado ? cy : cy - 420 * u;
214
+ const tt = pr && poe(bloco(P, pr, { tam, max: W * 0.34, cx: deitado ? cx - 64 * u : cx, cy: tY, alinha: deitado ? 'dir' : 'centro', cor: T }));
215
+ const roda = its.map((f) => bloco(P, f, { tam, cx: deitado ? cx + 64 * u : cx, cy: deitado ? cy : cy + 60 * u, alinha: deitado ? 'esq' : 'centro', cor: T }));
216
+ out.extra = { roda, tt, tam };
217
+ vai(ent[0], ent[1], forma(cx, deitado ? cy : cy - 260 * u, 14 * u, 14 * u, { solid: 1, ac: 1 }));
218
+ break;
219
+ }
220
+ case 'contraste': {
221
+ const [nao] = fr('nao'), [sim] = fr('sim');
222
+ const tam = 76 * u;
223
+ const nb = nao && poe(bloco(P, nao, { tam: tam * 0.8, max: larg, cx, cy: cy - (deitado ? 90 : 160) * u, cor: S }));
224
+ const sb = sim && poe(bloco(P, sim, { tam, max: larg, cx, cy: cy + (deitado ? 70 : 120) * u, cor: T }));
225
+ if (nb) {
226
+ const fimNao = nao.palavras[nao.palavras.length - 1].em + 0.3;
227
+ const ly = nb.top + nb.h - nb.lh / 2;
228
+ vai(ent[0], ent[1], forma(nb.left - 22 * u, ly, 12 * u, 12 * u, { solid: 1 }));
229
+ vai(fimNao, fimNao + 0.4, forma(nb.left + nb.w / 2, ly, nb.w + 36 * u, 4 * u, { solid: 0.8, r: 3 }));
230
+ out.risca = fimNao + 0.2;
231
+ }
232
+ if (sb) {
233
+ const ly = sb.top + sb.h + 14 * u;
234
+ vai(Math.max(sim.de - 0.3, (chaves[chaves.length - 1] || {}).b || 0), sim.de + 0.4, forma(sb.left + sb.w / 2, ly, sb.w, 4 * u, { solid: 1, ac: 1, r: 3 }));
235
+ }
236
+ break;
237
+ }
238
+ case 'citacao': {
239
+ const [au] = fr('autor');
240
+ const q = pr && bloco(P, pr, { tam: 58 * u, max: Math.min(W * 0.66, 1100 * u), cx, cy: cy - (au ? 30 * u : 0), cor: T });
241
+ if (q) poe(q);
242
+ const a = au && poe(bloco(P, au, { tam: 30 * u, cx, cy: (q ? q.top + q.h : cy) + 44 * u, cor: S }));
243
+ const w = (q ? q.w : 300 * u) + 160 * u, h = (q ? q.h : 60 * u) + (a ? 80 : 20) * u + 120 * u;
244
+ vai(ent[0], ent[1], forma(cx, cy + (a ? 10 * u : 0), w, h, { fill: 1, stroke: 0.2, r: 44 * u }));
245
+ break;
246
+ }
247
+ case 'cta': {
248
+ const [nome] = fr('marca');
249
+ const tam = 120 * u;
250
+ const nw = nome ? medir(b.marca.nome, tam, 600, false, P) : 0;
251
+ const mk = 190 * u, gap = 44 * u;
252
+ const caber = Math.min(1, (W * 0.9 - mk - gap) / Math.max(1, nw));
253
+ const largMarca = mk + gap + nw * caber;
254
+ const x0 = cx - largMarca / 2;
255
+ const nb = nome && poe(bloco(P, nome, { tam: tam * caber, peso: 600, cx: x0 + mk + gap, cy: cy - 30 * u, alinha: 'esq', cor: T }));
256
+ let y = cy + 130 * u;
257
+ for (const f of fr('principal')) { poe(bloco(P, f, { tam: 44 * u, max: W * 0.8, cx, cy: y, cor: T })); y += 90 * u; }
258
+ // O link vira um comando numa pílula, em mono.
259
+ for (const f of fr('sub')) out.comando = poe(bloco(P, f, { tam: 32 * u, cx, cy: y, cor: T, mono: true }));
260
+ // A marca se monta de três formas: o portador (círculo cheio), um anel e um ponto.
261
+ const mx = x0 + mk / 2, my = cy - 30 * u, q = mk / 190;
262
+ vai(ent[0], ent[1], forma(cx, cy, 80 * q, 80 * q, { solid: 1, ac: 1 }));
263
+ vai(b.de + 0.1, b.de + 0.5, forma(mx - 42 * q, my - 22 * q, 84 * q, 84 * q, { solid: 1, ac: 1 }));
264
+ out.extra = { mx, my, mk, nb, pecas: [
265
+ { de: forma(cx + 420 * u, cy - 300 * u, 84 * q, 84 * q), ate: forma(mx + 44 * q, my - 22 * q, 84 * q, 84 * q), anel: true },
266
+ { de: forma(cx - 380 * u, cy + 320 * u, 44 * q, 44 * q), ate: forma(mx + 2 * q, my + 54 * q, 44 * q, 44 * q), anel: false },
267
+ ] };
268
+ break;
269
+ }
270
+ default: { // titulo: a pílula que envolve a frase; em duas linhas, a barra de acento ao lado
271
+ let t = pr && bloco(P, pr, { tam: 76 * u, max: larg, cx: tx, cy: ty, cor: T });
272
+ // Em mais de uma linha o texto alinha à esquerda, junto da barra de acento.
273
+ if (t && t.linhas.length > 1) t = bloco(P, pr, { tam: 76 * u, max: larg, cx: tx - t.w / 2, cy: ty, alinha: 'esq', cor: T });
274
+ if (t) poe(t);
275
+ const [sb] = fr('sub');
276
+ const umaLinha = !t || t.linhas.length === 1;
277
+ const h = umaLinha ? (t ? t.tam * 2 : 60 * u) : t.h;
278
+ if (sb) poe(umaLinha ? bloco(P, sb, { tam: 34 * u, max: larg, cx: tx, cy: ty + h / 2 + 62 * u, cor: S })
279
+ : bloco(P, sb, { tam: 34 * u, max: larg, cx: t.left, cy: ty + h / 2 + 50 * u, alinha: 'esq', cor: S }));
280
+ /* A pílula nasce com a primeira palavra, não antes: entrando em b.de,
281
+ ela ficava VAZIA até a voz chegar na frase (teste de 08/10). */
282
+ const p0 = pr ? pr.palavras[0].em : b.de;
283
+ const entT = [Math.max(ent[0], p0 - 0.4), Math.max(ent[1], p0 + 0.1)];
284
+ if (umaLinha) vai(entT[0], entT[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
285
+ else vai(entT[0], entT[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
286
+ }
287
+ }
288
+ return out;
289
+ });
290
+ return { batidas, chaves, u, W, H, deitado };
291
+ }
292
+
293
+ /* O portador no tempo t: a última chave começada, saindo da anterior. */
294
+ function portadorEm(t, chaves) {
295
+ let i = -1;
296
+ while (i + 1 < chaves.length && chaves[i + 1].a <= t) i++;
297
+ if (i < 0) return { ...chaves[0].s, op: 0 };
298
+ const c = chaves[i], ant = i > 0 ? chaves[i - 1].s : { ...c.s, op: 0 };
299
+ const k = c.b <= c.a ? 1 : cam(c01((t - c.a) / (c.b - c.a)));
300
+ const s = {};
301
+ for (const key of Object.keys(c.s)) s[key] = lerp(ant[key], c.s[key], k);
302
+ return s;
303
+ }
304
+
305
+ // ── a palavra: entra quando é dita ───────────────────────────────────────
306
+ function Palavras({ bl, sai, t, P, op = 1, de = null }) {
307
+ const pal = P.paleta;
308
+ let n = 0;
309
+ return bl.linhas.map((l, li) => (
310
+ <div key={li} style={{ position: 'absolute', left: l.x, top: l.y, height: bl.lh, lineHeight: bl.lh + 'px', whiteSpace: 'nowrap' }}>
311
+ {l.palavras.map((w, wi) => {
312
+ const em = de != null ? de + (n++) * 0.07 : w.em;
313
+ const k = ed(c01((t - em) / 0.5));
314
+ const o = entra(c01((t - sai) / 0.35));
315
+ const vis = k * (1 - o) * op;
316
+ const mr = wi < l.palavras.length - 1 ? bl.esp : 0;
317
+ if (vis <= 0.001) return <span key={wi} style={{ display: 'inline-block', width: w.w, marginRight: mr }} />;
318
+ return (
319
+ <span key={wi} style={{
320
+ display: 'inline-block', marginRight: mr,
321
+ opacity: vis, transform: 'translateY(' + ((1 - k) * 0.4 - o * 0.25) * bl.tam + 'px)',
322
+ filter: (1 - k) + o > 0.02 ? 'blur(' + ((1 - k) * 8 + o * 6).toFixed(2) + 'px)' : undefined,
323
+ fontFamily: w.acento ? SERIF(P.fontes.serifa) : bl.mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto), fontStyle: w.acento ? 'italic' : 'normal',
324
+ fontWeight: w.acento ? 400 : bl.peso, fontSize: w.acento ? bl.tam * 1.14 : bl.tam, letterSpacing: w.acento || bl.mono ? 0 : '-0.02em',
325
+ color: w.acento ? (bl.corAcento || pal.acentos[0]) : bl.cor,
326
+ }}>{w.t}</span>
327
+ );
328
+ })}
329
+ </div>
330
+ ));
331
+ }
332
+
333
+ const numeroTexto = (n, k) => n.antes + (n.ate * k).toLocaleString('pt-BR', { minimumFractionDigits: n.casas, maximumFractionDigits: n.casas }) + n.depois;
334
+
335
+ // ── o cartão de produto: terminal, chat ou painel, de forma e texto ─────
336
+ function Cartao({ c, ui, de: de0, sai, t, P, u: u0 }) {
337
+ const pal = P.paleta, T = pal.tinta, A = pal.acentos[0];
338
+ // O conteúdo escala com o cartão (em pé ele é mais largo); entra logo depois da troca.
339
+ const u = u0 * c.w / (650 * u0), de = de0 + 0.2;
340
+ const k = ed(c01((t - de - 0.05) / 0.6)), o = entra(c01((t - sai) / 0.35));
341
+ const vis = k * (1 - o);
342
+ if (vis <= 0.001) return null;
343
+ const vidro = pal.escuro ? alfa(T, 0.06) : alfa('#ffffff', 0.78);
344
+ const linha = alfa(T, pal.escuro ? 0.14 : 0.1);
345
+ const pele = alfa(T, pal.escuro ? 0.2 : 0.1);
346
+ const barra = (x, y, w, h, cor, kk = 1) => <div style={{ position: 'absolute', left: x, top: y, width: w * kk, height: h, borderRadius: h / 2, background: cor }} />;
347
+ const pad = 34 * u, mono = { fontFamily: MONO(P.fontes.mono), fontSize: 26 * u, color: T, whiteSpace: 'nowrap', position: 'absolute' };
348
+ let corpo = null;
349
+ if (ui.tipo === 'terminal') {
350
+ const linhas = ui.linhas.map((ln, j) => {
351
+ const t0 = de + 0.55 + j * 0.5, n = Math.round(c01((t - t0) / 0.4) * ln.length);
352
+ if (n <= 0) return null;
353
+ const txt = ln.slice(0, n), ok = txt.startsWith('✓');
354
+ return <div key={j} style={{ ...mono, left: pad, top: 70 * u + j * 48 * u }}>
355
+ {ok ? <span style={{ color: A }}>✓</span> : null}{ok ? txt.slice(1) : txt}
356
+ {j === ui.linhas.length - 1 || n < ln.length ? <span style={{ display: 'inline-block', width: 14 * u, height: 28 * u, marginLeft: 6 * u, verticalAlign: 'middle', background: A, opacity: Math.floor(t * 2.5) % 2 ? 0.2 : 0.9 }} /> : null}
357
+ </div>;
358
+ });
359
+ const kk = (j) => ed(c01((t - de - 1.4 - j * 0.15) / 0.6));
360
+ const y0 = 70 * u + ui.linhas.length * 48 * u + 16 * u;
361
+ corpo = <>{linhas}{barra(pad, y0, c.w * 0.5, 14 * u, pele, kk(0))}{barra(pad, y0 + 30 * u, c.w * 0.34, 14 * u, pele, kk(1))}</>;
362
+ } else if (ui.tipo === 'chat') {
363
+ const k1 = ed(c01((t - de - 0.45) / 0.5)), k2 = ed(c01((t - de - 0.9) / 0.5)), k3 = ed(c01((t - de - 1.5) / 0.6));
364
+ corpo = <>
365
+ <div style={{ position: 'absolute', right: pad, top: 74 * u, maxWidth: c.w * 0.72, padding: (14 * u) + 'px ' + (22 * u) + 'px', borderRadius: 26 * u,
366
+ background: A, color: '#ffffff', fontFamily: SANS(P.fontes.texto), fontSize: 26 * u, lineHeight: 1.3, opacity: k1, transform: 'translateY(' + (1 - k1) * 20 * u + 'px)' }}>{ui.pergunta}</div>
367
+ <div style={{ position: 'absolute', left: pad, top: c.h * 0.55, width: c.w * 0.62, height: c.h * 0.28, borderRadius: 26 * u, background: pele, opacity: k2,
368
+ transform: 'translateY(' + (1 - k2) * 20 * u + 'px)' }}>
369
+ {k3 < 0.05 ? [0, 1, 2].map((j) => <div key={j} style={{ position: 'absolute', left: 24 * u + j * 22 * u, top: 24 * u, width: 12 * u, height: 12 * u, borderRadius: 99,
370
+ background: alfa(T, 0.35 + 0.4 * (0.5 + 0.5 * Math.sin(t * 9 - j))) }} />) : null}
371
+ {barra(24 * u, 22 * u, c.w * 0.5, 14 * u, alfa(T, 0.22), k3)}{barra(24 * u, 52 * u, c.w * 0.36, 14 * u, alfa(T, 0.22), k3)}
372
+ </div>
373
+ </>;
374
+ } else {
375
+ const bs = ui.barras || [];
376
+ const passo = (c.w - pad * 2) / Math.max(1, bs.length), base = c.h - pad;
377
+ corpo = <>
378
+ <div style={{ position: 'absolute', left: pad, top: 70 * u, fontFamily: SANS(P.fontes.texto), fontSize: 24 * u, color: pal.suave, whiteSpace: 'nowrap' }}>{ui.rotulo}</div>
379
+ <div style={{ position: 'absolute', right: pad, top: 76 * u, width: 12 * u, height: 12 * u, borderRadius: 99, background: A, boxShadow: '0 0 ' + 14 * u + 'px ' + A }} />
380
+ {bs.map((v, j) => {
381
+ const kb = ed(c01((t - de - 0.55 - j * 0.09) / 0.7)), h = (c.h - 170 * u) * v * kb;
382
+ return <div key={j} style={{ position: 'absolute', left: pad + j * passo, top: base - h, width: passo * 0.62, height: h, borderRadius: 8 * u,
383
+ background: j === bs.length - 1 ? A : pele }} />;
384
+ })}
385
+ {barra(pad, base + 6 * u, c.w - pad * 2, 2 * u, linha)}
386
+ </>;
387
+ }
388
+ // Flutua: entra de baixo e de longe, e segue derivando (nunca para).
389
+ const gy = Math.sin(t * 0.9) * 8 * u, gr = Math.sin(t * 0.55) * 2.5;
390
+ return (
391
+ <div style={{ position: 'absolute', left: c.x - c.w / 2, top: c.y - c.h / 2, width: c.w, height: c.h, borderRadius: 30 * u, overflow: 'hidden',
392
+ background: vidro, border: (1.2 * u) + 'px solid ' + linha, backdropFilter: 'blur(' + 22 * u + 'px)', boxSizing: 'border-box',
393
+ boxShadow: '0 ' + 40 * u + 'px ' + 80 * u + 'px -' + 30 * u + 'px ' + alfa(pal.escuro ? '#000000' : T, pal.escuro ? 0.55 : 0.28),
394
+ opacity: vis, filter: vis < 0.98 ? 'blur(' + ((1 - vis) * 8).toFixed(2) + 'px)' : undefined,
395
+ transform: 'perspective(' + 1600 * u + 'px) translateY(' + ((1 - k) * 90 * u + gy) + 'px) rotateY(' + (c.gira + gr) + 'deg) rotateX(' + (5 + (1 - k) * 10) + 'deg) scale(' + (0.9 + 0.1 * k) + ')' }}>
396
+ <div style={{ position: 'absolute', left: 0, right: 0, top: 0, height: 48 * u, borderBottom: (1 * u) + 'px solid ' + linha }}>
397
+ {[0, 1, 2].map((j) => <div key={j} style={{ position: 'absolute', left: 22 * u + j * 22 * u, top: 18 * u, width: 12 * u, height: 12 * u, borderRadius: 99, background: alfa(T, 0.22) }} />)}
398
+ </div>
399
+ {corpo}
400
+ </div>
401
+ );
402
+ }
403
+
404
+ // ── a foto: cinco jeitos de entrar, nenhum repetido no vídeo ────────────
405
+ function Foto({ im, b, sai, t, P, u, W, H }) {
406
+ const k = ed(c01((t - (im.t0 - 0.45)) / 0.75)); // a entrada
407
+ const o = entra(c01((t - sai) / 0.4)); // a saída
408
+ if (k <= 0.001 || o >= 0.999) return null;
409
+ const vida = c01((t - im.t0) / Math.max(1, b.ate - im.t0)); // o empurrão lento enquanto ela fica
410
+ const src = staticFile(im.src);
411
+ const f = im.foto;
412
+ const img = (extra = {}) => <Img src={src} style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', objectFit: 'cover', ...extra }} />;
413
+ const escurece = <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(0,0,0,0.05) 30%, rgba(0,0,0,0.62) 100%)' }} />;
414
+ if (im.jeito === 'janela') {
415
+ return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden', opacity: 1 - o,
416
+ clipPath: 'inset(0 ' + ((1 - k) * 100).toFixed(2) + '% 0 0)' }}>
417
+ {img({ transform: 'scale(' + (1.14 - 0.08 * vida) + ') translateX(' + ((1 - k) * -6) + '%)' })}{escurece}</div>;
418
+ }
419
+ if (im.jeito === 'circulo') {
420
+ const r = Math.hypot(W, H) * 0.55 * k * (1 - o);
421
+ return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden',
422
+ clipPath: 'circle(' + r.toFixed(1) + 'px at ' + W / 2 + 'px ' + H / 2 + 'px)' }}>
423
+ {img({ transform: 'scale(' + (1.2 - 0.12 * k - 0.05 * vida) + ')' })}{escurece}</div>;
424
+ }
425
+ if (im.jeito === 'cartao') {
426
+ const gira = (1 - k) * -38 + Math.sin(t * 0.6) * 3;
427
+ return <div style={{ position: 'absolute', left: f.x - f.w / 2, top: f.y - f.h / 2, width: f.w, height: f.h, borderRadius: 28 * u, overflow: 'hidden',
428
+ opacity: k * (1 - o), boxShadow: '0 ' + 50 * u + 'px ' + 90 * u + 'px -' + 30 * u + 'px rgba(0,0,0,0.55)',
429
+ transform: 'perspective(' + 1500 * u + 'px) translateY(' + ((1 - k) * 140 * u - o * 60 * u) + 'px) rotateY(' + gira + 'deg) rotateX(' + (6 * (1 - k)) + 'deg) scale(' + (0.86 + 0.14 * k) + ')' }}>
430
+ {img({ transform: 'scale(' + (1.08 - 0.06 * vida) + ')' })}</div>;
431
+ }
432
+ if (im.jeito === 'faixa') {
433
+ const meio = (1 - k + o) * 50;
434
+ return <div style={{ position: 'absolute', left: 0, top: f.y - f.h / 2, width: W, height: f.h, overflow: 'hidden',
435
+ clipPath: 'inset(' + meio.toFixed(2) + '% 0 ' + meio.toFixed(2) + '% 0)' }}>
436
+ {img({ transform: 'scale(1.12) translateX(' + (-4 + 8 * vida) + '%)' })}</div>;
437
+ }
438
+ // vazado: as letras do título, com a foto por dentro (SVG: o texto é o recorte).
439
+ const tb = im.texto;
440
+ const id = 'vz' + b.i;
441
+ const fam = '"' + P.fontes.texto + '", system-ui, sans-serif';
442
+ return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, opacity: (1 - o) }}>
443
+ <defs><clipPath id={id}>{tb.linhas.map((l, li) => (
444
+ <text key={li} x={l.x} y={l.y + tb.lh * 0.8} fontFamily={fam} fontWeight={700} fontSize={tb.tam} letterSpacing={-0.02 * tb.tam}>
445
+ {l.palavras.map((w) => w.t).join(' ')}</text>))}</clipPath></defs>
446
+ <g clipPath={'url(#' + id + ')'} style={{ opacity: k }}>
447
+ <image href={src} x={-W * 0.06 * (1 - vida)} y={tb.top - tb.h * 0.6} width={W * 1.12} height={tb.h * 2.2} preserveAspectRatio="xMidYMid slice"
448
+ transform={'translate(0 ' + ((1 - k) * 40 * u) + ')'} />
449
+ </g>
450
+ </svg>;
451
+ }
452
+
453
+ // ── a peça ───────────────────────────────────────────────────────────────
454
+ export function Fluido({ el, t }) {
455
+ const P = el.partitura;
456
+ const pronto = useFontes(P);
457
+ const L = useMemo(() => (pronto ? leiaute(P) : null), [pronto]);
458
+ const pal = P.paleta, W = P.largura, H = P.altura;
459
+ const [f0, f1, f2] = pal.fundo;
460
+ const fundo = 'linear-gradient(158deg, ' + f0 + ' 0%, ' + f1 + ' 58%, ' + f2 + ' 100%)';
461
+ if (!L) return <div style={{ position: 'absolute', inset: 0, background: fundo }} />;
462
+ const { u } = L;
463
+ const s = portadorEm(t, L.chaves);
464
+ const atras = portadorEm(Math.max(0, t - 0.9), L.chaves);
465
+ // O brilho difuso segue o portador com atraso; uma segunda mancha deriva devagar.
466
+ const gx = (atras.x / W) * 100, gy = (atras.y / H) * 100;
467
+ const dx = 70 + Math.sin(t * 0.21) * 14, dy = 20 + Math.cos(t * 0.17) * 12;
468
+ const brilho = alfa(pal.acentos[1], pal.escuro ? 0.34 : 0.28);
469
+ const sup = pal.escuro ? alfa(pal.tinta, 0.07) : alfa('#ffffff', 0.62);
470
+ /* A câmera nunca para: em cada batida ela avança um pouco (e só avança —
471
+ a soma é contínua na troca), deriva devagar e puxa de leve para o portador. */
472
+ let bi = 0;
473
+ while (bi < P.batidas.length - 1 && t >= P.batidas[bi + 1].de) bi++;
474
+ const bt = P.batidas[bi];
475
+ const z = 1 + 0.022 * (bi + cam(c01((t - bt.de) / Math.max(0.5, bt.ate - bt.de))));
476
+ const px = (W / 2 - s.x) * 0.04 + Math.sin(t * 0.47) * 10 * u, py = (H / 2 - s.y) * 0.04 + Math.cos(t * 0.39) * 7 * u;
477
+ // O portador respira.
478
+ const resp = 1 + 0.018 * Math.sin(t * 2.1);
479
+ const ult = P.batidas[P.batidas.length - 1];
480
+ // Com logo, o portador some quando o logo assume a marca.
481
+ const so = P.logo && ult.tipo === 'cta' ? 1 - ed(c01((t - ult.de - 0.65) / 0.5)) : 1;
482
+
483
+ const camadas = [];
484
+ L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando, imagem }, i) => {
485
+ const corte = b.transicao === 'corte' || (L.batidas[i + 1] && L.batidas[i + 1].b.transicao === 'corte');
486
+ const ultima = i === L.batidas.length - 1;
487
+ const sai = ultima ? 1e9 : b.ate - (corte ? 0 : 0.15), fimVis = b.ate + (corte ? 0 : 0.4);
488
+ if (t < b.de - 0.3 || t > fimVis || (corte && t >= b.ate && !ultima)) return;
489
+ if (cartao) camadas.push(<Cartao key={'k' + i} c={cartao} ui={b.ui} de={b.de} sai={sai} t={t} P={P} u={u} />);
490
+ if (imagem) camadas.push(<Foto key={'f' + i} im={imagem} b={b} sai={sai} t={t} P={P} u={u} W={W} H={H} />);
491
+ // A lista: as curvas que se desenham de uma pílula à próxima, e as pílulas que nascem na ponta.
492
+ if (b.tipo === 'lista' && extra) {
493
+ const ps = extra.pilulas;
494
+ const o = 1 - entra(c01((t - sai) / 0.35));
495
+ ps.forEach((p, j) => {
496
+ if (j > 0) {
497
+ const a = ps[j - 1], c = p;
498
+ const x0 = a.x + a.w / 2, y0 = a.y, x1 = c.x - c.w / 2, y1 = c.y;
499
+ const vert = Math.abs(x1 - x0) < 60 * u || x1 < x0;
500
+ const d = vert
501
+ ? 'M ' + a.x + ' ' + (a.y + a.h / 2) + ' C ' + a.x + ' ' + (a.y + a.h / 2 + 60 * u) + ' ' + c.x + ' ' + (c.y - c.h / 2 - 60 * u) + ' ' + c.x + ' ' + (c.y - c.h / 2)
502
+ : 'M ' + x0 + ' ' + y0 + ' C ' + (x0 + (x1 - x0) * 0.6) + ' ' + y0 + ' ' + (x1 - (x1 - x0) * 0.6) + ' ' + y1 + ' ' + x1 + ' ' + y1;
503
+ const k = ed(c01((t - (p.f.de - 0.4)) / 0.45));
504
+ camadas.push(<svg key={'c' + i + j} width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', opacity: o }}>
505
+ <path d={d} fill="none" stroke={alfa(pal.tinta, 0.5)} strokeWidth={2 * u} pathLength={1} strokeDasharray="1 1" strokeDashoffset={1 - k} />
506
+ </svg>);
507
+ const kp = ed(c01((t - (p.f.de - 0.1)) / 0.45));
508
+ if (kp > 0) camadas.push(<div key={'p' + i + j} style={{ position: 'absolute', left: p.x - p.w / 2, top: p.y - p.h / 2, width: p.w, height: p.h, borderRadius: p.h / 2,
509
+ background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kp * o, transform: 'scale(' + (0.7 + 0.3 * kp) + ')',
510
+ boxShadow: pal.escuro ? undefined : '0 14px 34px -14px ' + alfa(pal.tinta, 0.3) }} />);
511
+ }
512
+ /* O marcador de cada pílula muda de vídeo para vídeo (P.marcador): o
513
+ brilho de quatro pontas em toda lista era vício (08/10). */
514
+ const kd = ed(c01((t - p.f.de) / 0.5));
515
+ const mk = P.marcador || 'brilho';
516
+ const mx0 = p.x - p.w / 2 + 34 * u, my0 = p.y - p.ic / 2;
517
+ if (mk === 'brilho') camadas.push(<svg key={'d' + i + j} width={p.ic} height={p.ic} viewBox="-1 -1 2 2" style={{ position: 'absolute', left: mx0, top: my0,
518
+ opacity: kd * o, transform: 'rotate(' + ((1 - kd) * -90 + Math.sin(t * 1.3 + j) * 6) + 'deg) scale(' + kd + ')' }}>
519
+ <path d={BRILHO4} fill={pal.acentos[0]} />
520
+ </svg>);
521
+ else if (mk === 'numero') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 - 4 * u, top: p.y - p.ic * 0.62, height: p.ic * 1.24, lineHeight: p.ic * 1.24 + 'px',
522
+ fontFamily: MONO(P.fontes.mono), fontSize: p.ic * 0.78, color: pal.acentos[0], opacity: kd * o, transform: 'translateY(' + (1 - kd) * 10 * u + 'px)' }}>{String(j + 1).padStart(2, '0')}</div>);
523
+ else if (mk === 'traco') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0, top: p.y - 1.5 * u, width: p.ic * kd, height: 3 * u, borderRadius: 3,
524
+ background: pal.acentos[0], opacity: o }} />);
525
+ else if (mk === 'ponto') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 + p.ic * 0.3, top: p.y - p.ic * 0.2, width: p.ic * 0.4, height: p.ic * 0.4, borderRadius: 99,
526
+ background: pal.acentos[0], opacity: o, transform: 'scale(' + kd + ')' }} />);
527
+ });
528
+ }
529
+ for (const bl of blocos) {
530
+ const f = bl.f;
531
+ if (b.tipo === 'passos' && extra) continue;
532
+ if (imagem && imagem.jeito === 'vazado' && bl === imagem.texto) continue; // as letras são a foto (Foto desenha)
533
+ const op = f.papel === 'nao' && risca ? 1 - 0.55 * ed(c01((t - risca) / 0.4)) : 1;
534
+ if (f.papel === 'numero' && b.numero) {
535
+ const k = ed(c01((t - f.de) / 1.1));
536
+ const vis = ed(c01((t - f.de) / 0.45)) * (1 - entra(c01((t - sai) / 0.35)));
537
+ camadas.push(<div key={'n' + i} style={{ position: 'absolute', left: bl.cx - W / 2, width: W, top: bl.top, lineHeight: bl.lh + 'px', textAlign: 'center',
538
+ fontFamily: SANS(P.fontes.texto), fontWeight: 300, fontSize: bl.tam, letterSpacing: '-0.05em', color: pal.tinta, opacity: vis,
539
+ transform: 'translateY(' + (1 - vis) * 40 * u + 'px)', filter: vis < 0.98 ? 'blur(' + ((1 - vis) * 10).toFixed(2) + 'px)' : undefined,
540
+ fontVariantNumeric: 'tabular-nums' }}>{numeroTexto(b.numero, k)}</div>);
541
+ continue;
542
+ }
543
+ if (bl === comando) {
544
+ // O comando numa pílula própria, que acende quando ele entra.
545
+ const kc = ed(c01((t - f.de + 0.1) / 0.45));
546
+ camadas.push(<div key={'cp' + i} style={{ position: 'absolute', left: bl.left - 30 * u, top: bl.top - 14 * u, width: bl.w + 60 * u, height: bl.h + 28 * u,
547
+ borderRadius: 999, background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kc, transform: 'scale(' + (0.85 + 0.15 * kc) + ')' }} />);
548
+ }
549
+ camadas.push(<React.Fragment key={'t' + i + f.id}><Palavras bl={bl} sai={sai} t={t} P={P} op={op} /></React.Fragment>);
550
+ }
551
+ // A roda dos passos: o item ativo nítido, os outros girando para trás, transparentes e desfocados.
552
+ if (b.tipo === 'passos' && extra) {
553
+ if (extra.tt) camadas.push(<React.Fragment key={'pt' + i}><Palavras bl={extra.tt} sai={sai} t={t} P={P} /></React.Fragment>);
554
+ const its = extra.roda;
555
+ let at = 0;
556
+ its.forEach((bl, j) => { if (j > 0) at += cam(c01((t - (bl.f.de - 0.25)) / 0.45)); });
557
+ const o = (1 - entra(c01((t - sai) / 0.35))) * ed(c01((t - (its[0] ? its[0].f.de - 0.2 : b.de)) / 0.5));
558
+ its.forEach((bl, j) => {
559
+ const off = j - at, a = Math.abs(off);
560
+ if (a > 3.2) return;
561
+ const vis = Math.max(0, 1 - a * 0.3) * o;
562
+ const k = ed(c01((t - bl.f.de) / 0.5));
563
+ const op = Math.max(vis * (0.4 + 0.6 * c01(1 - a)), a < 0.5 ? vis * k : 0);
564
+ camadas.push(<div key={'r' + i + j} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: op,
565
+ transform: 'perspective(' + 900 * u + 'px) translateY(' + Math.sin(off * 0.42) * extra.tam * 3.6 + 'px) translateZ(' + (-(1 - Math.cos(off * 0.42)) * 300 * u) + 'px) rotateX(' + (-off * 24) + 'deg)',
566
+ transformOrigin: '50% ' + bl.cy + 'px', filter: a > 0.05 ? 'blur(' + (a * 3.5).toFixed(2) + 'px)' : undefined }}>
567
+ <Palavras bl={bl} de={-10} sai={1e9} t={t} P={P} />
568
+ </div>);
569
+ });
570
+ }
571
+ // A marca: um anel e um ponto chegam e travam no portador; com logo, as formas viram o logo.
572
+ if (b.tipo === 'cta' && extra) {
573
+ const trava = b.de + 0.5;
574
+ const kl = P.logo ? ed(c01((t - trava - 0.15) / 0.5)) : 0;
575
+ extra.pecas.forEach((pc, j) => {
576
+ const k = cam(c01((t - (b.de - 0.1 + j * 0.08)) / 0.55));
577
+ const q = {}; for (const key of Object.keys(pc.ate)) q[key] = lerp(pc.de[key], pc.ate[key], k);
578
+ const ent = c01((t - b.de + 0.2) / 0.3) * (1 - kl);
579
+ camadas.push(<div key={'m' + i + j} style={{ position: 'absolute', left: q.x - q.w / 2, top: q.y - q.h / 2, width: q.w, height: q.h, borderRadius: 999,
580
+ background: pc.anel ? 'transparent' : pal.tinta, border: pc.anel ? (8 * u) + 'px solid ' + pal.tinta : undefined, opacity: ent, boxSizing: 'border-box' }} />);
581
+ });
582
+ // O pulso de quando trava (o impacto do som).
583
+ const kp = c01((t - trava) / 0.7);
584
+ if (kp > 0 && kp < 1) {
585
+ const r = extra.mk * (0.5 + kp * 0.6);
586
+ camadas.push(<div key={'pu' + i} style={{ position: 'absolute', left: extra.mx - r, top: extra.my - r, width: r * 2, height: r * 2, borderRadius: 999,
587
+ border: (2 * u) + 'px solid ' + alfa(pal.acentos[0], 0.6 * (1 - kp)) }} />);
588
+ }
589
+ if (P.logo && kl > 0) camadas.push(<div key={'lg' + i} style={{ position: 'absolute', left: extra.mx - extra.mk / 2, top: extra.my - extra.mk / 2, width: extra.mk, height: extra.mk,
590
+ opacity: kl, transform: 'scale(' + (0.8 + 0.2 * kl) + ')' }}><Img src={staticFile(P.logo)} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /></div>);
591
+ }
592
+ });
593
+
594
+ return (
595
+ <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', background: fundo }}>
596
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 70% 60% at ' + gx + '% ' + gy + '%, ' + brilho + ' 0%, transparent 70%)' }} />
597
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 80% 70% at ' + dx + '% ' + dy + '%, ' + alfa(f2, pal.escuro ? 0.45 : 0.5) + ' 0%, transparent 65%)' }} />
598
+ <div style={{ position: 'absolute', inset: 0, transform: 'translate(' + px + 'px,' + py + 'px) scale(' + z + ')', transformOrigin: '50% 50%' }}>
599
+ <div style={{ position: 'absolute', left: s.x - s.w * resp / 2, top: s.y - s.h * resp / 2, width: Math.max(0, s.w * resp), height: Math.max(0, s.h * resp), borderRadius: s.r,
600
+ background: pal.escuro ? alfa(pal.tinta, 0.07 * s.fill) : alfa('#ffffff', 0.62 * s.fill),
601
+ border: s.stroke > 0.01 ? (1.6 * u) + 'px solid ' + alfa(pal.tinta, s.stroke * (pal.escuro ? 0.6 : 0.55)) : undefined,
602
+ boxShadow: !pal.escuro && s.fill > 0.5 && s.h > 30 * u ? '0 20px 50px -24px ' + alfa(pal.tinta, 0.35 * s.fill) : undefined,
603
+ opacity: s.op * so, boxSizing: 'border-box' }}>
604
+ <div style={{ position: 'absolute', inset: 0, borderRadius: s.r, background: alfa(pal.tinta, s.si) }} />
605
+ <div style={{ position: 'absolute', inset: 0, borderRadius: s.r, background: alfa(pal.acentos[0], s.sa),
606
+ boxShadow: s.sa > 0.05 && s.h < 100 * u ? '0 0 ' + 30 * u + 'px ' + alfa(pal.acentos[0], 0.55 * s.sa) : undefined }} />
607
+ </div>
608
+ {camadas}
609
+ </div>
610
+ </div>
611
+ );
612
+ }
613
+
614
+ export const FLUIDO = { fluido: Fluido };