primocode 10.10.0 → 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.
@@ -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/lib/tools.js CHANGED
@@ -594,6 +594,12 @@ async function despachar(name, args, ctx) {
594
594
  {
595
595
  switch (name) {
596
596
  case 'write_file': {
597
+ // O modelo grátis manda o caminho com outro nome (file_path, caminho) ou nenhum,
598
+ // e o write caía na própria pasta: EISDIR, e ele tentava de novo igual (14/10).
599
+ if (!args.path) args.path = args.file_path || args.filePath || args.caminho || args.arquivo || args.file || '';
600
+ if (!String(args.path).trim()) {
601
+ return { ok: false, error: 'faltou o caminho do arquivo em "path".', comoResolver: 'chame write_file de novo com path:"pasta/nome.ext" e content.' };
602
+ }
597
603
  // Vídeo e áudio NÃO se escrevem com write_file. Num pedido
598
604
  // real o agente criou um "video.mp4" de 43 bytes de texto e
599
605
  // disse "vídeo criado com sucesso" — um arquivo falso, morto,
@@ -610,6 +616,11 @@ async function despachar(name, args, ctx) {
610
616
  }
611
617
  const filePath = resolveInProject(args.path, projectDir);
612
618
  const content = args.content || '';
619
+ try {
620
+ if (fs.statSync(filePath).isDirectory()) {
621
+ return { ok: false, error: `"${args.path}" é uma PASTA, não um arquivo.`, comoResolver: `ponha o nome do arquivo no fim: path:"${String(args.path).replace(/\/+$/, '')}/index.html" (ou o nome que for).` };
622
+ }
623
+ } catch { /* não existe: vai ser criado */ }
613
624
  const created = !fs.existsSync(filePath);
614
625
  // Reescrever um arquivo que já existe é a operação mais
615
626
  // perigosa do agente: o conteúdo antigo some inteiro. Ler o
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.10.0",
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/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"
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"
@@ -22,59 +22,62 @@ Não faça nada disto, em nenhum vídeo:
22
22
 
23
23
  ## O que separa motion bom de slide animado
24
24
 
25
- **A câmera existe.** Zoom, pan e dolly são da CENA, não do elemento. Um zoom
26
- lento de 1.0 para 1.08 ao longo de uma cena dá vida a um quadro parado — e
27
- quase nenhum vídeo automático faz isso.
25
+ **A câmera existe.** Um zoom lento de 1.0 para 1.08 na cena inteira dá vida a
26
+ um quadro parado.
28
27
 
29
28
  **Nada entra do nada.** Um elemento entra vindo de algum lugar (de baixo, de
30
- trás, de fora do quadro), com `saida` ou `elastico` na suavização. Movimento
29
+ trás, de fora do quadro), com `spring` do Remotion ou easing de saída. Movimento
31
30
  linear é a assinatura da máquina: no mundo físico nada começa e para na mesma
32
31
  velocidade.
33
32
 
34
- **Profundidade.** Camadas que se movem em velocidades diferentes (parallax)
35
- custam duas linhas e mudam a cena inteira: fundo devagar, meio médio, frente
36
- rápido. Com `perspectiva` na cena, `rotacaoX`/`rotacaoY`/`z` viram 3D de
37
- verdade.
33
+ **Profundidade.** Parallax (fundo devagar, frente rápido); `perspective` com
34
+ `rotateX`/`translateZ`, ou three, para 3D de verdade.
38
35
 
39
- **Ritmo.** Cena de informação simples: 1,5 a 2,5s. Cena com frase para ler: 3
40
- a 4s. Fecho: 2s. Variar a duração é o que cria ritmo; repetir cria sono.
36
+ **Ritmo.** Informação simples 1,5–2,5 s; frase para ler 3–4 s; fecho 2 s.
41
37
 
42
38
  **Silêncio visual.** Um quadro com uma frase e mais nada vale mais que um com
43
39
  seis elementos. Espaço vazio é composição, não desperdício.
44
40
 
45
- ## Motion é contínuo, não uma tela por ideia
46
-
47
- Slide troca de tela. Motion é UM plano onde o mesmo objeto VIRA o próximo: a
48
- bolinha acende um fogo, se deforma em foguete, mergulha no celular e vira o
49
- ícone; o cursor gira e clica; o ícone salta e cobre a tela com o app dentro.
50
-
51
- Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
52
- `estudio_plano`, não uma cena por frase:
53
-
54
- - **`forma: "traco"`**: forma livre por `caminho` (`d` de SVG, caixa 0 0 100
55
- 100, M/L/C/Q/Z). `caminho` e `cor` animam:
56
- `"caminho": [{"t":1.6,"v":BOLA},{"t":2.5,"v":FOGUETE,"e":"elastico"}]`.
57
- - **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
58
- app nasce do tamanho e da COR do ícone.
59
-
60
- Sem imagem é o padrão (foto só se pedirem). Decida `clima`, o impacto (`destaque`, ≤1 em 3 partes), o
61
- respiro e o `estilo` deste roteiro: `gerativo`, `fluido`, `impacto`, `vox`, `marca`, `lancamento`; vazio, gira.
62
- `hibrido` só com imagem pedida. Leia os avisos da revisão.
41
+ ## O caminho: você escreve cada cena
42
+
43
+ Motion nesse nível não sai de molde: cada cena é UMA ideia desenhada para a
44
+ fala — a interface do produto montando, o número que cresce, o objeto em 3D,
45
+ o gráfico que mostra a ideia, a palavra gigante que se transforma. Quem
46
+ desenha é você, em React.
47
+
48
+ 1. `estudio_projeto` (criar). A marca tem site? `estudio_site` mede cores e fontes.
49
+ 2. `estudio_midia acao:"estilos"`: monte a identidade pelo assunto em
50
+ `roteiro.estiloVisual` (humor, paleta, display, texto…). Um vídeo, uma
51
+ identidade: 2 chãos, 1 acento, 2 fontes.
52
+ 3. `estudio_narrar` com o roteiro: os tempos MEDIDOS de cada fala.
53
+ Sem voz: pule; `semNarracao:true` e `duracao` por parte.
54
+ 4. Para cada parte, `write_file` com `path:"componentes/<nome>.jsx"`:
55
+ `export default function Cena({ el, t, fps })`, `t` em segundos desde o
56
+ começo da parte. Em `el.dados`: `titulo`, `fala`, `palavras` (cada uma com
57
+ o seu `t`, para entrar junto com a voz), `duracao`, `cores` {fundo, texto,
58
+ acento}, `fontes`, `largura`, `altura`. Imports só de react, remotion,
59
+ @remotion/*, three. Cor e fonte vêm de `el.dados`, nunca inventadas na cena.
60
+ 5. `estudio_plano` com o roteiro: `estiloVisual` e, em cada parte,
61
+ `componente:"componentes/<nome>.jsx"`. Voltou `cenasQuebradas`? Conserte o
62
+ arquivo e mande o roteiro de novo.
63
+ 6. `estudio_render`. A voz, a trilha, os sons e o fundo são do Estúdio.
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
+
68
+ Parte sem `componente` cai no gerativo (a tipografia anima sozinha): é a
69
+ reserva, não o objetivo. Sem imagem é o padrão (foto só se pedirem).
63
70
 
64
71
  ## Cor e tipo
65
72
 
66
- Escolha a paleta a partir do ASSUNTO, e escreva os valores. Três a cinco
67
- cores: uma dominante, uma de destaque, neutros com viés da dominante. Se a
68
- marca existe, leia o site dela com `estudio_site` e use as cores medidas —
69
- não as que você imagina que ela usa.
73
+ A paleta sai do ASSUNTO, no `estiloVisual` (um nome do cardápio ou 5 cores
74
+ suas). Se a marca existe, as cores medidas pelo `estudio_site` mandam — não
75
+ as que você imagina que ela usa.
70
76
 
71
- Duas fontes, no máximo: uma de display para o que grita, uma neutra para o
72
- que se lê. `estudio_midia acao:"fontes"` mostra um leque grande — sair do
73
- Inter/Montserrat é metade do trabalho de não parecer genérico.
77
+ Duas fontes, no máximo: display para o que grita, neutra para o que se lê.
74
78
 
75
79
  ## Antes de renderizar
76
80
 
77
- - Todo `src` aponta para arquivo que existe na pasta (baixe antes).
78
81
  - Os tempos das cenas batem com os tempos MEDIDOS da narração.
79
82
  - Nenhuma cena tem a mesma composição da anterior.
80
83
  - Leia o seu próprio plano e pergunte: em que este vídeo é diferente do