primocode 10.0.9 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/primocode.js +25 -28
- package/lib/act.js +5 -2
- package/lib/catalogo.js +7 -4
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +86 -6
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +214 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/edicao.js +36 -17
- package/lib/estudio/estilo.js +35 -8
- package/lib/estudio/fluido.js +352 -0
- package/lib/estudio/gerativo.js +21 -3
- package/lib/estudio/hibrido.js +16 -12
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +245 -42
- package/lib/estudio/kit/fluido.jsx +614 -0
- package/lib/estudio/kit/gerativo.jsx +15 -5
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/tres.jsx +88 -21
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +3 -4
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +36 -25
- package/lib/estudio/remotion.js +53 -6
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +140 -8
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +3 -5
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/voz_kokoro.py +98 -0
package/lib/estudio/estilo.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* hibrido O PADRÃO do vídeo: o cinema com o kit por cima — foto e vídeo reais
|
|
15
15
|
* que não param, e o motion por cima (hibrido.js)
|
|
16
16
|
* composicao a composição antiga, uma cena por parte (composicao.js)
|
|
17
|
+
* fluido minimalista: uma ideia por batida, a forma de uma vira a da próxima,
|
|
18
|
+
* sem corte (fluido.js). É o que "fluido", "minimalista", "clean" e "apple" pedem.
|
|
17
19
|
*
|
|
18
20
|
* Quem manda, em ordem: o estilo que o AGENTE pediu; o do roteiro (a menos
|
|
19
21
|
* que o pedido diga motion); o que o pedido descreve; e o motion.
|
|
@@ -21,15 +23,18 @@
|
|
|
21
23
|
|
|
22
24
|
'use strict';
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const variedade = require('./variedade.js');
|
|
27
|
+
|
|
28
|
+
const ESTILOS = ['hibrido', 'gerativo', 'motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao', 'fluido'];
|
|
25
29
|
// Nomes que o modelo usa para os mesmos estilos.
|
|
26
30
|
/* "motion" agora é o GERATIVO ("não pode ter template", 29–30/09); o motion
|
|
27
31
|
antigo segue pelo nome "classico" (e "limpo", o nome mais antigo dele). */
|
|
28
32
|
const APELIDOS = { motion: 'gerativo', generativo: 'gerativo', kinetic: 'gerativo', cinetico: 'gerativo', 'cinético': 'gerativo',
|
|
29
33
|
tipografico: 'gerativo', 'tipográfico': 'gerativo', animacao: 'gerativo', 'animação': 'gerativo',
|
|
30
34
|
classico: 'motion', 'clássico': 'motion', 'motion-classico': 'motion', motion3d: 'motion',
|
|
31
|
-
absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
|
|
35
|
+
absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', minimalista: 'fluido', minimal: 'fluido', clean: 'fluido', apple: 'fluido', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
|
|
32
36
|
|
|
37
|
+
const PEDE_FLUIDO = /\bfluid[oa]\b|minimalista|\bminimal\b|\bclean\b|\bapple\b/i;
|
|
33
38
|
const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
|
|
34
39
|
const PEDE_VOX = /\bvox\b|explica|explainer|document[aá]rio|editorial|hist[oó]ria d[aoe]s?\b/i;
|
|
35
40
|
const PEDE_MARCA = /\bbrand|identidade visual|revela[çc][ãa]o d[ae] marca|\blogo(tipo|marca)?\b|vinheta de marca/i;
|
|
@@ -43,24 +48,46 @@ const normal = (e) => {
|
|
|
43
48
|
return ESTILOS.includes(n) ? n : null;
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/* O RODÍZIO: quando ninguém escolheu ("vários estilos... que não sejam padrão
|
|
52
|
+
da IA", Joel 05/10), o padrão não é sempre o híbrido — gira entre os que
|
|
53
|
+
servem a qualquer roteiro, sem repetir os últimos (memória 'estilo'). */
|
|
54
|
+
/* Com imagem pedida (08/10): o padrão é o FLUIDO MISTO — motion com a
|
|
55
|
+
imagem em poucos momentos, cada uma entrando de um jeito. O híbrido e o
|
|
56
|
+
cinema ("10 mil imagens de banco e a mesma animação") ficam pelo nome. */
|
|
57
|
+
const RODIZIO = ['fluido'];
|
|
58
|
+
/* Sem imagem (o padrão desde 07/10), o rodízio fica nos dois motions que
|
|
59
|
+
nasceram das referências boas (motions-exemplos/motion-god): o fluido e o
|
|
60
|
+
gerativo. O impacto (emoji 3D em cada batida, cara de anúncio) saiu do
|
|
61
|
+
rodízio em 08/10 — "horríveis, nada a ver com as referências" — e fica
|
|
62
|
+
para quem o pede pelo nome (o agente ou o roteiro). */
|
|
63
|
+
const RODIZIO_SEM_IMAGEM = ['fluido', 'gerativo'];
|
|
64
|
+
|
|
65
|
+
function estiloDaVez(aleatorio = Math.random, semImagem = false) {
|
|
66
|
+
const lista = semImagem ? RODIZIO_SEM_IMAGEM : RODIZIO;
|
|
67
|
+
return variedade.sortear(lista, 'estilo', { aleatorio }) || lista[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, semImagem = false, aleatorio } = {}) {
|
|
47
71
|
const pedidoAgente = normal(agente);
|
|
48
72
|
if (pedidoAgente) return pedidoAgente;
|
|
49
73
|
const doRoteiro = normal(roteiro);
|
|
50
74
|
/* Pedir MOTION (ou animação) é pedir o gerativo — antes do vox, da marca e
|
|
51
75
|
do lançamento, que são leiaute fixo ("não pode ter template", 30/09). */
|
|
52
|
-
if (doRoteiro && !(doRoteiro !== 'gerativo' && PEDE_MOTION.test(pedido))) return doRoteiro;
|
|
76
|
+
if (doRoteiro && !(doRoteiro !== 'gerativo' && doRoteiro !== 'fluido' && PEDE_MOTION.test(pedido))) return doRoteiro;
|
|
77
|
+
// O fluido é um jeito de motion: "motion minimalista" é o fluido, não o gerativo.
|
|
78
|
+
if (PEDE_FLUIDO.test(pedido)) return 'fluido';
|
|
53
79
|
if (PEDE_MOTION.test(pedido)) return 'gerativo';
|
|
54
80
|
if (PEDE_VOX.test(pedido)) return 'vox';
|
|
55
81
|
if (PEDE_MARCA.test(pedido)) return 'marca';
|
|
56
82
|
if (PEDE_LANCAMENTO.test(pedido)) return 'lancamento';
|
|
57
83
|
/* Anúncio, reels e o resto: o HÍBRIDO. "Um mix da versão antiga (o reels
|
|
58
84
|
da imersão, no cinema) e da última (o kit) — absurdo, que nem parece
|
|
59
|
-
que foi IA" (29/09).
|
|
60
|
-
return
|
|
85
|
+
que foi IA" (29/09). Com `girar`, o padrão roda (RODIZIO). */
|
|
86
|
+
if (girar) return estiloDaVez(aleatorio, semImagem);
|
|
87
|
+
return semImagem ? 'gerativo' : 'hibrido';
|
|
61
88
|
}
|
|
62
89
|
|
|
63
90
|
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
64
|
-
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento']);
|
|
91
|
+
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
|
|
65
92
|
|
|
66
|
-
module.exports = { escolherEstilo, ESTILOS, LEVES, PEDE_MOTION };
|
|
93
|
+
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM };
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fluido.js — o estilo FLUIDO: uma ideia por batida, muito respiro, e nada
|
|
3
|
+
* de corte. O elemento de uma batida VIRA o da próxima.
|
|
4
|
+
*
|
|
5
|
+
* "Uma ideia por quadro, muito espaço vazio. Texto pequeno, caixa normal,
|
|
6
|
+
* sans limpa. Movimento contínuo: um elemento se transforma no próximo."
|
|
7
|
+
* — a referência de motion do Joel (05/10), os vídeos bons.
|
|
8
|
+
*
|
|
9
|
+
* Existe UMA forma que vive o vídeo inteiro (o "portador"): é o ponto da
|
|
10
|
+
* abertura, a pílula do título, o anel do número, a primeira pílula da lista,
|
|
11
|
+
* a linha que risca o "não", o cartão da citação e o primeiro círculo da
|
|
12
|
+
* marca no fim. Entre uma batida e outra ela muda de lugar, de tamanho, de
|
|
13
|
+
* raio e de preenchimento — o olho segue a forma, e o corte some.
|
|
14
|
+
*
|
|
15
|
+
* O texto entra palavra por palavra (stagger curto, ease-out forte, sem
|
|
16
|
+
* linear) e cada frase tem UM acento: uma palavra em serifa itálica na cor de
|
|
17
|
+
* acento. Fundo em gradiente grande e macio, de uma paleta curta (no máximo
|
|
18
|
+
* dois acentos e os neutros), clara ou escura.
|
|
19
|
+
*
|
|
20
|
+
* Proibido aqui, de propósito (é a lista do "cara de IA"): HUD, cantoneira,
|
|
21
|
+
* timecode, caixa na palavra-chave, neon, partícula e CAIXA ALTA.
|
|
22
|
+
*
|
|
23
|
+
* Puro: devolve a partitura (os testes olham) e o plano com UMA peça do kit
|
|
24
|
+
* (kit/fluido.jsx), que desenha a partitura quadro a quadro.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const { volumesDaTrilha } = require('./ritmo.js');
|
|
30
|
+
|
|
31
|
+
const FPS = 30;
|
|
32
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
33
|
+
const POR_PALAVRA = 0.3, MINIMO = 0.8; // o tempo de leitura: 0,3 s por palavra, nunca menos de 0,8 s
|
|
34
|
+
const STAGGER = 0.07; // entre uma palavra e a próxima (a que a voz não diz)
|
|
35
|
+
const ANTES = 0.12; // a batida troca isto antes da primeira palavra da frase
|
|
36
|
+
|
|
37
|
+
/* As paletas: um fundo em três tons (o gradiente), a tinta, a tinta suave e
|
|
38
|
+
NO MÁXIMO dois acentos (o da palavra e o brilho difuso). */
|
|
39
|
+
const PALETAS = {
|
|
40
|
+
noite: { escuro: true, fundo: ['#020615', '#061a5c', '#1240c9'], tinta: '#eef2ff', suave: '#9fb0d9', acentos: ['#8fb8ff', '#2f6bff'] },
|
|
41
|
+
/* As CHAPADAS das referências boas (08/10): fundo de uma cor só, sem o
|
|
42
|
+
gradiente — o laranja da "criatividade", o branco com azul, o preto puro
|
|
43
|
+
e o cobalto. O "sempre o mesmo azul" era vício. */
|
|
44
|
+
laranja: { escuro: false, fundo: ['#f2621f', '#f06a28', '#ec7a3c'], tinta: '#1a0d05', suave: '#5a2a10', acentos: ['#fff4ea', '#ffd2b0'] },
|
|
45
|
+
branco: { escuro: false, fundo: ['#ffffff', '#f6f7fa', '#eceff6'], tinta: '#0b0d12', suave: '#6b7180', acentos: ['#1f4bff', '#9fb3ff'] },
|
|
46
|
+
preto: { escuro: true, fundo: ['#000000', '#050505', '#121212'], tinta: '#f5f5f5', suave: '#8a8a8a', acentos: ['#ff7a3d', '#ff9a66'] },
|
|
47
|
+
cobalto: { escuro: true, fundo: ['#1238e6', '#1640f0', '#2350ff'], tinta: '#ffffff', suave: '#c8d4ff', acentos: ['#ffe14d', '#ffffff'] },
|
|
48
|
+
breu: { escuro: true, fundo: ['#030304', '#0b0a12', '#2a2156'], tinta: '#f4f4f6', suave: '#8d8a9c', acentos: ['#b4a6ff', '#5b45ff'] },
|
|
49
|
+
lavanda: { escuro: false, fundo: ['#f5f4ff', '#e4e1ff', '#b9b2ff'], tinta: '#17153a', suave: '#6e6a92', acentos: ['#4a3ce0', '#8f86ff'] },
|
|
50
|
+
creme: { escuro: false, fundo: ['#fbf8f2', '#f2e8d8', '#e7c9a5'], tinta: '#1e1a15', suave: '#857a6c', acentos: ['#c2502a', '#f0a36b'] },
|
|
51
|
+
};
|
|
52
|
+
const ESCURAS = ['noite', 'breu', 'preto', 'cobalto'], CLARAS = ['lavanda', 'creme', 'laranja', 'branco'];
|
|
53
|
+
|
|
54
|
+
const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
|
|
55
|
+
|
|
56
|
+
/** A paleta: a pedida pelo nome; senão o tema (claro/escuro); senão a semente. */
|
|
57
|
+
function paletaDe(roteiro, semente) {
|
|
58
|
+
const pedida = String(roteiro.paleta || '').toLowerCase();
|
|
59
|
+
const tema = String(roteiro.tema || '').toLowerCase();
|
|
60
|
+
const nome = PALETAS[pedida] ? pedida
|
|
61
|
+
: /claro|light/.test(tema) ? CLARAS[semente % CLARAS.length]
|
|
62
|
+
: /escuro|dark/.test(tema) ? ESCURAS[semente % ESCURAS.length]
|
|
63
|
+
: Object.keys(PALETAS)[semente % Object.keys(PALETAS).length];
|
|
64
|
+
const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
|
|
65
|
+
// A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
|
|
66
|
+
const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
|
|
67
|
+
if (/^#[0-9a-f]{6}$/i.test(dest || '')) p.acentos[0] = dest;
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Caixa alta não entra: frase GRITADA vira caixa normal (sigla curta fica). */
|
|
72
|
+
function caixaNormal(t) {
|
|
73
|
+
const s = String(t || '').trim();
|
|
74
|
+
const letras = s.replace(/[^A-Za-zÀ-ÿ]/g, '');
|
|
75
|
+
if (letras.length < 4 || letras !== letras.toUpperCase()) return s;
|
|
76
|
+
const m = s.toLowerCase();
|
|
77
|
+
return m.charAt(0).toUpperCase() + m.slice(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const VAZIAS = new Set('a o as os um uma de da do das dos e é em no na nos nas que com por para pra se seu sua você ao à mais muito isso este esta não sim'.split(' '));
|
|
81
|
+
const limpa = (w) => w.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
|
|
82
|
+
|
|
83
|
+
/** A frase em palavras, com UM acento: o destaque pedido, ou a palavra de mais peso. */
|
|
84
|
+
function frase(texto, { destaque = '', semAcento = false } = {}) {
|
|
85
|
+
const ws = caixaNormal(texto).split(/\s+/).filter(Boolean);
|
|
86
|
+
let ac = -1;
|
|
87
|
+
if (!semAcento && ws.length > 1) {
|
|
88
|
+
const d = limpa(String(destaque).split(/\s+/)[0] || '');
|
|
89
|
+
if (d) ac = ws.findIndex((w) => limpa(w) === d);
|
|
90
|
+
if (ac < 0) {
|
|
91
|
+
// A palavra mais longa que não é vazia; no empate, a mais tarde (a frase termina nela).
|
|
92
|
+
let melhor = 0;
|
|
93
|
+
ws.forEach((w, i) => { const n = limpa(w).length; if (!VAZIAS.has(limpa(w)) && n >= 4 && n >= melhor) { melhor = n; ac = i; } });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return ws.map((t, i) => ({ t, acento: i === ac }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const numeroDe = (v) => {
|
|
100
|
+
const m = String(v == null ? '' : v).match(/^(\D*)([\d.,]+)(.*)$/);
|
|
101
|
+
if (!m) return null;
|
|
102
|
+
const n = Number(m[2].replace(/\./g, '').replace(',', '.'));
|
|
103
|
+
return Number.isFinite(n) ? { antes: m[1], ate: n, depois: m[3], casas: /,\d/.test(m[2]) ? 1 : 0 } : null;
|
|
104
|
+
};
|
|
105
|
+
const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
|
|
106
|
+
const leitura = (n) => Math.max(MINIMO, POR_PALAVRA * n);
|
|
107
|
+
|
|
108
|
+
/* A forma do portador em cada tipo de parte (kit/fluido.jsx dá o tamanho). */
|
|
109
|
+
const FORMA = { abertura: 'ponto', titulo: 'pilula', numero: 'anel', lista: 'pilula', passos: 'ponto', contraste: 'linha', citacao: 'cartao', cta: 'marca' };
|
|
110
|
+
const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: 'lista', cores: 'lista' };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
|
|
114
|
+
* a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
|
|
115
|
+
*/
|
|
116
|
+
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], imagens = {} }) {
|
|
117
|
+
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
|
|
118
|
+
const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
|
|
119
|
+
const paleta = paletaDe(roteiro, semente);
|
|
120
|
+
const marca = roteiro.marca || {};
|
|
121
|
+
const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
|
|
122
|
+
/* O comando do terminal: o link da cta, se for comando; senão o nome da marca. */
|
|
123
|
+
const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
|
|
124
|
+
const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
|
|
125
|
+
let comCartao = 0;
|
|
126
|
+
/* O cartão de produto só quando o vídeo É de produto: tem marca, um
|
|
127
|
+
comando de instalar ou um link. Um vídeo sobre sono saía com um
|
|
128
|
+
terminal "$ app" ao lado do título (teste de 08/10). `p.ui` explícito vale sempre. */
|
|
129
|
+
const deProduto = !!(marca.nome || link || /\b(app|software|plataforma|ferramenta|produto|sistema|saas|api|cli)\b/i.test(String(roteiro.pedido || '')));
|
|
130
|
+
|
|
131
|
+
const batidas = partes.map((p, i) => {
|
|
132
|
+
/* Fecho sem marca não monta a "marca de bolinhas" (vício, 08/10): vira
|
|
133
|
+
uma frase — e, curta, a gigante do fim. */
|
|
134
|
+
const tipo = p.tipo === 'cta' && !marca.nome ? 'titulo' : TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
|
|
135
|
+
const titulo = p.titulo || p.texto || '';
|
|
136
|
+
const fr = [];
|
|
137
|
+
const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
|
|
138
|
+
const b = { i, tipo, forma: FORMA[tipo], transicao: p.corte === true && i > 0 ? 'corte' : 'morfa', frases: fr, lado: 'centro' };
|
|
139
|
+
/* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
|
|
140
|
+
de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
|
|
141
|
+
const ui = p.ui === false ? null : typeof p.ui === 'string' ? p.ui
|
|
142
|
+
: !deProduto ? null
|
|
143
|
+
: tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
|
|
144
|
+
if (ui) {
|
|
145
|
+
b.lado = comCartao++ % 2 ? 'dir' : 'esq';
|
|
146
|
+
b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
|
|
147
|
+
: ui === 'chat' ? { tipo: ui, pergunta: caixaNormal(titulo) }
|
|
148
|
+
: { tipo: 'painel', rotulo: caixaNormal(p.rotulo || titulo), barras: [0, 1, 2, 3, 4, 5, 6].map((k) => r2(0.35 + ((semente >>> (k * 3)) % 7) / 10)) };
|
|
149
|
+
}
|
|
150
|
+
switch (tipo) {
|
|
151
|
+
case 'numero': {
|
|
152
|
+
b.numero = numeroDe(p.valor);
|
|
153
|
+
poe('numero', p.valor, { semAcento: true });
|
|
154
|
+
poe('rotulo', p.rotulo || titulo, { destaque: p.destaque });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'lista': case 'passos':
|
|
158
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
159
|
+
itensDe(p).slice(0, tipo === 'passos' ? 6 : 4).forEach((it) => poe('item', it, { semAcento: true }));
|
|
160
|
+
break;
|
|
161
|
+
case 'contraste':
|
|
162
|
+
poe('nao', p.nao, { semAcento: true });
|
|
163
|
+
poe('sim', p.sim || titulo, { destaque: p.destaque });
|
|
164
|
+
break;
|
|
165
|
+
case 'citacao':
|
|
166
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
167
|
+
poe('autor', p.autor ? `— ${p.autor}` : '', { semAcento: true });
|
|
168
|
+
break;
|
|
169
|
+
case 'cta':
|
|
170
|
+
b.marca = { nome: caixaNormal(marca.nome || titulo) };
|
|
171
|
+
poe('marca', b.marca.nome, { semAcento: true });
|
|
172
|
+
poe('principal', marca.nome ? titulo : '', { destaque: p.destaque });
|
|
173
|
+
poe('sub', p.link || p.sub, { semAcento: true });
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
177
|
+
poe('sub', p.sub, { semAcento: true });
|
|
178
|
+
}
|
|
179
|
+
return b;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/* ── A PALAVRA GIGANTE e a VIRADA (08/10): nas referências boas, o
|
|
183
|
+
momento forte é uma frase curta que ENCHE a tela, e o fundo vira
|
|
184
|
+
(o ponto cresce até cobrir tudo de cor). Sem isso o vídeo era a mesma
|
|
185
|
+
pílula pequena no mesmo azul por 36 s — "horríveis". Regras de editor:
|
|
186
|
+
só frase de até 4 palavras, abertura ou com destaque (a palavra-tese)
|
|
187
|
+
primeiro; gigante em até metade das batidas; a virada só nas que têm
|
|
188
|
+
destaque, 1 em 3 e nunca seguida. `p.gigante` explícito manda. */
|
|
189
|
+
const curta = (b) => { const f = b.frases.find((x) => x.papel === 'principal'); return f && f.palavras.length <= 4 && b.frases.length <= 2; };
|
|
190
|
+
const candidatas = batidas.map((b, i) => ({ b, i, p: partes[i] }))
|
|
191
|
+
.filter(({ b, p }) => p.gigante !== false && (b.tipo === 'titulo' || b.tipo === 'abertura') && !b.ui && !b.imagem && curta(b))
|
|
192
|
+
.sort((x, y) => (y.p.gigante === true) - (x.p.gigante === true) || !!y.p.destaque - !!x.p.destaque || (x.b.tipo === 'abertura' ? -1 : 0) || x.i - y.i);
|
|
193
|
+
/* ── AS IMAGENS, quando a pessoa pediu (08/10: "deve ser misto com imagens
|
|
194
|
+
e motion, e com variadas animações"; o híbrido era "10 mil imagens de
|
|
195
|
+
banco e a mesma animação"). A imagem é UM momento: só nas batidas que
|
|
196
|
+
têm imagem baixada, no máximo 1 em 3, nunca duas seguidas. E cada uma
|
|
197
|
+
entra de um JEITO diferente, sem repetir no vídeo:
|
|
198
|
+
janela a foto abre da esquerda e cobre a tela, o texto por cima
|
|
199
|
+
circulo o ponto cresce em círculo com a foto dentro
|
|
200
|
+
cartao a foto num cartão que gira em 3D, o texto acima
|
|
201
|
+
faixa uma faixa da foto abre na altura, o texto abaixo
|
|
202
|
+
vazado o título gigante com a foto por DENTRO das letras */
|
|
203
|
+
const JEITOS = ['janela', 'circulo', 'cartao', 'faixa', 'vazado'];
|
|
204
|
+
const ordemJeitos = JEITOS.map((j, k) => ({ j, o: hash(j + semente) })).sort((a, b) => a.o - b.o).map((x) => x.j);
|
|
205
|
+
const tetoImagem = Math.max(1, Math.floor(batidas.length / 3));
|
|
206
|
+
let nImagem = 0;
|
|
207
|
+
for (const b of batidas) {
|
|
208
|
+
const img = imagens[b.i];
|
|
209
|
+
if (!img || !img.src || b.tipo === 'cta' || nImagem >= tetoImagem) continue;
|
|
210
|
+
if (batidas[b.i - 1] && batidas[b.i - 1].imagem) continue;
|
|
211
|
+
const pr = b.frases.find((f) => f.papel === 'principal');
|
|
212
|
+
let jeito = ordemJeitos[nImagem % ordemJeitos.length];
|
|
213
|
+
if (jeito === 'vazado' && !(pr && pr.palavras.length <= 3)) jeito = ordemJeitos[(nImagem + 1) % ordemJeitos.length] === 'vazado' ? 'janela' : ordemJeitos[(nImagem + 1) % ordemJeitos.length];
|
|
214
|
+
b.imagem = { src: img.src, jeito };
|
|
215
|
+
b.ui = null; b.lado = 'centro';
|
|
216
|
+
nImagem++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A gigante pode seguir outra (as referências fazem isso); a VIRADA é o golpe: 1 em 3, nunca seguida.
|
|
220
|
+
const teto = Math.max(1, Math.ceil(batidas.length / 2)), tetoVirada = Math.max(1, Math.floor(batidas.length / 3));
|
|
221
|
+
for (const { b, i, p } of candidatas) {
|
|
222
|
+
if (batidas.filter((x) => x.gigante).length >= teto && p.gigante !== true) break;
|
|
223
|
+
b.gigante = true;
|
|
224
|
+
const viradas = batidas.filter((x) => x.virada).length;
|
|
225
|
+
const vizinha = (batidas[i - 1] && batidas[i - 1].virada) || (batidas[i + 1] && batidas[i + 1].virada);
|
|
226
|
+
if (i > 0 && viradas < tetoVirada && !vizinha && (p.destaque || p.gigante === true)) b.virada = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
|
|
230
|
+
pouco antes da primeira palavra da parte) e cada palavra na tela
|
|
231
|
+
aparece no instante em que é dita (os tempos por palavra da narração).
|
|
232
|
+
A palavra que a voz não diz entra em cascata curta. Se o texto pede
|
|
233
|
+
mais leitura do que a frase deu, a batida estica e a próxima volta a
|
|
234
|
+
casar com a voz na primeira folga; o atraso medido vai na partitura. */
|
|
235
|
+
let atraso = 0, sincronia = 0;
|
|
236
|
+
batidas.forEach((b, i) => {
|
|
237
|
+
const tp = temVoz ? tempos[i] : null;
|
|
238
|
+
const deVoz = tp ? (i === 0 ? 0 : Math.max(0, tp.de - ANTES)) : null;
|
|
239
|
+
b.de = r2(i === 0 ? 0 : Math.max(batidas[i - 1].ate, deVoz == null ? 0 : deVoz));
|
|
240
|
+
if (deVoz != null) atraso = Math.max(atraso, r2(b.de - deVoz));
|
|
241
|
+
const ditas = ((tp && tp.palavras) || []).map((w) => ({ k: limpa(w.texto), de: w.de }));
|
|
242
|
+
let cursor = 0, livre = b.de + (b.tipo === 'cta' ? 0.55 : 0.12);
|
|
243
|
+
const fimVoz = tp ? (i < batidas.length - 1 ? tempos[i + 1].de - ANTES : tp.ate + 0.2) : 0;
|
|
244
|
+
for (const f of b.frases) {
|
|
245
|
+
/* Casa as palavras da tela com as faladas, na ordem. Tenta começar de
|
|
246
|
+
cada palavra (a frase da tela pode vir em outra ordem que a falada)
|
|
247
|
+
e fica com o casamento que acha mais palavras. */
|
|
248
|
+
const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
|
|
249
|
+
/* Cada frase procura as suas palavras na fala INTEIRA da batida: com
|
|
250
|
+
um cursor só, o título que casava com o fim da fala deixava o
|
|
251
|
+
subtítulo sem achar as dele (entrava 0,45 s atrasado, 08/10). */
|
|
252
|
+
let ts = f.palavras.map(() => null), fim = cursor;
|
|
253
|
+
for (let ini = 0; ini < f.palavras.length; ini++) {
|
|
254
|
+
let c = 0;
|
|
255
|
+
const tt = f.palavras.map((w, j) => {
|
|
256
|
+
const k = limpa(w.t);
|
|
257
|
+
if (j < ini || !k) return null;
|
|
258
|
+
for (let q = c; q < Math.min(ditas.length, c + 40); q++) if (casa(k, ditas[q].k)) { c = q + 1; return ditas[q].de; }
|
|
259
|
+
return null;
|
|
260
|
+
});
|
|
261
|
+
if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
|
|
262
|
+
}
|
|
263
|
+
cursor = fim;
|
|
264
|
+
f.falada = ts.filter((x) => x != null).length >= f.palavras.length / 2;
|
|
265
|
+
const ach = ts.findIndex((x) => x != null);
|
|
266
|
+
/* O que a voz não diz entra cedo o bastante para ser lido antes de a
|
|
267
|
+
frase acabar (e nunca antes da batida). */
|
|
268
|
+
/* O que a voz DIZ entra quando ela diz — nunca antes. Antes de 08/10
|
|
269
|
+
a frase entrava no começo da batida se a voz demorasse: "ontem à
|
|
270
|
+
noite" aparecia 2,8 s antes de ser dito, e o Joel viu "atrasos
|
|
271
|
+
entre a narração e o vídeo". */
|
|
272
|
+
let t = ach >= 0 && ts[ach] != null ? ts[ach] - ach * STAGGER
|
|
273
|
+
: tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
|
|
274
|
+
f.palavras.forEach((w, j) => {
|
|
275
|
+
const falada = ts[j];
|
|
276
|
+
t = Math.max(b.de + 0.06, falada != null ? Math.max(t, falada - 0.05) : t);
|
|
277
|
+
w.em = r2(t);
|
|
278
|
+
if (falada != null) sincronia = Math.max(sincronia, r2(w.em - falada));
|
|
279
|
+
t += STAGGER;
|
|
280
|
+
});
|
|
281
|
+
f.de = f.palavras[0].em;
|
|
282
|
+
// O que não é dito (rótulo, item, link) entra logo depois do anterior.
|
|
283
|
+
livre = Math.max(livre, f.papel === 'item' && b.tipo === 'passos' ? f.de + leitura(f.palavras.length) * 0.8 : f.palavras[f.palavras.length - 1].em + 0.18);
|
|
284
|
+
}
|
|
285
|
+
/* O texto que a voz DIZ é lido junto com ela: fica até depois da última
|
|
286
|
+
palavra dita (e nunca menos de 0,8 s). O que a voz não diz pede 0,3 s por palavra. */
|
|
287
|
+
const precisa = Math.max(...b.frases.map((f) => f.de + (f.falada
|
|
288
|
+
? Math.max(MINIMO, f.palavras[f.palavras.length - 1].em - f.de + 0.3) : leitura(f.palavras.length))), b.de + 0.9);
|
|
289
|
+
b.ate = r2(Math.max(precisa, fimVoz));
|
|
290
|
+
});
|
|
291
|
+
// A marca do fim segura um instante; o resto não espera.
|
|
292
|
+
const ult = batidas[batidas.length - 1];
|
|
293
|
+
ult.ate = r2(ult.ate + (ult.tipo === 'cta' ? 1.0 : 0.5));
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
|
|
297
|
+
marcador: roteiro.marcador || ['brilho', 'numero', 'traco', 'ponto', 'nenhum'][hash('marcador' + semente) % 5],
|
|
298
|
+
fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
|
|
299
|
+
logo: marca.logo || null,
|
|
300
|
+
batidas,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {object} op
|
|
306
|
+
* @param {object} op.roteiro { marca, paleta?: noite|breu|lavanda|creme, tema?: claro|escuro, partes }
|
|
307
|
+
* @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
|
|
308
|
+
* @param {object} op.sons os efeitos leves (sons-leves.js)
|
|
309
|
+
*/
|
|
310
|
+
function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, imagens = {} }) {
|
|
311
|
+
const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
|
|
312
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
313
|
+
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, imagens });
|
|
314
|
+
const DUR = partitura.duracao;
|
|
315
|
+
/* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
|
|
316
|
+
cada transformação, pop quando pílula ou cartão assenta, clique na roda e
|
|
317
|
+
no terminal, um bolha discreto na palavra de acento, o riser que leva à
|
|
318
|
+
marca e o impacto quando ela trava. Baixo: a voz vem primeiro. */
|
|
319
|
+
const efeitos = [];
|
|
320
|
+
const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
|
|
321
|
+
for (const b of partitura.batidas) {
|
|
322
|
+
if (b.i > 0 && b.tipo !== 'cta') som(b.de - 0.18, 'whoosh', 0.2);
|
|
323
|
+
if (b.ui) { som(b.de + 0.6, 'pop', 0.16); if (b.ui.tipo === 'terminal') b.ui.linhas.forEach((_, k) => som(b.de + 0.75 + k * 0.5, 'clique', 0.1)); }
|
|
324
|
+
for (const f of b.frases) {
|
|
325
|
+
if (f.papel === 'item') som(f.de - 0.06, b.tipo === 'passos' ? 'clique' : 'pop', 0.15);
|
|
326
|
+
const ac = f.palavras.find((w) => w.acento);
|
|
327
|
+
if (ac && f.papel !== 'item') som(ac.em, 'bolha', 0.09);
|
|
328
|
+
}
|
|
329
|
+
if (b.tipo === 'citacao') som(b.de + 0.3, 'pop', 0.14);
|
|
330
|
+
if (b.tipo === 'cta') { som(b.de - 0.75, 'riser', 0.2); som(b.de + 0.45, 'impacto', 0.26); som(b.de + 0.5, 'brilho', 0.16); }
|
|
331
|
+
}
|
|
332
|
+
const volumes = volumesDaTrilha(tempos.length === partes.length ? tempos : [], DUR, { alto: 0.55, baixo: 0.22 });
|
|
333
|
+
const P = partitura.paleta;
|
|
334
|
+
const plano = {
|
|
335
|
+
formato: { largura, altura, fps: FPS },
|
|
336
|
+
fundoGeral: P.fundo[0],
|
|
337
|
+
fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
|
|
338
|
+
cenas: [{
|
|
339
|
+
duracao: DUR,
|
|
340
|
+
fundo: P.fundo[0],
|
|
341
|
+
elementos: [{ tipo: 'kit', peca: 'fluido', partitura, fonte: partitura.fontes.texto, fonteSerifa: partitura.fontes.serifa, fonteMono: partitura.fontes.mono }],
|
|
342
|
+
}],
|
|
343
|
+
audio: {
|
|
344
|
+
...(narracao ? { narracao } : {}),
|
|
345
|
+
...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
|
|
346
|
+
efeitos,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, imagens: partitura.batidas.filter((b) => b.imagem).map((b) => b.imagem.jeito), segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -827,6 +827,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
827
827
|
};
|
|
828
828
|
const registra = (b) => { usadas[b.comp] = (usadas[b.comp] || 0) + 1; recentes.push(b.comp); anterior = b.comp; batidas.push(b); };
|
|
829
829
|
|
|
830
|
+
/* PEDIU 3D, O 3D APARECE: o sorteio podia passar o vídeo inteiro sem a
|
|
831
|
+
batida do produto (3 partes, "pilha → contador → pilha", 05/10). Se
|
|
832
|
+
nenhuma escolher sozinha, a melhor parte ganha: a de produto, o cta ou
|
|
833
|
+
a primeira com título. */
|
|
834
|
+
const ultimaDe = (tipos) => { for (let i = brutas.length - 1; i >= 0; i--) if (tipos.includes(brutas[i].conteudo.tipo)) return i; return -1; };
|
|
835
|
+
let alvo3d = -1;
|
|
836
|
+
if (tem3d) {
|
|
837
|
+
alvo3d = ultimaDe(['produto']);
|
|
838
|
+
if (alvo3d < 0) alvo3d = ultimaDe(['cta']);
|
|
839
|
+
if (alvo3d < 0) alvo3d = brutas.findIndex((b) => ['titulo', 'marca'].includes(b.conteudo.tipo));
|
|
840
|
+
}
|
|
841
|
+
|
|
830
842
|
brutas.forEach((br, bi) => {
|
|
831
843
|
const c = br.conteudo;
|
|
832
844
|
const tituloC = c.titulo || c.nome || c.texto || '';
|
|
@@ -836,6 +848,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
836
848
|
pergunta: /\?/.test(tituloC), exclama: /!/.test(tituloC), numeroNoTitulo: /\d/.test(tituloC),
|
|
837
849
|
};
|
|
838
850
|
let nome = escolher(pesosDe(c, feat));
|
|
851
|
+
if (bi === alvo3d && !usadas.produto) nome = 'produto';
|
|
839
852
|
const ctx = { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes, P, palavras: br.palavras || c.palavras || [], parte: br.parte, marca, roteiro, memoria };
|
|
840
853
|
if (nome === 'rajada' && br.ate - br.de < 1.6 * c.itens.length) nome = 'cascata';
|
|
841
854
|
if (nome === 'rajada') {
|
|
@@ -1042,20 +1055,25 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
1042
1055
|
|
|
1043
1056
|
// ── o produto em 3D: mora nas batidas que o hospedam ─────────────────
|
|
1044
1057
|
const produtos = batidas.filter((b) => b.produto3d).map((b) => ({ ...b.produto3d, de: b.de, ate: b.ate, modelo: roteiro.modelo3d,
|
|
1045
|
-
cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento }));
|
|
1058
|
+
cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento, ...(roteiro.variacao3d || {}) }));
|
|
1046
1059
|
|
|
1047
1060
|
// ── o fundo vivo ────────────────────────────────────────────────────
|
|
1048
|
-
|
|
1061
|
+
/* SEM CARA DE IA (05/10): grade, pontinhos flutuando e o HUD só a pedido
|
|
1062
|
+
(roteiro.hud, roteiro.particulas). O sorteio continua igual — a semente
|
|
1063
|
+
dá o mesmo vídeo de antes, só sem o enfeite. */
|
|
1064
|
+
const texturaSorteada = s.um(['grade', 'pontos', 'linhas', 'grade']);
|
|
1065
|
+
const fundo = { textura: roteiro.hud === true ? texturaSorteada : null, passo: Math.round(s.um([72, 96, 120]) * base), grao: luz(P.fundo) < 0.5 ? 0.07 : 0.05,
|
|
1049
1066
|
brilho: { x: s.entre(0.25, 0.75), y: s.entre(0.2, 0.5) },
|
|
1050
1067
|
// Partículas em três profundidades: o espaço respira mesmo no repouso.
|
|
1051
1068
|
particulas: Array.from({ length: 34 }, (_, i) => ({ x: r1(s.prox() * W), y: r1(s.prox() * H), z: [0.25, 0.5, 0.85][i % 3], r: r1((1.2 + s.prox() * 2.4) * base), v: r1(s.entre(6, 22) * base) })) };
|
|
1069
|
+
if (roteiro.particulas !== true) fundo.particulas = [];
|
|
1052
1070
|
|
|
1053
1071
|
return {
|
|
1054
1072
|
versao: 1, semente, fps: FPS, humor,
|
|
1055
1073
|
musica: { genero: s.um(H0.generos), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
|
|
1056
1074
|
paleta: { ...P, fundo: campos[0], tinta: campos[2], acento: campos[1] }, campos, fontes, eixos: Object.fromEntries(Object.values(fontes).map((f) => [f, (VARIAVEIS[f] || {}).eixos || { wght: [400, 700] }])),
|
|
1057
1075
|
margem: M, fundo, batidas, camera, produtos,
|
|
1058
|
-
hud: { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' },
|
|
1076
|
+
hud: roteiro.hud === true ? { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' } : null,
|
|
1059
1077
|
};
|
|
1060
1078
|
}
|
|
1061
1079
|
|
package/lib/estudio/hibrido.js
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* O que cada lado traz:
|
|
10
10
|
* do cinema (cinema.js) foto e vídeo REAIS ocupando a tela, escurecidos,
|
|
11
|
-
* trocando por baixo do mesmo texto
|
|
12
|
-
*
|
|
11
|
+
* trocando por baixo do mesmo texto, escrito como a
|
|
12
|
+
* pessoa escreveu, com a palavra da tese na serifa; o
|
|
13
13
|
* número que conta; a virada que bate no fundo; a
|
|
14
14
|
* marca só no fim.
|
|
15
15
|
* do kit (kit/*.jsx) a câmera que empurra e desliza em toda cena; a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* líquido entre blocos; grão e vinheta. A etiqueta
|
|
17
|
+
* da fonte, o HUD e as partículas só a pedido
|
|
18
|
+
* (roteiro.hud, roteiro.particulas).
|
|
19
19
|
*
|
|
20
20
|
* Três passos, e nenhum repete o que o cinema já faz:
|
|
21
21
|
* traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
|
|
@@ -106,7 +106,10 @@ function camera(d, i, deitado, forte) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/** O kit por cima do plano do cinema. Mexe no plano recebido e o devolve. */
|
|
109
|
-
|
|
109
|
+
/* SEM CARA DE IA (05/10): o HUD (cantos, timecode, fps, rótulos nas bordas)
|
|
110
|
+
e as partículas eram exatamente a lista do "ruim" que o Joel juntou. Agora
|
|
111
|
+
só entram quando o roteiro pede: roteiro.hud === true, roteiro.particulas === true. */
|
|
112
|
+
function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura, hud = false, particulas = false } = {}) {
|
|
110
113
|
if (!plano || !Array.isArray(plano.cenas)) return plano;
|
|
111
114
|
const W = largura || (plano.formato && plano.formato.largura) || 1920;
|
|
112
115
|
const H = altura || (plano.formato && plano.formato.altura) || 1080;
|
|
@@ -132,18 +135,18 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
132
135
|
const forte = tipo === 'numero' || tipo === 'contraste' || tipo === 'abertura';
|
|
133
136
|
if (!cena.camera) cena.camera = camera(d, c, deitado, forte);
|
|
134
137
|
|
|
135
|
-
// A FONTE que se digita, com a barra do acento:
|
|
136
|
-
if (ex.fonte && d > 1.4) {
|
|
138
|
+
// A FONTE que se digita, com a barra do acento (é rótulo de borda: vem com o HUD).
|
|
139
|
+
if (hud && ex.fonte && d > 1.4) {
|
|
137
140
|
els.push({ tipo: 'kit', peca: 'fonte', texto: String(ex.fonte).slice(0, 48), t: Math.min(0.9, d * 0.35),
|
|
138
141
|
x: M, y: Math.round(H - M * (deitado ? 1.1 : 2.6)), tamanho: Math.round(base * 0.016), cor: 'rgba(245,245,247,0.72)', corAcento: ACENTO });
|
|
139
142
|
}
|
|
140
143
|
// O NÚMERO estoura em partículas quando assenta.
|
|
141
|
-
if (tipo === 'numero' || tipo === 'grafico') {
|
|
144
|
+
if (particulas && (tipo === 'numero' || tipo === 'grafico')) {
|
|
142
145
|
els.push({ tipo: 'kit', peca: 'particulas', t: 0.45, x: Math.round(W / 2), y: Math.round(H * 0.42), raio: Math.round(base * 0.34), n: 46,
|
|
143
146
|
tamanho: Math.max(5, Math.round(base * 0.007)), cores: ['#ffffff', ACENTO, grad[0]] });
|
|
144
147
|
}
|
|
145
148
|
// O FIM: a marca entra com um brilho de partículas.
|
|
146
|
-
if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') {
|
|
149
|
+
if (particulas && (tipo === 'cta' || tipo === 'logo' || tipo === 'fim')) {
|
|
147
150
|
els.push({ tipo: 'kit', peca: 'particulas', t: 0.2, x: Math.round(W / 2), y: Math.round(H * 0.36), raio: Math.round(base * 0.45), n: 60,
|
|
148
151
|
tamanho: Math.max(4, Math.round(base * 0.006)), cores: ['#ffffff', ACENTO], forma: 'circulo' });
|
|
149
152
|
}
|
|
@@ -154,8 +157,9 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
154
157
|
}
|
|
155
158
|
});
|
|
156
159
|
|
|
157
|
-
// O HUD de motion designer
|
|
158
|
-
plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
|
|
160
|
+
// O HUD de motion designer (cantos, timecode e a marca): só a pedido.
|
|
161
|
+
if (hud) plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
|
|
162
|
+
else delete plano.hud;
|
|
159
163
|
// O acabamento de filme um pouco mais presente que o do cinema.
|
|
160
164
|
plano.acabamento = { ...(plano.acabamento || {}), grao: Math.max(0.06, (plano.acabamento || {}).grao || 0), vinheta: Math.max(0.55, (plano.acabamento || {}).vinheta || 0) };
|
|
161
165
|
plano.estilo = 'hibrido';
|