primocode 10.10.1 → 10.11.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/lib/catalogo.js CHANGED
@@ -94,6 +94,7 @@ const GRUPOS = [
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
96
  + 'componente:"componentes/x.jsx" numa parte = a cena que VOCÊ escreveu em React — é o caminho do motion (skill Motion de primeira): uma ideia desenhada por fala; a voz, a trilha, os sons e o fundo seguem do Estúdio; cena que não compila volta em cenasQuebradas. '
97
+ + 'semNarracao:true no roteiro (ou pedido "sem narração") = vídeo só com motion, trilha e efeitos sonoros: sem estudio_narrar, cada parte dura o seu "duracao" (s) ou o tempo de ler a tela, e um som leve entra em cada corte e entrada. '
97
98
  + 'estilo (deixe VAZIO: o gerativo, com as suas cenas em componente e o estiloVisual que você montou; outro só se a pessoa pedir pelo nome): 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 — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som — estiloVisual {humor, paleta, display, texto, mono, genero, ambiente, acento, material} é o estilo que VOCÊ monta pelo assunto (cardápio: estudio_midia acao:estilos; vazio, o PrimoCode sorteia); 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 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; 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) | '
98
99
  + '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 (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D no visual de blocos/voxel de render de cinema — é a escolha para animação 3D, personagem, bicho, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D; cada parte é UMA fala {falante, texto, emocao?, destaque?, acao?, cenario?}, sem tipo). NADA É FIXO, monte o que a pessoa pediu: cena:"texto livre do lugar" (praia, cidade, floresta, parque, fazenda, neve, deserto, espaço, fundo do mar, escritório, sala, cozinha, escola, palco… + hora: "à noite", "ao pôr do sol") ou cena:{lugar, hora, elementos:["palmeira","barraca","violão",…]} — elemento fora do catálogo vira ícone 3D em blocos; parte.cenario muda o lugar a partir daquela fala. Até 4 falantes; personagens:{"nome":{tipo:pessoa|robo|animal|coisa, especie (animal: cachorro|gato|cavalo|vaca|porco|ovelha|raposa|lobo|urso|panda|coelho|leao|tigre|elefante|girafa|tartaruga|galinha|pato|passaro|papagaio|coruja|pinguim|peixe|tubarao|golfinho|dinossauro|sapo|macaco), icone (coisa: "pizza", "café", "polvo" — qualquer objeto/bicho que fala), pessoa: pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba; robo: cor}} — sem tipo, o nome decide (nome de bicho vira bicho, de IA vira o mascote laranja, de objeto vira coisa); figurantes:["cachorro","pessoa"] (quem está na cena sem falar); BICHO NÃO FALA: a fala de um bicho sai da narração e ele fica na cena — só fala se a pessoa pedir ("cachorro falante") ou personagens.x.fala:true; a voz de cada falante sai do GÊNERO do nome (personagens.x.genero:f|m manda), uma voz por personagem do começo ao fim; bigode:true e cabeca:"cubo" (o rosto de pixel antigo) em personagens; sem lugar no pedido, o set é a cobertura à noite (janela da cidade, cartões de interface na frente). acao em cada parte: acenar|apontar|pular|dancar|comemorar|pensar|andar|correr. emocao feliz|serio|surpreso|pensativo|animado|curioso; destaque:"texto curto"; paineis:["título",…] só no set de telas (o padrão quando o pedido não diz lugar); visual:"liso" só se pedirem o boneco liso antigo). '
99
100
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
@@ -0,0 +1,25 @@
1
+ // Título com uma faixa de luz que atravessa as letras, e um brilho (bloom) que respira depois.
2
+ // el.dados (ou el): titulo, cores {fundo, texto, acento}, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, interpolate, useVideoConfig } from 'remotion';
5
+
6
+ export default function BrilhoVarredura({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const entra = spring({ frame: Math.round(t * fps), fps, config: { damping: 18, stiffness: 120 } });
12
+ const faixa = interpolate(t, [0.35, 1.25], [-40, 140], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
13
+ const respira = 0.5 + 0.5 * Math.sin(t * 2.4);
14
+ const tam = Math.min(W, H) * (d.tamanho || 0.13);
15
+ const base = { fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, lineHeight: 1.02, fontWeight: 800, letterSpacing: '-0.02em', textAlign: 'center', maxWidth: W * 0.84 };
16
+ return (
17
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#0a0a0a', display: 'grid', placeItems: 'center' }}>
18
+ <div style={{ position: 'relative', transform: `translateY(${((1 - entra) * tam * 0.6).toFixed(1)}px)`, opacity: entra }}>
19
+ <div style={{ ...base, position: 'absolute', inset: 0, color: c.acento || '#ffd166', filter: `blur(${(tam * 0.18).toFixed(0)}px)`, opacity: 0.25 + 0.35 * respira * entra }}>{d.titulo}</div>
20
+ <div style={{ ...base, position: 'relative', color: 'transparent', WebkitBackgroundClip: 'text', backgroundClip: 'text',
21
+ backgroundImage: `linear-gradient(105deg, ${c.texto || '#fff'} ${faixa - 18}%, ${c.acento || '#ffd166'} ${faixa - 4}%, #ffffff ${faixa}%, ${c.acento || '#ffd166'} ${faixa + 4}%, ${c.texto || '#fff'} ${faixa + 18}%)` }}>{d.titulo}</div>
22
+ </div>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,30 @@
1
+ // Transição em que três camadas de cor inundam a tela em ondas, e o título aparece por baixo.
2
+ // el.dados (ou el): titulo, cores {fundo, texto, acento}, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { interpolate, useVideoConfig } from 'remotion';
5
+
6
+ const sai = (x) => 1 - Math.pow(1 - x, 4);
7
+
8
+ export default function CorInunda({ el, t }) {
9
+ const d = el.dados || el;
10
+ const v = useVideoConfig();
11
+ const W = d.largura || v.width, H = d.altura || v.height;
12
+ const c = d.cores || {};
13
+ const camadas = [c.texto || '#f4efe6', c.fundo || '#111', c.acento || '#ff4d2e'];
14
+ const onda = (k, i) => {
15
+ const pts = [];
16
+ for (let j = 0; j <= 24; j++) { const x = (j / 24) * W; pts.push(`${x.toFixed(1)},${(H * (1 - k) + Math.sin(j * 0.7 + t * 6 + i) * 40 * (1 - k) * k * 4).toFixed(1)}`); }
17
+ return `M0,${H} L${pts.join(' L')} L${W},${H} Z`;
18
+ };
19
+ const titulo = interpolate(t, [0.75, 1.15], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
20
+ const tam = Math.min(W, H) * (d.tamanho || 0.12);
21
+ return (
22
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#111', overflow: 'hidden' }}>
23
+ <svg width={W} height={H} style={{ position: 'absolute', inset: 0 }}>
24
+ {camadas.map((cor, i) => <path key={i} d={onda(sai(Math.max(0, Math.min(1, (t - i * 0.16) / 0.7))) * 1.08, i)} fill={cor} />)}
25
+ </svg>
26
+ <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', color: c.fundo || '#111', fontFamily: (d.fontes && d.fontes.display) || 'serif',
27
+ fontSize: tam, fontWeight: 800, textAlign: 'center', padding: W * 0.08, opacity: titulo, transform: `scale(${(1.08 - 0.08 * titulo).toFixed(4)})`, letterSpacing: `${((1 - titulo) * 0.2).toFixed(3)}em` }}>{d.titulo}</div>
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,28 @@
1
+ // Entrada com física: antecipa (recua), cai com peso, amassa no pouso (squash & stretch) e assenta com rebote.
2
+ // el.dados (ou el): titulo (ou imagem), cores, fontes {display}, largura, altura, atraso (s).
3
+ import React from 'react';
4
+ import { Img, staticFile, useVideoConfig } from 'remotion';
5
+
6
+ export default function MolaComPeso({ el, t }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const u = t - (d.atraso || 0);
12
+ const queda = 0.42;
13
+ let y, sx = 1, sy = 1;
14
+ if (u < 0.12) { y = -H * 0.62 - Math.sin((u / 0.12) * Math.PI) * 18; }
15
+ else if (u < 0.12 + queda) { const k = (u - 0.12) / queda; y = -H * 0.62 * (1 - k * k); sy = 1 + 0.18 * k; sx = 1 - 0.1 * k; }
16
+ else { const p = u - 0.12 - queda; const amort = Math.exp(-p * 6); y = -Math.abs(Math.sin(p * 9)) * 60 * amort; sy = 1 - 0.22 * Math.cos(p * 18) * amort; sx = 1 + 0.16 * Math.cos(p * 18) * amort; }
17
+ const tam = Math.min(W, H) * (d.tamanho || 0.14);
18
+ return (
19
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#f4efe6', display: 'grid', placeItems: 'center' }}>
20
+ <div style={{ transform: `translateY(${y.toFixed(1)}px) scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`, transformOrigin: '50% 100%' }}>
21
+ {d.imagem ? <Img src={staticFile(d.imagem)} style={{ width: tam * 4, objectFit: 'contain' }} />
22
+ : <div style={{ fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, fontWeight: 900, color: c.texto || '#111' }}>{d.titulo}</div>}
23
+ </div>
24
+ <div style={{ position: 'absolute', top: '50%', width: tam * 2.4, height: tam * 0.18, borderRadius: '50%', background: '#000', marginTop: tam * 0.62,
25
+ opacity: Math.max(0, 0.25 * (1 + y / (H * 0.62))), filter: 'blur(10px)', transform: `scaleX(${(0.4 + 0.6 * (1 + y / (H * 0.62))).toFixed(3)})` }} />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,37 @@
1
+ // A peça do site entra em 3D: deitada, sobe, BATE (amassa, treme, flash) e o fundo se abre a partir dela.
2
+ // el.dados (ou el): imagem (PNG sem fundo, do estudio_site elemento), cores {fundo, texto, acento}, largura, altura.
3
+ import React from 'react';
4
+ import { Img, staticFile, spring, interpolate, useVideoConfig } from 'remotion';
5
+
6
+ export default function PecaImpacto3D({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const f = Math.round(t * fps);
12
+ const sobe = spring({ frame: f, fps, config: { mass: 1.1, damping: 13, stiffness: 90 } });
13
+ const tBate = 0.9;
14
+ const pos = Math.max(0, t - tBate);
15
+ const amassa = pos > 0 ? Math.exp(-pos * 9) * Math.cos(pos * 38) * 0.07 : 0;
16
+ const treme = pos > 0 ? Math.exp(-pos * 7) * Math.sin(pos * 70) * 14 : 0;
17
+ const flash = pos > 0 ? Math.exp(-pos * 10) : 0;
18
+ const abre = interpolate(pos, [0.05, 0.9], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: (x) => 1 - Math.pow(1 - x, 3) });
19
+ const deitado = interpolate(sobe, [0, 1], [82, 0]);
20
+ const y = interpolate(sobe, [0, 1], [H * 0.18, 0]);
21
+ const lado = Math.min(W, H) * (d.escala || 0.62);
22
+ return (
23
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, overflow: 'hidden', background: c.fundo || '#0b0b0c', perspective: 1400 }}>
24
+ <div style={{ position: 'absolute', inset: 0, clipPath: `circle(${(abre * 120).toFixed(2)}% at 50% 50%)`,
25
+ background: `radial-gradient(circle at 50% 50%, ${c.acento || '#ff5a1f'}33, transparent 60%), repeating-linear-gradient(0deg, ${c.texto || '#fff'}0d 0 1px, transparent 1px 48px), repeating-linear-gradient(90deg, ${c.texto || '#fff'}0d 0 1px, transparent 1px 48px)` }} />
26
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: lado * (1 + abre * 0.6), height: lado * 0.12, borderRadius: '50%',
27
+ transform: `translate(-50%, ${lado * 0.42}px)`, background: '#000', opacity: 0.35 * sobe, filter: 'blur(24px)' }} />
28
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: lado, height: lado,
29
+ transform: `translate(-50%, -50%) translate(${treme.toFixed(1)}px, ${y.toFixed(1)}px) rotateX(${deitado.toFixed(2)}deg) scale(${(1 + amassa).toFixed(4)}, ${(1 - amassa).toFixed(4)})`,
30
+ transformOrigin: '50% 100%', transformStyle: 'preserve-3d' }}>
31
+ {d.imagem ? <Img src={staticFile(d.imagem)} style={{ width: '100%', height: '100%', objectFit: 'contain', filter: `drop-shadow(0 30px 40px rgba(0,0,0,.45))` }} />
32
+ : <div style={{ width: '100%', height: '40%', marginTop: '30%', borderRadius: 28, background: c.texto || '#fff' }} />}
33
+ </div>
34
+ <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(circle at 50% 55%, ${c.acento || '#fff'}, transparent 45%)`, opacity: flash * 0.55, mixBlendMode: 'screen' }} />
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,29 @@
1
+ // Luz volumétrica: feixes giram devagar atrás do título, a cor do acento pulsa e partículas sobem na luz.
2
+ // el.dados (ou el): titulo, cores, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, random, useVideoConfig } from 'remotion';
5
+
6
+ export default function RaiosDeLuz({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const acento = c.acento || '#ffb547';
12
+ const entra = spring({ frame: Math.round(t * fps), fps, config: { damping: 20, stiffness: 70 } });
13
+ const tam = Math.min(W, H) * (d.tamanho || 0.12);
14
+ const giro = t * 6;
15
+ return (
16
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#07070a', overflow: 'hidden' }}>
17
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: Math.max(W, H) * 2, height: Math.max(W, H) * 2, transform: `translate(-50%,-50%) rotate(${giro}deg)`,
18
+ background: `repeating-conic-gradient(from 0deg, ${acento}2e 0deg 6deg, transparent 6deg 22deg)`, opacity: entra * (0.7 + 0.3 * Math.sin(t * 1.7)),
19
+ maskImage: 'radial-gradient(circle, #000 0%, transparent 55%)', WebkitMaskImage: 'radial-gradient(circle, #000 0%, transparent 55%)' }} />
20
+ {Array.from({ length: 28 }).map((_, i) => {
21
+ const x = random('x' + i) * W, vel = 30 + random('v' + i) * 70, r = 2 + random('r' + i) * 4;
22
+ const y = (H + 40 - ((t * vel + random('y' + i) * H) % (H + 80)));
23
+ return <div key={i} style={{ position: 'absolute', left: x, top: y, width: r, height: r, borderRadius: '50%', background: acento, opacity: 0.25 + 0.5 * random('o' + i), filter: 'blur(1px)' }} />;
24
+ })}
25
+ <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center', padding: W * 0.08, fontFamily: (d.fontes && d.fontes.display) || 'serif',
26
+ fontSize: tam, fontWeight: 800, color: c.texto || '#fff', textShadow: `0 0 ${tam * 0.4}px ${acento}88`, opacity: entra, transform: `scale(${(0.92 + 0.08 * entra).toFixed(4)})` }}>{d.titulo}</div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,30 @@
1
+ // Texto riscado que funciona com quebra de linha: um traço por linha, em sequência, e o texto esmaece.
2
+ // el.dados (ou el): titulo (use \n para quebrar, ou passe linhas: []), cores, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, useVideoConfig } from 'remotion';
5
+
6
+ export default function RiscoMultilinha({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const linhas = d.linhas || String(d.titulo || '').split('\n');
12
+ const tam = Math.min(W, H) * (d.tamanho || 0.11);
13
+ const t0 = d.riscaEm != null ? d.riscaEm : 0.7;
14
+ return (
15
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#0d0d0d', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: tam * 0.1 }}>
16
+ {linhas.map((ln, i) => {
17
+ const entra = spring({ frame: Math.round((t - i * 0.08) * fps), fps, config: { damping: 16 } });
18
+ const k = spring({ frame: Math.round((t - t0 - i * 0.14) * fps), fps, config: { damping: 22, stiffness: 160 } });
19
+ return (
20
+ <div key={i} style={{ position: 'relative', fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, fontWeight: 700, lineHeight: 1.05,
21
+ color: c.texto || '#fff', opacity: entra * (1 - 0.45 * k), transform: `translateY(${((1 - entra) * 30).toFixed(1)}px)` }}>
22
+ {ln}
23
+ <div style={{ position: 'absolute', left: '-3%', top: '54%', height: Math.max(6, tam * 0.08), width: `${(106 * k).toFixed(2)}%`, borderRadius: 99,
24
+ background: c.acento || '#ff3b30', transform: `rotate(${-2 + i * 1.5}deg)`, transformOrigin: '0 50%' }} />
25
+ </div>
26
+ );
27
+ })}
28
+ </div>
29
+ );
30
+ }
@@ -33,6 +33,21 @@ const catalogoArq = () => path.join(pasta(), 'catalogo.json');
33
33
  const PACOTES = /^(react|react\/jsx-runtime|remotion|@remotion\/[a-z-]+|three|three\/examples\/jsm\/.+|@react-three\/fiber)$/;
34
34
  const TETO = 150 * 1024;
35
35
 
36
+ /* OS DE FÁBRICA (14/10, "a IA pega sempre os mesmos; quero motions bem
37
+ profissionais e diversificados"): o cardápio nascia vazio, e a IA voltava
38
+ aos mesmos três truques. Estes vêm com o PrimoCode e entram no mesmo
39
+ cardápio, com a mesma memória de variedade. Guardado com o mesmo nome vence. */
40
+ const fabricaPasta = path.join(__dirname, 'efeitos-fabrica');
41
+ const FABRICA = [
42
+ { nome: 'peca-impacto-3d', descricao: 'a peça do site (PNG sem fundo do estudio_site elemento) entra deitada em 3D, sobe, BATE (amassa, treme, flash) e o fundo se abre a partir dela', tags: ['3d', 'site', 'impacto'] },
43
+ { nome: 'brilho-varredura', descricao: 'título com uma faixa de luz que atravessa as letras e um brilho que respira', tags: ['brilho', 'titulo'] },
44
+ { nome: 'cor-inunda', descricao: 'três ondas de cor inundam a tela e o título surge por baixo — transição de cor', tags: ['cor', 'transicao'] },
45
+ { nome: 'risco-multilinha', descricao: 'texto riscado com quebra de linha: um traço por linha, em sequência, e o texto esmaece (o "não" do contraste)', tags: ['risco', 'contraste'] },
46
+ { nome: 'mola-com-peso', descricao: 'entrada com física: antecipa, cai com peso, amassa no pouso e assenta com rebote (texto ou imagem)', tags: ['fisica', 'entrada'] },
47
+ { nome: 'raios-de-luz', descricao: 'feixes de luz giram atrás do título, o acento pulsa e partículas sobem na luz', tags: ['luz', 'brilho', 'titulo'] },
48
+ ];
49
+ const deFabrica = (nome) => FABRICA.find((x) => x.nome === nome && fs.existsSync(path.join(fabricaPasta, x.nome + '.jsx')));
50
+
36
51
  function ler() {
37
52
  try { const c = JSON.parse(fs.readFileSync(catalogoArq(), 'utf8')); return Array.isArray(c) ? c : []; } catch { return []; }
38
53
  }
@@ -72,7 +87,8 @@ function guardar({ pasta: proj, arquivo, nome, descricao, tags = [] } = {}) {
72
87
 
73
88
  /** O cardápio: os guardados, os menos usados primeiro (memória da variedade). */
74
89
  function cardapio({ aleatorio = Math.random } = {}) {
75
- const itens = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
90
+ const meus = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
91
+ const itens = [...meus, ...FABRICA.filter((f) => deFabrica(f.nome) && !meus.some((m) => m.nome === f.nome))];
76
92
  return variedade.pontuar(itens, 'efeito', { chave: (x) => x.nome })
77
93
  .map((p) => ({ ...p, pontos: p.pontos + aleatorio() * 0.05 }))
78
94
  .sort((a, b) => b.pontos - a.pontos)
@@ -81,8 +97,9 @@ function cardapio({ aleatorio = Math.random } = {}) {
81
97
 
82
98
  /** Copia o efeito para o projeto e devolve o elemento pronto. Marca o uso. */
83
99
  function reusar({ pasta: proj, nome } = {}) {
84
- const item = ler().find((x) => x.nome === nome);
85
- const de = item && path.join(pasta(), item.nome + '.jsx');
100
+ const meu = ler().find((x) => x.nome === nome && fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
101
+ const item = meu || deFabrica(nome);
102
+ const de = item && path.join(meu ? pasta() : fabricaPasta, item.nome + '.jsx');
86
103
  if (!item || !fs.existsSync(de)) return { ok: false, error: `não há efeito "${nome}" guardado — veja o cardápio (acao: efeitos).` };
87
104
  if (!proj) return { ok: false, error: 'diga o projeto (id).' };
88
105
  const src = `efeitos/${item.nome}.jsx`;
@@ -92,4 +109,4 @@ function reusar({ pasta: proj, nome } = {}) {
92
109
  return { ok: true, src, descricao: item.descricao, elemento: { tipo: 'componente', src, x: 0, y: 0 } };
93
110
  }
94
111
 
95
- module.exports = { guardar, cardapio, reusar, conferirFonte, pasta };
112
+ module.exports = { guardar, cardapio, reusar, conferirFonte, pasta, FABRICA };
@@ -76,6 +76,7 @@ const movimentos = require('./movimentos.js');
76
76
  const colecao = require('./colecao.js');
77
77
  const { escolherEstilo, LEVES } = require('./estilo.js');
78
78
  const sonsLeves = require('./sons-leves.js');
79
+ const semNarracao = require('./sem-narracao.js');
79
80
  const ende = require('./ende.js');
80
81
  const producao = require('./producao.js');
81
82
 
@@ -405,6 +406,13 @@ async function ferramentaPlano(args) {
405
406
  try { original = JSON.parse(typeof args.roteiro === 'string' ? args.roteiro : JSON.stringify(args.roteiro)); } catch { original = args.roteiro; }
406
407
  const r = await prepararRoteiro(p, args);
407
408
  if (!r.ok) return r;
409
+ /* SEM NARRAÇÃO: o som sobe de papel — um efeito leve em cada corte e em
410
+ cada entrada marcante, variando (sem-narracao.js, sons-leves.js). */
411
+ if (args.__semNarracao) {
412
+ const n = semNarracao.sonorizarPlano(r, { tempos: args.__semNarracao.tempos, sons: sonsLeves.preparar(p.pasta) });
413
+ if (r.resumo) r.resumo.semNarracao = true;
414
+ projeto.anotar(p.id, 'plano', `sem narração: ${n} efeitos leves nos cortes e entradas`);
415
+ }
408
416
  const ic = await variarIcones(r.plano, original);
409
417
  if (ic.trocados) projeto.anotar(p.id, 'icones', `coleção ${ic.colecao}: ${ic.trocados} ícones`);
410
418
  let conferida = plano.conferir(r.plano, { pasta: p.pasta });
@@ -536,7 +544,9 @@ async function ferramentaMidia(args) {
536
544
  }
537
545
 
538
546
  // O catálogo de efeitos que a IA escreveu (efeitos.js): guardar, listar, reusar.
539
- if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio() };
547
+ if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio(),
548
+ // A lista de técnicas mora aqui, e não na skill: a skill vai em todo pedido de vídeo e o teto é curto.
549
+ tecnicas: 'cor (ondas que inundam, paleta que vira na batida) · luz (faixa no título, bloom que respira, feixes, partículas) · física (antecipa, cai com peso, amassa no pouso, rebote; spring com massa, nunca linear) · 3D (perspective + rotateX: entra deitado e levanta, sombra no chão, câmera que orbita) · tipografia (letras que se montam, peso que pulsa, máscara que revela). Vídeo sobre site: tire a peça que conta a história com estudio_site elemento (PNG sem fundo) e anime ELA — peca-impacto-3d é ponto de partida, não molde.' };
540
550
  if (acao === 'guardar-efeito' || acao === 'usar-efeito') {
541
551
  if (!p) return { ok: false, error: 'diga em qual projeto (id).' };
542
552
  const r = acao === 'guardar-efeito'
@@ -1320,7 +1330,8 @@ function importarReferencia(p, arquivo) {
1320
1330
 
1321
1331
  async function prepararRoteiro(p, args) {
1322
1332
  const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
1323
- if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
1333
+ // O mesmo args (não uma cópia): o __semNarracao e o __cenasQuebradas voltam para quem chamou.
1334
+ if (Array.isArray(roteiro)) { args.roteiro = { partes: roteiro }; return prepararRoteiro(p, args); }
1324
1335
  /* O que o modelo manda e o disco não tem vira problema de render: um
1325
1336
  "src" que é endereço da internet vira busca, e um arquivo que não
1326
1337
  existe na pasta sai. A logo da marca vale só se estiver lá. */
@@ -1380,6 +1391,10 @@ async function prepararRoteiro(p, args) {
1380
1391
  const semImagem = referencias.semImagem(ref);
1381
1392
  let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
1382
1393
  if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1394
+ /* SEM NARRAÇÃO (14/10, "só o motion e os efeitos sonoros, os barulhinhos"):
1395
+ roteiro.semNarracao ou o pedido. O cinema e o híbrido vivem da voz: caem no gerativo. */
1396
+ const semVoz = semNarracao.ativo(roteiro, pedidoTxt);
1397
+ if (semVoz && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1383
1398
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1384
1399
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1385
1400
  o agente tenha escolhido outro estilo pelo nome. */
@@ -1772,7 +1787,17 @@ async function prepararRoteiro(p, args) {
1772
1787
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${hib ? 'hibrido' : 'cinema'}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
1773
1788
  return r;
1774
1789
  }
1775
- if (modo === 'video') {
1790
+ if (modo === 'video' && semVoz) {
1791
+ /* Sem voz, sem tempo medido: cada parte dura o `duracao` dela (ou o que o
1792
+ texto da tela pede), e as palavras são repartidas na parte. Os sons
1793
+ leves entram depois do plano, em cada corte e entrada (sem-narracao.js). */
1794
+ tempos = semNarracao.tempos(partes, { falaDe: composicao.falaDe });
1795
+ args.__semNarracao = { tempos };
1796
+ const total = tempos.length ? tempos[tempos.length - 1].ate : 0;
1797
+ projeto.anotar(p.id, 'narracao', `sem narração: ${Math.round(total * 10) / 10}s em ${partes.length} partes, só motion e efeitos sonoros`);
1798
+ sons = estilo === 'gerativo' ? {} : ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
1799
+ }
1800
+ if (modo === 'video' && !semVoz) {
1776
1801
  const lista = partes.map((pt, i) => ({ texto: composicao.falaDe(pt), registro: composicao.registroDe(pt, i, partes.length) }));
1777
1802
  const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
1778
1803
  fs.mkdirSync(path.dirname(arq), { recursive: true });
@@ -1852,6 +1877,9 @@ async function prepararRoteiro(p, args) {
1852
1877
  // O gerativo desenha os sons DEPOIS da partitura (um arquivo por evento, da própria trilha).
1853
1878
  sons = estilo === 'gerativo' ? {}
1854
1879
  : ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
1880
+ }
1881
+ // A trilha vale com e sem voz: o vídeo sem narração ainda tem música.
1882
+ if (modo === 'video') {
1855
1883
  // A trilha do motion: ende.app (CC BY 4.0, sem Content ID), clima tecnologia.
1856
1884
  if (ehMotion) {
1857
1885
  try {
@@ -1150,6 +1150,22 @@ export function Gerativo({ el, t, fps }) {
1150
1150
  return (
1151
1151
  <div key={l.id} style={{ position: 'absolute', inset: 0, ...eLinha }}>
1152
1152
  <Linha l={l} b={x} cx={cx} t={t} fps={fps} P={P} frame={frame} alvoSaida={alvo} centroBloco={{ x: (lay.bloco.x + lay.bloco.x1) / 2, y: (lay.bloco.y + lay.bloco.y1) / 2 }} />
1153
+ {l.riscado && (() => {
1154
+ /* O RISCO EM TODAS AS LINHAS (14/10, "quando tem quebra de linha no
1155
+ motion, ele não risca"): o acento é UM traço e só vai a uma linha.
1156
+ As outras linhas riscadas ganham o próprio risco, na mesma hora. */
1157
+ const ar = (x.acento || []).find((a) => a.forma === 'risco');
1158
+ if (ar && ar.alvo && ar.alvo.linha === l.id) return null;
1159
+ const rs = x.linhas.filter((q) => q.riscado);
1160
+ const t0 = (ar ? ar.t : l.t + 0.4) + Math.max(0, rs.indexOf(l)) * 0.1;
1161
+ const k = ed(c01((t - t0) / 0.35));
1162
+ if (k <= 0) return null;
1163
+ const s = cx.s, y = cx.y + cx.h * 0.56, x0 = cx.x, x1 = cx.x + cx.w * k;
1164
+ return <svg style={{ position: 'absolute', inset: 0, overflow: 'visible' }} width="100%" height="100%">
1165
+ <path d={`M${x0.toFixed(1)} ${(y + s * 0.04).toFixed(1)} L${x1.toFixed(1)} ${(y + s * 0.04 - (x1 - x0) / Math.max(1, cx.w) * s * 0.08).toFixed(1)}`}
1166
+ stroke={x.cores.acento} strokeWidth={Math.max(4, s * 0.07)} strokeLinecap="round" fill="none" />
1167
+ </svg>;
1168
+ })()}
1153
1169
  </div>
1154
1170
  );
1155
1171
  })}
@@ -13,6 +13,8 @@ import React, { useMemo, useLayoutEffect, useRef, useState } from 'react';
13
13
  import * as THREE from 'three';
14
14
  import { delayRender, continueRender, staticFile } from 'remotion';
15
15
  import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
16
+ // O pulo com peso (agacha, estica, cai acelerando, amassa no pouso): kit/natural.js.
17
+ import { salto, respiracao } from './natural';
16
18
 
17
19
  const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
18
20
  const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
@@ -903,15 +905,17 @@ function Quadrupede({ e, est, cor }) {
903
905
  const [W, H, D] = e.corpo;
904
906
  const anda = est.acao === 'andar' || est.acao === 'correr' || est.acao === 'pular';
905
907
  const passo = est.acao === 'correr' ? 14 : 8;
906
- const pulo = est.acao === 'pular' || est.acao === 'comemorar' ? Math.abs(Math.sin(est.t * 6)) * 0.25 : 0;
907
- const respira = Math.sin(est.t * 2.2) * 0.008;
908
+ const ka = Math.min(1, est.forcaAcao ?? 1);
909
+ const sl = est.acao === 'pular' || est.acao === 'comemorar' ? salto(est.t, { periodo: 0.85, altura: 0.25 }) : { y: 0, sx: 1, sy: 1 };
910
+ const pulo = sl.y * ka, sy = 1 + (sl.sy - 1) * ka, sx = 1 + (sl.sx - 1) * ka;
911
+ const respira = (respiracao(est.t, (est.semente || 0) + 2) - 0.5) * 0.016;
908
912
  const y0 = e.perna + H / 2;
909
913
  const [cy, cp] = est.cabeca;
910
914
  const pescoco = e.pescoco || 0;
911
915
  const cabPos = [0, y0 + H * 0.35 + pescoco * 0.85, D / 2 + e.cab * 0.25 + pescoco * 0.25];
912
916
  // O corpo vira de lado, e a cabeça volta para a câmera: o bicho se lê inteiro em 3/4.
913
917
  return (
914
- <group position={[0, pulo, 0]}>
918
+ <group position={[0, pulo, 0]} scale={[sx, sy, sx]}>
915
919
  <group rotation={[0, 0.55, 0]}>
916
920
  <B s={[W, H, D]} p={[0, y0 + respira, 0]} c={cor} r={0.06} rough={e.fofo ? 1 : 0.75} />
917
921
  {e.fofo && [0, 1, 2].map((i) => <B key={i} s={[W * 1.06, H * 0.32, D * 0.3]} p={[0, y0 + H * 0.2, -D * 0.3 + i * D * 0.3]} c={e.cor} r={0.08} rough={1} />)}
@@ -1044,8 +1048,10 @@ export function Animal({ p, est }) {
1044
1048
  export function Coisa({ p, est, icones = {} }) {
1045
1049
  const [abre] = est.boca;
1046
1050
  const bob = Math.sin(est.t * 1.9 + 0.7) * 0.05;
1047
- const estica = 1 + abre * 0.08, achata = 1 - abre * 0.04;
1048
- const pulo = est.acao === 'pular' || est.acao === 'comemorar' ? Math.abs(Math.sin(est.t * 6)) * 0.25 : 0;
1051
+ const ka = Math.min(1, est.forcaAcao ?? 1);
1052
+ const sl = est.acao === 'pular' || est.acao === 'comemorar' ? salto(est.t, { periodo: 0.85, altura: 0.25 }) : { y: 0, sx: 1, sy: 1 };
1053
+ const pulo = sl.y * ka;
1054
+ const estica = (1 + abre * 0.08) * (1 + (sl.sy - 1) * ka), achata = (1 - abre * 0.04) * (1 + (sl.sx - 1) * ka);
1049
1055
  if (!p.src) return null;
1050
1056
  return (
1051
1057
  <group position={[0, 0.45 + bob + pulo, 0]} rotation={[0, est.cabeca[0] * 0.6, Math.sin(est.t * 1.3) * 0.04]}>
@@ -0,0 +1,161 @@
1
+ /* kit/natural.js — o movimento com PESO: as curvas que tiram o 3D do robótico.
2
+ *
3
+ * "a animação do boneco, a movimentação, é muito robótico" (Joel, 14/10)
4
+ *
5
+ * O boneco mexia em seno puro: todo membro no mesmo tempo, a mesma volta
6
+ * idêntica para sempre, parada seca quando a ação acabava e o corpo congelado
7
+ * quando não falava. Aqui moram os princípios de animação, em funções puras
8
+ * (o node testa, o kit/voxel.jsx e o kit/personagem.jsx desenham):
9
+ * - mola: massa e amortecimento — acelera, passa um pouco do ponto e assenta;
10
+ * - antecipa: recua antes de ir e passa um tico depois (follow-through);
11
+ * - atraso: tronco, cabeça, braço e mão com tempos diferentes (sobreposição);
12
+ * - ruido: a micro variação que nunca repete, igual em todo render (semente);
13
+ * - respiracao: inspira mais rápido do que expira, o ritmo varia um pouco;
14
+ * - salto: agacha, estica no impulso, cai acelerando, amassa no pouso e assenta;
15
+ * - passo: o quadril que balança, o corpo que desce no contato, o tronco que
16
+ * gira contra o quadril e o braço que vai atrasado da perna.
17
+ * Nada de Math.random nem Date.now: o mesmo quadro sai igual sempre. */
18
+ 'use strict';
19
+
20
+ const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
21
+ const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
22
+
23
+ /** Um número de 0 a 1 a partir de um número inteiro e de uma semente (texto ou número). */
24
+ function sementeDe(s) {
25
+ if (typeof s === 'number') return s;
26
+ let h = 2166136261;
27
+ for (const c of String(s || '')) { h ^= c.charCodeAt(0); h = Math.imul(h, 16777619); }
28
+ return (h >>> 0) % 100000;
29
+ }
30
+ function sorte(n, semente = 0) {
31
+ const x = Math.sin(n * 127.1 + sementeDe(semente) * 311.7) * 43758.5453;
32
+ return x - Math.floor(x);
33
+ }
34
+
35
+ /** Ruído suave de -1 a 1 (dois tons somados): a deriva que nunca repete igual. */
36
+ function ruido(t, semente = 0) {
37
+ const tom = (x, s) => {
38
+ const i = Math.floor(x), f = x - i;
39
+ const a = sorte(i, s) * 2 - 1, b = sorte(i + 1, s) * 2 - 1;
40
+ const k = f * f * f * (f * (f * 6 - 15) + 10); // o quíntico: sem quina na emenda
41
+ return a + (b - a) * k;
42
+ };
43
+ const s = sementeDe(semente);
44
+ return tom(t, s) * 0.68 + tom(t * 2.17 + 13.3, s + 7) * 0.32;
45
+ }
46
+
47
+ /**
48
+ * A MOLA: a resposta de massa-mola-amortecedor indo de 0 a 1, `s` segundos
49
+ * depois do gatilho. `freq` em Hz, `amort` < 1 passa do ponto e volta.
50
+ */
51
+ function mola(s, { freq = 2.2, amort = 0.5 } = {}) {
52
+ if (s <= 0) return 0;
53
+ const w = 2 * Math.PI * freq;
54
+ if (amort >= 1) return 1 - Math.exp(-w * s) * (1 + w * s);
55
+ const wd = w * Math.sqrt(1 - amort * amort);
56
+ return 1 - Math.exp(-amort * w * s) * (Math.cos(wd * s) + (amort * w / wd) * Math.sin(wd * s));
57
+ }
58
+
59
+ /**
60
+ * A curva de 0 a 1 que RECUA antes de ir (antecipação) e passa um tico do fim
61
+ * antes de assentar (follow-through). Lenta nas pontas, rápida no meio.
62
+ */
63
+ function antecipa(x, { recuo = 0.1, passa = 0.06 } = {}) {
64
+ const k = clamp(x);
65
+ const base = suave(k);
66
+ const antes = -recuo * Math.sin(Math.PI * clamp(k / 0.3)) * (k < 0.3 ? 1 : 0);
67
+ const depois = passa * Math.sin(Math.PI * clamp((k - 0.62) / 0.38)) * (k > 0.62 ? 1 : 0);
68
+ return base + antes + depois;
69
+ }
70
+
71
+ /**
72
+ * O PESO DE UMA AÇÃO entre `de` e `ate` (segundos): entra com mola (passa e
73
+ * assenta), sai devagar, e nunca corta seco. `atraso` é a sobreposição: o
74
+ * membro que vem depois (cotovelo depois do ombro, cabeça depois do tronco).
75
+ */
76
+ function envelope(t, de, ate, { atraso = 0, entra = 2.4, sai = 0.55, amort = 0.55 } = {}) {
77
+ const tt = t - atraso;
78
+ if (tt < de) return 0;
79
+ const dentro = mola(tt - de, { freq: entra, amort }); // passa de 1 e volta: o membro assenta
80
+ const fora = tt > ate ? 1 - suave((tt - ate) / sai) : 1;
81
+ return Math.max(0, dentro * fora);
82
+ }
83
+
84
+ /**
85
+ * O GESTO de ênfase, `s` segundos depois de começar: o braço cai um pouco
86
+ * (antecipação), sobe rápido passando do ponto, segura e desce devagar.
87
+ * Dura ~1,1 s; fora disso é 0.
88
+ */
89
+ function golpe(s) {
90
+ if (s <= 0 || s >= 1.15) return 0;
91
+ const sobe = s < 0.1 ? -0.14 * Math.sin(Math.PI * s / 0.1) : mola(s - 0.1, { freq: 2.6, amort: 0.5 });
92
+ const desce = 1 - suave((s - 0.55) / 0.6);
93
+ return sobe * desce;
94
+ }
95
+
96
+ /**
97
+ * A RESPIRAÇÃO de 0 a 1: inspira em ~40% do ciclo, expira no resto, e o
98
+ * ciclo (≈3,4 s) varia um pouco a cada volta, pela semente.
99
+ */
100
+ function respiracao(t, semente = 0) {
101
+ const periodo = 3.4;
102
+ const fase = t / periodo + 0.12 * ruido(t * 0.15, semente) + sorte(1, semente);
103
+ const p = fase - Math.floor(fase);
104
+ return p < 0.4 ? suave(p / 0.4) : 1 - suave((p - 0.4) / 0.6);
105
+ }
106
+
107
+ /**
108
+ * O SALTO, num ciclo de `periodo` s: agacha (amassa), impulsiona (estica),
109
+ * sobe e cai em parábola (a gravidade: a queda acelera), amassa no pouso e
110
+ * assenta. Devolve {y, sx, sy}: a altura e a escala de amassar/esticar
111
+ * (o volume fica: sx ≈ 1/√sy).
112
+ */
113
+ function salto(t, { periodo = 0.95, altura = 0.32, fase = 0 } = {}) {
114
+ const p = ((t / periodo + fase) % 1 + 1) % 1;
115
+ const AGACHA = 0.16, AR = 0.6, POUSO = 1 - AGACHA - AR;
116
+ let y = 0, sy = 1;
117
+ if (p < AGACHA) {
118
+ const k = p / AGACHA;
119
+ const amassa = Math.sin(Math.PI * Math.min(1, k * 1.25)) * (k < 0.8 ? 1 : 0);
120
+ sy = 1 - 0.14 * amassa;
121
+ y = -0.05 * altura * amassa;
122
+ if (k >= 0.8) sy = 1 + 0.12 * suave((k - 0.8) / 0.2); // o impulso: estica antes de sair do chão
123
+ } else if (p < AGACHA + AR) {
124
+ const q = (p - AGACHA) / AR;
125
+ y = altura * 4 * q * (1 - q); // a parábola: sobe freando, desce acelerando
126
+ sy = 1 + 0.12 * Math.pow(Math.abs(1 - 2 * q), 1.5); // estica com a velocidade, redondo no alto
127
+ } else {
128
+ const k = (p - AGACHA - AR) / POUSO;
129
+ const amortece = Math.exp(-4.2 * k) * Math.cos(k * Math.PI * 2.2);
130
+ sy = 1 - 0.18 * amortece; // o pouso amassa e assenta em mola
131
+ y = -0.04 * altura * Math.max(0, amortece);
132
+ }
133
+ return { y, sy, sx: 1 / Math.sqrt(Math.max(0.5, sy)) };
134
+ }
135
+
136
+ /**
137
+ * O PASSO, `t` em s, `ritmo` em passos por segundo (andar ≈ 1,8, correr ≈ 3).
138
+ * Devolve os ângulos (rad) e o deslocamento do corpo:
139
+ * perna[esq,dir] balanço da coxa (para frente é negativo no eixo X)
140
+ * braco[esq,dir] o braço oposto à perna, atrasado (sobreposição)
141
+ * quadril o rolar do quadril para o lado da perna que apoia
142
+ * giro o tronco que gira contra o quadril
143
+ * sobe o corpo desce no contato e sobe na passagem
144
+ */
145
+ function passo(t, { ritmo = 1.8, amplitude = 0.42, correr = false, semente = 0 } = {}) {
146
+ const f = Math.PI * ritmo * t + sorte(3, semente) * 0.3;
147
+ // A perna vai para frente mais rápido do que volta no apoio: a onda torta.
148
+ const onda = (x) => Math.sin(x + 0.22 * Math.sin(x));
149
+ const a = amplitude * (correr ? 1.6 : 1);
150
+ const atrasoBraco = correr ? 0.18 : 0.32; // o braço chega depois da perna
151
+ return {
152
+ perna: [onda(f) * a, onda(f + Math.PI) * a],
153
+ braco: [onda(f + Math.PI - atrasoBraco) * a * 0.9, onda(f - atrasoBraco) * a * 0.9],
154
+ quadril: Math.sin(f - 0.35) * (correr ? 0.03 : 0.055),
155
+ giro: -Math.sin(f - 0.15) * (correr ? 0.12 : 0.07),
156
+ sobe: (0.5 + 0.5 * Math.cos(2 * f)) * (correr ? 0.09 : 0.045),
157
+ inclina: correr ? 0.14 : 0.03, // quem corre vai com o tronco para frente
158
+ };
159
+ }
160
+
161
+ module.exports = { clamp, suave, sorte, ruido, mola, antecipa, envelope, golpe, respiracao, salto, passo };
@@ -16,6 +16,8 @@ import { ThreeCanvas } from '@remotion/three';
16
16
  import * as THREE from 'three';
17
17
  import { useThree } from '@react-three/fiber';
18
18
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
19
+ // O movimento com peso (mola, antecipação, sobreposição, respiração): kit/natural.js.
20
+ import { ruido, envelope, golpe, respiracao } from './natural';
19
21
 
20
22
  const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
21
23
  const lerp = (a, b, k) => a + (b - a) * k;
@@ -93,10 +95,10 @@ function gesto(falas, quem, t) {
93
95
  if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
94
96
  f.palavras.forEach((w, k) => {
95
97
  if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
96
- const x = (t - (w.de - 0.15)) / 0.9;
97
- if (x < 0 || x > 1) return;
98
- const b = Math.sin(Math.PI * x);
99
- if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
98
+ // Recua, sobe passando do ponto e desce devagar (natural.golpe), não um seno.
99
+ const b = golpe(t - (w.de - 0.15));
100
+ if (b === 0) return;
101
+ if (k % 2) esq = Math.abs(b) > Math.abs(esq) ? b : esq; else dir = Math.abs(b) > Math.abs(dir) ? b : dir;
100
102
  });
101
103
  }
102
104
  return [esq, dir];
@@ -256,12 +258,14 @@ function RostoRobo({ p, est, acento }) {
256
258
  }
257
259
 
258
260
  /* ── O CORPO, OS BRAÇOS E O PERSONAGEM INTEIRO ───────────────────────────── */
259
- function Braco({ lado, g, cor, pele, robo, t }) {
260
- // Repouso: o antebraço deitado na mesa. Gesto: a mão sobe e mexe um pouco.
261
- const ombroX = lerp(-0.3, -0.62, g) + Math.sin(t * 6 + lado) * 0.05 * g;
262
- const ombroZ = lado * lerp(0.08, 0.28, g);
263
- const cotovelo = lerp(-1.17, -1.75, g) + Math.sin(t * 7.3 + lado * 2) * 0.12 * g;
264
- const pulso = Math.sin(t * 9 + lado) * 0.25 * g;
261
+ function Braco({ lado, g, gc = g, gp = gc, cor, pele, robo, t, sem = 0 }) {
262
+ // Repouso: o antebraço deitado na mesa, que deriva de leve (nunca congelado).
263
+ // Gesto: o ombro leva, o cotovelo (gc) e o pulso (gp) vêm depois — a sobreposição.
264
+ const deriva = ruido(t * 0.4, sem + lado * 5) * 0.03;
265
+ const ombroX = lerp(-0.3, -0.62, g) + Math.sin(t * 6 + lado) * 0.05 * Math.max(0, g) + deriva;
266
+ const ombroZ = lado * lerp(0.08, 0.28, Math.max(0, g));
267
+ const cotovelo = lerp(-1.17, -1.75, gc) + Math.sin(t * 7.3 + lado * 2 - 0.8) * 0.12 * Math.max(0, gc) + deriva * 0.5;
268
+ const pulso = Math.sin(t * 9 + lado - 1.4) * 0.25 * Math.max(0, gp) + ruido(t * 0.7, sem + lado) * 0.06;
265
269
  const tecido = robo ? <Mat c={cor} r={0.3} m={0.1} cc={1} /> : <Mat c={cor} r={0.85} sheen="#ffffff" />;
266
270
  return (
267
271
  <group position={[lado * 0.44, 1.18, 0]} rotation={[ombroX, 0, ombroZ]}>
@@ -279,7 +283,8 @@ function Braco({ lado, g, cor, pele, robo, t }) {
279
283
  function Personagem({ p, est, acento, pos, giro }) {
280
284
  const robo = p.tipo === 'robo';
281
285
  const corpo = robo ? p.cor : p.roupa;
282
- const resp = 1 + Math.sin(est.t * 1.6 + pos[0]) * 0.012;
286
+ // O fôlego: inspira rápido, expira devagar, o ritmo varia (natural.respiracao).
287
+ const resp = 1 + respiracao(est.t, (est.semente || 0) + pos[0]) * 0.016;
283
288
  const [cy, cp, cr] = est.cabeca;
284
289
  return (
285
290
  <group position={pos} rotation={[0, giro, 0]}>
@@ -293,8 +298,7 @@ function Personagem({ p, est, acento, pos, giro }) {
293
298
  : <mesh position={[0, 1.27, 0]} rotation={[Math.PI / 2, 0, 0]}><torusGeometry args={[0.15, 0.04, 12, 32]} /><Mat c={new THREE.Color(corpo).multiplyScalar(0.7).getStyle()} r={0.85} /></mesh>}
294
299
  <mesh position={[0, 1.3, 0]}><cylinderGeometry args={[0.12, 0.13, 0.25, 24]} />{robo ? <Mat c="#3a3e48" r={0.4} m={0.6} /> : <Mat c={p.pele} r={0.6} sheen="#ff9a8a" />}</mesh>
295
300
  </group>
296
- <Braco lado={-1} g={est.gesto[0]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
297
- <Braco lado={1} g={est.gesto[1]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
301
+ {[-1, 1].map((l) => { const k = l > 0 ? 1 : 0; return <Braco key={l} lado={l} g={est.gesto[k]} gc={(est.gestoAtras || est.gesto)[k]} gp={(est.gestoMao || est.gesto)[k]} cor={corpo} pele={p.pele} robo={robo} t={est.t} sem={est.semente || 0} />; })}
298
302
  <group position={[0, 1.62, 0]} rotation={[cp, cy, cr]} castShadow>
299
303
  {robo ? <RostoRobo p={p} est={est} acento={acento} /> : <Rosto p={p} est={est} />}
300
304
  </group>
@@ -529,19 +533,31 @@ export function PersonagemCena({ el, t }) {
529
533
  for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
530
534
  return e;
531
535
  };
536
+ // O quanto cada um está falando (0 a 1, sem degrau): os acenos trocam com mola.
537
+ const pesoDaFala = (i, reacao = 0) => falas.reduce((m, f) => (f.quem === i ? Math.max(m, Math.min(1, envelope(t, f.de + reacao, f.ate, { entra: 2.6, sai: 0.45, amort: 0.7 }))) : m), 0);
532
538
  const estado = (i) => {
539
+ const sem = i * 13 + 1;
533
540
  const fala = falandoAgora(i);
534
- const outro = dois && falandoAgora(1 - i);
541
+ const wFala = pesoDaFala(i);
542
+ // Quem ouve reage ~0,2 s depois, com mola, e volta devagar.
543
+ const wOuve = dois ? pesoDaFala(1 - i, 0.2) : 0;
535
544
  const enfase = gesto(falas, i, t);
536
- const nod = Math.sin(t * (fala ? 5.2 : 1.4) + i) * (fala ? 0.035 : 0.015) + (outro ? Math.max(0, Math.sin(t * 2.1 + i)) * 0.04 : 0);
545
+ const nod = lerp(ruido(t * 0.5, sem + 3) * 0.015, Math.sin(t * 5.2 + i) * 0.028 + ruido(t * 2.4, sem + 2) * 0.016, wFala)
546
+ + wOuve * Math.max(0, Math.sin(t * 2.1 + i)) * 0.04;
537
547
  const emo = emocaoDe(i);
538
- const e = EMO[emo] || EMO.neutro;
539
- const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.08;
548
+ // A inclinação da emoção chega e vai embora com peso, não troca no quadro.
549
+ let inclina = 0;
550
+ for (const f of falas) {
551
+ if (f.quem !== i || f.de > t + 0.2) continue;
552
+ inclina = ((EMO[f.emocao] || EMO.neutro).inclina || 0) * Math.min(1.05, envelope(t, f.de - 0.2, f.ate + 2.5, { entra: 1.4, sai: 0.8, amort: 0.6 }));
553
+ }
554
+ const micro = (hash(Math.floor(t * 1.5 + hash(i) * 0.9) + i * 7) - 0.5) * 0.08;
540
555
  return {
541
- t, falando: fala, emocao: emo, boca: boca(falas, i, t), pisca: piscada(i, t),
542
- gesto: enfase, enfase: Math.max(enfase[0], enfase[1]),
556
+ t, semente: sem, falando: fala, emocao: emo, boca: boca(falas, i, t), pisca: piscada(i, t),
557
+ gesto: enfase, gestoAtras: gesto(falas, i, t - 0.08), gestoMao: gesto(falas, i, t - 0.15), enfase: Math.max(enfase[0], enfase[1], 0),
543
558
  olhar: dois ? [micro, 0.02] : [micro * 0.5, 0.04],
544
- cabeca: [Math.sin(t * 0.7 + i * 2) * 0.08 + (outro ? 0.05 : 0), nod + (fala ? 0 : 0.03), (e.inclina || 0) * (i ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
559
+ // A cabeça deriva pelo ruído (nunca a mesma volta) e vem ~0,06 s atrás do corpo.
560
+ cabeca: [ruido((t - 0.06) * 0.35, sem + 5) * 0.09 + wOuve * 0.05, nod + (1 - wFala) * 0.03, inclina * (i ? -1 : 1) + ruido((t - 0.06) * 0.25, sem + 7) * 0.025],
545
561
  };
546
562
  };
547
563
  const posDe = (i) => (dois ? [(elenco[i].lado || 0) * 0.95, 0, 0] : [0, 0, 0]);
@@ -31,6 +31,8 @@ import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
31
31
  import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
32
32
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
33
33
  import { Mundo, Animal, Coisa, alturaDoBicho, frenteDoBicho, useIcones } from './mundo.jsx';
34
+ // O movimento com peso (mola, antecipação, sobreposição, respiração, salto, passo): kit/natural.js.
35
+ import { ruido, envelope, golpe, respiracao, salto, passo } from './natural';
34
36
 
35
37
  const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
36
38
  const lerp = (a, b, k) => a + (b - a) * k;
@@ -144,10 +146,10 @@ function gesto(falas, quem, t) {
144
146
  if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
145
147
  f.palavras.forEach((w, k) => {
146
148
  if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
147
- const x = (t - (w.de - 0.15)) / 1.0;
148
- if (x < 0 || x > 1) return;
149
- const b = Math.sin(Math.PI * x);
150
- if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
149
+ // O braço recua, sobe passando do ponto e desce devagar (natural.golpe), não um seno.
150
+ const b = golpe(t - (w.de - 0.15));
151
+ if (b === 0) return;
152
+ if (k % 2) esq = Math.abs(b) > Math.abs(esq) ? b : esq; else dir = Math.abs(b) > Math.abs(dir) ? b : dir;
151
153
  });
152
154
  }
153
155
  return [esq, dir];
@@ -372,26 +374,36 @@ function CabecaFacetada({ p, est }) {
372
374
  /* A AÇÃO da fala mexe o braço por cima do gesto: acenar (a mão direita no
373
375
  alto, abanando), apontar (o braço esticado para a frente), comemorar (os
374
376
  dois para cima), pensar (a mão no queixo), dançar (os dois balançando). */
375
- function poseDoBraco(acao, lado, t, k) {
377
+ function poseDoBraco(acao, lado, t, k, sem = 0) {
376
378
  if (!acao || k <= 0) return null;
377
379
  const dir = lado > 0;
378
- if (acao === 'acenar' && dir) return { x: -0.25, z: 2.55 + Math.sin(t * 9) * 0.25, c: -0.55 + Math.sin(t * 9) * 0.35 };
379
- if (acao === 'apontar' && dir) return { x: -1.45, z: 0.12, c: -0.05 };
380
- if (acao === 'comemorar') return { x: -0.2, z: lado * (2.7 + Math.sin(t * 8 + lado) * 0.15), c: -0.25 };
381
- if (acao === 'pensar' && dir) return { x: -1.5, z: -0.35, c: -2.1 };
382
- if (acao === 'dancar') return { x: -0.6 + Math.sin(t * 6 + lado) * 0.5, z: lado * (0.6 + Math.sin(t * 6) * 0.3), c: -1.1 };
383
- if (acao === 'correr') return { x: Math.sin(t * 13 + (lado > 0 ? 0 : Math.PI)) * 0.9, z: lado * 0.1, c: -1.3 };
384
- if (acao === 'andar') return { x: Math.sin(t * 7 + (lado > 0 ? 0 : Math.PI)) * 0.45, z: lado * 0.06, c: -0.25 };
380
+ // O antebraço vem ATRASADO do braço (sobreposição): a mesma onda, a fase depois.
381
+ if (acao === 'acenar' && dir) return { x: -0.25, z: 2.55 + Math.sin(t * 9) * 0.22, c: -0.55 + Math.sin(t * 9 - 1.1) * 0.4 };
382
+ if (acao === 'apontar' && dir) return { x: -1.45 + ruido(t * 0.8, sem + 3) * 0.04, z: 0.12, c: -0.05 };
383
+ if (acao === 'comemorar') return { x: -0.2, z: lado * (2.7 + Math.sin(t * 8 + lado) * 0.15), c: -0.25 + Math.sin(t * 8 + lado - 1) * 0.2 };
384
+ if (acao === 'pensar' && dir) return { x: -1.5 + ruido(t * 0.5, sem + 4) * 0.05, z: -0.35, c: -2.1 };
385
+ if (acao === 'dancar') return { x: -0.6 + Math.sin(t * 6 + lado) * 0.5, z: lado * (0.6 + Math.sin(t * 6 - 0.5) * 0.3), c: -1.1 + Math.sin(t * 6 + lado - 0.9) * 0.3 };
386
+ // Andar e correr: o braço oposto à perna, atrasado dela (natural.passo).
387
+ if (acao === 'correr' || acao === 'andar') {
388
+ const ps = passo(t, { correr: acao === 'correr', semente: sem });
389
+ const b = ps.braco[dir ? 1 : 0];
390
+ return acao === 'correr' ? { x: b * 1.2, z: lado * 0.1, c: -1.3 + b * 0.25 } : { x: b, z: lado * 0.06, c: -0.25 + Math.min(0, b) * 0.5 };
391
+ }
385
392
  return null;
386
393
  }
387
394
 
388
- function BracoBloco({ lado, g, cor, pele, t, acao, k = 1 }) {
389
- // Repouso: o braço caído, um tico para fora. Gesto: o antebraço sobe e a mão mexe.
390
- let ombroX = lerp(0.08, -0.85, g) + Math.sin(t * 6 + lado) * 0.06 * g;
391
- let ombroZ = lado * lerp(0.06, 0.22, g);
392
- let cotovelo = lerp(-0.12, -1.05, g) + Math.sin(t * 7.3 + lado * 2) * 0.14 * g;
393
- const pose = poseDoBraco(acao, lado, t, k);
394
- if (pose) { ombroX = lerp(ombroX, pose.x, k); ombroZ = lerp(ombroZ, pose.z, k); cotovelo = lerp(cotovelo, pose.c, k); }
395
+ function BracoBloco({ lado, g, gc = g, cor, pele, t, acao, k = 1, kc = k, ar = 0, sem = 0 }) {
396
+ // Repouso: o braço caído, um tico para fora — e nunca parado: ele acompanha o
397
+ // fôlego (abre na inspiração) e deriva pelo ruído. O cotovelo vem depois do
398
+ // ombro (gc e kc são o gesto e a ação de ~0,1 s antes): a sobreposição.
399
+ const deriva = ruido(t * 0.45, sem + lado * 5) * 0.035;
400
+ let ombroX = lerp(0.08, -0.85, g) + Math.sin(t * 6 + lado) * 0.06 * Math.max(0, g) + deriva;
401
+ let ombroZ = lado * (lerp(0.06, 0.22, Math.max(0, g)) + ar * 0.025);
402
+ let cotovelo = lerp(-0.12, -1.05, gc) + Math.sin(t * 7.3 + lado * 2 - 0.8) * 0.14 * Math.max(0, gc) + deriva * 0.6;
403
+ const pose = poseDoBraco(acao, lado, t, k, sem);
404
+ const poseC = poseDoBraco(acao, lado, t - 0.09, kc, sem);
405
+ if (pose) { ombroX = lerp(ombroX, pose.x, k); ombroZ = lerp(ombroZ, pose.z, k); }
406
+ if (poseC) cotovelo = lerp(cotovelo, poseC.c, kc);
395
407
  return (
396
408
  <group position={[lado * 0.47, 1.4, 0]} rotation={[ombroX, 0, ombroZ]}>
397
409
  <Bloco s={[0.2, 0.36, 0.2]} p={[0, -0.17, 0]} c={cor} r={0.04} rough={0.8} />
@@ -404,42 +416,65 @@ function BracoBloco({ lado, g, cor, pele, t, acao, k = 1 }) {
404
416
  }
405
417
 
406
418
  function PessoaBloco({ p, est }) {
407
- const resp = Math.sin(est.t * 1.7) * 0.008;
419
+ const t = est.t, sem = est.semente || 0;
420
+ // O fôlego: inspira rápido, expira devagar, o ritmo varia (natural.respiracao).
421
+ const ar = respiracao(t, sem);
422
+ const resp = ar * 0.012;
408
423
  const [cy, cp, cr] = est.cabeca;
409
424
  const calca = escurecer(p.roupa, 0.35);
410
425
  const a = est.acao, ka = est.forcaAcao || 0;
411
- // O corpo inteiro: pular e comemorar sobem, dançar balança, andar e correr marcham.
412
- const pulo = (a === 'pular' || a === 'comemorar') ? Math.abs(Math.sin(est.t * 6.5)) * 0.32 * ka : 0;
413
- const balanco = a === 'dancar' ? Math.sin(est.t * 6) * 0.14 * ka : 0;
414
- const marcha = (a === 'andar' || a === 'correr') ? (a === 'correr' ? 13 : 7) : 0;
426
+ // O corpo inteiro. Pular e comemorar: agacha, estica, cai acelerando e amassa
427
+ // no pouso (natural.salto). Dançar: o quadril leva, o tronco e a cabeça vêm
428
+ // atrasados. Andar e correr: o passo com quadril, contato e contragiro.
429
+ const pula = a === 'pular' || a === 'comemorar';
430
+ const sl = pula ? salto(t, a === 'comemorar' ? { periodo: 0.8, altura: 0.22 } : { periodo: 0.95, altura: 0.36 }) : null;
431
+ const anda = a === 'andar' || a === 'correr';
432
+ const ps = anda ? passo(t, { correr: a === 'correr', semente: sem }) : null;
433
+ const danca = a === 'dancar';
434
+ // Parado, o peso troca de perna devagar: nunca congelado.
435
+ const peso = ruido(t * 0.22, sem + 11) * 0.018;
436
+ const y = (sl ? sl.y * ka : 0) + (ps ? ps.sobe * ka : 0) + (danca ? (0.5 + 0.5 * Math.cos(t * 12)) * 0.04 * ka : 0);
437
+ const sy = sl ? lerp(1, sl.sy, Math.min(1, ka)) : 1, sx = sl ? lerp(1, sl.sx, Math.min(1, ka)) : 1;
438
+ const quadril = peso + (ps ? ps.quadril * ka : 0) + (danca ? Math.sin(t * 6) * 0.14 * ka : 0);
439
+ // O tronco gira contra o quadril (andar) ou vem atrasado dele (dançar).
440
+ const troncoZ = danca ? -Math.sin(t * 6 - 0.6) * 0.07 * ka : -peso * 0.5;
441
+ const troncoY = ps ? ps.giro * ka : 0;
442
+ const troncoX = (ps ? ps.inclina * ka : 0) + ar * -0.006;
443
+ // A cabeça segura o olhar: desfaz parte do giro do tronco, e balança por último.
444
+ const cabecaZ = cr + (danca ? Math.sin(t * 6 - 1.2) * 0.08 * ka : 0) - quadril * 0.4;
415
445
  return (
416
- <group position={[0, pulo, 0]} rotation={[0, 0, balanco]}>
446
+ <group position={[0, y, 0]} rotation={[0, 0, quadril]} scale={[sx, sy, sx]}>
417
447
  {/* as pernas e o sapato */}
418
448
  {[-1, 1].map((k) => {
419
- const passo = marcha ? Math.sin(est.t * marcha + (k > 0 ? 0 : Math.PI)) * (a === 'correr' ? 0.7 : 0.4) * ka : 0;
449
+ const perna = ps ? ps.perna[k > 0 ? 1 : 0] * ka : 0;
420
450
  return (
421
- <group key={k} position={[k * 0.15, 0.76, 0]} rotation={[passo, 0, 0]}>
451
+ <group key={k} position={[k * 0.15, 0.76, 0]} rotation={[perna, 0, -quadril * 0.8]}>
422
452
  <Bloco s={[0.25, 0.72, 0.27]} p={[0, -0.36, 0]} c={calca} r={0.03} rough={0.85} />
423
453
  <Bloco s={[0.27, 0.1, 0.36]} p={[0, -0.71, 0.04]} c="#1a1a1f" r={0.03} />
424
454
  </group>
425
455
  );
426
456
  })}
427
- <group position={[0, resp, 0]}>
428
- {/* o tronco, com a faixa branca da camisa da referência */}
429
- <Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
430
- <Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
431
- <Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
432
- </group>
433
- <BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
434
- <BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
435
- <group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
436
- {p.cabeca !== 'facetada' ? (
437
- <>
438
- <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
439
- <RostoPixel p={p} est={est} />
440
- <Cabelo estilo={p.cabelo} cor={p.corCabelo} />
441
- </>
442
- ) : <CabecaFacetada p={p} est={est} />}
457
+ {/* do quadril para cima: o tronco, os braços e a cabeça giram juntos no quadril */}
458
+ <group position={[0, 0.76, 0]} rotation={[troncoX, troncoY, troncoZ]}>
459
+ <group position={[0, -0.76, 0]}>
460
+ <group position={[0, resp, 0]} scale={[1 + ar * 0.008, 1 + ar * 0.014, 1 + ar * 0.02]}>
461
+ {/* o tronco, com a faixa branca da camisa da referência */}
462
+ <Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
463
+ <Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
464
+ <Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
465
+ </group>
466
+ <BracoBloco lado={-1} g={est.gesto[0]} gc={(est.gestoAtras || est.gesto)[0]} cor={p.roupa} pele={p.pele} t={t} acao={a} k={ka} kc={est.forcaAtras ?? ka} ar={ar} sem={sem} />
467
+ <BracoBloco lado={1} g={est.gesto[1]} gc={(est.gestoAtras || est.gesto)[1]} cor={p.roupa} pele={p.pele} t={t} acao={a} k={ka} kc={est.forcaAtras ?? ka} ar={ar} sem={sem} />
468
+ <group position={[0, 1.8 + resp * 1.2, 0]} rotation={[cp - troncoX * 0.6, cy - troncoY * 0.6, cabecaZ]}>
469
+ {p.cabeca !== 'facetada' ? (
470
+ <>
471
+ <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
472
+ <RostoPixel p={p} est={est} />
473
+ <Cabelo estilo={p.cabelo} cor={p.corCabelo} />
474
+ </>
475
+ ) : <CabecaFacetada p={p} est={est} />}
476
+ </group>
477
+ </group>
443
478
  </group>
444
479
  </group>
445
480
  );
@@ -454,15 +489,20 @@ function PessoaBloco({ p, est }) {
454
489
  function Mascote({ p, est }) {
455
490
  const cor = p.cor || '#d9532b';
456
491
  const [abre] = est.boca;
457
- const pulo = (est.acao === 'pular' || est.acao === 'comemorar') ? Math.abs(Math.sin(est.t * 6)) * 0.25 * (est.forcaAcao || 0) : 0;
458
- const respira = Math.sin(est.t * 1.9 + 0.7) * 0.012;
459
- const estica = 1 + abre * 0.07 + respira, achata = 1 - abre * 0.035;
492
+ const ka = Math.min(1, est.forcaAcao || 0);
493
+ // O pulo com peso: agacha, estica no impulso, cai acelerando e amassa no pouso (natural.salto).
494
+ const sl = (est.acao === 'pular' || est.acao === 'comemorar') ? salto(est.t, { periodo: 0.85, altura: 0.3 }) : { y: 0, sx: 1, sy: 1 };
495
+ const pulo = sl.y * ka;
496
+ const respira = (respiracao(est.t, (est.semente || 0) + 0.7) - 0.5) * 0.024;
497
+ const estica = (1 + abre * 0.07 + respira) * lerp(1, sl.sy, ka), achata = (1 - abre * 0.035) * lerp(1, sl.sx, ka);
460
498
  const pisca = clamp(est.pisca);
461
499
  const [cy, cp, cr] = est.cabeca;
462
500
  const W = 1.0, H = 0.92, D = 0.78, perna = 0.46;
463
501
  const pernas = [-0.38, -0.19, 0, 0.19, 0.38];
502
+ // O corpo balança de leve sobre as perninhas, atrasado da cabeça (sobreposição).
503
+ const ginga = ruido(est.t * 0.3, (est.semente || 0) + 21) * 0.025;
464
504
  return (
465
- <group position={[0, pulo, 0]} rotation={[0, cy * 0.8, cr * 0.5]}>
505
+ <group position={[0, pulo, 0]} rotation={[0, cy * 0.8, cr * 0.5 + ginga]}>
466
506
  {pernas.map((x, i) => (
467
507
  <group key={i}>
468
508
  <Bloco s={[0.13, perna, 0.13]} p={[x, perna / 2 + Math.max(0, Math.sin(est.t * 9 + i)) * (est.falando ? 0.015 : 0), D / 2 - 0.12]} c={escurecer(cor, 0.9)} r={0.015} rough={0.6} />
@@ -804,33 +844,58 @@ export function VoxelCena({ el, t }) {
804
844
  for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
805
845
  return e;
806
846
  };
807
- // A ação da fala vale da voz até 0,6 s depois, entrando e saindo suave.
847
+ // A ação da fala vale da voz até 0,6 s depois: entra em mola (passa do ponto e
848
+ // assenta) e sai devagar. O cotovelo pega a mesma ação ~0,1 s depois (sobreposição).
808
849
  const acaoDe = (i) => {
809
850
  for (const f of falas) {
810
851
  if (f.quem !== i || !f.acao || f.acao === 'falar') continue;
811
- if (t >= f.de - 0.3 && t <= f.ate + 0.6) return [f.acao, Math.min(suave((t - f.de + 0.3) / 0.4), 1 - suave((t - f.ate - 0.1) / 0.5))];
852
+ if (t >= f.de - 0.3 && t <= f.ate + 0.6) {
853
+ const opc = { entra: 2.2, sai: 0.5, amort: 0.55 };
854
+ return [f.acao, envelope(t, f.de - 0.3, f.ate + 0.1, opc), envelope(t, f.de - 0.3, f.ate + 0.1, { ...opc, atraso: 0.09 })];
855
+ }
812
856
  }
813
- return [null, 0];
857
+ return [null, 0, 0];
814
858
  };
815
- const quemFala = () => { const f = falas.find((x) => t >= x.de && t <= x.ate); return f ? f.quem : -1; };
859
+ // O quanto cada um está falando (0 a 1, sem degrau): o aceno de cabeça de quem
860
+ // fala e o de quem ouve trocam com mola, não num corte seco.
861
+ const pesoDaFala = (i, reacao = 0) => falas.reduce((m, f) => (f.quem === i ? Math.max(m, Math.min(1, envelope(t, f.de + reacao, f.ate, { entra: 2.6, sai: 0.45, amort: 0.7 }))) : m), 0);
816
862
  const estado = (i, figurante = false) => {
863
+ const sem = i * 13 + (figurante ? 101 : 1);
817
864
  const fala = !figurante && falandoAgora(i);
818
- const qf = quemFala();
819
- const outro = qf >= 0 && qf !== i;
865
+ const wFala = figurante ? 0 : pesoDaFala(i);
820
866
  const enfase = figurante ? [0, 0] : gesto(falas, i, t);
821
- const nod = Math.sin(t * (fala ? 5.2 : 1.4) + i) * (fala ? 0.04 : 0.015) + (outro ? Math.max(0, Math.sin(t * 2.1 + i)) * 0.04 : 0);
867
+ const enfaseAtras = figurante ? [0, 0] : gesto(falas, i, t - 0.09);
868
+ // Quem ouve vira para quem fala com ~0,2 s de reação e mola, e volta devagar.
869
+ let paraQuemFala = 0, wOuve = 0, dx = 0;
870
+ if (!figurante) {
871
+ for (const f of falas) {
872
+ if (f.quem === i) continue;
873
+ const w = Math.min(1, envelope(t, f.de + 0.2, f.ate + 0.3, { entra: 1.6, sai: 0.7, amort: 0.62 }));
874
+ if (w > wOuve) { wOuve = w; dx = lugarDe(elenco, f.quem)[0] - lugarDe(elenco, i)[0]; }
875
+ }
876
+ paraQuemFala = clamp(dx * 0.35, -0.35, 0.35) * wOuve;
877
+ }
878
+ const nodFala = Math.sin(t * 5.2 + i) * 0.03 + ruido(t * 2.4, sem + 2) * 0.02;
879
+ const nodQuieto = ruido(t * 0.5, sem + 3) * 0.018;
880
+ const nod = lerp(nodQuieto, nodFala, wFala) + wOuve * Math.max(0, Math.sin(t * 2.1 + i)) * 0.04;
822
881
  const emo = figurante ? 'feliz' : emocaoDe(i);
823
- const e = EMO[emo] || EMO.neutro;
824
- const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.6;
825
- // Quem ouve vira um pouco para quem fala; quem fala olha para a câmera de vez em quando.
826
- const dx = outro && !figurante ? lugarDe(elenco, qf)[0] - lugarDe(elenco, i)[0] : 0;
827
- const paraQuemFala = clamp(dx * 0.35, -0.35, 0.35);
828
- const [acao, forcaAcao] = figurante ? [null, 0] : acaoDe(i);
882
+ // A inclinação da emoção chega e vai embora com peso, não troca no quadro.
883
+ let inclina = 0;
884
+ if (!figurante) {
885
+ for (const f of falas) {
886
+ if (f.quem !== i || f.de > t + 0.2) continue;
887
+ const ef = EMO[f.emocao] || EMO.neutro;
888
+ inclina = (ef.inclina || 0) * Math.min(1.05, envelope(t, f.de - 0.2, f.ate + 2.5, { entra: 1.4, sai: 0.8, amort: 0.6 }));
889
+ }
890
+ } else inclina = (EMO.feliz.inclina || 0);
891
+ const micro = (hash(Math.floor(t * 1.5 + hash(i) * 0.9) + i * 7) - 0.5) * 0.6;
892
+ const [acao, forcaAcao, forcaAtras] = figurante ? [null, 0, 0] : acaoDe(i);
829
893
  return {
830
- t, falando: fala, emocao: emo, boca: figurante ? [0, 0.55] : boca(falas, i, t), pisca: piscada(i + (figurante ? 5 : 0), t),
831
- gesto: enfase, enfase: Math.max(enfase[0], enfase[1]), acao, forcaAcao,
832
- olhar: [micro + (outro ? Math.sign(dx) * 0.8 : 0), 0.1],
833
- cabeca: [Math.sin(t * 0.7 + i * 2) * 0.07 + paraQuemFala, nod, (e.inclina || 0) * (i % 2 ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
894
+ t, semente: sem, falando: fala, emocao: emo, boca: figurante ? [0, 0.55] : boca(falas, i, t), pisca: piscada(i + (figurante ? 5 : 0), t),
895
+ gesto: enfase, gestoAtras: enfaseAtras, enfase: Math.max(enfase[0], enfase[1], 0), acao, forcaAcao, forcaAtras,
896
+ olhar: [micro + Math.sign(dx) * 0.8 * wOuve, 0.1],
897
+ // A cabeça deriva pelo ruído (nunca a mesma volta) e vem ~0,06 s atrás do corpo.
898
+ cabeca: [ruido((t - 0.06) * 0.35, sem + 5) * 0.08 + paraQuemFala, nod, inclina * (i % 2 ? -1 : 1) + ruido((t - 0.06) * 0.25, sem + 7) * 0.025],
834
899
  };
835
900
  };
836
901
  const desenhar = (p, est) => (p.tipo === 'robo' ? <Mascote p={p} est={est} />
@@ -67,7 +67,7 @@ const PECAS = {
67
67
  animacao: { precisa: ['src'] },
68
68
  };
69
69
 
70
- const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'personagem.jsx', 'mundo.jsx', 'voxel.jsx', 'animacao.jsx', 'index.jsx'];
70
+ const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'personagem.jsx', 'mundo.jsx', 'voxel.jsx', 'animacao.jsx', 'natural.js', 'index.jsx'];
71
71
 
72
72
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
73
73
  function arquivos() {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * sem-narracao.js — o vídeo inteiro sem voz: só o motion e os barulhinhos.
3
+ *
4
+ * "Motions sem narração: só o motion e os efeitos sonoros, os barulhinhos."
5
+ * — Joel (14/10/2026)
6
+ *
7
+ * Sem voz não há tempo MEDIDO: cada parte dura o `duracao` (s) que o roteiro
8
+ * deu ou, sem ele, o que o texto da tela pede para ser lido. As palavras ganham
9
+ * tempos repartidos na parte, para as cenas que entram palavra por palavra
10
+ * continuarem entrando. E o som sobe de papel: um efeito leve em cada corte e
11
+ * em cada entrada marcante (sons-leves.js), sem repetir o mesmo em sequência.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const r3 = (x) => Math.round(x * 1000) / 1000;
17
+
18
+ /* O pedido que tira a voz: "sem narração", "sem voz", "sem locução", "só efeito sonoro". */
19
+ const PEDIDO = /\bsem\s+(narra[cç][aã]o|narrador|voz|locu[cç][aã]o)\b|\bs[oó]\s+(o\s+motion\s+e\s+)?(os\s+)?(efeitos?\s+sonoros?|barulhinhos?|sfx)\b/i;
20
+
21
+ /** O vídeo é sem voz? `roteiro.semNarracao` manda (false desliga); sem ele, o pedido. */
22
+ function ativo(roteiro, pedido = '') {
23
+ const r = roteiro || {};
24
+ if (r.semNarracao === true || r.semNarracao === 'true') return true;
25
+ if (r.semNarracao === false || r.semNarracao === 'false') return false;
26
+ return PEDIDO.test(String(pedido || ''));
27
+ }
28
+
29
+ /** O que a TELA mostra da parte (é o que se lê; a fala só se não há mais nada). */
30
+ function textoDaTela(pt) {
31
+ if (!pt || typeof pt !== 'object') return '';
32
+ const lista = (x) => (Array.isArray(x) ? x.map((i) => (typeof i === 'string' ? i : (i && (i.texto || i.titulo || i.nome || i.rotulo)) || '')) : []);
33
+ const txt = [pt.titulo, pt.sub, pt.texto, pt.nao, pt.sim, pt.valor, pt.rotulo, pt.autor, ...lista(pt.itens), ...lista(pt.linhas)]
34
+ .filter((x) => x != null && String(x).trim()).join(' ');
35
+ return txt.trim() || String(pt.fala || '').trim();
36
+ }
37
+
38
+ /** Quanto a parte fica na tela: `duracao` do roteiro; senão 1,8 s + 0,25 s por palavra, entre 1,5 e 4 s. */
39
+ function duracaoDe(pt) {
40
+ const d = Number(pt && pt.duracao);
41
+ if (d > 0) return Math.max(0.5, Math.min(30, d));
42
+ const n = textoDaTela(pt).split(/\s+/).filter(Boolean).length;
43
+ return Math.max(1.5, Math.min(4, 1.8 + 0.25 * n));
44
+ }
45
+
46
+ /**
47
+ * Os tempos de cada parte, no mesmo formato que a narração devolve
48
+ * ({de, ate, palavras:[{texto, de, ate}]}): partes coladas uma na outra e as
49
+ * palavras repartidas nos primeiros 70% da parte (o resto é o respiro).
50
+ * `falaDe` dá o texto cujas palavras a cena procura (o do compositor).
51
+ */
52
+ function tempos(partes, { falaDe = textoDaTela } = {}) {
53
+ let t = 0;
54
+ return (partes || []).map((pt) => {
55
+ const dur = duracaoDe(pt);
56
+ const de = r3(t), ate = r3(t + dur);
57
+ const ws = String(falaDe(pt) || textoDaTela(pt) || '').split(/\s+/).filter(Boolean);
58
+ const ini = de + Math.min(0.2, dur * 0.1), janela = dur * 0.7 - (ini - de);
59
+ const passo = janela / Math.max(1, ws.length);
60
+ const palavras = ws.map((w, j) => ({ texto: w, de: r3(ini + j * passo), ate: r3(ini + j * passo + Math.max(0.08, passo * 0.8)) }));
61
+ t += dur;
62
+ return { de, ate, palavras };
63
+ });
64
+ }
65
+
66
+ /* Os momentos que pedem som: os CORTES (começo de parte, de batida, de cena)
67
+ e as ENTRADAS marcantes (o ícone, o item, a cena que a IA escreveu). */
68
+ function momentos({ tempos: tps = [], plano = null, partitura = null } = {}) {
69
+ const cortes = [], entradas = [];
70
+ const perto = (lista, t, d = 0.3) => lista.some((x) => Math.abs(x - t) < d);
71
+ const corte = (t) => { if (t > 0.3 && !perto(cortes, t)) cortes.push(r3(t)); };
72
+ const entrada = (t) => { if (t != null && t >= 0 && !perto(entradas, t, 0.35) && !perto(cortes, t, 0.2)) entradas.push(r3(t)); };
73
+ for (const t of tps.slice(1)) corte(t.de);
74
+ const bs = (partitura && partitura.batidas) || [];
75
+ for (const b of bs) corte(b.de);
76
+ // Vídeo de várias cenas (os outros estilos): o começo de cada cena é um corte.
77
+ let acc = 0;
78
+ for (const c of (plano && plano.cenas) || []) { corte(acc); acc += Number(c.duracao) || 0; }
79
+ entrada(tps.length ? tps[0].de + 0.1 : 0.1); // a abertura entra com som
80
+ for (const b of bs) {
81
+ if (b.icone3d && b.icone3d.t != null) entrada(b.icone3d.t);
82
+ for (const l of b.linhas || []) if ((l.papel === 'item' || l.papel === 'numero') && l.t != null) entrada(l.t);
83
+ if (b.comp === 'marca') entrada(b.de + 0.35);
84
+ }
85
+ for (const c of (plano && plano.cenas) || []) for (const el of c.elementos || []) if (el && el.tipo === 'componente' && el.de != null) entrada(el.de + 0.08);
86
+ return { cortes: cortes.sort((a, b) => a - b), entradas: entradas.sort((a, b) => a - b) };
87
+ }
88
+
89
+ // O corte passa com ar; a entrada chega com um toque pequeno. Ordem de rodízio.
90
+ const DO_CORTE = ['whoosh', 'sopro', 'toque', 'riser'];
91
+ const DA_ENTRADA = ['pop', 'bolha', 'clique', 'brilho'];
92
+ // O whoosh e o sopro antecipam o corte (o ar chega antes da imagem); o riser sobe até ele.
93
+ const ANTES = { whoosh: 0.18, sopro: 0.3, riser: 1.0, toque: 0 };
94
+ const VOLUME = { whoosh: 0.5, sopro: 0.6, toque: 0.6, riser: 0.4, pop: 0.42, bolha: 0.45, clique: 0.4, brilho: 0.32 };
95
+
96
+ /**
97
+ * Soma aos efeitos do plano um som leve em cada corte e em cada entrada.
98
+ * `sons` é o mapa nome → arquivo do sons-leves.preparar. Não encosta em som
99
+ * que já existe (0,12 s) e nunca repete o mesmo som duas vezes seguidas.
100
+ */
101
+ function sonorizar(efeitos, { cortes = [], entradas = [], sons = {}, duracao = Infinity } = {}) {
102
+ const fora = (efeitos || []).slice();
103
+ const eventos = [...cortes.map((t) => ({ t, tipo: 'corte' })), ...entradas.map((t) => ({ t, tipo: 'entrada' }))].sort((a, b) => a.t - b.t);
104
+ const giro = { corte: 0, entrada: 0 };
105
+ let ultimo = null;
106
+ const novos = [];
107
+ for (const ev of eventos) {
108
+ const roda = ev.tipo === 'corte' ? DO_CORTE : DA_ENTRADA;
109
+ let nome = null;
110
+ for (let k = 0; k < roda.length; k++) {
111
+ const n = roda[(giro[ev.tipo] + k) % roda.length];
112
+ if (!sons[n] || n === ultimo) continue;
113
+ // O riser precisa de espaço antes do corte.
114
+ if (n === 'riser' && ev.t < 1.2) continue;
115
+ nome = n; giro[ev.tipo] = (giro[ev.tipo] + k + 1) % roda.length; break;
116
+ }
117
+ if (!nome) continue;
118
+ const t = r3(Math.max(0, ev.t - (ANTES[nome] || 0)));
119
+ if (t > duracao - 0.1) continue;
120
+ if ([...fora, ...novos].some((e) => Math.abs(e.t - t) < 0.12)) continue;
121
+ novos.push({ t, src: sons[nome], volume: VOLUME[nome] || 0.4, som: nome });
122
+ ultimo = nome;
123
+ }
124
+ return { efeitos: [...fora, ...novos.map(({ som, ...e }) => e)].sort((a, b) => a.t - b.t), novos };
125
+ }
126
+
127
+ /** O plano pronto ganha os barulhinhos (o plano é mudado no lugar). Devolve quantos entraram. */
128
+ function sonorizarPlano(r, { tempos: tps = [], sons = {} } = {}) {
129
+ if (!r || !r.plano) return 0;
130
+ const plano = r.plano;
131
+ plano.audio = plano.audio || {};
132
+ const duracao = (plano.cenas || []).reduce((n, c) => n + (Number(c.duracao) || 0), 0) || Infinity;
133
+ const m = momentos({ tempos: tps, plano, partitura: r.partitura });
134
+ const s = sonorizar(plano.audio.efeitos || [], { ...m, sons, duracao });
135
+ plano.audio.efeitos = s.efeitos;
136
+ delete plano.audio.narracao;
137
+ return s.novos.length;
138
+ }
139
+
140
+ module.exports = { ativo, textoDaTela, duracaoDe, tempos, momentos, sonorizar, sonorizarPlano, PEDIDO };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.10.1",
3
+ "version": "10.11.0",
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/planos-tela.test.js && 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/conferencia.test.js && node test/cor.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/rolar.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/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.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/planos-tela.test.js && 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/conferencia.test.js && node test/cor.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/rolar.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/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -50,6 +50,7 @@ desenha é você, em React.
50
50
  `roteiro.estiloVisual` (humor, paleta, display, texto…). Um vídeo, uma
51
51
  identidade: 2 chãos, 1 acento, 2 fontes.
52
52
  3. `estudio_narrar` com o roteiro: os tempos MEDIDOS de cada fala.
53
+ Sem voz: pule; `semNarracao:true` e `duracao` por parte.
53
54
  4. Para cada parte, `write_file` com `path:"componentes/<nome>.jsx"`:
54
55
  `export default function Cena({ el, t, fps })`, `t` em segundos desde o
55
56
  começo da parte. Em `el.dados`: `titulo`, `fala`, `palavras` (cada uma com
@@ -61,6 +62,9 @@ desenha é você, em React.
61
62
  arquivo e mande o roteiro de novo.
62
63
  6. `estudio_render`. A voz, a trilha, os sons e o fundo são do Estúdio.
63
64
 
65
+ **Variedade:** `estudio_midia acao:"efeitos"` antes das cenas; adapte e use
66
+ como `componente`. Uma técnica nova por vídeo. Risco em 2+ linhas: traço por linha.
67
+
64
68
  Parte sem `componente` cai no gerativo (a tipografia anima sozinha): é a
65
69
  reserva, não o objetivo. Sem imagem é o padrão (foto só se pedirem).
66
70