primocode 10.4.3 → 10.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +1 -1
- package/lib/catalogo.js +1 -1
- package/lib/claude-engine.js +1 -1
- package/lib/entrada.js +4 -0
- package/lib/estudio/batida.js +95 -10
- package/lib/estudio/gerativo.js +173 -29
- package/lib/estudio/index.js +53 -5
- package/lib/estudio/kit/gerativo.jsx +42 -4
- package/lib/estudio/kit/tres.jsx +156 -0
- package/lib/local/motor-cli.js +14 -11
- package/lib/tela.js +3 -1
- package/lib/voz.js +2 -0
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +60 -0
- package/voz/primo_voz/orbe.py +444 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.4.
|
|
1
|
+
# PrimoCode v10.4.7
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.4.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.4.7
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -2875,7 +2875,7 @@ async function main() {
|
|
|
2875
2875
|
vozNaTarefa = false;
|
|
2876
2876
|
const ultima = state.history.slice().reverse().find((m) => m.role === 'assistant');
|
|
2877
2877
|
if (ultima && ultima.content) vozEmEspera.falar(ultima.content);
|
|
2878
|
-
}
|
|
2878
|
+
} else if (vozEmEspera && vozEmEspera.estado) vozEmEspera.estado('esperando');
|
|
2879
2879
|
// Recado que chegou tarde demais para a tarefa (ela já tinha
|
|
2880
2880
|
// acabado): vira o próximo pedido, na ordem. Nada se perde.
|
|
2881
2881
|
queue.unshift(...sobrasDaCaixa());
|
package/lib/catalogo.js
CHANGED
|
@@ -93,7 +93,7 @@ const GRUPOS = [
|
|
|
93
93
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
94
94
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
|
|
95
95
|
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
96
|
-
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa
|
|
96
|
+
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; procure com estudio_midia acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
97
|
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (o vídeo GRAVADO em 3D — cada parte é UMA fala {falante, texto, emocao?, destaque?}, sem tipo: personagens modelados — gente ou robô — conversando num set de podcast/estúdio/escritório, a boca segue a voz, gestos, e a câmera corta entre aberto, ombro e close; use quando o pedido fala em personagem, boneco, avatar, podcast, entrevista, ou alguém FALANDO em 3D. Com dois falantes (falante em cada parte) eles conversam; personagens:{"nome":{tipo:robo|pessoa, pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba, rotulo}} — nome de IA vira robô sozinho; emocao feliz|serio|surpreso|pensativo|animado|curioso e destaque:"texto curto" (o cartão na tela) em cada parte; cenario podcast|estudio|escritorio|noite). '
|
|
98
98
|
+ 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
99
99
|
+ 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
package/lib/claude-engine.js
CHANGED
|
@@ -668,6 +668,6 @@ module.exports = {
|
|
|
668
668
|
MODELO_CLAUDE, argsDoModelo, argsDoMotor,
|
|
669
669
|
SKILL_DIR, SKILL_PATH, garantirSkill,
|
|
670
670
|
temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
|
|
671
|
-
garantirEngine, requestAct, emitirDecisao,
|
|
671
|
+
garantirEngine, requestAct, emitirDecisao, rodarClaude,
|
|
672
672
|
_emitirDecisao: emitirDecisao,
|
|
673
673
|
};
|
package/lib/entrada.js
CHANGED
|
@@ -191,6 +191,10 @@ function montar(e) {
|
|
|
191
191
|
if (st.linhas) {
|
|
192
192
|
for (const linha of st.linhas) out.push(' ' + truncate(linha, largura - 2));
|
|
193
193
|
} else {
|
|
194
|
+
/* Uma linha vazia entre o painel e o rodapé. Colado, o rodapé parecia
|
|
195
|
+
SOBREPOSTO ao painel em terminais de entrelinha apertada (o Orca):
|
|
196
|
+
o fundo do painel encostava nas letras da pasta. */
|
|
197
|
+
out.push('');
|
|
194
198
|
const esq = st.esquerda || '';
|
|
195
199
|
const dir = st.direita || '';
|
|
196
200
|
const sobra = largura - 2 - visibleWidth(esq) - visibleWidth(dir);
|
package/lib/estudio/batida.js
CHANGED
|
@@ -141,7 +141,16 @@ const CONFIANCA_MINIMA = 0.7; // abaixo disso a batida é chute: a faixa entra
|
|
|
141
141
|
|
|
142
142
|
const TRANSICOES_FORTES = new Set(['soco', 'flash', 'chicote', 'zoom', 'giro']);
|
|
143
143
|
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Os cortes do plano no tempo do vídeo: [{t, peso}]. Corte de impacto pesa 3; o maior impacto vira `impacto`.
|
|
146
|
+
*
|
|
147
|
+
* O GERATIVO É UMA CENA SÓ: as batidas dele moram dentro da partitura do kit
|
|
148
|
+
* (cena.elementos[].partitura.batidas). Contando só as cenas, o vídeo dele
|
|
149
|
+
* tinha 0 cortes, e o log dizia "0 de 0 cortes caem na batida" (10/10). Cada
|
|
150
|
+
* batida (menos a primeira) é um corte; a troca de campo (fundo de cor) e a
|
|
151
|
+
* tese gigante pesam 3. O corte leva a referência da batida para ser
|
|
152
|
+
* ENCOSTADO na batida da música depois (melhorInicio).
|
|
153
|
+
*/
|
|
145
154
|
function cortesDoPlano(plano) {
|
|
146
155
|
const cortes = [];
|
|
147
156
|
let t = 0;
|
|
@@ -150,35 +159,111 @@ function cortesDoPlano(plano) {
|
|
|
150
159
|
const tr = cena.transicao && (cena.transicao.tipo || cena.transicao);
|
|
151
160
|
cortes.push({ t: r3(t), peso: cena.impacto || TRANSICOES_FORTES.has(tr) ? 3 : 1 });
|
|
152
161
|
}
|
|
162
|
+
for (const el of cena.elementos || []) {
|
|
163
|
+
const bs = el && el.partitura && Array.isArray(el.partitura.batidas) ? el.partitura.batidas : null;
|
|
164
|
+
if (!bs) continue;
|
|
165
|
+
bs.forEach((b, j) => {
|
|
166
|
+
if (j === 0) return;
|
|
167
|
+
const forte = b.troca || b.comp === 'gigante' || b.campoCor;
|
|
168
|
+
// A palavra que a batida mostra: a entrada da primeira linha (sem linha, a batida mesmo).
|
|
169
|
+
const ts = (b.linhas || []).map((l) => l.t).filter((x) => typeof x === 'number');
|
|
170
|
+
const palavra = ts.length ? Math.min(...ts) : b.de;
|
|
171
|
+
cortes.push({ t: r3(t + b.de), peso: forte ? 3 : 1, ref: { bs, j, base: t, palavra } });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
153
174
|
t += Number(cena.duracao) || 0;
|
|
154
175
|
}
|
|
155
|
-
return cortes;
|
|
176
|
+
return cortes.sort((a, b) => a.t - b.t);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A batida mais perto de `t` numa lista ordenada: {bt, d}. */
|
|
180
|
+
function maisPerto(lista, t) {
|
|
181
|
+
let lo = 0, hi = lista.length - 1;
|
|
182
|
+
while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
|
|
183
|
+
const a = lista[lo], b = lo > 0 ? lista[lo - 1] : Infinity;
|
|
184
|
+
return Math.abs(a - t) <= Math.abs(b - t) ? { bt: a, d: Math.abs(a - t) } : { bt: b, d: Math.abs(b - t) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Quanto a troca de tela pode se afastar da PALAVRA que ela mostra: a fala
|
|
188
|
+
continua onde estava, e a troca adianta ou atrasa a palavra em no máximo
|
|
189
|
+
150 ms — abaixo do que o olho percebe como fora de sincronia. A janela é
|
|
190
|
+
medida da palavra, não do corte de antes (que já vinha ~120 ms adiantado). */
|
|
191
|
+
const EMPURRAO = 0.15;
|
|
192
|
+
/** A janela [de, até] em que o corte pode cair, no tempo do vídeo. */
|
|
193
|
+
const janelaDo = (c) => (c.ref ? [c.ref.base + c.ref.palavra - EMPURRAO, c.ref.base + c.ref.palavra + EMPURRAO] : [c.t, c.t]);
|
|
194
|
+
/** A batida da música (no tempo do vídeo) dentro da janela, a mais perto do corte; ou null. */
|
|
195
|
+
function batidaNaJanela(bats, c, o) {
|
|
196
|
+
const [a, z] = janelaDo(c);
|
|
197
|
+
let lo = 0, hi = bats.length;
|
|
198
|
+
while (lo < hi) { const m = (lo + hi) >> 1; if (bats[m] - o < a) lo = m + 1; else hi = m; }
|
|
199
|
+
let melhor = null;
|
|
200
|
+
for (let i = lo; i < bats.length && bats[i] - o <= z; i++) {
|
|
201
|
+
const v = bats[i] - o;
|
|
202
|
+
if (!melhor || Math.abs(v - c.t) < Math.abs(melhor - c.t)) melhor = v;
|
|
203
|
+
}
|
|
204
|
+
return melhor;
|
|
156
205
|
}
|
|
206
|
+
const MIN_BATIDA = 0.6; // nenhuma batida fica mais curta que isto depois do empurrão
|
|
157
207
|
|
|
158
208
|
/**
|
|
159
209
|
* De onde a faixa começa (em s) para o máximo de cortes cair numa batida.
|
|
160
210
|
* Procura em passos de 10 ms, de 0 a `maximo`; empate fica com o início mais
|
|
161
211
|
* cedo. O corte mais pesado ganha bônus se cair num acento.
|
|
212
|
+
*
|
|
213
|
+
* Os cortes do gerativo (com `ref`) contam como "na batida" até EMPURRAO de
|
|
214
|
+
* distância, porque depois da escolha eles são ENCOSTADOS nela: a batida do
|
|
215
|
+
* vídeo anda (b.de e o ate da anterior), e o golpe da tese também. O plano é
|
|
216
|
+
* mudado no lugar — é ele que vai para o render.
|
|
162
217
|
*/
|
|
163
218
|
function melhorInicio(cortes, analise, { maximo = 0, tolerancia = 0.05 } = {}) {
|
|
164
219
|
const bats = (analise && analise.batidas) || [];
|
|
165
220
|
if (!cortes.length || bats.length < 4 || (analise.confianca != null && analise.confianca < CONFIANCA_MINIMA)) return { inicio: 0, alinhados: 0, total: cortes.length };
|
|
166
|
-
const perto = (lista, t) =>
|
|
167
|
-
let lo = 0, hi = lista.length - 1;
|
|
168
|
-
while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
|
|
169
|
-
return Math.min(Math.abs(lista[lo] - t), lo > 0 ? Math.abs(lista[lo - 1] - t) : Infinity);
|
|
170
|
-
};
|
|
221
|
+
const perto = (lista, t) => maisPerto(lista, t).d;
|
|
171
222
|
const maior = cortes.reduce((a, b) => (b.peso > a.peso ? b : a), cortes[0]);
|
|
172
223
|
const acentos = (analise.acentos || []);
|
|
173
224
|
let melhor = { inicio: 0, pontos: -1 };
|
|
174
225
|
for (let o = 0; o <= Math.max(0, maximo) + 1e-9; o += 0.01) {
|
|
175
226
|
let pontos = 0;
|
|
176
|
-
for (const c of cortes)
|
|
227
|
+
for (const c of cortes) {
|
|
228
|
+
if (c.ref) {
|
|
229
|
+
const v = batidaNaJanela(bats, c, o);
|
|
230
|
+
if (v != null) pontos += c.peso * (1 - 0.3 * Math.min(1, Math.abs(v - c.t) / (2 * EMPURRAO)));
|
|
231
|
+
} else pontos += c.peso * Math.exp(-0.5 * Math.pow(perto(bats, c.t + o) / tolerancia, 2));
|
|
232
|
+
}
|
|
177
233
|
if (maior.peso > 1 && acentos.length && perto(acentos, maior.t + o) < 0.1) pontos += 2;
|
|
178
234
|
if (pontos > melhor.pontos + 1e-6) melhor = { inicio: r3(o), pontos };
|
|
179
235
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
236
|
+
const o = melhor.inicio;
|
|
237
|
+
// O empurrão: cada corte do gerativo vai para a batida mais perto, se couber.
|
|
238
|
+
let empurrados = 0;
|
|
239
|
+
for (const c of cortes) {
|
|
240
|
+
if (!c.ref) continue;
|
|
241
|
+
const { bs, j, base } = c.ref;
|
|
242
|
+
const v = batidaNaJanela(bats, c, o);
|
|
243
|
+
if (v == null || Math.abs(v - c.t) <= 1e-3) continue;
|
|
244
|
+
const delta = v - c.t;
|
|
245
|
+
const b = bs[j], a = bs[j - 1];
|
|
246
|
+
const novo = b.de + delta;
|
|
247
|
+
if (novo - a.de < MIN_BATIDA || b.ate - novo < MIN_BATIDA) continue;
|
|
248
|
+
const colado = Math.abs(a.ate - b.de) < 0.02;
|
|
249
|
+
b.de = r3(novo);
|
|
250
|
+
if (colado) a.ate = b.de;
|
|
251
|
+
c.t = r3(base + b.de);
|
|
252
|
+
empurrados++;
|
|
253
|
+
}
|
|
254
|
+
// O golpe (a tese que bate) também cai na batida, sem sair da batida dele.
|
|
255
|
+
const vistos = new Set();
|
|
256
|
+
for (const c of cortes) {
|
|
257
|
+
if (!c.ref || vistos.has(c.ref.bs)) continue;
|
|
258
|
+
vistos.add(c.ref.bs);
|
|
259
|
+
for (const b of c.ref.bs) for (const g of b.golpes || []) {
|
|
260
|
+
const { bt, d } = maisPerto(bats, c.ref.base + g.t + o);
|
|
261
|
+
const alvo = bt - o - c.ref.base;
|
|
262
|
+
if (d <= EMPURRAO && alvo > b.de + 0.05 && alvo < b.ate - 0.1) g.t = r3(alvo);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const alinhados = cortes.filter((c) => perto(bats, c.t + o) <= tolerancia).length;
|
|
266
|
+
return { inicio: o, alinhados, total: cortes.length, empurrados };
|
|
182
267
|
}
|
|
183
268
|
|
|
184
269
|
module.exports = { analisarPCM, analisar, cortesDoPlano, melhorInicio, TAXA, CONFIANCA_MINIMA };
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -265,7 +265,7 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
|
|
|
265
265
|
roteiro É do assunto (uma palavra só não basta). UMA forma por vídeo —
|
|
266
266
|
é identidade, não sorteio por batida — ou 'nenhum', que a memória também gira. */
|
|
267
267
|
const forca = (ABSTRATO_DO_HUMOR[humor] || 0) * Math.min(1, n[humor] / 3);
|
|
268
|
-
const h1 = forca > 0 ? variedade.sortear([...HEROIS_OBJETO, ...HEROIS_CAMPO, 'nenhum'], 'heroi-gerativo', { aleatorio, adequacao: (h) => (h === 'nenhum' ? 1 - forca : forca * fitH(h)) }) : 'nenhum';
|
|
268
|
+
const h1 = forca > 0 ? variedade.sortear([...HEROIS_3D, ...HEROIS_OBJETO, ...HEROIS_CAMPO, 'nenhum'], 'heroi-gerativo', { aleatorio, adequacao: (h) => (h === 'nenhum' ? 1 - forca : HEROIS_3D.includes(h) ? forca * 1.3 : forca * fitH(h)) }) : 'nenhum';
|
|
269
269
|
out.herois = { favoritos: h1 === 'nenhum' ? [] : [h1] };
|
|
270
270
|
// 'produto' e 'marca' quem decide é o conteúdo (o 3D pedido, a marca), não o gosto do projeto.
|
|
271
271
|
const favC = [];
|
|
@@ -274,7 +274,10 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
|
|
|
274
274
|
/* A MOLDURA (09/10): o quadro fixo do reel de referência — cantos, o assunto
|
|
275
275
|
no alto, a seção, o tempo embaixo — é identidade, mas em todo vídeo vira
|
|
276
276
|
vício. Sai do cardápio com memória: 'sem' pesa um pouco mais. */
|
|
277
|
-
|
|
277
|
+
/* (10/10) "Essa borda entrega que foi IA que fez" (Joel): os cantos, o
|
|
278
|
+
"01 · seção" e o tempo embaixo são a assinatura de vídeo gerado. Saiu
|
|
279
|
+
do sorteio: só entra quando o roteiro PEDE (moldura: cantos|completa). */
|
|
280
|
+
out.moldura = 'sem';
|
|
278
281
|
// O bokeh combina com luz de tela (tecnologia, energia, natureza); numa padaria, quase nunca.
|
|
279
282
|
const bokeh = ['tecnologia', 'energia', 'natureza'].includes(humor) ? 0.6 : 0.2;
|
|
280
283
|
out.ambiente = variedade.sortear(AMBIENTES, 'ambiente-gerativo', { aleatorio, adequacao: { limpo: 1, luz: 0.85, bokeh } });
|
|
@@ -298,6 +301,18 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
|
|
|
298
301
|
O assunto decide quanto 3D: tecnologia e energia puxam os objetos (esfera,
|
|
299
302
|
toro, polígono, explosão); editorial e luxo, os campos. */
|
|
300
303
|
const HEROIS_OBJETO = ['explosao', 'poligono', 'esfera', 'toro'], HEROIS_CAMPO = ['truchet', 'aneis', 'grade', 'rastro'];
|
|
304
|
+
/* O 3D DE VERDADE (10/10, "quero um 3d bem presente", com o reel de
|
|
305
|
+
referência): three.js, perspectiva, luz — grande na tela, não um enfeite.
|
|
306
|
+
globo3d: a esfera de pontos e malha girando em profundidade; fita3d: o nó
|
|
307
|
+
que se torce, com material e luz de recorte; colunas3d: o campo de colunas
|
|
308
|
+
que a câmera sobrevoa, atrás do texto. (kit/tres.jsx, Heroi3D) */
|
|
309
|
+
const HEROIS_3D = ['globo3d', 'fita3d', 'colunas3d'];
|
|
310
|
+
const CAMPOS_3D = ['colunas3d'];
|
|
311
|
+
/* A FAMÍLIA de cada herói: dois heróis da mesma família seguidos são o
|
|
312
|
+
"mesmo desenho" para quem assiste (o anel de pontos com a forma no meio
|
|
313
|
+
saiu em 4 batidas seguidas no vídeo do Joel, 10/10). */
|
|
314
|
+
const FAMILIA_HEROI = { explosao: 'raio', poligono: 'orbita', aneis: 'orbita', esfera: 'malha', toro: 'malha', globo3d: 'malha',
|
|
315
|
+
fita3d: 'fita', colunas3d: 'campo3d', truchet: 'azulejo', grade: 'grade', rastro: 'rastro' };
|
|
301
316
|
const OBJETO3D_DO_HUMOR = { tecnologia: 1, energia: 1, natureza: 0.8, luxo: 0.6, editorial: 0.4 };
|
|
302
317
|
/* Quanto o assunto aceita forma abstrata (o resto: a tipografia e o ícone do conteúdo). */
|
|
303
318
|
const ABSTRATO_DO_HUMOR = { tecnologia: 1, energia: 0.7, natureza: 0.5, luxo: 0, editorial: 0 };
|
|
@@ -327,7 +342,7 @@ function escolhasValidas(e) {
|
|
|
327
342
|
texto: todas('textos').includes(e.texto) ? e.texto : null,
|
|
328
343
|
mono: todas('monos').includes(e.mono) ? e.mono : null,
|
|
329
344
|
genero: todas('generos').includes(e.genero) ? e.genero : null,
|
|
330
|
-
herois: (e.herois && Array.isArray(e.herois.favoritos) ? e.herois.favoritos : []).filter((h) => HEROIS_OBJETO.includes(h) || HEROIS_CAMPO.includes(h)),
|
|
345
|
+
herois: (e.herois && Array.isArray(e.herois.favoritos) ? e.herois.favoritos : []).filter((h) => HEROIS_OBJETO.includes(h) || HEROIS_CAMPO.includes(h) || HEROIS_3D.includes(h)),
|
|
331
346
|
composicoes: (e.composicoes && Array.isArray(e.composicoes.favoritas) ? e.composicoes.favoritas : []).filter((c) => COMPOSICOES.includes(c)),
|
|
332
347
|
icones: e.icones && ARRANJOS_ICONE.includes(e.icones.arranjo) ? {
|
|
333
348
|
arranjo: e.icones.arranjo,
|
|
@@ -570,6 +585,9 @@ function frasesDe(fala, palavras) {
|
|
|
570
585
|
|
|
571
586
|
// ── as cores ────────────────────────────────────────────────────────────
|
|
572
587
|
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; };
|
|
588
|
+
/** A luminância relativa do WCAG (com o gama) e o contraste por ela. */
|
|
589
|
+
const lumWcag = (h) => { const n = parseInt(String(h).slice(1), 16); const c = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; };
|
|
590
|
+
const contrasteWcag = (a, b) => { const la = lumWcag(a) + 0.05, lb = lumWcag(b) + 0.05; return la > lb ? la / lb : lb / la; };
|
|
573
591
|
const contraste = (a, b) => { const la = luz(a) + 0.05, lb = luz(b) + 0.05; return la > lb ? la / lb : lb / la; };
|
|
574
592
|
const PALETAS = HUMORES.tecnologia.paletas;
|
|
575
593
|
|
|
@@ -999,6 +1017,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
999
1017
|
const comp = nome === 'rajada' ? 'gigante' : nome === 'duelo' ? 'pilha' : nome;
|
|
1000
1018
|
let q = p;
|
|
1001
1019
|
if (comp === anterior || evita.includes(comp)) q = 0;
|
|
1020
|
+
// No máximo 2 vezes no vídeo (10/10: "cascata" saiu 3 vezes no vídeo do Joel).
|
|
1021
|
+
if ((usadas[comp] || 0) >= 2) q = 0;
|
|
1002
1022
|
if (recentes.slice(-3).includes(comp)) q *= 0.35;
|
|
1003
1023
|
q *= Math.pow(0.72, usadas[comp] || 0);
|
|
1004
1024
|
if (comp === 'parede' && (usadas.parede || 0) >= 1) q *= 0.25;
|
|
@@ -1033,6 +1053,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1033
1053
|
if (bi === alvo3d && !usadas.produto) nome = 'produto';
|
|
1034
1054
|
const ctx = { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes, P, palavras: br.palavras || c.palavras || [], parte: br.parte, marca, roteiro, memoria };
|
|
1035
1055
|
if (nome === 'rajada' && br.ate - br.de < 1.6 * c.itens.length) nome = 'cascata';
|
|
1056
|
+
// A cascata que veio da rajada também respeita o teto de 2 (e nunca segue outra cascata).
|
|
1057
|
+
if (nome === 'cascata' && ((usadas.cascata || 0) >= 2 || anterior === 'cascata')) nome = anterior === 'pilha' ? 'palavras' : 'pilha';
|
|
1036
1058
|
if (nome === 'rajada') {
|
|
1037
1059
|
// Cada item vira a sua batida; o título da lista fica como rótulo.
|
|
1038
1060
|
const n = c.itens.length;
|
|
@@ -1075,6 +1097,45 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1075
1097
|
batidas.forEach((b) => { b.secao = secoes.idx[b.parte] || 0; b.campo = b.secao % 2 === 0 ? comeca : 2 - comeca; });
|
|
1076
1098
|
for (const b of batidas) { b.fundo = 'campo' + b.campo; b.cores = coresDoCampo(campos, b.campo, s, ident); }
|
|
1077
1099
|
|
|
1100
|
+
/* ── OS CAMPOS DE COR CHAPADA (10/10) ────────────────────────────────
|
|
1101
|
+
"O vídeo fica bege e grafite do começo ao fim" — o reel de referência
|
|
1102
|
+
troca o quadro INTEIRO para laranja, azul elétrico, lima. Os campos 3 e
|
|
1103
|
+
4 tinham saído em 09/10 porque entravam a esmo ("cores nada a ver uma
|
|
1104
|
+
com a outra"); voltam com regra: só DUAS cores fortes — o acento do
|
|
1105
|
+
vídeo e a ousada do mesmo sistema (campos[3]) —, em ~1 de cada 3,5
|
|
1106
|
+
batidas (pelo menos 2 num vídeo de 6+), nunca duas seguidas, nunca a
|
|
1107
|
+
primeira nem a marca. Tinta de contraste alto; o corte é seco (o fundo
|
|
1108
|
+
troca no quadro da batida, que o batida.js encosta na música). */
|
|
1109
|
+
// A segunda cor é a ousada do sistema MAIS LONGE no matiz do acento: duas laranjas lado a lado
|
|
1110
|
+
// (o terracota e o #FF4A1C, render real de 10/10) não fazem o contraste do reel (laranja × azul).
|
|
1111
|
+
const matiz = (h) => { const n = parseInt(String(h).slice(1), 16); const r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, bb = (n & 255) / 255;
|
|
1112
|
+
const mx = Math.max(r, g, bb), mn = Math.min(r, g, bb), d = mx - mn; if (!d) return 0;
|
|
1113
|
+
const hh = mx === r ? ((g - bb) / d) % 6 : mx === g ? (bb - r) / d + 2 : (r - g) / d + 4; return (hh * 60 + 360) % 360; };
|
|
1114
|
+
const longe = (x) => { const d = Math.abs(matiz(x) - matiz(ident.acento)); return Math.min(d, 360 - d); };
|
|
1115
|
+
const ousada = [campos[3], campos[4]].filter(ehCor).sort((x, y) => longe(y) - longe(x))[0];
|
|
1116
|
+
const fortes = [ident.acento, ousada].filter((x, i, a) => ehCor(x) && a.indexOf(x) === i);
|
|
1117
|
+
const nCor = batidas.length >= 6 ? Math.max(2, Math.round(batidas.length / 3.5)) : batidas.length >= 4 ? 1 : 0;
|
|
1118
|
+
const candCor = batidas.map((b, i) => i).filter((i) => i > 0 && !['marca', 'produto'].includes(batidas[i].comp));
|
|
1119
|
+
const pesoCor = { gigante: 3, palavras: 2.5, contador: 2.5, pilha: 2, parede: 1.5, divisao: 1, cascata: 0.8, citacao: 0.8 };
|
|
1120
|
+
const comCor = [];
|
|
1121
|
+
for (const i of candCor.sort((x, y) => (pesoCor[batidas[y].comp] || 1) - (pesoCor[batidas[x].comp] || 1) || x - y)) {
|
|
1122
|
+
if (comCor.length >= nCor || !fortes.length) break;
|
|
1123
|
+
if (comCor.some((j) => Math.abs(j - i) < 2)) continue;
|
|
1124
|
+
comCor.push(i);
|
|
1125
|
+
}
|
|
1126
|
+
comCor.sort((x, y) => x - y).forEach((i, k) => {
|
|
1127
|
+
const b = batidas[i];
|
|
1128
|
+
const fundo = fortes[k % fortes.length], outra = fortes[(k + 1) % fortes.length];
|
|
1129
|
+
/* A tinta pelo contraste WCAG (luminância relativa, com o gama): a `luz`
|
|
1130
|
+
daqui acha o azul elétrico "médio" e punha texto PRETO nele — o olho
|
|
1131
|
+
lê muito melhor o branco. */
|
|
1132
|
+
const tinta = [...ident.chaos, '#0B0B0B', '#FAFAF7'].sort((x, y) => contrasteWcag(y, fundo) - contrasteWcag(x, fundo))[0];
|
|
1133
|
+
const acento = outra && outra !== fundo && contrasteWcag(outra, fundo) >= 2.6 ? outra : tinta;
|
|
1134
|
+
b.cores = { fundo, texto: tinta, fraco: misturarCor(tinta, fundo, 0.32), acento, contorno: misturarCor(tinta, fundo, 0.5) };
|
|
1135
|
+
b.campoCor = true;
|
|
1136
|
+
b.fundo = 'cor';
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1078
1139
|
/* ── O HERÓI DE CADA BATIDA VEM DO CONTEÚDO (09/10): "é uma conversa sobre
|
|
1079
1140
|
pão e ele vai naquela bolinha" (Joel). Quem diz é o roteiro: parte.heroi
|
|
1080
1141
|
(ou parte.icone) — um ícone do Iconify ("mdi:bread-slice", o desenho
|
|
@@ -1084,7 +1145,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1084
1145
|
e a forma abstrata só no assunto abstrato, a escolhida do projeto, em
|
|
1085
1146
|
até 1 de cada 3 batidas. Nada serve: a tipografia carrega a batida. ── */
|
|
1086
1147
|
const pedidoDe = partes.map((p) => { const h = p.heroi != null ? p.heroi : p.icone; return typeof h === 'string' && h.trim() ? h.trim() : null; });
|
|
1087
|
-
const ABSTRATOS = [...HEROIS_OBJETO, ...HEROIS_CAMPO];
|
|
1148
|
+
const ABSTRATOS = [...HEROIS_OBJETO, ...HEROIS_CAMPO, ...HEROIS_3D];
|
|
1088
1149
|
const ICONES3D = new Set(Object.values(ICONE_DE));
|
|
1089
1150
|
const materialSorteado = s.um(['argila', 'verniz', 'metal']);
|
|
1090
1151
|
// O arranjo do projeto (escolherVisual); sem ele, o de antes: um lado só, ícones seguidos.
|
|
@@ -1135,24 +1196,100 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1135
1196
|
ultimoIcone = bi;
|
|
1136
1197
|
}
|
|
1137
1198
|
|
|
1138
|
-
|
|
1199
|
+
/* ── A FORMA ABSTRATA, BATIDA A BATIDA (10/10) ───────────────────────
|
|
1200
|
+
Era UMA forma por vídeo, repetida a cada duas batidas: o anel com a
|
|
1201
|
+
forma no meio saiu em 4 batidas seguidas e "as animações são muito
|
|
1202
|
+
iguais" (Joel). Agora cada batida que recebe forma tira a sua de um
|
|
1203
|
+
cardápio, com três regras:
|
|
1204
|
+
1. nunca a mesma FAMÍLIA (FAMILIA_HEROI) em duas batidas seguidas;
|
|
1205
|
+
2. cada forma no máximo 2 vezes no vídeo (uma repetição);
|
|
1206
|
+
3. no assunto que aceita (tecnologia, energia, natureza), pelo menos
|
|
1207
|
+
1 de cada 3 batidas é 3D DE VERDADE (HEROIS_3D) — grande, com
|
|
1208
|
+
perspectiva e luz, não o enfeite pequeno de antes.
|
|
1209
|
+
A forma pedida pelo roteiro (parte.heroi) continua valendo onde pediu. */
|
|
1139
1210
|
const OBJETOS = HEROIS_OBJETO;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1211
|
+
/* O assunto aceita forma abstrata quando o humor é de tecnologia, energia
|
|
1212
|
+
ou natureza E o roteiro É do assunto (3 palavras do humor, como na
|
|
1213
|
+
escolha do projeto) — a padaria que fala de Pix não vira reel de tech. */
|
|
1214
|
+
const acertos = acertosDeHumor(roteiro)[humor] || 0;
|
|
1215
|
+
// E o projeto escolheu ter forma (escolherVisual, E.herois): sem essa escolha, o herói vem só do conteúdo.
|
|
1216
|
+
const aceita = (E.herois || []).length > 0 && (ABSTRATO_DO_HUMOR[humor] || 0) >= 0.5 && acertos >= 3;
|
|
1217
|
+
const nB = batidas.length;
|
|
1218
|
+
const usoH = {}, usoFam = {};
|
|
1219
|
+
let famAnterior = null, ultimoH = -9;
|
|
1220
|
+
// A parede de texto e a marca já cobrem o quadro: forma ali é ruído.
|
|
1221
|
+
const livreParaHeroi = (b) => !b.icone3d && !b.produto3d && !pedidoDe[b.parte] && b.comp !== 'marca' && b.comp !== 'parede';
|
|
1222
|
+
const elegiveis = aceita ? batidas.map((b, i) => (livreParaHeroi(b) ? i : -1)).filter((i) => i >= 0) : [];
|
|
1223
|
+
// Quantas batidas ganham forma: metade do vídeo (as outras ficam com a tipografia e o ícone).
|
|
1224
|
+
const quantas = Math.min(elegiveis.length, Math.ceil(nB / 2));
|
|
1225
|
+
const escolhidas = new Set();
|
|
1226
|
+
for (let k = 0; k < quantas; k++) escolhidas.add(elegiveis[Math.floor((k * elegiveis.length) / quantas)]);
|
|
1227
|
+
const meta3d = aceita ? Math.min(quantas, Math.ceil(nB / 3)) : 0;
|
|
1228
|
+
let n3d = 0, feitas = 0;
|
|
1229
|
+
const favoritos = new Set(E.herois || []);
|
|
1230
|
+
const sortearHeroi = (b, i, so3d) => {
|
|
1231
|
+
const lista = so3d ? HEROIS_3D : [...HEROIS_3D, ...HEROIS_OBJETO, 'truchet', 'aneis', 'rastro'];
|
|
1232
|
+
const w = {};
|
|
1233
|
+
const pode = (h) => w[h] > 0;
|
|
1234
|
+
for (const h of lista.concat([...favoritos].filter((f) => !lista.includes(f) && (!so3d || HEROIS_3D.includes(f))))) {
|
|
1235
|
+
if ((usoH[h] || 0) >= 2) continue;
|
|
1236
|
+
const fam = FAMILIA_HEROI[h];
|
|
1237
|
+
if (fam === famAnterior && i - ultimoH <= 2) continue;
|
|
1238
|
+
// Cada família uma vez no vídeo: a esfera plana depois do globo 3D lia como repetição.
|
|
1239
|
+
if ((usoFam[fam] || 0) >= 1) continue;
|
|
1240
|
+
// As formas planas que pedem o vazio ao lado não cabem na batida sem vazio.
|
|
1241
|
+
if (OBJETOS.includes(h) && ['gigante', 'parede', 'divisao'].includes(b.comp)) continue;
|
|
1242
|
+
// O campo 3D e a parede de texto brigam: as duas cobrem o quadro.
|
|
1243
|
+
if ((CAMPOS_3D.includes(h) || HEROIS_CAMPO.includes(h)) && b.comp === 'parede') continue;
|
|
1244
|
+
// O 3D que mora AO LADO do texto (globo, fita) precisa de um lado livre: a tese gigante,
|
|
1245
|
+
// o contraste e a citação ocupam a largura — ali ele não teria onde ficar sem cruzar a letra.
|
|
1246
|
+
if (HEROIS_3D.includes(h) && !CAMPOS_3D.includes(h) && ['gigante', 'divisao', 'citacao', 'contador'].includes(b.comp)) continue;
|
|
1247
|
+
let p = HEROIS_3D.includes(h) ? 1.6 : 1;
|
|
1248
|
+
if (favoritos.has(h)) p *= 2;
|
|
1249
|
+
p *= Math.pow(0.4, usoH[h] || 0);
|
|
1250
|
+
w[h] = p;
|
|
1251
|
+
}
|
|
1252
|
+
// A forma que o projeto escolheu é a identidade: entra na primeira batida em que cabe.
|
|
1253
|
+
const fav = [...favoritos].find((f) => !usoH[f] && pode(f));
|
|
1254
|
+
if (fav) return fav;
|
|
1255
|
+
// A batida pede 3D e a escolhida é plana: vem a versão 3D da mesma família (o toro vira o globo).
|
|
1256
|
+
if (so3d) {
|
|
1257
|
+
const irma = [...favoritos].map((f) => HEROIS_3D.find((h) => FAMILIA_HEROI[h] === FAMILIA_HEROI[f])).find((h) => h && pode(h));
|
|
1258
|
+
if (irma) return irma;
|
|
1259
|
+
}
|
|
1260
|
+
return s.pesado(w);
|
|
1261
|
+
};
|
|
1143
1262
|
batidas.forEach((b, i) => {
|
|
1144
1263
|
let h = b.heroiPedido || null;
|
|
1145
|
-
if (!h && favorito && !b.icone3d && !b.produto3d && !pedidoDe[b.parte] && nH < tetoH && i - ultimoH >= 2
|
|
1146
|
-
&& !(OBJETOS.includes(favorito) && ['gigante', 'parede', 'divisao'].includes(b.comp))) h = favorito;
|
|
1147
1264
|
delete b.heroiPedido;
|
|
1265
|
+
if (!h && escolhidas.has(i)) {
|
|
1266
|
+
const faltam3d = meta3d - n3d, restam = quantas - feitas;
|
|
1267
|
+
h = sortearHeroi(b, i, faltam3d > 0 && faltam3d >= restam) || sortearHeroi(b, i, false);
|
|
1268
|
+
feitas++;
|
|
1269
|
+
}
|
|
1148
1270
|
if (!h) return;
|
|
1149
|
-
|
|
1271
|
+
usoH[h] = (usoH[h] || 0) + 1;
|
|
1272
|
+
usoFam[FAMILIA_HEROI[h] || h] = (usoFam[FAMILIA_HEROI[h] || h] || 0) + 1;
|
|
1273
|
+
famAnterior = FAMILIA_HEROI[h] || h; ultimoH = i;
|
|
1274
|
+
const tres = HEROIS_3D.includes(h);
|
|
1275
|
+
if (tres) n3d++;
|
|
1150
1276
|
const comCor = b.acentos < REGRAS.acentos;
|
|
1151
1277
|
if (comCor) b.acentos += 1;
|
|
1152
|
-
b.heroi = { tipo: h, modo:
|
|
1153
|
-
|
|
1278
|
+
b.heroi = { tipo: h, modo: CAMPOS_3D.includes(h) ? 'campo' : tres ? 'objeto3d' : OBJETOS.includes(h) ? 'objeto' : 'campo',
|
|
1279
|
+
tres, semente: hash(b.id + h) % 9973,
|
|
1280
|
+
cor: comCor ? b.cores.acento : misturarCor(b.cores.texto, b.cores.fundo, 0.25), cor2: b.cores.texto, fundo: b.cores.fundo };
|
|
1154
1281
|
});
|
|
1155
1282
|
|
|
1283
|
+
/* O 3D COMO ASSUNTO (10/10, "uma coisa gigante por tela"): na batida com
|
|
1284
|
+
globo ou fita, o texto vai para a metade esquerda (no 9:16, para o alto)
|
|
1285
|
+
e a outra metade fica inteira para o 3D — que o kit faz do tamanho do
|
|
1286
|
+
vão (até ~60% da altura), sem nunca cruzar a letra. */
|
|
1287
|
+
for (const b of batidas) {
|
|
1288
|
+
if (!b.heroi || b.heroi.modo !== 'objeto3d' || !b.bloco || b.bloco.colunas || b.bloco.blocos) continue;
|
|
1289
|
+
if (deitado) b.bloco = { ...b.bloco, x: M, alinha: 'esq', largura: Math.min(b.bloco.largura, W * 0.46) };
|
|
1290
|
+
else b.bloco = { ...b.bloco, y: H * 0.3, largura: Math.min(b.bloco.largura, larguraUtil) };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1156
1293
|
// ── as passagens: como uma batida vira a outra ──────────────────────
|
|
1157
1294
|
let ultimaSaida = null, desdeCorte = 9;
|
|
1158
1295
|
const saidasRecentes = [];
|
|
@@ -1361,9 +1498,12 @@ function compor(comp, br, ctx) {
|
|
|
1361
1498
|
const fonte = s.chance(0.5) ? fontes.cinetica : fontes.display;
|
|
1362
1499
|
const eixos = { wght: Math.min(VARIAVEIS[fonte].eixos.wght[1], wTese) };
|
|
1363
1500
|
const op = { fonte, wght: eixos.wght, maiusc };
|
|
1364
|
-
|
|
1501
|
+
// A frase de 3+ palavras pode ir em 3 linhas no 16:9: cresce na altura em vez de encolher na largura.
|
|
1502
|
+
const nMax = deitado ? Math.min(3, Math.max(2, txt.split(/\s+/).length)) : Math.min(4, txt.split(/\s+/).length);
|
|
1365
1503
|
const alvo = deitado ? larguraUtil * 0.96 : larguraUtil;
|
|
1366
|
-
|
|
1504
|
+
/* UMA COISA GIGANTE POR TELA (10/10, comparado ao reel de referência: "o
|
|
1505
|
+
texto pequeno no meio do bege"). A tese gigante enche a largura. */
|
|
1506
|
+
const fit = caberEmLinhas(txt, alvo, nMax, (deitado ? 0.42 : 0.26) * H, op);
|
|
1367
1507
|
const alinha = deitado && s.chance(0.35) ? 'esq' : 'centro';
|
|
1368
1508
|
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 };
|
|
1369
1509
|
if (c.selo) linha(c.selo, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.16, maiusc: true, eixos: { wght: 500 } });
|
|
@@ -1384,11 +1524,11 @@ function compor(comp, br, ctx) {
|
|
|
1384
1524
|
}
|
|
1385
1525
|
case 'pilha': case 'produto': {
|
|
1386
1526
|
const noProduto = comp === 'produto';
|
|
1387
|
-
const lg = noProduto ? (deitado ? W * 0.42 : larguraUtil) : (deitado ? W * s.entre(0.
|
|
1527
|
+
const lg = noProduto ? (deitado ? W * 0.42 : larguraUtil) : (deitado ? W * s.entre(0.74, 0.84) : larguraUtil);
|
|
1388
1528
|
const principal = c.tipo === 'numero' ? c.valor : (c.titulo || c.nome || c.nao || c.sim || c.texto || '');
|
|
1389
1529
|
const nL = 3;
|
|
1390
1530
|
const op = { fonte: fontes.display, wght: wTese };
|
|
1391
|
-
const fit = caberEmLinhas(principal, lg, nL, (deitado ? 0.
|
|
1531
|
+
const fit = caberEmLinhas(principal, lg, nL, (deitado ? 0.23 : 0.115) * H * (noProduto ? 0.85 : 1), op);
|
|
1392
1532
|
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]);
|
|
1393
1533
|
b.bloco = { x: M, y, alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
|
|
1394
1534
|
const rot = c.selo || (c.tipo === 'cta' ? '' : '');
|
|
@@ -1426,9 +1566,9 @@ function compor(comp, br, ctx) {
|
|
|
1426
1566
|
case 'palavras': {
|
|
1427
1567
|
const txt = c.texto || c.titulo || '';
|
|
1428
1568
|
const alinha = s.chance(0.55) ? 'esq' : 'centro';
|
|
1429
|
-
const lg = deitado ? W * 0.
|
|
1569
|
+
const lg = deitado ? W * 0.86 : larguraUtil;
|
|
1430
1570
|
const op = { fonte: fontes.display, wght: 600 };
|
|
1431
|
-
const fit = caberEmLinhas(txt, lg, deitado ? 3 : 4, (deitado ? 0.
|
|
1571
|
+
const fit = caberEmLinhas(txt, lg, deitado ? 3 : 4, (deitado ? 0.18 : 0.1) * H, op);
|
|
1432
1572
|
b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert([0.48, 0.52]), alinha, vert: 'meio', largura: lg, gap: 0.1 };
|
|
1433
1573
|
// Sem destaque, a tese é a palavra mais longa que não é função.
|
|
1434
1574
|
if (!destaqueTk.length) {
|
|
@@ -1463,7 +1603,7 @@ function compor(comp, br, ctx) {
|
|
|
1463
1603
|
// Fonte larga (Unbounded) aperta a lista: os itens vão na fonte de texto.
|
|
1464
1604
|
const fonte = VARIAVEIS[fontes.display].largura > 1.1 ? fontes.texto : fontes.display;
|
|
1465
1605
|
const op = { fonte, wght: 560 };
|
|
1466
|
-
const maxT = (deitado ? 0.
|
|
1606
|
+
const maxT = (deitado ? 0.12 : 0.056) * H;
|
|
1467
1607
|
const nL = itens.length <= 3 ? 2 : 1;
|
|
1468
1608
|
const tam = Math.min(...itens.map((it) => caberEmLinhas((c.numerada ? '00 ' : '') + it, lg, nL, maxT, op).tamanho));
|
|
1469
1609
|
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 };
|
|
@@ -1483,8 +1623,8 @@ function compor(comp, br, ctx) {
|
|
|
1483
1623
|
case 'divisao': {
|
|
1484
1624
|
const lado = deitado ? (larguraUtil - M * 0.6) / 2 : larguraUtil;
|
|
1485
1625
|
const op = { fonte: fontes.display, wght: wTese };
|
|
1486
|
-
const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.
|
|
1487
|
-
const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.
|
|
1626
|
+
const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.13 : 0.08) * H, { ...op, wght: 400 });
|
|
1627
|
+
const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.17 : 0.1) * H, op);
|
|
1488
1628
|
if (deitado) {
|
|
1489
1629
|
b.bloco = { x: M, y: H * 0.5, alinha: 'esq', vert: 'meio', largura: lado, gap: 0.1, colunas: [M, M + lado + M * 0.6] };
|
|
1490
1630
|
} else {
|
|
@@ -1502,7 +1642,7 @@ function compor(comp, br, ctx) {
|
|
|
1502
1642
|
const fonte = fontes.serifa;
|
|
1503
1643
|
const lg = deitado ? W * 0.78 : larguraUtil;
|
|
1504
1644
|
const q = String(c.texto || '').replace(/^["“]+|["”]+$/g, '');
|
|
1505
|
-
const fit = caberEmLinhas(q, lg, deitado ? 4 : 5, (deitado ? 0.
|
|
1645
|
+
const fit = caberEmLinhas(q, lg, deitado ? 4 : 5, (deitado ? 0.15 : 0.08) * H, { fonte, wght: 420 });
|
|
1506
1646
|
b.bloco = { x: M, y: H * vert([0.48, 0.52]), alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
|
|
1507
1647
|
linha('“', 'aspas', { fonte, tamanho: fit.tamanho * 1.6, eixos: { wght: 800 }, cor: 'acento' });
|
|
1508
1648
|
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; } }); });
|
|
@@ -1516,7 +1656,7 @@ function compor(comp, br, ctx) {
|
|
|
1516
1656
|
const nome = c.nome || c.titulo || (ctx.marca.nome || '');
|
|
1517
1657
|
const fonte = fontes.display;
|
|
1518
1658
|
const lg = deitado ? W * 0.7 : larguraUtil * 0.9;
|
|
1519
|
-
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.
|
|
1659
|
+
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
|
|
1520
1660
|
b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
|
|
1521
1661
|
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, VARIAVEIS[fonte].eixos.wght[1]) }, comportamento: 'contorno' }).marca = true;
|
|
1522
1662
|
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.2, maiusc: true, eixos: { wght: 400 } });
|
|
@@ -1527,7 +1667,7 @@ function compor(comp, br, ctx) {
|
|
|
1527
1667
|
const txt = c.titulo || c.nome || '';
|
|
1528
1668
|
const fonte = fontes.cinetica;
|
|
1529
1669
|
const maiusc = true;
|
|
1530
|
-
const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.
|
|
1670
|
+
const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.26 : 0.13) * H, { fonte, wght: 900, maiusc });
|
|
1531
1671
|
b.bloco = { x: W / 2, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: larguraUtil, gap: 0.1 };
|
|
1532
1672
|
linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900, VARIAVEIS[fonte].eixos.wght[1]) }, maiusc });
|
|
1533
1673
|
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) };
|
|
@@ -1578,8 +1718,12 @@ function compor(comp, br, ctx) {
|
|
|
1578
1718
|
componente mede e cresce o grupo principal até a largura do bloco,
|
|
1579
1719
|
com o teto vertical daqui (o respiro de 30–40% continua garantido). */
|
|
1580
1720
|
// A palavra-herói de ponta a ponta (o CLAUDE do reel): teto alto, a largura manda.
|
|
1581
|
-
|
|
1582
|
-
|
|
1721
|
+
/* (10/10) "Uma coisa gigante por tela": os tetos subiram — a frase curta
|
|
1722
|
+
chega a ~14–18% da altura em altura de maiúscula, a lista a ~9%. Era o
|
|
1723
|
+
teto daqui (0,12 × 0,72 da altura) que deixava "Entregue a próxima
|
|
1724
|
+
tarefa" pequena no meio do bege, não o tamanho que a composição pedia. */
|
|
1725
|
+
const tetoV = { gigante: deitado ? 0.5 : 0.28, marca: deitado ? 0.34 : 0.18, parede: deitado ? 0.3 : 0.15, contador: deitado ? 0.55 : 0.25,
|
|
1726
|
+
pilha: deitado ? 0.3 : 0.15, produto: deitado ? 0.16 : 0.09, palavras: deitado ? 0.22 : 0.12, cascata: deitado ? 0.14 : 0.07, divisao: deitado ? 0.18 : 0.1, citacao: deitado ? 0.16 : 0.09 }[b.comp] || 0.12;
|
|
1583
1727
|
// A SANGRIA (a palavra de ponta a ponta) é um momento, não o padrão: até duas por vídeo, só em palavra curta.
|
|
1584
1728
|
const memS = ctx.memoria || {};
|
|
1585
1729
|
if (['gigante', 'parede'].includes(b.comp) && (memS.cheios || 0) < 2 && b.linhas.filter((l) => l.papel === 'tese').length <= 2
|
|
@@ -1591,7 +1735,7 @@ function compor(comp, br, ctx) {
|
|
|
1591
1735
|
if (!enche(l)) continue;
|
|
1592
1736
|
l.encher = l.papel === 'item' ? 'item' : 'principal';
|
|
1593
1737
|
// 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.
|
|
1594
|
-
l.max = Math.round(b.cheio ? Math.min(tetoV * H, (H * 0.
|
|
1738
|
+
l.max = Math.round(b.cheio ? Math.min(tetoV * H, (H * 0.7) / (nEnche * 1.05)) : Math.min(tetoV * H * 0.9, (H * 0.62) / (nEnche * 1.05)));
|
|
1595
1739
|
}
|
|
1596
1740
|
// ── o tempo de cada linha e palavra: quando a voz diz ────────────────
|
|
1597
1741
|
tempoDaBatida(b, palavras, ctx);
|
|
@@ -1709,5 +1853,5 @@ function contarAcentos(b) {
|
|
|
1709
1853
|
|
|
1710
1854
|
module.exports = {
|
|
1711
1855
|
montarGerativo, partituraDe, sonsDoVideo, musicaDe, desenharSom, humorDe, escolherVisual, MOLDURAS, AMBIENTES, ACENTOS_JEITO, ARRANJOS_ICONE, RITMOS_ICONE, MATERIAIS_ICONE, corGenerica, BANIDAS, HUMORES, REGRAS, VARIAVEIS, COMPOSICOES, COMPORTAMENTOS, SAIDAS, ACENTOS, FPS,
|
|
1712
|
-
especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO,
|
|
1856
|
+
especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO, HEROIS_3D, FAMILIA_HEROI,
|
|
1713
1857
|
};
|