primocode 10.2.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/act.js +5 -0
- package/lib/browser.js +1 -1
- package/lib/catalogo.js +16 -7
- package/lib/estudio/assinatura.js +108 -0
- package/lib/estudio/assinaturas/fluido.json +15 -0
- package/lib/estudio/assinaturas/gerativo.json +15 -0
- package/lib/estudio/diretor.js +70 -4
- package/lib/estudio/efeitos.js +95 -0
- package/lib/estudio/falantes.js +97 -0
- package/lib/estudio/fluido.js +28 -3
- package/lib/estudio/gerativo.js +309 -85
- package/lib/estudio/hibrido.js +1 -1
- package/lib/estudio/index.js +130 -15
- package/lib/estudio/kit/gerativo.jsx +90 -21
- package/lib/estudio/kit/marca.jsx +58 -1
- package/lib/estudio/kit/tres.jsx +4 -1
- package/lib/estudio/kit.js +2 -0
- package/lib/estudio/plano.js +5 -1
- package/lib/estudio/print-site.js +152 -0
- package/lib/estudio/referencias.js +12 -1
- package/lib/estudio/remotion.js +72 -3
- package/lib/estudio/variedade.js +13 -5
- package/package.json +2 -2
package/lib/estudio/index.js
CHANGED
|
@@ -38,6 +38,8 @@ const hibrido = require('./hibrido.js');
|
|
|
38
38
|
const biblioteca = require('./biblioteca.js');
|
|
39
39
|
const referencias = require('./referencias.js');
|
|
40
40
|
const variedade = require('./variedade.js');
|
|
41
|
+
const diretor = require('./diretor.js');
|
|
42
|
+
const falantes = require('./falantes.js');
|
|
41
43
|
const variacao3d = require('./variacao3d.js');
|
|
42
44
|
const plano = require('./plano.js');
|
|
43
45
|
const remotion = require('./remotion.js');
|
|
@@ -45,11 +47,14 @@ const assets = require('./assets.js');
|
|
|
45
47
|
const video = require('./video.js');
|
|
46
48
|
const batida = require('./batida.js');
|
|
47
49
|
const revisor = require('./revisor.js');
|
|
50
|
+
const assinatura = require('./assinatura.js');
|
|
48
51
|
const identidade = require('./identidade.js');
|
|
49
52
|
const slides = require('./slides.js');
|
|
50
53
|
const edicao = require('./edicao.js');
|
|
51
54
|
const composicao = require('./composicao.js');
|
|
52
55
|
const acervoSite = require('./acervo-site.js');
|
|
56
|
+
const printSite = require('./print-site.js');
|
|
57
|
+
const efeitos = require('./efeitos.js');
|
|
53
58
|
const impacto = require('./impacto.js');
|
|
54
59
|
const cinema = require('./cinema.js');
|
|
55
60
|
const motion = require('./motion.js');
|
|
@@ -200,6 +205,17 @@ async function ferramentaProjeto(args, ctx = {}) {
|
|
|
200
205
|
return { ok: true };
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/* O FEEDBACK que vira estilo: "adorei a virada", "texto demais" fica na
|
|
209
|
+
assinatura da pessoa (assinatura.js), e o próximo vídeo já parte dele.
|
|
210
|
+
Sem texto, devolve a assinatura (as faixas e as notas). */
|
|
211
|
+
if (acao === 'assinatura' || acao === 'feedback' || acao === 'gostei') {
|
|
212
|
+
const nome = args.nome || args.assinatura || 'fluido';
|
|
213
|
+
if (!args.texto) { const a = assinatura.ler(nome); return a ? { ok: true, assinatura: a } : { ok: false, error: `a assinatura "${nome}" não existe.` }; }
|
|
214
|
+
const r = assinatura.anotar(nome, args.texto);
|
|
215
|
+
if (r.ok && args.id) projeto.anotar(args.id, 'assinatura', `feedback gravado na assinatura ${nome}: ${args.texto}`);
|
|
216
|
+
return r;
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
const todos = projeto.listar({ limite: Number(args.limite) || 20 });
|
|
204
220
|
return {
|
|
205
221
|
ok: true,
|
|
@@ -262,7 +278,8 @@ function roteiroValido(r) {
|
|
|
262
278
|
const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
|
|
263
279
|
const NOVOS = ['logo', 'cores', 'produto']; // vox.js e lancamento.js
|
|
264
280
|
if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo)))) return null;
|
|
265
|
-
|
|
281
|
+
// Duas vozes: roteiro.vozes e parte.falante, limpos (falantes.js). Sem eles, nada muda.
|
|
282
|
+
const obj = falantes.normalizar(Array.isArray(x) ? { partes } : x);
|
|
266
283
|
return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
|
|
267
284
|
}
|
|
268
285
|
|
|
@@ -365,6 +382,13 @@ async function ferramentaPlano(args) {
|
|
|
365
382
|
// O roteiro fica guardado: ajustar uma frase é mandar o roteiro de novo, não reescrever tudo.
|
|
366
383
|
try { fs.writeFileSync(path.join(p.pasta, 'roteiro.json'), JSON.stringify(original || args.roteiro, null, 2)); } catch { /* segue */ }
|
|
367
384
|
projeto.salvar(args.id, { duracao: conferida.duracao });
|
|
385
|
+
/* A memória da estrutura (diretor.cardapioEstruturas): o próximo roteiro
|
|
386
|
+
vê o que este usou. Uma vez por projeto — mandar o roteiro de novo
|
|
387
|
+
para ajustar uma frase não é um vídeo novo. */
|
|
388
|
+
if (!p.estrutura) {
|
|
389
|
+
const usada = diretor.registrarEstrutura(original || args.roteiro);
|
|
390
|
+
projeto.salvar(args.id, { estrutura: usada.estrutura || '-' });
|
|
391
|
+
}
|
|
368
392
|
return {
|
|
369
393
|
ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
|
|
370
394
|
proximo: 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
|
|
@@ -437,6 +461,26 @@ async function ferramentaMidia(args) {
|
|
|
437
461
|
return { ok: true, src: relativo, metodo: r.metodo, aviso: r.aviso || undefined };
|
|
438
462
|
}
|
|
439
463
|
|
|
464
|
+
/* O print de verdade do site (print-site.js), para a peça 3D `printsite`.
|
|
465
|
+
Só roda quando o modelo pede: o cardápio diz quando vale. */
|
|
466
|
+
if (acao === 'print') {
|
|
467
|
+
if (!p) return { ok: false, error: 'para tirar o print é preciso dizer em qual projeto (id).' };
|
|
468
|
+
const r = await printSite.capturarSite(args.url || args.site || args.termo, { pasta: p.pasta, celular: !!args.celular, inteira: args.inteira !== false });
|
|
469
|
+
if (r.ok) projeto.anotar(p.id, 'midia', `print de ${r.url} (${r.tela}): ${r.src}`);
|
|
470
|
+
return r;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// O catálogo de efeitos que a IA escreveu (efeitos.js): guardar, listar, reusar.
|
|
474
|
+
if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio() };
|
|
475
|
+
if (acao === 'guardar-efeito' || acao === 'usar-efeito') {
|
|
476
|
+
if (!p) return { ok: false, error: 'diga em qual projeto (id).' };
|
|
477
|
+
const r = acao === 'guardar-efeito'
|
|
478
|
+
? efeitos.guardar({ pasta: p.pasta, arquivo: args.arquivo || args.src, nome: args.nome, descricao: args.descricao, tags: args.tags })
|
|
479
|
+
: efeitos.reusar({ pasta: p.pasta, nome: args.nome });
|
|
480
|
+
if (r.ok) projeto.anotar(p.id, 'midia', `${acao === 'guardar-efeito' ? 'guardou' : 'reusou'} o efeito ${r.nome || args.nome}`);
|
|
481
|
+
return r;
|
|
482
|
+
}
|
|
483
|
+
|
|
440
484
|
if (acao === 'logo') {
|
|
441
485
|
if (!p) return { ok: false, error: 'para baixar a logo é preciso dizer em qual projeto (id).' };
|
|
442
486
|
const nome = String(args.marca || args.nome || args.termo || '').trim();
|
|
@@ -970,6 +1014,20 @@ function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokor
|
|
|
970
1014
|
});
|
|
971
1015
|
}
|
|
972
1016
|
|
|
1017
|
+
/* DUAS VOZES (falantes.js): os mp3 de cada falante, um depois do outro, num
|
|
1018
|
+
arquivo só — reamostrados, porque o Gemini sai a 24 kHz e o Edge não. A
|
|
1019
|
+
duração de cada um é MEDIDA: é ela que empurra os tempos do grupo seguinte. */
|
|
1020
|
+
async function juntarVozes(arquivos, mp3) {
|
|
1021
|
+
const fil = arquivos.map((_, i) => `[${i}:a]aresample=44100,aformat=channel_layouts=mono[a${i}]`).join(';')
|
|
1022
|
+
+ ';' + arquivos.map((_, i) => `[a${i}]`).join('') + `concat=n=${arquivos.length}:v=0:a=1[s]`;
|
|
1023
|
+
const c = await video.rodar(video.ffmpeg(), ['-y', ...arquivos.flatMap((a) => ['-i', a]), '-filter_complex', fil, '-map', '[s]',
|
|
1024
|
+
'-c:a', 'libmp3lame', '-q:a', '3', mp3], { timeout: 120000 });
|
|
1025
|
+
if (!c.ok) return { ok: false, error: 'não consegui juntar as vozes' };
|
|
1026
|
+
const duracoes = [];
|
|
1027
|
+
for (const a of arquivos) duracoes.push((await video.medir(a)).segundos);
|
|
1028
|
+
return { ok: true, duracoes };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
973
1031
|
/* A MÚSICA TEM LETRA? O acervo não diz se a faixa é instrumental, então ela
|
|
974
1032
|
é ouvida: 25 s do meio vão para a transcrição, e mais de 6 palavras é voz
|
|
975
1033
|
cantada. Sem servidor para ouvir, a faixa passa (melhor música que nenhuma). */
|
|
@@ -1015,6 +1073,14 @@ async function sonsReais(p, video) {
|
|
|
1015
1073
|
const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
|
|
1016
1074
|
|
|
1017
1075
|
/** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
|
|
1076
|
+
/* A ASSINATURA (assinatura.js): o vídeo é medido e conferido contra as
|
|
1077
|
+
faixas do estilo; o que saiu volta para o agente e fica no caderno. */
|
|
1078
|
+
function conferirAssinatura(p, r, nome) {
|
|
1079
|
+
const fora = assinatura.descrever(assinatura.conferir(nome, assinatura.medir(r.partitura, r.plano)).fora);
|
|
1080
|
+
r.resumo.assinatura = { nome, seguiu: !fora.length, fora: fora.length ? fora : undefined };
|
|
1081
|
+
projeto.anotar(p.id, 'assinatura', fora.length ? `fora da assinatura ${nome}: ${fora.join('; ')}` : `seguiu a assinatura ${nome}`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1018
1084
|
async function prepararRoteiro(p, args) {
|
|
1019
1085
|
const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
|
|
1020
1086
|
if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
|
|
@@ -1066,6 +1132,14 @@ async function prepararRoteiro(p, args) {
|
|
|
1066
1132
|
roteiro.semente = p.semente != null ? p.semente : Math.floor(Math.random() * 1e9);
|
|
1067
1133
|
if (p.semente == null) projeto.salvar(p.id, { semente: roteiro.semente });
|
|
1068
1134
|
}
|
|
1135
|
+
/* O humor, a paleta, as fontes e o gênero do gerativo, com memória entre
|
|
1136
|
+
vídeos (09/10, "vício em cores, músicas, fontes"): por PROJETO, como a semente. */
|
|
1137
|
+
// O assunto do vídeo é o rótulo da moldura (quando ela sai): o título do projeto.
|
|
1138
|
+
if (estilo === 'gerativo' && !roteiro.titulo && p.titulo) roteiro.titulo = p.titulo;
|
|
1139
|
+
if (estilo === 'gerativo' && !roteiro.escolhasGerativo) {
|
|
1140
|
+
roteiro.escolhasGerativo = p.escolhasGerativo || gerativo.escolherVisual(roteiro);
|
|
1141
|
+
if (!p.escolhasGerativo) projeto.salvar(p.id, { escolhasGerativo: roteiro.escolhasGerativo });
|
|
1142
|
+
}
|
|
1069
1143
|
if (ref.modo === 'fechado' && !roteiro.busca && referencias.termos().length) roteiro.busca = referencias.termos();
|
|
1070
1144
|
/* A BIBLIOTECA (biblioteca.js): sem fonte e cor da marca, o híbrido, o
|
|
1071
1145
|
cinema e o impacto caíam sempre no Inter e no azul e roxo — "os vídeos
|
|
@@ -1135,7 +1209,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1135
1209
|
// Só as partes com voz vão para a narração; o depoimento tem a voz do aluno.
|
|
1136
1210
|
// Só as partes com voz: o depoimento tem a voz do aluno, e a parte muda não tem voz.
|
|
1137
1211
|
// A parte com voz forte é narrada à parte: se entrasse aqui também, seria falada duas vezes (e muda uma delas).
|
|
1138
|
-
const
|
|
1212
|
+
const comVoz = partes.filter((pt) => pt.tipo !== 'depoimento' && cinema.falaDe(pt) !== '' && !pt.falaForte);
|
|
1213
|
+
const lista = comVoz.map((pt) => ({ texto: cinema.falaDe(pt), registro: 'normal' }));
|
|
1214
|
+
// Duas vozes: a voz de cada parte pelo falante dela (roteiro.vozes).
|
|
1215
|
+
const vozesL = falantes.vozesDasPartes(comVoz, roteiro, args.voz || roteiro.voz);
|
|
1216
|
+
const variasVozes = new Set(vozesL).size > 1;
|
|
1217
|
+
const DIRECAO_CINEMA = 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
1218
|
+
+ 'ritmo firme, pausas curtas entre as frases, sem arrastar.';
|
|
1139
1219
|
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
1140
1220
|
/* O CACHE DA VOZ: a mesma fala, na mesma voz, não é gerada duas vezes.
|
|
1141
1221
|
Refazer o roteiro em outro formato, ou mexer numa cena, não gasta a
|
|
@@ -1146,7 +1226,20 @@ async function prepararRoteiro(p, args) {
|
|
|
1146
1226
|
let v = null;
|
|
1147
1227
|
// motorVoz "edge": a voz do Edge direto, sem esperar a cota do Gemini ("a narração pode manter a do Edge", 25/09).
|
|
1148
1228
|
const soEdge = String(roteiro.motorVoz || args.motorVoz || '').toLowerCase() === 'edge';
|
|
1149
|
-
if (
|
|
1229
|
+
if (variasVozes) {
|
|
1230
|
+
/* Um grupo por falante, cada um com a sua voz e o seu cache (a chave
|
|
1231
|
+
do narrarComCache leva a voz). Falhou? Fica anotado, e a reserva
|
|
1232
|
+
de baixo narra numa voz só — melhor o vídeo do que nada. */
|
|
1233
|
+
v = await falantes.narrarPorFalante(lista.map((x, i) => ({ ...x, voz: vozesL[i] })), mp3, { juntar: juntarVozes,
|
|
1234
|
+
narrarUm: async (l, m, voz) => {
|
|
1235
|
+
const g = soEdge ? { ok: false } : await narrarComCache(l, m, { voz, direcao: DIRECAO_CINEMA });
|
|
1236
|
+
if (g.ok) return g;
|
|
1237
|
+
const a = m.replace(/\.mp3$/, '.json');
|
|
1238
|
+
fs.writeFileSync(a, JSON.stringify(l));
|
|
1239
|
+
return narrarReserva(l, m, a, voz, 'antonio', { soEdge });
|
|
1240
|
+
} });
|
|
1241
|
+
if (!v.ok) projeto.anotar(p.id, 'narracao', `as duas vozes falharam (${v.error}); saiu numa voz só`);
|
|
1242
|
+
} else if (soEdge) {
|
|
1150
1243
|
v = { ok: false, error: 'edge pedido' };
|
|
1151
1244
|
} else if (fs.existsSync(cacheMp3) && fs.existsSync(cacheJson)) {
|
|
1152
1245
|
fs.copyFileSync(cacheMp3, mp3);
|
|
@@ -1165,9 +1258,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1165
1258
|
} else if (kokoro.querKokoro(args.voz || roteiro.voz)) {
|
|
1166
1259
|
v = { ok: false, error: 'kokoro pedido' };
|
|
1167
1260
|
} else {
|
|
1168
|
-
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz,
|
|
1169
|
-
direcao: 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
1170
|
-
+ 'ritmo firme, pausas curtas entre as frases, sem arrastar.' });
|
|
1261
|
+
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz, direcao: DIRECAO_CINEMA });
|
|
1171
1262
|
if (v.ok) {
|
|
1172
1263
|
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.copyFileSync(mp3, cacheMp3); fs.writeFileSync(cacheJson, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes })); } catch { /* sem cache */ }
|
|
1173
1264
|
}
|
|
@@ -1342,12 +1433,14 @@ async function prepararRoteiro(p, args) {
|
|
|
1342
1433
|
// falaForte: true → a voz forte lê o texto da tela.
|
|
1343
1434
|
const textoForte = pt.falaForte === true ? cinema.falaDe({ ...pt, fala: undefined }) : pt.falaForte;
|
|
1344
1435
|
const soEdgeF = String(roteiro.motorVoz || args.motorVoz || '').toLowerCase() === 'edge';
|
|
1436
|
+
// A voz forte é a de quem fala a parte (duas vozes), não sempre a do narrador.
|
|
1437
|
+
const vozForte = falantes.vozesDasPartes([pt], roteiro, args.voz || roteiro.voz)[0];
|
|
1345
1438
|
const vf = soEdgeF ? await (async () => {
|
|
1346
1439
|
// No Edge, a ênfase é o registro "contraste": a mesma voz, um pouco mais firme.
|
|
1347
1440
|
const arqJ = arqF.replace(/\.mp3$/, '.json');
|
|
1348
1441
|
fs.writeFileSync(arqJ, JSON.stringify([{ texto: textoForte, registro: 'contraste' }]));
|
|
1349
|
-
return rodarPython('narrar.py', ['roteiro', '--arquivo', arqJ, '--saida', arqF, '--voz', String(
|
|
1350
|
-
})() : await narrarComCache([{ texto: textoForte }], arqF, { voz:
|
|
1442
|
+
return rodarPython('narrar.py', ['roteiro', '--arquivo', arqJ, '--saida', arqF, '--voz', String(vozForte || 'antonio')], { timeout: 5 * 60000 });
|
|
1443
|
+
})() : await narrarComCache([{ texto: textoForte }], arqF, { voz: vozForte,
|
|
1351
1444
|
// A MESMA voz do narrador, só com um pouco mais de ênfase: "não é mudar a voz, é só um pouco a entonação" (25/09).
|
|
1352
1445
|
direcao: pt.entonacao || 'Leia em português do Brasil com a mesma voz e o mesmo ritmo de um narrador de trailer de marca: '
|
|
1353
1446
|
+ 'confiante e natural, só um pouco mais de ênfase nas palavras principais. Sem dramatizar.' });
|
|
@@ -1382,8 +1475,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1382
1475
|
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
1383
1476
|
// A voz de gente primeiro (Gemini, pelo servidor); o Edge é a reserva.
|
|
1384
1477
|
// Com cache: refazer o vídeo (outro estilo, outra proporção) não gasta a cota da voz.
|
|
1385
|
-
|
|
1386
|
-
direcao: estilo === 'gerativo'
|
|
1478
|
+
const direcao = estilo === 'gerativo'
|
|
1387
1479
|
/* O gerativo pede voz com CORPO ("narração encorpada, extravagante",
|
|
1388
1480
|
30/09): cheia, quente, perto do microfone — e o ritmo segue 1,12×. */
|
|
1389
1481
|
/* (A primeira versão pedia "trailer" e "ênfase sustentada": a voz saiu 25% mais lenta, medido em 30/09.) */
|
|
@@ -1397,9 +1489,19 @@ async function prepararRoteiro(p, args) {
|
|
|
1397
1489
|
+ 'voz encorpada, ênfase forte nas palavras de impacto, pausas curtas e secas entre as frases, sorrindo na voz.'
|
|
1398
1490
|
// "A narração tá lenta, deixa mais viva" (29/09): todo estilo fala com energia de reels.
|
|
1399
1491
|
: 'Leia em português do Brasil como narrador de reels de tecnologia: vivo, confiante e rápido, sorrindo na voz, '
|
|
1400
|
-
+ 'ênfase nas palavras que importam, pausas curtas entre as frases. Nada de leitura lenta ou solene.'
|
|
1401
|
-
|
|
1402
|
-
|
|
1492
|
+
+ 'ênfase nas palavras que importam, pausas curtas entre as frases. Nada de leitura lenta ou solene.';
|
|
1493
|
+
/* Uma voz por falante (falantes.js). Com uma voz só, um grupo só: é a
|
|
1494
|
+
narração de sempre, com o mesmo cache. */
|
|
1495
|
+
const vozesL = falantes.vozesDasPartes(partes, roteiro, args.voz || roteiro.voz);
|
|
1496
|
+
let v = await falantes.narrarPorFalante(lista.map((x, i) => ({ ...x, voz: vozesL[i] })), mp3, { juntar: juntarVozes,
|
|
1497
|
+
narrarUm: async (l, m, voz) => {
|
|
1498
|
+
const g = await narrarComCache(l, m, { voz, direcao });
|
|
1499
|
+
if (g.ok) return g;
|
|
1500
|
+
// O Kokoro pedido já foi tentado no narrarComCache: a reserva não repete.
|
|
1501
|
+
const a = m === mp3 ? arq : m.replace(/\.mp3$/, '.json');
|
|
1502
|
+
if (a !== arq) fs.writeFileSync(a, JSON.stringify(l));
|
|
1503
|
+
return narrarReserva(l, m, a, voz, 'thalita', { semKokoro: kokoro.querKokoro(voz) });
|
|
1504
|
+
} });
|
|
1403
1505
|
if (!v.ok) return v;
|
|
1404
1506
|
/* O RITMO DA VOZ. Mesmo com a direção, a voz sai no passo de leitura, e
|
|
1405
1507
|
o vídeo — que segue a voz palavra por palavra — sai lento junto. O
|
|
@@ -1528,13 +1630,22 @@ async function prepararRoteiro(p, args) {
|
|
|
1528
1630
|
'-af', 'afade=t=in:st=0:d=0.4,loudnorm=I=-18:TP=-2:LRA=11', '-ar', '44100', norm], { timeout: 5 * 60000 });
|
|
1529
1631
|
if (c.ok && fs.existsSync(norm)) trilha = path.relative(p.pasta, norm).replace(/\\/g, '/');
|
|
1530
1632
|
}
|
|
1531
|
-
|
|
1633
|
+
/* O herói que o roteiro pediu do Iconify ("mdi:bread-slice"): o desenho é
|
|
1634
|
+
baixado aqui (fica em midia/icones); o que não baixar, a batida fica sem. */
|
|
1635
|
+
const icones = {};
|
|
1636
|
+
for (const pt of partes) {
|
|
1637
|
+
const h = typeof (pt.heroi != null ? pt.heroi : pt.icone) === 'string' ? String(pt.heroi != null ? pt.heroi : pt.icone).trim() : '';
|
|
1638
|
+
if (!/^[a-z0-9-]+:[a-z0-9-]+$/i.test(h) || icones[h]) continue;
|
|
1639
|
+
try { const ic = await assets.icone(h, { pasta: p.pasta }); if (ic && ic.ok) icones[h] = ic.svg; else projeto.anotar(p.id, 'icones', `herói ${h} não baixou: ${(ic && ic.error) || '?'}`); } catch { /* fica sem */ }
|
|
1640
|
+
}
|
|
1641
|
+
const r = gerativo.montarGerativo({ roteiro, largura, altura, tempos, narracao, sons, trilha, icones });
|
|
1532
1642
|
if (r.ok) {
|
|
1533
1643
|
const feitos = sonsLeves.prepararReceitas(p.pasta, r.somReceitas);
|
|
1534
1644
|
// Som que o ffmpeg não conseguiu fazer sai do plano (o vídeo não espera um arquivo que não existe).
|
|
1535
1645
|
r.plano.audio.efeitos = r.plano.audio.efeitos.filter((e) => Object.values(feitos).includes(e.src));
|
|
1536
1646
|
}
|
|
1537
1647
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo gerativo: ${r.resumo.batidas} batidas (${r.resumo.composicoes.join(' → ')}), ${r.resumo.segundos}s, semente ${r.resumo.semente}`);
|
|
1648
|
+
if (r.ok) conferirAssinatura(p, r, roteiro.assinatura || 'gerativo');
|
|
1538
1649
|
return r;
|
|
1539
1650
|
}
|
|
1540
1651
|
if (estilo === 'fluido' && modo === 'video') {
|
|
@@ -1562,6 +1673,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1562
1673
|
p.paletaFluido = escolha;
|
|
1563
1674
|
}
|
|
1564
1675
|
roteiro.paleta = p.paletaFluido;
|
|
1676
|
+
// O trio de fontes, também por projeto e sem repetir os últimos vídeos (09/10, "vício em fontes").
|
|
1677
|
+
if (!p.fontesFluido) { p.fontesFluido = fluido.escolherFontes(roteiro.fontesFluido); projeto.salvar(p.id, { fontesFluido: p.fontesFluido }); }
|
|
1678
|
+
roteiro.fontesFluido = p.fontesFluido;
|
|
1565
1679
|
/* As IMAGENS do fluido misto, só com imagem pedida: as partes com
|
|
1566
1680
|
"busca", no máximo 1 em 3 e espalhadas (fluido.js decide o jeito). */
|
|
1567
1681
|
const fotosFluido = {};
|
|
@@ -1580,6 +1694,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1580
1694
|
}
|
|
1581
1695
|
const r = fluido.montarFluido({ imagens: fotosFluido, roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1582
1696
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo fluido: ${r.resumo.batidas} batidas${r.resumo.imagens.length ? ` (imagens: ${r.resumo.imagens.join(', ')})` : ''}, paleta ${r.resumo.paleta}, ${r.resumo.segundos}s, batida atrasada no máximo ${r.resumo.atraso}s, palavra atrás da voz no máximo ${r.resumo.sincronia}s`);
|
|
1697
|
+
if (r.ok) conferirAssinatura(p, r, roteiro.assinatura || 'fluido');
|
|
1583
1698
|
return r;
|
|
1584
1699
|
}
|
|
1585
1700
|
if (estilo === 'motion' && modo === 'video') {
|
|
@@ -1898,5 +2013,5 @@ async function ferramentaSite(args) {
|
|
|
1898
2013
|
module.exports = { variarIcones,
|
|
1899
2014
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
1900
2015
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
1901
|
-
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro,
|
|
2016
|
+
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
1902
2017
|
};
|
|
@@ -284,6 +284,17 @@ function geometria(a, b, L, P) {
|
|
|
284
284
|
ponto e anda até onde o próximo traço começa) e os estados dela. */
|
|
285
285
|
function estadosDoAcento(P, L) {
|
|
286
286
|
const base = Math.min(P.largura, P.altura) / 1080;
|
|
287
|
+
/* O ACENTO EM TRAÇO (09/10, "todo vídeo tem aquela bolinha"): sem a semente
|
|
288
|
+
no meio da tela, sem a viagem em ponto e sem o recolher — cada traço se
|
|
289
|
+
desenha no seu lugar e some com a batida (acentoEm). */
|
|
290
|
+
const semBola = P.fundo && P.fundo.acento === 'traco';
|
|
291
|
+
if (semBola) {
|
|
292
|
+
const out = [];
|
|
293
|
+
P.batidas.forEach((b, bi) => b.acento.forEach((a) => out.push({ t: a.t, dur: a.dur, g: geometria(a, b, L, P), cor: b.cores.acento, batida: bi, forma: a.forma, ate: b.ate })));
|
|
294
|
+
out.sort((a, b) => a.t - b.t);
|
|
295
|
+
out.semBola = true;
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
287
298
|
const out = [{ t: 0, dur: 0.35, g: (() => { const r = 16 * base; const pts = []; for (let i = 0; i < N; i++) { const q = (i / (N - 1)) * Math.PI * 2; pts.push([P.largura / 2 + Math.cos(q) * r * 0.35, P.altura / 2 + Math.sin(q) * r * 0.35]); } return { pts, larg: r * 1.35, alfa: 1, frente: true, cx: P.largura / 2, cy: P.altura / 2 }; })(), cor: P.batidas[0].cores.acento, batida: 0 }];
|
|
288
299
|
P.batidas.forEach((b, bi) => {
|
|
289
300
|
b.acento.forEach((a, k) => {
|
|
@@ -295,12 +306,32 @@ function estadosDoAcento(P, L) {
|
|
|
295
306
|
}
|
|
296
307
|
out.push({ t: a.t, dur: a.dur, g, cor: b.cores.acento, batida: bi, forma: a.forma });
|
|
297
308
|
});
|
|
309
|
+
/* No fim da batida o acento se RECOLHE num ponto: ele ficava na última
|
|
310
|
+
forma até a batida seguinte, e a moldura sobrava sozinha na tela depois
|
|
311
|
+
que a palavra saía (teste de 08/10). */
|
|
312
|
+
const ult = b.acento[b.acento.length - 1];
|
|
313
|
+
if (ult && b.ate != null) {
|
|
314
|
+
const g = geometria(ult, b, L, P);
|
|
315
|
+
const t0 = Math.max(ult.t + (ult.dur || 0), b.ate - 0.3);
|
|
316
|
+
if (t0 < b.ate) out.push({ t: t0, dur: 0.25, g: geometria({ forma: 'viagem', x: g.cx, y: g.cy, r: 7 * base }, b, L, P), cor: b.cores.acento, batida: bi });
|
|
317
|
+
}
|
|
298
318
|
});
|
|
299
319
|
out.sort((a, b) => a.t - b.t);
|
|
300
320
|
return out;
|
|
301
321
|
}
|
|
302
322
|
|
|
303
323
|
function acentoEm(t, estados) {
|
|
324
|
+
if (estados.semBola) {
|
|
325
|
+
// O traço se desenha (os pontos aparecem em ordem) e some no fim da batida; antes do primeiro, nada.
|
|
326
|
+
let k = -1;
|
|
327
|
+
while (k < estados.length - 1 && estados[k + 1].t <= t) k++;
|
|
328
|
+
const cur = estados[k];
|
|
329
|
+
if (!cur) return { pts: [[0, 0], [0, 0]], larg: 0.1, alfa: 0, frente: true, cor: '#000000', cx: 0, cy: 0 };
|
|
330
|
+
const p = arrasto(c01((t - cur.t) / Math.max(0.01, cur.dur)));
|
|
331
|
+
const n = Math.max(2, Math.ceil(cur.g.pts.length * p));
|
|
332
|
+
const some = 1 - c01((t - (cur.ate - 0.22)) / 0.22);
|
|
333
|
+
return { pts: cur.g.pts.slice(0, n), larg: cur.g.larg, alfa: cur.g.alfa * some, frente: cur.g.frente, tampa: cur.g.tampa, cor: cur.cor, cx: cur.g.cx, cy: cur.g.cy };
|
|
334
|
+
}
|
|
304
335
|
let k = 0;
|
|
305
336
|
while (k < estados.length - 1 && estados[k + 1].t <= t) k++;
|
|
306
337
|
const cur = estados[k], prev = estados[Math.max(0, k - 1)];
|
|
@@ -650,7 +681,7 @@ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
|
|
|
650
681
|
return (
|
|
651
682
|
<div style={{ position: 'absolute', inset: 0, background: corBase, overflow: 'hidden' }}>
|
|
652
683
|
{circulo}
|
|
653
|
-
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />
|
|
684
|
+
{(f.ambiente || 'bokeh') !== 'limpo' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
|
|
654
685
|
{(f.particulas || []).map((q, i) => {
|
|
655
686
|
const x = ((q.x - cam.x * q.z * 1.6 + t * q.v * (i % 2 ? 1 : -0.6)) % W + W) % W;
|
|
656
687
|
const y = ((q.y - cam.y * q.z * 1.6 - t * q.v * 0.7) % H + H) % H;
|
|
@@ -773,25 +804,39 @@ function Heroi({ b, t, P, slot }) {
|
|
|
773
804
|
return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', opacity: op }}>{els}</svg>;
|
|
774
805
|
}
|
|
775
806
|
|
|
776
|
-
/*
|
|
777
|
-
a
|
|
807
|
+
/* A MOLDURA (o HUD do motion designer): cantos de enquadramento, o assunto do
|
|
808
|
+
vídeo, a SEÇÃO ("01 · A CONVERSA"), o timecode e a régua do vídeo — fixa do
|
|
809
|
+
começo ao fim, como no reel de referência. Mora dentro da área segura
|
|
810
|
+
(P.hud.area: no 9:16, longe do status e dos botões do celular). 'cantos' é
|
|
811
|
+
a versão mínima: os cantos e a seção. Partitura antiga (sem área): a margem de antes. */
|
|
778
812
|
const SECOES = { gigante: 'TESE', pilha: 'ARGUMENTO', divisao: 'CONTRASTE', contador: 'NÚMERO', cascata: 'LISTA', parede: 'TEXTURA', citacao: 'CITAÇÃO', marca: 'MARCA', palavras: 'FALA', produto: 'PRODUTO' };
|
|
779
813
|
function HudG({ P, b, bi, t, fps }) {
|
|
780
|
-
const W = P.largura, H = P.altura,
|
|
814
|
+
const W = P.largura, H = P.altura, h = P.hud, deitado = W >= H;
|
|
815
|
+
const m0 = Math.round(Math.min(W, H) * 0.035);
|
|
816
|
+
const A = h.area || { x: m0, topo: m0, base: m0 };
|
|
817
|
+
const tam = Math.max(11, Math.round(Math.min(W, H) * (deitado ? 0.0125 : 0.018)));
|
|
818
|
+
const c = Math.round(Math.min(W, H) * 0.026);
|
|
781
819
|
const cor = alfa(b.cores.texto, 0.72);
|
|
782
820
|
const f = Math.round(t * fps);
|
|
783
821
|
const tc = [Math.floor(t / 60), Math.floor(t) % 60, f % fps].map((n) => String(n).padStart(2, '0')).join(':');
|
|
784
|
-
const rot = { position: 'absolute', fontFamily: familia(P.fontes.mono), fontSize: tam, letterSpacing: '0.14em', color: cor, whiteSpace: 'nowrap', textTransform: 'uppercase' };
|
|
785
|
-
const
|
|
822
|
+
const rot = { position: 'absolute', fontFamily: familia(P.fontes.mono), fontSize: tam, letterSpacing: '0.14em', color: cor, whiteSpace: 'nowrap', textTransform: 'uppercase', lineHeight: 1 };
|
|
823
|
+
const L = A.x, T = A.topo, R = W - A.x, B = H - A.base;
|
|
824
|
+
const canto = (x, y, sx, sy) => <div key={x + '-' + y} style={{ position: 'absolute', left: sx > 0 ? x : x - c, top: sy > 0 ? y : y - c, width: c, height: c,
|
|
786
825
|
borderLeft: sx > 0 ? '1.5px solid ' + cor : undefined, borderRight: sx < 0 ? '1.5px solid ' + cor : undefined, borderTop: sy > 0 ? '1.5px solid ' + cor : undefined, borderBottom: sy < 0 ? '1.5px solid ' + cor : undefined }} />;
|
|
826
|
+
// A seção: o número e o nome dela (a partitura antiga não tem seção — vale a composição).
|
|
827
|
+
const k = b.secao != null ? b.secao : bi;
|
|
828
|
+
const nome = (h.secoes || [])[k] || SECOES[b.comp] || String(b.comp).toUpperCase();
|
|
829
|
+
const secao = <div style={{ ...rot, right: W - R + c * 0.6, top: T + c * 0.6 }}>{String(k + 1).padStart(2, '0') + ' · ' + nome}</div>;
|
|
830
|
+
if (h.tipo === 'cantos') return <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>{canto(L, T, 1, 1)}{canto(R, T, -1, 1)}{canto(L, B, 1, -1)}{canto(R, B, -1, -1)}{secao}</div>;
|
|
831
|
+
const n = Math.max(1, (h.secoes || []).length);
|
|
787
832
|
return (
|
|
788
833
|
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
|
789
|
-
{canto(
|
|
790
|
-
<div style={{ ...rot, left:
|
|
791
|
-
|
|
792
|
-
<div style={{ ...rot, left:
|
|
793
|
-
<div style={{ ...rot, right:
|
|
794
|
-
<div style={{ position: 'absolute', left:
|
|
834
|
+
{canto(L, T, 1, 1)}{canto(R, T, -1, 1)}{canto(L, B, 1, -1)}{canto(R, B, -1, -1)}
|
|
835
|
+
<div style={{ ...rot, left: L + c * 0.6, top: T + c * 0.6 }}><b>{h.marca}</b>{deitado && <span style={{ opacity: 0.6 }}>{' · ' + h.ano}</span>}</div>
|
|
836
|
+
{secao}
|
|
837
|
+
<div style={{ ...rot, left: L + c * 0.6, bottom: H - B + c * 0.6 }}>{tc}<span style={{ opacity: 0.55 }}>{' ' + fps + ' FPS'}</span></div>
|
|
838
|
+
<div style={{ ...rot, right: W - R + c * 0.6, bottom: H - B + c * 0.6 }}>{(h.secoes ? String(k + 1).padStart(2, '0') + ' / ' + String(n).padStart(2, '0') : W + '×' + H)}</div>
|
|
839
|
+
<div style={{ position: 'absolute', left: L + c * 0.6, top: B - c * 0.35, height: 1.5, width: (R - L - c * 1.2) * c01(t / P.duracao), background: cor }} />
|
|
795
840
|
</div>
|
|
796
841
|
);
|
|
797
842
|
}
|
|
@@ -806,6 +851,9 @@ export function Gerativo({ el, t, fps }) {
|
|
|
806
851
|
if (!L) return <div style={{ position: 'absolute', inset: 0, background: P.paleta.fundo }} />;
|
|
807
852
|
const frame = Math.round(t * fps);
|
|
808
853
|
const bs = P.batidas;
|
|
854
|
+
/* O AMBIENTE (09/10): o bokeh e o feixe de luz só quando a identidade pede
|
|
855
|
+
(P.fundo.ambiente). Partitura antiga, sem o campo: como antes (ambiente || 'bokeh'). */
|
|
856
|
+
const ambiente = P.fundo.ambiente || 'bokeh', comLuz = ambiente !== 'limpo';
|
|
809
857
|
let bi = 0;
|
|
810
858
|
while (bi < bs.length - 1 && t >= bs[bi + 1].de) bi++;
|
|
811
859
|
const b = bs[bi];
|
|
@@ -914,18 +962,39 @@ export function Gerativo({ el, t, fps }) {
|
|
|
914
962
|
if (W - M - bl.x1 >= R * 2.3) slot = { cx: bl.x1 + (W - M - bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R };
|
|
915
963
|
else if (bl.y - M * 1.4 >= R * 2.2) slot = { cx: W / 2, cy: M * 1.4 + (bl.y - M * 1.4) / 2, R };
|
|
916
964
|
else if (H - M * 1.4 - bl.y1 >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + (H - M * 1.4 - bl.y1) / 2, R };
|
|
917
|
-
|
|
965
|
+
// Sem vazio, não entra (09/10): o anel no meio do texto era enfeite que ninguém pediu.
|
|
966
|
+
else return null;
|
|
918
967
|
return <Heroi b={x} t={t} P={P} slot={slot} />;
|
|
919
968
|
})()}
|
|
920
969
|
<Textura b={x} cx={lay.caixas[x.linhas[0].id]} t={t} P={P} periodo={lay.textura} />
|
|
921
970
|
{x.icone3d && (() => {
|
|
922
|
-
/* O ícone 3D mora no vazio do bloco
|
|
923
|
-
(
|
|
971
|
+
/* O ícone 3D mora no vazio do bloco, no lugar que o projeto escolheu
|
|
972
|
+
(ic.lados, gerativo.js: direita, esquerda, acima, abaixo ou canto);
|
|
973
|
+
sem espaço lá, tenta o próximo; sem nenhum, não entra — nunca
|
|
974
|
+
por cima do texto. Partitura antiga (sem lados): direita ou acima. */
|
|
924
975
|
const ic = x.icone3d, tam = ic.tamanho, M = P.margem, bl = lay.bloco;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
976
|
+
const xTopo = x.bloco.alinha === 'centro' ? (bl.x + bl.x1) / 2 - tam / 2 : bl.x - tam * 0.08;
|
|
977
|
+
const fora = (qx, qy) => qx + tam <= bl.x - 8 || qx >= bl.x1 + 8 || qy + tam <= bl.y - 8 || qy >= bl.y1 + 8;
|
|
978
|
+
const LUGAR = {
|
|
979
|
+
'direita': () => (W - M - bl.x1 >= tam * 0.9 ? [Math.min(W - M - tam, bl.x1 + (W - M - bl.x1 - tam) / 2), (bl.y + bl.y1) / 2 - tam / 2] : null),
|
|
980
|
+
'esquerda': () => (bl.x - M >= tam * 0.9 ? [Math.max(M * 0.6, M + (bl.x - M - tam) / 2), (bl.y + bl.y1) / 2 - tam / 2] : null),
|
|
981
|
+
'acima': () => (bl.y - M * 1.2 >= tam * 0.85 ? [xTopo, Math.max(M, bl.y - tam - 0.02 * H)] : null),
|
|
982
|
+
'abaixo': () => (H - M * 1.2 - bl.y1 >= tam * 0.85 ? [xTopo, Math.min(H - M - tam, bl.y1 + 0.02 * H)] : null),
|
|
983
|
+
// O canto livre mais longe do texto: o ícone pequeno, de assinatura.
|
|
984
|
+
'canto': () => [[W - M - tam, M], [W - M - tam, H - M - tam], [M, M], [M, H - M - tam]].find(([qx, qy]) => fora(qx, qy)) || null,
|
|
985
|
+
};
|
|
986
|
+
const ordem = ic.lados || [ic.lado, 'acima'];
|
|
987
|
+
const achou = ordem.map((l) => LUGAR[l] && LUGAR[l]()).find(Boolean);
|
|
988
|
+
if (!achou) return null;
|
|
989
|
+
const [px, py] = achou;
|
|
990
|
+
/* O ÍCONE PLANO (09/10): o do Iconify que o roteiro pediu (o pão, o QR
|
|
991
|
+
code), na cor do acento da identidade — entra em mola e flutua de leve. */
|
|
992
|
+
if (ic.svg) {
|
|
993
|
+
const k = MOLA(t - ic.t), fl = Math.sin((t - ic.t) * 1.3) * 0.012 * tam, sai = c01((t - (x.ate - 0.25)) / 0.25);
|
|
994
|
+
return <div style={{ position: 'absolute', left: Math.round(px), top: Math.round(py + fl), width: tam, height: tam, color: ic.cor, opacity: c01(k * 1.4) * (1 - sai),
|
|
995
|
+
transform: 'translateZ(120px) scale(' + (0.6 + 0.4 * k).toFixed(4) + ') rotate(' + ((1 - k) * -8).toFixed(2) + 'deg)', transformOrigin: '50% 60%' }}
|
|
996
|
+
dangerouslySetInnerHTML={{ __html: ic.svg }} />;
|
|
997
|
+
}
|
|
929
998
|
return <div style={{ position: 'absolute', inset: 0, transform: 'translateZ(150px)' }}><Icone3D el={{ ...ic, x: Math.round(px), y: Math.round(py) }} t={t} /></div>;
|
|
930
999
|
})()}
|
|
931
1000
|
{x.linhas.map((l, li) => {
|
|
@@ -961,7 +1030,7 @@ export function Gerativo({ el, t, fps }) {
|
|
|
961
1030
|
</div>
|
|
962
1031
|
{/* A FRENTE: manchas de luz que passam perto da lente (bokeh) e o feixe
|
|
963
1032
|
de luz que varre o quadro quando uma batida chega. */}
|
|
964
|
-
{[0, 1, 2, 3].map((k) => {
|
|
1033
|
+
{ambiente === 'bokeh' && [0, 1, 2, 3].map((k) => {
|
|
965
1034
|
const per = 5 + acaso(P.semente % 997 + k * 7) * 4, fase = acaso(P.semente % 991 + k * 13) * per;
|
|
966
1035
|
const u = ((t + fase) % per) / per;
|
|
967
1036
|
const tam = Math.min(W, H) * (0.28 + acaso(k * 17 + 3) * 0.3);
|
|
@@ -970,7 +1039,7 @@ export function Gerativo({ el, t, fps }) {
|
|
|
970
1039
|
return <div key={'bk' + k} style={{ position: 'absolute', left: x - tam / 2, top: y - tam / 2, width: tam, height: tam, borderRadius: '50%', pointerEvents: 'none',
|
|
971
1040
|
background: 'radial-gradient(circle, ' + alfa(cor, k % 2 ? 0.16 : 0.08) + ' 0%, transparent 66%)', opacity: Math.sin(Math.PI * u) }} />;
|
|
972
1041
|
})}
|
|
973
|
-
{(() => {
|
|
1042
|
+
{comLuz && (() => {
|
|
974
1043
|
const k = c01((t - b.de) / 0.95);
|
|
975
1044
|
if (k <= 0 || k >= 1) return null;
|
|
976
1045
|
const x = lerp(-0.7 * W, 1.2 * W, ed(k));
|
|
@@ -735,7 +735,64 @@ export function Site({ el, t, fps }) {
|
|
|
735
735
|
);
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
+
// ── o print do site de verdade, em 3D ───────────────────────────────────
|
|
739
|
+
/* A peça `site` acima DESENHA um site genérico; esta mostra o print real
|
|
740
|
+
(print-site.js), numa janela de navegador ou num celular, inclinados em
|
|
741
|
+
perspectiva, com espessura, reflexo e a página rolando devagar.
|
|
742
|
+
"proporcao" é altura/largura do print: é ela que diz quanto há para rolar.
|
|
743
|
+
Só CSS 3D — o three.js entraria aqui só para o mesmo plano com textura. */
|
|
744
|
+
export function PrintSite({ el, t, fps }) {
|
|
745
|
+
const W = el.largura || 1280, H = el.altura || (el.celular ? 900 : 820);
|
|
746
|
+
const t0 = el.t || 0;
|
|
747
|
+
const cel = !!el.celular;
|
|
748
|
+
const m = mola(t, t0, fps, { damping: 30, stiffness: 90, mass: 1 });
|
|
749
|
+
const inc = el.inclinacao ?? (cel ? 14 : 18); // graus de giro em Y no repouso
|
|
750
|
+
const deriva = Math.sin((t - t0) * 0.6) * 3; // a câmera que respira, sem parar
|
|
751
|
+
const rotY = lerp(-48, -inc, m) + deriva;
|
|
752
|
+
const rotX = lerp(22, cel ? 6 : 9, m);
|
|
753
|
+
const z = lerp(-700, 0, m);
|
|
754
|
+
const barra = cel ? 0 : Math.round(H * 0.06); // a barra do navegador
|
|
755
|
+
const borda = cel ? Math.round(W * 0.045) : 0; // a moldura do celular
|
|
756
|
+
const telaW = W - borda * 2, telaH = H - barra - borda * 2;
|
|
757
|
+
// A página rola de cima até o fim, com respiro parado nas duas pontas.
|
|
758
|
+
const total = Math.max(0, telaW * (el.proporcao || 0) - telaH);
|
|
759
|
+
const dur = el.duracao || 6;
|
|
760
|
+
const k = el.rolar === false ? 0 : CURVAS_KIT.camera(entre(t, t0 + 1.0, t0 + dur - 0.6));
|
|
761
|
+
const brilho = 30 + 50 * entre(t, t0, t0 + dur);
|
|
762
|
+
const raio = cel ? Math.round(W * 0.14) : Math.round(H * 0.018);
|
|
763
|
+
return (
|
|
764
|
+
<div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, perspective: 2400, opacity: t >= t0 ? Math.min(1, m * 2) : 0 }}>
|
|
765
|
+
<div style={{ position: 'absolute', inset: 0, transformStyle: 'preserve-3d',
|
|
766
|
+
transform: 'translateZ(' + z + 'px) rotateX(' + rotX + 'deg) rotateY(' + rotY + 'deg)' }}>
|
|
767
|
+
{/* A espessura: duas placas atrás, mais escuras — o aparelho tem corpo. */}
|
|
768
|
+
{[18, 9].map((d) => (
|
|
769
|
+
<div key={d} style={{ position: 'absolute', inset: 0, borderRadius: raio, background: d > 10 ? '#0b0c10' : '#1d1f26', transform: 'translateZ(' + -d + 'px)' }} />
|
|
770
|
+
))}
|
|
771
|
+
<div style={{ position: 'absolute', inset: 0, borderRadius: raio, overflow: 'hidden', background: cel ? '#0e0f13' : '#f4f4f6',
|
|
772
|
+
boxShadow: '0 80px 160px -40px rgba(0,0,0,0.55), 0 30px 60px -30px rgba(0,0,0,0.4)' }}>
|
|
773
|
+
{cel ? null : (
|
|
774
|
+
<div style={{ height: barra, display: 'flex', alignItems: 'center', gap: barra * 0.18, padding: '0 ' + barra * 0.4 + 'px', background: '#e9e9ee' }}>
|
|
775
|
+
{['#ff5f57', '#febc2e', '#28c840'].map((c) => <div key={c} style={{ width: barra * 0.24, height: barra * 0.24, borderRadius: 99, background: c }} />)}
|
|
776
|
+
<div style={{ marginLeft: barra * 0.4, flex: 1, maxWidth: W * 0.5, height: barra * 0.5, borderRadius: 99, background: '#fff', display: 'flex', alignItems: 'center',
|
|
777
|
+
padding: '0 ' + barra * 0.35 + 'px', fontFamily: MONO(), fontSize: barra * 0.26, color: '#55585f', whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
|
778
|
+
{String(el.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '')}
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
)}
|
|
782
|
+
<div style={{ position: 'absolute', left: borda, top: barra + borda, width: telaW, height: telaH, overflow: 'hidden', borderRadius: cel ? raio * 0.75 : 0, background: '#fff' }}>
|
|
783
|
+
<Img src={staticFile(el.src)} style={{ position: 'absolute', left: 0, top: 0, width: telaW, transform: 'translateY(' + (-total * k).toFixed(1) + 'px)' }} />
|
|
784
|
+
</div>
|
|
785
|
+
{cel ? <div style={{ position: 'absolute', left: '50%', top: borda * 1.3, width: W * 0.28, height: borda * 1.1, marginLeft: -W * 0.14, borderRadius: 99, background: '#0e0f13' }} /> : null}
|
|
786
|
+
{/* O reflexo que atravessa o vidro enquanto o aparelho gira. */}
|
|
787
|
+
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none',
|
|
788
|
+
background: 'linear-gradient(115deg, rgba(255,255,255,0) ' + (brilho - 18) + '%, rgba(255,255,255,0.16) ' + brilho + '%, rgba(255,255,255,0) ' + (brilho + 18) + '%)' }} />
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
</div>
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
738
795
|
export const MARCA = {
|
|
739
796
|
orbe: Orbe, vetor: Vetor, manchete: Manchete, cromo: Cromo, cores: Cores, padrao: Padrao, particulas: Particulas,
|
|
740
|
-
estrela: Estrela, cartoes: Cartoes, painel: Painel, esfera: Esfera, grade: Grade, site: Site,
|
|
797
|
+
estrela: Estrela, cartoes: Cartoes, painel: Painel, esfera: Esfera, grade: Grade, site: Site, printsite: PrintSite,
|
|
741
798
|
};
|
package/lib/estudio/kit/tres.jsx
CHANGED
|
@@ -278,8 +278,11 @@ const FORMAS = {
|
|
|
278
278
|
};
|
|
279
279
|
|
|
280
280
|
function Mat({ tipo, cor }) {
|
|
281
|
-
// Metal escovado, bala (verniz) ou argila —
|
|
281
|
+
// Metal escovado, bala (verniz), pérola, acetinado ou argila — todos refletem a sala (Ambiente).
|
|
282
282
|
if (tipo === 'metal') return <meshPhysicalMaterial color={cor} roughness={0.32} metalness={0.85} clearcoat={0.4} envMapIntensity={1.2} />;
|
|
283
|
+
// Pérola (iridescente) e acetinado (meio metal, meio fosco): o cardápio de 09/10 (gerativo.MATERIAIS_ICONE).
|
|
284
|
+
if (tipo === 'perola') return <meshPhysicalMaterial color={cor} roughness={0.25} metalness={0.1} clearcoat={1} clearcoatRoughness={0.1} iridescence={1} iridescenceIOR={1.3} />;
|
|
285
|
+
if (tipo === 'acetinado') return <meshPhysicalMaterial color={cor} roughness={0.5} metalness={0.45} clearcoat={0.4} />;
|
|
283
286
|
if (tipo === 'verniz') return <meshPhysicalMaterial color={cor} roughness={0.18} metalness={0} clearcoat={1} clearcoatRoughness={0.08} sheen={0.3} />;
|
|
284
287
|
return <meshPhysicalMaterial color={cor} roughness={0.72} metalness={0} clearcoat={0.15} />; // argila: fosca, macia
|
|
285
288
|
}
|
package/lib/estudio/kit.js
CHANGED
|
@@ -49,6 +49,8 @@ const PECAS = {
|
|
|
49
49
|
esfera: {},
|
|
50
50
|
grade: {},
|
|
51
51
|
site: {},
|
|
52
|
+
// O print REAL do site (print-site.js) numa janela ou num celular em 3D, rolando.
|
|
53
|
+
printsite: { precisa: ['src'] },
|
|
52
54
|
// O produto em 3D (kit/tres.jsx): fone, celular, lata ou caixa, de geometria.
|
|
53
55
|
produto3d: { precisa: ['modelo'] },
|
|
54
56
|
// Os ícones 3D de geometria (kit/tres.jsx): raio, coração, bateria, relógio, fone…
|
package/lib/estudio/plano.js
CHANGED
|
@@ -54,7 +54,9 @@ const kit = require('./kit.js');
|
|
|
54
54
|
const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
|
|
55
55
|
const FORMAS = new Set(['retangulo', 'circulo', 'linha', 'poligono', 'traco']);
|
|
56
56
|
const AJUSTES = new Set(['cover', 'contain', 'preencher']);
|
|
57
|
-
const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash', 'liquido'
|
|
57
|
+
const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash', 'liquido',
|
|
58
|
+
// as 3D, que mexem nas duas cenas (remotion.js, estilo3d)
|
|
59
|
+
'virar', 'cubo', 'profundidade', 'inclinar']);
|
|
58
60
|
|
|
59
61
|
/* As propriedades que a animação por quadro-chave alcança. Cada uma é um
|
|
60
62
|
número no tempo; a curva entre dois quadros é a suavização declarada.
|
|
@@ -248,6 +250,8 @@ function conferirElemento(caminho, el, pasta, problemas, duracao) {
|
|
|
248
250
|
for (const oque of kit.conferirPeca(el)) problemas.push(erro(`${caminho}.peca`, oque));
|
|
249
251
|
// A logo de uma peça é arquivo da pasta, como a imagem.
|
|
250
252
|
if (el.logo) conferirArquivo(`${caminho}.logo`, el.logo, pasta, problemas);
|
|
253
|
+
// E o print do site (printsite) também: sem o arquivo, a janela 3D sai vazia.
|
|
254
|
+
if (el.src) conferirArquivo(`${caminho}.src`, el.src, pasta, problemas);
|
|
251
255
|
break;
|
|
252
256
|
}
|
|
253
257
|
}
|