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,1471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gerativo.js — o motion GERATIVO: sem template, a tipografia é o filme.
|
|
3
|
+
*
|
|
4
|
+
* "O motion que ele faz é tudo igual — só muda a informação num template
|
|
5
|
+
* pronto. Não pode ter template. Motion é animação contínua, que
|
|
6
|
+
* transforma as coisas, o texto: mexe a própria FONTE." — Joel (29–30/09)
|
|
7
|
+
*
|
|
8
|
+
* Os estilos de cena por parte (motion.js, vox.js, lancamento.js) ligam cada
|
|
9
|
+
* tipo de parte a um leiaute fixo: a lista é sempre a mesma lista. Aqui não.
|
|
10
|
+
* O roteiro e os tempos da voz viram uma PARTITURA: uma linha do tempo só,
|
|
11
|
+
* dividida em BATIDAS. Cada batida tem:
|
|
12
|
+
*
|
|
13
|
+
* as palavras que a voz diz nela, cada uma com o seu instante (o áudio
|
|
14
|
+
* comanda o quadro — regra 01 do guia VOX);
|
|
15
|
+
* um papel por palavra (tese, apoio, função) — a tese é a mais forte;
|
|
16
|
+
* uma COMPOSIÇÃO sorteada entre as que o CONTEÚDO permite (palavra
|
|
17
|
+
* gigante, pilha à esquerda, divisão, contador, cascata, parede de texto,
|
|
18
|
+
* citação marcada, marca que se constrói, palavras faladas, produto 3D) —
|
|
19
|
+
* nunca a mesma duas batidas seguidas;
|
|
20
|
+
* um COMPORTAMENTO por linha, do vocabulário que os eixos da fonte
|
|
21
|
+
* permitem (carimbo, máscara, onda, decodificar, peso 100→900 no eixo
|
|
22
|
+
* wght, largura no wdth, tracking, desfoque, split-flap, pontos, contorno);
|
|
23
|
+
* a forma de ACENTO — um traço só que atravessa o vídeo inteiro e vira
|
|
24
|
+
* sublinhado, anel, risco, barra, marcador, marca-texto, ponto final;
|
|
25
|
+
* a SAÍDA, que transforma em vez de cortar: as letras viram pontos e voam
|
|
26
|
+
* para o acento, a fonte afina e evapora, a câmera atravessa a palavra, o
|
|
27
|
+
* acento cresce e vira o fundo da próxima.
|
|
28
|
+
*
|
|
29
|
+
* O sorteio é DETERMINÍSTICO: a semente é o hash do roteiro. O mesmo roteiro
|
|
30
|
+
* dá a mesma partitura; dois roteiros diferentes, filmes diferentes. As
|
|
31
|
+
* regras do guia são restrições do sorteio: hierarquia, 30–40% de respiro,
|
|
32
|
+
* um acento em no máximo 3 pontos, até 6 elementos, uma ação dominante por
|
|
33
|
+
* vez, stagger de 3–6 quadros, burst de 16–18, traço de 10–16, leitura de
|
|
34
|
+
* pelo menos 1 s, mola SNAPPY no elemento e bezier longa na câmera.
|
|
35
|
+
*
|
|
36
|
+
* Quem desenha é kit/gerativo.jsx: UM componente que lê a partitura quadro
|
|
37
|
+
* a quadro. Puro: tempos, narração, trilha e sons entram; o plano sai.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const { misturarCor, ehCor } = require('./morfar.js');
|
|
43
|
+
const { volumesDaTrilha } = require('./ritmo.js');
|
|
44
|
+
const { falaDe } = require('./composicao.js');
|
|
45
|
+
|
|
46
|
+
const FPS = 30;
|
|
47
|
+
const QUADRO = 1 / FPS;
|
|
48
|
+
const r3 = (n) => Math.round(n * 1000) / 1000;
|
|
49
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
50
|
+
|
|
51
|
+
/* As regras do guia MOTION VOX STYLE, em números. Os testes leem daqui. */
|
|
52
|
+
const REGRAS = {
|
|
53
|
+
SNAPPY: { damping: 26, stiffness: 240, mass: 0.65 },
|
|
54
|
+
stagger: [3, 6], // quadros entre elementos
|
|
55
|
+
burst: [16, 18], // quadros da entrada de um elemento
|
|
56
|
+
traco: [10, 16], // quadros do traço de markup
|
|
57
|
+
leitura: 1.0, // segundos parado antes de sair
|
|
58
|
+
elementos: 6, // por quadro
|
|
59
|
+
acentos: 3, // pontos de cor por batida
|
|
60
|
+
respiro: [0.3, 0.4], // fração do quadro que é fundo puro
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ── o acaso com semente ─────────────────────────────────────────────────
|
|
64
|
+
function hash(txt) {
|
|
65
|
+
let h = 0x811c9dc5;
|
|
66
|
+
const s = String(txt);
|
|
67
|
+
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
|
|
68
|
+
return h >>> 0;
|
|
69
|
+
}
|
|
70
|
+
/** mulberry32: o mesmo número de partida dá sempre a mesma sequência. */
|
|
71
|
+
function sorteador(semente) {
|
|
72
|
+
let a = semente >>> 0;
|
|
73
|
+
const prox = () => {
|
|
74
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
75
|
+
let t = a;
|
|
76
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
77
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
78
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
79
|
+
};
|
|
80
|
+
const s = {
|
|
81
|
+
prox,
|
|
82
|
+
entre: (a0, b0) => a0 + (b0 - a0) * prox(),
|
|
83
|
+
um: (lista) => lista[Math.floor(prox() * lista.length) % lista.length],
|
|
84
|
+
chance: (p) => prox() < p,
|
|
85
|
+
/** Sorteio com peso: {nome: peso}. Peso 0 não sai. */
|
|
86
|
+
pesado: (pesos) => {
|
|
87
|
+
const itens = Object.entries(pesos).filter(([, p]) => p > 0);
|
|
88
|
+
if (!itens.length) return null;
|
|
89
|
+
const total = itens.reduce((n, [, p]) => n + p, 0);
|
|
90
|
+
let x = prox() * total;
|
|
91
|
+
for (const [nome, p] of itens) { if ((x -= p) <= 0) return nome; }
|
|
92
|
+
return itens[itens.length - 1][0];
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── as fontes variáveis (Google Fonts, eixos conferidos em 30/09) ─────────
|
|
99
|
+
/* largura: o quanto a fonte é mais larga que uma grotesca comum (para a
|
|
100
|
+
estimativa; o componente mede de verdade e encolhe o que não couber). */
|
|
101
|
+
const VARIAVEIS = {
|
|
102
|
+
'Roboto Flex': { eixos: { wght: [100, 1000], wdth: [25, 151], opsz: [8, 144], slnt: [-10, 0], GRAD: [-200, 150] }, largura: 0.98 },
|
|
103
|
+
'Fraunces': { eixos: { wght: [100, 900], opsz: [9, 144], SOFT: [0, 100], WONK: [0, 1] }, largura: 1.0, serifa: true },
|
|
104
|
+
'Inter Tight': { eixos: { wght: [100, 900] }, largura: 0.93 },
|
|
105
|
+
'Inter': { eixos: { wght: [100, 900], opsz: [14, 32] }, largura: 1.0 },
|
|
106
|
+
'Unbounded': { eixos: { wght: [200, 900] }, largura: 1.3 },
|
|
107
|
+
'Newsreader': { eixos: { wght: [200, 800], opsz: [6, 72] }, largura: 0.88, serifa: true },
|
|
108
|
+
'DM Sans': { eixos: { wght: [100, 1000], opsz: [9, 40] }, largura: 1.0 },
|
|
109
|
+
'Plus Jakarta Sans': { eixos: { wght: [200, 800] }, largura: 1.04 },
|
|
110
|
+
'Bricolage Grotesque': { eixos: { wght: [200, 800], wdth: [75, 100], opsz: [12, 96] }, largura: 0.98 },
|
|
111
|
+
'Anybody': { eixos: { wght: [100, 900], wdth: [50, 150] }, largura: 1.0 },
|
|
112
|
+
'Archivo': { eixos: { wght: [100, 900], wdth: [62, 125] }, largura: 1.0 },
|
|
113
|
+
'Mona Sans': { eixos: { wght: [200, 900], wdth: [75, 125] }, largura: 1.0 },
|
|
114
|
+
'IBM Plex Sans': { eixos: { wght: [100, 700], wdth: [85, 100] }, largura: 0.98 },
|
|
115
|
+
'Space Grotesk': { eixos: { wght: [300, 700] }, largura: 1.02 },
|
|
116
|
+
'Big Shoulders': { eixos: { wght: [100, 900], opsz: [10, 72] }, largura: 0.72 },
|
|
117
|
+
'Geist': { eixos: { wght: [100, 900] }, largura: 0.98 },
|
|
118
|
+
'Manrope': { eixos: { wght: [200, 800] }, largura: 1.02 },
|
|
119
|
+
'Sora': { eixos: { wght: [100, 800] }, largura: 1.06 },
|
|
120
|
+
'Outfit': { eixos: { wght: [100, 900] }, largura: 0.98 },
|
|
121
|
+
'Montserrat': { eixos: { wght: [100, 900] }, largura: 1.12 },
|
|
122
|
+
'JetBrains Mono': { eixos: { wght: [100, 800] }, largura: 1.1, mono: true },
|
|
123
|
+
// A biblioteca anti-genérica (30/09): faces com caráter, conferidas no Google Fonts.
|
|
124
|
+
'Bodoni Moda': { eixos: { wght: [400, 900], opsz: [6, 96] }, largura: 0.96, serifa: true },
|
|
125
|
+
'Playfair Display': { eixos: { wght: [400, 900] }, largura: 0.98, serifa: true },
|
|
126
|
+
'Familjen Grotesk': { eixos: { wght: [400, 700] }, largura: 0.96 },
|
|
127
|
+
'Schibsted Grotesk': { eixos: { wght: [400, 900] }, largura: 0.98 },
|
|
128
|
+
'Hanken Grotesk': { eixos: { wght: [100, 900] }, largura: 0.97 },
|
|
129
|
+
'Syne': { eixos: { wght: [400, 800] }, largura: 1.1 },
|
|
130
|
+
'Martian Mono': { eixos: { wdth: [75, 112.5], wght: [100, 800] }, largura: 1.15, mono: true },
|
|
131
|
+
// As estáticas: sem eixo para animar, servem de texto, citação e rótulo.
|
|
132
|
+
'Instrument Serif': { eixos: {}, css: 'ital@0;1', largura: 0.86, serifa: true },
|
|
133
|
+
'Space Mono': { eixos: {}, css: 'wght@400;700', largura: 1.1, mono: true },
|
|
134
|
+
'Fragment Mono': { eixos: {}, css: '', largura: 1.1, mono: true },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/* AS FONTES DE IA: o que todo vídeo feito por IA usa. Nunca são o padrão do
|
|
138
|
+
gerativo; se a marca manda uma delas como título, ela vira o texto e o
|
|
139
|
+
título ganha uma face com caráter. */
|
|
140
|
+
const BANIDAS = ['Inter', 'Inter Tight', 'Roboto', 'Roboto Flex', 'Poppins', 'Montserrat', 'Open Sans', 'DM Sans', 'Plus Jakarta Sans',
|
|
141
|
+
'Unbounded', 'Lato', 'Arial', 'Helvetica', 'Nunito', 'Raleway', 'Space Grotesk', 'Manrope', 'Outfit', 'Geist', 'Sora'];
|
|
142
|
+
/* AS CORES DE IA: o violeta de SaaS (#6366f1 e vizinhos) e o neon sobre preto. */
|
|
143
|
+
function corGenerica(hex) {
|
|
144
|
+
const n = parseInt(String(hex).slice(1), 16), r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, b = (n & 255) / 255;
|
|
145
|
+
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
|
|
146
|
+
if (!d) return false;
|
|
147
|
+
let h = mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
148
|
+
h = (h * 60 + 360) % 360;
|
|
149
|
+
const sat = d / (1 - Math.abs(mx + mn - 1) || 1);
|
|
150
|
+
if (h >= 238 && h <= 300 && sat > 0.35) return true; // violeta, roxo, índigo de SaaS
|
|
151
|
+
if (sat > 0.85 && mx > 0.9 && ((h >= 170 && h <= 200) || (h >= 295 && h <= 330))) return true; // ciano e magenta neon
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* OS HUMORES: do assunto do roteiro saem a dupla de fontes, a paleta, o
|
|
156
|
+
gênero da trilha e a família de sons. Cada humor tem várias opções; a
|
|
157
|
+
semente escolhe — dois vídeos do mesmo assunto não saem iguais. */
|
|
158
|
+
const HUMORES = {
|
|
159
|
+
editorial: {
|
|
160
|
+
palavras: 'historia por que porque explica cancelamento atenas guerra livro politica cultura democracia voto lei grego roma imperio arte jornal',
|
|
161
|
+
displays: ['Fraunces', 'Bodoni Moda', 'Playfair Display'], textos: ['Schibsted Grotesk', 'Familjen Grotesk'], monos: ['Martian Mono', 'Space Mono'],
|
|
162
|
+
serifas: ['Instrument Serif', 'Fraunces'], cineticas: ['Anybody', 'Archivo'],
|
|
163
|
+
paletas: [['#EFE8DA', '#1A1714', '#7A1F1F'], ['#F3EEE3', '#14161F', '#1F3FBF'], ['#E9E3D3', '#211E1A', '#D9481C']],
|
|
164
|
+
generos: ['jazz', 'acoustic', 'classical'], som: 'papel',
|
|
165
|
+
},
|
|
166
|
+
tecnologia: {
|
|
167
|
+
palavras: 'ia ai app software codigo modelo modelos dados claude sonnet gpt algoritmo digital tecnologia plataforma api chip computador agente agentes agentic rapido barato versao lancamento',
|
|
168
|
+
displays: ['Bricolage Grotesque', 'Mona Sans', 'Schibsted Grotesk', 'Syne'], textos: ['Familjen Grotesk', 'Hanken Grotesk'], monos: ['Martian Mono', 'Fragment Mono'],
|
|
169
|
+
serifas: ['Instrument Serif', 'Fraunces'], cineticas: ['Mona Sans', 'Anybody', 'Archivo'],
|
|
170
|
+
paletas: [['#1B1D1A', '#E9E6DD', '#C6F432'], ['#10161F', '#F1ECE0', '#FF5F1F'], ['#2F3A26', '#EDE6D3', '#FF6F59'], ['#F0EDE6', '#141414', '#FF4A1C']],
|
|
171
|
+
generos: ['electronica', 'lofi-chill-beats', 'funk'], som: 'analogico',
|
|
172
|
+
},
|
|
173
|
+
energia: {
|
|
174
|
+
palavras: 'som audio fone fones headset musica bateria esporte treino velocidade energia jogo game festa barulho silencio ouca ouvir escute vento corrida',
|
|
175
|
+
displays: ['Anybody', 'Big Shoulders', 'Syne', 'Archivo'], textos: ['Hanken Grotesk', 'Familjen Grotesk'], monos: ['Space Mono', 'Martian Mono'],
|
|
176
|
+
serifas: ['Bodoni Moda', 'Instrument Serif'], cineticas: ['Anybody', 'Archivo'],
|
|
177
|
+
paletas: [['#121110', '#F2EEE6', '#FF4F00'], ['#1E1A16', '#F4EBDD', '#E8C547'], ['#0F1E3D', '#F3EDE1', '#FF7A4D']],
|
|
178
|
+
generos: ['funk', 'dance-club', 'electronica'], som: 'digital',
|
|
179
|
+
},
|
|
180
|
+
natureza: {
|
|
181
|
+
palavras: 'ceu azul sol luz natureza planta plantas agua mar terra fisica ciencia universo estrela estrelas clima ar cor cores olhos violeta vermelho por do',
|
|
182
|
+
displays: ['Fraunces', 'Bricolage Grotesque', 'Bodoni Moda'], textos: ['Hanken Grotesk', 'Schibsted Grotesk'], monos: ['Martian Mono', 'Space Mono'],
|
|
183
|
+
serifas: ['Instrument Serif', 'Fraunces'], cineticas: ['Anybody', 'Mona Sans'],
|
|
184
|
+
paletas: [['#F2EDE2', '#13233F', '#2546D9'], ['#0F2A2A', '#EDE7DA', '#F2A541'], ['#E7E2D2', '#22301F', '#C8553D']],
|
|
185
|
+
generos: ['ambient', 'acoustic', 'cinematic'], som: 'organico',
|
|
186
|
+
},
|
|
187
|
+
luxo: {
|
|
188
|
+
palavras: 'luxo moda perfume joia premium elegante vinho couro ouro atelier',
|
|
189
|
+
displays: ['Bodoni Moda', 'Playfair Display', 'Fraunces'], textos: ['Hanken Grotesk', 'Familjen Grotesk'], monos: ['Fragment Mono', 'Martian Mono'],
|
|
190
|
+
serifas: ['Bodoni Moda', 'Instrument Serif'], cineticas: ['Anybody', 'Mona Sans'],
|
|
191
|
+
paletas: [['#2A0E12', '#F0E6D6', '#C9A24A'], ['#F1EBE1', '#1B1A17', '#8C1C13']],
|
|
192
|
+
generos: ['jazz', 'soul-rnb', 'classical'], som: 'papel',
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
/* A cor que o próprio assunto diz ("por que o céu é AZUL") vira o acento. */
|
|
196
|
+
const CORES_DITAS = { azul: '#2546D9', vermelho: '#C8291E', verde: '#2F7D4F', laranja: '#FF5F1F', amarelo: '#E8B92F', dourado: '#C9A24A', rosa: '#E86A8A', coral: '#FF6F59' };
|
|
197
|
+
|
|
198
|
+
/* A palavra que pede ícone → o ícone 3D (kit/tres.jsx ICONES). */
|
|
199
|
+
const ICONE_DE = {
|
|
200
|
+
bateria: 'bateria', carga: 'bateria', tomada: 'bateria', horas: 'relogio', hora: 'relogio', minutos: 'relogio', tempo: 'relogio', espera: 'relogio', dia: 'relogio',
|
|
201
|
+
rapido: 'raio', velocidade: 'raio', energia: 'raio', agil: 'raio', instantaneo: 'raio', conversa: 'balao', conversar: 'balao', chat: 'balao', pergunta: 'balao',
|
|
202
|
+
seguro: 'escudo', seguranca: 'escudo', protege: 'escudo', protecao: 'escudo', confianca: 'escudo', coracao: 'coracao', amor: 'coracao', saude: 'coracao', pessoas: 'coracao',
|
|
203
|
+
melhor: 'estrela', estrela: 'estrela', estrelas: 'estrela', qualidade: 'estrela', sol: 'sol', luz: 'sol', video: 'play', assista: 'play', experimente: 'play',
|
|
204
|
+
engrenagem: 'engrenagem', ajuste: 'engrenagem', agentes: 'engrenagem', agente: 'engrenagem', trabalho: 'engrenagem', senha: 'cadeado', privacidade: 'cadeado', bloqueio: 'cadeado',
|
|
205
|
+
grafico: 'barras', cresce: 'barras', crescimento: 'barras', dados: 'barras', barato: 'barras', conta: 'barras', fone: 'fone', headset: 'fone', almofadas: 'fone',
|
|
206
|
+
som: 'onda', audio: 'onda', microfone: 'onda', vento: 'onda', escute: 'onda', ouca: 'onda', barulho: 'onda', celular: 'celular', app: 'celular', aparelho: 'celular', aparelhos: 'celular',
|
|
207
|
+
notebook: 'celular', pronto: 'check', feito: 'check', certo: 'check', proximo: 'seta', passo: 'seta', adiante: 'seta', ricocheteia: 'seta',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** O humor do roteiro: as palavras dele contra as de cada humor (empate: a semente). */
|
|
211
|
+
function humorDe(roteiro, s) {
|
|
212
|
+
const txt = new Set(tokens((roteiro.partes || []).map((p) => [p.titulo, p.texto, p.fala, p.sub, ...(p.itens || [])].filter(Boolean).join(' ')).join(' ')
|
|
213
|
+
+ ' ' + ((roteiro.marca && roteiro.marca.nome) || '') + ' ' + (roteiro.pedido || '')));
|
|
214
|
+
const nota = {};
|
|
215
|
+
for (const [h, d] of Object.entries(HUMORES)) nota[h] = d.palavras.split(' ').filter((w) => txt.has(w)).length + s.prox() * 0.5;
|
|
216
|
+
return Object.entries(nota).sort((a, b) => b[1] - a[1])[0][0];
|
|
217
|
+
}
|
|
218
|
+
/* As que têm largura (wdth): é nelas que a palavra estica. */
|
|
219
|
+
const CINETICAS = ['Roboto Flex', 'Anybody', 'Archivo', 'Mona Sans'];
|
|
220
|
+
|
|
221
|
+
/** A linha do css2 do Google para os eixos: minúsculos em ordem, depois os maiúsculos. */
|
|
222
|
+
function especEixos(familia) {
|
|
223
|
+
const v = VARIAVEIS[familia];
|
|
224
|
+
if (!v) return null;
|
|
225
|
+
if (v.css != null) return v.css;
|
|
226
|
+
if (!Object.keys(v.eixos).length) return '';
|
|
227
|
+
const tags = Object.keys(v.eixos).sort((a, b) => {
|
|
228
|
+
const A = a === a.toUpperCase(), B = b === b.toUpperCase();
|
|
229
|
+
if (A !== B) return A ? 1 : -1;
|
|
230
|
+
return a < b ? -1 : 1;
|
|
231
|
+
});
|
|
232
|
+
return tags.join(',') + '@' + tags.map((t) => v.eixos[t].join('..')).join(',');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const eixoDe = (familia, tag) => (VARIAVEIS[familia] && VARIAVEIS[familia].eixos[tag]) || null;
|
|
236
|
+
const prende = (v, [a, b]) => Math.max(a, Math.min(b, v));
|
|
237
|
+
|
|
238
|
+
// ── o texto ─────────────────────────────────────────────────────────────
|
|
239
|
+
const norm = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
240
|
+
const tokens = (t) => norm(t).split(/[^a-z0-9%]+/).filter(Boolean);
|
|
241
|
+
const FUNCAO = new Set(('o a os as um uma uns umas de do da dos das em no na nos nas por pra pro para com sem e ou que se '
|
|
242
|
+
+ 'ao aos à às é ser seu sua seus suas te lhe me meu minha isso isto esse essa este esta ele ela eles elas voce nao mais '
|
|
243
|
+
+ 'the a an of to in on for and or with is are it this that your you our by at as from be').split(' '));
|
|
244
|
+
const funcao = (w) => FUNCAO.has(tokens(w)[0] || '');
|
|
245
|
+
/* As que não podem terminar linha: artigo, preposição, conjunção. */
|
|
246
|
+
const FRACAS = new Set('o a os as um uma de do da dos das em no na nos nas por pra pro para com sem e ou que se ao aos à às the an of to in on for and or with by at from'.split(' '));
|
|
247
|
+
const fraca = (w) => FRACAS.has(tokens(w)[0] || '');
|
|
248
|
+
|
|
249
|
+
/* A largura do texto em "em", pela forma de cada letra. É uma ESTIMATIVA:
|
|
250
|
+
serve para quebrar linha e escolher tamanho; quem garante que cabe é o
|
|
251
|
+
componente, que mede no navegador e encolhe. */
|
|
252
|
+
function emDe(ch) {
|
|
253
|
+
if (ch === ' ') return 0.27;
|
|
254
|
+
if (/[iIl1.,:;'!|j·]/.test(ch)) return 0.3;
|
|
255
|
+
if (/[mwMW]/.test(ch)) return 0.86;
|
|
256
|
+
if (/[A-ZÀ-Ý]/.test(ch)) return 0.68;
|
|
257
|
+
if (/[0-9]/.test(ch)) return 0.58;
|
|
258
|
+
if (/[%&@#]/.test(ch)) return 0.8;
|
|
259
|
+
return 0.54;
|
|
260
|
+
}
|
|
261
|
+
function larguraEm(texto, { fonte = 'Roboto Flex', wght = 400, wdth = 100, tracking = 0, maiusc = false } = {}) {
|
|
262
|
+
const v = VARIAVEIS[fonte] || { largura: 1 };
|
|
263
|
+
const t = maiusc ? String(texto).toUpperCase() : String(texto);
|
|
264
|
+
let em = 0;
|
|
265
|
+
for (const ch of t) em += (v.mono ? 0.6 : emDe(ch)) + tracking;
|
|
266
|
+
const peso = 1 + ((wght - 400) / 500) * 0.1;
|
|
267
|
+
const larg = 0.3 + 0.7 * (wdth / 100);
|
|
268
|
+
return em * v.largura * peso * larg;
|
|
269
|
+
}
|
|
270
|
+
/** O maior tamanho (px) em que o texto cabe na largura, com teto. */
|
|
271
|
+
function tamanhoQueCabe(texto, largura, max, op) {
|
|
272
|
+
const em = larguraEm(texto, op) || 1;
|
|
273
|
+
return Math.max(12, Math.min(max, Math.floor((largura * 0.94) / em)));
|
|
274
|
+
}
|
|
275
|
+
/* A QUEBRA DE LINHA de diretor de arte, não de editor de texto: todas as
|
|
276
|
+
partições da frase em até `n` linhas, e ganha a que tem linhas parelhas,
|
|
277
|
+
quebra depois da pontuação e NUNCA termina linha em "da", "o", "que" —
|
|
278
|
+
"Barulho da / rua" é o erro que denuncia máquina. */
|
|
279
|
+
function particoes(ws, n) {
|
|
280
|
+
const out = [];
|
|
281
|
+
const rec = (i, acc) => {
|
|
282
|
+
if (i === ws.length) { out.push(acc); return; }
|
|
283
|
+
if (acc.length >= n) return;
|
|
284
|
+
for (let j = i + 1; j <= ws.length; j++) rec(j, [...acc, ws.slice(i, j).join(' ')]);
|
|
285
|
+
};
|
|
286
|
+
if (ws.length <= 14) rec(0, []);
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function custoQuebra(linhas, larguras) {
|
|
290
|
+
let c = 0;
|
|
291
|
+
const max = Math.max(...larguras);
|
|
292
|
+
larguras.forEach((w, i) => { if (i < larguras.length - 1) c += Math.pow(1 - w / max, 2) * 2; });
|
|
293
|
+
linhas.forEach((l, i) => {
|
|
294
|
+
if (i === linhas.length - 1) { if (larguras[i] < max * 0.25 && linhas.length > 1) c += 1.2; return; }
|
|
295
|
+
const ult = l.split(' ').pop();
|
|
296
|
+
if (fraca(ult) || /^\d+$/.test(ult)) c += 6;
|
|
297
|
+
if (/[,.;:!?—–-]$/.test(ult)) c -= 0.8;
|
|
298
|
+
});
|
|
299
|
+
return c + (linhas.length - 1) * 0.35;
|
|
300
|
+
}
|
|
301
|
+
/** Quebra em linhas que caibam (a melhor partição; gulosa se a frase é longa). */
|
|
302
|
+
function quebrar(texto, largura, tamanho, op, n = 99) {
|
|
303
|
+
const ws = String(texto).split(/\s+/).filter(Boolean);
|
|
304
|
+
const cabe = (l) => larguraEm(l, op) * tamanho <= largura * 0.94;
|
|
305
|
+
let melhor = null, custo = Infinity;
|
|
306
|
+
for (const ls of particoes(ws, n)) {
|
|
307
|
+
if (!ls.every(cabe)) continue;
|
|
308
|
+
const c = custoQuebra(ls, ls.map((l) => larguraEm(l, op)));
|
|
309
|
+
if (c < custo) { custo = c; melhor = ls; }
|
|
310
|
+
}
|
|
311
|
+
if (melhor) return melhor;
|
|
312
|
+
const linhas = [];
|
|
313
|
+
let atual = '';
|
|
314
|
+
for (const w of ws) {
|
|
315
|
+
const tenta = atual ? atual + ' ' + w : w;
|
|
316
|
+
if (!atual || cabe(tenta)) atual = tenta;
|
|
317
|
+
else { linhas.push(atual); atual = w; }
|
|
318
|
+
}
|
|
319
|
+
if (atual) linhas.push(atual);
|
|
320
|
+
return linhas;
|
|
321
|
+
}
|
|
322
|
+
/** O maior tamanho em que o texto cabe em até `n` linhas — sem quebra feia, se der. */
|
|
323
|
+
function caberEmLinhas(texto, largura, n, max, op, min = 14) {
|
|
324
|
+
let reserva = null;
|
|
325
|
+
for (let t = Math.round(max); t >= min; t -= Math.max(1, Math.round(t * 0.04))) {
|
|
326
|
+
const ls = quebrar(texto, largura, t, op, n);
|
|
327
|
+
if (ls.length > n || !ls.every((l) => larguraEm(l, op) * t <= largura * 0.96)) continue;
|
|
328
|
+
const feia = ls.slice(0, -1).some((l) => fraca(l.split(' ').pop()));
|
|
329
|
+
if (!feia) return { tamanho: t, linhas: ls };
|
|
330
|
+
if (!reserva) reserva = { tamanho: t, linhas: ls };
|
|
331
|
+
if (t < max * 0.5) break;
|
|
332
|
+
}
|
|
333
|
+
return reserva || { tamanho: min, linhas: quebrar(texto, largura, min, op, n) };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const numeroDe = (v) => {
|
|
337
|
+
const m = String(v == null ? '' : v).match(/^(\D*?)(\d[\d.,]*)(.*)$/);
|
|
338
|
+
if (!m) return null;
|
|
339
|
+
const bruto = m[2];
|
|
340
|
+
const milhar = /\.\d{3}(\D|$)/.test(bruto) ? '.' : (/,\d{3}(\D|$)/.test(bruto) && !/\./.test(bruto) ? ',' : '');
|
|
341
|
+
const limpo = milhar === '.' ? bruto.replace(/\./g, '').replace(',', '.') : milhar === ',' ? bruto.replace(/,/g, '') : bruto.replace(',', '.');
|
|
342
|
+
const n = Number(limpo);
|
|
343
|
+
if (!Number.isFinite(n)) return null;
|
|
344
|
+
const casas = (/[.,](\d+)$/.exec(milhar ? '' : bruto) || [, ''])[1].length;
|
|
345
|
+
return { antes: m[1], n, depois: m[3], casas, milhar, virgula: /,/.test(bruto) && !milhar };
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// ── o conteúdo de cada parte (o que aparece, não o leiaute) ────────────
|
|
349
|
+
const itensDe = (p) => [...(p.itens || []), ...(p.barras || []).map((b) => `${b.rotulo || ''} ${b.valor != null ? b.valor : ''}`.trim()),
|
|
350
|
+
...(p.cores || []).map((c) => (typeof c === 'string' ? c : `${c.nome || ''} ${c.hex || ''}`.trim()))]
|
|
351
|
+
.map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
|
|
352
|
+
|
|
353
|
+
function conteudoDe(p, marca = {}) {
|
|
354
|
+
const titulo = p.titulo || p.texto || '';
|
|
355
|
+
const itens = itensDe(p);
|
|
356
|
+
if (p.tipo === 'numero' && numeroDe(p.valor)) return { tipo: 'numero', valor: String(p.valor), rotulo: p.rotulo || titulo, sub: p.sub || '' };
|
|
357
|
+
if (itens.length >= 2) return { tipo: 'lista', titulo: p.tipo === 'cores' ? titulo : titulo, itens: itens.slice(0, 5), numerada: p.tipo === 'passos' };
|
|
358
|
+
if (p.tipo === 'contraste' && (p.nao || p.sim)) return { tipo: 'contraste', nao: p.nao || '', sim: p.sim || titulo };
|
|
359
|
+
if (p.tipo === 'citacao') return { tipo: 'citacao', texto: titulo, autor: p.autor || '' };
|
|
360
|
+
const nome = marca.nome || '';
|
|
361
|
+
if (p.tipo === 'logo' || (p.tipo === 'abertura' && titulo && norm(titulo).replace(/\s/g, '') === norm(nome).replace(/\s/g, '')))
|
|
362
|
+
return { tipo: 'marca', nome: titulo || nome, sub: p.sub || '' };
|
|
363
|
+
if (p.tipo === 'cta') return { tipo: 'cta', titulo, link: p.link || '' };
|
|
364
|
+
if (p.tipo === 'produto') return { tipo: 'produto', titulo, sub: p.texto && p.texto !== titulo ? p.texto : (p.sub || ''), selo: p.selo || '' };
|
|
365
|
+
return { tipo: 'titulo', titulo, sub: p.sub || '', selo: p.selo || '' };
|
|
366
|
+
}
|
|
367
|
+
const textoDoConteudo = (c) => [c.titulo, c.sub, c.valor, c.rotulo, c.nao, c.sim, c.texto, c.autor, c.nome, c.link, c.selo, ...(c.itens || [])].filter(Boolean).join(' ');
|
|
368
|
+
|
|
369
|
+
// ── as composições e o que cada conteúdo aceita ─────────────────────────
|
|
370
|
+
const COMPOSICOES = ['gigante', 'pilha', 'divisao', 'contador', 'cascata', 'parede', 'citacao', 'marca', 'palavras', 'produto'];
|
|
371
|
+
const COMPORTAMENTOS = ['carimbo', 'mascara', 'onda', 'decodifica', 'peso', 'largura', 'tracking', 'desfoque', 'flap', 'pontos', 'contorno', 'monta'];
|
|
372
|
+
const SAIDAS = ['pontos', 'evapora', 'atravessa', 'desliza', 'comprime', 'explode', 'chicote', 'inunda', 'corte'];
|
|
373
|
+
const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta'];
|
|
374
|
+
|
|
375
|
+
function pesosDe(c, { curto, palavras, primeira, ultima, tem3d, pergunta, exclama, numeroNoTitulo }) {
|
|
376
|
+
switch (c.tipo) {
|
|
377
|
+
case 'numero': return { contador: 5, gigante: 2, pilha: 1.2 };
|
|
378
|
+
case 'lista': return { cascata: 4, pilha: c.itens.length <= 3 && c.itens.every((i) => i.length <= 26) ? 1.6 : 0, rajada: c.itens.length <= 4 ? 2.4 : 0 };
|
|
379
|
+
case 'contraste': return { divisao: 5, duelo: 2.6 };
|
|
380
|
+
case 'citacao': return { citacao: 5, pilha: 1 };
|
|
381
|
+
case 'marca': return { marca: primeira || ultima ? 4.5 : 3, gigante: 2.4, parede: curto ? 2 : 0 };
|
|
382
|
+
case 'cta': return { pilha: 3, gigante: curto ? 2.6 : 0.6, parede: c.titulo.length <= 16 ? 1.6 : 0, marca: 1, produto: tem3d ? 3.5 : 0 };
|
|
383
|
+
case 'produto': return tem3d ? { produto: 9, pilha: 0.5 } : { pilha: 3, gigante: curto ? 2.5 : 0.5, parede: c.titulo.length <= 12 ? 1.2 : 0 };
|
|
384
|
+
case 'falado': return { palavras: 4, gigante: palavras <= 3 ? 2.6 : 0, pilha: palavras <= 9 ? 1.2 : 0 };
|
|
385
|
+
default: return {
|
|
386
|
+
gigante: (curto ? 4.2 : 0.6) + (primeira ? 1 : 0) + (exclama ? 1 : 0) + (numeroNoTitulo ? 1 : 0),
|
|
387
|
+
pilha: 3 + (pergunta ? 1 : 0),
|
|
388
|
+
parede: c.titulo.length <= 14 ? 1.4 + (primeira ? 1 : 0) : 0,
|
|
389
|
+
palavras: 0,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── o tempo: casar o que aparece com o que a voz diz ────────────────────
|
|
395
|
+
/** As palavras de cada parte, com tempo. Sem voz, 0,34 s por palavra. */
|
|
396
|
+
function temposDasPartes(partes, tempos) {
|
|
397
|
+
const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
|
|
398
|
+
if (temVoz) {
|
|
399
|
+
return tempos.map((t, i) => {
|
|
400
|
+
let ws = (t.palavras || []).filter((w) => w && w.texto != null && w.de != null).map((w) => ({ texto: String(w.texto), de: Number(w.de), ate: Number(w.ate != null ? w.ate : w.de + 0.25) }));
|
|
401
|
+
if (!ws.length) {
|
|
402
|
+
const falas = String(falaDe(partes[i])).split(/\s+/).filter(Boolean);
|
|
403
|
+
const passo = (t.ate - t.de) / Math.max(1, falas.length);
|
|
404
|
+
ws = falas.map((w, j) => ({ texto: w, de: t.de + j * passo, ate: t.de + (j + 0.8) * passo }));
|
|
405
|
+
}
|
|
406
|
+
return { de: t.de, ate: t.ate, palavras: ws };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
let t = 0.35;
|
|
410
|
+
return partes.map((p) => {
|
|
411
|
+
const falas = String(falaDe(p) || '').split(/\s+/).filter(Boolean);
|
|
412
|
+
const ws = falas.map((w) => { const q = { texto: w, de: r3(t), ate: r3(t + 0.28) }; t += 0.34; return q; });
|
|
413
|
+
const de = ws.length ? ws[0].de : t;
|
|
414
|
+
const ate = ws.length ? ws[ws.length - 1].ate : t + 1.4;
|
|
415
|
+
t = ate + 0.45;
|
|
416
|
+
return { de, ate, palavras: ws };
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Para cada texto (em ordem), o instante da voz em que ele é dito — ou null. */
|
|
421
|
+
function casar(textos, palavras, { de = 0, cursor0 = 0 } = {}) {
|
|
422
|
+
const ws = palavras.map((w) => ({ ...w, k: tokens(w.texto)[0] || '' }));
|
|
423
|
+
let cursor = cursor0;
|
|
424
|
+
return textos.map((txt) => {
|
|
425
|
+
const tk = tokens(txt);
|
|
426
|
+
const alvo = tk.find((w) => w.length >= 3 && !FUNCAO.has(w)) || tk[0];
|
|
427
|
+
if (!alvo) return null;
|
|
428
|
+
for (let j = cursor; j < ws.length; j++) {
|
|
429
|
+
if (ws[j].de < de - 0.05) continue;
|
|
430
|
+
if (ws[j].k === alvo || (alvo.length >= 5 && ws[j].k.startsWith(alvo.slice(0, 5)))) { cursor = j + 1; return ws[j].de; }
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* As frases da fala: as palavras com tempo repartidas pelo ponto final. */
|
|
437
|
+
function frasesDe(fala, palavras) {
|
|
438
|
+
const frases = String(fala || '').split(/(?<=[.!?…])\s+/).map((f) => f.trim()).filter(Boolean);
|
|
439
|
+
const out = [];
|
|
440
|
+
let j = 0;
|
|
441
|
+
for (const f of frases) {
|
|
442
|
+
const n = f.split(/\s+/).filter(Boolean).length;
|
|
443
|
+
const ws = palavras.slice(j, j + n);
|
|
444
|
+
j += n;
|
|
445
|
+
if (ws.length) out.push({ texto: f, palavras: ws.map((w, k) => ({ ...w, texto: f.split(/\s+/).filter(Boolean)[k] || w.texto })) });
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── as cores ────────────────────────────────────────────────────────────
|
|
451
|
+
const luz = (h) => { const n = parseInt(String(h).slice(1), 16); return ((n >> 16 & 255) * 0.299 + (n >> 8 & 255) * 0.587 + (n & 255) * 0.114) / 255; };
|
|
452
|
+
const contraste = (a, b) => { const la = luz(a) + 0.05, lb = luz(b) + 0.05; return la > lb ? la / lb : lb / la; };
|
|
453
|
+
const PALETAS = HUMORES.tecnologia.paletas;
|
|
454
|
+
|
|
455
|
+
function paletaDe(marca, s, humor = null, roteiro = {}) {
|
|
456
|
+
const c = marca.cores || {};
|
|
457
|
+
const op = humor ? HUMORES[humor].paletas : PALETAS;
|
|
458
|
+
const [f0, t0, a0] = op[Math.floor(s.prox() * op.length) % op.length];
|
|
459
|
+
let base = { fundo: f0, tinta: t0, acento: a0 };
|
|
460
|
+
// A cor que o assunto diz manda no acento (se não sumir no fundo).
|
|
461
|
+
const ditas = tokens((roteiro.partes || []).map((p) => [p.titulo, p.destaque].join(' ')).join(' ')).filter((w) => CORES_DITAS[w]);
|
|
462
|
+
if (ditas.length && contraste(CORES_DITAS[ditas[0]], f0) >= 2.2) base = { ...base, acento: CORES_DITAS[ditas[0]] };
|
|
463
|
+
const fundo = ehCor(c.fundo) ? c.fundo : base.fundo;
|
|
464
|
+
let tinta = ehCor(c.tinta) ? c.tinta : (ehCor(c.fundo) ? (luz(fundo) < 0.5 ? '#F4F1EA' : '#121212') : base.tinta);
|
|
465
|
+
if (contraste(fundo, tinta) < 4) tinta = luz(fundo) < 0.5 ? '#F4F1EA' : '#121212';
|
|
466
|
+
const acento = ehCor(c.destaque) ? c.destaque : (marca.gradiente && ehCor(marca.gradiente[1]) && !corGenerica(marca.gradiente[1]) ? marca.gradiente[1] : base.acento);
|
|
467
|
+
return { fundo, tinta, acento };
|
|
468
|
+
}
|
|
469
|
+
/* OS CAMPOS DE COR (30/09, "olha a diferença"): o reel de referência troca o
|
|
470
|
+
fundo inteiro a cada batida — preto, laranja, creme, azul elétrico, lima —
|
|
471
|
+
cor chapada, saturada, contraste alto. Cada humor tem o seu sistema de 5
|
|
472
|
+
(0 = a tinta escura, 1 = a cor-assinatura, 2 = o papel, 3 e 4 = as ousadas);
|
|
473
|
+
a marca entra no lugar das dela. Nada de bege lavado o vídeo inteiro. */
|
|
474
|
+
const CAMPOS = {
|
|
475
|
+
tecnologia: [['#0D0D0D', '#FF5B2E', '#EFEAE0', '#2B3BFF', '#DFFF3A'], ['#111111', '#C6F432', '#F1ECE0', '#FF4A1C', '#1F3CFF']],
|
|
476
|
+
editorial: [['#14110F', '#B3261E', '#F1EADB', '#1F4E3D', '#E9B44C'], ['#121417', '#1F3FBF', '#F3EEE3', '#D9481C', '#E8C547']],
|
|
477
|
+
energia: [['#0B0B0B', '#FF4F00', '#F2EEE6', '#1D3BD1', '#C9F23A'], ['#101010', '#E8C547', '#F4EBDD', '#E5322D', '#2A57FF']],
|
|
478
|
+
natureza: [['#0F2A2A', '#F2A541', '#F2EDE2', '#2546D9', '#E4573D'], ['#13233F', '#2546D9', '#F2EDE2', '#F2A541', '#C8553D']],
|
|
479
|
+
luxo: [['#120D0B', '#C9A24A', '#F0E6D6', '#5B1A1F', '#2F4F4F']],
|
|
480
|
+
};
|
|
481
|
+
function camposDe(marca, humor, s, dita = null) {
|
|
482
|
+
const c = marca.cores || {};
|
|
483
|
+
let k = [...s.um(CAMPOS[humor] || CAMPOS.tecnologia)];
|
|
484
|
+
// A cor que o assunto diz vira a assinatura (se não for a de outro papel).
|
|
485
|
+
if (dita && !ehCor(c.destaque)) { const j = k.indexOf(dita); if (j > 0) k[j] = k[1]; k[1] = dita; }
|
|
486
|
+
// A marca: o fundo dela, o destaque dela e a tinta dela tomam os lugares de mesmo papel.
|
|
487
|
+
const escuro = (x) => luz(x) < 0.35, claro = (x) => luz(x) > 0.7;
|
|
488
|
+
for (const cor of [c.fundo, c.destaque, c.tinta].filter(ehCor)) {
|
|
489
|
+
const lugar = escuro(cor) ? 0 : claro(cor) ? 2 : 1;
|
|
490
|
+
if (!k.includes(cor)) k[lugar] = cor;
|
|
491
|
+
}
|
|
492
|
+
if (ehCor(c.destaque) && k[1] !== c.destaque) k[1] = c.destaque;
|
|
493
|
+
return k;
|
|
494
|
+
}
|
|
495
|
+
/** As cores de uma batida pelo campo dela: texto no contraste máximo, acento noutra cor do sistema. */
|
|
496
|
+
function coresDoCampo(campos, i, s) {
|
|
497
|
+
const fundo = campos[i];
|
|
498
|
+
const tintas = [campos[0], campos[2]].sort((a, b) => contraste(b, fundo) - contraste(a, fundo));
|
|
499
|
+
const texto = tintas[0];
|
|
500
|
+
const outras = campos.filter((c, j) => j !== i && c !== texto && contraste(c, fundo) >= 2.2);
|
|
501
|
+
const acento = outras.length ? (outras.includes(campos[1]) && i !== 1 ? campos[1] : s.um(outras)) : texto;
|
|
502
|
+
return { fundo, texto, fraco: misturarCor(texto, fundo, 0.45), acento, contorno: misturarCor(texto, fundo, 0.6) };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** As cores de uma batida pelo fundo dela: base (a marca) ou acento (o fundo vira a cor). */
|
|
506
|
+
function coresDaBatida(P, fundo) {
|
|
507
|
+
if (fundo === 'acento') {
|
|
508
|
+
const texto = contraste(P.acento, P.fundo) >= contraste(P.acento, P.tinta) ? P.fundo : P.tinta;
|
|
509
|
+
return { fundo: P.acento, texto, fraco: misturarCor(texto, P.acento, 0.45), acento: texto, contorno: misturarCor(texto, P.acento, 0.6) };
|
|
510
|
+
}
|
|
511
|
+
return { fundo: P.fundo, texto: P.tinta, fraco: misturarCor(P.tinta, P.fundo, 0.58), acento: P.acento, contorno: misturarCor(P.tinta, P.fundo, 0.72) };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── o som de cada vídeo: uma FAMÍLIA, não um pacote ─────────────────────
|
|
515
|
+
/* "Os efeitos sonoros estão muito repetitivos" e "todo mundo usa os mesmos
|
|
516
|
+
sons" (30/09). Cada vídeo sintetiza a própria família no ffmpeg — 13
|
|
517
|
+
variações em 6 gestos (passagem, golpe, nasce, tique, revela, brilho) —
|
|
518
|
+
com afinação, duração, envelope e filtro sorteados da semente, no timbre
|
|
519
|
+
do humor: papel (foley de papel e madeira), analógico (pluck e fita),
|
|
520
|
+
digital (bitcrush), orgânico (madeira e sopro). Receita = argumentos do
|
|
521
|
+
ffmpeg; sons-leves.js gera os arquivos. */
|
|
522
|
+
const NOTAS = [220, 246.94, 261.63, 293.66, 329.63, 349.23, 392, 440];
|
|
523
|
+
function sonsDoVideo(roteiro = {}) {
|
|
524
|
+
const semente = hash('som:' + JSON.stringify((roteiro.partes || []).map((p) => [p.titulo, p.fala])) + ((roteiro.marca && roteiro.marca.nome) || ''));
|
|
525
|
+
const s = sorteador(semente);
|
|
526
|
+
const timbre = HUMORES[humorDe(roteiro, sorteador(hash('humor')))] ? HUMORES[humorDe(roteiro, s)].som : 'analogico';
|
|
527
|
+
const raiz = s.um(NOTAS);
|
|
528
|
+
const escala = [1, 1.189, 1.335, 1.498, 1.782, 2]; // pentatônica menor sobre a raiz
|
|
529
|
+
const nota = () => Math.round(raiz * s.um(escala) * (s.chance(0.3) ? 2 : 1));
|
|
530
|
+
const f = (x) => Math.round(x * 1000) / 1000;
|
|
531
|
+
const crush = timbre === 'digital' ? ',acrusher=bits=' + Math.round(s.entre(5, 9)) + ':mode=log:aa=1' : '';
|
|
532
|
+
const receitas = {};
|
|
533
|
+
const varre = (nome) => {
|
|
534
|
+
const d = f(s.entre(0.32, 0.7)), at = f(d * s.entre(0.45, 0.7)), fc = Math.round(s.entre(timbre === 'papel' ? 500 : 800, timbre === 'papel' ? 1600 : 2600));
|
|
535
|
+
const cor = timbre === 'papel' ? s.um(['brown', 'pink']) : timbre === 'digital' ? 'white' : 'pink';
|
|
536
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `anoisesrc=d=${d}:c=${cor}:a=0.9:s=${Math.round(s.entre(1, 9999))}`, '-af',
|
|
537
|
+
`bandpass=f=${fc}:width_type=o:w=${f(s.entre(1.2, 2.6))},afade=t=in:st=0:d=${at}:curve=exp,afade=t=out:st=${at}:d=${f(d - at)}:curve=qsin,volume=${f(s.entre(1.6, 2.4))}${crush}`];
|
|
538
|
+
};
|
|
539
|
+
const fita = (nome) => { // a fita que para: o tom cai até zero
|
|
540
|
+
const f0 = nota() * 2, d = f(s.entre(0.35, 0.6));
|
|
541
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.5*sin(2*PI*${f0}*(t-t*t/(2*${d})))*(1-t/${d})':s=44100:d=${d}`, '-af', `lowpass=f=${Math.round(s.entre(1800, 4000))}${crush}`];
|
|
542
|
+
};
|
|
543
|
+
const golpe = (nome) => {
|
|
544
|
+
const f0 = Math.round(s.entre(timbre === 'papel' ? 85 : 60, timbre === 'papel' ? 120 : 80)), f1 = Math.round(f0 * s.entre(0.4, 0.6)), d = f(s.entre(0.5, 1.0));
|
|
545
|
+
const ruido = f(timbre === 'papel' ? s.entre(0.4, 0.7) : s.entre(0.15, 0.4));
|
|
546
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.9*sin(2*PI*(${f0}*t-(${f0 - f1})*t*t/(2*${d})))*exp(-${f(s.entre(4, 8))}*t)+${ruido}*(random(0)*2-1)*exp(-${Math.round(s.entre(30, 60))}*t)':s=44100:d=${d}`,
|
|
547
|
+
'-af', `lowpass=f=${Math.round(s.entre(1400, 3200))},volume=1.4${crush}`];
|
|
548
|
+
};
|
|
549
|
+
const nasce = (nome) => {
|
|
550
|
+
if (timbre === 'papel' || timbre === 'organico') { // madeira: tom curto e seco, com o estalo do toque
|
|
551
|
+
const fq = Math.round(s.entre(380, 900));
|
|
552
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.7*sin(2*PI*${fq}*t)*exp(-${Math.round(s.entre(45, 80))}*t)+0.25*(random(0)*2-1)*exp(-${Math.round(s.entre(120, 220))}*t)':s=44100:d=0.16`,
|
|
553
|
+
'-af', `bandpass=f=${fq}:width_type=o:w=2.5,volume=2.2`];
|
|
554
|
+
} else { // pluck afinado na raiz do vídeo
|
|
555
|
+
const fq = nota(), k = Math.round(s.entre(14, 26));
|
|
556
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.55*exp(-${k}*t)*(sin(2*PI*${fq}*t)+0.45*sin(2*PI*${fq * 2}*t)*exp(-${k}*t)+0.2*sin(2*PI*${fq * 3}*t))':s=44100:d=0.32`,
|
|
557
|
+
'-af', `afade=t=out:st=0.26:d=0.06${crush}`];
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const tique = (nome) => {
|
|
561
|
+
if (timbre === 'papel' || timbre === 'organico') {
|
|
562
|
+
const fc = Math.round(s.entre(2500, 6500));
|
|
563
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `anoisesrc=d=0.035:c=white:a=0.8:s=${Math.round(s.entre(1, 9999))}`, '-af', `bandpass=f=${fc}:width_type=o:w=1.5,afade=t=out:st=0.005:d=0.03,volume=2.5`];
|
|
564
|
+
} else {
|
|
565
|
+
const fq = Math.round(s.entre(1800, 3600));
|
|
566
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.45*sin(2*PI*${fq}*t)*exp(-${Math.round(s.entre(70, 140))}*t)':s=44100:d=0.07`, ...(crush ? ['-af', crush.slice(1)] : [])];
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const revela = (nome) => {
|
|
570
|
+
const f0 = nota() / 2, d = f(s.entre(0.9, 1.4));
|
|
571
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='(0.3*sin(2*PI*(${f0}+${Math.round(s.entre(300, 800))}*t*t)*t)+${timbre === 'papel' ? 0.32 : 0.18}*(random(0)*2-1))*pow(t/${d},2)':s=44100:d=${d}`,
|
|
572
|
+
'-af', `highpass=f=${Math.round(s.entre(180, 420))},afade=t=out:st=${f(d - 0.05)}:d=0.05,volume=1.3${crush}`];
|
|
573
|
+
};
|
|
574
|
+
const brilho = (nome) => {
|
|
575
|
+
const a = nota() * 2, b = Math.round(a * s.um([1.25, 1.335, 1.5]));
|
|
576
|
+
receitas[nome] = ['-f', 'lavfi', '-i', `aevalsrc='0.3*sin(2*PI*${a}*t)*exp(-${f(s.entre(2.5, 4))}*t)+0.22*sin(2*PI*${b}*t)*exp(-${f(s.entre(3, 5))}*t)':s=44100:d=1.3`,
|
|
577
|
+
...(timbre === 'papel' ? ['-af', 'lowpass=f=2400'] : [])];
|
|
578
|
+
};
|
|
579
|
+
varre('passagem0'); varre('passagem1'); if (timbre === 'papel') varre('passagem2'); else fita('passagem2');
|
|
580
|
+
golpe('golpe0'); golpe('golpe1');
|
|
581
|
+
nasce('nasce0'); nasce('nasce1'); nasce('nasce2');
|
|
582
|
+
tique('tique0'); tique('tique1');
|
|
583
|
+
revela('revela0'); brilho('brilho0'); brilho('brilho1');
|
|
584
|
+
return { timbre, raiz, semente, receitas };
|
|
585
|
+
}
|
|
586
|
+
/** O gênero da trilha pelo humor do roteiro, e a semente para sortear a faixa. */
|
|
587
|
+
function musicaDe(roteiro = {}) {
|
|
588
|
+
const semente = hash('musica:' + JSON.stringify((roteiro.partes || []).map((p) => [p.titulo, p.fala])));
|
|
589
|
+
const s = sorteador(semente);
|
|
590
|
+
const h = humorDe(roteiro, s);
|
|
591
|
+
return { humor: h, genero: s.um(HUMORES[h].generos), semente };
|
|
592
|
+
}
|
|
593
|
+
/* ── O SOM DESENHADO (30/09, terceira volta): "os efeitos repetem e são os
|
|
594
|
+
genéricos que toda IA usa". Então:
|
|
595
|
+
densidade um evento por batida, no máximo (mais dois risers nas grandes
|
|
596
|
+
revelações); batida sem som também existe — a música carrega;
|
|
597
|
+
nunca repete cada evento é um arquivo seu, com parâmetros sorteados
|
|
598
|
+
(altura, duração, envelope, filtro, pan, cauda de eco);
|
|
599
|
+
da própria trilha o riser é um trecho da música AO CONTRÁRIO crescendo,
|
|
600
|
+
o golpe é a música parando como fita + um sub que cai, a
|
|
601
|
+
passagem é uma fatia filtrada dela, o chicote é um gaguejo de
|
|
602
|
+
um pedaço; o ar da câmera é ruído que atravessa o estéreo.
|
|
603
|
+
Sem trilha, os mesmos gestos saem sintetizados, também um por evento.
|
|
604
|
+
As receitas usam "{pasta}" no lugar da pasta do projeto (sons-leves.js). */
|
|
605
|
+
function desenharSom(P, { trilha = null, sons = {} } = {}) {
|
|
606
|
+
const sh = sorteador((P.semente ^ 0x2c1b3c6d) >>> 0);
|
|
607
|
+
const DUR = P.duracao;
|
|
608
|
+
const f = (x) => Math.round(x * 1000) / 1000;
|
|
609
|
+
const musica = trilha ? '{pasta}/' + trilha : null;
|
|
610
|
+
const receitas = {}, efeitos = [];
|
|
611
|
+
const pan = () => { const x = sh.entre(-0.7, 0.7); return [f(Math.min(1, 1 - x)), f(Math.min(1, 1 + x))]; };
|
|
612
|
+
const panF = () => { const [l, r] = pan(); return `pan=stereo|c0=${l}*c0|c1=${r}*c1`; };
|
|
613
|
+
const eco = () => `aecho=0.8:${f(sh.entre(0.4, 0.75))}:${Math.round(sh.entre(30, 90))}|${Math.round(sh.entre(100, 220))}:${f(sh.entre(0.15, 0.35))}|${f(sh.entre(0.08, 0.2))}`;
|
|
614
|
+
const ss = () => f(sh.entre(4, Math.max(6, DUR + 20))); // de onde da trilha sai o pedaço
|
|
615
|
+
const sint = sonsDoVideo({ partes: P.batidas.map((b) => ({ titulo: b.comp, fala: String(b.de) })) }).receitas;
|
|
616
|
+
const receita = (gesto) => {
|
|
617
|
+
if (!musica) { // sem trilha: a síntese, um arquivo por evento (a semente muda a cada um)
|
|
618
|
+
const base = { revela: 'revela0', golpe: sh.um(['golpe0', 'golpe1']), fatia: sh.um(['nasce0', 'nasce1', 'nasce2']), gaguejo: sh.um(['passagem0', 'passagem1']), ar: sh.um(['passagem1', 'passagem2']) }[gesto];
|
|
619
|
+
const args = [...sint[base]];
|
|
620
|
+
const i = args.indexOf('-af');
|
|
621
|
+
const extra = `asetrate=44100*${f(sh.entre(0.8, 1.25))},aresample=44100,${eco()},${panF()}`;
|
|
622
|
+
if (i >= 0) args[i + 1] += ',' + extra; else args.push('-af', extra);
|
|
623
|
+
return args;
|
|
624
|
+
}
|
|
625
|
+
if (gesto === 'revela') {
|
|
626
|
+
const d = f(sh.entre(0.9, 1.5));
|
|
627
|
+
return ['-ss', String(ss()), '-t', String(d), '-i', musica, '-af',
|
|
628
|
+
`areverse,highpass=f=${Math.round(sh.entre(150, 500))},afade=t=in:st=0:d=${f(d * 0.92)}:curve=exp,${eco()},volume=${f(sh.entre(4, 6))},${panF()}`];
|
|
629
|
+
}
|
|
630
|
+
if (gesto === 'golpe') {
|
|
631
|
+
const d = f(sh.entre(0.55, 0.95)), f0 = Math.round(sh.entre(55, 80)), f1 = Math.round(f0 * sh.entre(0.35, 0.55));
|
|
632
|
+
return ['-ss', String(ss()), '-t', String(d), '-i', musica, '-f', 'lavfi', '-i',
|
|
633
|
+
`aevalsrc='0.9*sin(2*PI*(${f0}*t-${f((f0 - f1) / (2 * d))}*t*t))*exp(-${f(sh.entre(3.5, 6))}*t)':s=44100:d=${d}`,
|
|
634
|
+
'-filter_complex', `[0:a]aformat=channel_layouts=stereo,asetrate=44100*${f(sh.entre(0.45, 0.7))},aresample=44100,lowpass=f=${Math.round(sh.entre(900, 2400))},afade=t=out:st=0:d=${d}:curve=exp[m];`
|
|
635
|
+
+ `[1:a]aformat=channel_layouts=stereo[s];[m][s]amix=inputs=2:weights=${f(sh.entre(0.8, 1.3))} ${f(sh.entre(0.7, 1.1))}:normalize=0,volume=${f(sh.entre(1.4, 2))}`];
|
|
636
|
+
}
|
|
637
|
+
if (gesto === 'gaguejo') {
|
|
638
|
+
const g = f(sh.entre(0.06, 0.13)), n = Math.round(sh.entre(3, 6));
|
|
639
|
+
return ['-ss', String(ss()), '-t', String(g), '-i', musica, '-af',
|
|
640
|
+
`aloop=loop=${n}:size=${Math.round(g * 44100)},highpass=f=${Math.round(sh.entre(200, 700))},afade=t=out:st=${f(g * (n + 1) * 0.55)}:d=${f(g * (n + 1) * 0.45)},volume=${f(sh.entre(1.6, 2.4))},${panF()}`];
|
|
641
|
+
}
|
|
642
|
+
if (gesto === 'fatia') {
|
|
643
|
+
const d = f(sh.entre(0.16, 0.38)), fc = Math.round(sh.entre(300, 3200));
|
|
644
|
+
return ['-ss', String(ss()), '-t', String(d), '-i', musica, '-af',
|
|
645
|
+
`bandpass=f=${fc}:width_type=o:w=${f(sh.entre(0.8, 2))},afade=t=in:st=0:d=0.006,afade=t=out:st=${f(d * 0.35)}:d=${f(d * 0.65)},${eco()},volume=${f(sh.entre(5, 8))},${panF()}`];
|
|
646
|
+
}
|
|
647
|
+
// ar: o ruído que atravessa o estéreo, como a câmera passando
|
|
648
|
+
const d = f(sh.entre(0.45, 0.8)), ida = sh.chance(0.5);
|
|
649
|
+
return ['-f', 'lavfi', '-i', `anoisesrc=d=${d}:c=${sh.um(['pink', 'brown'])}:a=0.8:s=${Math.round(sh.entre(1, 99999))}`, '-af',
|
|
650
|
+
`bandpass=f=${Math.round(sh.entre(500, 1800))}:width_type=o:w=${f(sh.entre(1.4, 2.6))},afade=t=in:st=0:d=${f(d * 0.55)}:curve=exp,afade=t=out:st=${f(d * 0.55)}:d=${f(d * 0.45)},`
|
|
651
|
+
+ `aformat=channel_layouts=stereo,aeval=exprs='val(0)*${ida ? '(1-t/' + d + ')' : '(t/' + d + ')'}|val(1)*${ida ? '(t/' + d + ')' : '(1-t/' + d + ')'}':channel_layout=stereo,volume=${f(sh.entre(4, 6))}`];
|
|
652
|
+
};
|
|
653
|
+
const evento = (t, gesto, volume) => {
|
|
654
|
+
if (t < 0 || t > DUR - 0.1) return;
|
|
655
|
+
if (efeitos.some((e) => Math.abs(e.t - t) < 0.25)) return;
|
|
656
|
+
const nome = 'e' + String(efeitos.length).padStart(2, '0') + '-' + gesto;
|
|
657
|
+
receitas[nome] = receita(gesto);
|
|
658
|
+
efeitos.push({ t: f(Math.max(0, t + (sh.prox() - 0.5) * 0.02)), nome, gesto, volume: f(volume * (0.85 + sh.prox() * 0.3)) });
|
|
659
|
+
};
|
|
660
|
+
const bs = P.batidas;
|
|
661
|
+
// Os dois risers: antes da primeira grande virada de fundo e antes do fecho.
|
|
662
|
+
const virada = bs.find((b, i) => i > 0 && b.entrada === 'inunda');
|
|
663
|
+
if (virada) evento(virada.de - 1.1, 'revela', 0.7);
|
|
664
|
+
if (bs.length > 2) evento(bs[bs.length - 1].de - 1.2, 'revela', 0.7);
|
|
665
|
+
// O golpe é raro (um a cada três batidas, nunca dois seguidos): golpe em toda tese vira bate-estaca.
|
|
666
|
+
let desdeGolpe = 9;
|
|
667
|
+
const tetoGolpes = Math.max(1, Math.round(bs.length / 3));
|
|
668
|
+
let golpes = 0;
|
|
669
|
+
bs.forEach((b, i) => {
|
|
670
|
+
const g = (b.golpes || [])[0];
|
|
671
|
+
const forte = g && g.forca >= 1 && desdeGolpe >= 2 && golpes < tetoGolpes;
|
|
672
|
+
desdeGolpe++;
|
|
673
|
+
if (forte) { desdeGolpe = 0; golpes++; }
|
|
674
|
+
if (i === 0) { if (forte) evento(g.t, 'golpe', 0.75); return; }
|
|
675
|
+
if (!forte && sh.chance(0.18)) return; // a batida que respira só com a música
|
|
676
|
+
if (forte) return evento(g.t, 'golpe', 0.8);
|
|
677
|
+
const tr = b.entrada;
|
|
678
|
+
if (tr === 'corte') return evento(b.de, 'golpe', 0.75);
|
|
679
|
+
if (tr === 'chicote' || tr === 'explode') return evento(b.de - 0.2, 'gaguejo', 0.7);
|
|
680
|
+
if (tr === 'atravessa' || tr === 'inunda') return evento(b.de - 0.4, 'ar', 0.75);
|
|
681
|
+
return evento(b.de - 0.08, 'fatia', 0.75);
|
|
682
|
+
});
|
|
683
|
+
efeitos.sort((a, b) => a.t - b.t);
|
|
684
|
+
// O src é o nome que sons-leves.prepararReceitas dá (semente + nome): um arquivo por evento.
|
|
685
|
+
const sufixo = (P.semente >>> 0).toString(36);
|
|
686
|
+
const fim = efeitos.map((e) => ({ t: e.t, src: (sons.__pasta || 'midia') + `/sfx-g${sufixo}-${e.nome}.mp3`, volume: e.volume }));
|
|
687
|
+
return { efeitos: fim, receitas: { semente: P.semente, receitas } };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Os nomes antigos (sons-leves.js) valem como reserva de cada gesto. */
|
|
691
|
+
const LEGADO = { passagem: ['whoosh', 'sopro'], golpe: ['impacto', 'toque'], nasce: ['pop', 'bolha'], tique: ['clique'], revela: ['riser', 'subida'], brilho: ['brilho'] };
|
|
692
|
+
|
|
693
|
+
// ── a montagem ──────────────────────────────────────────────────────────
|
|
694
|
+
/**
|
|
695
|
+
* @param {object} op
|
|
696
|
+
* @param {object} op.roteiro { marca, partes, modelo3d?, semente? }
|
|
697
|
+
* @param {Array} op.tempos [{de, ate, palavras:[{texto,de,ate}]}] por parte
|
|
698
|
+
* @returns {{ok, plano, partitura, resumo}}
|
|
699
|
+
*/
|
|
700
|
+
function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
|
|
701
|
+
const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
|
|
702
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
703
|
+
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
|
|
704
|
+
const DUR = partitura.duracao;
|
|
705
|
+
|
|
706
|
+
// ── o som: efeitos nas transformações, a música abaixa sob a voz ─────
|
|
707
|
+
/* O SOM (desenharSom): um evento por batida, no máximo — e cada um é um
|
|
708
|
+
arquivo PRÓPRIO, feito da trilha do vídeo (ou sintetizado, sem trilha). */
|
|
709
|
+
const somDesenhado = desenharSom(partitura, { trilha, sons });
|
|
710
|
+
const efeitos = somDesenhado.efeitos;
|
|
711
|
+
// A música vem cheia nos respiros e abaixa só sob a voz (a faixa chega normalizada a -18 LUFS).
|
|
712
|
+
const volumes = volumesDaTrilha(tempos.length === partes.length ? tempos : [], DUR, { alto: 0.95, baixo: 0.38 });
|
|
713
|
+
|
|
714
|
+
const P = partitura.paleta;
|
|
715
|
+
const fontesVariaveis = {};
|
|
716
|
+
for (const f of Object.values(partitura.fontes)) { const e = especEixos(f); if (e != null) fontesVariaveis[f] = e; }
|
|
717
|
+
const plano = {
|
|
718
|
+
formato: { largura, altura, fps: FPS },
|
|
719
|
+
fundoGeral: P.fundo,
|
|
720
|
+
fontesVariaveis,
|
|
721
|
+
cenas: [{
|
|
722
|
+
duracao: DUR,
|
|
723
|
+
fundo: P.fundo,
|
|
724
|
+
elementos: [{
|
|
725
|
+
tipo: 'kit', peca: 'gerativo', partitura,
|
|
726
|
+
// As chaves "fonte*" são o que o fontes.css procura (remotion.js).
|
|
727
|
+
fonte: partitura.fontes.display, fonteTexto: partitura.fontes.texto, fonteMono: partitura.fontes.mono,
|
|
728
|
+
fonteCinetica: partitura.fontes.cinetica, fonteSerifa: partitura.fontes.serifa,
|
|
729
|
+
}],
|
|
730
|
+
}],
|
|
731
|
+
audio: {
|
|
732
|
+
...(narracao ? { narracao } : {}),
|
|
733
|
+
...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
|
|
734
|
+
efeitos,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
ok: true, plano, partitura, somReceitas: somDesenhado.receitas,
|
|
739
|
+
resumo: { cenas: 1, batidas: partitura.batidas.length, segundos: DUR, fps: FPS, composicoes: partitura.batidas.map((b) => b.comp), semente: partitura.semente },
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** A partitura, sem áudio nem plano — é o que os testes olham. */
|
|
744
|
+
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
745
|
+
const partes = roteiro.partes || [];
|
|
746
|
+
const marca = roteiro.marca || {};
|
|
747
|
+
const W = largura, H = altura, deitado = W >= H;
|
|
748
|
+
const semente = roteiro.semente != null ? hash('semente:' + roteiro.semente)
|
|
749
|
+
: hash(JSON.stringify([partes.map((p) => [p.tipo, p.titulo, p.texto, p.fala, p.valor, p.itens, p.nao, p.sim, p.destaque]), marca.nome || '']));
|
|
750
|
+
const s = sorteador(semente);
|
|
751
|
+
const humor = humorDe(roteiro, s);
|
|
752
|
+
const H0 = HUMORES[humor];
|
|
753
|
+
const P = paletaDe(marca, s, humor, roteiro);
|
|
754
|
+
|
|
755
|
+
/* ── as fontes: NADA de cara de IA. A da marca vale se tiver caráter (e
|
|
756
|
+
eixos para animar); a genérica da marca vira texto, e o título ganha
|
|
757
|
+
uma face da biblioteca do humor. ─────────────────────────────────── */
|
|
758
|
+
const pedida = (marca.fontes && marca.fontes.titulo) || '';
|
|
759
|
+
const pedidaTexto = (marca.fontes && marca.fontes.texto) || '';
|
|
760
|
+
const boa = (f) => f && VARIAVEIS[f] && !BANIDAS.includes(f) && VARIAVEIS[f].eixos.wght;
|
|
761
|
+
const display = boa(pedida) ? pedida : s.um(H0.displays);
|
|
762
|
+
const cinetica = eixoDe(display, 'wdth') && eixoDe(display, 'wdth')[0] <= 75 ? display : s.um(H0.cineticas);
|
|
763
|
+
const texto = pedidaTexto ? pedidaTexto : (pedida && BANIDAS.includes(pedida) ? pedida : s.um(H0.textos));
|
|
764
|
+
const serifa = VARIAVEIS[display].serifa ? display : s.um(H0.serifas);
|
|
765
|
+
const fontes = { display, cinetica, texto, mono: s.um(H0.monos), serifa };
|
|
766
|
+
|
|
767
|
+
// ── o quadro: margens e respiro ─────────────────────────────────────
|
|
768
|
+
// A margem conta com a câmera: ela empurra até ~8% e desliza, e nada pode sair do quadro.
|
|
769
|
+
const M = Math.round(W * (deitado ? 0.085 : 0.095));
|
|
770
|
+
const larguraUtil = W - 2 * M;
|
|
771
|
+
const base = Math.min(W, H) / 1080;
|
|
772
|
+
const tamRotulo = Math.round((deitado ? 24 : 30) * base);
|
|
773
|
+
const tem3d = !!roteiro.modelo3d;
|
|
774
|
+
const ws = temposDasPartes(partes, tempos);
|
|
775
|
+
|
|
776
|
+
// ── os limites de cada parte na linha do tempo ────────────────────────
|
|
777
|
+
const inicio = [];
|
|
778
|
+
partes.forEach((_, i) => inicio.push(i === 0 ? 0 : r3(Math.max(inicio[i - 1] + 1.4, ws[i].de - 0.12))));
|
|
779
|
+
const ultimaFala = ws[ws.length - 1];
|
|
780
|
+
const duracao = r1(Math.max(inicio[inicio.length - 1] + 2.0, (ultimaFala.ate || 0) + 1.15));
|
|
781
|
+
const fimParte = partes.map((_, i) => (i < partes.length - 1 ? inicio[i + 1] : duracao));
|
|
782
|
+
|
|
783
|
+
// ── as batidas: de cada parte, 1 a 3 ─────────────────────────────────
|
|
784
|
+
const brutas = [];
|
|
785
|
+
partes.forEach((p, i) => {
|
|
786
|
+
const c = conteudoDe(p, marca);
|
|
787
|
+
const palavrasP = ws[i].palavras;
|
|
788
|
+
const t0 = inicio[i], t1 = fimParte[i];
|
|
789
|
+
let de = t0;
|
|
790
|
+
/* A FALA que não é o que aparece ("Feche os olhos. Agora escute." antes
|
|
791
|
+
de "VÓRTEX H1") vira uma batida própria, palavra por palavra — quando
|
|
792
|
+
há tempo para as duas e a primeira frase não diz o conteúdo. */
|
|
793
|
+
const frases = frasesDe(falaDe(p), palavrasP);
|
|
794
|
+
const chave = tokens(textoDoConteudo(c)).filter((w) => w.length >= 3 && !FUNCAO.has(w));
|
|
795
|
+
if (frases.length >= 2 && t1 - t0 >= 2.6 && c.tipo !== 'lista') {
|
|
796
|
+
const f0 = frases[0];
|
|
797
|
+
const diz = tokens(f0.texto);
|
|
798
|
+
const cobre = chave.filter((w) => diz.some((d) => d === w || (w.length >= 5 && d.startsWith(w.slice(0, 5))))).length / Math.max(1, chave.length);
|
|
799
|
+
const ultimaW = f0.palavras[f0.palavras.length - 1];
|
|
800
|
+
const fimF = r3(Math.max(frases[1].palavras[0].de - 0.1, ultimaW.de + REGRAS.leitura + 0.1));
|
|
801
|
+
if (cobre < 0.5 && fimF - de >= 1.3 && t1 - fimF >= 1.9 && f0.palavras.length <= 9 && s.chance(0.9)) {
|
|
802
|
+
brutas.push({ parte: i, de, ate: fimF, conteudo: { tipo: 'falado', texto: f0.texto, palavras: f0.palavras, destaque: p.destaque || '' } });
|
|
803
|
+
de = fimF;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
brutas.push({ parte: i, de, ate: t1, conteudo: { ...c, destaque: p.destaque || '' }, palavras: palavrasP });
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// ── a composição de cada batida: sorteada entre as que cabem ─────────
|
|
810
|
+
const usadas = {};
|
|
811
|
+
const memoria = {};
|
|
812
|
+
const batidas = [];
|
|
813
|
+
let anterior = null;
|
|
814
|
+
const recentes = [];
|
|
815
|
+
const escolher = (pesos, { evita = [] } = {}) => {
|
|
816
|
+
const w = {};
|
|
817
|
+
for (const [nome, p] of Object.entries(pesos)) {
|
|
818
|
+
const comp = nome === 'rajada' ? 'gigante' : nome === 'duelo' ? 'pilha' : nome;
|
|
819
|
+
let q = p;
|
|
820
|
+
if (comp === anterior || evita.includes(comp)) q = 0;
|
|
821
|
+
if (recentes.slice(-3).includes(comp)) q *= 0.35;
|
|
822
|
+
q *= Math.pow(0.72, usadas[comp] || 0);
|
|
823
|
+
if (comp === 'parede' && (usadas.parede || 0) >= 1) q *= 0.25;
|
|
824
|
+
w[nome] = q;
|
|
825
|
+
}
|
|
826
|
+
return s.pesado(w) || s.pesado({ pilha: anterior === 'pilha' ? 0 : 1, gigante: anterior === 'gigante' ? 0 : 1, palavras: anterior === 'palavras' ? 0 : 1 });
|
|
827
|
+
};
|
|
828
|
+
const registra = (b) => { usadas[b.comp] = (usadas[b.comp] || 0) + 1; recentes.push(b.comp); anterior = b.comp; batidas.push(b); };
|
|
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
|
+
|
|
842
|
+
brutas.forEach((br, bi) => {
|
|
843
|
+
const c = br.conteudo;
|
|
844
|
+
const tituloC = c.titulo || c.nome || c.texto || '';
|
|
845
|
+
const feat = {
|
|
846
|
+
curto: tituloC.length <= 22 && tituloC.split(/\s+/).length <= 4,
|
|
847
|
+
palavras: (c.palavras || []).length, primeira: bi === 0, ultima: bi === brutas.length - 1, tem3d,
|
|
848
|
+
pergunta: /\?/.test(tituloC), exclama: /!/.test(tituloC), numeroNoTitulo: /\d/.test(tituloC),
|
|
849
|
+
};
|
|
850
|
+
let nome = escolher(pesosDe(c, feat));
|
|
851
|
+
if (bi === alvo3d && !usadas.produto) nome = 'produto';
|
|
852
|
+
const ctx = { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes, P, palavras: br.palavras || c.palavras || [], parte: br.parte, marca, roteiro, memoria };
|
|
853
|
+
if (nome === 'rajada' && br.ate - br.de < 1.6 * c.itens.length) nome = 'cascata';
|
|
854
|
+
if (nome === 'rajada') {
|
|
855
|
+
// Cada item vira a sua batida; o título da lista fica como rótulo.
|
|
856
|
+
const n = c.itens.length;
|
|
857
|
+
const quandoItem = casar(c.itens, ctx.palavras, { de: br.de });
|
|
858
|
+
let de = br.de;
|
|
859
|
+
c.itens.forEach((it, j) => {
|
|
860
|
+
// Cada item fica no ar o bastante para ser lido: 1 s depois da última linha dele.
|
|
861
|
+
const MIN = 1.6;
|
|
862
|
+
const prox = j < n - 1 ? (quandoItem[j + 1] != null ? Math.max(de + MIN, quandoItem[j + 1] - 0.1) : de + (br.ate - de) / (n - j)) : br.ate;
|
|
863
|
+
const ate = j < n - 1 ? r3(Math.min(prox, br.ate - MIN * (n - j - 1))) : br.ate;
|
|
864
|
+
const comp = (anterior === 'gigante') ? 'pilha' : 'gigante';
|
|
865
|
+
const b = compor(comp, { parte: br.parte, de: r3(de), ate, conteudo: { tipo: 'titulo', titulo: (c.numerada ? String(j + 1).padStart(2, '0') + ' ' : '') + it, sub: '', selo: c.titulo, destaque: c.destaque } }, ctx);
|
|
866
|
+
registra(b);
|
|
867
|
+
de = ate;
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (nome === 'duelo') {
|
|
872
|
+
const meio = casar([c.sim], ctx.palavras, { de: br.de + 1.2 })[0];
|
|
873
|
+
const corte = r3(Math.min(br.ate - 1.4, Math.max(br.de + 1.3, meio != null ? meio - 0.1 : (br.de + br.ate) / 2)));
|
|
874
|
+
const cNao = anterior !== 'pilha' ? 'pilha' : 'gigante';
|
|
875
|
+
registra(compor(cNao, { parte: br.parte, de: br.de, ate: corte, conteudo: { tipo: 'titulo', titulo: c.nao, sub: '', riscado: true, destaque: '' } }, ctx));
|
|
876
|
+
registra(compor(cNao === 'pilha' ? 'gigante' : 'pilha', { parte: br.parte, de: corte, ate: br.ate, conteudo: { tipo: 'titulo', titulo: c.sim, sub: '', destaque: c.destaque } }, ctx));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
registra(compor(nome, br, ctx));
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
/* ── os campos: o fundo inteiro troca de cor a cada batida (quase sempre),
|
|
883
|
+
nunca igual ao anterior nem ao de duas antes; a fala e o conteúdo da
|
|
884
|
+
mesma parte às vezes dividem o campo. O produto 3D pede o campo escuro. */
|
|
885
|
+
const ditaCor = tokens(partes.map((p) => [p.titulo, p.destaque].join(' ')).join(' ')).map((w) => CORES_DITAS[w]).find(Boolean) || null;
|
|
886
|
+
const campos = camposDe(marca, humor, s, ditaCor);
|
|
887
|
+
batidas.forEach((b, i) => {
|
|
888
|
+
if (i === 0) { b.campo = 0; return; }
|
|
889
|
+
const a = batidas[i - 1];
|
|
890
|
+
if (a.parte === b.parte && s.chance(0.4)) { b.campo = a.campo; return; }
|
|
891
|
+
if (b.produto3d) { b.campo = a.campo === 0 ? 3 : 0; return; }
|
|
892
|
+
const evita = [a.campo, i > 1 ? batidas[i - 2].campo : -1];
|
|
893
|
+
const op = campos.map((_, j) => j).filter((j) => !evita.includes(j));
|
|
894
|
+
b.campo = s.um(op);
|
|
895
|
+
});
|
|
896
|
+
for (const b of batidas) { b.fundo = 'campo' + b.campo; b.cores = coresDoCampo(campos, b.campo, s); }
|
|
897
|
+
|
|
898
|
+
/* ── os ÍCONES 3D: onde a parte pediria um ícone (lista, número,
|
|
899
|
+
recurso), um objeto de geometria no material do vídeo. A palavra
|
|
900
|
+
decide qual; nunca o mesmo duas vezes, e no máximo em ~45% das batidas
|
|
901
|
+
— ícone em toda batida vira papel de parede de IA. ───────────────── */
|
|
902
|
+
const material = s.um(['argila', 'verniz', 'metal']);
|
|
903
|
+
const iconesUsados = new Set();
|
|
904
|
+
const tetoIcones = Math.ceil(batidas.length * 0.45);
|
|
905
|
+
for (const b of batidas) {
|
|
906
|
+
if (iconesUsados.size >= tetoIcones) break;
|
|
907
|
+
if (b.produto3d || ['parede', 'marca', 'citacao', 'divisao'].includes(b.comp)) continue;
|
|
908
|
+
const ditas = tokens(b.linhas.map((l) => l.texto).join(' ') + ' ' + ((partes[b.parte] && partes[b.parte].fala) || ''));
|
|
909
|
+
const ic = ditas.map((w) => ICONE_DE[w]).find((x) => x && !iconesUsados.has(x));
|
|
910
|
+
if (!ic) continue;
|
|
911
|
+
iconesUsados.add(ic);
|
|
912
|
+
const prim = b.linhas[0];
|
|
913
|
+
b.icone3d = { icone: ic, lado: deitado ? 'direita' : 'acima', tamanho: Math.round(deitado ? 0.4 * H : 0.38 * W), material,
|
|
914
|
+
cor: b.cores.texto, cor2: b.cores.acento, luz: b.cores.acento, t: r3(Math.min(b.ate - 1, (prim ? prim.t : b.de) + 0.15)) };
|
|
915
|
+
b.acentos += 1;
|
|
916
|
+
if (b.acentos > REGRAS.acentos) { b.icone3d.cor2 = misturarCor(b.cores.texto, b.cores.fundo, 0.3); b.acentos -= 1; }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* ── o HERÓI GRÁFICO: toda batida tem uma ideia visual grande além do
|
|
920
|
+
texto, como no reel de referência — a explosão radial que nasce de um
|
|
921
|
+
ponto, o polígono que vira estrela e quadrado com partículas em órbita,
|
|
922
|
+
o campo de truchet com uma onda de cor, a esfera e o toro de pontos em
|
|
923
|
+
3D, os anéis de órbita, a grade que deforma, os rastros de velocidade.
|
|
924
|
+
Procedurais, da semente, na paleta da batida; nunca o mesmo seguido. ── */
|
|
925
|
+
const OBJETOS = ['explosao', 'poligono', 'esfera', 'toro'], CAMPOS_H = ['truchet', 'aneis', 'grade', 'rastro'];
|
|
926
|
+
const usosH = {};
|
|
927
|
+
let ultimoH = null;
|
|
928
|
+
const recentesH = [];
|
|
929
|
+
batidas.forEach((b, i) => {
|
|
930
|
+
const ocupado = b.icone3d || b.produto3d;
|
|
931
|
+
let op = ocupado || ['gigante', 'parede', 'divisao'].includes(b.comp) ? CAMPOS_H : [...OBJETOS, ...CAMPOS_H];
|
|
932
|
+
if (b.comp === 'parede') op = ['aneis', 'rastro', 'grade'];
|
|
933
|
+
const w = {};
|
|
934
|
+
for (const h of op) {
|
|
935
|
+
let q = OBJETOS.includes(h) ? 2.2 : 1;
|
|
936
|
+
if (recentesH.slice(-3).includes(h)) q = 0;
|
|
937
|
+
if (h === 'truchet' && (usosH.truchet || 0) >= 1) q = 0;
|
|
938
|
+
if ((h === 'esfera' || h === 'toro') && (usosH[h] || 0) >= 1) q = 0;
|
|
939
|
+
q *= Math.pow(0.5, usosH[h] || 0);
|
|
940
|
+
w[h] = q;
|
|
941
|
+
}
|
|
942
|
+
// Tudo recente: o menos recente de todos os de campo.
|
|
943
|
+
const reserva = () => CAMPOS_H.filter((x) => !recentesH.slice(-3).includes(x))[0] || CAMPOS_H[0];
|
|
944
|
+
let h = i === 0 && b.comp === 'marca' ? 'explosao' : s.pesado(w) || reserva();
|
|
945
|
+
usosH[h] = (usosH[h] || 0) + 1;
|
|
946
|
+
ultimoH = h;
|
|
947
|
+
recentesH.push(h);
|
|
948
|
+
const comCor = b.acentos < REGRAS.acentos;
|
|
949
|
+
if (comCor) b.acentos += 1;
|
|
950
|
+
b.heroi = { tipo: h, modo: OBJETOS.includes(h) ? 'objeto' : 'campo', semente: hash(b.id + h) % 9973,
|
|
951
|
+
cor: comCor ? b.cores.acento : misturarCor(b.cores.texto, b.cores.fundo, 0.25), cor2: b.cores.texto };
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// ── as passagens: como uma batida vira a outra ──────────────────────
|
|
955
|
+
let ultimaSaida = null, desdeCorte = 9;
|
|
956
|
+
const saidasRecentes = [];
|
|
957
|
+
batidas.forEach((b, i) => {
|
|
958
|
+
if (i === 0) { b.entrada = 'semente'; return; }
|
|
959
|
+
const a = batidas[i - 1];
|
|
960
|
+
let tr;
|
|
961
|
+
// O campo troca por corte seco, por cortina ou pela inundação do acento — independente do gesto do texto.
|
|
962
|
+
b.troca = a.campo === b.campo ? null : s.pesado({ corte: 5, cortina: 2.5, inunda: 1.5 });
|
|
963
|
+
if (b.troca === 'inunda' && s.chance(0.5)) tr = 'inunda';
|
|
964
|
+
else {
|
|
965
|
+
const tese = b.linhas.find((l) => l.papel === 'tese' || l.papel === 'numero');
|
|
966
|
+
/* As passagens EXTRAVAGANTES pesam mais ("motion é vida", 30/09): a
|
|
967
|
+
letra explode e remonta a próxima palavra, a câmera chicoteia,
|
|
968
|
+
atravessa a palavra. As calmas ficam de respiro. */
|
|
969
|
+
const w = { explode: 3, chicote: 2.8, atravessa: 2.4, pontos: 2.2, evapora: 0.8, desliza: 0.3, comprime: eixoDe(fontes.display, 'wdth') ? 1.2 : 0.7,
|
|
970
|
+
corte: desdeCorte >= 3 && tese && tese.comportamento === 'carimbo' ? 1.8 : 0 };
|
|
971
|
+
if (a.parte === b.parte) { w.atravessa *= 0.5; w.pontos *= 1.4; }
|
|
972
|
+
for (const x of saidasRecentes.slice(-3)) w[x] = 0;
|
|
973
|
+
tr = s.pesado(w);
|
|
974
|
+
}
|
|
975
|
+
desdeCorte = tr === 'corte' ? 0 : desdeCorte + 1;
|
|
976
|
+
ultimaSaida = tr;
|
|
977
|
+
saidasRecentes.push(tr);
|
|
978
|
+
b.entrada = tr;
|
|
979
|
+
a.saida = tr;
|
|
980
|
+
// O que explode REMONTA: as letras da próxima tese chegam dos estilhaços.
|
|
981
|
+
if (tr === 'explode') b.linhas.filter((l) => l.papel === 'tese' && !l.marca && !l.riscado).forEach((l) => { l.comportamento = 'monta'; });
|
|
982
|
+
});
|
|
983
|
+
batidas[batidas.length - 1].saida = 'fim';
|
|
984
|
+
|
|
985
|
+
/* O gesto da tese não volta antes de três batidas — conferido DEPOIS das
|
|
986
|
+
passagens (o "monta" que a explosão pede também conta): se repetir, a
|
|
987
|
+
batida ganha outro gesto; se o repetido é o monta, a passagem muda. */
|
|
988
|
+
const recG = [];
|
|
989
|
+
const gestoLivre = (fonte) => ['peso', 'carimbo', 'largura', 'contorno', 'onda', 'pontos', 'mascara', 'desfoque']
|
|
990
|
+
.filter((g) => !recG.slice(-3).includes(g) && (g !== 'largura' || eixoDe(fonte, 'wdth')) && (g !== 'peso' || eixoDe(fonte, 'wght')));
|
|
991
|
+
batidas.forEach((b, i) => {
|
|
992
|
+
const teses = b.linhas.filter((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.marca);
|
|
993
|
+
if (!teses.length) return;
|
|
994
|
+
let g = teses[0].comportamento;
|
|
995
|
+
// O contador é sempre "peso" (o número engorda contando): quem vem antes dele cede o peso.
|
|
996
|
+
const contadorPerto = !b.contador && batidas.slice(i + 1, i + 4).some((x) => x.contador);
|
|
997
|
+
if (recG.slice(-3).includes(g) || (contadorPerto && g === 'peso')) {
|
|
998
|
+
if (contadorPerto) recG.push('peso');
|
|
999
|
+
if (g === 'monta' && i > 0) {
|
|
1000
|
+
const alt = ['pontos', 'atravessa', 'evapora', 'comprime', 'desliza'].find((x) => !saidasRecentes.slice(Math.max(0, i - 4), i - 1).includes(x)) || 'pontos';
|
|
1001
|
+
b.entrada = alt; batidas[i - 1].saida = alt;
|
|
1002
|
+
}
|
|
1003
|
+
const livres = gestoLivre(teses[0].fonte);
|
|
1004
|
+
const novo = livres.length ? s.um(livres) : g;
|
|
1005
|
+
teses.filter((l) => l.comportamento === g).forEach((l) => { l.comportamento = novo; });
|
|
1006
|
+
if (b.contador) teses.forEach((l) => { if (l.id === b.contador.linha) l.comportamento = 'peso'; });
|
|
1007
|
+
g = teses[0].comportamento;
|
|
1008
|
+
if (contadorPerto) recG.pop();
|
|
1009
|
+
}
|
|
1010
|
+
recG.push(g);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// ── a vida no repouso: a fonte respira enquanto a gente lê ───────────
|
|
1014
|
+
for (const b of batidas) {
|
|
1015
|
+
const tese = b.linhas.find((l) => l.papel === 'tese' || l.papel === 'numero');
|
|
1016
|
+
const f = tese ? tese.fonte : fontes.display;
|
|
1017
|
+
const op = ['onda', 'deriva', ...(eixoDe(f, 'wght') ? ['respira', 'respira'] : []), ...(eixoDe(f, 'wdth') ? ['estica'] : [])];
|
|
1018
|
+
b.vida = b.comp === 'contador' ? 'respira' : s.um(op);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/* ── os GOLPES: a tese bate — escala que soca, tela que treme, impacto
|
|
1022
|
+
no som. Um por batida no máximo, onde a palavra mais forte assenta. */
|
|
1023
|
+
for (const b of batidas) {
|
|
1024
|
+
const t = b.linhas.find((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.riscado);
|
|
1025
|
+
b.golpes = [];
|
|
1026
|
+
if (!t) continue;
|
|
1027
|
+
const pt = t.palavras.find((p) => p.papel === 'tese') || t.palavras[0];
|
|
1028
|
+
const quando = b.contador ? b.contador.t + b.contador.dur : (pt.t || t.t) + (t.comportamento === 'monta' ? 0.32 : t.comportamento === 'carimbo' ? 0.03 : 0.2);
|
|
1029
|
+
if (quando < b.ate - 0.3) b.golpes.push({ t: r3(quando), linha: t.id, forca: b.comp === 'gigante' || b.comp === 'contador' || b.comp === 'parede' ? 1 : 0.6 });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ── a câmera: nunca para; acelera na passagem ────────────────────────
|
|
1033
|
+
const camera = [{ t: 0, x: 0, y: 0, zoom: 1.12, rot: 0, e: 'camera' }];
|
|
1034
|
+
let lado = 1;
|
|
1035
|
+
batidas.forEach((b, i) => {
|
|
1036
|
+
// A câmera tem inércia: o sentido do movimento quase sempre continua (nada de virar a cada batida).
|
|
1037
|
+
if (s.chance(0.2)) lado = -lado;
|
|
1038
|
+
const z0 = r3(1 + s.entre(0, 0.02)), z1 = r3(z0 + s.entre(0.05, 0.09));
|
|
1039
|
+
const x0 = r1(lado * s.entre(0.004, 0.014) * W), x1 = r1(-lado * s.entre(0.01, 0.022) * W);
|
|
1040
|
+
const y0 = r1(s.entre(-0.006, 0.006) * H), y1 = r1(s.entre(-0.01, 0.01) * H);
|
|
1041
|
+
const g0 = r3(s.entre(-0.6, 0.6)), g1 = r3(g0 + lado * s.entre(0.5, 1.1));
|
|
1042
|
+
const assenta = Math.min(b.de + 0.55, (b.de + b.ate) / 2);
|
|
1043
|
+
if (b.entrada === 'corte') camera.push({ t: r3(b.de), x: x0, y: y0, zoom: z0 + 0.04, rot: g0, e: 'degrau' });
|
|
1044
|
+
camera.push({ t: r3(assenta), x: x0, y: y0, zoom: z0, rot: g0, e: i === 0 ? 'camera' : 'editorial' });
|
|
1045
|
+
const fimCalmo = r3(Math.max(assenta + 0.1, b.ate - 0.32));
|
|
1046
|
+
camera.push({ t: fimCalmo, x: x1, y: y1, zoom: z1, rot: g1, e: 'camera' });
|
|
1047
|
+
if (i < batidas.length - 1) {
|
|
1048
|
+
const prox = batidas[i + 1].entrada;
|
|
1049
|
+
const empurra = prox === 'atravessa' ? 0.4 : prox === 'explode' ? 0.16 : prox === 'inunda' ? 0.14 : prox === 'corte' ? 0.03 : 0.1;
|
|
1050
|
+
// O chicote: a câmera sai rasgando de lado, e a próxima chega do outro.
|
|
1051
|
+
const xs = prox === 'chicote' ? -lado * 0.22 * W : x1 * 2.2;
|
|
1052
|
+
camera.push({ t: r3(b.ate), x: r1(xs), y: r1(y1 * 2), zoom: r3(z1 + empurra), rot: r3(g1 + lado * (prox === 'corte' ? 0.3 : prox === 'chicote' ? 2.6 : 2)), e: 'acelera' });
|
|
1053
|
+
} else camera.push({ t: r3(b.ate), x: 0, y: 0, zoom: r3(z1 + 0.03), rot: 0, e: 'camera' });
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// ── o produto em 3D: mora nas batidas que o hospedam ─────────────────
|
|
1057
|
+
const produtos = batidas.filter((b) => b.produto3d).map((b) => ({ ...b.produto3d, de: b.de, ate: b.ate, modelo: roteiro.modelo3d,
|
|
1058
|
+
cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento, ...(roteiro.variacao3d || {}) }));
|
|
1059
|
+
|
|
1060
|
+
// ── o fundo vivo ────────────────────────────────────────────────────
|
|
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,
|
|
1066
|
+
brilho: { x: s.entre(0.25, 0.75), y: s.entre(0.2, 0.5) },
|
|
1067
|
+
// Partículas em três profundidades: o espaço respira mesmo no repouso.
|
|
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 = [];
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
versao: 1, semente, fps: FPS, humor,
|
|
1073
|
+
musica: { genero: s.um(H0.generos), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
|
|
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] }])),
|
|
1075
|
+
margem: M, fundo, batidas, camera, produtos,
|
|
1076
|
+
hud: roteiro.hud === true ? { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' } : null,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ── as composições: de conteúdo + contexto, uma batida ─────────────────
|
|
1081
|
+
function compor(comp, br, ctx) {
|
|
1082
|
+
const { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes } = ctx;
|
|
1083
|
+
const c = br.conteudo;
|
|
1084
|
+
const palavras = ctx.palavras || [];
|
|
1085
|
+
const b = { id: 'b' + Math.round(br.de * 1000), parte: br.parte, de: r3(br.de), ate: r3(br.ate), comp, origem: c.tipo === 'falado' ? 'falado' : 'conteudo', linhas: [], acento: [] };
|
|
1086
|
+
const eixosDisplay = VARIAVEIS[fontes.display].eixos;
|
|
1087
|
+
const wMax = eixosDisplay.wght[1];
|
|
1088
|
+
const wTese = Math.min(wMax, s.um([800, 900, 900, 1000]));
|
|
1089
|
+
const destaqueTk = tokens(c.destaque || '');
|
|
1090
|
+
const ehTese = (w) => destaqueTk.length && tokens(w).some((k) => destaqueTk.includes(k));
|
|
1091
|
+
let id = 0;
|
|
1092
|
+
const linha = (texto, papel, o = {}) => {
|
|
1093
|
+
const l = {
|
|
1094
|
+
id: b.id + '-' + (id++), texto: String(texto), papel, fonte: o.fonte || fontes.display, tamanho: Math.round(o.tamanho),
|
|
1095
|
+
eixos: o.eixos || { wght: papel === 'tese' || papel === 'numero' ? wTese : papel === 'rotulo' ? 500 : 420 },
|
|
1096
|
+
cor: o.cor || (papel === 'rotulo' || papel === 'fraco' ? 'fraco' : 'texto'), tracking: o.tracking || 0, maiusc: !!o.maiusc,
|
|
1097
|
+
comportamento: o.comportamento || null, riscado: !!o.riscado, envelhece: null,
|
|
1098
|
+
};
|
|
1099
|
+
l.palavras = l.texto.split(/\s+/).filter(Boolean).map((w) => ({ texto: w, papel: ehTese(w) ? 'tese' : funcao(w) ? 'funcao' : 'normal' }));
|
|
1100
|
+
b.linhas.push(l);
|
|
1101
|
+
return l;
|
|
1102
|
+
};
|
|
1103
|
+
// Os pesos DENTRO da frase: artigo leve, verbo regular, tese pesada e maior.
|
|
1104
|
+
const misturarPesos = (l, { maiorTese = 1.12, corTese = true } = {}) => {
|
|
1105
|
+
const [wmin, wmax] = (VARIAVEIS[l.fonte] && VARIAVEIS[l.fonte].eixos.wght) || [300, 800];
|
|
1106
|
+
const temTese = l.palavras.some((p) => p.papel === 'tese');
|
|
1107
|
+
for (const p of l.palavras) {
|
|
1108
|
+
if (p.papel === 'tese') { p.wght = Math.min(wmax, wTese); p.escala = maiorTese; if (corTese) p.cor = 'acento'; }
|
|
1109
|
+
else if (p.papel === 'funcao') p.wght = prende(temTese ? 300 : 380, [wmin, wmax]);
|
|
1110
|
+
else p.wght = prende(temTese ? 520 : l.eixos.wght, [wmin, wmax]);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
const vert = (opcoes) => s.um(opcoes);
|
|
1114
|
+
const tese = (l) => { l.papel = 'tese'; return l; };
|
|
1115
|
+
|
|
1116
|
+
switch (comp) {
|
|
1117
|
+
case 'gigante': {
|
|
1118
|
+
const txt = c.titulo || c.nome || c.sim || c.valor || c.texto || '';
|
|
1119
|
+
const maiusc = txt.length <= 16 && s.chance(0.65);
|
|
1120
|
+
const fonte = s.chance(0.5) ? fontes.cinetica : fontes.display;
|
|
1121
|
+
const eixos = { wght: Math.min(VARIAVEIS[fonte].eixos.wght[1], wTese) };
|
|
1122
|
+
const op = { fonte, wght: eixos.wght, maiusc };
|
|
1123
|
+
const nMax = deitado ? 2 : Math.min(4, txt.split(/\s+/).length);
|
|
1124
|
+
const alvo = deitado ? larguraUtil * 0.96 : larguraUtil;
|
|
1125
|
+
const fit = caberEmLinhas(txt, alvo, nMax, (deitado ? 0.3 : 0.2) * H, op);
|
|
1126
|
+
const alinha = deitado && s.chance(0.35) ? 'esq' : 'centro';
|
|
1127
|
+
b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert(deitado ? [0.5, 0.52, 0.47] : [0.47, 0.5, 0.55]), alinha, vert: 'meio', largura: alvo, gap: 0.04 };
|
|
1128
|
+
if (c.selo) linha(c.selo, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.16, maiusc: true, eixos: { wght: 500 } });
|
|
1129
|
+
for (const t of fit.linhas) {
|
|
1130
|
+
const l = linha(t, 'tese', { fonte, tamanho: fit.tamanho, eixos, maiusc, riscado: c.riscado, cor: c.riscado ? 'fraco' : 'texto' });
|
|
1131
|
+
if (!destaqueTk.length) l.palavras.forEach((p) => { p.papel = 'normal'; });
|
|
1132
|
+
}
|
|
1133
|
+
const ultima = b.linhas[b.linhas.length - 1];
|
|
1134
|
+
if (c.riscado) b.acento.push({ forma: 'risco', alvo: { linha: ultima.id } });
|
|
1135
|
+
else if (/[.!]$/.test(txt) || s.chance(0.4)) b.acento.push({ forma: 'ponto', alvo: { linha: ultima.id, fim: true } });
|
|
1136
|
+
else {
|
|
1137
|
+
const lt = b.linhas.find((l) => l.papel === 'tese' && l.palavras.some((p) => p.papel === 'tese')) || ultima;
|
|
1138
|
+
const pi = Math.max(0, lt.palavras.findIndex((p) => p.papel === 'tese'));
|
|
1139
|
+
b.acento.push({ forma: 'sublinha', alvo: { linha: lt.id, palavra: lt.palavras.some((p) => p.papel === 'tese') ? pi : null } });
|
|
1140
|
+
}
|
|
1141
|
+
if (s.chance(0.5)) b.linhas.filter((l) => l.papel === 'tese').forEach((l) => l.palavras.forEach((p) => { if (p.papel === 'tese') p.cor = 'acento'; }));
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
case 'pilha': case 'produto': {
|
|
1145
|
+
const noProduto = comp === 'produto';
|
|
1146
|
+
const lg = noProduto ? (deitado ? W * 0.42 : larguraUtil) : (deitado ? W * s.entre(0.62, 0.74) : larguraUtil);
|
|
1147
|
+
const principal = c.tipo === 'numero' ? c.valor : (c.titulo || c.nome || c.nao || c.sim || c.texto || '');
|
|
1148
|
+
const nL = 3;
|
|
1149
|
+
const op = { fonte: fontes.display, wght: wTese };
|
|
1150
|
+
const fit = caberEmLinhas(principal, lg, nL, (deitado ? 0.15 : 0.085) * H * (noProduto ? 0.85 : 1), op);
|
|
1151
|
+
const y = noProduto ? (deitado ? H * 0.52 : H * 0.72) : H * vert(deitado ? [0.42, 0.5, 0.58] : [0.44, 0.52, 0.6]);
|
|
1152
|
+
b.bloco = { x: M, y, alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
|
|
1153
|
+
const rot = c.selo || (c.tipo === 'cta' ? '' : '');
|
|
1154
|
+
if (rot) linha(rot, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.16, maiusc: true, eixos: { wght: 500 }, cor: noProduto ? 'acento' : 'fraco' });
|
|
1155
|
+
for (const t of fit.linhas) {
|
|
1156
|
+
const l = linha(t, 'tese', { tamanho: fit.tamanho, riscado: c.riscado, cor: c.riscado ? 'fraco' : 'texto' });
|
|
1157
|
+
if (!c.riscado) misturarPesos(l, { corTese: !noProduto });
|
|
1158
|
+
}
|
|
1159
|
+
if (c.tipo === 'numero') {
|
|
1160
|
+
const t0 = b.linhas.find((l) => l.papel === 'tese');
|
|
1161
|
+
t0.papel = 'numero';
|
|
1162
|
+
b.contador = { linha: t0.id, ...numeroDe(c.valor), dur: 0.9 };
|
|
1163
|
+
if (c.rotulo) linha(c.rotulo, 'apoio', { tamanho: Math.max(30 * base, fit.tamanho * 0.34), eixos: { wght: 600 } });
|
|
1164
|
+
}
|
|
1165
|
+
const apoio = c.sub || c.link || '';
|
|
1166
|
+
if (apoio) {
|
|
1167
|
+
const fa = caberEmLinhas(apoio, lg, 2, Math.max(26 * base, fit.tamanho * 0.36), { fonte: fontes.texto, wght: 400 });
|
|
1168
|
+
fa.linhas.forEach((t) => linha(t, 'apoio', { fonte: fontes.texto, tamanho: fa.tamanho, eixos: { wght: 400 }, cor: c.link ? 'acento' : 'fraco' }));
|
|
1169
|
+
}
|
|
1170
|
+
const teses = b.linhas.filter((l) => l.papel === 'tese' || l.papel === 'numero');
|
|
1171
|
+
if (c.riscado) b.acento.push({ forma: 'risco', alvo: { linha: teses[teses.length - 1].id } });
|
|
1172
|
+
else {
|
|
1173
|
+
const comTese = teses.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1174
|
+
const forma = s.pesado({ barra: 2, sublinha: comTese ? 2.5 : 0.6, anel: comTese && comTese.palavras.find((p) => p.papel === 'tese').texto.length <= 9 ? 1.6 : 0 });
|
|
1175
|
+
if (forma === 'barra') b.acento.push({ forma: 'barra', alvo: { bloco: true } });
|
|
1176
|
+
else {
|
|
1177
|
+
const lt = comTese || teses[teses.length - 1];
|
|
1178
|
+
b.acento.push({ forma, alvo: { linha: lt.id, palavra: comTese ? lt.palavras.findIndex((p) => p.papel === 'tese') : null } });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// O ThreeCanvas só aceita pixel inteiro; ele ocupa a altura toda, para o chão não cortar.
|
|
1182
|
+
if (noProduto) b.produto3d = deitado ? { x: Math.round(W * 0.42), y: 0, largura: Math.round(W * 0.58), altura: H, escala: 0.95 } : { x: 0, y: -Math.round(H * 0.2), largura: W, altura: H, escala: 0.62 };
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
case 'palavras': {
|
|
1186
|
+
const txt = c.texto || c.titulo || '';
|
|
1187
|
+
const alinha = s.chance(0.55) ? 'esq' : 'centro';
|
|
1188
|
+
const lg = deitado ? W * 0.8 : larguraUtil;
|
|
1189
|
+
const op = { fonte: fontes.display, wght: 600 };
|
|
1190
|
+
const fit = caberEmLinhas(txt, lg, deitado ? 3 : 4, (deitado ? 0.12 : 0.075) * H, op);
|
|
1191
|
+
b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert([0.48, 0.52]), alinha, vert: 'meio', largura: lg, gap: 0.1 };
|
|
1192
|
+
// Sem destaque, a tese é a palavra mais longa que não é função.
|
|
1193
|
+
if (!destaqueTk.length) {
|
|
1194
|
+
const cand = txt.split(/\s+/).filter((w) => !funcao(w)).sort((a, b2) => tokens(b2).join('').length - tokens(a).join('').length)[0];
|
|
1195
|
+
if (cand) destaqueTk.push(...tokens(cand));
|
|
1196
|
+
}
|
|
1197
|
+
for (const t of fit.linhas) { const l = linha(t, 'apoio', { tamanho: fit.tamanho, eixos: { wght: 560 } }); misturarPesos(l, { maiorTese: 1.14 }); }
|
|
1198
|
+
const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1199
|
+
if (lt) { tese(lt); b.acento.push({ forma: s.chance(0.6) ? 'sublinha' : 'anel', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } }); }
|
|
1200
|
+
else b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[b.linhas.length - 1].id, fim: true } });
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
case 'contador': {
|
|
1204
|
+
const n = numeroDe(c.valor);
|
|
1205
|
+
const fonte = s.chance(0.6) ? fontes.cinetica : fontes.display;
|
|
1206
|
+
const eixos = { wght: Math.min(VARIAVEIS[fonte].eixos.wght[1], wTese) };
|
|
1207
|
+
const alinha = 'centro';
|
|
1208
|
+
const lg = deitado ? W * 0.62 : larguraUtil;
|
|
1209
|
+
const tam = tamanhoQueCabe(c.valor, lg * 0.82, (deitado ? 0.36 : 0.17) * H, { fonte, wght: eixos.wght });
|
|
1210
|
+
b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert(deitado ? [0.48, 0.5] : [0.46, 0.5]), alinha, vert: 'meio', largura: lg, gap: 0.08 };
|
|
1211
|
+
const l = linha(c.valor, 'numero', { fonte, tamanho: tam, eixos });
|
|
1212
|
+
const tRot = Math.max(30 * base, Math.min(tam * 0.22, 64 * base));
|
|
1213
|
+
if (c.rotulo) linha(c.rotulo, 'apoio', { tamanho: tRot, eixos: { wght: 600 }, fonte: fontes.display });
|
|
1214
|
+
if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.08, eixos: { wght: 400 } });
|
|
1215
|
+
b.contador = { linha: l.id, ...n, dur: 0.9 };
|
|
1216
|
+
b.acento.push({ forma: c.valor.length <= 4 ? 'anel' : 'sublinha', alvo: { linha: l.id } });
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
case 'cascata': {
|
|
1220
|
+
const lg = deitado ? W * 0.72 : larguraUtil - 40 * base;
|
|
1221
|
+
const itens = c.itens.slice(0, 5);
|
|
1222
|
+
// Fonte larga (Unbounded) aperta a lista: os itens vão na fonte de texto.
|
|
1223
|
+
const fonte = VARIAVEIS[fontes.display].largura > 1.1 ? fontes.texto : fontes.display;
|
|
1224
|
+
const op = { fonte, wght: 560 };
|
|
1225
|
+
const maxT = (deitado ? 0.09 : 0.042) * H;
|
|
1226
|
+
const nL = itens.length <= 3 ? 2 : 1;
|
|
1227
|
+
const tam = Math.min(...itens.map((it) => caberEmLinhas((c.numerada ? '00 ' : '') + it, lg, nL, maxT, op).tamanho));
|
|
1228
|
+
b.bloco = { x: M + 30 * base, y: H * vert(deitado ? [0.5, 0.52] : [0.5, 0.54]), alinha: 'esq', vert: 'meio', largura: lg, gap: 0.42 };
|
|
1229
|
+
if (c.titulo) linha(c.titulo, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.16, maiusc: true, eixos: { wght: 500 } });
|
|
1230
|
+
itens.forEach((it, j) => {
|
|
1231
|
+
const txt = (c.numerada ? String(j + 1).padStart(2, '0') + ' ' : '') + it;
|
|
1232
|
+
quebrar(txt, lg, tam, op, nL).forEach((pedaco, q) => {
|
|
1233
|
+
const l = linha(pedaco, 'item', { tamanho: tam, fonte, eixos: { wght: 560 } });
|
|
1234
|
+
if (q > 0) l.continua = true;
|
|
1235
|
+
if (c.numerada && q === 0) l.palavras[0].papel = 'indice';
|
|
1236
|
+
l.palavras.forEach((p) => { if (p.papel === 'tese') p.papel = 'normal'; });
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
b.acento.push({ forma: 'bala', alvo: { linha: b.linhas.find((l) => l.papel === 'item').id }, segue: true });
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
case 'divisao': {
|
|
1243
|
+
const lado = deitado ? (larguraUtil - M * 0.6) / 2 : larguraUtil;
|
|
1244
|
+
const op = { fonte: fontes.display, wght: wTese };
|
|
1245
|
+
const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.1 : 0.065) * H, { ...op, wght: 400 });
|
|
1246
|
+
const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.12 : 0.08) * H, op);
|
|
1247
|
+
if (deitado) {
|
|
1248
|
+
b.bloco = { x: M, y: H * 0.5, alinha: 'esq', vert: 'meio', largura: lado, gap: 0.1, colunas: [M, M + lado + M * 0.6] };
|
|
1249
|
+
} else {
|
|
1250
|
+
b.bloco = { x: M, y: H * 0.5, alinha: 'esq', vert: 'meio', largura: lado, gap: 0.12, blocos: 2 };
|
|
1251
|
+
}
|
|
1252
|
+
fN.linhas.forEach((t) => { const l = linha(t, 'fraco', { tamanho: fN.tamanho, eixos: { wght: 380 }, cor: 'fraco' }); l.coluna = 0; l.riscado = true; });
|
|
1253
|
+
fS.linhas.forEach((t) => { const l = linha(t, 'tese', { tamanho: fS.tamanho }); l.coluna = 1; misturarPesos(l); });
|
|
1254
|
+
const nao = b.linhas.filter((l) => l.coluna === 0), sim = b.linhas.filter((l) => l.coluna === 1);
|
|
1255
|
+
b.acento.push({ forma: 'risco', alvo: { linha: nao[Math.floor((nao.length - 1) / 2)].id } });
|
|
1256
|
+
const lt = sim.find((l) => l.palavras.some((p) => p.papel === 'tese')) || sim[sim.length - 1];
|
|
1257
|
+
b.acento.push({ forma: 'sublinha', alvo: { linha: lt.id, palavra: lt.palavras.some((p) => p.papel === 'tese') ? lt.palavras.findIndex((p) => p.papel === 'tese') : null } });
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case 'citacao': {
|
|
1261
|
+
const fonte = fontes.serifa;
|
|
1262
|
+
const lg = deitado ? W * 0.78 : larguraUtil;
|
|
1263
|
+
const q = String(c.texto || '').replace(/^["“]+|["”]+$/g, '');
|
|
1264
|
+
const fit = caberEmLinhas(q, lg, deitado ? 4 : 5, (deitado ? 0.12 : 0.065) * H, { fonte, wght: 420 });
|
|
1265
|
+
b.bloco = { x: M, y: H * vert([0.48, 0.52]), alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
|
|
1266
|
+
linha('“', 'aspas', { fonte, tamanho: fit.tamanho * 1.6, eixos: { wght: 800 }, cor: 'acento' });
|
|
1267
|
+
fit.linhas.forEach((t) => { const l = linha(t, 'apoio', { fonte, tamanho: fit.tamanho, eixos: { wght: 420, ...(VARIAVEIS[fonte].eixos.SOFT ? { SOFT: 100, WONK: 1 } : {}) } }); l.palavras.forEach((p) => { if (p.papel === 'tese') { p.wght = 700; } }); });
|
|
1268
|
+
if (c.autor) linha('— ' + c.autor, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.06, eixos: { wght: 400 } });
|
|
1269
|
+
const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1270
|
+
if (lt) { tese(lt); b.acento.push({ forma: 'marca', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese'), ate: lt.palavras.map((p) => p.papel).lastIndexOf('tese') } }); }
|
|
1271
|
+
else b.acento.push({ forma: 'barra', alvo: { bloco: true } });
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
case 'marca': {
|
|
1275
|
+
const nome = c.nome || c.titulo || (ctx.marca.nome || '');
|
|
1276
|
+
const fonte = fontes.display;
|
|
1277
|
+
const lg = deitado ? W * 0.7 : larguraUtil * 0.9;
|
|
1278
|
+
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.22 : 0.13) * H, { fonte, wght: 800 });
|
|
1279
|
+
b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
|
|
1280
|
+
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, VARIAVEIS[fonte].eixos.wght[1]) }, comportamento: 'contorno' }).marca = true;
|
|
1281
|
+
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.2, maiusc: true, eixos: { wght: 400 } });
|
|
1282
|
+
b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[0].id, antes: true } });
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
case 'parede': {
|
|
1286
|
+
const txt = c.titulo || c.nome || '';
|
|
1287
|
+
const fonte = fontes.cinetica;
|
|
1288
|
+
const maiusc = true;
|
|
1289
|
+
const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.2 : 0.1) * H, { fonte, wght: 900, maiusc });
|
|
1290
|
+
b.bloco = { x: W / 2, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: larguraUtil, gap: 0.1 };
|
|
1291
|
+
linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900, VARIAVEIS[fonte].eixos.wght[1]) }, maiusc });
|
|
1292
|
+
b.textura = { texto: (maiusc ? txt.toUpperCase() : txt), tamanho: tam, fonte, linhas: Math.ceil(H / (tam * 1.02)) + 3, velocidade: Math.round(s.entre(40, 90) * base) };
|
|
1293
|
+
b.acento.push({ forma: s.chance(0.5) ? 'moldura' : 'sublinha', alvo: { linha: b.linhas[0].id } });
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
default: throw new Error('composição desconhecida: ' + comp);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// ── o comportamento de cada linha, do que os eixos permitem ─────────
|
|
1300
|
+
/* Um GESTO por grupo: as linhas da mesma frase entram do mesmo jeito (a
|
|
1301
|
+
frase é uma ação só); rótulo, apoio e itens têm o gesto deles. */
|
|
1302
|
+
const gestos = {};
|
|
1303
|
+
for (const l of b.linhas) {
|
|
1304
|
+
if (l.comportamento) continue;
|
|
1305
|
+
const grupo = l.papel === 'numero' ? 'tese' : l.papel;
|
|
1306
|
+
if (gestos[grupo] && l.fonte === gestos[grupo].fonte) { l.comportamento = gestos[grupo].c; continue; }
|
|
1307
|
+
const eixosF = VARIAVEIS[l.fonte] ? VARIAVEIS[l.fonte].eixos : {};
|
|
1308
|
+
const temW = eixosF.wght && eixosF.wght[1] - eixosF.wght[0] >= 400;
|
|
1309
|
+
const temL = !!eixosF.wdth && eixosF.wdth[0] <= 80;
|
|
1310
|
+
let op;
|
|
1311
|
+
/* Sem embaralhar e sem split-flap: leem como defeito ("natural, não
|
|
1312
|
+
estranho", 30/09). Os gestos ficam físicos: peso, largura, máscara. */
|
|
1313
|
+
if (l.papel === 'tese' || l.papel === 'numero') op = { carimbo: 2, peso: temW ? 2.6 : 0, largura: temL ? 2.4 : 0, contorno: 1.3, onda: 1.1, pontos: 1.2, mascara: 1.2, desfoque: 1 };
|
|
1314
|
+
else if (l.papel === 'rotulo') op = { tracking: 1.5, mascara: 2, desfoque: 1.2 };
|
|
1315
|
+
else if (l.papel === 'aspas') op = { carimbo: 1 };
|
|
1316
|
+
else op = { mascara: 2, desfoque: 2.2, tracking: 1.2, onda: 1, peso: temW ? 1.2 : 0 };
|
|
1317
|
+
l.comportamento = s.pesado(op);
|
|
1318
|
+
gestos[grupo] = { c: l.comportamento, fonte: l.fonte };
|
|
1319
|
+
}
|
|
1320
|
+
// A MESMA tese não entra do mesmo jeito da batida anterior (não é template).
|
|
1321
|
+
const tese0 = b.linhas.find((l) => l.papel === 'tese' || l.papel === 'numero');
|
|
1322
|
+
// Nenhum gesto da tese volta antes de três batidas.
|
|
1323
|
+
const mem = ctx.memoria || {};
|
|
1324
|
+
mem.gestos = mem.gestos || [];
|
|
1325
|
+
if (tese0 && !tese0.marca && mem.gestos.slice(-3).includes(tese0.comportamento)) {
|
|
1326
|
+
const livres = ['peso', 'carimbo', 'largura', 'contorno', 'onda', 'pontos', 'mascara', 'desfoque'].filter((g) => !mem.gestos.slice(-3).includes(g)
|
|
1327
|
+
&& (g !== 'largura' || (VARIAVEIS[tese0.fonte] && VARIAVEIS[tese0.fonte].eixos.wdth)) && (g !== 'peso' || (VARIAVEIS[tese0.fonte] && VARIAVEIS[tese0.fonte].eixos.wght)));
|
|
1328
|
+
const novo = livres.length ? s.um(livres) : tese0.comportamento;
|
|
1329
|
+
const velho = tese0.comportamento;
|
|
1330
|
+
b.linhas.filter((l) => (l.papel === 'tese' || l.papel === 'numero') && l.comportamento === velho && !l.marca).forEach((l) => { l.comportamento = novo; });
|
|
1331
|
+
}
|
|
1332
|
+
if (tese0) mem.gestos.push(tese0.comportamento);
|
|
1333
|
+
if (b.contador) b.linhas.find((l) => l.id === b.contador.linha).comportamento = 'peso';
|
|
1334
|
+
|
|
1335
|
+
/* ENCHER: a estimativa de largura erra para fonte larga ou estreita; o
|
|
1336
|
+
componente mede e cresce o grupo principal até a largura do bloco,
|
|
1337
|
+
com o teto vertical daqui (o respiro de 30–40% continua garantido). */
|
|
1338
|
+
// A palavra-herói de ponta a ponta (o CLAUDE do reel): teto alto, a largura manda.
|
|
1339
|
+
const tetoV = { gigante: deitado ? 0.44 : 0.24, marca: deitado ? 0.3 : 0.16, parede: deitado ? 0.26 : 0.13, contador: deitado ? 0.52 : 0.22,
|
|
1340
|
+
pilha: deitado ? 0.22 : 0.12, produto: deitado ? 0.14 : 0.08, palavras: deitado ? 0.12 : 0.08, cascata: deitado ? 0.1 : 0.05, divisao: deitado ? 0.13 : 0.085, citacao: deitado ? 0.12 : 0.07 }[b.comp] || 0.1;
|
|
1341
|
+
// A SANGRIA (a palavra de ponta a ponta) é um momento, não o padrão: até duas por vídeo, só em palavra curta.
|
|
1342
|
+
const memS = ctx.memoria || {};
|
|
1343
|
+
if (['gigante', 'parede'].includes(b.comp) && (memS.cheios || 0) < 2 && b.linhas.filter((l) => l.papel === 'tese').length <= 2
|
|
1344
|
+
&& b.linhas.every((l) => l.texto.length <= 12) && s.chance(0.6)) { b.cheio = true; memS.cheios = (memS.cheios || 0) + 1; }
|
|
1345
|
+
const enche = (l) => ['tese', 'numero', 'item'].includes(l.papel) || (b.comp === 'citacao' && l.papel === 'apoio') || (b.comp === 'palavras' && l.papel === 'apoio');
|
|
1346
|
+
// O bloco inteiro cabe na área segura: o teto por linha divide a altura útil (~72%) pelo número de linhas que enchem.
|
|
1347
|
+
const nEnche = Math.max(1, ...[0, 1].map((col) => b.linhas.filter((l) => enche(l) && (l.coluna == null || l.coluna === col)).length));
|
|
1348
|
+
for (const l of b.linhas) {
|
|
1349
|
+
if (!enche(l)) continue;
|
|
1350
|
+
l.encher = l.papel === 'item' ? 'item' : 'principal';
|
|
1351
|
+
// O respiro do guia: fora dos (no máximo dois) momentos de sangria, o bloco usa ~55% da altura e a tese não encosta em nada.
|
|
1352
|
+
l.max = Math.round(b.cheio ? Math.min(tetoV * H, (H * 0.66) / (nEnche * 1.05)) : Math.min(tetoV * H * 0.72, (H * 0.5) / (nEnche * 1.05)));
|
|
1353
|
+
}
|
|
1354
|
+
// ── o tempo de cada linha e palavra: quando a voz diz ────────────────
|
|
1355
|
+
tempoDaBatida(b, palavras, ctx);
|
|
1356
|
+
// ── o acento: quando cada estado dele começa ─────────────────────────
|
|
1357
|
+
acentoNoTempo(b, ctx);
|
|
1358
|
+
// A linha que continua o item é o mesmo elemento (o item quebrou em duas).
|
|
1359
|
+
b.elementos = b.linhas.filter((l) => !l.continua).length + (b.textura ? 1 : 0) + (b.produto3d ? 1 : 0);
|
|
1360
|
+
b.acentos = contarAcentos(b);
|
|
1361
|
+
return b;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function tempoDaBatida(b, palavras, ctx) {
|
|
1365
|
+
const Q = QUADRO;
|
|
1366
|
+
const fim = b.ate - REGRAS.leitura; // a última entrada deixa 1 s de leitura
|
|
1367
|
+
const ordem = b.linhas.filter((l) => l.papel !== 'aspas');
|
|
1368
|
+
const quando = casar(ordem.map((l) => l.texto), palavras, { de: b.de - 0.05 });
|
|
1369
|
+
// Lista: cada item no instante em que é dito; o resto, em cascata depois do anterior.
|
|
1370
|
+
let t = b.de + 0.12;
|
|
1371
|
+
/* O passo entre o que NÃO é dito: a cascata dá tempo de ler cada item
|
|
1372
|
+
(até 0,7 s); o resto, o stagger do guia (3–6 quadros). */
|
|
1373
|
+
const naoDitos = quando.filter((q) => q == null).length;
|
|
1374
|
+
const janela = Math.max(0, fim - b.de - 0.3);
|
|
1375
|
+
const passo = b.comp === 'cascata' && naoDitos > 1
|
|
1376
|
+
? Math.max(5 * Q, Math.min(0.7, janela / Math.max(1, ordem.length - 1)))
|
|
1377
|
+
: ctx.s.um([3, 4, 5, 6]) * Q;
|
|
1378
|
+
ordem.forEach((l, i) => {
|
|
1379
|
+
if (l.continua && i) { l.t = r3(t + 2 * Q); t = l.t; return; }
|
|
1380
|
+
const falado = quando[i];
|
|
1381
|
+
l.t = r3(Math.max(i === 0 ? b.de + 0.1 : t + REGRAS.stagger[0] * Q, falado != null ? falado : (i === 0 ? b.de + 0.12 : t + passo)));
|
|
1382
|
+
t = l.t;
|
|
1383
|
+
});
|
|
1384
|
+
/* Quadro morto não: a primeira coisa da batida entra em até 0,35 s, mesmo
|
|
1385
|
+
que a voz ainda não a tenha dito (o resto segue a voz). */
|
|
1386
|
+
if (ordem[0] && ordem[0].t > b.de + 0.35) ordem[0].t = r3(b.de + 0.35);
|
|
1387
|
+
for (let i = 1; i < ordem.length; i++) if (ordem[i].t < ordem[i - 1].t + REGRAS.stagger[0] * Q) ordem[i].t = r3(ordem[i - 1].t + REGRAS.stagger[0] * Q);
|
|
1388
|
+
// Nada entra tarde demais para ser lido: de trás para frente, com o stagger mínimo.
|
|
1389
|
+
for (let i = ordem.length - 1; i >= 0; i--) {
|
|
1390
|
+
const teto = i === ordem.length - 1 ? fim : ordem[i + 1].t - (ordem[i + 1].continua ? 2 : REGRAS.stagger[0]) * Q;
|
|
1391
|
+
if (ordem[i].t > teto) ordem[i].t = r3(teto);
|
|
1392
|
+
}
|
|
1393
|
+
for (let i = 0; i < ordem.length; i++) {
|
|
1394
|
+
const piso = i === 0 ? b.de + 0.03 : ordem[i - 1].t + (ordem[i].continua ? 2 : REGRAS.stagger[0]) * Q;
|
|
1395
|
+
if (ordem[i].t < piso) ordem[i].t = r3(piso);
|
|
1396
|
+
}
|
|
1397
|
+
const aspas = b.linhas.find((l) => l.papel === 'aspas');
|
|
1398
|
+
if (aspas) aspas.t = r3(Math.max(b.de + 0.03, ordem[0].t - 4 * Q));
|
|
1399
|
+
/* A FALA, palavra por palavra: na batida falada, a k-ésima palavra na tela
|
|
1400
|
+
é a k-ésima da frase — o tempo dela é o da voz, exato. */
|
|
1401
|
+
if (b.origem === 'falado') {
|
|
1402
|
+
let k = 0;
|
|
1403
|
+
for (const l of b.linhas) {
|
|
1404
|
+
for (const p of l.palavras) { const w = palavras[k++]; if (w) p.t = r3(Math.min(w.de, Math.max(b.de + 0.03, fim))); }
|
|
1405
|
+
l.t = l.palavras[0].t;
|
|
1406
|
+
}
|
|
1407
|
+
for (let i = 1; i < ordem.length; i++) if (ordem[i].t < ordem[i - 1].t + REGRAS.stagger[0] * Q) ordem[i].t = r3(ordem[i - 1].t + REGRAS.stagger[0] * Q);
|
|
1408
|
+
for (const l of b.linhas) l.palavras.forEach((p, j) => { if (j === 0 || p.t < l.t) p.t = Math.max(p.t, l.t); });
|
|
1409
|
+
}
|
|
1410
|
+
// As palavras: a fala faladas na hora; as outras seguem a linha.
|
|
1411
|
+
for (const l of b.linhas) {
|
|
1412
|
+
if (b.origem === 'falado') continue;
|
|
1413
|
+
const qs = b.origem === 'falado' || b.comp === 'palavras' ? casar(l.palavras.map((p) => p.texto), palavras, { de: l.t - 0.05 }) : l.palavras.map(() => null);
|
|
1414
|
+
let tw = l.t;
|
|
1415
|
+
l.palavras.forEach((p, j) => {
|
|
1416
|
+
const alvo = qs[j] != null ? qs[j] : (j === 0 ? l.t : tw + 2 * Q);
|
|
1417
|
+
p.t = r3(Math.min(Math.max(j === 0 ? l.t : tw + 2 * Q, alvo), Math.max(l.t, fim)));
|
|
1418
|
+
tw = p.t;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
// A cascata envelhece: o item anterior perde a cor quando o próximo entra.
|
|
1422
|
+
const itens = b.linhas.filter((l) => l.papel === 'item');
|
|
1423
|
+
const cabecas = itens.filter((l) => !l.continua);
|
|
1424
|
+
itens.forEach((l) => { const prox = cabecas.find((h) => h.t > l.t + 0.001 && itens.indexOf(h) > itens.indexOf(l)); if (prox) l.envelhece = prox.t; });
|
|
1425
|
+
if (b.contador) b.contador.t = b.linhas.find((l) => l.id === b.contador.linha).t;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function acentoNoTempo(b, ctx) {
|
|
1429
|
+
const Q = QUADRO;
|
|
1430
|
+
const [ta, tb] = REGRAS.traco;
|
|
1431
|
+
const dur = () => r3(ctx.s.um([ta, 12, 14, tb]) * Q);
|
|
1432
|
+
const alvoT = (a) => {
|
|
1433
|
+
const l = a.alvo && a.alvo.linha ? b.linhas.find((x) => x.id === a.alvo.linha) : b.linhas.find((x) => x.papel === 'tese') || b.linhas[0];
|
|
1434
|
+
return l;
|
|
1435
|
+
};
|
|
1436
|
+
const fim = b.ate - 0.5;
|
|
1437
|
+
if (b.acento[0] && b.acento[0].segue) {
|
|
1438
|
+
// O marcador anda com a lista: um estado por item.
|
|
1439
|
+
const itens = b.linhas.filter((l) => l.papel === 'item' && !l.continua);
|
|
1440
|
+
const a0 = b.acento[0];
|
|
1441
|
+
b.acento = itens.map((l) => ({ forma: 'bala', alvo: { linha: l.id }, t: r3(Math.max(b.de + 0.05, l.t - 2 * Q)), dur: r3(8 * Q) }));
|
|
1442
|
+
void a0;
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
let prev = b.de;
|
|
1446
|
+
b.acento.forEach((a, i) => {
|
|
1447
|
+
const l = alvoT(a);
|
|
1448
|
+
const base = l ? l.t + (a.forma === 'ponto' && a.alvo.antes ? -0.05 : REGRAS.burst[0] * Q * 0.75) : b.de + 0.2;
|
|
1449
|
+
a.t = r3(Math.min(fim, Math.max(prev + (i ? 0.25 : 0.05), base)));
|
|
1450
|
+
a.dur = dur();
|
|
1451
|
+
prev = a.t + a.dur;
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function contarAcentos(b) {
|
|
1456
|
+
const formas = b.acento.length ? 1 : 0; // o traço é UM, que anda
|
|
1457
|
+
const linhasAcento = b.linhas.filter((l) => l.cor === 'acento').length;
|
|
1458
|
+
const palavrasAcento = b.linhas.reduce((n, l) => n + (l.cor === 'acento' ? 0 : l.palavras.filter((p) => p.cor === 'acento').length ? 1 : 0), 0);
|
|
1459
|
+
let n = formas + linhasAcento + palavrasAcento;
|
|
1460
|
+
// Passou de três: a cor sai primeiro das palavras, depois das linhas de apoio.
|
|
1461
|
+
if (n > REGRAS.acentos) {
|
|
1462
|
+
for (const l of b.linhas) { if (n <= REGRAS.acentos) break; if (l.cor !== 'acento' && l.palavras.some((p) => p.cor === 'acento')) { l.palavras.forEach((p) => { if (p.cor === 'acento') p.cor = null; }); n--; } }
|
|
1463
|
+
for (const l of b.linhas) { if (n <= REGRAS.acentos) break; if (l.cor === 'acento' && l.papel !== 'aspas') { l.cor = 'fraco'; n--; } }
|
|
1464
|
+
}
|
|
1465
|
+
return n;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
module.exports = {
|
|
1469
|
+
montarGerativo, partituraDe, sonsDoVideo, musicaDe, desenharSom, humorDe, corGenerica, BANIDAS, HUMORES, REGRAS, VARIAVEIS, COMPOSICOES, COMPORTAMENTOS, SAIDAS, ACENTOS, FPS,
|
|
1470
|
+
especEixos, sorteador, hash, larguraEm, conteudoDe, casar,
|
|
1471
|
+
};
|