primocode 10.0.0 → 10.1.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 (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
@@ -0,0 +1,493 @@
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 entra = (t) => t * t;
27
+ const SANS = (f) => '"' + f + '", system-ui, sans-serif';
28
+ const SERIF = (f) => '"' + f + '", Georgia, serif';
29
+ const MONO = (f) => '"' + f + '", ui-monospace, monospace';
30
+ 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';
31
+
32
+ // ── a medida ─────────────────────────────────────────────────────────────
33
+ function useFontes(P) {
34
+ const [pronto, setPronto] = useState(false);
35
+ const [alca] = useState(() => delayRender('fluido: carregar as fontes'));
36
+ useEffect(() => {
37
+ const txt = P.batidas.flatMap((b) => b.frases.flatMap((f) => f.palavras.map((w) => w.t))).join(' ') + ' 0123456789—$✓';
38
+ Promise.all([300, 400, 500, 600].map((w) => document.fonts.load(w + ' 40px ' + SANS(P.fontes.texto), txt).catch(() => null))
39
+ .concat([document.fonts.load('italic 400 40px ' + SERIF(P.fontes.serifa), txt).catch(() => null),
40
+ document.fonts.load('400 40px ' + MONO(P.fontes.mono), txt).catch(() => null)]))
41
+ .then(() => document.fonts.ready).then(() => setPronto(true), () => setPronto(true));
42
+ }, []);
43
+ useEffect(() => { if (pronto) continueRender(alca); }, [pronto]);
44
+ return pronto;
45
+ }
46
+ let ctx = null;
47
+ function medir(t, tam, peso, serifa, P, mono) {
48
+ if (!ctx) ctx = document.createElement('canvas').getContext('2d');
49
+ ctx.font = serifa ? 'italic 400 ' + tam + 'px ' + SERIF(P.fontes.serifa) : peso + ' ' + tam + 'px ' + (mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto));
50
+ return ctx.measureText(t).width;
51
+ }
52
+
53
+ /* Um bloco de texto: quebra palavra a palavra (largura medida), cada linha
54
+ centrada em cx (ou alinhada à esquerda/direita), o bloco centrado em cy. */
55
+ function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, mono = false }) {
56
+ const esp = medir(' ', tam, peso, false, P, mono);
57
+ const ws = f.palavras.map((w) => ({ ...w, w: medir(w.t, w.acento ? tam * 1.14 : tam, peso, w.acento, P, mono) }));
58
+ const linhas = [[]];
59
+ let lw = 0;
60
+ for (const w of ws) {
61
+ const cur = linhas[linhas.length - 1];
62
+ 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); }
63
+ }
64
+ const lh = tam * 1.2;
65
+ const larg = (l) => l.reduce((s, w) => s + w.w, 0) + esp * (l.length - 1);
66
+ const w = Math.max(...linhas.map(larg)), h = linhas.length * lh;
67
+ const top = cy - h / 2;
68
+ const x0 = alinha === 'centro' ? cx - w / 2 : alinha === 'esq' ? cx : cx - w;
69
+ return {
70
+ f, tam, peso, cor, esp, lh, w, h, top, cx, cy, mono, left: x0,
71
+ linhas: linhas.map((l, i) => ({ palavras: l, y: top + i * lh, x: alinha === 'centro' ? cx - larg(l) / 2 : alinha === 'esq' ? cx : cx - larg(l) })),
72
+ };
73
+ }
74
+
75
+ // ── o leiaute de cada batida: os blocos, o cartão e as chaves do portador ─
76
+ function leiaute(P) {
77
+ const W = P.largura, H = P.altura, u = Math.min(W, H) / 1080, deitado = W >= H;
78
+ const cx = W / 2, cy = H / 2;
79
+ const pal = P.paleta, T = pal.tinta, S = pal.suave;
80
+ /* O estado do portador. A cor cheia vai PRÉ-MULTIPLICADA (sa: quanto de
81
+ acento, si: quanto de tinta): ponto de acento que vira pílula clara
82
+ cruza as duas camadas, sem passar por um cinza no meio. */
83
+ const forma = (x, y, w, h, o = {}) => {
84
+ const solid = o.solid ?? 0, ac = o.ac ?? 0;
85
+ 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 };
86
+ };
87
+ const chaves = []; // [{a, b, s}]: de a até b o portador vai para s
88
+ const vai = (a, b, s) => chaves.push({ a, b, s });
89
+ const batidas = P.batidas.map((b, bi) => {
90
+ const fr = (papel) => b.frases.filter((f) => f.papel === papel);
91
+ const [pr] = fr('principal');
92
+ const out = { b, blocos: [], extra: null };
93
+ /* A COMPOSIÇÃO: com cartão, o texto vai para um lado (ou para cima, em
94
+ pé) e o cartão para o outro; sem cartão, o centro. */
95
+ const lado = b.lado || 'centro';
96
+ const tx = !deitado || lado === 'centro' ? cx : lado === 'esq' ? cx - W * 0.21 : cx + W * 0.21;
97
+ const ty = deitado || lado === 'centro' ? cy : cy - H * 0.2;
98
+ const larg = lado === 'centro' ? W * (deitado ? 0.72 : 0.86) : deitado ? W * 0.36 : W * 0.86;
99
+ if (b.ui) {
100
+ const cw = deitado ? W * 0.34 : W * 0.8, ch = cw * (b.ui.tipo === 'terminal' ? 0.6 : 0.7);
101
+ 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 };
102
+ }
103
+ let ent = b.transicao === 'corte' ? [b.de, b.de] : [Math.max(0, b.de - 0.22), b.de + 0.45];
104
+ // A primeira batida (sem ser abertura) nasce de um ponto, que então vira a forma dela.
105
+ if (bi === 0 && b.tipo !== 'abertura') {
106
+ vai(0, 0.01, forma(tx, ty, 0, 0, { solid: 1, ac: 1, op: 0 }));
107
+ vai(0.02, 0.3, forma(tx, ty, 16 * u, 16 * u, { solid: 1, ac: 1 }));
108
+ ent = [0.3, 0.8];
109
+ }
110
+ const poe = (bl) => { out.blocos.push(bl); return bl; };
111
+ switch (b.tipo) {
112
+ case 'abertura': {
113
+ const t = pr && poe(bloco(P, pr, { tam: (lado === 'centro' ? 96 : 80) * u, max: larg, cx: tx, cy: ty, cor: T }));
114
+ const [sb] = fr('sub');
115
+ 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 }));
116
+ const y = ty - (t ? t.h / 2 : 0) - 80 * u;
117
+ vai(0, 0.01, forma(tx, y, 0, 0, { solid: 1, ac: 1, op: 0 }));
118
+ vai(0.02, 0.45, forma(tx, y, 18 * u, 18 * u, { solid: 1, ac: 1 }));
119
+ break;
120
+ }
121
+ case 'numero': {
122
+ const [nb] = fr('numero'), [rot] = fr('rotulo');
123
+ const n = nb && poe(bloco(P, nb, { tam: 280 * u, peso: 300, cx: tx, cy: ty - 40 * u, cor: T }));
124
+ const d = Math.max(n ? n.w : 0, 260 * u) + 140 * u;
125
+ if (rot) poe(bloco(P, rot, { tam: 40 * u, max: larg, cx: tx, cy: ty - 40 * u + d / 2 + 64 * u, cor: S }));
126
+ vai(ent[0], ent[1], forma(tx, ty - 40 * u, d, d, { stroke: 0.55 }));
127
+ break;
128
+ }
129
+ case 'lista': {
130
+ const its = fr('item');
131
+ const tt = pr && poe(bloco(P, pr, { tam: 60 * u, max: W * 0.8, cx, cy: cy - (deitado ? 200 : 380) * u, cor: T }));
132
+ const tam = 42 * u, padX = 36 * u, ph = tam * 2.2, ic = 30 * u;
133
+ const ws = its.map((f) => medir(f.palavras.map((w) => w.t).join(' '), tam, 400, false, P) + padX * 2 + ic + 14 * u);
134
+ const gap = 70 * u;
135
+ const total = ws.reduce((s, w) => s + w, 0) + gap * (ws.length - 1);
136
+ const linha = deitado && total < W * 0.94;
137
+ let x = cx - total / 2;
138
+ const pilulas = its.map((f, j) => {
139
+ let pcx, pcy;
140
+ if (linha) { pcx = x + ws[j] / 2; pcy = cy + 30 * u + (j % 2 ? 90 : -10) * u; x += ws[j] + gap; }
141
+ else { pcx = cx + (j % 2 ? 1 : -1) * Math.min(W * 0.14, 150 * u); pcy = cy - 160 * u + j * (ph + 90 * u); }
142
+ const bl = bloco(P, f, { tam, cx: pcx + (ic + 14 * u) / 2, cy: pcy, cor: T });
143
+ return { f, x: pcx, y: pcy, w: ws[j], h: ph, bl, ic };
144
+ });
145
+ out.blocos.push(...pilulas.map((p) => p.bl));
146
+ out.extra = { pilulas, tt };
147
+ // O portador vira a PRIMEIRA pílula; as outras nascem na ponta da curva.
148
+ const p0 = pilulas[0];
149
+ if (p0) vai(ent[0], ent[1], forma(p0.x, p0.y, p0.w, p0.h, { fill: 1, stroke: 0.35 }));
150
+ else vai(ent[0], ent[1], forma(cx, cy, 12 * u, 12 * u, { solid: 1, ac: 1 }));
151
+ break;
152
+ }
153
+ case 'passos': {
154
+ const its = fr('item');
155
+ const tam = 68 * u;
156
+ const tY = deitado ? cy : cy - 420 * u;
157
+ 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 }));
158
+ 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 }));
159
+ out.extra = { roda, tt, tam };
160
+ vai(ent[0], ent[1], forma(cx, deitado ? cy : cy - 260 * u, 14 * u, 14 * u, { solid: 1, ac: 1 }));
161
+ break;
162
+ }
163
+ case 'contraste': {
164
+ const [nao] = fr('nao'), [sim] = fr('sim');
165
+ const tam = 76 * u;
166
+ const nb = nao && poe(bloco(P, nao, { tam: tam * 0.8, max: larg, cx, cy: cy - (deitado ? 90 : 160) * u, cor: S }));
167
+ const sb = sim && poe(bloco(P, sim, { tam, max: larg, cx, cy: cy + (deitado ? 70 : 120) * u, cor: T }));
168
+ if (nb) {
169
+ const fimNao = nao.palavras[nao.palavras.length - 1].em + 0.3;
170
+ const ly = nb.top + nb.h - nb.lh / 2;
171
+ vai(ent[0], ent[1], forma(nb.left - 22 * u, ly, 12 * u, 12 * u, { solid: 1 }));
172
+ vai(fimNao, fimNao + 0.4, forma(nb.left + nb.w / 2, ly, nb.w + 36 * u, 4 * u, { solid: 0.8, r: 3 }));
173
+ out.risca = fimNao + 0.2;
174
+ }
175
+ if (sb) {
176
+ const ly = sb.top + sb.h + 14 * u;
177
+ 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 }));
178
+ }
179
+ break;
180
+ }
181
+ case 'citacao': {
182
+ const [au] = fr('autor');
183
+ 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 });
184
+ if (q) poe(q);
185
+ const a = au && poe(bloco(P, au, { tam: 30 * u, cx, cy: (q ? q.top + q.h : cy) + 44 * u, cor: S }));
186
+ const w = (q ? q.w : 300 * u) + 160 * u, h = (q ? q.h : 60 * u) + (a ? 80 : 20) * u + 120 * u;
187
+ vai(ent[0], ent[1], forma(cx, cy + (a ? 10 * u : 0), w, h, { fill: 1, stroke: 0.2, r: 44 * u }));
188
+ break;
189
+ }
190
+ case 'cta': {
191
+ const [nome] = fr('marca');
192
+ const tam = 120 * u;
193
+ const nw = nome ? medir(b.marca.nome, tam, 600, false, P) : 0;
194
+ const mk = 190 * u, gap = 44 * u;
195
+ const caber = Math.min(1, (W * 0.9 - mk - gap) / Math.max(1, nw));
196
+ const largMarca = mk + gap + nw * caber;
197
+ const x0 = cx - largMarca / 2;
198
+ const nb = nome && poe(bloco(P, nome, { tam: tam * caber, peso: 600, cx: x0 + mk + gap, cy: cy - 30 * u, alinha: 'esq', cor: T }));
199
+ let y = cy + 130 * u;
200
+ 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; }
201
+ // O link vira um comando numa pílula, em mono.
202
+ for (const f of fr('sub')) out.comando = poe(bloco(P, f, { tam: 32 * u, cx, cy: y, cor: T, mono: true }));
203
+ // A marca se monta de três formas: o portador (círculo cheio), um anel e um ponto.
204
+ const mx = x0 + mk / 2, my = cy - 30 * u, q = mk / 190;
205
+ vai(ent[0], ent[1], forma(cx, cy, 80 * q, 80 * q, { solid: 1, ac: 1 }));
206
+ vai(b.de + 0.1, b.de + 0.5, forma(mx - 42 * q, my - 22 * q, 84 * q, 84 * q, { solid: 1, ac: 1 }));
207
+ out.extra = { mx, my, mk, nb, pecas: [
208
+ { 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 },
209
+ { 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 },
210
+ ] };
211
+ break;
212
+ }
213
+ default: { // titulo: a pílula que envolve a frase; em duas linhas, a barra de acento ao lado
214
+ let t = pr && bloco(P, pr, { tam: 76 * u, max: larg, cx: tx, cy: ty, cor: T });
215
+ // Em mais de uma linha o texto alinha à esquerda, junto da barra de acento.
216
+ 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 });
217
+ if (t) poe(t);
218
+ const [sb] = fr('sub');
219
+ const umaLinha = !t || t.linhas.length === 1;
220
+ const h = umaLinha ? (t ? t.tam * 2 : 60 * u) : t.h;
221
+ if (sb) poe(umaLinha ? bloco(P, sb, { tam: 34 * u, max: larg, cx: tx, cy: ty + h / 2 + 62 * u, cor: S })
222
+ : bloco(P, sb, { tam: 34 * u, max: larg, cx: t.left, cy: ty + h / 2 + 50 * u, alinha: 'esq', cor: S }));
223
+ if (umaLinha) vai(ent[0], ent[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
224
+ else vai(ent[0], ent[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
225
+ }
226
+ }
227
+ return out;
228
+ });
229
+ return { batidas, chaves, u, W, H, deitado };
230
+ }
231
+
232
+ /* O portador no tempo t: a última chave começada, saindo da anterior. */
233
+ function portadorEm(t, chaves) {
234
+ let i = -1;
235
+ while (i + 1 < chaves.length && chaves[i + 1].a <= t) i++;
236
+ if (i < 0) return { ...chaves[0].s, op: 0 };
237
+ const c = chaves[i], ant = i > 0 ? chaves[i - 1].s : { ...c.s, op: 0 };
238
+ const k = c.b <= c.a ? 1 : cam(c01((t - c.a) / (c.b - c.a)));
239
+ const s = {};
240
+ for (const key of Object.keys(c.s)) s[key] = lerp(ant[key], c.s[key], k);
241
+ return s;
242
+ }
243
+
244
+ // ── a palavra: entra quando é dita ───────────────────────────────────────
245
+ function Palavras({ bl, sai, t, P, op = 1, de = null }) {
246
+ const pal = P.paleta;
247
+ let n = 0;
248
+ return bl.linhas.map((l, li) => (
249
+ <div key={li} style={{ position: 'absolute', left: l.x, top: l.y, height: bl.lh, lineHeight: bl.lh + 'px', whiteSpace: 'nowrap' }}>
250
+ {l.palavras.map((w, wi) => {
251
+ const em = de != null ? de + (n++) * 0.07 : w.em;
252
+ const k = ed(c01((t - em) / 0.5));
253
+ const o = entra(c01((t - sai) / 0.35));
254
+ const vis = k * (1 - o) * op;
255
+ const mr = wi < l.palavras.length - 1 ? bl.esp : 0;
256
+ if (vis <= 0.001) return <span key={wi} style={{ display: 'inline-block', width: w.w, marginRight: mr }} />;
257
+ return (
258
+ <span key={wi} style={{
259
+ display: 'inline-block', marginRight: mr,
260
+ opacity: vis, transform: 'translateY(' + ((1 - k) * 0.4 - o * 0.25) * bl.tam + 'px)',
261
+ filter: (1 - k) + o > 0.02 ? 'blur(' + ((1 - k) * 8 + o * 6).toFixed(2) + 'px)' : undefined,
262
+ fontFamily: w.acento ? SERIF(P.fontes.serifa) : bl.mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto), fontStyle: w.acento ? 'italic' : 'normal',
263
+ fontWeight: w.acento ? 400 : bl.peso, fontSize: w.acento ? bl.tam * 1.14 : bl.tam, letterSpacing: w.acento || bl.mono ? 0 : '-0.02em',
264
+ color: w.acento ? pal.acentos[0] : bl.cor,
265
+ }}>{w.t}</span>
266
+ );
267
+ })}
268
+ </div>
269
+ ));
270
+ }
271
+
272
+ const numeroTexto = (n, k) => n.antes + (n.ate * k).toLocaleString('pt-BR', { minimumFractionDigits: n.casas, maximumFractionDigits: n.casas }) + n.depois;
273
+
274
+ // ── o cartão de produto: terminal, chat ou painel, de forma e texto ─────
275
+ function Cartao({ c, ui, de: de0, sai, t, P, u: u0 }) {
276
+ const pal = P.paleta, T = pal.tinta, A = pal.acentos[0];
277
+ // O conteúdo escala com o cartão (em pé ele é mais largo); entra logo depois da troca.
278
+ const u = u0 * c.w / (650 * u0), de = de0 + 0.2;
279
+ const k = ed(c01((t - de - 0.05) / 0.6)), o = entra(c01((t - sai) / 0.35));
280
+ const vis = k * (1 - o);
281
+ if (vis <= 0.001) return null;
282
+ const vidro = pal.escuro ? alfa(T, 0.06) : alfa('#ffffff', 0.78);
283
+ const linha = alfa(T, pal.escuro ? 0.14 : 0.1);
284
+ const pele = alfa(T, pal.escuro ? 0.2 : 0.1);
285
+ 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 }} />;
286
+ const pad = 34 * u, mono = { fontFamily: MONO(P.fontes.mono), fontSize: 26 * u, color: T, whiteSpace: 'nowrap', position: 'absolute' };
287
+ let corpo = null;
288
+ if (ui.tipo === 'terminal') {
289
+ const linhas = ui.linhas.map((ln, j) => {
290
+ const t0 = de + 0.55 + j * 0.5, n = Math.round(c01((t - t0) / 0.4) * ln.length);
291
+ if (n <= 0) return null;
292
+ const txt = ln.slice(0, n), ok = txt.startsWith('✓');
293
+ return <div key={j} style={{ ...mono, left: pad, top: 70 * u + j * 48 * u }}>
294
+ {ok ? <span style={{ color: A }}>✓</span> : null}{ok ? txt.slice(1) : txt}
295
+ {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}
296
+ </div>;
297
+ });
298
+ const kk = (j) => ed(c01((t - de - 1.4 - j * 0.15) / 0.6));
299
+ const y0 = 70 * u + ui.linhas.length * 48 * u + 16 * u;
300
+ 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))}</>;
301
+ } else if (ui.tipo === 'chat') {
302
+ 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));
303
+ corpo = <>
304
+ <div style={{ position: 'absolute', right: pad, top: 74 * u, maxWidth: c.w * 0.72, padding: (14 * u) + 'px ' + (22 * u) + 'px', borderRadius: 26 * u,
305
+ 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>
306
+ <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,
307
+ transform: 'translateY(' + (1 - k2) * 20 * u + 'px)' }}>
308
+ {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,
309
+ background: alfa(T, 0.35 + 0.4 * (0.5 + 0.5 * Math.sin(t * 9 - j))) }} />) : null}
310
+ {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)}
311
+ </div>
312
+ </>;
313
+ } else {
314
+ const bs = ui.barras || [];
315
+ const passo = (c.w - pad * 2) / Math.max(1, bs.length), base = c.h - pad;
316
+ corpo = <>
317
+ <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>
318
+ <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 }} />
319
+ {bs.map((v, j) => {
320
+ const kb = ed(c01((t - de - 0.55 - j * 0.09) / 0.7)), h = (c.h - 170 * u) * v * kb;
321
+ return <div key={j} style={{ position: 'absolute', left: pad + j * passo, top: base - h, width: passo * 0.62, height: h, borderRadius: 8 * u,
322
+ background: j === bs.length - 1 ? A : pele }} />;
323
+ })}
324
+ {barra(pad, base + 6 * u, c.w - pad * 2, 2 * u, linha)}
325
+ </>;
326
+ }
327
+ // Flutua: entra de baixo e de longe, e segue derivando (nunca para).
328
+ const gy = Math.sin(t * 0.9) * 8 * u, gr = Math.sin(t * 0.55) * 2.5;
329
+ return (
330
+ <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',
331
+ background: vidro, border: (1.2 * u) + 'px solid ' + linha, backdropFilter: 'blur(' + 22 * u + 'px)', boxSizing: 'border-box',
332
+ boxShadow: '0 ' + 40 * u + 'px ' + 80 * u + 'px -' + 30 * u + 'px ' + alfa(pal.escuro ? '#000000' : T, pal.escuro ? 0.55 : 0.28),
333
+ opacity: vis, filter: vis < 0.98 ? 'blur(' + ((1 - vis) * 8).toFixed(2) + 'px)' : undefined,
334
+ 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) + ')' }}>
335
+ <div style={{ position: 'absolute', left: 0, right: 0, top: 0, height: 48 * u, borderBottom: (1 * u) + 'px solid ' + linha }}>
336
+ {[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) }} />)}
337
+ </div>
338
+ {corpo}
339
+ </div>
340
+ );
341
+ }
342
+
343
+ // ── a peça ───────────────────────────────────────────────────────────────
344
+ export function Fluido({ el, t }) {
345
+ const P = el.partitura;
346
+ const pronto = useFontes(P);
347
+ const L = useMemo(() => (pronto ? leiaute(P) : null), [pronto]);
348
+ const pal = P.paleta, W = P.largura, H = P.altura;
349
+ const [f0, f1, f2] = pal.fundo;
350
+ const fundo = 'linear-gradient(158deg, ' + f0 + ' 0%, ' + f1 + ' 58%, ' + f2 + ' 100%)';
351
+ if (!L) return <div style={{ position: 'absolute', inset: 0, background: fundo }} />;
352
+ const { u } = L;
353
+ const s = portadorEm(t, L.chaves);
354
+ const atras = portadorEm(Math.max(0, t - 0.9), L.chaves);
355
+ // O brilho difuso segue o portador com atraso; uma segunda mancha deriva devagar.
356
+ const gx = (atras.x / W) * 100, gy = (atras.y / H) * 100;
357
+ const dx = 70 + Math.sin(t * 0.21) * 14, dy = 20 + Math.cos(t * 0.17) * 12;
358
+ const brilho = alfa(pal.acentos[1], pal.escuro ? 0.34 : 0.28);
359
+ const sup = pal.escuro ? alfa(pal.tinta, 0.07) : alfa('#ffffff', 0.62);
360
+ /* A câmera nunca para: em cada batida ela avança um pouco (e só avança —
361
+ a soma é contínua na troca), deriva devagar e puxa de leve para o portador. */
362
+ let bi = 0;
363
+ while (bi < P.batidas.length - 1 && t >= P.batidas[bi + 1].de) bi++;
364
+ const bt = P.batidas[bi];
365
+ const z = 1 + 0.022 * (bi + cam(c01((t - bt.de) / Math.max(0.5, bt.ate - bt.de))));
366
+ 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;
367
+ // O portador respira.
368
+ const resp = 1 + 0.018 * Math.sin(t * 2.1);
369
+ const ult = P.batidas[P.batidas.length - 1];
370
+ // Com logo, o portador some quando o logo assume a marca.
371
+ const so = P.logo && ult.tipo === 'cta' ? 1 - ed(c01((t - ult.de - 0.65) / 0.5)) : 1;
372
+
373
+ const camadas = [];
374
+ L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando }, i) => {
375
+ const corte = b.transicao === 'corte' || (L.batidas[i + 1] && L.batidas[i + 1].b.transicao === 'corte');
376
+ const ultima = i === L.batidas.length - 1;
377
+ const sai = ultima ? 1e9 : b.ate - (corte ? 0 : 0.15), fimVis = b.ate + (corte ? 0 : 0.4);
378
+ if (t < b.de - 0.3 || t > fimVis || (corte && t >= b.ate && !ultima)) return;
379
+ if (cartao) camadas.push(<Cartao key={'k' + i} c={cartao} ui={b.ui} de={b.de} sai={sai} t={t} P={P} u={u} />);
380
+ // A lista: as curvas que se desenham de uma pílula à próxima, e as pílulas que nascem na ponta.
381
+ if (b.tipo === 'lista' && extra) {
382
+ const ps = extra.pilulas;
383
+ const o = 1 - entra(c01((t - sai) / 0.35));
384
+ ps.forEach((p, j) => {
385
+ if (j > 0) {
386
+ const a = ps[j - 1], c = p;
387
+ const x0 = a.x + a.w / 2, y0 = a.y, x1 = c.x - c.w / 2, y1 = c.y;
388
+ const vert = Math.abs(x1 - x0) < 60 * u || x1 < x0;
389
+ const d = vert
390
+ ? '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)
391
+ : 'M ' + x0 + ' ' + y0 + ' C ' + (x0 + (x1 - x0) * 0.6) + ' ' + y0 + ' ' + (x1 - (x1 - x0) * 0.6) + ' ' + y1 + ' ' + x1 + ' ' + y1;
392
+ const k = ed(c01((t - (p.f.de - 0.4)) / 0.45));
393
+ camadas.push(<svg key={'c' + i + j} width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', opacity: o }}>
394
+ <path d={d} fill="none" stroke={alfa(pal.tinta, 0.5)} strokeWidth={2 * u} pathLength={1} strokeDasharray="1 1" strokeDashoffset={1 - k} />
395
+ </svg>);
396
+ const kp = ed(c01((t - (p.f.de - 0.1)) / 0.45));
397
+ 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,
398
+ background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kp * o, transform: 'scale(' + (0.7 + 0.3 * kp) + ')',
399
+ boxShadow: pal.escuro ? undefined : '0 14px 34px -14px ' + alfa(pal.tinta, 0.3) }} />);
400
+ }
401
+ // O ícone de cada pílula: um brilho de quatro pontas que gira ao nascer.
402
+ const kd = ed(c01((t - p.f.de) / 0.5));
403
+ camadas.push(<svg key={'d' + i + j} width={p.ic} height={p.ic} viewBox="-1 -1 2 2" style={{ position: 'absolute', left: p.x - p.w / 2 + 34 * u, top: p.y - p.ic / 2,
404
+ opacity: kd * o, transform: 'rotate(' + ((1 - kd) * -90 + Math.sin(t * 1.3 + j) * 6) + 'deg) scale(' + kd + ')' }}>
405
+ <path d={BRILHO4} fill={pal.acentos[0]} />
406
+ </svg>);
407
+ });
408
+ }
409
+ for (const bl of blocos) {
410
+ const f = bl.f;
411
+ if (b.tipo === 'passos' && extra) continue;
412
+ const op = f.papel === 'nao' && risca ? 1 - 0.55 * ed(c01((t - risca) / 0.4)) : 1;
413
+ if (f.papel === 'numero' && b.numero) {
414
+ const k = ed(c01((t - f.de) / 1.1));
415
+ const vis = ed(c01((t - f.de) / 0.45)) * (1 - entra(c01((t - sai) / 0.35)));
416
+ 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',
417
+ fontFamily: SANS(P.fontes.texto), fontWeight: 300, fontSize: bl.tam, letterSpacing: '-0.05em', color: pal.tinta, opacity: vis,
418
+ transform: 'translateY(' + (1 - vis) * 40 * u + 'px)', filter: vis < 0.98 ? 'blur(' + ((1 - vis) * 10).toFixed(2) + 'px)' : undefined,
419
+ fontVariantNumeric: 'tabular-nums' }}>{numeroTexto(b.numero, k)}</div>);
420
+ continue;
421
+ }
422
+ if (bl === comando) {
423
+ // O comando numa pílula própria, que acende quando ele entra.
424
+ const kc = ed(c01((t - f.de + 0.1) / 0.45));
425
+ 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,
426
+ borderRadius: 999, background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kc, transform: 'scale(' + (0.85 + 0.15 * kc) + ')' }} />);
427
+ }
428
+ camadas.push(<React.Fragment key={'t' + i + f.id}><Palavras bl={bl} sai={sai} t={t} P={P} op={op} /></React.Fragment>);
429
+ }
430
+ // A roda dos passos: o item ativo nítido, os outros girando para trás, transparentes e desfocados.
431
+ if (b.tipo === 'passos' && extra) {
432
+ if (extra.tt) camadas.push(<React.Fragment key={'pt' + i}><Palavras bl={extra.tt} sai={sai} t={t} P={P} /></React.Fragment>);
433
+ const its = extra.roda;
434
+ let at = 0;
435
+ its.forEach((bl, j) => { if (j > 0) at += cam(c01((t - (bl.f.de - 0.25)) / 0.45)); });
436
+ const o = (1 - entra(c01((t - sai) / 0.35))) * ed(c01((t - (its[0] ? its[0].f.de - 0.2 : b.de)) / 0.5));
437
+ its.forEach((bl, j) => {
438
+ const off = j - at, a = Math.abs(off);
439
+ if (a > 3.2) return;
440
+ const vis = Math.max(0, 1 - a * 0.3) * o;
441
+ const k = ed(c01((t - bl.f.de) / 0.5));
442
+ const op = Math.max(vis * (0.4 + 0.6 * c01(1 - a)), a < 0.5 ? vis * k : 0);
443
+ camadas.push(<div key={'r' + i + j} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: op,
444
+ 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)',
445
+ transformOrigin: '50% ' + bl.cy + 'px', filter: a > 0.05 ? 'blur(' + (a * 3.5).toFixed(2) + 'px)' : undefined }}>
446
+ <Palavras bl={bl} de={-10} sai={1e9} t={t} P={P} />
447
+ </div>);
448
+ });
449
+ }
450
+ // A marca: um anel e um ponto chegam e travam no portador; com logo, as formas viram o logo.
451
+ if (b.tipo === 'cta' && extra) {
452
+ const trava = b.de + 0.5;
453
+ const kl = P.logo ? ed(c01((t - trava - 0.15) / 0.5)) : 0;
454
+ extra.pecas.forEach((pc, j) => {
455
+ const k = cam(c01((t - (b.de - 0.1 + j * 0.08)) / 0.55));
456
+ const q = {}; for (const key of Object.keys(pc.ate)) q[key] = lerp(pc.de[key], pc.ate[key], k);
457
+ const ent = c01((t - b.de + 0.2) / 0.3) * (1 - kl);
458
+ 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,
459
+ background: pc.anel ? 'transparent' : pal.tinta, border: pc.anel ? (8 * u) + 'px solid ' + pal.tinta : undefined, opacity: ent, boxSizing: 'border-box' }} />);
460
+ });
461
+ // O pulso de quando trava (o impacto do som).
462
+ const kp = c01((t - trava) / 0.7);
463
+ if (kp > 0 && kp < 1) {
464
+ const r = extra.mk * (0.5 + kp * 0.6);
465
+ 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,
466
+ border: (2 * u) + 'px solid ' + alfa(pal.acentos[0], 0.6 * (1 - kp)) }} />);
467
+ }
468
+ 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,
469
+ opacity: kl, transform: 'scale(' + (0.8 + 0.2 * kl) + ')' }}><Img src={staticFile(P.logo)} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /></div>);
470
+ }
471
+ });
472
+
473
+ return (
474
+ <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', background: fundo }}>
475
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 70% 60% at ' + gx + '% ' + gy + '%, ' + brilho + ' 0%, transparent 70%)' }} />
476
+ <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%)' }} />
477
+ <div style={{ position: 'absolute', inset: 0, transform: 'translate(' + px + 'px,' + py + 'px) scale(' + z + ')', transformOrigin: '50% 50%' }}>
478
+ <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,
479
+ background: pal.escuro ? alfa(pal.tinta, 0.07 * s.fill) : alfa('#ffffff', 0.62 * s.fill),
480
+ border: s.stroke > 0.01 ? (1.6 * u) + 'px solid ' + alfa(pal.tinta, s.stroke * (pal.escuro ? 0.6 : 0.55)) : undefined,
481
+ boxShadow: !pal.escuro && s.fill > 0.5 && s.h > 30 * u ? '0 20px 50px -24px ' + alfa(pal.tinta, 0.35 * s.fill) : undefined,
482
+ opacity: s.op * so, boxSizing: 'border-box' }}>
483
+ <div style={{ position: 'absolute', inset: 0, borderRadius: s.r, background: alfa(pal.tinta, s.si) }} />
484
+ <div style={{ position: 'absolute', inset: 0, borderRadius: s.r, background: alfa(pal.acentos[0], s.sa),
485
+ boxShadow: s.sa > 0.05 && s.h < 100 * u ? '0 0 ' + 30 * u + 'px ' + alfa(pal.acentos[0], 0.55 * s.sa) : undefined }} />
486
+ </div>
487
+ {camadas}
488
+ </div>
489
+ </div>
490
+ );
491
+ }
492
+
493
+ export const FLUIDO = { fluido: Fluido };