primocode 10.4.4 → 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/lib/catalogo.js +1 -1
- 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/package.json +2 -2
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/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/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
|
};
|
package/lib/estudio/index.js
CHANGED
|
@@ -896,6 +896,32 @@ function alinharPartes(lista, palavras, total) {
|
|
|
896
896
|
});
|
|
897
897
|
}
|
|
898
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Onde o roteiro COMEÇA na fala transcrita, em segundos — 0 se começa logo.
|
|
901
|
+
*
|
|
902
|
+
* Dois jeitos de o Gemini vazar a direção, os dois vistos no vídeo do Joel
|
|
903
|
+
* (10/10): falar "Rápido e contínuo." ANTES do roteiro, e falar NO LUGAR da
|
|
904
|
+
* primeira frase ("Rápido e contínuo. Você pergunta…", sem o "IA é pra tirar
|
|
905
|
+
* dúvida"). Então: acha o primeiro par de palavras seguidas que existe no
|
|
906
|
+
* roteiro (não só o par do começo), e corta o que veio antes — mas só se o que
|
|
907
|
+
* veio antes for, na maior parte, palavra da DIREÇÃO. Fala do roteiro que o
|
|
908
|
+
* transcritor errou não é vazamento e não sai.
|
|
909
|
+
*/
|
|
910
|
+
function vazamentoNoComeco(lista, palavras, direcao = '') {
|
|
911
|
+
const norm = (t) => String(t || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
|
912
|
+
const roteiro = norm(lista.slice(0, 2).map((x) => x.texto).join(' ')).split(' ').filter(Boolean);
|
|
913
|
+
if (roteiro.length < 2 || !palavras.length) return 0;
|
|
914
|
+
const pares = new Set(roteiro.slice(0, -1).map((w, i) => w + ' ' + roteiro[i + 1]));
|
|
915
|
+
const ditas = palavras.slice(0, 14).map((w) => norm(w.texto));
|
|
916
|
+
let i = 0;
|
|
917
|
+
while (i < ditas.length - 1 && !pares.has(ditas[i] + ' ' + ditas[i + 1])) i++;
|
|
918
|
+
if (i === 0 || i >= ditas.length - 1) return 0;
|
|
919
|
+
const daDirecao = new Set(norm(direcao).split(' ').filter(Boolean));
|
|
920
|
+
const antes = ditas.slice(0, i).join(' ').split(' ').filter(Boolean);
|
|
921
|
+
const vazou = !direcao || antes.filter((w) => daDirecao.has(w)).length / antes.length >= 0.6;
|
|
922
|
+
return vazou ? Math.max(0, palavras[i].de - 0.08) : 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
899
925
|
async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
900
926
|
try {
|
|
901
927
|
const cfg = require('../config').loadConfig();
|
|
@@ -931,9 +957,25 @@ async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
|
931
957
|
const chars = lista.reduce((n, x) => n + x.texto.length, 0);
|
|
932
958
|
if (med.segundos < chars / 22) return { ok: false, error: `a voz veio cortada (${med.segundos.toFixed(1)} s para ${chars} caracteres)` };
|
|
933
959
|
const tr = await api.requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
934
|
-
|
|
960
|
+
let palavras = (tr && tr.palavras) || [];
|
|
935
961
|
if (!palavras.length) return { ok: false, error: 'sem a transcrição da voz para achar as partes' };
|
|
936
|
-
|
|
962
|
+
let segundos = med.segundos;
|
|
963
|
+
/* A DIREÇÃO LIDA EM VOZ ALTA. O servidor manda a direção ("leia RÁPIDO e
|
|
964
|
+
contínuo…") e o texto no mesmo pedido, e o Gemini às vezes fala o
|
|
965
|
+
começo dela: o vídeo do Joel abria com "Rápido e contínuo." antes do
|
|
966
|
+
roteiro (10/10). Se o roteiro só começa depois de algumas palavras,
|
|
967
|
+
o que vem antes é vazamento — e sai do áudio. */
|
|
968
|
+
const corte = vazamentoNoComeco(lista, palavras, direcao);
|
|
969
|
+
if (corte > 0.15) {
|
|
970
|
+
const cortado = mp3.replace(/\.mp3$/, '-c.mp3');
|
|
971
|
+
const k = await video.rodar(video.ffmpeg(), ['-y', '-ss', corte.toFixed(3), '-i', mp3, '-c:a', 'libmp3lame', '-q:a', '3', cortado], { timeout: 120000 });
|
|
972
|
+
if (k.ok && fs.existsSync(cortado)) {
|
|
973
|
+
fs.renameSync(cortado, mp3);
|
|
974
|
+
palavras = palavras.filter((w) => w.de >= corte - 0.02).map((w) => ({ ...w, de: Math.max(0, w.de - corte), ate: Math.max(0, w.ate - corte) }));
|
|
975
|
+
segundos = Math.max(0, segundos - corte);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return { ok: true, voz: `gemini ${r.voz}`, segundos, partes: alinharPartes(lista, palavras, segundos), ...(corte > 0.15 ? { cortouDirecao: corte } : {}) };
|
|
937
979
|
} catch (e) {
|
|
938
980
|
return { ok: false, error: e.message };
|
|
939
981
|
}
|
|
@@ -1011,7 +1053,10 @@ async function prepararDepoimento(p, c) {
|
|
|
1011
1053
|
|
|
1012
1054
|
/* A narração com cache (a mesma fala, na mesma direção, não é gerada duas vezes). */
|
|
1013
1055
|
async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
1014
|
-
|
|
1056
|
+
/* 'v2' (10/10): as narrações guardadas antes do corte da direção lida em
|
|
1057
|
+
voz alta ("Rápido e contínuo.") ficam para trás — reaproveitar uma delas
|
|
1058
|
+
trazia o vazamento de volta mesmo com o conserto. */
|
|
1059
|
+
const chave = require('crypto').createHash('sha1').update(JSON.stringify(['v2', lista.map((x) => x.texto), voz || '', direcao || ''])).digest('hex').slice(0, 16);
|
|
1015
1060
|
const dir = path.join(os.homedir(), '.primocode', 'studio', 'cache-voz');
|
|
1016
1061
|
const cm = path.join(dir, chave + '.mp3'), cj = path.join(dir, chave + '.json');
|
|
1017
1062
|
if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
|
|
@@ -1497,7 +1542,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1497
1542
|
/* O gerativo pede voz com CORPO ("narração encorpada, extravagante",
|
|
1498
1543
|
30/09): cheia, quente, perto do microfone — e o ritmo segue 1,12×. */
|
|
1499
1544
|
/* (A primeira versão pedia "trailer" e "ênfase sustentada": a voz saiu 25% mais lenta, medido em 30/09.) */
|
|
1500
|
-
|
|
1545
|
+
/* Sem a ênfase em caixa alta logo no começo ("rápido" gritado): era
|
|
1546
|
+
exatamente o trecho que o Gemini lia em voz alta (10/10). */
|
|
1547
|
+
? 'Leia em português do Brasil como locutor de reels de tecnologia, num ritmo rápido e sem pausas, com voz cheia, quente e encorpada, '
|
|
1501
1548
|
+ 'bem perto do microfone, muita energia e sorriso na voz. Ataque firme e ênfase curta e forte nas palavras de impacto; '
|
|
1502
1549
|
+ 'quase sem pausa entre as frases. Nunca arraste, nunca leia devagar nem solene.'
|
|
1503
1550
|
// O fluido é o vídeo de produto: voz limpa e segura, e rápida ("mais rápido", 06/10).
|
|
@@ -2042,7 +2089,8 @@ async function ferramentaSite(args) {
|
|
|
2042
2089
|
return r;
|
|
2043
2090
|
}
|
|
2044
2091
|
|
|
2045
|
-
module.exports = {
|
|
2092
|
+
module.exports = {
|
|
2093
|
+
vazamentoNoComeco, variarIcones,
|
|
2046
2094
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
2047
2095
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
2048
2096
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
15
15
|
import { delayRender, continueRender } from 'remotion';
|
|
16
16
|
import { SNAPPY, CURVAS_KIT, acaso, suave, alfa, mistura } from './base';
|
|
17
|
-
import { Produto3D, Icone3D } from './tres';
|
|
17
|
+
import { Produto3D, Icone3D, Heroi3D } from './tres';
|
|
18
18
|
|
|
19
19
|
const ed = CURVAS_KIT.editorial, curvaCam = CURVAS_KIT.camera, arrasto = CURVAS_KIT.arrasto, acelera = CURVAS_KIT.acelera;
|
|
20
20
|
const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
@@ -193,7 +193,9 @@ function leiaute(P, med) {
|
|
|
193
193
|
if (!l.encher || !med[l.id]) continue;
|
|
194
194
|
const k = l.encher + (l.coluna != null ? l.coluna : '');
|
|
195
195
|
// O grid manda: a tese ocupa até ~74% da largura do bloco; só a sangria vai a 97%.
|
|
196
|
-
|
|
196
|
+
/* (10/10) A tese gigante, a parede e a marca ENCHEM o bloco (97%): com fonte
|
|
197
|
+
larga, os 74% de antes paravam "CLAUDE CODE" em 9% da altura. O resto, 80%. */
|
|
198
|
+
const s = Math.min(l.max || l.tamanho, (bl.largura * (b.cheio || ['gigante', 'parede', 'marca'].includes(b.comp) ? 0.97 : 0.8)) / med[l.id].w);
|
|
197
199
|
grupo[k] = grupo[k] == null ? s : Math.min(grupo[k], s);
|
|
198
200
|
}
|
|
199
201
|
const tam = (l) => {
|
|
@@ -950,9 +952,24 @@ export function Gerativo({ el, t, fps }) {
|
|
|
950
952
|
e do texto — e calmo: desenhado por cima, brigava com os dois (05/10). */}
|
|
951
953
|
{noAr.filter((x) => x.heroi && x.heroi.modo === 'campo' && L[x.id]).map((x) => {
|
|
952
954
|
const bl = L[x.id].bloco, some = 1 - c01((t - x.ate) / 0.14);
|
|
955
|
+
let campo3d = null;
|
|
956
|
+
if (x.heroi.tres) {
|
|
957
|
+
/* As colunas NUNCA encostam no texto: só existem numa faixa abaixo
|
|
958
|
+
(ou acima, de ponta-cabeça) do bloco MEDIDO, com folga. O campo mora
|
|
959
|
+
mais fundo que o texto (translateZ -200), então na tela ele encolhe
|
|
960
|
+
para o centro mais que o texto: a folga medida aqui só cresce lá. */
|
|
961
|
+
const folga = Math.max(x.vao || 0, 0.07 * H);
|
|
962
|
+
const ds = DUR_SAIDA[x.saida] || 0;
|
|
963
|
+
const saiDe = x.ate - ds - 0.15;
|
|
964
|
+
if (H - (bl.y1 + folga) >= 0.34 * H) campo3d = { op: 0.7, faixa: { y0: bl.y1 + folga, y1: H }, saiDe };
|
|
965
|
+
else if (bl.y - folga >= 0.34 * H) campo3d = { op: 0.7, faixa: { y0: 0, y1: bl.y - folga }, teto: true, saiDe };
|
|
966
|
+
if (!campo3d) return null; // sem faixa livre: sem colunas
|
|
967
|
+
}
|
|
953
968
|
return (
|
|
954
969
|
<div key={'campo' + x.id} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transform: 'translateZ(-200px)', opacity: some }}>
|
|
955
|
-
|
|
970
|
+
{x.heroi.tres
|
|
971
|
+
? <Heroi3D h={x.heroi} t={t} de={x.de} ate={x.ate} W={W} H={H} slot={campo3d} />
|
|
972
|
+
: <Heroi b={x} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.32, op: 0.18 }} />}
|
|
956
973
|
</div>
|
|
957
974
|
);
|
|
958
975
|
})}
|
|
@@ -998,10 +1015,31 @@ export function Gerativo({ el, t, fps }) {
|
|
|
998
1015
|
// O lugar do herói: o vazio do bloco (como o ícone); sem vazio, atrás do texto e mais fraco.
|
|
999
1016
|
const bl = lay.bloco, M = P.margem;
|
|
1000
1017
|
if (x.heroi.modo === 'campo') return null; // desenhado atrás, antes do produto
|
|
1001
|
-
const
|
|
1018
|
+
const tres = x.heroi.modo === 'objeto3d';
|
|
1019
|
+
// O 3D é GRANDE ("um 3d bem presente"): o raio pede o vazio inteiro.
|
|
1020
|
+
const R = Math.min(W, H) * (tres ? (W >= H ? 0.26 : 0.22) : (W >= H ? 0.2 : 0.17));
|
|
1002
1021
|
let slot = null;
|
|
1003
1022
|
// A área segura (margem.js), com o vão do bloco; partitura antiga: a margem do mundo.
|
|
1004
1023
|
const S = x.seguro || { x0: M, y0: M * 1.4, x1: W - M, y1: H - M * 1.4 }, V = x.vao || 0;
|
|
1024
|
+
if (tres) {
|
|
1025
|
+
/* O 3D NUNCA cruza o texto: mora no maior retângulo livre ao lado do
|
|
1026
|
+
bloco MEDIDO (com o vão e uma folga), dentro da área segura; o raio
|
|
1027
|
+
é o círculo que cabe nele, e o Heroi3D garante que a projeção inteira
|
|
1028
|
+
(anel, tubo, dolly, perspectiva) fica dentro desse raio. Sem
|
|
1029
|
+
retângulo grande o bastante: sem 3D nesta batida. */
|
|
1030
|
+
const folga = Math.max(V, 0.035 * Math.min(W, H));
|
|
1031
|
+
const livres = [
|
|
1032
|
+
{ x0: bl.x1 + folga, y0: S.y0, x1: S.x1, y1: S.y1 },
|
|
1033
|
+
{ x0: S.x0, y0: S.y0, x1: bl.x - folga, y1: S.y1 },
|
|
1034
|
+
{ x0: S.x0, y0: S.y0, x1: S.x1, y1: bl.y - folga },
|
|
1035
|
+
{ x0: S.x0, y0: bl.y1 + folga, x1: S.x1, y1: S.y1 },
|
|
1036
|
+
].map((q) => ({ ...q, r: Math.min(q.x1 - q.x0, q.y1 - q.y0) / 2 })).sort((a, c) => c.r - a.r);
|
|
1037
|
+
const q = livres[0];
|
|
1038
|
+
if (!q || q.r < Math.min(W, H) * 0.12) return null;
|
|
1039
|
+
const ds = DUR_SAIDA[x.saida] || 0;
|
|
1040
|
+
slot = { cx: (q.x0 + q.x1) / 2, cy: (q.y0 + q.y1) / 2, R: Math.min(q.r, Math.min(W, H) * 0.3), saiDe: x.ate - ds - 0.12 };
|
|
1041
|
+
return <Heroi3D h={x.heroi} t={t} de={x.de} ate={x.ate} W={W} H={H} slot={slot} />;
|
|
1042
|
+
}
|
|
1005
1043
|
if (S.x1 - bl.x1 - V >= R * 2.3) slot = { cx: bl.x1 + V + (S.x1 - bl.x1 - V) / 2, cy: (bl.y + bl.y1) / 2, R };
|
|
1006
1044
|
else if (bl.y - V - S.y0 >= R * 2.2) slot = { cx: W / 2, cy: S.y0 + (bl.y - V - S.y0) / 2, R };
|
|
1007
1045
|
else if (S.y1 - bl.y1 - V >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + V + (S.y1 - bl.y1 - V) / 2, R };
|
package/lib/estudio/kit/tres.jsx
CHANGED
|
@@ -375,4 +375,160 @@ export function Icone3D({ el, t }) {
|
|
|
375
375
|
);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
|
|
379
|
+
/* ── OS HERÓIS EM 3D DE VERDADE (10/10) ───────────────────────────────────
|
|
380
|
+
* "As animações são muito iguais, quero um 3d bem presente" (Joel, com um
|
|
381
|
+
* reel de motion de referência). O herói abstrato era SVG — pontos girando
|
|
382
|
+
* num plano. Estes três são three.js: perspectiva, profundidade, luz e
|
|
383
|
+
* câmera que anda. Grandes: o raio vem do vazio do bloco (gerativo.jsx).
|
|
384
|
+
*
|
|
385
|
+
* globo3d a esfera de pontos e malha, girando inclinada, com um anel
|
|
386
|
+
* de órbita; os pontos de perto maiores (perspectiva real);
|
|
387
|
+
* fita3d o nó que se torce, em material com verniz e a luz de
|
|
388
|
+
* recorte na cor de acento;
|
|
389
|
+
* colunas3d o campo de colunas que sobe em onda, sobrevoado pela câmera
|
|
390
|
+
* e sumindo na névoa da cor do fundo — fica ATRÁS do texto.
|
|
391
|
+
*
|
|
392
|
+
* Canvas do quadro inteiro, transparente: o objeto é posto no lugar do
|
|
393
|
+
* vazio convertendo pixel → mundo (câmera fixa, fov 30, a 12 unidades).
|
|
394
|
+
*/
|
|
395
|
+
const FOV3D = 30, DIST3D = 12;
|
|
396
|
+
const VIS3D = 2 * DIST3D * Math.tan((FOV3D / 2) * Math.PI / 180);
|
|
397
|
+
const mola3d = (x) => { const k = Math.max(0, Math.min(1, x)); return 1 - Math.pow(1 - k, 3) * Math.cos(k * 4.2); };
|
|
398
|
+
|
|
399
|
+
function CameraFixa({ pos, alvo, teto }) {
|
|
400
|
+
const { camera } = useThree();
|
|
401
|
+
// De ponta-cabeça: o campo pende do alto (o texto mora embaixo).
|
|
402
|
+
camera.up.set(0, teto ? -1 : 1, 0);
|
|
403
|
+
camera.position.set(pos[0], pos[1], pos[2]);
|
|
404
|
+
camera.near = 0.1; camera.far = 80;
|
|
405
|
+
camera.lookAt(alvo[0], alvo[1], alvo[2]);
|
|
406
|
+
camera.updateProjectionMatrix();
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function Globo({ r, cor, cor2, sem, tl }) {
|
|
411
|
+
const geo = useMemo(() => {
|
|
412
|
+
const N = 1100, pos = new Float32Array(N * 3), col = new Float32Array(N * 3);
|
|
413
|
+
const c1 = new THREE.Color(cor2), c2 = new THREE.Color(cor);
|
|
414
|
+
const ouro = Math.PI * (3 - Math.sqrt(5));
|
|
415
|
+
for (let i = 0; i < N; i++) {
|
|
416
|
+
const y = 1 - (i / (N - 1)) * 2, rr = Math.sqrt(1 - y * y), a = i * ouro;
|
|
417
|
+
pos.set([Math.cos(a) * rr * r, y * r, Math.sin(a) * rr * r], i * 3);
|
|
418
|
+
const c = (i * 7 + sem) % 6 === 0 ? c2 : c1;
|
|
419
|
+
col.set([c.r, c.g, c.b], i * 3);
|
|
420
|
+
}
|
|
421
|
+
const g = new THREE.BufferGeometry();
|
|
422
|
+
g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
423
|
+
g.setAttribute('color', new THREE.BufferAttribute(col, 3));
|
|
424
|
+
return g;
|
|
425
|
+
}, [r, cor, cor2, sem]);
|
|
426
|
+
return (
|
|
427
|
+
<group rotation={[0.38 + Math.sin(tl * 0.5) * 0.08, tl * 0.55 + sem * 0.01, 0.12]}>
|
|
428
|
+
<points geometry={geo}><pointsMaterial size={r * 0.06} vertexColors sizeAttenuation /></points>
|
|
429
|
+
<mesh><sphereGeometry args={[r * 0.99, 32, 20]} /><meshBasicMaterial color={cor2} wireframe transparent opacity={0.22} /></mesh>
|
|
430
|
+
<mesh rotation={[1.2, 0.3, tl * 0.4]}><torusGeometry args={[r * 1.14, r * 0.008, 8, 160]} /><meshBasicMaterial color={cor} transparent opacity={0.85} /></mesh>
|
|
431
|
+
<mesh position={[Math.cos(tl * 1.3) * r * 1.14, 0, Math.sin(tl * 1.3) * r * 1.14]} rotation={[1.2, 0.3, 0]}>
|
|
432
|
+
<sphereGeometry args={[r * 0.05, 16, 12]} /><meshBasicMaterial color={cor} />
|
|
433
|
+
</mesh>
|
|
434
|
+
</group>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function Fita({ r, cor, cor2, sem, tl }) {
|
|
439
|
+
return (
|
|
440
|
+
<group rotation={[0.5 + tl * 0.35, tl * 0.6 + sem * 0.02, 0.2]}>
|
|
441
|
+
<mesh>
|
|
442
|
+
<torusKnotGeometry args={[r * 0.52, r * 0.13, 280, 32, 2 + (sem % 2), 3]} />
|
|
443
|
+
<meshPhysicalMaterial color={cor2} roughness={0.28} metalness={0.35} clearcoat={0.9} clearcoatRoughness={0.2} />
|
|
444
|
+
</mesh>
|
|
445
|
+
<mesh scale={[1.02, 1.02, 1.02]}>
|
|
446
|
+
<torusKnotGeometry args={[r * 0.52, r * 0.13, 140, 12, 2 + (sem % 2), 3]} />
|
|
447
|
+
<meshBasicMaterial color={cor} wireframe transparent opacity={0.22} />
|
|
448
|
+
</mesh>
|
|
449
|
+
</group>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function Colunas({ cor, cor2, fundo, sem, tl }) {
|
|
454
|
+
const n = 15, passo = 0.9;
|
|
455
|
+
const base = new THREE.Color(fundo).lerp(new THREE.Color(cor2), 0.22).getStyle();
|
|
456
|
+
const acesa = new THREE.Color(cor).getStyle();
|
|
457
|
+
const els = [];
|
|
458
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
|
|
459
|
+
const x = (i - (n - 1) / 2) * passo, z = (j - (n - 1) / 2) * passo - tl * 1.1;
|
|
460
|
+
const d = Math.hypot(i - n / 2, j - n / 2);
|
|
461
|
+
const onda = 0.5 + 0.5 * Math.sin(d * 0.85 - tl * 2.6 + (sem % 7));
|
|
462
|
+
const h = 0.25 + 2.2 * onda * onda;
|
|
463
|
+
const crista = onda > 0.93;
|
|
464
|
+
els.push(
|
|
465
|
+
<mesh key={i + '-' + j} position={[x, h / 2 - 1.6, z]} scale={[passo * 0.78, h, passo * 0.78]}>
|
|
466
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
467
|
+
<meshStandardMaterial color={crista ? acesa : base} roughness={0.55} metalness={0.15}
|
|
468
|
+
{...(crista ? { emissive: acesa, emissiveIntensity: 0.6 } : {})} />
|
|
469
|
+
</mesh>,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return <group>{els}</group>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* O quanto cada forma passa do raio r, no pior quadro: o anel e a lua do
|
|
476
|
+
globo (1,14 r + a lua), o nó (o tubo vai a 1,5 × 0,52 r + 0,13 r). */
|
|
477
|
+
const ALCANCE3D = { globo3d: 1.2, fita3d: 0.92 };
|
|
478
|
+
|
|
479
|
+
export function Heroi3D({ h, t, de, ate, W, H, slot }) {
|
|
480
|
+
const tl = Math.max(0, t - de), dur = Math.max(0.8, ate - de);
|
|
481
|
+
const k = Math.min(1.02, mola3d(tl / 0.8));
|
|
482
|
+
// Some ANTES de o texto sair (a saída dele voa pela tela): o herói nunca cruza a letra.
|
|
483
|
+
const saiDe = slot.saiDe != null ? slot.saiDe : ate - 0.25;
|
|
484
|
+
const sai = Math.max(0, Math.min(1, (t - saiDe) / 0.2));
|
|
485
|
+
const sem = h.semente || 0;
|
|
486
|
+
const cor = h.cor || '#ff5a1f', cor2 = h.cor2 || '#eeeeee', fundo = h.fundo || '#111111';
|
|
487
|
+
if (h.tipo === 'colunas3d') {
|
|
488
|
+
// O sobrevoo: a câmera alta, olhando para a frente e para baixo, andando sobre o campo.
|
|
489
|
+
const avanca = tl * 0.25;
|
|
490
|
+
return (
|
|
491
|
+
<div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: (slot.op != null ? slot.op : 0.85) * Math.min(1, tl / 0.5) * (1 - sai),
|
|
492
|
+
/* A FAIXA: as colunas só existem fora do texto (clip), com a borda esfumada. */
|
|
493
|
+
...(slot.faixa ? { clipPath: `inset(${slot.faixa.y0}px 0 ${H - slot.faixa.y1}px 0)`,
|
|
494
|
+
WebkitMaskImage: slot.teto ? `linear-gradient(to top, transparent ${H - slot.faixa.y1}px, #000 ${H - slot.faixa.y1 + 0.06 * H}px)` : `linear-gradient(to bottom, transparent ${slot.faixa.y0}px, #000 ${slot.faixa.y0 + 0.06 * H}px)` } : {}) }}>
|
|
495
|
+
<ThreeCanvas width={W} height={H} camera={{ position: [0, 4, 9], fov: 38 }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
|
|
496
|
+
{/* Câmera baixa olhando quase reto: o campo ocupa a metade de baixo e some na névoa — o meio fica para o texto. */}
|
|
497
|
+
<CameraFixa teto={slot.teto} pos={[Math.sin(tl * 0.3) * 1.2, 2.3, 8 - avanca]} alvo={[0, 0.9, -4 - avanca]} />
|
|
498
|
+
<fog attach="fog" args={[fundo, 6, 19]} />
|
|
499
|
+
<ambientLight intensity={0.45} />
|
|
500
|
+
<directionalLight position={[4, 8, 5]} intensity={2.2} />
|
|
501
|
+
<directionalLight position={[-6, 2, -4]} intensity={2.6} color={cor} />
|
|
502
|
+
<Colunas cor={cor} cor2={cor2} fundo={fundo} sem={sem} tl={tl} />
|
|
503
|
+
</ThreeCanvas>
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const { cx, cy, R } = slot;
|
|
508
|
+
const wx = ((cx - W / 2) / H) * VIS3D, wy = (-(cy - H / 2) / H) * VIS3D;
|
|
509
|
+
/* R é o TETO do que aparece na tela (o vão livre ao lado do texto). O raio
|
|
510
|
+
no mundo sai dele de trás para a frente: o alcance da forma, a escala
|
|
511
|
+
máxima e a perspectiva do ponto mais perto da lente no fim do dolly. */
|
|
512
|
+
const F = ALCANCE3D[h.tipo] || 1.2, zMax = 0.45;
|
|
513
|
+
let r = (R / H) * VIS3D / (F * 1.02);
|
|
514
|
+
for (let i = 0; i < 3; i++) r = ((R / H) * VIS3D / (F * 1.02)) * ((DIST3D - zMax - F * r) / DIST3D);
|
|
515
|
+
// O dolly: o objeto chega do fundo e vem devagar para a lente durante a batida.
|
|
516
|
+
const z = -3.5 * (1 - Math.min(1, k)) + zMax * Math.min(1, tl / dur);
|
|
517
|
+
const esc = 0.55 + 0.45 * k;
|
|
518
|
+
return (
|
|
519
|
+
<div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: (slot.op != null ? slot.op : 1) * Math.min(1, tl / 0.3) * (1 - sai) }}>
|
|
520
|
+
<ThreeCanvas width={W} height={H} camera={{ position: [0, 0, DIST3D], fov: FOV3D }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
|
|
521
|
+
<Ambiente forca={0.9} />
|
|
522
|
+
<ambientLight intensity={0.3} />
|
|
523
|
+
<directionalLight position={[3, 4, 6]} intensity={2.6} />
|
|
524
|
+
<directionalLight position={[-5, -1, -3]} intensity={3.2} color={cor} />
|
|
525
|
+
<pointLight position={[0, 0, 5]} intensity={0.6} />
|
|
526
|
+
<group position={[wx, wy, z]} scale={[esc, esc, esc]}>
|
|
527
|
+
{h.tipo === 'fita3d' ? <Fita r={r} cor={cor} cor2={cor2} sem={sem} tl={tl} /> : <Globo r={r} cor={cor} cor2={cor2} sem={sem} tl={tl} />}
|
|
528
|
+
</group>
|
|
529
|
+
</ThreeCanvas>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
378
534
|
export const TRES = { produto3d: Produto3D, icone3d: Icone3D };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.4.
|
|
3
|
+
"version": "10.4.7",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/vazamento.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|