primocode 10.0.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- 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/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hibrido.js — o cinema com o kit por cima: foto real que não para, e o
|
|
3
|
+
* motion que parece feito à mão.
|
|
4
|
+
*
|
|
5
|
+
* "Deve funcionar como um mix da versão antiga (o reels da imersão) e a
|
|
6
|
+
* última versão — trabalhar juntas, absurdas, coisas que nem parece que
|
|
7
|
+
* foi IA que fez." (29/09)
|
|
8
|
+
*
|
|
9
|
+
* O que cada lado traz:
|
|
10
|
+
* do cinema (cinema.js) foto e vídeo REAIS ocupando a tela, escurecidos,
|
|
11
|
+
* trocando por baixo do mesmo texto, escrito como a
|
|
12
|
+
* pessoa escreveu, com a palavra da tese na serifa; o
|
|
13
|
+
* número que conta; a virada que bate no fundo; a
|
|
14
|
+
* marca só no fim.
|
|
15
|
+
* do kit (kit/*.jsx) a câmera que empurra e desliza em toda cena; a
|
|
16
|
+
* 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
|
+
*
|
|
20
|
+
* Três passos, e nenhum repete o que o cinema já faz:
|
|
21
|
+
* traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
|
|
22
|
+
* tipos do cinema; o que o cinema não usa (a fonte, o
|
|
23
|
+
* destaque) fica guardado por parte.
|
|
24
|
+
* termosDeBusca(roteiro) o que procurar de b-roll para cada parte.
|
|
25
|
+
* enfeitar(plano, …) o kit por cima do plano que o cinema montou.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
31
|
+
const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
|
|
32
|
+
const limpo = (x) => String(x || '').replace(/\s+/g, ' ').trim();
|
|
33
|
+
|
|
34
|
+
/* O b-roll quando a parte não diz o que procurar. Genérico de propósito: é o
|
|
35
|
+
fundo que se move, não a prova — a prova é o texto e a foto da marca. */
|
|
36
|
+
const BUSCA_PADRAO = ['technology abstract light', 'people working on laptop', 'hands typing keyboard close up',
|
|
37
|
+
'data center servers', 'city night timelapse', 'team meeting office', 'developer code screen', 'futuristic digital network'];
|
|
38
|
+
|
|
39
|
+
/** O roteiro de qualquer estilo → o roteiro do cinema. Devolve {roteiro, extras}. */
|
|
40
|
+
function traduzir(roteiro) {
|
|
41
|
+
const partes = (roteiro && roteiro.partes) || [];
|
|
42
|
+
const saida = [];
|
|
43
|
+
const extras = [];
|
|
44
|
+
// A virada: a parte marcada com climax, ou o último contraste antes do fim.
|
|
45
|
+
let iClimax = partes.findIndex((p) => p.climax);
|
|
46
|
+
if (iClimax < 0) {
|
|
47
|
+
for (let i = partes.length - 2; i > 0; i--) if (partes[i].tipo === 'contraste') { iClimax = i; break; }
|
|
48
|
+
}
|
|
49
|
+
partes.forEach((p, i) => {
|
|
50
|
+
const tipo = p.tipo || 'frase';
|
|
51
|
+
const marcar = [p.destaque, ...(p.marcar || [])].filter(Boolean);
|
|
52
|
+
const base = { fala: p.fala, ...(p.mudo ? { mudo: true } : {}), ...(p.falaForte ? { falaForte: p.falaForte } : {}), marcar };
|
|
53
|
+
let q;
|
|
54
|
+
// Os tipos que o cinema já conhece passam como estão.
|
|
55
|
+
if (['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'].includes(tipo)) q = { ...p };
|
|
56
|
+
else if (i === iClimax) q = { tipo: 'climax', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
|
|
57
|
+
// A abertura é o título, grande; o subtítulo vira a etiqueta digitada (a fonte).
|
|
58
|
+
else if (tipo === 'abertura') q = { tipo: 'montagem', linhas: [limpo(p.titulo)], ritmo: 0.6, ...base };
|
|
59
|
+
else if (tipo === 'titulo' || tipo === 'imagem') q = { tipo: 'montagem', linhas: [p.titulo || p.texto, p.sub].filter(Boolean).map(limpo), ...base };
|
|
60
|
+
else if (tipo === 'numero' || tipo === 'grafico') {
|
|
61
|
+
// "30% mais rápido": o cinema conta o número e escreve o resto embaixo.
|
|
62
|
+
q = { tipo: 'numeros', itens: [limpo(`${p.valor || ''} ${p.rotulo || p.titulo || ''}`)], ...base };
|
|
63
|
+
} else if (tipo === 'passos' || tipo === 'lista') {
|
|
64
|
+
/* Na escada, o item é o NOME ("Sonnet 4"); a data e o detalhe depois do
|
|
65
|
+
"·" ou do "—" estão na fala. Cinco itens de cinco palavras eram 7 s
|
|
66
|
+
de escada pela regra de 0,3 s por palavra. */
|
|
67
|
+
const itens = (p.itens || []).map((x) => limpo(typeof x === 'string' ? x : (x.titulo || x.texto || '')).split(/\s+[·—–-]\s+/)[0]).filter(Boolean);
|
|
68
|
+
q = { tipo: 'escada', itens: itens.slice(0, 5), ...(p.titulo ? { final: limpo(p.titulo) } : {}), ...base };
|
|
69
|
+
} else if (tipo === 'citacao') q = { tipo: 'frase', linhas: [`“${limpo(p.texto)}”`], ...base };
|
|
70
|
+
else if (tipo === 'contraste') q = { tipo: 'montagem', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
|
|
71
|
+
else if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') q = { tipo: 'final', linhas: [p.titulo || p.texto, p.link].filter(Boolean).map(limpo), ...base };
|
|
72
|
+
else q = { tipo: 'montagem', linhas: [p.titulo || p.texto || p.fala].filter(Boolean).map(limpo), ...base };
|
|
73
|
+
saida.push(q);
|
|
74
|
+
extras.push({ tipoOriginal: tipo, fonte: tipo === 'abertura' && p.sub ? p.sub : (p.fonte || (tipo === 'citacao' ? p.autor : '')), autor: p.autor || '', busca: p.busca || '' });
|
|
75
|
+
});
|
|
76
|
+
return { roteiro: { ...roteiro, partes: saida, estilo: 'hibrido' }, extras };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** O que procurar de b-roll para cada parte: a busca dela, a do roteiro, ou o padrão. */
|
|
80
|
+
function termosDeBusca(roteiro, extras = []) {
|
|
81
|
+
const global = [].concat(roteiro.busca || []).filter(Boolean);
|
|
82
|
+
// Escopo fechado: só os termos da pessoa, nunca o padrão genérico.
|
|
83
|
+
const fechado = require('./referencias.js').atual().modo === 'fechado';
|
|
84
|
+
const lista = global.length ? global : fechado ? [] : BUSCA_PADRAO;
|
|
85
|
+
const termos = new Set();
|
|
86
|
+
(roteiro.partes || []).forEach((p, i) => {
|
|
87
|
+
const b = (extras[i] && extras[i].busca) || p.busca;
|
|
88
|
+
if (b) termos.add(String(b));
|
|
89
|
+
});
|
|
90
|
+
for (const t of lista) termos.add(t);
|
|
91
|
+
return [...termos].slice(0, 8);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* A câmera de cada cena do cinema: empurra e desliza, alternando o lado. A
|
|
95
|
+
foto já se move por dentro (os movimentos do cinema); a câmera move o
|
|
96
|
+
CONJUNTO — é a soma dos dois que dá a profundidade. */
|
|
97
|
+
function camera(d, i, deitado, forte) {
|
|
98
|
+
const lado = i % 2 ? 1 : -1;
|
|
99
|
+
const z = forte ? 1.14 : 1.07;
|
|
100
|
+
const desliza = (deitado ? 26 : 18);
|
|
101
|
+
return {
|
|
102
|
+
zoom: [k(0, 1.0), k(Math.max(0.3, d - 0.25), z, 'camera'), k(d, z + 0.04, 'acelera')],
|
|
103
|
+
x: [k(0, -lado * desliza), k(d, lado * desliza, 'camera')],
|
|
104
|
+
rotacao: [k(0, -lado * 0.35), k(d, lado * 0.35, 'camera')],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** O kit por cima do plano do cinema. Mexe no plano recebido e o devolve. */
|
|
109
|
+
/* 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 } = {}) {
|
|
113
|
+
if (!plano || !Array.isArray(plano.cenas)) return plano;
|
|
114
|
+
const W = largura || (plano.formato && plano.formato.largura) || 1920;
|
|
115
|
+
const H = altura || (plano.formato && plano.formato.altura) || 1080;
|
|
116
|
+
const deitado = W >= H;
|
|
117
|
+
const base = Math.min(W, H);
|
|
118
|
+
const M = Math.round(base * 0.08);
|
|
119
|
+
const grad = marca.gradiente && marca.gradiente.length >= 2 ? marca.gradiente : ['#2E81ED', '#9567DB'];
|
|
120
|
+
const ACENTO = (marca.cores && marca.cores.destaque) || grad[1];
|
|
121
|
+
|
|
122
|
+
/* As cenas do cinema não são uma por parte: o depoimento com três clipes
|
|
123
|
+
vira três cenas. O mapa cena → parte sai das partes já traduzidas. */
|
|
124
|
+
const parteDaCena = [];
|
|
125
|
+
partes.forEach((p, i) => {
|
|
126
|
+
const n = p.tipo === 'depoimento' ? Math.max(1, (p.clipes || []).length) : 1;
|
|
127
|
+
for (let q = 0; q < n; q++) parteDaCena.push(i);
|
|
128
|
+
});
|
|
129
|
+
plano.cenas.forEach((cena, c) => {
|
|
130
|
+
const iParte = parteDaCena[c] != null ? parteDaCena[c] : c;
|
|
131
|
+
const ex = extras[iParte] || {};
|
|
132
|
+
const d = cena.duracao || 2;
|
|
133
|
+
const tipo = ex.tipoOriginal || '';
|
|
134
|
+
const els = cena.elementos || (cena.elementos = []);
|
|
135
|
+
const forte = tipo === 'numero' || tipo === 'contraste' || tipo === 'abertura';
|
|
136
|
+
if (!cena.camera) cena.camera = camera(d, c, deitado, forte);
|
|
137
|
+
|
|
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) {
|
|
140
|
+
els.push({ tipo: 'kit', peca: 'fonte', texto: String(ex.fonte).slice(0, 48), t: Math.min(0.9, d * 0.35),
|
|
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 });
|
|
142
|
+
}
|
|
143
|
+
// O NÚMERO estoura em partículas quando assenta.
|
|
144
|
+
if (particulas && (tipo === 'numero' || tipo === 'grafico')) {
|
|
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,
|
|
146
|
+
tamanho: Math.max(5, Math.round(base * 0.007)), cores: ['#ffffff', ACENTO, grad[0]] });
|
|
147
|
+
}
|
|
148
|
+
// O FIM: a marca entra com um brilho de partículas.
|
|
149
|
+
if (particulas && (tipo === 'cta' || tipo === 'logo' || tipo === 'fim')) {
|
|
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,
|
|
151
|
+
tamanho: Math.max(4, Math.round(base * 0.006)), cores: ['#ffffff', ACENTO], forma: 'circulo' });
|
|
152
|
+
}
|
|
153
|
+
/* O LÍQUIDO entre blocos: a cada três partes a passagem é a mancha que
|
|
154
|
+
cobre a tela, não o esmaecer. Mais que isso vira truque. */
|
|
155
|
+
if (c > 0 && iParte % 3 === 0 && cena.transicao && cena.transicao.tipo !== 'corte' && tipo !== 'cta') {
|
|
156
|
+
cena.transicao = { tipo: 'liquido', duracao: 0.5, cor: ACENTO };
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
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;
|
|
163
|
+
// O acabamento de filme um pouco mais presente que o do cinema.
|
|
164
|
+
plano.acabamento = { ...(plano.acabamento || {}), grao: Math.max(0.06, (plano.acabamento || {}).grao || 0), vinheta: Math.max(0.55, (plano.acabamento || {}).vinheta || 0) };
|
|
165
|
+
plano.estilo = 'hibrido';
|
|
166
|
+
return plano;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { traduzir, termosDeBusca, enfeitar, BUSCA_PADRAO };
|
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
|