primocode 10.5.0 → 10.6.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.
@@ -30,6 +30,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
30
30
  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
+ import { Mundo, Animal, Coisa, alturaDoBicho, useIcones } from './mundo.jsx';
33
34
 
34
35
  const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
35
36
  const lerp = (a, b, k) => a + (b - a) * k;
@@ -261,11 +262,121 @@ function RostoPixel({ p, est }) {
261
262
  );
262
263
  }
263
264
 
264
- function BracoBloco({ lado, g, cor, pele, t }) {
265
+
266
+ /* ── A CABEÇA FACETADA (o padrão desde a 2ª referência, 11/10) ────────────
267
+ "olha como quero o 3D": o rosto da referência é LOW-POLY — uma esfera de
268
+ poucas faces com sombreado chapado, nariz de pirâmide, sobrancelha grossa,
269
+ o cabelo cacheado feito de dezenas de pedrinhas facetadas. O corpo segue
270
+ de blocos. `cabeca: 'cubo'` volta ao rosto de pixel da 10.5. */
271
+ const GEO_FACETA = new Map();
272
+ function faceta(det) {
273
+ if (!GEO_FACETA.has(det)) GEO_FACETA.set(det, new THREE.IcosahedronGeometry(1, det));
274
+ return GEO_FACETA.get(det);
275
+ }
276
+ const Faceta = ({ p = [0, 0, 0], s = [1, 1, 1], rot, c, det = 1, rough = 0.75, sombra = true }) => (
277
+ <mesh position={p} scale={s} rotation={rot} geometry={faceta(det)} castShadow={sombra} receiveShadow>
278
+ <meshStandardMaterial color={c} roughness={rough} flatShading />
279
+ </mesh>
280
+ );
281
+
282
+ function CabeloFacetado({ estilo, cor }) {
283
+ const pedras = useMemo(() => {
284
+ if (estilo === 'careca') return [];
285
+ const out = [];
286
+ const n = estilo === 'cacheado' ? 46 : estilo === 'longo' ? 34 : estilo === 'coque' ? 22 : 24;
287
+ for (let i = 0; i < n; i++) {
288
+ // Espalhadas na calota de cima e de trás da cabeça (nunca na testa baixa nem no rosto).
289
+ const u = hash(i * 3.7 + 1), v = hash(i * 9.1 + 4);
290
+ const az = u * Math.PI * 2;
291
+ const el = estilo === 'longo' ? -0.9 + v * 2.2 : 0.15 + v * 1.25;
292
+ const atras = Math.cos(az) < 0.35 || el > 0.75;
293
+ if (!atras) continue;
294
+ const r = estilo === 'cacheado' ? 0.105 + hash(i) * 0.05 : 0.09 + hash(i) * 0.03;
295
+ out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, 0.04 + Math.sin(el) * 0.38, -Math.cos(az) * Math.cos(el) * 0.34 + (Math.cos(az) > 0 ? 0.0 : -0.02)], r, k: i });
296
+ }
297
+ if (estilo === 'coque') out.push({ p: [0, 0.36, -0.3], r: 0.16, k: 99 });
298
+ return out;
299
+ }, [estilo]);
300
+ // A franja: a frente do cabelo, um pouco acima da sobrancelha.
301
+ return (
302
+ <group>
303
+ {estilo !== 'careca' && <Faceta p={[0, 0.2, -0.02]} s={[0.36, 0.26, 0.36]} c={cor} det={1} rough={0.9} />}
304
+ {pedras.map((x) => <Faceta key={x.k} p={x.p} s={[x.r, x.r * 0.9, x.r]} rot={[hash(x.k) * 3, hash(x.k + 1) * 3, 0]} c={escurecer(cor, 0.85 + hash(x.k + 2) * 0.3)} det={0} rough={0.95} />)}
305
+ </group>
306
+ );
307
+ }
308
+
309
+ function CabecaFacetada({ p, est }) {
310
+ const e = EMO[est.emocao] || EMO.neutro;
311
+ const sobr = e.sobr + est.enfase * 0.4;
312
+ const [abre, larg] = est.boca;
313
+ const pisca = clamp(est.pisca);
314
+ const escuro = '#17110f';
315
+ const fz = 0.335; // a frente do rosto
316
+ return (
317
+ <group>
318
+ {/* o crânio e o queixo: dois volumes facetados */}
319
+ <Faceta s={[0.34, 0.39, 0.35]} c={p.pele} det={1} rough={0.7} />
320
+ <Faceta p={[0, -0.17, 0.05]} s={[0.26, 0.2, 0.27]} c={p.pele} det={1} rough={0.7} />
321
+ {[-1, 1].map((k) => <Faceta key={'o' + k} p={[k * 0.335, -0.01, -0.02]} s={[0.05, 0.08, 0.06]} c={escurecer(p.pele, 0.9)} det={0} />)}
322
+ {/* os olhos: amêndoa escura que fecha na piscada, e a sobrancelha grossa */}
323
+ {[-1, 1].map((k) => (
324
+ <group key={k} position={[k * 0.125, 0.04, fz - 0.03]} rotation={[0, k * 0.28, 0]}>
325
+ <mesh scale={[0.045, 0.05 * (1 - pisca * 0.88), 0.02]}><sphereGeometry args={[1, 8, 6]} /><meshStandardMaterial color={escuro} roughness={0.4} /></mesh>
326
+ <mesh position={[0.012, 0.015, 0.016]} scale={[0.012, 0.012, 0.006]}><sphereGeometry args={[1, 6, 4]} /><meshStandardMaterial color="#ffffff" roughness={0.3} /></mesh>
327
+ <mesh position={[0, 0.085 + sobr * 0.025, 0.012]} rotation={[0, 0, k * (est.emocao === 'serio' ? 0.25 : 0.05) - k * sobr * 0.06]}>
328
+ <boxGeometry args={[0.14, 0.035, 0.04]} /><meshStandardMaterial color={p.corCabelo} roughness={0.9} flatShading />
329
+ </mesh>
330
+ </group>
331
+ ))}
332
+ {/* o nariz: uma pirâmide de quatro faces */}
333
+ <mesh position={[0, -0.045, fz + 0.025]} rotation={[Math.PI / 2 - 0.25, Math.PI / 4, 0]} scale={[0.05, 0.11, 0.05]} castShadow>
334
+ <coneGeometry args={[1, 1, 4]} /><meshStandardMaterial color={escurecer(p.pele, 0.95)} roughness={0.7} flatShading />
335
+ </mesh>
336
+ {/* a boca: abre com a vogal e estica no "i" */}
337
+ <mesh position={[0, -0.17 - abre * 0.012, fz - 0.03]} scale={[0.05 + 0.04 * larg, 0.008 + 0.04 * abre, 0.02]}>
338
+ <sphereGeometry args={[1, 8, 6]} /><meshStandardMaterial color="#3a1416" roughness={0.6} />
339
+ </mesh>
340
+ {p.bigode && [-1, 1].map((k) => (
341
+ <mesh key={'b' + k} position={[k * 0.06, -0.115, fz + 0.005]} rotation={[0, 0, k * -0.35]} scale={[0.075, 0.03, 0.03]}>
342
+ <icosahedronGeometry args={[1, 0]} /><meshStandardMaterial color={p.corCabelo} roughness={0.9} flatShading />
343
+ </mesh>
344
+ ))}
345
+ {p.barba && <Faceta p={[0, -0.2, 0.08]} s={[0.27, 0.17, 0.26]} c={p.corCabelo} det={1} rough={0.95} />}
346
+ {p.oculos && (
347
+ <group position={[0, 0.04, fz + 0.02]}>
348
+ {[-1, 1].map((k) => <mesh key={k} position={[k * 0.125, 0, 0]}><torusGeometry args={[0.065, 0.012, 6, 12]} /><meshStandardMaterial color="#111114" roughness={0.4} /></mesh>)}
349
+ <mesh><boxGeometry args={[0.06, 0.012, 0.012]} /><meshStandardMaterial color="#111114" /></mesh>
350
+ </group>
351
+ )}
352
+ <CabeloFacetado estilo={p.cabelo} cor={p.corCabelo} />
353
+ </group>
354
+ );
355
+ }
356
+
357
+ /* A AÇÃO da fala mexe o braço por cima do gesto: acenar (a mão direita no
358
+ alto, abanando), apontar (o braço esticado para a frente), comemorar (os
359
+ dois para cima), pensar (a mão no queixo), dançar (os dois balançando). */
360
+ function poseDoBraco(acao, lado, t, k) {
361
+ if (!acao || k <= 0) return null;
362
+ const dir = lado > 0;
363
+ 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 };
364
+ if (acao === 'apontar' && dir) return { x: -1.45, z: 0.12, c: -0.05 };
365
+ if (acao === 'comemorar') return { x: -0.2, z: lado * (2.7 + Math.sin(t * 8 + lado) * 0.15), c: -0.25 };
366
+ if (acao === 'pensar' && dir) return { x: -1.5, z: -0.35, c: -2.1 };
367
+ 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 };
368
+ if (acao === 'correr') return { x: Math.sin(t * 13 + (lado > 0 ? 0 : Math.PI)) * 0.9, z: lado * 0.1, c: -1.3 };
369
+ if (acao === 'andar') return { x: Math.sin(t * 7 + (lado > 0 ? 0 : Math.PI)) * 0.45, z: lado * 0.06, c: -0.25 };
370
+ return null;
371
+ }
372
+
373
+ function BracoBloco({ lado, g, cor, pele, t, acao, k = 1 }) {
265
374
  // Repouso: o braço caído, um tico para fora. Gesto: o antebraço sobe e a mão mexe.
266
- const ombroX = lerp(0.08, -0.85, g) + Math.sin(t * 6 + lado) * 0.06 * g;
267
- const ombroZ = lado * lerp(0.06, 0.22, g);
268
- const cotovelo = lerp(-0.12, -1.05, g) + Math.sin(t * 7.3 + lado * 2) * 0.14 * g;
375
+ let ombroX = lerp(0.08, -0.85, g) + Math.sin(t * 6 + lado) * 0.06 * g;
376
+ let ombroZ = lado * lerp(0.06, 0.22, g);
377
+ let cotovelo = lerp(-0.12, -1.05, g) + Math.sin(t * 7.3 + lado * 2) * 0.14 * g;
378
+ const pose = poseDoBraco(acao, lado, t, k);
379
+ if (pose) { ombroX = lerp(ombroX, pose.x, k); ombroZ = lerp(ombroZ, pose.z, k); cotovelo = lerp(cotovelo, pose.c, k); }
269
380
  return (
270
381
  <group position={[lado * 0.47, 1.4, 0]} rotation={[ombroX, 0, ombroZ]}>
271
382
  <Bloco s={[0.2, 0.36, 0.2]} p={[0, -0.17, 0]} c={cor} r={0.04} rough={0.8} />
@@ -281,54 +392,92 @@ function PessoaBloco({ p, est }) {
281
392
  const resp = Math.sin(est.t * 1.7) * 0.008;
282
393
  const [cy, cp, cr] = est.cabeca;
283
394
  const calca = escurecer(p.roupa, 0.35);
395
+ const a = est.acao, ka = est.forcaAcao || 0;
396
+ // O corpo inteiro: pular e comemorar sobem, dançar balança, andar e correr marcham.
397
+ const pulo = (a === 'pular' || a === 'comemorar') ? Math.abs(Math.sin(est.t * 6.5)) * 0.32 * ka : 0;
398
+ const balanco = a === 'dancar' ? Math.sin(est.t * 6) * 0.14 * ka : 0;
399
+ const marcha = (a === 'andar' || a === 'correr') ? (a === 'correr' ? 13 : 7) : 0;
284
400
  return (
285
- <group>
401
+ <group position={[0, pulo, 0]} rotation={[0, 0, balanco]}>
286
402
  {/* as pernas e o sapato */}
287
- {[-1, 1].map((k) => (
288
- <group key={k}>
289
- <Bloco s={[0.25, 0.72, 0.27]} p={[k * 0.15, 0.4, 0]} c={calca} r={0.03} rough={0.85} />
290
- <Bloco s={[0.27, 0.1, 0.36]} p={[k * 0.15, 0.05, 0.04]} c="#1a1a1f" r={0.03} />
291
- </group>
292
- ))}
403
+ {[-1, 1].map((k) => {
404
+ const passo = marcha ? Math.sin(est.t * marcha + (k > 0 ? 0 : Math.PI)) * (a === 'correr' ? 0.7 : 0.4) * ka : 0;
405
+ return (
406
+ <group key={k} position={[k * 0.15, 0.76, 0]} rotation={[passo, 0, 0]}>
407
+ <Bloco s={[0.25, 0.72, 0.27]} p={[0, -0.36, 0]} c={calca} r={0.03} rough={0.85} />
408
+ <Bloco s={[0.27, 0.1, 0.36]} p={[0, -0.71, 0.04]} c="#1a1a1f" r={0.03} />
409
+ </group>
410
+ );
411
+ })}
293
412
  <group position={[0, resp, 0]}>
294
413
  {/* o tronco, com a faixa branca da camisa da referência */}
295
414
  <Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
296
415
  <Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
297
416
  <Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
298
417
  </group>
299
- <BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} />
300
- <BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} />
418
+ <BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
419
+ <BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
301
420
  <group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
302
- <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
303
- <RostoPixel p={p} est={est} />
304
- <Cabelo estilo={p.cabelo} cor={p.corCabelo} />
421
+ {p.cabeca === 'cubo' ? (
422
+ <>
423
+ <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
424
+ <RostoPixel p={p} est={est} />
425
+ <Cabelo estilo={p.cabelo} cor={p.corCabelo} />
426
+ </>
427
+ ) : <CabecaFacetada p={p} est={est} />}
305
428
  </group>
306
429
  </group>
307
430
  );
308
431
  }
309
432
 
310
433
  /* ── O MASCOTE (quem é IA) ──────────────────────────────────────────────
311
- O corpo laranja de quina mole, dois olhos em fenda, os bracinhos de lado e
312
- quatro perninhas. Não tem boca: ele fala com o CORPO — estica para cima na
313
- vogal aberta e encolhe no "m" —, e flutua devagar. */
434
+ O da 2ª referência (11/10): um CUBO laranja de quina viva, dois olhos
435
+ quadrados pretos, uma orelha de bloco de cada lado e uma fileira de
436
+ perninhas, em pé no chão. Não tem boca: ele fala com o CORPO — estica para
437
+ cima na vogal aberta, encolhe no "m" — e pisca fechando o olho em traço.
438
+ `bigode: true` põe o bigode da referência. */
314
439
  function Mascote({ p, est }) {
315
- const cor = p.cor || '#d97757';
440
+ const cor = p.cor || '#d9532b';
316
441
  const [abre] = est.boca;
317
- const bob = Math.sin(est.t * 1.9 + 0.7) * 0.05;
318
- const estica = 1 + abre * 0.1, achata = 1 - abre * 0.05;
442
+ const pulo = (est.acao === 'pular' || est.acao === 'comemorar') ? Math.abs(Math.sin(est.t * 6)) * 0.25 * (est.forcaAcao || 0) : 0;
443
+ const respira = Math.sin(est.t * 1.9 + 0.7) * 0.012;
444
+ const estica = 1 + abre * 0.07 + respira, achata = 1 - abre * 0.035;
319
445
  const pisca = clamp(est.pisca);
320
446
  const [cy, cp, cr] = est.cabeca;
321
- const pe = (x, z, k) => <Bloco key={k} s={[0.1, 0.22, 0.1]} p={[x, -0.33 + Math.sin(est.t * 5 + k) * 0.02, z]} c={escurecer(cor, 0.92)} r={0.025} />;
447
+ const W = 1.0, H = 0.92, D = 0.78, perna = 0.46;
448
+ const pernas = [-0.38, -0.19, 0, 0.19, 0.38];
322
449
  return (
323
- <group position={[0, 1.55 + bob, 0]} rotation={[cp * 0.6, cy, cr + Math.sin(est.t * 1.3) * 0.03]}>
324
- <group scale={[achata, estica, achata]}>
325
- <Bloco s={[0.82, 0.5, 0.52]} c={cor} r={0.07} rough={0.55} />
326
- {[-1, 1].map((k) => (
327
- <Bloco key={k} s={[0.14, 0.16, 0.22]} p={[k * 0.47, 0.02 + (est.gesto[k > 0 ? 1 : 0] * 0.08), 0]} c={cor} r={0.04} rough={0.55} />
328
- ))}
329
- {[-1, 1].map((k) => <Chapa key={'o' + k} s={[0.065, 0.15 * (1 - pisca * 0.85), 0.012]} p={[k * 0.17, 0.05, 0.264]} c="#141016" />)}
450
+ <group position={[0, pulo, 0]} rotation={[0, cy * 0.8, cr * 0.5]}>
451
+ {pernas.map((x, i) => (
452
+ <group key={i}>
453
+ <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} />
454
+ {i % 2 === 0 && <Bloco s={[0.13, perna, 0.13]} p={[x, perna / 2, -D / 2 + 0.12]} c={escurecer(cor, 0.8)} r={0.015} rough={0.6} />}
455
+ </group>
456
+ ))}
457
+ <group position={[0, perna, 0]} rotation={[cp * 0.5, 0, 0]}>
458
+ <group scale={[achata, estica, achata]}>
459
+ <Bloco s={[W, H, D]} p={[0, H / 2, 0]} c={cor} r={0.025} rough={0.55} />
460
+ {[-1, 1].map((k) => (
461
+ <Bloco key={k} s={[0.16, 0.34, 0.3]} p={[k * (W / 2 + 0.08), H * 0.5 + (est.gesto[k > 0 ? 1 : 0] * 0.08), 0]} c={escurecer(cor, 0.93)} r={0.015} rough={0.55} />
462
+ ))}
463
+ {[-1, 1].map((k) => (
464
+ <Chapa key={'o' + k} s={[0.13, pisca > 0.5 ? 0.025 : 0.155, 0.012]} p={[k * 0.23, H * 0.66, D / 2 + 0.006]} c="#0d0a0b" />
465
+ ))}
466
+ {/* o bigode da referência: a barra grossa no meio e as pontas que sobem em curva */}
467
+ {p.bigode && (
468
+ <group position={[0, H * 0.43, D / 2 + 0.012]}>
469
+ {/* grosso no meio (dois gomos), afinando e subindo nas pontas */}
470
+ {[-1, 1].map((k) => (
471
+ <group key={k}>
472
+ <Chapa s={[0.2, 0.14, 0.024]} p={[k * 0.09, -0.005, 0]} rot={[0, 0, k * 0.12]} c="#0d0a0b" />
473
+ <Chapa s={[0.14, 0.1, 0.024]} p={[k * 0.235, 0.012, 0]} rot={[0, 0, k * 0.38]} c="#0d0a0b" />
474
+ <Chapa s={[0.08, 0.07, 0.024]} p={[k * 0.32, 0.07, 0]} rot={[0, 0, k * 0.95]} c="#0d0a0b" />
475
+ </group>
476
+ ))}
477
+ </group>
478
+ )}
479
+ </group>
330
480
  </group>
331
- {pe(-0.27, 0.13, 1)}{pe(-0.1, 0.13, 2)}{pe(0.1, 0.13, 3)}{pe(0.27, 0.13, 4)}
332
481
  </group>
333
482
  );
334
483
  }
@@ -426,12 +575,37 @@ function ChaoDeLadrilhos({ c }) {
426
575
  }
427
576
 
428
577
  /* ── A CÂMERA ──────────────────────────────────────────────────────────── */
578
+ /* O LUGAR de cada um: um sozinho no meio; dois frente a frente; três e
579
+ quatro num arco aberto para a câmera, todos virados para o centro. Bicho
580
+ pequeno fica um passo à frente, para não sumir atrás de quem é alto. */
581
+ const ARCO = { 1: [[0, 0]], 2: [[-0.78, 0], [0.78, 0]], 3: [[-1.35, 0.1], [0, -0.35], [1.35, 0.1]], 4: [[-1.9, 0.25], [-0.65, -0.25], [0.65, -0.25], [1.9, 0.25]] };
429
582
  function lugarDe(el, i) {
430
- const dois = el.length > 1;
431
- return dois ? [(el[i].lado || 0) * 0.78, 0, 0] : [0, 0, 0];
583
+ const n = Math.min(4, el.length) || 1;
584
+ const [x, z] = (ARCO[n] || ARCO[1])[Math.min(i, n - 1)];
585
+ const baixo = el[i] && (el[i].tipo === 'animal' || el[i].tipo === 'coisa') && alturaDaCabeca(el[i]) < 1.2;
586
+ return [x, 0, z + (baixo && n > 1 ? 0.35 : 0)];
587
+ }
588
+ function giroDe(el, i) {
589
+ const n = el.length;
590
+ if (n < 2) return 0;
591
+ const [x] = lugarDe(el, i);
592
+ return clamp(-x * 0.5, -0.45, 0.45);
593
+ }
594
+ function alturaDaCabeca(p) {
595
+ if (!p) return 1.8;
596
+ if (p.tipo === 'robo') return 0.46 + 0.62; // o meio dos olhos do cubo
597
+ if (p.tipo === 'animal') return alturaDoBicho(p.especie);
598
+ if (p.tipo === 'coisa') return 1.1;
599
+ return 1.8;
600
+ }
601
+ /* Os figurantes ficam nas pontas do arco, um pouco atrás: estão na cena, mas não roubam o plano. */
602
+ function lugarDoFigurante(el, fig, j) {
603
+ const n = Math.max(1, el.length);
604
+ const borda = n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
605
+ const lado = j % 2 ? 1 : -1;
606
+ const baixo = fig.tipo === 'animal' || fig.tipo === 'coisa';
607
+ return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
432
608
  }
433
- function giroDe(el, i) { return el.length > 1 ? (el[i].lado < 0 ? 0.42 : -0.42) : 0; }
434
- function alturaDaCabeca(p) { return p && p.tipo === 'robo' ? 1.55 : 1.8; }
435
609
 
436
610
  function cameraDe(pl, t, part, aspecto) {
437
611
  const vert = aspecto < 1;
@@ -443,14 +617,22 @@ function cameraDe(pl, t, part, aspecto) {
443
617
  const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
444
618
  const fovK = vert ? 1.45 : 1;
445
619
  let pos, alvo, fov;
620
+ // Bicho e coisa falam DE FRENTE: por cima do ombro, o focinho ficava de perfil.
621
+ if (pl.tipo === 'ombro' && el[quem] && (el[quem].tipo === 'animal' || el[quem].tipo === 'coisa')) pl = { ...pl, tipo: 'medio' };
446
622
  if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
447
623
  // O plano aberto de cima, como o da referência: o set inteiro e a parede de telas.
448
624
  fov = (pl.tipo === 'largo' ? 38 : 34) * fovK;
449
- const dist = pl.tipo === 'largo' ? (vert ? 8.2 : 6.6) : (vert ? 6.6 : 5.2);
450
- pos = [pl.lado * 0.5, pl.tipo === 'largo' ? 3.1 : 2.5, dist];
451
- alvo = [0, 1.45, -0.6];
625
+ // Mais gente no set, a câmera recua; elenco baixo (bichos), ela desce.
626
+ const largura = el.length > 2 ? 1 + (el.length - 2) * (vert ? 0.42 : 0.28) : 1;
627
+ const dist = (pl.tipo === 'largo' ? (vert ? 8.2 : 6.6) : (vert ? 6.6 : 5.2)) * largura;
628
+ const alturaMedia = el.length ? el.reduce((s, p) => s + alturaDaCabeca(p), 0) / el.length : 1.8;
629
+ pos = [pl.lado * (el.length > 2 ? 0.2 : 0.5), (pl.tipo === 'largo' ? 3.1 : 2.5) * (0.55 + 0.45 * alturaMedia / 1.8), dist];
630
+ alvo = [0, Math.max(0.6, alturaMedia - 0.35), -0.6];
452
631
  } else if (pl.tipo === 'ombro' && dois) {
453
- const o = 1 - quem, h = cabeca(o), s = cabeca(quem);
632
+ // Por cima do ombro de quem está MAIS PERTO de quem fala (com 3 ou 4 no set).
633
+ let o = quem === 0 ? 1 : 0;
634
+ for (let j = 0; j < el.length; j++) if (j !== quem && Math.abs(lugarDe(el, j)[0] - lugarDe(el, quem)[0]) < Math.abs(lugarDe(el, o)[0] - lugarDe(el, quem)[0])) o = j;
635
+ const h = cabeca(o), s = cabeca(quem);
454
636
  const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
455
637
  pos = [h[0] + (d[0] / n) * 0.9, h[1] + 0.25, h[2] + 1.6];
456
638
  alvo = [s[0], s[1] - 0.1, s[2]]; fov = 32 * fovK;
@@ -466,6 +648,8 @@ function cameraDe(pl, t, part, aspecto) {
466
648
  pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
467
649
  alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
468
650
  }
651
+ // No vertical a legenda está no alto: a câmera mira acima, e o rosto desce no quadro.
652
+ if (vert) alvo = [alvo[0], alvo[1] + (pl.tipo === 'aberto' || pl.tipo === 'largo' ? 0.45 : pl.tipo === 'medio' ? 0.3 : pl.tipo === 'ombro' ? 0.22 : 0.17), alvo[2]];
469
653
  const m = suave(k);
470
654
  if (pl.mov === 'entra' || pl.mov === 'sai') {
471
655
  const s = pl.mov === 'entra' ? m : 1 - m;
@@ -490,18 +674,39 @@ function cameraDe(pl, t, part, aspecto) {
490
674
  /* ── POR CIMA: a legenda da referência ─────────────────────────────────── */
491
675
  const FONTE = "'Inter','SF Pro Display','Helvetica Neue',Arial,sans-serif";
492
676
 
677
+ // Palavra que não fecha frase: o pedaço da legenda não termina nela ("uma fogueira no").
678
+ const FRACA = /^(no|na|nos|nas|de|do|da|dos|das|o|a|os|as|e|que|um|uma|com|em|pra|pro|por|para|ao|se|mais|muito|meu|minha|seu|sua)$/i;
493
679
  function pedacos(palavras) {
494
680
  const out = [];
495
681
  let cur = [];
496
682
  for (const w of palavras) {
497
683
  cur.push(w);
498
684
  const letras = cur.reduce((n, x) => n + String(x.t).length + 1, 0);
499
- if (letras > 30 || /[.!?;:]$/.test(w.t) || (cur.length >= 4 && /,$/.test(w.t))) { out.push(cur); cur = []; }
685
+ if (letras > 30 || /[.!?;:]$/.test(w.t) || (cur.length >= 4 && /,$/.test(w.t))) {
686
+ // Terminou numa palavra fraca: ela desce para o próximo pedaço.
687
+ if (cur.length > 2 && FRACA.test(String(w.t).replace(/[^\p{L}]/gu, '')) && !/[.!?;:,]$/.test(w.t)) { out.push(cur.slice(0, -1)); cur = [w]; continue; }
688
+ out.push(cur); cur = [];
689
+ }
500
690
  }
501
691
  if (cur.length) out.push(cur);
502
692
  return out;
503
693
  }
504
694
 
695
+ /* O LAYOUT DO QUE VAI POR CIMA (11/10, "nada fora do lugar nem sobrepondo"):
696
+ - a legenda mora no ALTO no vertical (25% da altura, como na 2ª
697
+ referência) e embaixo no horizontal (80%); a câmera do vertical mira
698
+ acima do rosto (cameraDe), então o rosto fica do meio para baixo e a
699
+ legenda nunca passa por cima dele;
700
+ - cabe em no máximo DUAS linhas na largura útil (84%): a fonte encolhe para
701
+ o pedaço mais longo, em vez de quebrar em três ou vazar da tela;
702
+ - o cartão de destaque fica no alto (12% / 8%), numa linha só, e também
703
+ encolhe para caber — os dois nunca dividem a mesma faixa. */
704
+ const LARGURA_LETRA = 0.54; // a largura média de uma letra da Inter, em "em"
705
+ function tamanhoQueCabe(texto, base, largura, linhas) {
706
+ const porLinha = Math.ceil(String(texto).length / linhas) + 2;
707
+ return Math.max(Math.round(base * 0.6), Math.min(base, Math.floor(largura / (porLinha * LARGURA_LETRA))));
708
+ }
709
+
505
710
  function Legenda({ part, t, W, H }) {
506
711
  const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.35);
507
712
  if (!f) return null;
@@ -509,12 +714,14 @@ function Legenda({ part, t, W, H }) {
509
714
  const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.2) || grupos[grupos.length - 1];
510
715
  const entra = suave((t - (g[0].de - 0.08)) / 0.18);
511
716
  const sai = 1 - suave((t - (f.ate + 0.15)) / 0.2);
512
- const tam = Math.round(H > W ? W * 0.052 : H * 0.05);
717
+ const vertical = H > W;
718
+ const util = W * 0.84;
513
719
  const texto = g.map((w) => w.t).join(' ');
720
+ const tam = tamanhoQueCabe(texto, Math.round(vertical ? W * 0.05 : H * 0.05), util, 2);
514
721
  return (
515
- <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.36 : 0.74), display: 'flex', justifyContent: 'center', padding: `0 ${W * 0.1}px` }}>
516
- <div style={{ fontFamily: FONTE, fontWeight: 600, fontSize: tam, lineHeight: 1.2, color: '#ffffff', textAlign: 'center', letterSpacing: '-0.005em',
517
- opacity: entra * sai, transform: `translateY(${(1 - entra) * tam * 0.15}px)`,
722
+ <div style={{ position: 'absolute', left: 0, right: 0, top: H * (vertical ? 0.25 : 0.8), display: 'flex', justifyContent: 'center' }}>
723
+ <div style={{ maxWidth: util, fontFamily: FONTE, fontWeight: 600, fontSize: tam, lineHeight: 1.18, color: '#ffffff', textAlign: 'center', letterSpacing: '-0.005em',
724
+ overflowWrap: 'break-word', textWrap: 'balance', opacity: entra * sai, transform: `translateY(${(1 - entra) * tam * 0.15}px)`,
518
725
  textShadow: '0 2px 12px rgba(0,0,0,0.55), 0 1px 2px rgba(0,0,0,0.6)' }}>{texto}</div>
519
726
  </div>
520
727
  );
@@ -526,10 +733,11 @@ function Destaque({ part, t, W, H, acento }) {
526
733
  const k = t - (f.de + 0.3);
527
734
  const fim = Math.min(f.ate + 0.3, f.de + 3) - (f.de + 0.3);
528
735
  const s = suave(k / 0.3) * (1 - suave((k - fim + 0.3) / 0.3));
529
- const tam = Math.round(Math.min(W, H) * 0.045);
736
+ const util = W * 0.86;
737
+ const tam = tamanhoQueCabe(f.destaque, Math.round(Math.min(W, H) * 0.045), util - Math.min(W, H) * 0.07, 1);
530
738
  return (
531
739
  <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.12 : 0.08), display: 'flex', justifyContent: 'center' }}>
532
- <div style={{ opacity: s, transform: `translateY(${(1 - s) * -12}px)`, background: 'rgba(12,14,32,0.72)', border: `2px solid ${acento}`, borderRadius: tam * 0.5,
740
+ <div style={{ maxWidth: util, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', opacity: s, transform: `translateY(${(1 - s) * -12}px)`, background: 'rgba(12,14,32,0.72)', border: `2px solid ${acento}`, borderRadius: tam * 0.5,
533
741
  padding: `${tam * 0.3}px ${tam * 0.7}px`, fontFamily: FONTE, fontWeight: 700, fontSize: tam, color: '#fff', boxShadow: `0 0 ${tam * 0.8}px ${acento}66` }}>{f.destaque}</div>
534
742
  </div>
535
743
  );
@@ -553,23 +761,50 @@ export function VoxelCena({ el, t }) {
553
761
  for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
554
762
  return e;
555
763
  };
556
- const estado = (i) => {
557
- const fala = falandoAgora(i);
558
- const outro = dois && falandoAgora(1 - i);
559
- const enfase = gesto(falas, i, t);
764
+ // A ação da fala vale da voz até 0,6 s depois, entrando e saindo suave.
765
+ const acaoDe = (i) => {
766
+ for (const f of falas) {
767
+ if (f.quem !== i || !f.acao || f.acao === 'falar') continue;
768
+ 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))];
769
+ }
770
+ return [null, 0];
771
+ };
772
+ const quemFala = () => { const f = falas.find((x) => t >= x.de && t <= x.ate); return f ? f.quem : -1; };
773
+ const estado = (i, figurante = false) => {
774
+ const fala = !figurante && falandoAgora(i);
775
+ const qf = quemFala();
776
+ const outro = qf >= 0 && qf !== i;
777
+ const enfase = figurante ? [0, 0] : gesto(falas, i, t);
560
778
  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);
561
- const emo = emocaoDe(i);
779
+ const emo = figurante ? 'feliz' : emocaoDe(i);
562
780
  const e = EMO[emo] || EMO.neutro;
563
781
  const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.6;
564
782
  // Quem ouve vira um pouco para quem fala; quem fala olha para a câmera de vez em quando.
565
- const paraOOutro = dois ? (elenco[i].lado < 0 ? 0.25 : -0.25) : 0;
783
+ const dx = outro && !figurante ? lugarDe(elenco, qf)[0] - lugarDe(elenco, i)[0] : 0;
784
+ const paraQuemFala = clamp(dx * 0.35, -0.35, 0.35);
785
+ const [acao, forcaAcao] = figurante ? [null, 0] : acaoDe(i);
566
786
  return {
567
- t, falando: fala, emocao: emo, boca: boca(falas, i, t), pisca: piscada(i, t),
568
- gesto: enfase, enfase: Math.max(enfase[0], enfase[1]),
569
- olhar: [micro + (outro ? (elenco[i].lado < 0 ? 0.8 : -0.8) : 0), 0.1],
570
- cabeca: [Math.sin(t * 0.7 + i * 2) * 0.07 + (outro ? paraOOutro : 0), nod, (e.inclina || 0) * (i ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
787
+ t, falando: fala, emocao: emo, boca: figurante ? [0, 0.55] : boca(falas, i, t), pisca: piscada(i + (figurante ? 5 : 0), t),
788
+ gesto: enfase, enfase: Math.max(enfase[0], enfase[1]), acao, forcaAcao,
789
+ olhar: [micro + (outro ? Math.sign(dx) * 0.8 : 0), 0.1],
790
+ 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],
571
791
  };
572
792
  };
793
+ const desenhar = (p, est) => (p.tipo === 'robo' ? <Mascote p={p} est={est} />
794
+ : p.tipo === 'animal' ? <Animal p={p} est={est} />
795
+ : p.tipo === 'coisa' ? <Coisa p={p} est={est} icones={icones} />
796
+ : <PessoaBloco p={p} est={est} />);
797
+ /* O MUNDO da hora: o da fala que está (ou acabou de estar) no ar. A troca
798
+ acontece no começo da fala — junto com o corte de câmera. */
799
+ const mundos = part.mundos || [];
800
+ let iMundo = 0;
801
+ for (const f of falas) if (f.de - 0.1 <= t && f.mundo != null) iMundo = f.mundo;
802
+ const mundo = mundos[iMundo];
803
+ const ehTelas = !mundo || mundo.telas;
804
+ const figurantes = part.figurantes || [];
805
+ // Todo PNG que vira bloco (peças fora do catálogo e coisas que falam), lido antes do quadro.
806
+ const srcs = useMemo(() => [...new Set([...mundos.flatMap((m) => m.props.map((p) => p.src)), ...elenco.map((e) => e.src), ...figurantes.map((e) => e.src)].filter(Boolean))].sort(), [part]);
807
+ const icones = useIcones(srcs);
573
808
  const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);
574
809
 
575
810
  return (
@@ -579,24 +814,34 @@ export function VoxelCena({ el, t }) {
579
814
  gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.0 }}>
580
815
  <Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
581
816
  <Ambiente />
582
- <color attach="background" args={[c.parede]} />
583
- <fog attach="fog" args={[c.parede, 9, 20]} />
584
- <hemisphereLight args={[c.recorte, escurecer(c.chao, 0.4), 0.55]} />
585
- {/* a luz principal, quente, de cima e da frente: o que dá volume aos blocos */}
586
- <spotLight position={[2.2, 5.2, 4.4]} angle={0.5} penumbra={0.8} intensity={120} distance={22} decay={2} color={c.luz} castShadow
587
- shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0003} shadow-radius={4} />
588
- {/* o recorte roxo por trás, que separa o boneco do fundo */}
589
- <spotLight position={[-2.5, 3.6, -2.4]} angle={0.7} penumbra={1} intensity={60} distance={14} decay={2} color={c.recorte} />
590
- <pointLight position={[0, 2, -2.6]} intensity={16} distance={8} color="#6f7dff" />
591
- <directionalLight position={[-3, 2, 4]} intensity={0.35} color="#c9d4ff" />
592
- <ChaoDeLadrilhos c={c} />
593
- <ParedeDeTelas c={c} paineis={part.paineis} t={t} />
817
+ {ehTelas ? (
818
+ <>
819
+ <color attach="background" args={[c.parede]} />
820
+ <fog attach="fog" args={[c.parede, 9, 20]} />
821
+ <hemisphereLight args={[c.recorte, escurecer(c.chao, 0.4), 0.55]} />
822
+ {/* a luz principal, quente, de cima e da frente: o que dá volume aos blocos */}
823
+ <spotLight position={[2.2, 5.2, 4.4]} angle={0.5} penumbra={0.8} intensity={120} distance={22} decay={2} color={c.luz} castShadow
824
+ shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0003} shadow-radius={4} />
825
+ {/* o recorte roxo por trás, que separa o boneco do fundo */}
826
+ <spotLight position={[-2.5, 3.6, -2.4]} angle={0.7} penumbra={1} intensity={60} distance={14} decay={2} color={c.recorte} />
827
+ <pointLight position={[0, 2, -2.6]} intensity={16} distance={8} color="#6f7dff" />
828
+ <directionalLight position={[-3, 2, 4]} intensity={0.35} color="#c9d4ff" />
829
+ <ChaoDeLadrilhos c={c} />
830
+ <ParedeDeTelas c={c} paineis={part.paineis} t={t} />
831
+ </>
832
+ ) : <Mundo key={iMundo} m={mundo} t={t} icones={icones} paineis={part.paineis} />}
594
833
  {elenco.map((p, i) => (
595
834
  <group key={i} position={lugarDe(elenco, i)} rotation={[0, giroDe(elenco, i), 0]}>
596
- {p.tipo === 'robo' ? <Mascote p={p} est={estado(i)} /> : <PessoaBloco p={p} est={estado(i)} />}
835
+ {desenhar(p, estado(i))}
836
+ </group>
837
+ ))}
838
+ {figurantes.map((p, j) => (
839
+ <group key={'f' + j} position={lugarDoFigurante(elenco, p, j)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
840
+ {desenhar(p, estado(10 + j, true))}
597
841
  </group>
598
842
  ))}
599
- <Lente W={W} H={H} foco={cam.foco} abertura={cam.abertura} desfoqueMax={cam.desfoqueMax} brilho={0.5} />
843
+ {/* Fora do set de telas o céu é claro: o brilho cai, senão o céu inteiro estoura. */}
844
+ <Lente W={W} H={H} foco={cam.foco} abertura={cam.abertura} desfoqueMax={cam.desfoqueMax} brilho={ehTelas ? 0.5 : mundo.cobertura ? 0.5 : mundo.estrelas || mundo.hora === 'noite' ? 0.45 : 0.18} />
600
845
  </ThreeCanvas>
601
846
  <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, rgba(0,0,0,0) 60%, rgba(0,0,0,0.45) 100%)' }} />
602
847
  <Destaque part={part} t={t} W={W} H={H} acento={c.acento} />
@@ -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', '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', 'index.jsx'];
71
71
 
72
72
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
73
73
  function arquivos() {
@@ -87,11 +87,25 @@ async function narrar(lista, mp3, { voz, uv, espeak = acharEspeak(), executar =
87
87
  }
88
88
  }
89
89
 
90
- /** A ordem das vozes: pedida → Kokoro e depois o Edge; senão Gemini → Kokoro → Edge.
91
- `motorVoz: 'edge'` no roteiro pula as outras. */
90
+ /** A ordem das vozes: pedida → Kokoro e depois o Edge; senão Gemini → Edge → Kokoro.
91
+ O Edge (vozes neurais da Microsoft, ou a ElevenLabs quando há chave) vem
92
+ ANTES do Kokoro desde 11/10: com a cota do Gemini no fim, o Kokoro em
93
+ português era a "voz robótica" que a pessoa ouvia. `motorVoz: 'edge'` no
94
+ roteiro pula as outras. */
92
95
  function ordem(voz, env = process.env, { soEdge = false } = {}) {
93
96
  if (soEdge) return ['edge'];
94
- return querKokoro(voz, env) ? ['kokoro', 'edge'] : ['gemini', 'kokoro', 'edge'];
97
+ return querKokoro(voz, env) ? ['kokoro', 'edge'] : ['gemini', 'edge', 'kokoro'];
98
+ }
99
+
100
+ /* A voz do Gemini no Edge: o narrar.py só conhece antonio, francisca e
101
+ thalita — o nome do Gemini ("Kore") caía na primeira, e toda fala de mulher
102
+ saía na voz do Antonio. Mantém o GÊNERO de quem fala. */
103
+ const GEMINI_FEMININAS = new Set(['kore', 'aoede', 'leda', 'zephyr', 'sulafat', 'achird', 'despina', 'erinome', 'laomedeia', 'pulcherrima', 'vindemiatrix', 'callirrhoe', 'autonoe', 'gacrux']);
104
+ function vozNoEdge(voz, reserva = 'thalita') {
105
+ const v = String(voz || '').trim().toLowerCase();
106
+ if (['antonio', 'francisca', 'thalita'].includes(v)) return v;
107
+ if (!v || querKokoro(v, {})) return reserva;
108
+ return GEMINI_FEMININAS.has(v) ? 'francisca' : 'antonio';
95
109
  }
96
110
 
97
111
  /** Roda a corrente: a primeira voz que der certo ganha; a falha de uma nunca derruba. */
@@ -108,4 +122,4 @@ async function corrente(passos, provedores) {
108
122
  return ultimo;
109
123
  }
110
124
 
111
- module.exports = { querKokoro, pronuncia, vozKokoro, acharEspeak, comando, narrar, ordem, corrente, DEPS, SCRIPT };
125
+ module.exports = { vozNoEdge, querKokoro, pronuncia, vozKokoro, acharEspeak, comando, narrar, ordem, corrente, DEPS, SCRIPT };
@@ -44,7 +44,7 @@ function areaSegura(W, H, m = MARGEM) {
44
44
  /* As peças do kit que são o QUADRO inteiro (textura, padrão, o gerativo que
45
45
  tem a própria conta) ou enfeite de fundo. */
46
46
  const PECAS_FUNDO = new Set(['textura', 'padrao', 'particulas', 'papel', 'grade', 'orbe', 'cromo', 'esfera', 'estrela',
47
- 'gerativo', 'fluido', 'personagem', 'site']);
47
+ 'gerativo', 'fluido', 'personagem', 'voxel', 'site']);
48
48
 
49
49
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
50
50