primocode 9.10.0 → 10.0.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 (60) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/estilo.js +51 -0
  12. package/lib/estudio/glifos.js +215 -0
  13. package/lib/estudio/index.js +41 -18
  14. package/lib/estudio/kit/base.jsx +285 -0
  15. package/lib/estudio/kit/index.jsx +11 -0
  16. package/lib/estudio/kit/marca.jsx +741 -0
  17. package/lib/estudio/kit/vox.jsx +357 -0
  18. package/lib/estudio/kit.js +72 -0
  19. package/lib/estudio/lancamento.js +413 -0
  20. package/lib/estudio/motion.js +2 -5
  21. package/lib/estudio/plano.js +28 -3
  22. package/lib/estudio/remotion.js +87 -43
  23. package/lib/estudio/ritmo.js +77 -0
  24. package/lib/estudio/vox.js +385 -0
  25. package/lib/fala.js +1 -94
  26. package/lib/pipeline.js +55 -56
  27. package/lib/repeticao.js +3 -1
  28. package/lib/skills.js +8 -2
  29. package/lib/snapshot.js +111 -0
  30. package/lib/tools.js +79 -20
  31. package/lib/ui.js +13 -6
  32. package/lib/video/analise.js +89 -0
  33. package/lib/video/cli.js +74 -0
  34. package/lib/video/delta.js +90 -0
  35. package/lib/video/edits.js +379 -0
  36. package/lib/video/filters.js +247 -0
  37. package/lib/video/ops.js +322 -0
  38. package/lib/video/projeto.js +133 -0
  39. package/lib/video/render.js +221 -0
  40. package/lib/video/texto.js +142 -0
  41. package/lib/video/timeline.js +310 -0
  42. package/lib/video/transcript.js +124 -0
  43. package/lib/voz.js +75 -223
  44. package/package.json +2 -3
  45. package/skills/motion-de-primeira/SKILL.md +3 -4
  46. package/studio/texto.py +208 -0
  47. package/voz/primo_voz/__main__.py +96 -236
  48. package/voz/primo_voz/transcrever.py +31 -1
  49. package/app/ansi.js +0 -55
  50. package/app/app.css +0 -500
  51. package/app/app.js +0 -1017
  52. package/app/icone.svg +0 -6
  53. package/app/index.html +0 -220
  54. package/app/primo-marca.js +0 -192
  55. package/lib/app.js +0 -615
  56. package/lib/icone.js +0 -263
  57. package/lib/instalar-app.js +0 -231
  58. package/lib/painel.js +0 -291
  59. package/voz/primo_voz/desperta.py +0 -195
  60. package/voz/primo_voz/orbe.py +0 -406
@@ -0,0 +1,413 @@
1
+ /**
2
+ * lancamento.js — os estilos "marca" (brand reveal) e "lancamento" (filme de
3
+ * lançamento de produto).
4
+ *
5
+ * O repertório dos vídeos de referência que o Joel mandou (29/09/2026): o
6
+ * brilho no escuro que vira a marca, as letras desenhadas em vetor com as
7
+ * âncoras e o cursor, a marca chapada na cor, o padrão ladrilhado, a
8
+ * mancha líquida, a marca cromada, os cartões de cor em 3D; o HUD de motion
9
+ * designer; os cartões flutuando, o número gigante, o cartão do produto com
10
+ * o marca-texto e as barras, o calendário e o mural que se afasta.
11
+ *
12
+ * Uma parte pode virar DUAS cenas (a logo: o vetor e depois a marca chapada;
13
+ * a chamada: o padrão e depois o fecho). O corte cai no beat; as cenas que
14
+ * pedem ("cta") entram pela mancha líquida. As peças são do kit
15
+ * (kit/marca.jsx); aqui só se decide o quê, onde e quando.
16
+ *
17
+ * Tipos de parte: abertura, logo, titulo, numero, lista, passos, contraste,
18
+ * citacao, imagem, grafico, cores, produto, cta. Qualquer parte pode pedir
19
+ * a peça que a conduz com "visual": orbe, vetor, estrela, esfera, grade,
20
+ * site, padrao, cromo, cartoes, painel, cores.
21
+ *
22
+ * Puro: roteiro, tempos da voz, glifos da fonte (glifos.js) e arquivos
23
+ * entram; o plano sai.
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const { caber } = require('./composicao.js');
29
+ const { misturarCor, ehCor } = require('./morfar.js');
30
+ const { tempoDasPartes, gatilhos, volumesDaTrilha, r2 } = require('./ritmo.js');
31
+ const { tese, termoDef } = require('./vox.js');
32
+
33
+ const FPS = 30;
34
+ const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
35
+ const luz = (hex) => {
36
+ const h = String(hex || '').replace('#', '');
37
+ const n = h.length === 3 ? h.split('').map((c) => c + c).join('') : h.slice(0, 6);
38
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) || 0);
39
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
40
+ };
41
+ const sobre = (c) => (luz(c) > 0.6 ? '#111114' : '#ffffff');
42
+ const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
43
+ const VISUAIS = ['orbe', 'vetor', 'estrela', 'esfera', 'grade', 'site', 'padrao', 'cromo', 'cartoes', 'painel', 'cores'];
44
+
45
+ /**
46
+ * @param {object} op
47
+ * @param {object} op.roteiro { marca: {nome, cores, fontes, logo}, partes: [...] }
48
+ * @param {string} op.variante 'marca' | 'lancamento'
49
+ * @param {object} op.glifos { d, pontos, caixa } do nome da marca (glifos.js), para o vetor
50
+ * @param {object} op.imagens { indiceDaParte: {src, tipo} }
51
+ */
52
+ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, imagens = {}, glifos = null }) {
53
+ const partes = (roteiro && roteiro.partes) || [];
54
+ if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
55
+ const ehMarca = variante === 'marca';
56
+ const W = largura, H = altura, deitado = W >= H;
57
+ const b = Math.min(W, H) / 1080;
58
+ const M = Math.round(Math.min(W, H) * 0.08);
59
+ const TOPO = Math.round(M * 1.45); // abaixo do HUD: o rótulo dele mora na banda de cima
60
+
61
+ // ── a marca ────────────────────────────────────────────────────────
62
+ const marca = roteiro.marca || {};
63
+ const cores = marca.cores || {};
64
+ const ACENTO = ehCor(cores.destaque) ? cores.destaque : (ehMarca ? '#1f3cff' : '#ff5a1f');
65
+ const ESCURO = ehCor(cores.fundo) && luz(cores.fundo) < 0.2 ? cores.fundo : '#09090c';
66
+ const CLARO = '#f1f1ef';
67
+ const nome = marca.nome || (partes.find((p) => p.tipo === 'abertura' || p.tipo === 'logo') || {}).titulo || 'marca';
68
+ const F = {
69
+ display: (marca.fontes && marca.fontes.titulo) || (ehMarca ? 'Plus Jakarta Sans' : 'Inter Tight'),
70
+ texto: (marca.fontes && marca.fontes.texto) || 'Inter',
71
+ mono: 'JetBrains Mono',
72
+ };
73
+ const logo = marca.logo || null;
74
+ const base = { fonte: F.display, fonteMono: F.mono, corAcento: ACENTO };
75
+
76
+ // ── o tempo ────────────────────────────────────────────────────────
77
+ const leitura = (p) => 1.8 + 0.28 * String([p.titulo, p.sub, p.valor, p.rotulo, p.nao, p.sim, p.texto, ...itensDe(p)].filter(Boolean).join(' ')).split(/\s+/).length;
78
+ const { ini, dur, total, temVoz } = tempoDasPartes(partes, tempos, { semVoz: leitura, minimo: 1.4 });
79
+ const efeitos = [];
80
+ const som = (t, nome_, volume) => { if (sons[nome_]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome_], volume }); };
81
+
82
+ // ── ferramentas de cena ────────────────────────────────────────────
83
+ const fundoClaro = { tipo: 'gradiente', radial: true, centro: '50% 40%', cores: ['#fbfbfa', '#e6e6e3'] };
84
+ const fundoEscuro = { tipo: 'gradiente', radial: true, centro: '50% 30%', cores: [misturarCor(ESCURO, '#ffffff', 0.08), ESCURO] };
85
+ const fundoEstudio = { tipo: 'gradiente', angulo: 180, cores: [misturarCor(ESCURO, '#ffffff', 0.22), misturarCor(ESCURO, '#ffffff', 0.05), ESCURO] };
86
+ const fundoAcento = { tipo: 'gradiente', radial: true, centro: '50% 45%', cores: [misturarCor(ACENTO, '#ffffff', 0.12), ACENTO, misturarCor(ACENTO, '#000000', 0.12)] };
87
+ const push = (d, z = 1.06) => ({ zoom: [k(0, 1), k(Math.max(0.3, d - 0.25), z, 'camera'), k(d, z + 0.025, 'acelera')] });
88
+ const cena = (d, fundo, escuro, elementos, { zoom = 1.05, ...extra } = {}) => ({ duracao: r2(d), fundo, escuro, camera: push(d, zoom), elementos, ...extra });
89
+ const fit = (t, larg, linhas, max, min = 36) => caber(t, { largura: larg, linhas, max: max * b, min: min * b, fonte: F.display });
90
+ const alturaDe = (f) => f.linhas * f.tamanho * 1.0;
91
+ const palavrasT = (texto, tp, t0, d, op) => gatilhos(String(texto).split(/\s+/).filter(Boolean), tp, t0, d, op);
92
+ const partir = (d, k1) => [r2(d * k1), r2(d - d * k1)];
93
+ const brilhoSom = (t) => som(t, 'brilho', 0.28);
94
+
95
+ /* O cartão de enfeite: interface genérica, sem texto inventado. É a
96
+ "multidão" de posts do filme de lançamento, e não diz nada falso. */
97
+ const enfeites = (n, semente = 0) => Array.from({ length: n }).map((_, j) => ({ titulo: nome, rotulo: ['agora', '2 min', 'ontem', '1 h'][(j + semente) % 4] }));
98
+
99
+ // ── cada tipo de parte vira uma (ou duas) cenas ────────────────────
100
+ const CENAS = {
101
+ abertura(p, d, tp, t0) {
102
+ const titulo = p.titulo || nome;
103
+ const els = [
104
+ { tipo: 'kit', peca: 'orbe', x: W / 2, y: r2(H * 0.46), tamanho: r2(240 * b), cor: ehMarca ? '#5fd6c8' : ACENTO, corMarca: ehMarca ? '#dffcff' : ACENTO,
105
+ t: 0.05, tMarca: r2(Math.min(1.1, d * 0.4)), logo, particulas: ehMarca ? 30 : 56 },
106
+ ];
107
+ const tt = Math.min(d - 0.9, Math.max(1.4, gatilhos([titulo], tp, t0, d)[0]));
108
+ els.push({ tipo: 'kit', peca: 'manchete', modo: 'digitar', texto: titulo, x: W / 2, y: r2(H * 0.46 + 190 * b), largura: W - 2 * M, alinhamento: 'center',
109
+ tamanho: r2(fit(titulo, W * 0.6, 1, 64).tamanho), peso: 700, cor: '#f4f4f6', t: tt, ...base, fonte: F.display });
110
+ if (p.sub) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.sub, x: W / 2, y: r2(H * 0.46 + 280 * b), largura: W - 2 * M, alinhamento: 'center',
111
+ tamanho: r2(30 * b), peso: 500, cor: '#9a9ca6', t: Math.min(d - 0.6, tt + 0.6), passo: 2, fonte: F.texto });
112
+ brilhoSom(t0 + Math.min(1.1, d * 0.4));
113
+ return [cena(d, ehMarca ? { tipo: 'gradiente', angulo: 100, cores: ['#1c5f64', '#06080c', '#06080c', '#233a8a'] } : fundoEscuro, true, els, { zoom: 1.08 })];
114
+ },
115
+
116
+ logo(p, d, tp, t0) {
117
+ const [d1, d2] = partir(d, 0.58);
118
+ const palavra = p.titulo || nome;
119
+ const altura = r2(Math.min(H * 0.6, (W * 1.5) / Math.max(3, palavra.length * 0.6)));
120
+ const a = cena(d1, '#050506', true, [
121
+ { tipo: 'kit', peca: 'vetor', glifos: glifos || undefined, texto: palavra, fonte: F.display, x: W / 2, y: r2(H * 0.52), altura, t: 0.05, durTraco: Math.min(1.3, d1 * 0.55),
122
+ pan: { de: r2(W * 0.22), ate: r2(-W * 0.22), dur: d1 } },
123
+ ], { camera: { zoom: [k(0, 1.18), k(d1, 1.0, 'camera')] } });
124
+ const tam = fit(palavra, W * 0.4, 1, 120).tamanho;
125
+ const icone = r2(tam * 1.05);
126
+ const larg = palavra.length * tam * 0.56 + icone + 30 * b;
127
+ const b2 = cena(d2, ACENTO, luz(ACENTO) < 0.6, [
128
+ { tipo: 'kit', peca: 'cromo', modo: 'solido', tom: sobre(ACENTO), logo, reflexo: false, giro: 0, x: r2(W / 2 - larg / 2 + icone / 2), y: r2(H / 2), tamanho: icone, t: 0 },
129
+ { tipo: 'kit', peca: 'manchete', modo: 'digitar', texto: palavra.toLowerCase(), x: r2(W / 2 - larg / 2 + icone + 30 * b), y: r2(H / 2), ancora: 'meio', tamanho: tam, peso: 700,
130
+ cor: sobre(ACENTO), t: 0.25, ...base, espacamento: '-0.03em' },
131
+ ], { zoom: 1.04 });
132
+ som(t0 + d1, 'toque', 0.35);
133
+ return [a, b2];
134
+ },
135
+
136
+ titulo(p, d, tp, t0) {
137
+ const titulo = p.titulo || '';
138
+ const curto = titulo.length <= 12;
139
+ const f = fit(curto ? titulo.toUpperCase() : titulo, W - 2 * M, curto ? 1 : 3, curto ? 300 : 150, 44);
140
+ const ts = palavrasT(titulo, tp, t0, d);
141
+ if (!ehMarca && !curto) {
142
+ // O campo de cartões flutuando, e a pergunta entrando palavra a palavra por cima.
143
+ return [cena(d, fundoAcento, true, [
144
+ { tipo: 'kit', peca: 'cartoes', modo: 'voo', cartoes: enfeites(12, 1), x: 0, y: 0, largura: W, altura: H, larguraCartao: r2((deitado ? 330 : 420) * b), t: 0, ...base, corAcento: ACENTO },
145
+ { tipo: 'forma', forma: 'circulo', x: r2(W / 2), y: r2(H / 2), largura: r2(W * 0.9), altura: r2(W * 0.5), ancora: 'centro', cor: ACENTO, opacidade: 0.55, anima: { desfoque: [k(0, 90)] } },
146
+ { tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: titulo, destaque: '', x: W / 2, y: r2(H / 2 - alturaDe(f) / 2), largura: W - 2 * M, alinhamento: 'center',
147
+ tamanho: f.tamanho, peso: 800, cor: '#ffffff', tempos: ts, ...base },
148
+ ], { perspectiva: 1400, zoom: 1.07 })];
149
+ }
150
+ const els = [{ tipo: 'kit', peca: 'manchete', modo: curto ? 'carimbo' : 'subir', texto: curto ? titulo.toUpperCase() : titulo, destaque: curto ? '' : tese(titulo, p.destaque),
151
+ x: W / 2, y: r2(H / 2 - alturaDe(f) / 2), largura: W - 2 * M, alinhamento: 'center', tamanho: f.tamanho, peso: 900, cor: '#111114', tempos: ts,
152
+ corAcento: '#ffffff', fonte: F.display, espacamento: curto ? '-0.02em' : '-0.035em',
153
+ saida: curto && d > 1.6 ? { t: r2(d - 0.55), modo: 'pontos' } : undefined }];
154
+ if (p.sub) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.sub, x: W / 2, y: r2(H / 2 + alturaDe(f) / 2 + 30 * b), largura: W - 2 * M, alinhamento: 'center',
155
+ tamanho: r2(34 * b), peso: 500, cor: '#111114', t: Math.min(d - 0.7, ts[ts.length - 1] + 0.3), passo: 2, fonte: F.texto });
156
+ som(t0 + ts[0], 'toque', 0.35);
157
+ return [cena(d, ehMarca ? ACENTO : fundoAcento, true, els, { zoom: 1.08 })];
158
+ },
159
+
160
+ numero(p, d, tp, t0) {
161
+ const v = String(p.valor || p.titulo || '');
162
+ const m = v.match(/^(\D*)([\d.,]+)(.*)$/);
163
+ const n = m ? Number(m[2].replace(/\./g, '').replace(',', '.')) : null;
164
+ const rot = p.rotulo || (p.valor ? p.titulo : '') || '';
165
+ const xL = deitado ? M * 1.4 : M;
166
+ const tamN = fit(v, deitado ? W * 0.36 : W - 2 * M, 1, 300).tamanho;
167
+ const tamR = fit(rot, deitado ? W * 0.42 : W - 2 * M, 2, 76).tamanho;
168
+ const yN = deitado ? H * 0.28 : H * 0.2;
169
+ const [tv, tr] = gatilhos([v, rot || v], tp, t0, d);
170
+ const els = [
171
+ { tipo: 'kit', peca: 'fonte', texto: (p.secao || p.tipo).toUpperCase(), x: r2(xL), y: r2(yN - 60 * b), t: 0.05, tamanho: r2(18 * b), fonteMono: F.mono, corAcento: ACENTO, cor: ACENTO },
172
+ { tipo: 'kit', peca: 'manchete', texto: v, ...(n != null ? { contar: { ate: n, antes: m[1], depois: m[3], casas: /,\d/.test(m[2]) ? 1 : 0, dur: 0.9 } } : { modo: 'carimbo' }),
173
+ x: r2(xL), y: r2(yN), tamanho: tamN, peso: 900, cor: '#111114', t: tv, ...base },
174
+ ];
175
+ if (rot) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: rot, x: r2(xL), y: r2(yN + tamN * 0.98), largura: r2(deitado ? W * 0.44 : W - 2 * M), tamanho: tamR, peso: 800,
176
+ cor: '#111114', t: tr, passo: 4, ...base });
177
+ if (p.sub) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.sub, destaque: tese(p.sub), x: r2(xL), y: r2(yN + tamN * 0.98 + alturaDe({ linhas: Math.min(2, Math.ceil(rot.length * tamR * 0.56 / (W * 0.44))), tamanho: tamR }) + 26 * b),
178
+ largura: r2(W * 0.5), tamanho: r2(34 * b), peso: 600, cor: '#8a8c94', corAcento: ACENTO, t: Math.min(d - 0.6, tr + 0.5), passo: 3, fonte: F.texto });
179
+ // A pilha de cartões: ao lado do número no deitado, embaixo dele em pé.
180
+ const area = deitado ? { x: W * 0.52, y: H * 0.12, w: W * 0.44, h: H * 0.6 } : { x: M, y: H * 0.52, w: W - 2 * M, h: H * 0.36 };
181
+ els.push({ tipo: 'kit', peca: 'cartoes', modo: 'voo', x: r2(area.x), y: r2(area.y), largura: r2(area.w), altura: r2(area.h), larguraCartao: r2((deitado ? 380 : 520) * b), t: tv + 0.1,
182
+ cartoes: enfeites(Math.min(6, Math.max(3, Math.round(n || 4))), 2).map((c, j) => ({ ...c, x: r2(area.w / 2 + (j % 2 ? 60 : -40) * b), y: r2(j * 70 * b) })), ...base, fonte: F.texto });
183
+ som(t0 + tv, 'brilho', 0.25);
184
+ return [cena(d, fundoClaro, false, els)];
185
+ },
186
+
187
+ lista(p, d, tp, t0) {
188
+ if (!ehMarca) return CENAS.passos(p, d, tp, t0);
189
+ const itens = itensDe(p).slice(0, 6);
190
+ const titulo = p.titulo || '';
191
+ const ts = gatilhos(itens, tp, t0, d, { t0: 0.5 });
192
+ const w = r2((deitado ? 440 : 520) * b);
193
+ const lugares = deitado
194
+ ? [[0.2, 0.2], [0.8, 0.18], [0.16, 0.66], [0.84, 0.62], [0.5, 0.78], [0.62, 0.08]]
195
+ : [[0.5, 0.12], [0.3, 0.3], [0.7, 0.62], [0.32, 0.72], [0.66, 0.86], [0.5, 0.95]];
196
+ const cartoes = itens.map((it, j) => ({ titulo: nome, texto: it, rotulo: String(j + 1).padStart(2, '0'), x: r2(W * lugares[j][0]), y: r2(H * lugares[j][1] - 60 * b), t: ts[j], destaque: j === itens.length - 1 }));
197
+ const f = fit(titulo, W * 0.5, 2, 110);
198
+ return [cena(d, fundoEscuro, true, [
199
+ { tipo: 'kit', peca: 'cartoes', modo: 'voo', cartoes, x: 0, y: 0, largura: W, altura: H, larguraCartao: w, t: 0, ...base, fonte: F.texto },
200
+ { tipo: 'kit', peca: 'manchete', modo: 'subir', texto: titulo, destaque: tese(titulo, p.destaque), x: W / 2, y: r2(H / 2 - alturaDe(f) / 2), largura: W * 0.52, alinhamento: 'center',
201
+ tamanho: f.tamanho, peso: 800, cor: '#ffffff', t: 0.1, passo: 3, ...base },
202
+ ], { perspectiva: 1400 })];
203
+ },
204
+
205
+ passos(p, d, tp, t0) {
206
+ const itens = itensDe(p).slice(0, deitado ? 5 : 3);
207
+ const titulo = p.titulo || '';
208
+ const ts = gatilhos(itens.map((x) => termoDef(x).def || x), tp, t0, d, { t0: 0.6 });
209
+ const colunas = itens.map((x, j) => {
210
+ const td = termoDef(x);
211
+ return { rotulo: td.def ? td.termo : 'passo', titulo: td.def || td.termo, numero: j + 1, itens: [], t: ts[j] };
212
+ });
213
+ const f = fit(titulo, W - 2 * M, 1, 96);
214
+ const yC = r2(TOPO + f.tamanho * 1.4 + 30 * b);
215
+ return [cena(d, fundoClaro, false, [
216
+ { tipo: 'kit', peca: 'fonte', texto: (p.secao || 'semana').toUpperCase(), x: M, y: r2(TOPO - 38 * b), t: 0.05, tamanho: r2(18 * b), fonteMono: F.mono, corAcento: ACENTO, cor: ACENTO },
217
+ { tipo: 'kit', peca: 'manchete', modo: 'subir', texto: titulo, destaque: tese(titulo, p.destaque), x: M, y: r2(TOPO), largura: W - 2 * M, tamanho: f.tamanho, peso: 800,
218
+ cor: '#111114', t: 0.1, passo: 3, ...base },
219
+ { tipo: 'kit', peca: 'cartoes', modo: 'colunas', cartoes: colunas, x: M, y: yC, largura: W - 2 * M, altura: r2(Math.min(H - yC - M, (deitado ? 0.56 : 0.5) * H)), t: 0.35, ...base, fonte: F.texto },
220
+ ])];
221
+ },
222
+
223
+ contraste(p, d, tp, t0) {
224
+ const [d1, d2] = partir(d, 0.5);
225
+ const nao = p.nao || '—', sim = p.sim || p.titulo || '';
226
+ const campo = ehMarca ? '#d8f53a' : ACENTO;
227
+ const tinta = sobre(campo);
228
+ const a = cena(d1, campo, luz(campo) < 0.6, [
229
+ { tipo: 'kit', peca: 'textura', modo: 'contorno', texto: nao, largura: W, altura: H, tamanho: r2(fit(nao.toUpperCase(), W * 0.5, 1, 170).tamanho), cor: tinta, corDestaque: tinta,
230
+ fonte: F.display, t: 0.02, velocidade: 90, separador: ' ' },
231
+ ], { zoom: 1.1 });
232
+ const f = fit(sim, W - 2 * M, 2, 190);
233
+ const b2 = cena(d2, ESCURO, true, [
234
+ { tipo: 'kit', peca: 'particulas', x: W / 2, y: H / 2, t: 0.05, n: 46, raio: r2(W * 0.4), cores: [ACENTO, '#ffffff', misturarCor(ACENTO, '#ffffff', 0.5)] },
235
+ { tipo: 'kit', peca: 'manchete', modo: 'carimbo', texto: sim, x: W / 2, y: r2(H / 2 - alturaDe(f) / 2), largura: W - 2 * M, alinhamento: 'center', tamanho: f.tamanho,
236
+ peso: 900, cor: '#ffffff', t: 0.02, passo: 3, destaque: tese(sim, p.destaque), ...base },
237
+ ], { zoom: 1.1 });
238
+ som(t0 + d1, 'toque', 0.4);
239
+ return [a, b2];
240
+ },
241
+
242
+ citacao(p, d, tp, t0) {
243
+ const pw = r2(deitado ? W * 0.7 : W - 2 * M);
244
+ const texto = p.titulo || p.texto || '';
245
+ return [cena(d, fundoClaro, false, [
246
+ { tipo: 'kit', peca: 'painel', titulo: p.autor || nome, texto: '“' + texto + '”', destaque: p.destaque || tese(texto), abas: [], botao: 'Responder',
247
+ x: r2((W - pw) / 2), y: r2(H * 0.5 - pw * (deitado ? 0.2 : 0.3)), largura: pw, t: 0, escala: deitado ? 1 : 1.35, tMarca: Math.max(0.7, gatilhos([p.destaque || tese(texto)], tp, t0, d)[0]), ...base, fonte: F.texto },
248
+ ], { perspectiva: 1600 })];
249
+ },
250
+
251
+ imagem(p, d, tp, t0, i) {
252
+ const m = imagens[i] || (p.src ? { src: p.src, tipo: 'imagem' } : null);
253
+ if (!m) return CENAS.titulo(p, d, tp, t0);
254
+ const iw = r2(deitado ? W * 0.62 : W - 2 * M), ih = r2(iw * 0.6);
255
+ const f = fit(p.titulo || '', deitado ? W * 0.3 : W - 2 * M, 3, 84);
256
+ const x = deitado ? W - M - iw : M, y = deitado ? (H - ih) / 2 : H * 0.14;
257
+ const els = [
258
+ { tipo: 'grupo', x: r2(x), y: r2(y), largura: iw, altura: ih, recorte: true, raio: r2(22 * b), sombra: '0 40px 90px -30px rgba(0,0,0,0.55)',
259
+ anima: { rotacaoY: [k(0, -24), k(0.8, -6, 'editorial'), k(d, -3, 'camera')], x: [k(0, 220 * b), k(0.8, 0, 'editorial')], opacidade: [k(0, 0), k(0.15, 1)] },
260
+ elementos: [{ tipo: m.tipo === 'video' ? 'video' : 'imagem', src: m.src, ...(m.tipo === 'video' ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
261
+ anima: { escala: [k(0, 1.1), k(d, 1.0, 'camera')] } }] },
262
+ ];
263
+ if (p.titulo) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo, destaque: tese(p.titulo, p.destaque), x: M, y: r2(deitado ? H / 2 - alturaDe(f) / 2 : y + ih + 60 * b),
264
+ largura: r2(deitado ? W * 0.3 : W - 2 * M), tamanho: f.tamanho, peso: 800, cor: '#111114', t: 0.4, ...base });
265
+ return [cena(d, fundoClaro, false, els, { perspectiva: 1600 })];
266
+ },
267
+
268
+ grafico(p, d, tp, t0) {
269
+ const barras = (p.barras || itensDe(p).map((x, j, l) => ({ rotulo: x, valor: 100 - j * (60 / Math.max(1, l.length)) }))).slice(0, 5);
270
+ const max = Math.max(1, ...barras.map((q) => Number(q.valor) || 0));
271
+ const pw = r2(deitado ? W * 0.7 : W - 2 * M);
272
+ return [cena(d, fundoClaro, false, [
273
+ { tipo: 'kit', peca: 'painel', titulo: nome, texto: p.titulo || '', destaque: tese(p.titulo || '', p.destaque), abas: [], barras: barras.map((q) => ({ rotulo: q.rotulo, valor: Math.round(100 * (Number(q.valor) || 0) / max) })),
274
+ tituloBarras: p.sub || 'resultado', x: r2((W - pw) / 2), y: r2(deitado ? H * 0.5 - pw * 0.18 : H * 0.28), largura: pw, t: 0.15, tBarras: 0.6, empilhar: !deitado, escala: deitado ? 1 : 1.3, ...base, fonte: F.texto },
275
+ ], { perspectiva: 1600 })];
276
+ },
277
+
278
+ cores(p, d) {
279
+ const lista = (p.cores || [cores.fundo, cores.destaque, cores.tinta, cores.apoio].filter(ehCor).map((hex, j) => ({ nome: ['Fundo', 'Destaque', 'Tinta', 'Apoio'][j], hex })))
280
+ .slice(0, 5).map((c) => (typeof c === 'string' ? { nome: c, hex: c } : c));
281
+ const cs = lista.length >= 2 ? lista : [{ nome: 'Acento', hex: ACENTO }, { nome: 'Escuro', hex: ESCURO }, { nome: 'Claro', hex: '#f7f7f5' }];
282
+ return [cena(d, '#fafafa', false, [
283
+ ...(p.titulo ? [{ tipo: 'kit', peca: 'fonte', texto: p.titulo.toUpperCase(), x: M, y: TOPO, t: 0.1, tamanho: r2(20 * b), fonteMono: F.mono, corAcento: ACENTO, cor: '#555862' }] : []),
284
+ { tipo: 'kit', peca: 'cores', cores: cs, x: W / 2, y: r2(H / 2), largura: r2((deitado ? 290 : 300) * b), raioX: r2(deitado ? W * 0.3 : W * 0.3), raioY: r2(deitado ? H * 0.3 : H * 0.28),
285
+ t: 0.1, fonte: F.texto, fonteMono: F.mono },
286
+ { tipo: 'kit', peca: 'cromo', logo, x: W / 2, y: r2(H / 2), tamanho: r2(250 * b), t: 0, reflexo: false },
287
+ ], { zoom: 1.04 })];
288
+ },
289
+
290
+ produto(p, d, tp, t0) {
291
+ const titulo = p.titulo || '';
292
+ const pw = r2(deitado ? W * 0.72 : W - 2 * M);
293
+ const f = fit(titulo, W - 2 * M, 1, 96);
294
+ const texto = p.texto || p.sub || titulo;
295
+ return [cena(d, fundoClaro, false, [
296
+ { tipo: 'kit', peca: 'fonte', texto: (p.secao || 'produto').toUpperCase(), x: M, y: r2(TOPO - 38 * b), t: 0.05, tamanho: r2(18 * b), fonteMono: F.mono, corAcento: ACENTO, cor: ACENTO },
297
+ { tipo: 'kit', peca: 'manchete', modo: 'subir', texto: titulo, destaque: tese(titulo, p.destaque), x: M, y: r2(TOPO), largura: W - 2 * M, tamanho: f.tamanho, peso: 800, cor: '#111114', t: 0.05, passo: 3, ...base },
298
+ { tipo: 'kit', peca: 'painel', titulo: nome, texto, destaque: p.destaque || tese(texto), selo: p.selo, barras: p.barras, x: r2((W - pw) / 2), y: r2(deitado ? TOPO + f.tamanho * 1.5 + 40 * b : H * 0.24), largura: pw,
299
+ empilhar: !deitado, escala: deitado ? 1 : 1.3,
300
+ t: 0.3, tMarca: Math.max(0.8, gatilhos([p.destaque || tese(texto)], tp, t0, d)[0] - 0.3), ...base, fonte: F.texto },
301
+ ], { perspectiva: 1600 })];
302
+ },
303
+
304
+ cta(p, d, tp, t0) {
305
+ const [d1, d2] = partir(d, ehMarca ? 0.4 : 0.46);
306
+ const titulo = p.titulo || '';
307
+ const a = ehMarca
308
+ ? cena(d1, ACENTO, true, [{ tipo: 'kit', peca: 'padrao', logo, largura: W, altura: H, celula: r2(170 * b), cor: sobre(ACENTO), corOnda: misturarCor(sobre(ACENTO), ACENTO, 0.35), t: 0 }], { zoom: 1.1 })
309
+ : cena(d1, { tipo: 'gradiente', radial: true, centro: '50% 50%', cores: ['#d9d9d6', '#9c9c99'] }, false, [
310
+ { tipo: 'kit', peca: 'cartoes', modo: 'mural', cartoes: enfeites(10), x: 0, y: 0, largura: W, altura: H, colunas: deitado ? 7 : 4, linhas: deitado ? 5 : 8, dur: d1, ...base, fonte: F.texto },
311
+ ], { camera: { zoom: [k(0, 1)] } });
312
+ const f = fit(titulo, W - 2 * M, 2, 120);
313
+ const icone = r2(220 * b);
314
+ const els = [
315
+ { tipo: 'kit', peca: 'cromo', logo, x: W / 2, y: r2(H * 0.36), tamanho: icone, t: 0.05, ...(ehMarca ? {} : { modo: 'solido', tom: ACENTO, reflexo: false, giro: 6 }) },
316
+ { tipo: 'kit', peca: 'particulas', x: W / 2, y: r2(H * 0.36), t: 0.2, n: 40, raio: r2(420 * b), cores: ehMarca ? ['#ffffff', ACENTO, '#9fb0ff'] : [ACENTO, '#111114', misturarCor(ACENTO, '#ffffff', 0.5)] },
317
+ { tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: titulo, destaque: tese(titulo, p.destaque), x: W / 2, y: r2(H * 0.36 + icone * 0.75 + 30 * b), largura: W - 2 * M, alinhamento: 'center',
318
+ tamanho: f.tamanho, peso: 800, cor: ehMarca ? '#ffffff' : '#111114', t: 0.45, passo: 3, ...base },
319
+ ];
320
+ if (p.link) els.push({ tipo: 'kit', peca: 'manchete', modo: 'digitar', texto: p.link, x: W / 2, y: r2(H * 0.36 + icone * 0.75 + 60 * b + alturaDe(f)), largura: W - 2 * M, alinhamento: 'center',
321
+ tamanho: r2(38 * b), peso: 500, cor: ehMarca ? '#b8bcc8' : '#555862', t: 1.0, fonte: F.mono });
322
+ const b2 = cena(d2, ehMarca ? fundoEstudio : '#fbfbfa', ehMarca, els, { transicao: { tipo: 'liquido', duracao: 0.6 }, zoom: 1.04 });
323
+ brilhoSom(t0 + d1 + 0.2);
324
+ return [a, b2];
325
+ },
326
+ };
327
+
328
+ // ── as peças que qualquer parte pode pedir pelo nome ("visual") ────
329
+ const VISUAL = {
330
+ esfera(p, d, tp, t0) {
331
+ const f = fit(p.titulo || '', W - 2 * M, 1, 64);
332
+ return [cena(d, ESCURO, true, [
333
+ { tipo: 'kit', peca: 'esfera', x: W / 2, y: r2(H * 0.46), raio: r2(Math.min(W, H) * 0.3), cor: '#f4f4f6', corAcento: ACENTO, t: 0 },
334
+ { tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo || '', x: W / 2, y: r2(H * 0.84), largura: W - 2 * M, alinhamento: 'center', tamanho: f.tamanho, peso: 700, cor: '#f4f4f6',
335
+ t: gatilhos([p.titulo || ''], tp, t0, d)[0], ...base },
336
+ ])];
337
+ },
338
+ estrela(p, d, tp, t0) {
339
+ return [cena(d, ESCURO, true, [
340
+ { tipo: 'kit', peca: 'estrela', x: W / 2, y: H / 2, tamanho: r2(170 * b), cor: ACENTO, t: 0.05, texto: (p.titulo || nome) + ' · ' + nome, fonteMono: F.mono },
341
+ ], { zoom: 1.1 })];
342
+ },
343
+ grade(p, d) {
344
+ return [cena(d, '#ffffff', false, [
345
+ { tipo: 'kit', peca: 'grade', largura: W, altura: H, horizonte: r2(H * 0.46), cor: alfaHex(ACENTO, 0.28) },
346
+ { tipo: 'kit', peca: 'cromo', logo, x: W / 2, y: r2(H * 0.42), tamanho: r2(220 * b), t: 0.05, modo: 'solido', tom: ACENTO, reflexo: false },
347
+ ...(p.titulo ? [{ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo, x: W / 2, y: r2(H * 0.66), largura: W - 2 * M, alinhamento: 'center', tamanho: r2(fit(p.titulo, W * 0.7, 1, 72).tamanho),
348
+ peso: 800, cor: '#111114', t: 0.6, ...base }] : []),
349
+ ])];
350
+ },
351
+ site(p, d) {
352
+ const sw = r2(deitado ? W * 0.8 : W - 2 * M), sh = r2(deitado ? sw * 0.56 : sw * 1.45);
353
+ return [cena(d, fundoClaro, false, [
354
+ { tipo: 'kit', peca: 'site', marca: nome, logo, titulo: p.titulo || '', sub: p.sub, destaque: tese(p.titulo || '', p.destaque), menu: p.menu, x: r2((W - sw) / 2), y: r2((H - sh) / 2),
355
+ largura: sw, altura: sh, t: 0, ...base, fonte: F.display },
356
+ ])];
357
+ },
358
+ padrao(p, d) {
359
+ return [cena(d, ACENTO, true, [{ tipo: 'kit', peca: 'padrao', modo: p.modo === 'truchet' ? 'truchet' : 'icone', logo, largura: W, altura: H, celula: r2(170 * b),
360
+ cor: sobre(ACENTO), corOnda: misturarCor(sobre(ACENTO), ACENTO, 0.4), t: 0 }], { zoom: 1.08 })];
361
+ },
362
+ orbe: (p, d, tp, t0) => CENAS.abertura(p, d, tp, t0),
363
+ vetor: (p, d, tp, t0) => CENAS.logo(p, d, tp, t0),
364
+ cromo(p, d) {
365
+ const f = fit(p.titulo || '', W - 2 * M, 1, 72);
366
+ return [cena(d, fundoEstudio, true, [
367
+ { tipo: 'kit', peca: 'cromo', logo, x: W / 2, y: r2(H * 0.44), tamanho: r2(300 * b), t: 0.05 },
368
+ ...(p.titulo ? [{ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo, x: W / 2, y: r2(H * 0.74), largura: W - 2 * M, alinhamento: 'center', tamanho: f.tamanho, peso: 700, cor: '#f4f4f6', t: 0.7, ...base }] : []),
369
+ ])];
370
+ },
371
+ cartoes: (p, d, tp, t0) => CENAS.passos(p, d, tp, t0),
372
+ painel: (p, d, tp, t0) => CENAS.produto(p, d, tp, t0),
373
+ cores: (p, d, tp, t0) => CENAS.cores(p, d, tp, t0),
374
+ };
375
+
376
+ // ── montar ─────────────────────────────────────────────────────────
377
+ const cenas = [];
378
+ partes.forEach((p, i) => {
379
+ const fazer = (p.visual && VISUAL[p.visual]) || CENAS[p.tipo] || CENAS.titulo;
380
+ const feitas = fazer(p, dur[i], tempos[i], ini[i], i);
381
+ feitas.forEach((c, j) => { c.secao = String(i + 1).padStart(2, '0') + ' · ' + String(p.visual || p.tipo || 'parte').toUpperCase() + (feitas.length > 1 ? ' ' + (j + 1) + '/' + feitas.length : ''); });
382
+ cenas.push(...feitas);
383
+ });
384
+ /* A transição cruza as cenas: a que entra começa antes de a anterior
385
+ acabar. Para a voz não adiantar, a anterior ganha esse tempo. */
386
+ cenas.forEach((c, i) => {
387
+ const tr = c.transicao && typeof c.transicao === 'object' ? c.transicao.duracao || 0 : 0;
388
+ if (i > 0 && tr) cenas[i - 1].duracao = r2(cenas[i - 1].duracao + tr);
389
+ if (!c.transicao) c.transicao = 'corte';
390
+ });
391
+
392
+ const plano = {
393
+ formato: { largura: W, altura: H, fps: FPS },
394
+ fundoGeral: ESCURO,
395
+ estilo: ehMarca ? 'marca' : 'lancamento',
396
+ hud: { marca: String(nome).toUpperCase(), rotulo: ehMarca ? 'revelação de marca' : 'filme de lançamento', direita: W + '×' + H, fonte: F.mono },
397
+ acabamento: { grao: 0.05, vinheta: 0.18 },
398
+ cenas,
399
+ audio: {
400
+ ...(narracao ? { narracao } : {}),
401
+ ...(trilha ? { trilha: { src: trilha, volumes: volumesDaTrilha(temVoz ? tempos : [], total), loop: true } } : {}),
402
+ efeitos,
403
+ },
404
+ };
405
+ return { ok: true, plano, resumo: { cenas: cenas.length, segundos: total, partes: partes.length, fps: FPS, estilo: plano.estilo } };
406
+ }
407
+
408
+ function alfaHex(hex, a) {
409
+ const h = String(hex).replace('#', '');
410
+ return '#' + (h.length === 3 ? h.split('').map((c) => c + c).join('') : h.slice(0, 6)) + Math.round(a * 255).toString(16).padStart(2, '0');
411
+ }
412
+
413
+ module.exports = { montarLancamento, VISUAIS };
@@ -32,6 +32,7 @@
32
32
 
33
33
  const { caber, falaDe } = require('./composicao.js');
34
34
  const { misturarCor, ehCor } = require('./morfar.js');
35
+ const { volumesDaTrilha } = require('./ritmo.js');
35
36
 
36
37
  const k = (t, v, e) => (e ? { t, v, e } : { t, v });
37
38
  const r2 = (n) => Math.round(n * 100) / 100;
@@ -306,11 +307,7 @@ function montarMotion({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
306
307
  cam.rotacaoX.push(k(DUR, 0, 'suave'));
307
308
 
308
309
  // ── o som: a música abaixa quando a voz fala ─────────────────────────
309
- const volumes = [k(0, 0.05), k(0.4, 0.22)];
310
- if (temVoz) {
311
- for (const tp of tempos) volumes.push(k(r2(Math.max(0.41, tp.de - 0.15)), 0.22), k(r2(tp.de), 0.11), k(r2(tp.ate), 0.11), k(r2(tp.ate + 0.35), 0.22));
312
- }
313
- volumes.push(k(r2(DUR - 0.6), 0.2), k(DUR, 0));
310
+ const volumes = volumesDaTrilha(temVoz ? tempos : [], DUR);
314
311
  som(0.05, 'sopro', 0.3);
315
312
  som(DUR - 1.2, 'brilho', 0.35);
316
313
 
@@ -48,11 +48,13 @@
48
48
  const fs = require('fs');
49
49
  const path = require('path');
50
50
  const morfar = require('./morfar.js');
51
+ const kit = require('./kit.js');
51
52
 
52
- const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo']);
53
+ // "kit": uma peça pronta do kit de motion (kit.js), pelo nome — ver kit/*.jsx.
54
+ const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
53
55
  const FORMAS = new Set(['retangulo', 'circulo', 'linha', 'poligono', 'traco']);
54
56
  const AJUSTES = new Set(['cover', 'contain', 'preencher']);
55
- const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash']);
57
+ const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash', 'liquido']);
56
58
 
57
59
  /* As propriedades que a animação por quadro-chave alcança. Cada uma é um
58
60
  número no tempo; a curva entre dois quadros é a suavização declarada.
@@ -78,7 +80,9 @@ const TEXTUAIS = new Set(['cor', 'caminho']);
78
80
  // rotacaoX/rotacaoY/z: a câmera gira em volta do mundo e avança nele (precisa de `perspectiva` na cena).
79
81
  const ANIMAVEIS_CAMERA = new Set(['zoom', 'x', 'y', 'rotacao', 'rotacaoX', 'rotacaoY', 'z']);
80
82
 
81
- const SUAVIZACOES = new Set(['linear', 'suave', 'entrada', 'saida', 'elastico', 'salto']);
83
+ /* editorial (ataque rápido, freio forte), camera (bezier longa), arrasto (a
84
+ mão que desenha) e acelera (a câmera antes do corte) vêm do guia VOX. */
85
+ const SUAVIZACOES = new Set(['linear', 'suave', 'entrada', 'saida', 'elastico', 'salto', 'editorial', 'camera', 'arrasto', 'acelera']);
82
86
 
83
87
  const COR = /^(#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|transparent|currentColor)$/i;
84
88
 
@@ -240,6 +244,11 @@ function conferirElemento(caminho, el, pasta, problemas, duracao) {
240
244
  // A válvula de escape: um arquivo React escrito pela própria IA.
241
245
  conferirArquivo(`${caminho}.src`, el.src, pasta, problemas);
242
246
  break;
247
+ case 'kit':
248
+ for (const oque of kit.conferirPeca(el)) problemas.push(erro(`${caminho}.peca`, oque));
249
+ // A logo de uma peça é arquivo da pasta, como a imagem.
250
+ if (el.logo) conferirArquivo(`${caminho}.logo`, el.logo, pasta, problemas);
251
+ break;
243
252
  }
244
253
  }
245
254
 
@@ -298,6 +307,22 @@ function conferir(plano, { pasta = null } = {}) {
298
307
  conferirFundo(`${onde}.fundo`, cena.fundo, pasta, problemas);
299
308
  conferirAnima(`${onde}.camera`, cena.camera, problemas, ANIMAVEIS_CAMERA);
300
309
 
310
+ /* As CAMADAS de profundidade: [{p, desfoque, sujeito}]. "p" é a
311
+ profundidade (0,12 longe … 1,25 perto), e o elemento diz a dele por
312
+ índice. Índice fora da lista põe o elemento em lugar nenhum. */
313
+ const nCamadas = Array.isArray(cena.camadas) ? cena.camadas.length : 0;
314
+ if (cena.camadas != null) {
315
+ if (!nCamadas) problemas.push(erro(`${onde}.camadas`, 'camadas é uma lista de {p, desfoque, sujeito}'));
316
+ else cena.camadas.forEach((c, k) => {
317
+ if (!c || typeof c.p !== 'number' || !(c.p > 0)) problemas.push(erro(`${onde}.camadas[${k}].p`, 'p é a profundidade, um número maior que 0'));
318
+ });
319
+ }
320
+ for (const [k, el] of (Array.isArray(cena.elementos) ? cena.elementos : []).entries()) {
321
+ if (el && el.camada != null && !(Number.isInteger(el.camada) && el.camada >= 0 && el.camada < nCamadas)) {
322
+ problemas.push(erro(`${onde}.elementos[${k}].camada`, nCamadas ? `camada vai de 0 a ${nCamadas - 1}` : 'a cena não declara "camadas"'));
323
+ }
324
+ }
325
+
301
326
  if (cena.transicao) {
302
327
  const t = cena.transicao.tipo || cena.transicao;
303
328
  if (!TRANSICOES.has(t)) {