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.
@@ -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() {
@@ -67,7 +67,10 @@ function revisar(plano, { semImagem = false } = {}) {
67
67
  const efeitos = ((plano.audio && plano.audio.efeitos) || []).slice().sort((a, b) => (a.t || 0) - (b.t || 0));
68
68
  if (efeitos.length) {
69
69
  const fica = [];
70
- for (const e of efeitos) {
70
+ /* O micro (o tique, a escada de notas) não conta para o teto: é a
71
+ textura das referências, 2 a 3 por segundo, baixinho (14/10). */
72
+ const micros = efeitos.filter((e) => e.micro);
73
+ for (const e of efeitos.filter((x) => !x.micro)) {
71
74
  const ult = fica[fica.length - 1];
72
75
  if (ult && e.src === ult.src && (e.t || 0) - (ult.t || 0) < 0.3) {
73
76
  if ((e.volume ?? 1) > (ult.volume ?? 1)) fica[fica.length - 1] = e;
@@ -88,7 +91,7 @@ function revisar(plano, { semImagem = false } = {}) {
88
91
  final = guardados.sort((a, b) => (a.t || 0) - (b.t || 0));
89
92
  consertos.push(`${fica.length} efeitos sonoros em ${r2(total)}s eram ruído — ficaram os ${final.length} mais fortes`);
90
93
  }
91
- plano.audio.efeitos = final;
94
+ plano.audio.efeitos = [...final, ...micros].sort((a, b) => (a.t || 0) - (b.t || 0));
92
95
  }
93
96
 
94
97
  // 4. Texto que não dá tempo de ler.