primocode 10.10.1 → 10.11.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.10.1
1
+ # PrimoCode v10.11.1
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v10.10.1
12
+ ▐▛███▜▌ Primo Code v10.11.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -17,6 +17,14 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
17
17
  Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
18
18
  veja `/mascot` e `/name`.
19
19
 
20
+ ## O que mudou na 10.11
21
+
22
+ - **Barulhinhos:** cada palavra, item e ponto que entra no motion faz um som pequeno, numa escada de notas, com tique, clique de mouse, blip, swoosh e sub na revelação. A cena pode pedir o som no quadro certo (`sons: [{t, som}]`).
23
+ - **Vídeo sem narração:** `semNarracao: true` (ou "sem voz" no pedido) gera só motion, trilha e efeitos, e a trilha não abaixa.
24
+ - **Efeitos de fábrica:** peça do site em 3D com impacto, brilho que varre, cor que inunda, risco em várias linhas, entrada com física e raios de luz. Quando nenhuma cena é escrita, o plano devolve o cardápio de efeitos.
25
+ - **3D com peso:** o boneco antecipa, respira, pula amassando e anda com o quadril.
26
+ - **Risco:** texto riscado que quebra em várias linhas ganha um traço por linha.
27
+
20
28
  ## O que mudou na 10.4
21
29
 
22
30
  - **A tela do OpenCode.** O PrimoCode abre em tela cheia: a conversa rola em
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. Toda parte aceita sons:[{t, som}] (t na parte; pop, mouse, tique, digital, swoosh, sub, ponto1..ponto5) para o barulhinho cair no quadro em que a coisa acontece. '
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. '
@@ -17,6 +17,8 @@ const fs = require('fs');
17
17
  const os = require('os');
18
18
  const path = require('path');
19
19
 
20
+ const EASING = ['linear', 'ease', 'quad', 'cubic', 'poly', 'sin', 'circle', 'exp', 'elastic', 'back', 'bounce', 'bezier', 'in', 'out', 'inOut', 'step0', 'step1'];
21
+ const TROCA = { expo: 'exp', sine: 'sin', circ: 'circle', cubicBezier: 'bezier', easeIn: 'in', easeOut: 'out', easeInOut: 'inOut', quart: 'poly' };
20
22
  const PERMITIDOS = /^(react|remotion|three|@remotion\/[\w-]+|\.{1,2}\/.+)$/;
21
23
 
22
24
  let esbuild;
@@ -33,6 +35,10 @@ function conferirCena(arquivo, { compilar = compilador() } = {}) {
33
35
  if (!/export\s+default\b/.test(codigo)) return { ok: false, erro: 'falta o export default da função da cena ({el, t, fps}).' };
34
36
  const fora = [...codigo.matchAll(/\bfrom\s+['"]([^'"]+)['"]|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => m[1] || m[2]).filter((x) => !PERMITIDOS.test(x));
35
37
  if (fora.length) return { ok: false, erro: `import fora do permitido: ${[...new Set(fora)].join(', ')} (só react, remotion, @remotion/*, three).` };
38
+ /* Easing que não existe compila, mas mata o render inteiro (14/10, a
39
+ padaria: "Easing.expo" — easing is not a function, 3 renders perdidos). */
40
+ const easingErrado = [...new Set([...codigo.matchAll(/\bEasing\.(\w+)/g)].map((m) => m[1]))].filter((n) => !EASING.includes(n));
41
+ if (easingErrado.length) return { ok: false, erro: `Easing.${easingErrado[0]} não existe no Remotion${TROCA[easingErrado[0]] ? ` — use Easing.${TROCA[easingErrado[0]]}` : ''} (existem: ${EASING.join(', ')}).` };
36
42
  if (!compilar) return { ok: true, semCompilador: true };
37
43
  try {
38
44
  compilar.transformSync(codigo, { loader: /\.tsx$/i.test(arquivo) ? 'tsx' : 'jsx', jsx: 'automatic', logLevel: 'silent' });
@@ -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,17 @@ 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, cues: args.__semNarracao.cues, 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, entradas e micros`);
415
+ } else if (args.__barulhinhos && r.plano) {
416
+ // Com voz: os micros e os sons pedidos pela cena, baixinho por baixo da fala.
417
+ const n = semNarracao.sonorizarPlano(r, { tempos: args.__barulhinhos.tempos, cues: args.__barulhinhos.cues, sons: sonsLeves.preparar(p.pasta), comVoz: true });
418
+ if (n) projeto.anotar(p.id, 'plano', `barulhinhos: ${n} micros por baixo da fala`);
419
+ }
408
420
  const ic = await variarIcones(r.plano, original);
409
421
  if (ic.trocados) projeto.anotar(p.id, 'icones', `coleção ${ic.colecao}: ${ic.trocados} ícones`);
410
422
  let conferida = plano.conferir(r.plano, { pasta: p.pasta });
@@ -431,9 +443,18 @@ async function ferramentaPlano(args) {
431
443
  projeto.salvar(args.id, { estrutura: usada.estrutura || '-' });
432
444
  }
433
445
  const quebradas = args.__cenasQuebradas || [];
446
+ /* NENHUMA CENA ESCRITA (14/10, a padaria da 10.11.0 saiu toda no
447
+ gerativo): a regra morava só na skill, e regra escrita não é regra
448
+ obedecida. O plano volta com o cardápio na mão — a IA ainda decide. */
449
+ const pts = Array.isArray(original) ? original : (original && original.partes) || [];
450
+ const nenhuma = pts.length > 1 && !pts.some((pt) => pt && pt.componente) && !quebradas.length;
434
451
  return {
435
452
  ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
436
453
  ...(quebradas.length ? { cenasQuebradas: quebradas } : {}),
454
+ ...(nenhuma ? { nenhumaCenaEscrita: {
455
+ aviso: 'Todas as partes caíram no gerativo automático. Para motion de verdade, escreva ao menos uma cena (componente), partindo de um destes efeitos (estudio_midia usar-efeito) se servir, e mande o roteiro de novo. Cada parte pode pedir som: sons:[{t, som}] (pop, mouse, tique, digital, swoosh, sub, ponto1..ponto5). Se o automático é o que você quer, siga para o render.',
456
+ efeitos: efeitos.cardapio().slice(0, 6),
457
+ } } : {}),
437
458
  proximo: quebradas.length
438
459
  ? `${quebradas.length} cena(s) sua(s) não compilaram e ficaram no gerativo: conserte o arquivo (o erro está em cenasQuebradas) e mande o roteiro de novo antes do render.`
439
460
  : 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
@@ -536,7 +557,9 @@ async function ferramentaMidia(args) {
536
557
  }
537
558
 
538
559
  // O catálogo de efeitos que a IA escreveu (efeitos.js): guardar, listar, reusar.
539
- if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio() };
560
+ if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio(),
561
+ // 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.
562
+ 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
563
  if (acao === 'guardar-efeito' || acao === 'usar-efeito') {
541
564
  if (!p) return { ok: false, error: 'diga em qual projeto (id).' };
542
565
  const r = acao === 'guardar-efeito'
@@ -1320,7 +1343,8 @@ function importarReferencia(p, arquivo) {
1320
1343
 
1321
1344
  async function prepararRoteiro(p, args) {
1322
1345
  const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
1323
- if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
1346
+ // O mesmo args (não uma cópia): o __semNarracao e o __cenasQuebradas voltam para quem chamou.
1347
+ if (Array.isArray(roteiro)) { args.roteiro = { partes: roteiro }; return prepararRoteiro(p, args); }
1324
1348
  /* O que o modelo manda e o disco não tem vira problema de render: um
1325
1349
  "src" que é endereço da internet vira busca, e um arquivo que não
1326
1350
  existe na pasta sai. A logo da marca vale só se estiver lá. */
@@ -1380,6 +1404,10 @@ async function prepararRoteiro(p, args) {
1380
1404
  const semImagem = referencias.semImagem(ref);
1381
1405
  let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
1382
1406
  if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1407
+ /* SEM NARRAÇÃO (14/10, "só o motion e os efeitos sonoros, os barulhinhos"):
1408
+ roteiro.semNarracao ou o pedido. O cinema e o híbrido vivem da voz: caem no gerativo. */
1409
+ const semVoz = semNarracao.ativo(roteiro, pedidoTxt);
1410
+ if (semVoz && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1383
1411
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1384
1412
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1385
1413
  o agente tenha escolhido outro estilo pelo nome. */
@@ -1772,7 +1800,17 @@ async function prepararRoteiro(p, args) {
1772
1800
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${hib ? 'hibrido' : 'cinema'}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
1773
1801
  return r;
1774
1802
  }
1775
- if (modo === 'video') {
1803
+ if (modo === 'video' && semVoz) {
1804
+ /* Sem voz, sem tempo medido: cada parte dura o `duracao` dela (ou o que o
1805
+ texto da tela pede), e as palavras são repartidas na parte. Os sons
1806
+ leves entram depois do plano, em cada corte e entrada (sem-narracao.js). */
1807
+ tempos = semNarracao.tempos(partes, { falaDe: composicao.falaDe });
1808
+ args.__semNarracao = { tempos, cues: semNarracao.cuesDe(partes, tempos) };
1809
+ const total = tempos.length ? tempos[tempos.length - 1].ate : 0;
1810
+ projeto.anotar(p.id, 'narracao', `sem narração: ${Math.round(total * 10) / 10}s em ${partes.length} partes, só motion e efeitos sonoros`);
1811
+ sons = estilo === 'gerativo' ? {} : ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
1812
+ }
1813
+ if (modo === 'video' && !semVoz) {
1776
1814
  const lista = partes.map((pt, i) => ({ texto: composicao.falaDe(pt), registro: composicao.registroDe(pt, i, partes.length) }));
1777
1815
  const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
1778
1816
  fs.mkdirSync(path.dirname(arq), { recursive: true });
@@ -1845,6 +1883,7 @@ async function prepararRoteiro(p, args) {
1845
1883
  }
1846
1884
  }
1847
1885
  tempos = v.partes;
1886
+ args.__barulhinhos = { tempos, cues: semNarracao.cuesDe(partes, tempos) };
1848
1887
  narracao = path.relative(p.pasta, mp3).replace(/\\/g, '/');
1849
1888
  projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
1850
1889
  // O motion usa os efeitos leves; os estilos de corte, os cheios.
@@ -1852,6 +1891,9 @@ async function prepararRoteiro(p, args) {
1852
1891
  // O gerativo desenha os sons DEPOIS da partitura (um arquivo por evento, da própria trilha).
1853
1892
  sons = estilo === 'gerativo' ? {}
1854
1893
  : ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
1894
+ }
1895
+ // A trilha vale com e sem voz: o vídeo sem narração ainda tem música.
1896
+ if (modo === 'video') {
1855
1897
  // A trilha do motion: ende.app (CC BY 4.0, sem Content ID), clima tecnologia.
1856
1898
  if (ehMotion) {
1857
1899
  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]}>