primocode 10.0.9 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +19 -2
- package/lib/act.js +5 -2
- package/lib/catalogo.js +5 -2
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/estilo.js +24 -8
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +21 -3
- package/lib/estudio/hibrido.js +16 -12
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +134 -23
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +15 -5
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/tres.jsx +88 -21
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +3 -4
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/remotion.js +51 -6
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +80 -2
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/voz_kokoro.py +98 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fluido.js — o estilo FLUIDO: uma ideia por batida, muito respiro, e nada
|
|
3
|
+
* de corte. O elemento de uma batida VIRA o da próxima.
|
|
4
|
+
*
|
|
5
|
+
* "Uma ideia por quadro, muito espaço vazio. Texto pequeno, caixa normal,
|
|
6
|
+
* sans limpa. Movimento contínuo: um elemento se transforma no próximo."
|
|
7
|
+
* — a referência de motion do Joel (05/10), os vídeos bons.
|
|
8
|
+
*
|
|
9
|
+
* Existe UMA forma que vive o vídeo inteiro (o "portador"): é o ponto da
|
|
10
|
+
* abertura, a pílula do título, o anel do número, a primeira pílula da lista,
|
|
11
|
+
* a linha que risca o "não", o cartão da citação e o primeiro círculo da
|
|
12
|
+
* marca no fim. Entre uma batida e outra ela muda de lugar, de tamanho, de
|
|
13
|
+
* raio e de preenchimento — o olho segue a forma, e o corte some.
|
|
14
|
+
*
|
|
15
|
+
* O texto entra palavra por palavra (stagger curto, ease-out forte, sem
|
|
16
|
+
* linear) e cada frase tem UM acento: uma palavra em serifa itálica na cor de
|
|
17
|
+
* acento. Fundo em gradiente grande e macio, de uma paleta curta (no máximo
|
|
18
|
+
* dois acentos e os neutros), clara ou escura.
|
|
19
|
+
*
|
|
20
|
+
* Proibido aqui, de propósito (é a lista do "cara de IA"): HUD, cantoneira,
|
|
21
|
+
* timecode, caixa na palavra-chave, neon, partícula e CAIXA ALTA.
|
|
22
|
+
*
|
|
23
|
+
* Puro: devolve a partitura (os testes olham) e o plano com UMA peça do kit
|
|
24
|
+
* (kit/fluido.jsx), que desenha a partitura quadro a quadro.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const { volumesDaTrilha } = require('./ritmo.js');
|
|
30
|
+
|
|
31
|
+
const FPS = 30;
|
|
32
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
33
|
+
const POR_PALAVRA = 0.3, MINIMO = 0.8; // o tempo de leitura: 0,3 s por palavra, nunca menos de 0,8 s
|
|
34
|
+
const STAGGER = 0.07; // entre uma palavra e a próxima (a que a voz não diz)
|
|
35
|
+
const ANTES = 0.12; // a batida troca isto antes da primeira palavra da frase
|
|
36
|
+
|
|
37
|
+
/* As paletas: um fundo em três tons (o gradiente), a tinta, a tinta suave e
|
|
38
|
+
NO MÁXIMO dois acentos (o da palavra e o brilho difuso). */
|
|
39
|
+
const PALETAS = {
|
|
40
|
+
noite: { escuro: true, fundo: ['#020615', '#061a5c', '#1240c9'], tinta: '#eef2ff', suave: '#9fb0d9', acentos: ['#8fb8ff', '#2f6bff'] },
|
|
41
|
+
breu: { escuro: true, fundo: ['#030304', '#0b0a12', '#2a2156'], tinta: '#f4f4f6', suave: '#8d8a9c', acentos: ['#b4a6ff', '#5b45ff'] },
|
|
42
|
+
lavanda: { escuro: false, fundo: ['#f5f4ff', '#e4e1ff', '#b9b2ff'], tinta: '#17153a', suave: '#6e6a92', acentos: ['#4a3ce0', '#8f86ff'] },
|
|
43
|
+
creme: { escuro: false, fundo: ['#fbf8f2', '#f2e8d8', '#e7c9a5'], tinta: '#1e1a15', suave: '#857a6c', acentos: ['#c2502a', '#f0a36b'] },
|
|
44
|
+
};
|
|
45
|
+
const ESCURAS = ['noite', 'breu'], CLARAS = ['lavanda', 'creme'];
|
|
46
|
+
|
|
47
|
+
const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
|
|
48
|
+
|
|
49
|
+
/** A paleta: a pedida pelo nome; senão o tema (claro/escuro); senão a semente. */
|
|
50
|
+
function paletaDe(roteiro, semente) {
|
|
51
|
+
const pedida = String(roteiro.paleta || '').toLowerCase();
|
|
52
|
+
const tema = String(roteiro.tema || '').toLowerCase();
|
|
53
|
+
const nome = PALETAS[pedida] ? pedida
|
|
54
|
+
: /claro|light/.test(tema) ? CLARAS[semente % 2]
|
|
55
|
+
: /escuro|dark/.test(tema) ? ESCURAS[semente % 2]
|
|
56
|
+
: Object.keys(PALETAS)[semente % 4];
|
|
57
|
+
const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
|
|
58
|
+
// A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
|
|
59
|
+
const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
|
|
60
|
+
if (/^#[0-9a-f]{6}$/i.test(dest || '')) p.acentos[0] = dest;
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Caixa alta não entra: frase GRITADA vira caixa normal (sigla curta fica). */
|
|
65
|
+
function caixaNormal(t) {
|
|
66
|
+
const s = String(t || '').trim();
|
|
67
|
+
const letras = s.replace(/[^A-Za-zÀ-ÿ]/g, '');
|
|
68
|
+
if (letras.length < 4 || letras !== letras.toUpperCase()) return s;
|
|
69
|
+
const m = s.toLowerCase();
|
|
70
|
+
return m.charAt(0).toUpperCase() + m.slice(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VAZIAS = new Set('a o as os um uma de da do das dos e é em no na nos nas que com por para pra se seu sua você ao à mais muito isso este esta não sim'.split(' '));
|
|
74
|
+
const limpa = (w) => w.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
|
|
75
|
+
|
|
76
|
+
/** A frase em palavras, com UM acento: o destaque pedido, ou a palavra de mais peso. */
|
|
77
|
+
function frase(texto, { destaque = '', semAcento = false } = {}) {
|
|
78
|
+
const ws = caixaNormal(texto).split(/\s+/).filter(Boolean);
|
|
79
|
+
let ac = -1;
|
|
80
|
+
if (!semAcento && ws.length > 1) {
|
|
81
|
+
const d = limpa(String(destaque).split(/\s+/)[0] || '');
|
|
82
|
+
if (d) ac = ws.findIndex((w) => limpa(w) === d);
|
|
83
|
+
if (ac < 0) {
|
|
84
|
+
// A palavra mais longa que não é vazia; no empate, a mais tarde (a frase termina nela).
|
|
85
|
+
let melhor = 0;
|
|
86
|
+
ws.forEach((w, i) => { const n = limpa(w).length; if (!VAZIAS.has(limpa(w)) && n >= 4 && n >= melhor) { melhor = n; ac = i; } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return ws.map((t, i) => ({ t, acento: i === ac }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const numeroDe = (v) => {
|
|
93
|
+
const m = String(v == null ? '' : v).match(/^(\D*)([\d.,]+)(.*)$/);
|
|
94
|
+
if (!m) return null;
|
|
95
|
+
const n = Number(m[2].replace(/\./g, '').replace(',', '.'));
|
|
96
|
+
return Number.isFinite(n) ? { antes: m[1], ate: n, depois: m[3], casas: /,\d/.test(m[2]) ? 1 : 0 } : null;
|
|
97
|
+
};
|
|
98
|
+
const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
|
|
99
|
+
const leitura = (n) => Math.max(MINIMO, POR_PALAVRA * n);
|
|
100
|
+
|
|
101
|
+
/* A forma do portador em cada tipo de parte (kit/fluido.jsx dá o tamanho). */
|
|
102
|
+
const FORMA = { abertura: 'ponto', titulo: 'pilula', numero: 'anel', lista: 'pilula', passos: 'ponto', contraste: 'linha', citacao: 'cartao', cta: 'marca' };
|
|
103
|
+
const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: 'lista', cores: 'lista' };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
|
|
107
|
+
* a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
|
|
108
|
+
*/
|
|
109
|
+
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
110
|
+
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
|
|
111
|
+
const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
|
|
112
|
+
const paleta = paletaDe(roteiro, semente);
|
|
113
|
+
const marca = roteiro.marca || {};
|
|
114
|
+
const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
|
|
115
|
+
/* O comando do terminal: o link da cta, se for comando; senão o nome da marca. */
|
|
116
|
+
const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
|
|
117
|
+
const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
|
|
118
|
+
let comCartao = 0;
|
|
119
|
+
|
|
120
|
+
const batidas = partes.map((p, i) => {
|
|
121
|
+
const tipo = TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
|
|
122
|
+
const titulo = p.titulo || p.texto || '';
|
|
123
|
+
const fr = [];
|
|
124
|
+
const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
|
|
125
|
+
const b = { i, tipo, forma: FORMA[tipo], transicao: p.corte === true && i > 0 ? 'corte' : 'morfa', frases: fr, lado: 'centro' };
|
|
126
|
+
/* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
|
|
127
|
+
de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
|
|
128
|
+
const ui = p.ui === false ? null : typeof p.ui === 'string' ? p.ui
|
|
129
|
+
: tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
|
|
130
|
+
if (ui) {
|
|
131
|
+
b.lado = comCartao++ % 2 ? 'dir' : 'esq';
|
|
132
|
+
b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
|
|
133
|
+
: ui === 'chat' ? { tipo: ui, pergunta: caixaNormal(titulo) }
|
|
134
|
+
: { tipo: 'painel', rotulo: caixaNormal(p.rotulo || titulo), barras: [0, 1, 2, 3, 4, 5, 6].map((k) => r2(0.35 + ((semente >>> (k * 3)) % 7) / 10)) };
|
|
135
|
+
}
|
|
136
|
+
switch (tipo) {
|
|
137
|
+
case 'numero': {
|
|
138
|
+
b.numero = numeroDe(p.valor);
|
|
139
|
+
poe('numero', p.valor, { semAcento: true });
|
|
140
|
+
poe('rotulo', p.rotulo || titulo, { destaque: p.destaque });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'lista': case 'passos':
|
|
144
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
145
|
+
itensDe(p).slice(0, tipo === 'passos' ? 6 : 4).forEach((it) => poe('item', it, { semAcento: true }));
|
|
146
|
+
break;
|
|
147
|
+
case 'contraste':
|
|
148
|
+
poe('nao', p.nao, { semAcento: true });
|
|
149
|
+
poe('sim', p.sim || titulo, { destaque: p.destaque });
|
|
150
|
+
break;
|
|
151
|
+
case 'citacao':
|
|
152
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
153
|
+
poe('autor', p.autor ? `— ${p.autor}` : '', { semAcento: true });
|
|
154
|
+
break;
|
|
155
|
+
case 'cta':
|
|
156
|
+
b.marca = { nome: caixaNormal(marca.nome || titulo) };
|
|
157
|
+
poe('marca', b.marca.nome, { semAcento: true });
|
|
158
|
+
poe('principal', marca.nome ? titulo : '', { destaque: p.destaque });
|
|
159
|
+
poe('sub', p.link || p.sub, { semAcento: true });
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
163
|
+
poe('sub', p.sub, { semAcento: true });
|
|
164
|
+
}
|
|
165
|
+
return b;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
|
|
169
|
+
pouco antes da primeira palavra da parte) e cada palavra na tela
|
|
170
|
+
aparece no instante em que é dita (os tempos por palavra da narração).
|
|
171
|
+
A palavra que a voz não diz entra em cascata curta. Se o texto pede
|
|
172
|
+
mais leitura do que a frase deu, a batida estica e a próxima volta a
|
|
173
|
+
casar com a voz na primeira folga; o atraso medido vai na partitura. */
|
|
174
|
+
let atraso = 0, sincronia = 0;
|
|
175
|
+
batidas.forEach((b, i) => {
|
|
176
|
+
const tp = temVoz ? tempos[i] : null;
|
|
177
|
+
const deVoz = tp ? (i === 0 ? 0 : Math.max(0, tp.de - ANTES)) : null;
|
|
178
|
+
b.de = r2(i === 0 ? 0 : Math.max(batidas[i - 1].ate, deVoz == null ? 0 : deVoz));
|
|
179
|
+
if (deVoz != null) atraso = Math.max(atraso, r2(b.de - deVoz));
|
|
180
|
+
const ditas = ((tp && tp.palavras) || []).map((w) => ({ k: limpa(w.texto), de: w.de }));
|
|
181
|
+
let cursor = 0, livre = b.de + (b.tipo === 'cta' ? 0.55 : 0.12);
|
|
182
|
+
const fimVoz = tp ? (i < batidas.length - 1 ? tempos[i + 1].de - ANTES : tp.ate + 0.2) : 0;
|
|
183
|
+
for (const f of b.frases) {
|
|
184
|
+
/* Casa as palavras da tela com as faladas, na ordem. Tenta começar de
|
|
185
|
+
cada palavra (a frase da tela pode vir em outra ordem que a falada)
|
|
186
|
+
e fica com o casamento que acha mais palavras. */
|
|
187
|
+
const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
|
|
188
|
+
let ts = f.palavras.map(() => null), fim = cursor;
|
|
189
|
+
for (let ini = 0; ini < f.palavras.length; ini++) {
|
|
190
|
+
let c = cursor;
|
|
191
|
+
const tt = f.palavras.map((w, j) => {
|
|
192
|
+
const k = limpa(w.t);
|
|
193
|
+
if (j < ini || !k) return null;
|
|
194
|
+
for (let q = c; q < Math.min(ditas.length, c + 12); q++) if (casa(k, ditas[q].k)) { c = q + 1; return ditas[q].de; }
|
|
195
|
+
return null;
|
|
196
|
+
});
|
|
197
|
+
if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
|
|
198
|
+
}
|
|
199
|
+
cursor = fim;
|
|
200
|
+
f.falada = ts.filter((x) => x != null).length >= f.palavras.length / 2;
|
|
201
|
+
const ach = ts.findIndex((x) => x != null);
|
|
202
|
+
/* O que a voz não diz entra cedo o bastante para ser lido antes de a
|
|
203
|
+
frase acabar (e nunca antes da batida). */
|
|
204
|
+
let t = ach >= 0 ? ts[ach] - ach * STAGGER
|
|
205
|
+
: tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
|
|
206
|
+
f.palavras.forEach((w, j) => {
|
|
207
|
+
const falada = ts[j];
|
|
208
|
+
t = Math.max(b.de + 0.06, falada != null ? Math.max(t, falada - 0.05) : t);
|
|
209
|
+
w.em = r2(t);
|
|
210
|
+
if (falada != null) sincronia = Math.max(sincronia, r2(w.em - falada));
|
|
211
|
+
t += STAGGER;
|
|
212
|
+
});
|
|
213
|
+
f.de = f.palavras[0].em;
|
|
214
|
+
// O que não é dito (rótulo, item, link) entra logo depois do anterior.
|
|
215
|
+
livre = Math.max(livre, f.papel === 'item' && b.tipo === 'passos' ? f.de + leitura(f.palavras.length) * 0.8 : f.palavras[f.palavras.length - 1].em + 0.18);
|
|
216
|
+
}
|
|
217
|
+
/* O texto que a voz DIZ é lido junto com ela: fica até depois da última
|
|
218
|
+
palavra dita (e nunca menos de 0,8 s). O que a voz não diz pede 0,3 s por palavra. */
|
|
219
|
+
const precisa = Math.max(...b.frases.map((f) => f.de + (f.falada
|
|
220
|
+
? Math.max(MINIMO, f.palavras[f.palavras.length - 1].em - f.de + 0.3) : leitura(f.palavras.length))), b.de + 0.9);
|
|
221
|
+
b.ate = r2(Math.max(precisa, fimVoz));
|
|
222
|
+
});
|
|
223
|
+
// A marca do fim segura um instante; o resto não espera.
|
|
224
|
+
const ult = batidas[batidas.length - 1];
|
|
225
|
+
ult.ate = r2(ult.ate + (ult.tipo === 'cta' ? 1.0 : 0.5));
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
|
|
229
|
+
fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
|
|
230
|
+
logo: marca.logo || null,
|
|
231
|
+
batidas,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {object} op
|
|
237
|
+
* @param {object} op.roteiro { marca, paleta?: noite|breu|lavanda|creme, tema?: claro|escuro, partes }
|
|
238
|
+
* @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
|
|
239
|
+
* @param {object} op.sons os efeitos leves (sons-leves.js)
|
|
240
|
+
*/
|
|
241
|
+
function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
|
|
242
|
+
const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
|
|
243
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
244
|
+
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
|
|
245
|
+
const DUR = partitura.duracao;
|
|
246
|
+
/* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
|
|
247
|
+
cada transformação, pop quando pílula ou cartão assenta, clique na roda e
|
|
248
|
+
no terminal, um bolha discreto na palavra de acento, o riser que leva à
|
|
249
|
+
marca e o impacto quando ela trava. Baixo: a voz vem primeiro. */
|
|
250
|
+
const efeitos = [];
|
|
251
|
+
const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
|
|
252
|
+
for (const b of partitura.batidas) {
|
|
253
|
+
if (b.i > 0 && b.tipo !== 'cta') som(b.de - 0.18, 'whoosh', 0.2);
|
|
254
|
+
if (b.ui) { som(b.de + 0.6, 'pop', 0.16); if (b.ui.tipo === 'terminal') b.ui.linhas.forEach((_, k) => som(b.de + 0.75 + k * 0.5, 'clique', 0.1)); }
|
|
255
|
+
for (const f of b.frases) {
|
|
256
|
+
if (f.papel === 'item') som(f.de - 0.06, b.tipo === 'passos' ? 'clique' : 'pop', 0.15);
|
|
257
|
+
const ac = f.palavras.find((w) => w.acento);
|
|
258
|
+
if (ac && f.papel !== 'item') som(ac.em, 'bolha', 0.09);
|
|
259
|
+
}
|
|
260
|
+
if (b.tipo === 'citacao') som(b.de + 0.3, 'pop', 0.14);
|
|
261
|
+
if (b.tipo === 'cta') { som(b.de - 0.75, 'riser', 0.2); som(b.de + 0.45, 'impacto', 0.26); som(b.de + 0.5, 'brilho', 0.16); }
|
|
262
|
+
}
|
|
263
|
+
const volumes = volumesDaTrilha(tempos.length === partes.length ? tempos : [], DUR, { alto: 0.55, baixo: 0.22 });
|
|
264
|
+
const P = partitura.paleta;
|
|
265
|
+
const plano = {
|
|
266
|
+
formato: { largura, altura, fps: FPS },
|
|
267
|
+
fundoGeral: P.fundo[0],
|
|
268
|
+
fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
|
|
269
|
+
cenas: [{
|
|
270
|
+
duracao: DUR,
|
|
271
|
+
fundo: P.fundo[0],
|
|
272
|
+
elementos: [{ tipo: 'kit', peca: 'fluido', partitura, fonte: partitura.fontes.texto, fonteSerifa: partitura.fontes.serifa, fonteMono: partitura.fontes.mono }],
|
|
273
|
+
}],
|
|
274
|
+
audio: {
|
|
275
|
+
...(narracao ? { narracao } : {}),
|
|
276
|
+
...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
|
|
277
|
+
efeitos,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -827,6 +827,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
827
827
|
};
|
|
828
828
|
const registra = (b) => { usadas[b.comp] = (usadas[b.comp] || 0) + 1; recentes.push(b.comp); anterior = b.comp; batidas.push(b); };
|
|
829
829
|
|
|
830
|
+
/* PEDIU 3D, O 3D APARECE: o sorteio podia passar o vídeo inteiro sem a
|
|
831
|
+
batida do produto (3 partes, "pilha → contador → pilha", 05/10). Se
|
|
832
|
+
nenhuma escolher sozinha, a melhor parte ganha: a de produto, o cta ou
|
|
833
|
+
a primeira com título. */
|
|
834
|
+
const ultimaDe = (tipos) => { for (let i = brutas.length - 1; i >= 0; i--) if (tipos.includes(brutas[i].conteudo.tipo)) return i; return -1; };
|
|
835
|
+
let alvo3d = -1;
|
|
836
|
+
if (tem3d) {
|
|
837
|
+
alvo3d = ultimaDe(['produto']);
|
|
838
|
+
if (alvo3d < 0) alvo3d = ultimaDe(['cta']);
|
|
839
|
+
if (alvo3d < 0) alvo3d = brutas.findIndex((b) => ['titulo', 'marca'].includes(b.conteudo.tipo));
|
|
840
|
+
}
|
|
841
|
+
|
|
830
842
|
brutas.forEach((br, bi) => {
|
|
831
843
|
const c = br.conteudo;
|
|
832
844
|
const tituloC = c.titulo || c.nome || c.texto || '';
|
|
@@ -836,6 +848,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
836
848
|
pergunta: /\?/.test(tituloC), exclama: /!/.test(tituloC), numeroNoTitulo: /\d/.test(tituloC),
|
|
837
849
|
};
|
|
838
850
|
let nome = escolher(pesosDe(c, feat));
|
|
851
|
+
if (bi === alvo3d && !usadas.produto) nome = 'produto';
|
|
839
852
|
const ctx = { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes, P, palavras: br.palavras || c.palavras || [], parte: br.parte, marca, roteiro, memoria };
|
|
840
853
|
if (nome === 'rajada' && br.ate - br.de < 1.6 * c.itens.length) nome = 'cascata';
|
|
841
854
|
if (nome === 'rajada') {
|
|
@@ -1042,20 +1055,25 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
1042
1055
|
|
|
1043
1056
|
// ── o produto em 3D: mora nas batidas que o hospedam ─────────────────
|
|
1044
1057
|
const produtos = batidas.filter((b) => b.produto3d).map((b) => ({ ...b.produto3d, de: b.de, ate: b.ate, modelo: roteiro.modelo3d,
|
|
1045
|
-
cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento }));
|
|
1058
|
+
cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento, ...(roteiro.variacao3d || {}) }));
|
|
1046
1059
|
|
|
1047
1060
|
// ── o fundo vivo ────────────────────────────────────────────────────
|
|
1048
|
-
|
|
1061
|
+
/* SEM CARA DE IA (05/10): grade, pontinhos flutuando e o HUD só a pedido
|
|
1062
|
+
(roteiro.hud, roteiro.particulas). O sorteio continua igual — a semente
|
|
1063
|
+
dá o mesmo vídeo de antes, só sem o enfeite. */
|
|
1064
|
+
const texturaSorteada = s.um(['grade', 'pontos', 'linhas', 'grade']);
|
|
1065
|
+
const fundo = { textura: roteiro.hud === true ? texturaSorteada : null, passo: Math.round(s.um([72, 96, 120]) * base), grao: luz(P.fundo) < 0.5 ? 0.07 : 0.05,
|
|
1049
1066
|
brilho: { x: s.entre(0.25, 0.75), y: s.entre(0.2, 0.5) },
|
|
1050
1067
|
// Partículas em três profundidades: o espaço respira mesmo no repouso.
|
|
1051
1068
|
particulas: Array.from({ length: 34 }, (_, i) => ({ x: r1(s.prox() * W), y: r1(s.prox() * H), z: [0.25, 0.5, 0.85][i % 3], r: r1((1.2 + s.prox() * 2.4) * base), v: r1(s.entre(6, 22) * base) })) };
|
|
1069
|
+
if (roteiro.particulas !== true) fundo.particulas = [];
|
|
1052
1070
|
|
|
1053
1071
|
return {
|
|
1054
1072
|
versao: 1, semente, fps: FPS, humor,
|
|
1055
1073
|
musica: { genero: s.um(H0.generos), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
|
|
1056
1074
|
paleta: { ...P, fundo: campos[0], tinta: campos[2], acento: campos[1] }, campos, fontes, eixos: Object.fromEntries(Object.values(fontes).map((f) => [f, (VARIAVEIS[f] || {}).eixos || { wght: [400, 700] }])),
|
|
1057
1075
|
margem: M, fundo, batidas, camera, produtos,
|
|
1058
|
-
hud: { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' },
|
|
1076
|
+
hud: roteiro.hud === true ? { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' } : null,
|
|
1059
1077
|
};
|
|
1060
1078
|
}
|
|
1061
1079
|
|
package/lib/estudio/hibrido.js
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* O que cada lado traz:
|
|
10
10
|
* do cinema (cinema.js) foto e vídeo REAIS ocupando a tela, escurecidos,
|
|
11
|
-
* trocando por baixo do mesmo texto
|
|
12
|
-
*
|
|
11
|
+
* trocando por baixo do mesmo texto, escrito como a
|
|
12
|
+
* pessoa escreveu, com a palavra da tese na serifa; o
|
|
13
13
|
* número que conta; a virada que bate no fundo; a
|
|
14
14
|
* marca só no fim.
|
|
15
15
|
* do kit (kit/*.jsx) a câmera que empurra e desliza em toda cena; a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* líquido entre blocos; grão e vinheta. A etiqueta
|
|
17
|
+
* da fonte, o HUD e as partículas só a pedido
|
|
18
|
+
* (roteiro.hud, roteiro.particulas).
|
|
19
19
|
*
|
|
20
20
|
* Três passos, e nenhum repete o que o cinema já faz:
|
|
21
21
|
* traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
|
|
@@ -106,7 +106,10 @@ function camera(d, i, deitado, forte) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/** O kit por cima do plano do cinema. Mexe no plano recebido e o devolve. */
|
|
109
|
-
|
|
109
|
+
/* SEM CARA DE IA (05/10): o HUD (cantos, timecode, fps, rótulos nas bordas)
|
|
110
|
+
e as partículas eram exatamente a lista do "ruim" que o Joel juntou. Agora
|
|
111
|
+
só entram quando o roteiro pede: roteiro.hud === true, roteiro.particulas === true. */
|
|
112
|
+
function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura, hud = false, particulas = false } = {}) {
|
|
110
113
|
if (!plano || !Array.isArray(plano.cenas)) return plano;
|
|
111
114
|
const W = largura || (plano.formato && plano.formato.largura) || 1920;
|
|
112
115
|
const H = altura || (plano.formato && plano.formato.altura) || 1080;
|
|
@@ -132,18 +135,18 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
132
135
|
const forte = tipo === 'numero' || tipo === 'contraste' || tipo === 'abertura';
|
|
133
136
|
if (!cena.camera) cena.camera = camera(d, c, deitado, forte);
|
|
134
137
|
|
|
135
|
-
// A FONTE que se digita, com a barra do acento:
|
|
136
|
-
if (ex.fonte && d > 1.4) {
|
|
138
|
+
// A FONTE que se digita, com a barra do acento (é rótulo de borda: vem com o HUD).
|
|
139
|
+
if (hud && ex.fonte && d > 1.4) {
|
|
137
140
|
els.push({ tipo: 'kit', peca: 'fonte', texto: String(ex.fonte).slice(0, 48), t: Math.min(0.9, d * 0.35),
|
|
138
141
|
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
142
|
}
|
|
140
143
|
// O NÚMERO estoura em partículas quando assenta.
|
|
141
|
-
if (tipo === 'numero' || tipo === 'grafico') {
|
|
144
|
+
if (particulas && (tipo === 'numero' || tipo === 'grafico')) {
|
|
142
145
|
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
146
|
tamanho: Math.max(5, Math.round(base * 0.007)), cores: ['#ffffff', ACENTO, grad[0]] });
|
|
144
147
|
}
|
|
145
148
|
// O FIM: a marca entra com um brilho de partículas.
|
|
146
|
-
if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') {
|
|
149
|
+
if (particulas && (tipo === 'cta' || tipo === 'logo' || tipo === 'fim')) {
|
|
147
150
|
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
151
|
tamanho: Math.max(4, Math.round(base * 0.006)), cores: ['#ffffff', ACENTO], forma: 'circulo' });
|
|
149
152
|
}
|
|
@@ -154,8 +157,9 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
154
157
|
}
|
|
155
158
|
});
|
|
156
159
|
|
|
157
|
-
// O HUD de motion designer
|
|
158
|
-
plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
|
|
160
|
+
// O HUD de motion designer (cantos, timecode e a marca): só a pedido.
|
|
161
|
+
if (hud) plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
|
|
162
|
+
else delete plano.hud;
|
|
159
163
|
// O acabamento de filme um pouco mais presente que o do cinema.
|
|
160
164
|
plano.acabamento = { ...(plano.acabamento || {}), grao: Math.max(0.06, (plano.acabamento || {}).grao || 0), vinheta: Math.max(0.55, (plano.acabamento || {}).vinheta || 0) };
|
|
161
165
|
plano.estilo = 'hibrido';
|
package/lib/estudio/impacto.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
const { caber, paleta } = require('./composicao.js');
|
|
26
26
|
const { iconeDe, peso } = require('./edicao.js');
|
|
27
|
+
const { minimoLeitura, sobra } = require('./legibilidade.js');
|
|
27
28
|
|
|
28
29
|
const r2 = (n) => Math.round(n * 100) / 100;
|
|
29
30
|
const limpa = (t) => String(t || '').replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}%!?]+$/gu, '');
|
|
@@ -72,11 +73,12 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
72
73
|
const icOk = (ic) => ic && (!iconesOk || iconesOk.has(ic));
|
|
73
74
|
|
|
74
75
|
// A palavra que bate: letra gigante, a palavra forte na cor de destaque.
|
|
75
|
-
const bater = (els, texto, { y = null, corTexto = cor.tinta, destaque = cor.destaque, max = (deitado ? H * 0.2 : W * 0.2), linhas = 3, glitch = false, girar = 0 } = {}) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
25/09: "CONSTRUÇÃO" e
|
|
76
|
+
const bater = (els, texto, { y = null, corTexto = cor.tinta, destaque = cor.destaque, max = (deitado ? H * 0.2 : W * 0.2), linhas = 3, glitch = false, girar = 0, entra = 0.22 } = {}) => {
|
|
77
|
+
// Como a pessoa escreveu: caixa alta forçada era a "cara de IA" (05/10).
|
|
78
|
+
const t = String(texto || '');
|
|
79
|
+
/* Peso 900 é mais largo que a letra comum, e a batida ainda cresce 5%:
|
|
80
|
+
mede numa caixa mais estreita (medido em 25/09: "CONSTRUÇÃO" e
|
|
81
|
+
"DEMONSTRAÇÃO" saíam pela borda). */
|
|
80
82
|
const fit = caber(t, { largura: W0 * 0.8, linhas, max, fonte: F.titulo });
|
|
81
83
|
const h = fit.tamanho * 0.98 * fit.linhas;
|
|
82
84
|
const ws = t.split(/\s+/);
|
|
@@ -85,12 +87,14 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
85
87
|
els.push({
|
|
86
88
|
tipo: 'texto', texto: t, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: corTexto, entrelinha: 0.98,
|
|
87
89
|
x: M, y: Math.round(y == null ? (H - h) / 2 : y), largura: W0, alinhamento: 'center', espacamento: -1,
|
|
88
|
-
|
|
90
|
+
// A palavra forte é UM acento: a serifa em itálico, que chega um instante depois.
|
|
91
|
+
...(forte >= 0 && ws.length > 1 ? { revelar: ws.map(() => 0), destaques: [forte], corDestaque: destaque, fonteDestaque: 'Instrument Serif' } : {}),
|
|
89
92
|
...(glitch ? { glitch: { forca: 14, dur: 0.3 } } : {}),
|
|
90
93
|
anima: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// A entrada encolhe junto (entra < 0,22) quando a batida é curta: a leitura não.
|
|
95
|
+
escala: [{ t: 0, v: 1.45 }, { t: r2(entra * 0.6), v: 0.97, e: 'saida' }, { t: r2(entra), v: 1, e: 'suave' }, { t: 3, v: 1.05, e: 'linear' }],
|
|
96
|
+
opacidade: [{ t: 0, v: 0 }, { t: r2(entra * 0.23), v: 1 }],
|
|
97
|
+
rotacao: [{ t: 0, v: girar * 2 }, { t: r2(entra * 0.9), v: girar, e: 'saida' }],
|
|
94
98
|
},
|
|
95
99
|
});
|
|
96
100
|
return h;
|
|
@@ -126,18 +130,51 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
126
130
|
let bs = batidas((tp.palavras || []).filter((w) => w && w.texto), fim);
|
|
127
131
|
if (!bs.length) bs = [[{ de: ini, ate: fim, texto: p.titulo || p.sim || p.valor || '' }]];
|
|
128
132
|
const itens = (p.itens || []).map((it) => (typeof it === 'string' ? it : it.texto));
|
|
133
|
+
const semNaoE = (t) => String(t || '').replace(/^\s*(n[aã]o\s+[ée]\s+|[ée]\s+)/i, '');
|
|
134
|
+
// Os textos que a batida k (de n) mostra — os mesmos que o desenho abaixo usa.
|
|
135
|
+
const textosDe = (b, k, n) => {
|
|
136
|
+
const falado = b.map((w) => limpa(w.texto)).join(' ');
|
|
137
|
+
if (p.tipo === 'numero' && k === 0) return [String(p.valor), p.rotulo || ''];
|
|
138
|
+
if ((p.tipo === 'lista' || p.tipo === 'passos') && itens.length) {
|
|
139
|
+
const it = itens[Math.min(k, itens.length - 1)];
|
|
140
|
+
return [p.tipo === 'passos' ? `${k + 1}. ${it}` : it, k === 0 ? p.titulo || '' : ''];
|
|
141
|
+
}
|
|
142
|
+
if (p.tipo === 'cta' && k === n - 1) return [p.titulo || falado, p.link || ''];
|
|
143
|
+
if (p.tipo === 'contraste') return [k < n / 2 ? `não é ${semNaoE(p.nao)}` : semNaoE(p.sim)];
|
|
144
|
+
// O título no fim só se der tempo de ler; senão, a fala dessa batida.
|
|
145
|
+
return [k === n - 1 && p.titulo && k > 0 && vao(k) >= minimoLeitura(p.titulo) + 0.08 ? p.titulo : falado];
|
|
146
|
+
};
|
|
147
|
+
/* A lei do /brag: cada batida fica no ar a entrada mínima mais
|
|
148
|
+
max(0,8 s; 0,3 s por palavra) — dentro do tempo da parte, sem esticar
|
|
149
|
+
o vídeo. Batida curta pega a folga da vizinha (o corte anda um pouco);
|
|
150
|
+
sem folga, gruda nela. */
|
|
151
|
+
const inis = bs.map((b, k) => (k === 0 ? ini : b[0].de - 0.04));
|
|
152
|
+
const vao = (k) => (k < bs.length - 1 ? inis[k + 1] : fim) - inis[k];
|
|
153
|
+
const precisa = (k) => Math.max(...textosDe(bs[k], k, bs.length).filter(Boolean).map(minimoLeitura))
|
|
154
|
+
+ (p.tipo === 'cta' && k === bs.length - 1 && p.link ? 0.23 : 0) + 0.08; // o link entra depois do título
|
|
155
|
+
for (let k = 0; bs.length > 1 && k < bs.length;) {
|
|
156
|
+
const falta = precisa(k) - vao(k);
|
|
157
|
+
if (falta <= 0) { k++; continue; }
|
|
158
|
+
if (k + 1 < bs.length && vao(k + 1) - falta >= precisa(k + 1)) { inis[k + 1] = r2(inis[k + 1] + falta); k++; continue; }
|
|
159
|
+
if (k > 0 && vao(k - 1) - falta >= precisa(k - 1)) { inis[k] = r2(inis[k] - falta); k++; continue; }
|
|
160
|
+
const a = k > 0 ? k - 1 : 0;
|
|
161
|
+
bs.splice(a, 2, [...bs[a], ...bs[a + 1]]);
|
|
162
|
+
inis.splice(a + 1, 1);
|
|
163
|
+
k = 0;
|
|
164
|
+
}
|
|
129
165
|
|
|
130
166
|
// Impacto no começo de cada parte; o riser anuncia a chamada final.
|
|
131
167
|
som(ini, 'impacto', i === 0 ? 0.9 : 0.6);
|
|
132
168
|
if (p.tipo === 'cta') som(ini - 2, 'riser', 0.6);
|
|
133
169
|
|
|
134
170
|
bs.forEach((b, k) => {
|
|
135
|
-
const de = k
|
|
136
|
-
const ate = k < bs.length - 1 ?
|
|
171
|
+
const de = inis[k];
|
|
172
|
+
const ate = k < bs.length - 1 ? inis[k + 1] : fim;
|
|
137
173
|
const falado = b.map((w) => limpa(w.texto)).join(' ');
|
|
138
174
|
const els = [];
|
|
139
175
|
const cena = { fundo: { tipo: 'cor', valor: cor.fundo }, elementos: els };
|
|
140
176
|
const ultimo = k === bs.length - 1;
|
|
177
|
+
const entra = r2(sobra(vao(k), precisa(k) - 0.08, 0.08, 0.22));
|
|
141
178
|
let tela; // 'palavra' | 'foto' | 'marca'
|
|
142
179
|
|
|
143
180
|
if (p.tipo === 'numero' && k === 0) {
|
|
@@ -148,8 +185,8 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
148
185
|
const fit = caber(valor, { largura: W0, linhas: 1, max: H * (deitado ? 0.55 : 0.32), fonte: F.titulo });
|
|
149
186
|
els.push({ tipo: 'texto', texto: valor, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: '#FFFFFF', x: M, y: Math.round(H * 0.5 - fit.tamanho * 0.62), largura: W0, alinhamento: 'center',
|
|
150
187
|
...(m ? { contar: { ate: Number(m[2].replace(/\./g, '').replace(',', '.')), dur: 0.7, antes: m[1], depois: m[3] } } : {}),
|
|
151
|
-
anima: { escala: [{ t: 0, v: 1.6 }, { t: 0.18, v: 1, e: 'saida' }] } });
|
|
152
|
-
bater(els, p.rotulo || '', { y: H * 0.5 + fit.tamanho * 0.45, max: H * 0.07, linhas: 2, corTexto: '#FFFFFF', destaque: '#FFFFFF' });
|
|
188
|
+
anima: { escala: [{ t: 0, v: 1.6 }, { t: r2(Math.min(0.18, entra)), v: 1, e: 'saida' }] } });
|
|
189
|
+
bater(els, p.rotulo || '', { y: H * 0.5 + fit.tamanho * 0.45, max: H * 0.07, linhas: 2, corTexto: '#FFFFFF', destaque: '#FFFFFF', entra });
|
|
153
190
|
cena.tremor = [{ de: 0, ate: 0.5, forca: 26 }];
|
|
154
191
|
tela = 'marca';
|
|
155
192
|
} else if ((p.tipo === 'lista' || p.tipo === 'passos') && itens.length) {
|
|
@@ -159,9 +196,9 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
159
196
|
const rot = p.tipo === 'passos' ? `${k + 1}. ${it}` : it;
|
|
160
197
|
const temE = emoji(els, it, { y: H * (deitado ? 0.28 : 0.3) });
|
|
161
198
|
// No gradiente, o destaque é o escuro da marca: laranja sobre azul some.
|
|
162
|
-
bater(els, rot, { y: temE ? H * (deitado ? 0.48 : 0.45) : null, max: deitado ? H * 0.11 : W * 0.12, linhas: 4, glitch: k === 0,
|
|
199
|
+
bater(els, rot, { y: temE ? H * (deitado ? 0.48 : 0.45) : null, max: deitado ? H * 0.11 : W * 0.12, linhas: 4, glitch: k === 0, entra,
|
|
163
200
|
...(k % 2 === 1 ? { corTexto: '#FFFFFF', destaque: cor.fundo } : {}) });
|
|
164
|
-
if (k === 0 && p.titulo) els.push({ tipo: 'texto', texto: String(p.titulo)
|
|
201
|
+
if (k === 0 && p.titulo) els.push({ tipo: 'texto', texto: String(p.titulo), fonte: F.texto, peso: 700, tamanho: Math.round(Math.min(W, H) * 0.04), cor: cor.destaque, x: M, y: M * 2.2, largura: W0, alinhamento: 'center' });
|
|
165
202
|
tela = k % 2 ? 'marca' : 'palavra';
|
|
166
203
|
cena.socos = [{ t: 0, forca: 0.14 }];
|
|
167
204
|
som(de, 'estalo', 0.55);
|
|
@@ -173,7 +210,7 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
173
210
|
els.push({ tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(W / 2 - R / 2), y: Math.round(H * 0.4 - R / 2), cor: grad[1], opacidade: 0.4,
|
|
174
211
|
anima: { desfoque: [{ t: 0, v: Math.round(R * 0.2) }], escala: [{ t: 0, v: 0.7 }, { t: 1, v: 1.1, e: 'suave' }] } });
|
|
175
212
|
const yT = H * 0.26;
|
|
176
|
-
const hT = bater(els, p.titulo || falado, { y: yT, max: deitado ? H * 0.14 : W * 0.14, linhas: 3, corTexto: '#FFFFFF', destaque: cor.destaque });
|
|
213
|
+
const hT = bater(els, p.titulo || falado, { y: yT, max: deitado ? H * 0.14 : W * 0.14, linhas: 3, corTexto: '#FFFFFF', destaque: cor.destaque, entra });
|
|
177
214
|
if (p.link) {
|
|
178
215
|
const fl = Math.round(Math.min(W, H) * 0.055);
|
|
179
216
|
const wP = Math.min(W0, String(p.link).length * fl * 0.62 + fl * 2.4);
|
|
@@ -193,39 +230,37 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
|
|
|
193
230
|
// O rodízio das três telas, com a primeira batida sempre na palavra.
|
|
194
231
|
const ciclo = ['palavra', 'foto', 'marca', 'foto'];
|
|
195
232
|
tela = k === 0 ? (p.tipo === 'imagem' || i === 0 ? 'foto' : 'palavra') : ciclo[(nBatida + k) % ciclo.length];
|
|
196
|
-
const
|
|
197
|
-
const texto = p.tipo === 'contraste' ? (k < bs.length / 2 ? `não é ${semNaoE(p.nao)}` : semNaoE(p.sim))
|
|
198
|
-
: (ultimo && p.titulo && k > 0 ? p.titulo : falado);
|
|
233
|
+
const texto = textosDe(b, k, bs.length)[0];
|
|
199
234
|
if (tela === 'foto' && fotos.length) {
|
|
200
235
|
fotoCheia(els, proxFoto());
|
|
201
|
-
bater(els, texto, { y: H * (deitado ? 0.56 : 0.6), max: deitado ? H * 0.12 : W * 0.13, linhas: 3 });
|
|
236
|
+
bater(els, texto, { y: H * (deitado ? 0.56 : 0.6), max: deitado ? H * 0.12 : W * 0.13, linhas: 3, entra });
|
|
202
237
|
if (k % 2 === 0) cena.flashes = [0];
|
|
203
238
|
} else if (tela === 'marca') {
|
|
204
239
|
cena.fundo = { tipo: 'gradiente', cores: [grad[0], grad[1]], angulo: 135 };
|
|
205
240
|
const temE = emoji(els, texto, { y: H * (deitado ? 0.26 : 0.28) });
|
|
206
|
-
bater(els, texto, { y: temE ? H * (deitado ? 0.46 : 0.44) : null, corTexto: '#FFFFFF', destaque: cor.fundo, max: deitado ? H * 0.16 : W * 0.16 });
|
|
241
|
+
bater(els, texto, { y: temE ? H * (deitado ? 0.46 : 0.44) : null, corTexto: '#FFFFFF', destaque: cor.fundo, max: deitado ? H * 0.16 : W * 0.16, entra });
|
|
207
242
|
} else {
|
|
208
243
|
tela = 'palavra';
|
|
209
244
|
const temE = emoji(els, texto, { y: H * (deitado ? 0.25 : 0.27) });
|
|
210
245
|
bater(els, texto, { y: temE ? H * (deitado ? 0.45 : 0.43) : null, glitch: p.tipo === 'contraste' || k % 3 === 0,
|
|
211
|
-
girar: k % 2 ? -2 : 2, corTexto: p.tipo === 'contraste' && k < bs.length / 2 ? 'rgba(255,255,255,0.55)' : cor.tinta });
|
|
246
|
+
girar: k % 2 ? -2 : 2, entra, corTexto: p.tipo === 'contraste' && k < bs.length / 2 ? 'rgba(255,255,255,0.55)' : cor.tinta });
|
|
212
247
|
cena.socos = [{ t: 0, forca: 0.2 }];
|
|
213
248
|
}
|
|
214
249
|
if (k === 0) cena.tremor = [{ de: 0, ate: 0.35, forca: 18 }];
|
|
215
250
|
som(de, tela === 'foto' ? 'whoosh' : 'estalo', tela === 'foto' ? 0.5 : 0.5);
|
|
216
251
|
}
|
|
217
252
|
if (!(p.tipo === 'cta' && ultimo)) logoTopo(els);
|
|
218
|
-
/* A
|
|
219
|
-
letra pequena
|
|
253
|
+
/* A LINHA DE CIMA: o subtítulo da parte (é onde moram a data e o local)
|
|
254
|
+
em letra pequena no alto — o anúncio não pode esconder a data. Sem a
|
|
255
|
+
faixa de cor atrás (caixa de cor era a "cara de IA", 05/10): só o
|
|
256
|
+
texto, que sobe e aparece. */
|
|
220
257
|
if (p.sub && p.tipo !== 'numero' && !(p.tipo === 'lista' || p.tipo === 'passos')) {
|
|
221
|
-
const fk = Math.round(Math.min(W, H) * 0.
|
|
222
|
-
els.push({ tipo: 'forma', forma: 'retangulo', x: M, y: Math.round(H * (deitado ? 0.14 : 0.1)), largura: W0, altura: Math.round(fk * 2), cor: cor.destaque, raio: Math.round(fk),
|
|
223
|
-
anima: { escalaX: [{ t: 0.05, v: 0 }, { t: 0.3, v: 1, e: 'saida' }] }, origem: 'center' });
|
|
258
|
+
const fk = Math.round(Math.min(W, H) * 0.036);
|
|
224
259
|
// Com largura, e não pela âncora: sem largura o texto quebrava na metade da tela.
|
|
225
|
-
const tk = caber(String(p.sub)
|
|
226
|
-
els.push({ tipo: 'texto', texto: String(p.sub)
|
|
227
|
-
cor:
|
|
228
|
-
anima: { opacidade: [{ t: 0.2, v: 0 }, { t: 0.
|
|
260
|
+
const tk = caber(String(p.sub), { largura: W0 * 0.78, linhas: 1, max: fk, fonte: F.texto }).tamanho;
|
|
261
|
+
els.push({ tipo: 'texto', texto: String(p.sub), fonte: F.texto, peso: 600, tamanho: tk, entrelinha: 1,
|
|
262
|
+
cor: tela === 'marca' ? '#FFFFFF' : cor.destaque, x: M, largura: W0, alinhamento: 'center', y: Math.round(H * (deitado ? 0.14 : 0.1)),
|
|
263
|
+
anima: { opacidade: [{ t: 0.2, v: 0 }, { t: 0.5, v: 1 }], y: [{ t: 0.2, v: 12 }, { t: 0.6, v: 0, e: 'saida' }] } });
|
|
229
264
|
}
|
|
230
265
|
|
|
231
266
|
/* Uma transição curta a cada tanto; o resto é corte seco — é o
|