primocode 9.9.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.
- package/README.md +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/ende.js +2 -0
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +63 -11
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/morfar.js +191 -0
- package/lib/estudio/motion.js +333 -0
- package/lib/estudio/plano.js +54 -7
- package/lib/estudio/remotion.js +167 -52
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +62 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +21 -1
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- 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 };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* morfar.js — uma forma que vira outra, sem corte.
|
|
3
|
+
*
|
|
4
|
+
* "Uma bolinha viaja, aparece um fogo atrás, ela vai se deformando e se
|
|
5
|
+
* transforma no foguete." — Joel, sobre o que separa motion de slide
|
|
6
|
+
* (28/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* O "morph" antigo era uma transição entre cenas (esmaece e desfoca). Isto é
|
|
9
|
+
* a deformação DE VERDADE: a `forma: "traco"` tem um `caminho` (o `d` de um
|
|
10
|
+
* SVG), e o `caminho` anima por quadro-chave como qualquer número. Entre dois
|
|
11
|
+
* caminhos, cada contorno é reamostrado no mesmo número de pontos, alinhado
|
|
12
|
+
* (mesmo sentido, mesmo ponto de partida) e interpolado ponto a ponto.
|
|
13
|
+
*
|
|
14
|
+
* O alinhamento é o que faz a bolinha "esticar" em foguete em vez de girar e
|
|
15
|
+
* se embolar no meio: sem ele, o ponto 0 de uma forma vai para o outro lado da
|
|
16
|
+
* outra.
|
|
17
|
+
*
|
|
18
|
+
* Aceita M, L, H, V, C, S, Q, T e Z (maiúsculo e minúsculo). Arco (A) não:
|
|
19
|
+
* escreva a curva com C. Só o primeiro contorno do caminho conta.
|
|
20
|
+
*
|
|
21
|
+
* Puro e sem dependência. O MESMO código vai para o projeto Remotion
|
|
22
|
+
* (fonteEsm), então o teste do Node prova o que o render desenha.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
// ── embutido: daqui até o fim do bloco vai para src/morfar.js do Remotion ──
|
|
28
|
+
const PONTOS_PADRAO = 120;
|
|
29
|
+
const PASSOS_CURVA = 24;
|
|
30
|
+
const _memo = new Map();
|
|
31
|
+
|
|
32
|
+
function lerComandos(d) {
|
|
33
|
+
const partes = String(d || '').match(/[a-df-z]|-?(?:\d+\.?\d*|\.\d+)(?:e-?\d+)?/gi) || [];
|
|
34
|
+
const cmds = [];
|
|
35
|
+
let atual = null;
|
|
36
|
+
for (const p of partes) {
|
|
37
|
+
if (/[a-z]/i.test(p) && !/^e/i.test(p)) {
|
|
38
|
+
if (/a/i.test(p)) throw new Error('arco (A) não é aceito no caminho: desenhe a curva com C');
|
|
39
|
+
atual = { c: p, n: [] };
|
|
40
|
+
cmds.push(atual);
|
|
41
|
+
} else if (atual) atual.n.push(Number(p));
|
|
42
|
+
}
|
|
43
|
+
return cmds;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** O primeiro contorno do caminho, como uma linha de pontos. */
|
|
47
|
+
function contorno(d) {
|
|
48
|
+
const cmds = lerComandos(d);
|
|
49
|
+
const pts = [];
|
|
50
|
+
let x = 0, y = 0, x0 = 0, y0 = 0, cx = null, cy = null, qx = null, qy = null;
|
|
51
|
+
const curvaC = (x1, y1, x2, y2, x3, y3) => {
|
|
52
|
+
for (let i = 1; i <= PASSOS_CURVA; i++) {
|
|
53
|
+
const t = i / PASSOS_CURVA, u = 1 - t;
|
|
54
|
+
pts.push([u * u * u * x + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3,
|
|
55
|
+
u * u * u * y + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3]);
|
|
56
|
+
}
|
|
57
|
+
cx = x2; cy = y2; x = x3; y = y3;
|
|
58
|
+
};
|
|
59
|
+
const curvaQ = (x1, y1, x2, y2) => {
|
|
60
|
+
for (let i = 1; i <= PASSOS_CURVA; i++) {
|
|
61
|
+
const t = i / PASSOS_CURVA, u = 1 - t;
|
|
62
|
+
pts.push([u * u * x + 2 * u * t * x1 + t * t * x2, u * u * y + 2 * u * t * y1 + t * t * y2]);
|
|
63
|
+
}
|
|
64
|
+
qx = x1; qy = y1; x = x2; y = y2;
|
|
65
|
+
};
|
|
66
|
+
let fechou = false;
|
|
67
|
+
for (const { c, n } of cmds) {
|
|
68
|
+
if (fechou) break;
|
|
69
|
+
const rel = c === c.toLowerCase();
|
|
70
|
+
const C = c.toUpperCase();
|
|
71
|
+
const ox = () => (rel ? x : 0), oy = () => (rel ? y : 0);
|
|
72
|
+
if (C !== 'C' && C !== 'S') { cx = null; cy = null; }
|
|
73
|
+
if (C !== 'Q' && C !== 'T') { qx = null; qy = null; }
|
|
74
|
+
if (C === 'Z') { fechou = pts.length > 0; x = x0; y = y0; continue; }
|
|
75
|
+
const passo = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2 }[C];
|
|
76
|
+
if (!passo) continue;
|
|
77
|
+
for (let i = 0; i + passo <= n.length; i += passo) {
|
|
78
|
+
const a = n.slice(i, i + passo);
|
|
79
|
+
if (C === 'M') {
|
|
80
|
+
if (i === 0 && pts.length) { fechou = true; break; }
|
|
81
|
+
x = a[0] + ox(); y = a[1] + oy();
|
|
82
|
+
if (i === 0) { x0 = x; y0 = y; }
|
|
83
|
+
pts.push([x, y]);
|
|
84
|
+
} else if (C === 'L') { x = a[0] + ox(); y = a[1] + oy(); pts.push([x, y]); }
|
|
85
|
+
else if (C === 'H') { x = a[0] + (rel ? x : 0); pts.push([x, y]); }
|
|
86
|
+
else if (C === 'V') { y = a[0] + (rel ? y : 0); pts.push([x, y]); }
|
|
87
|
+
else if (C === 'C') { const [bx, by] = [ox(), oy()]; curvaC(a[0] + bx, a[1] + by, a[2] + bx, a[3] + by, a[4] + bx, a[5] + by); }
|
|
88
|
+
else if (C === 'S') {
|
|
89
|
+
const [bx, by] = [ox(), oy()];
|
|
90
|
+
const x1 = cx == null ? x : 2 * x - cx, y1 = cy == null ? y : 2 * y - cy;
|
|
91
|
+
curvaC(x1, y1, a[0] + bx, a[1] + by, a[2] + bx, a[3] + by);
|
|
92
|
+
} else if (C === 'Q') { const [bx, by] = [ox(), oy()]; curvaQ(a[0] + bx, a[1] + by, a[2] + bx, a[3] + by); }
|
|
93
|
+
else if (C === 'T') {
|
|
94
|
+
const x1 = qx == null ? x : 2 * x - qx, y1 = qy == null ? y : 2 * y - qy;
|
|
95
|
+
curvaQ(x1, y1, a[0] + ox(), a[1] + oy());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (pts.length < 2) throw new Error('o caminho precisa de pelo menos dois pontos');
|
|
100
|
+
return pts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `n` pontos igualmente espaçados ao longo do contorno fechado. */
|
|
104
|
+
function reamostrar(pts, n) {
|
|
105
|
+
const fechado = [...pts, pts[0]];
|
|
106
|
+
const acum = [0];
|
|
107
|
+
for (let i = 1; i < fechado.length; i++) {
|
|
108
|
+
acum.push(acum[i - 1] + Math.hypot(fechado[i][0] - fechado[i - 1][0], fechado[i][1] - fechado[i - 1][1]));
|
|
109
|
+
}
|
|
110
|
+
const total = acum[acum.length - 1] || 1;
|
|
111
|
+
const saida = [];
|
|
112
|
+
let j = 0;
|
|
113
|
+
for (let k = 0; k < n; k++) {
|
|
114
|
+
const alvo = (k / n) * total;
|
|
115
|
+
while (j < acum.length - 2 && acum[j + 1] < alvo) j++;
|
|
116
|
+
const seg = acum[j + 1] - acum[j] || 1;
|
|
117
|
+
const t = (alvo - acum[j]) / seg;
|
|
118
|
+
saida.push([fechado[j][0] + (fechado[j + 1][0] - fechado[j][0]) * t, fechado[j][1] + (fechado[j + 1][1] - fechado[j][1]) * t]);
|
|
119
|
+
}
|
|
120
|
+
return saida;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function area(pts) {
|
|
124
|
+
let s = 0;
|
|
125
|
+
for (let i = 0; i < pts.length; i++) { const a = pts[i], b = pts[(i + 1) % pts.length]; s += a[0] * b[1] - b[0] * a[1]; }
|
|
126
|
+
return s / 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pontos(d, n = PONTOS_PADRAO) {
|
|
130
|
+
const chave = n + '|' + d;
|
|
131
|
+
if (!_memo.has(chave)) _memo.set(chave, reamostrar(contorno(d), n));
|
|
132
|
+
return _memo.get(chave);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Os dois contornos no mesmo sentido e com o ponto de partida que menos anda. */
|
|
136
|
+
function alinhados(dA, dB, n = PONTOS_PADRAO) {
|
|
137
|
+
const chave = 'al|' + n + '|' + dA + '|' + dB;
|
|
138
|
+
if (_memo.has(chave)) return _memo.get(chave);
|
|
139
|
+
const a = pontos(dA, n);
|
|
140
|
+
let b = pontos(dB, n);
|
|
141
|
+
if (Math.sign(area(a)) !== Math.sign(area(b))) b = [...b].reverse();
|
|
142
|
+
let melhor = 0, menor = Infinity;
|
|
143
|
+
for (let s = 0; s < n; s++) {
|
|
144
|
+
let soma = 0;
|
|
145
|
+
for (let i = 0; i < n && soma < menor; i++) {
|
|
146
|
+
const p = a[i], q = b[(i + s) % n];
|
|
147
|
+
soma += (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2;
|
|
148
|
+
}
|
|
149
|
+
if (soma < menor) { menor = soma; melhor = s; }
|
|
150
|
+
}
|
|
151
|
+
const r = { a, b: b.map((_, i) => b[(i + melhor) % n]) };
|
|
152
|
+
_memo.set(chave, r);
|
|
153
|
+
return r;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const f2 = (v) => String(Math.round(v * 100) / 100);
|
|
157
|
+
function caminhoDe(pts) {
|
|
158
|
+
return 'M ' + pts.map(([x, y]) => f2(x) + ' ' + f2(y)).join(' L ') + ' Z';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** O caminho no instante `k` (0 = dA, 1 = dB). */
|
|
162
|
+
function morfar(dA, dB, k, n = PONTOS_PADRAO) {
|
|
163
|
+
const { a, b } = alinhados(dA, dB, n);
|
|
164
|
+
const t = Math.max(0, Math.min(1, k));
|
|
165
|
+
return caminhoDe(a.map((p, i) => [p[0] + (b[i][0] - p[0]) * t, p[1] + (b[i][1] - p[1]) * t]));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ehCor = (v) => typeof v === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v.trim());
|
|
169
|
+
function rgb(h) {
|
|
170
|
+
let s = h.trim().slice(1);
|
|
171
|
+
if (s.length === 3) s = s.split('').map((c) => c + c).join('');
|
|
172
|
+
return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
|
|
173
|
+
}
|
|
174
|
+
function misturarCor(a, b, k) {
|
|
175
|
+
const x = rgb(a), y = rgb(b);
|
|
176
|
+
// A curva elástica passa de 1: a cor não pode passar de 255.
|
|
177
|
+
return '#' + x.map((v, i) => Math.max(0, Math.min(255, Math.round(v + (y[i] - v) * k))).toString(16).padStart(2, '0')).join('');
|
|
178
|
+
}
|
|
179
|
+
// ── fim do embutido ──
|
|
180
|
+
|
|
181
|
+
/** Este mesmo código como ES module, para o src/morfar.js do projeto Remotion. */
|
|
182
|
+
function fonteEsm() {
|
|
183
|
+
const src = require('fs').readFileSync(__filename, 'utf-8');
|
|
184
|
+
const ini = src.indexOf('// ── embutido');
|
|
185
|
+
const fim = src.indexOf('// ── fim do embutido');
|
|
186
|
+
return '/* Gerado de lib/estudio/morfar.js do PrimoCode — não edite aqui. */\n'
|
|
187
|
+
+ src.slice(ini, fim)
|
|
188
|
+
+ 'export { pontos, alinhados, caminhoDe, morfar, ehCor, misturarCor };\n';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { pontos, alinhados, caminhoDe, morfar, ehCor, misturarCor, fonteEsm, PONTOS_PADRAO };
|