primocode 9.10.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 (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
@@ -0,0 +1,215 @@
1
+ /**
2
+ * glifos.js — as letras de uma palavra como VETOR de verdade: o contorno, as
3
+ * âncoras e as alças, lidos da própria fonte TrueType.
4
+ *
5
+ * A cena do brand reveal que mais vende "feito por designer" é a palavra
6
+ * enorme em contorno, com os quadradinhos das âncoras, as alças de bezier e o
7
+ * cursor passeando. Isso só convence com os pontos REAIS da letra — pontos
8
+ * inventados em cima de um <text> denunciam na hora. Então o PrimoCode lê a
9
+ * fonte: a tabela glyf de um .ttf guarda cada contorno como pontos na curva
10
+ * (âncoras) e fora dela (controles de quadrática, as alças).
11
+ *
12
+ * Zero dependência: um leitor de TrueType de ~150 linhas (cmap formatos 4 e
13
+ * 12, loca, glyf simples e composto, hmtx). A fonte vem do Google Fonts
14
+ * (a API antiga devolve .ttf) e fica em cache; sem rede, `palavra()` devolve
15
+ * null e a peça "vetor" desenha o contorno do próprio texto, sem alças.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+
24
+ // ── o leitor ─────────────────────────────────────────────────────────────
25
+ function tabelas(buf) {
26
+ const n = buf.readUInt16BE(4);
27
+ const t = {};
28
+ for (let i = 0; i < n; i++) {
29
+ const o = 12 + i * 16;
30
+ t[buf.toString('latin1', o, o + 4)] = { off: buf.readUInt32BE(o + 8), len: buf.readUInt32BE(o + 12) };
31
+ }
32
+ return t;
33
+ }
34
+
35
+ /** Caractere → índice do glifo (cmap 3/10 formato 12, ou 3/1 e 0/x formato 4). */
36
+ function mapaDeCaracteres(buf, cmap) {
37
+ const n = buf.readUInt16BE(cmap.off + 2);
38
+ const subs = [];
39
+ for (let i = 0; i < n; i++) {
40
+ const o = cmap.off + 4 + i * 8;
41
+ subs.push({ plat: buf.readUInt16BE(o), enc: buf.readUInt16BE(o + 2), off: cmap.off + buf.readUInt32BE(o + 4) });
42
+ }
43
+ subs.sort((a, b) => buf.readUInt16BE(b.off) - buf.readUInt16BE(a.off)); // o formato 12 primeiro
44
+ for (const s of subs) {
45
+ const fmt = buf.readUInt16BE(s.off);
46
+ if (fmt === 12) {
47
+ const grupos = buf.readUInt32BE(s.off + 12);
48
+ return (cp) => {
49
+ for (let g = 0; g < grupos; g++) {
50
+ const o = s.off + 16 + g * 12;
51
+ const ini = buf.readUInt32BE(o), fim = buf.readUInt32BE(o + 4);
52
+ if (cp >= ini && cp <= fim) return buf.readUInt32BE(o + 8) + (cp - ini);
53
+ }
54
+ return 0;
55
+ };
56
+ }
57
+ if (fmt === 4) {
58
+ const segs = buf.readUInt16BE(s.off + 6) / 2;
59
+ const fins = s.off + 14, inis = fins + segs * 2 + 2, deltas = inis + segs * 2, ranges = deltas + segs * 2;
60
+ return (cp) => {
61
+ for (let i = 0; i < segs; i++) {
62
+ const fim = buf.readUInt16BE(fins + i * 2);
63
+ if (cp > fim) continue;
64
+ const ini = buf.readUInt16BE(inis + i * 2);
65
+ if (cp < ini) return 0;
66
+ const delta = buf.readInt16BE(deltas + i * 2), ro = buf.readUInt16BE(ranges + i * 2);
67
+ if (!ro) return (cp + delta) & 0xffff;
68
+ const g = buf.readUInt16BE(ranges + i * 2 + ro + (cp - ini) * 2);
69
+ return g ? (g + delta) & 0xffff : 0;
70
+ }
71
+ return 0;
72
+ };
73
+ }
74
+ }
75
+ return () => 0;
76
+ }
77
+
78
+ /** Os contornos de um glifo: [[{x, y, on}]], já com os componentes do composto. */
79
+ function contornos(f, gid, profundidade = 0) {
80
+ const a = f.loca(gid), b = f.loca(gid + 1);
81
+ if (b <= a || profundidade > 4) return [];
82
+ const buf = f.buf, o = f.glyf + a;
83
+ const nc = buf.readInt16BE(o);
84
+ if (nc < 0) {
85
+ // Composto: cada componente é outro glifo, deslocado (e às vezes escalado).
86
+ const saida = [];
87
+ let p = o + 10, flags;
88
+ do {
89
+ flags = buf.readUInt16BE(p); const g = buf.readUInt16BE(p + 2); p += 4;
90
+ let dx, dy;
91
+ if (flags & 1) { dx = buf.readInt16BE(p); dy = buf.readInt16BE(p + 2); p += 4; } else { dx = buf.readInt8(p); dy = buf.readInt8(p + 1); p += 2; }
92
+ let sx = 1, sy = 1;
93
+ if (flags & 8) { sx = sy = buf.readInt16BE(p) / 16384; p += 2; }
94
+ else if (flags & 0x40) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 2) / 16384; p += 4; }
95
+ else if (flags & 0x80) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 6) / 16384; p += 8; }
96
+ if (!(flags & 2)) { dx = 0; dy = 0; } // pontos casados em vez de deslocamento: raro em fonte do Google
97
+ for (const c of contornos(f, g, profundidade + 1)) saida.push(c.map((q) => ({ x: q.x * sx + dx, y: q.y * sy + dy, on: q.on })));
98
+ } while (flags & 0x20);
99
+ return saida;
100
+ }
101
+ const fins = [];
102
+ for (let i = 0; i < nc; i++) fins.push(buf.readUInt16BE(o + 10 + i * 2));
103
+ const n = nc ? fins[nc - 1] + 1 : 0;
104
+ let p = o + 10 + nc * 2;
105
+ p += 2 + buf.readUInt16BE(p); // pula as instruções
106
+ const fl = [];
107
+ while (fl.length < n) {
108
+ const f0 = buf.readUInt8(p++); fl.push(f0);
109
+ if (f0 & 8) { let r = buf.readUInt8(p++); while (r-- > 0) fl.push(f0); }
110
+ }
111
+ const coord = (curto, igual) => {
112
+ const v = []; let atual = 0;
113
+ for (let i = 0; i < n; i++) {
114
+ const q = fl[i];
115
+ if (q & curto) { const d = buf.readUInt8(p++); atual += q & igual ? d : -d; }
116
+ else if (!(q & igual)) { atual += buf.readInt16BE(p); p += 2; }
117
+ v.push(atual);
118
+ }
119
+ return v;
120
+ };
121
+ const xs = coord(2, 16), ys = coord(4, 32);
122
+ const saida = [];
123
+ let ini = 0;
124
+ for (const fim of fins) {
125
+ const c = [];
126
+ for (let i = ini; i <= fim; i++) c.push({ x: xs[i], y: ys[i], on: Boolean(fl[i] & 1) });
127
+ saida.push(c);
128
+ ini = fim + 1;
129
+ }
130
+ return saida;
131
+ }
132
+
133
+ function abrirFonte(buf) {
134
+ const t = tabelas(buf);
135
+ if (!t.glyf || !t.loca || !t.head || !t.cmap || !t.hmtx || !t.hhea) throw new Error('não é uma fonte TrueType com contornos (glyf)');
136
+ const longo = buf.readInt16BE(t.head.off + 50) === 1;
137
+ const nMetr = buf.readUInt16BE(t.hhea.off + 34);
138
+ return {
139
+ buf, glyf: t.glyf.off,
140
+ porEm: buf.readUInt16BE(t.head.off + 18),
141
+ loca: (i) => (longo ? buf.readUInt32BE(t.loca.off + i * 4) : buf.readUInt16BE(t.loca.off + i * 2) * 2),
142
+ avanco: (g) => buf.readUInt16BE(t.hmtx.off + Math.min(g, nMetr - 1) * 4),
143
+ glifo: mapaDeCaracteres(buf, t.cmap),
144
+ };
145
+ }
146
+
147
+ const r1 = (n) => Math.round(n * 10) / 10;
148
+
149
+ /**
150
+ * A palavra desenhada: {d, pontos: [[x, y, naCurva, contorno]], caixa: [x, y, w, h]}.
151
+ * Coordenadas em unidades da fonte, com y para baixo (como no SVG).
152
+ */
153
+ function desenhar(buf, texto, { espacamento = 0 } = {}) {
154
+ const f = abrirFonte(buf);
155
+ let x = 0, nC = 0;
156
+ const d = [], pontos = [];
157
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
158
+ for (const ch of String(texto)) {
159
+ const g = f.glifo(ch.codePointAt(0));
160
+ for (const c of contornos(f, g)) {
161
+ if (c.length < 2) continue;
162
+ const P = c.map((q) => ({ x: r1(q.x + x), y: r1(-q.y), on: q.on }));
163
+ for (const q of P) {
164
+ pontos.push([q.x, q.y, q.on ? 1 : 0, nC]);
165
+ if (q.x < minX) minX = q.x; if (q.x > maxX) maxX = q.x; if (q.y < minY) minY = q.y; if (q.y > maxY) maxY = q.y;
166
+ }
167
+ // Quadrática com pontos implícitos: entre dois controles seguidos mora uma âncora no meio.
168
+ let k0 = P.findIndex((q) => q.on);
169
+ let inicio = k0 >= 0 ? P[k0] : { x: r1((P[0].x + P[1].x) / 2), y: r1((P[0].y + P[1].y) / 2), on: true };
170
+ if (k0 < 0) k0 = 0;
171
+ const seg = [`M ${inicio.x} ${inicio.y}`];
172
+ let ctrl = null;
173
+ for (let j = 1; j <= P.length; j++) {
174
+ const q = P[(k0 + j) % P.length];
175
+ if (q.on) { seg.push(ctrl ? `Q ${ctrl.x} ${ctrl.y} ${q.x} ${q.y}` : `L ${q.x} ${q.y}`); ctrl = null; }
176
+ else if (ctrl) { const m = { x: r1((ctrl.x + q.x) / 2), y: r1((ctrl.y + q.y) / 2) }; seg.push(`Q ${ctrl.x} ${ctrl.y} ${m.x} ${m.y}`); ctrl = q; }
177
+ else ctrl = q;
178
+ }
179
+ if (ctrl) seg.push(`Q ${ctrl.x} ${ctrl.y} ${inicio.x} ${inicio.y}`);
180
+ d.push(seg.join(' ') + ' Z');
181
+ nC++;
182
+ }
183
+ x += f.avanco(g) + espacamento * f.porEm;
184
+ }
185
+ if (!pontos.length) return null;
186
+ return { d: d.join(' '), pontos, caixa: [r1(minX), r1(minY), r1(maxX - minX), r1(maxY - minY)], porEm: f.porEm };
187
+ }
188
+
189
+ // ── a fonte, do Google Fonts para o cache ────────────────────────────────
190
+ const CACHE = path.join(os.homedir(), '.primocode', 'studio', 'cache-fontes');
191
+
192
+ async function baixarFonte(familia, peso = 700) {
193
+ const nome = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase() + '-' + peso + '.ttf';
194
+ const arq = path.join(CACHE, nome);
195
+ if (fs.existsSync(arq)) return arq;
196
+ // A API antiga (css, não css2) devolve .ttf; a nova devolve woff2, que não tem a glyf crua.
197
+ const css = await (await fetch('https://fonts.googleapis.com/css?family=' + encodeURIComponent(familia) + ':' + peso, { signal: AbortSignal.timeout(10000) })).text();
198
+ const url = (css.match(/url\((https:[^)]+\.ttf)\)/) || [])[1];
199
+ if (!url) return null;
200
+ const bin = Buffer.from(await (await fetch(url, { signal: AbortSignal.timeout(15000) })).arrayBuffer());
201
+ fs.mkdirSync(CACHE, { recursive: true });
202
+ fs.writeFileSync(arq, bin);
203
+ return arq;
204
+ }
205
+
206
+ /** A palavra em vetor na fonte da marca, ou null (sem rede, fonte sem glyf, palavra vazia). */
207
+ async function palavra(texto, { fonte = 'Plus Jakarta Sans', peso = 700 } = {}) {
208
+ if (!String(texto || '').trim()) return null;
209
+ try {
210
+ const arq = await baixarFonte(fonte, peso);
211
+ return arq ? desenhar(fs.readFileSync(arq), texto) : null;
212
+ } catch { return null; }
213
+ }
214
+
215
+ module.exports = { palavra, desenhar, abrirFonte, baixarFonte, CACHE };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * hibrido.js — o cinema com o kit por cima: foto real que não para, e o
3
+ * motion que parece feito à mão.
4
+ *
5
+ * "Deve funcionar como um mix da versão antiga (o reels da imersão) e a
6
+ * última versão — trabalhar juntas, absurdas, coisas que nem parece que
7
+ * foi IA que fez." (29/09)
8
+ *
9
+ * O que cada lado traz:
10
+ * do cinema (cinema.js) foto e vídeo REAIS ocupando a tela, escurecidos,
11
+ * trocando por baixo do mesmo texto; letra grossa em
12
+ * caixa alta com a palavra da tese no gradiente; o
13
+ * número que conta; a virada que bate no fundo; a
14
+ * marca só no fim.
15
+ * do kit (kit/*.jsx) a câmera que empurra e desliza em toda cena; a
16
+ * etiqueta da fonte que se digita; partículas no
17
+ * número e no fim; o HUD de motion designer; o
18
+ * líquido entre blocos; grão e vinheta.
19
+ *
20
+ * Três passos, e nenhum repete o que o cinema já faz:
21
+ * traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
22
+ * tipos do cinema; o que o cinema não usa (a fonte, o
23
+ * destaque) fica guardado por parte.
24
+ * termosDeBusca(roteiro) o que procurar de b-roll para cada parte.
25
+ * enfeitar(plano, …) o kit por cima do plano que o cinema montou.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const r2 = (n) => Math.round(n * 100) / 100;
31
+ const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
32
+ const limpo = (x) => String(x || '').replace(/\s+/g, ' ').trim();
33
+
34
+ /* O b-roll quando a parte não diz o que procurar. Genérico de propósito: é o
35
+ fundo que se move, não a prova — a prova é o texto e a foto da marca. */
36
+ const BUSCA_PADRAO = ['technology abstract light', 'people working on laptop', 'hands typing keyboard close up',
37
+ 'data center servers', 'city night timelapse', 'team meeting office', 'developer code screen', 'futuristic digital network'];
38
+
39
+ /** O roteiro de qualquer estilo → o roteiro do cinema. Devolve {roteiro, extras}. */
40
+ function traduzir(roteiro) {
41
+ const partes = (roteiro && roteiro.partes) || [];
42
+ const saida = [];
43
+ const extras = [];
44
+ // A virada: a parte marcada com climax, ou o último contraste antes do fim.
45
+ let iClimax = partes.findIndex((p) => p.climax);
46
+ if (iClimax < 0) {
47
+ for (let i = partes.length - 2; i > 0; i--) if (partes[i].tipo === 'contraste') { iClimax = i; break; }
48
+ }
49
+ partes.forEach((p, i) => {
50
+ const tipo = p.tipo || 'frase';
51
+ const marcar = [p.destaque, ...(p.marcar || [])].filter(Boolean);
52
+ const base = { fala: p.fala, ...(p.mudo ? { mudo: true } : {}), ...(p.falaForte ? { falaForte: p.falaForte } : {}), marcar };
53
+ let q;
54
+ // Os tipos que o cinema já conhece passam como estão.
55
+ if (['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'].includes(tipo)) q = { ...p };
56
+ else if (i === iClimax) q = { tipo: 'climax', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
57
+ // A abertura é o título, grande; o subtítulo vira a etiqueta digitada (a fonte).
58
+ else if (tipo === 'abertura') q = { tipo: 'montagem', linhas: [limpo(p.titulo)], ritmo: 0.6, ...base };
59
+ else if (tipo === 'titulo' || tipo === 'imagem') q = { tipo: 'montagem', linhas: [p.titulo || p.texto, p.sub].filter(Boolean).map(limpo), ...base };
60
+ else if (tipo === 'numero' || tipo === 'grafico') {
61
+ // "30% mais rápido": o cinema conta o número e escreve o resto embaixo.
62
+ q = { tipo: 'numeros', itens: [limpo(`${p.valor || ''} ${p.rotulo || p.titulo || ''}`)], ...base };
63
+ } else if (tipo === 'passos' || tipo === 'lista') {
64
+ /* Na escada, o item é o NOME ("Sonnet 4"); a data e o detalhe depois do
65
+ "·" ou do "—" estão na fala. Cinco itens de cinco palavras eram 7 s
66
+ de escada pela regra de 0,3 s por palavra. */
67
+ const itens = (p.itens || []).map((x) => limpo(typeof x === 'string' ? x : (x.titulo || x.texto || '')).split(/\s+[·—–-]\s+/)[0]).filter(Boolean);
68
+ q = { tipo: 'escada', itens: itens.slice(0, 5), ...(p.titulo ? { final: limpo(p.titulo) } : {}), ...base };
69
+ } else if (tipo === 'citacao') q = { tipo: 'frase', linhas: [`“${limpo(p.texto)}”`], ...base };
70
+ else if (tipo === 'contraste') q = { tipo: 'montagem', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
71
+ else if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') q = { tipo: 'final', linhas: [p.titulo || p.texto, p.link].filter(Boolean).map(limpo), ...base };
72
+ else q = { tipo: 'montagem', linhas: [p.titulo || p.texto || p.fala].filter(Boolean).map(limpo), ...base };
73
+ saida.push(q);
74
+ extras.push({ tipoOriginal: tipo, fonte: tipo === 'abertura' && p.sub ? p.sub : (p.fonte || (tipo === 'citacao' ? p.autor : '')), autor: p.autor || '', busca: p.busca || '' });
75
+ });
76
+ return { roteiro: { ...roteiro, partes: saida, estilo: 'hibrido' }, extras };
77
+ }
78
+
79
+ /** O que procurar de b-roll para cada parte: a busca dela, a do roteiro, ou o padrão. */
80
+ function termosDeBusca(roteiro, extras = []) {
81
+ const global = [].concat(roteiro.busca || []).filter(Boolean);
82
+ // Escopo fechado: só os termos da pessoa, nunca o padrão genérico.
83
+ const fechado = require('./referencias.js').atual().modo === 'fechado';
84
+ const lista = global.length ? global : fechado ? [] : BUSCA_PADRAO;
85
+ const termos = new Set();
86
+ (roteiro.partes || []).forEach((p, i) => {
87
+ const b = (extras[i] && extras[i].busca) || p.busca;
88
+ if (b) termos.add(String(b));
89
+ });
90
+ for (const t of lista) termos.add(t);
91
+ return [...termos].slice(0, 8);
92
+ }
93
+
94
+ /* A câmera de cada cena do cinema: empurra e desliza, alternando o lado. A
95
+ foto já se move por dentro (os movimentos do cinema); a câmera move o
96
+ CONJUNTO — é a soma dos dois que dá a profundidade. */
97
+ function camera(d, i, deitado, forte) {
98
+ const lado = i % 2 ? 1 : -1;
99
+ const z = forte ? 1.14 : 1.07;
100
+ const desliza = (deitado ? 26 : 18);
101
+ return {
102
+ zoom: [k(0, 1.0), k(Math.max(0.3, d - 0.25), z, 'camera'), k(d, z + 0.04, 'acelera')],
103
+ x: [k(0, -lado * desliza), k(d, lado * desliza, 'camera')],
104
+ rotacao: [k(0, -lado * 0.35), k(d, lado * 0.35, 'camera')],
105
+ };
106
+ }
107
+
108
+ /** O kit por cima do plano do cinema. Mexe no plano recebido e o devolve. */
109
+ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura } = {}) {
110
+ if (!plano || !Array.isArray(plano.cenas)) return plano;
111
+ const W = largura || (plano.formato && plano.formato.largura) || 1920;
112
+ const H = altura || (plano.formato && plano.formato.altura) || 1080;
113
+ const deitado = W >= H;
114
+ const base = Math.min(W, H);
115
+ const M = Math.round(base * 0.08);
116
+ const grad = marca.gradiente && marca.gradiente.length >= 2 ? marca.gradiente : ['#2E81ED', '#9567DB'];
117
+ const ACENTO = (marca.cores && marca.cores.destaque) || grad[1];
118
+
119
+ /* As cenas do cinema não são uma por parte: o depoimento com três clipes
120
+ vira três cenas. O mapa cena → parte sai das partes já traduzidas. */
121
+ const parteDaCena = [];
122
+ partes.forEach((p, i) => {
123
+ const n = p.tipo === 'depoimento' ? Math.max(1, (p.clipes || []).length) : 1;
124
+ for (let q = 0; q < n; q++) parteDaCena.push(i);
125
+ });
126
+ plano.cenas.forEach((cena, c) => {
127
+ const iParte = parteDaCena[c] != null ? parteDaCena[c] : c;
128
+ const ex = extras[iParte] || {};
129
+ const d = cena.duracao || 2;
130
+ const tipo = ex.tipoOriginal || '';
131
+ const els = cena.elementos || (cena.elementos = []);
132
+ const forte = tipo === 'numero' || tipo === 'contraste' || tipo === 'abertura';
133
+ if (!cena.camera) cena.camera = camera(d, c, deitado, forte);
134
+
135
+ // A FONTE que se digita, com a barra do acento: vira prova, não enfeite.
136
+ if (ex.fonte && d > 1.4) {
137
+ els.push({ tipo: 'kit', peca: 'fonte', texto: String(ex.fonte).slice(0, 48), t: Math.min(0.9, d * 0.35),
138
+ x: M, y: Math.round(H - M * (deitado ? 1.1 : 2.6)), tamanho: Math.round(base * 0.016), cor: 'rgba(245,245,247,0.72)', corAcento: ACENTO });
139
+ }
140
+ // O NÚMERO estoura em partículas quando assenta.
141
+ if (tipo === 'numero' || tipo === 'grafico') {
142
+ els.push({ tipo: 'kit', peca: 'particulas', t: 0.45, x: Math.round(W / 2), y: Math.round(H * 0.42), raio: Math.round(base * 0.34), n: 46,
143
+ tamanho: Math.max(5, Math.round(base * 0.007)), cores: ['#ffffff', ACENTO, grad[0]] });
144
+ }
145
+ // O FIM: a marca entra com um brilho de partículas.
146
+ if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') {
147
+ els.push({ tipo: 'kit', peca: 'particulas', t: 0.2, x: Math.round(W / 2), y: Math.round(H * 0.36), raio: Math.round(base * 0.45), n: 60,
148
+ tamanho: Math.max(4, Math.round(base * 0.006)), cores: ['#ffffff', ACENTO], forma: 'circulo' });
149
+ }
150
+ /* O LÍQUIDO entre blocos: a cada três partes a passagem é a mancha que
151
+ cobre a tela, não o esmaecer. Mais que isso vira truque. */
152
+ if (c > 0 && iParte % 3 === 0 && cena.transicao && cena.transicao.tipo !== 'corte' && tipo !== 'cta') {
153
+ cena.transicao = { tipo: 'liquido', duracao: 0.5, cor: ACENTO };
154
+ }
155
+ });
156
+
157
+ // O HUD de motion designer: cantos, timecode e a marca.
158
+ plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
159
+ // O acabamento de filme um pouco mais presente que o do cinema.
160
+ plano.acabamento = { ...(plano.acabamento || {}), grao: Math.max(0.06, (plano.acabamento || {}).grao || 0), vinheta: Math.max(0.55, (plano.acabamento || {}).vinheta || 0) };
161
+ plano.estilo = 'hibrido';
162
+ return plano;
163
+ }
164
+
165
+ module.exports = { traduzir, termosDeBusca, enfeitar, BUSCA_PADRAO };