primocode 10.4.7 → 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.
- package/README.md +2 -2
- package/bin/primocode.js +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +29 -5
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/conta.js +9 -3
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +218 -0
- package/lib/estudio/dados/fluent-3d.json +1 -0
- package/lib/estudio/dados/noto-animado.json +1 -0
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/estilo.js +6 -2
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/gerativo.js +8 -0
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +202 -83
- package/lib/estudio/kit/animacao.jsx +41 -0
- package/lib/estudio/kit/gerativo.jsx +21 -1
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +854 -0
- package/lib/estudio/kit.js +5 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/mundo.js +305 -0
- package/lib/estudio/personagem.js +234 -10
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/estudio/remotion.js +2 -0
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/* kit/voxel.jsx — o vídeo GRAVADO em 3D, no visual de blocos (voxel).
|
|
2
|
+
*
|
|
3
|
+
* {"tipo":"kit","peca":"voxel","partitura":{…},"largura":1080,"altura":1920}
|
|
4
|
+
*
|
|
5
|
+
* "esse 3D que é como eu quero e não tudo fudido como tá lá" (Joel, 11/10,
|
|
6
|
+
* com o vídeo de referência: bonecos de blocos, o bichinho laranja do
|
|
7
|
+
* Claude flutuando, chão de ladrilhos roxo, a parede de telas e o fundo
|
|
8
|
+
* desfocado de lente de cinema)
|
|
9
|
+
*
|
|
10
|
+
* A partitura é a mesma do kit/personagem.jsx (lib/estudio/personagem.js: quem
|
|
11
|
+
* fala quando, os planos, a aparência). Muda o desenho:
|
|
12
|
+
* - tudo é bloco com a quina arredondada (RoundedBoxGeometry), nada de esfera;
|
|
13
|
+
* - o rosto é pixel: olho quadrado, sobrancelha grossa, a boca que abre com a voz;
|
|
14
|
+
* - quem é IA vira o mascote: o corpo laranja, os olhos em fenda, quatro
|
|
15
|
+
* perninhas, flutuando — e "fala" esticando o corpo no ritmo da voz;
|
|
16
|
+
* - o set: chão de ladrilhos, a parede de telas com os cartões do ASSUNTO do
|
|
17
|
+
* vídeo (os títulos saem da fala), luz de recorte roxa;
|
|
18
|
+
* - a lente: o desfoque de profundidade (BokehPass) e o brilho das telas
|
|
19
|
+
* (UnrealBloomPass), que é o que faz parecer render de cinema e não jogo;
|
|
20
|
+
* - a legenda é a da referência: frase inteira, branca, no meio, sem caixa alta.
|
|
21
|
+
* Tudo é geometria do three.js: nada é baixado. */
|
|
22
|
+
import React, { useMemo } from 'react';
|
|
23
|
+
import { ThreeCanvas } from '@remotion/three';
|
|
24
|
+
import * as THREE from 'three';
|
|
25
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
26
|
+
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
|
|
27
|
+
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
|
28
|
+
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
|
|
29
|
+
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
|
|
30
|
+
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
|
|
31
|
+
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
|
|
32
|
+
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|
33
|
+
import { Mundo, Animal, Coisa, alturaDoBicho, useIcones } from './mundo.jsx';
|
|
34
|
+
|
|
35
|
+
const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
|
36
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
37
|
+
const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
|
|
38
|
+
const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
|
|
39
|
+
|
|
40
|
+
/* ── O BLOCO ─────────────────────────────────────────────────────────────
|
|
41
|
+
Toda peça é uma caixa de quina arredondada. A geometria é guardada pelo
|
|
42
|
+
tamanho: o mesmo ladrilho repetido 200 vezes é UMA geometria. */
|
|
43
|
+
const GEOS = new Map();
|
|
44
|
+
function geo(w, h, d, r) {
|
|
45
|
+
const raio = Math.min(r, w / 2 - 0.001, h / 2 - 0.001, d / 2 - 0.001);
|
|
46
|
+
const k = `${w}|${h}|${d}|${raio}`;
|
|
47
|
+
if (!GEOS.has(k)) GEOS.set(k, new RoundedBoxGeometry(w, h, d, 3, Math.max(0.001, raio)));
|
|
48
|
+
return GEOS.get(k);
|
|
49
|
+
}
|
|
50
|
+
const Bloco = ({ s, p = [0, 0, 0], rot, c, r = 0.03, rough = 0.62, metal = 0, e, ei = 1, sombra = true }) => (
|
|
51
|
+
<mesh position={p} rotation={rot} geometry={geo(s[0], s[1], s[2], r)} castShadow={sombra} receiveShadow>
|
|
52
|
+
<meshStandardMaterial color={c} roughness={rough} metalness={metal} {...(e ? { emissive: e, emissiveIntensity: ei } : {})} />
|
|
53
|
+
</mesh>
|
|
54
|
+
);
|
|
55
|
+
// O que é chapado no rosto (olho, boca, sobrancelha): fino, sem quina e sem sombra.
|
|
56
|
+
const Chapa = ({ s, p, c, rot }) => (
|
|
57
|
+
<mesh position={p} rotation={rot}><boxGeometry args={s} /><meshStandardMaterial color={c} roughness={0.7} /></mesh>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/* ── A CENA POR TRÁS: a luz de ambiente e a câmera ───────────────────────── */
|
|
61
|
+
function Ambiente() {
|
|
62
|
+
const { gl, scene } = useThree();
|
|
63
|
+
useMemo(() => {
|
|
64
|
+
const pm = new THREE.PMREMGenerator(gl);
|
|
65
|
+
scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
66
|
+
scene.environmentIntensity = 0.22;
|
|
67
|
+
pm.dispose();
|
|
68
|
+
}, [gl, scene]);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Camera({ pos, alvo, fov }) {
|
|
73
|
+
const { camera } = useThree();
|
|
74
|
+
camera.position.set(pos[0], pos[1], pos[2]);
|
|
75
|
+
camera.fov = fov;
|
|
76
|
+
camera.near = 0.05;
|
|
77
|
+
camera.far = 60;
|
|
78
|
+
camera.lookAt(alvo[0], alvo[1], alvo[2]);
|
|
79
|
+
camera.updateProjectionMatrix();
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* A LENTE. O compositor desenha a cena, o brilho das telas e o desfoque de
|
|
84
|
+
profundidade, nesta ordem. O useFrame com prioridade 1 tira o desenho do
|
|
85
|
+
R3F e passa para o compositor — o Remotion chama o advance() a cada quadro,
|
|
86
|
+
e é ele que dispara isto. */
|
|
87
|
+
function Lente({ W, H, foco, abertura, desfoqueMax, brilho }) {
|
|
88
|
+
const { gl, scene, camera } = useThree();
|
|
89
|
+
const fx = useMemo(() => {
|
|
90
|
+
const comp = new EffectComposer(gl);
|
|
91
|
+
comp.setPixelRatio(1);
|
|
92
|
+
comp.setSize(W, H);
|
|
93
|
+
comp.addPass(new RenderPass(scene, camera));
|
|
94
|
+
const bloom = new UnrealBloomPass(new THREE.Vector2(W, H), 0.55, 0.65, 0.92);
|
|
95
|
+
comp.addPass(bloom);
|
|
96
|
+
const bokeh = new BokehPass(scene, camera, { focus: 3, aperture: 0.004, maxblur: 0.01 });
|
|
97
|
+
comp.addPass(bokeh);
|
|
98
|
+
comp.addPass(new OutputPass());
|
|
99
|
+
return { comp, bokeh, bloom };
|
|
100
|
+
}, [gl, scene, camera, W, H]);
|
|
101
|
+
fx.bokeh.uniforms.focus.value = foco;
|
|
102
|
+
fx.bokeh.uniforms.aperture.value = abertura;
|
|
103
|
+
fx.bokeh.uniforms.maxblur.value = desfoqueMax;
|
|
104
|
+
fx.bloom.strength = brilho;
|
|
105
|
+
useFrame(() => { fx.comp.render(); }, 1);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── A BOCA, O GESTO E A PISCADA (os mesmos do kit/personagem.jsx) ──────── */
|
|
110
|
+
const VISEMA = (ch) => {
|
|
111
|
+
if (/[aáàãâ]/.test(ch)) return [1, 0.6];
|
|
112
|
+
if (/[eéê]/.test(ch)) return [0.6, 0.85];
|
|
113
|
+
if (/[iíy]/.test(ch)) return [0.32, 1];
|
|
114
|
+
if (/[oóôõ]/.test(ch)) return [0.78, 0.3];
|
|
115
|
+
if (/[uúw]/.test(ch)) return [0.45, 0.15];
|
|
116
|
+
if (/[mbp]/.test(ch)) return [0, 0.5];
|
|
117
|
+
if (/[fv]/.test(ch)) return [0.12, 0.62];
|
|
118
|
+
if (/[lnrdt]/.test(ch)) return [0.3, 0.6];
|
|
119
|
+
return [0.22, 0.55];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function bocaEm(falas, quem, t) {
|
|
123
|
+
for (const f of falas) {
|
|
124
|
+
if (f.quem !== quem || t < f.de - 0.05 || t > f.ate + 0.05) continue;
|
|
125
|
+
const w = f.palavras.find((x) => t >= x.de && t <= x.ate);
|
|
126
|
+
if (!w) return [0, 0.55];
|
|
127
|
+
const letras = String(w.t).toLowerCase().replace(/[^a-záàãâéêíóôõúçy]/g, '') || 'a';
|
|
128
|
+
const p = clamp((t - w.de) / Math.max(0.05, w.ate - w.de), 0, 0.999);
|
|
129
|
+
const pos = p * letras.length;
|
|
130
|
+
const [a, l] = VISEMA(letras[Math.floor(pos)]);
|
|
131
|
+
return [a * (0.62 + 0.38 * Math.sin(Math.PI * (pos % 1))), l];
|
|
132
|
+
}
|
|
133
|
+
return [0, 0.55];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function boca(falas, quem, t) {
|
|
137
|
+
const amostras = [0, 1 / 60, 1 / 30].map((d) => bocaEm(falas, quem, t - d));
|
|
138
|
+
return [amostras.reduce((s, x) => s + x[0], 0) / 3, amostras.reduce((s, x) => s + x[1], 0) / 3];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function gesto(falas, quem, t) {
|
|
142
|
+
let esq = 0, dir = 0;
|
|
143
|
+
for (const f of falas) {
|
|
144
|
+
if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
|
|
145
|
+
f.palavras.forEach((w, k) => {
|
|
146
|
+
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);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return [esq, dir];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function piscada(quem, t) {
|
|
157
|
+
let base = 0.6 + quem * 1.3, k = 0;
|
|
158
|
+
while (base < t + 1) {
|
|
159
|
+
const d = t - base;
|
|
160
|
+
if (d >= 0 && d < 0.14) return Math.sin((d / 0.14) * Math.PI);
|
|
161
|
+
if (hash(quem * 31 + k) < 0.2) { const d2 = t - (base + 0.28); if (d2 >= 0 && d2 < 0.14) return Math.sin((d2 / 0.14) * Math.PI); }
|
|
162
|
+
base += 2.4 + hash(quem * 17 + k) * 2.6;
|
|
163
|
+
k++;
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const EMO = {
|
|
169
|
+
neutro: { sobr: 0, inclina: 0 },
|
|
170
|
+
feliz: { sobr: 0.3, inclina: 0.04 },
|
|
171
|
+
animado: { sobr: 0.6, inclina: 0 },
|
|
172
|
+
serio: { sobr: -0.5, inclina: 0 },
|
|
173
|
+
surpreso: { sobr: 0.9, inclina: 0 },
|
|
174
|
+
pensativo: { sobr: -0.15, inclina: 0.12 },
|
|
175
|
+
curioso: { sobr: 0.45, inclina: 0.1 },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/* ── A PESSOA DE BLOCOS ─────────────────────────────────────────────────
|
|
179
|
+
A cabeça é um cubo de 0,62: o rosto mora na face da frente (z = 0,31). */
|
|
180
|
+
const CAB = 0.62;
|
|
181
|
+
const FACE = CAB / 2 + 0.004;
|
|
182
|
+
const escurecer = (c, k) => new THREE.Color(c).multiplyScalar(k).getStyle();
|
|
183
|
+
|
|
184
|
+
function Cabelo({ estilo, cor }) {
|
|
185
|
+
const s = CAB;
|
|
186
|
+
const topo = <Bloco s={[s + 0.04, 0.16, s + 0.04]} p={[0, s / 2 + 0.02, -0.005]} c={cor} r={0.03} rough={0.85} />;
|
|
187
|
+
const nuca = <Bloco s={[s + 0.04, s * 0.62, 0.1]} p={[0, s * 0.14, -s / 2 - 0.02]} c={cor} r={0.03} rough={0.85} />;
|
|
188
|
+
const lados = [-1, 1].map((k) => <Bloco key={k} s={[0.06, s * 0.42, s * 0.7]} p={[k * (s / 2 + 0.02), s * 0.24, -0.08]} c={cor} r={0.02} rough={0.85} />);
|
|
189
|
+
if (estilo === 'careca') return null;
|
|
190
|
+
if (estilo === 'cacheado') {
|
|
191
|
+
return (
|
|
192
|
+
<group>
|
|
193
|
+
{nuca}{lados}
|
|
194
|
+
{[...Array(9)].map((_, i) => {
|
|
195
|
+
const x = ((i % 3) - 1) * 0.22, z = (Math.floor(i / 3) - 1) * 0.22;
|
|
196
|
+
return <Bloco key={i} s={[0.24, 0.2 + hash(i) * 0.06, 0.24]} p={[x, s / 2 + 0.06 + hash(i + 3) * 0.03, z - 0.01]} c={cor} r={0.05} rough={0.9} />;
|
|
197
|
+
})}
|
|
198
|
+
</group>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return (
|
|
202
|
+
<group>
|
|
203
|
+
{topo}{nuca}{lados}
|
|
204
|
+
{/* a franja: um degrau na testa, o que dá a cara do boneco da referência */}
|
|
205
|
+
<Bloco s={[s * 0.62, 0.1, 0.1]} p={[-s * 0.16, s / 2 - 0.05, s / 2 - 0.02]} c={cor} r={0.02} rough={0.85} />
|
|
206
|
+
{estilo === 'topete' && <Bloco s={[s * 0.7, 0.16, 0.26]} p={[0.04, s / 2 + 0.13, s / 2 - 0.12]} c={cor} r={0.04} rough={0.85} />}
|
|
207
|
+
{estilo === 'coque' && <Bloco s={[0.24, 0.24, 0.24]} p={[0, s / 2 + 0.08, -s / 2 + 0.02]} c={cor} r={0.05} rough={0.85} />}
|
|
208
|
+
{estilo === 'longo' && (
|
|
209
|
+
<>
|
|
210
|
+
<Bloco s={[s + 0.06, s * 1.05, 0.12]} p={[0, -0.06, -s / 2 - 0.04]} c={cor} r={0.03} rough={0.85} />
|
|
211
|
+
{[-1, 1].map((k) => <Bloco key={k} s={[0.08, s * 0.95, s * 0.6]} p={[k * (s / 2 + 0.03), -0.02, -0.1]} c={cor} r={0.02} rough={0.85} />)}
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</group>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function RostoPixel({ p, est }) {
|
|
219
|
+
const e = EMO[est.emocao] || EMO.neutro;
|
|
220
|
+
const sobr = e.sobr + est.enfase * 0.4;
|
|
221
|
+
const [abre, larg] = est.boca;
|
|
222
|
+
const pisca = clamp(est.pisca);
|
|
223
|
+
const escuro = '#141016';
|
|
224
|
+
const olhoH = 0.14 * (1 - pisca * 0.9);
|
|
225
|
+
// O olhar: a pupila anda dentro do branco do olho.
|
|
226
|
+
const ox = clamp(est.olhar[0], -1, 1) * 0.025, oy = clamp(est.olhar[1], -1, 1) * 0.02;
|
|
227
|
+
return (
|
|
228
|
+
<group>
|
|
229
|
+
{[-1, 1].map((k) => (
|
|
230
|
+
<group key={k} position={[k * 0.135, 0.035, FACE]}>
|
|
231
|
+
<Chapa s={[0.13, olhoH, 0.012]} p={[0, 0, 0]} c="#d6d2cc" />
|
|
232
|
+
{pisca < 0.6 && <Chapa s={[0.065, Math.min(olhoH, 0.085), 0.016]} p={[ox + k * 0.012, oy - 0.005, 0.002]} c={escuro} />}
|
|
233
|
+
{/* a sobrancelha grossa: sobe e desce com a emoção, inclina no sério */}
|
|
234
|
+
<Chapa s={[0.17, 0.05, 0.02]} p={[0, 0.125 + sobr * 0.03, 0.004]}
|
|
235
|
+
rot={[0, 0, k * (est.emocao === 'serio' ? 0.22 : est.emocao === 'surpreso' ? -0.08 : 0.04) - k * sobr * 0.06]} c={p.corCabelo} />
|
|
236
|
+
</group>
|
|
237
|
+
))}
|
|
238
|
+
{/* o nariz: um degrauzinho de pele mais escura */}
|
|
239
|
+
<Bloco s={[0.07, 0.11, 0.05]} p={[0, -0.04, FACE + 0.015]} c={escurecer(p.pele, 0.86)} r={0.012} sombra={false} />
|
|
240
|
+
{/* a boca: um retângulo escuro que abre com a vogal e estica no "i" */}
|
|
241
|
+
<Chapa s={[0.1 + 0.1 * larg, 0.028 + 0.1 * abre, 0.012]} p={[0, -0.165 - 0.03 * abre, FACE]} c="#3a1416" />
|
|
242
|
+
{abre > 0.35 && <Chapa s={[0.08 + 0.06 * larg, 0.022, 0.014]} p={[0, -0.155 + 0.02 * abre, FACE + 0.001]} c="#efe9e1" />}
|
|
243
|
+
{p.barba && (
|
|
244
|
+
<>
|
|
245
|
+
<Chapa s={[CAB + 0.006, 0.17, 0.008]} p={[0, -CAB / 2 + 0.085, FACE - 0.002]} c={escurecer(p.corCabelo, 1)} />
|
|
246
|
+
{[-1, 1].map((k) => <Chapa key={k} s={[0.008, 0.26, CAB * 0.7]} p={[k * (CAB / 2 + 0.003), -CAB / 2 + 0.13, 0.04]} c={p.corCabelo} />)}
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
{p.oculos && (
|
|
250
|
+
<group position={[0, 0.035, FACE + 0.03]}>
|
|
251
|
+
{[-1, 1].map((k) => (
|
|
252
|
+
<group key={k} position={[k * 0.135, 0, 0]}>
|
|
253
|
+
{[[0, 0.085, 0.2, 0.025], [0, -0.085, 0.2, 0.025], [-0.0875, 0, 0.025, 0.19], [0.0875, 0, 0.025, 0.19]].map(([x, y, w, h], i) => (
|
|
254
|
+
<Chapa key={i} s={[w, h, 0.02]} p={[x, y, 0]} c="#111114" />
|
|
255
|
+
))}
|
|
256
|
+
</group>
|
|
257
|
+
))}
|
|
258
|
+
<Chapa s={[0.07, 0.022, 0.02]} p={[0, 0.03, 0]} c="#111114" />
|
|
259
|
+
</group>
|
|
260
|
+
)}
|
|
261
|
+
</group>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
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 }) {
|
|
374
|
+
// Repouso: o braço caído, um tico para fora. Gesto: o antebraço sobe e a mão mexe.
|
|
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); }
|
|
380
|
+
return (
|
|
381
|
+
<group position={[lado * 0.47, 1.4, 0]} rotation={[ombroX, 0, ombroZ]}>
|
|
382
|
+
<Bloco s={[0.2, 0.36, 0.2]} p={[0, -0.17, 0]} c={cor} r={0.04} rough={0.8} />
|
|
383
|
+
<group position={[0, -0.34, 0]} rotation={[cotovelo, 0, 0]}>
|
|
384
|
+
<Bloco s={[0.19, 0.3, 0.19]} p={[0, -0.14, 0]} c={cor} r={0.04} rough={0.8} />
|
|
385
|
+
<Bloco s={[0.17, 0.17, 0.17]} p={[0, -0.36, 0]} c={pele} r={0.035} />
|
|
386
|
+
</group>
|
|
387
|
+
</group>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function PessoaBloco({ p, est }) {
|
|
392
|
+
const resp = Math.sin(est.t * 1.7) * 0.008;
|
|
393
|
+
const [cy, cp, cr] = est.cabeca;
|
|
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;
|
|
400
|
+
return (
|
|
401
|
+
<group position={[0, pulo, 0]} rotation={[0, 0, balanco]}>
|
|
402
|
+
{/* as pernas e o sapato */}
|
|
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
|
+
})}
|
|
412
|
+
<group position={[0, resp, 0]}>
|
|
413
|
+
{/* o tronco, com a faixa branca da camisa da referência */}
|
|
414
|
+
<Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
|
|
415
|
+
<Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
|
|
416
|
+
<Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
|
|
417
|
+
</group>
|
|
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} />
|
|
420
|
+
<group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
|
|
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} />}
|
|
428
|
+
</group>
|
|
429
|
+
</group>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* ── O MASCOTE (quem é IA) ──────────────────────────────────────────────
|
|
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. */
|
|
439
|
+
function Mascote({ p, est }) {
|
|
440
|
+
const cor = p.cor || '#d9532b';
|
|
441
|
+
const [abre] = est.boca;
|
|
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;
|
|
445
|
+
const pisca = clamp(est.pisca);
|
|
446
|
+
const [cy, cp, cr] = est.cabeca;
|
|
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];
|
|
449
|
+
return (
|
|
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>
|
|
480
|
+
</group>
|
|
481
|
+
</group>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* ── O SET ─────────────────────────────────────────────────────────────── */
|
|
486
|
+
|
|
487
|
+
/* Um cartão de tela: o painel escuro com o ícone, o título do assunto, duas
|
|
488
|
+
linhas de texto, a barra de progresso e a bolinha verde de "rodando". É
|
|
489
|
+
desenhado num canvas 2D e vira textura que BRILHA (o bloom pega). */
|
|
490
|
+
function texturaCartao(titulo, linha, cor, acento, k) {
|
|
491
|
+
const cv = document.createElement('canvas');
|
|
492
|
+
cv.width = 512; cv.height = 320;
|
|
493
|
+
const g = cv.getContext('2d');
|
|
494
|
+
const r = 26;
|
|
495
|
+
g.fillStyle = cor; g.beginPath(); g.roundRect(6, 6, 500, 308, r); g.fill();
|
|
496
|
+
g.strokeStyle = 'rgba(160,175,255,0.35)'; g.lineWidth = 3; g.stroke();
|
|
497
|
+
g.fillStyle = acento; g.beginPath(); g.roundRect(34, 34, 44, 44, 8); g.fill();
|
|
498
|
+
g.fillStyle = '#eef1ff'; g.font = '600 34px Inter, "Helvetica Neue", Arial, sans-serif';
|
|
499
|
+
g.fillText(String(titulo).slice(0, 18), 96, 68);
|
|
500
|
+
g.fillStyle = '#3ddc84'; g.beginPath(); g.arc(462, 56, 10, 0, Math.PI * 2); g.fill();
|
|
501
|
+
g.fillStyle = 'rgba(205,212,255,0.75)'; g.font = '400 24px Inter, "Helvetica Neue", Arial, sans-serif';
|
|
502
|
+
g.fillText(String(linha).slice(0, 30), 36, 132);
|
|
503
|
+
g.fillStyle = 'rgba(205,212,255,0.28)';
|
|
504
|
+
g.beginPath(); g.roundRect(36, 160, 300 + hash(k) * 120, 16, 8); g.fill();
|
|
505
|
+
g.beginPath(); g.roundRect(36, 192, 200 + hash(k + 4) * 160, 16, 8); g.fill();
|
|
506
|
+
g.fillStyle = 'rgba(205,212,255,0.18)'; g.beginPath(); g.roundRect(36, 262, 440, 10, 5); g.fill();
|
|
507
|
+
g.fillStyle = '#8fa2ff'; g.beginPath(); g.roundRect(36, 262, 120 + hash(k + 9) * 300, 10, 5); g.fill();
|
|
508
|
+
const tx = new THREE.CanvasTexture(cv);
|
|
509
|
+
tx.colorSpace = THREE.SRGBColorSpace;
|
|
510
|
+
tx.anisotropy = 4;
|
|
511
|
+
return tx;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function ParedeDeTelas({ c, paineis, t }) {
|
|
515
|
+
const texturas = useMemo(() => {
|
|
516
|
+
const lista = paineis && paineis.length ? paineis : [{ titulo: 'Agora', linha: '' }];
|
|
517
|
+
return Array.from({ length: 16 }, (_, i) => {
|
|
518
|
+
const pn = lista[i % lista.length];
|
|
519
|
+
return texturaCartao(pn.titulo, pn.linha || '', c.tela, c.acento, i);
|
|
520
|
+
});
|
|
521
|
+
}, [paineis, c.tela, c.acento]);
|
|
522
|
+
// Duas fileiras de seis telas, num arco suave atrás do set.
|
|
523
|
+
const telas = [];
|
|
524
|
+
for (let fila = 0; fila < 2; fila++) {
|
|
525
|
+
for (let i = 0; i < 8; i++) {
|
|
526
|
+
const x = (i - 3.5) * 1.62;
|
|
527
|
+
const ang = -x * 0.07;
|
|
528
|
+
const z = -3.4 + Math.abs(x) * 0.12;
|
|
529
|
+
telas.push({ k: fila * 8 + i, x, y: 1.25 + fila * 1.08, z, ang });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return (
|
|
533
|
+
<group>
|
|
534
|
+
{/* a estrutura escura onde as telas moram */}
|
|
535
|
+
<Bloco s={[16, 3.2, 0.3]} p={[0, 1.78, -3.75]} c={c.parede} r={0.05} rough={0.9} sombra={false} />
|
|
536
|
+
{telas.map((s) => {
|
|
537
|
+
const vivo = 0.82 + 0.18 * Math.sin(t * 2 + s.k * 1.7);
|
|
538
|
+
return (
|
|
539
|
+
<group key={s.k} position={[s.x, s.y, s.z]} rotation={[0, s.ang, 0]}>
|
|
540
|
+
<Bloco s={[1.54, 0.98, 0.07]} c="#0a0c18" r={0.04} rough={0.4} sombra={false} />
|
|
541
|
+
<mesh position={[0, 0, 0.04]}>
|
|
542
|
+
<planeGeometry args={[1.46, 0.9]} />
|
|
543
|
+
<meshStandardMaterial map={texturas[s.k]} emissiveMap={texturas[s.k]} emissive="#ffffff" emissiveIntensity={0.55 * vivo} roughness={0.35} toneMapped />
|
|
544
|
+
</mesh>
|
|
545
|
+
</group>
|
|
546
|
+
);
|
|
547
|
+
})}
|
|
548
|
+
{/* a fileira de luzinhas em cima da parede, como na referência */}
|
|
549
|
+
{Array.from({ length: 9 }, (_, i) => (
|
|
550
|
+
<Bloco key={'l' + i} s={[0.16, 0.1, 0.1]} p={[(i - 4) * 1.15, 3.15, -3.55]} c={c.acento} e={c.acento} ei={1.6 + 0.6 * Math.sin(t * 3 + i)} r={0.02} sombra={false} />
|
|
551
|
+
))}
|
|
552
|
+
</group>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function ChaoDeLadrilhos({ c }) {
|
|
557
|
+
// Ladrilhos de 0,9 com junta escura: a grade que dá escala ao set.
|
|
558
|
+
const pecas = useMemo(() => {
|
|
559
|
+
const out = [];
|
|
560
|
+
for (let i = -7; i <= 7; i++) for (let j = -5; j <= 6; j++) out.push([i * 0.92 + (j % 2 ? 0.46 : 0), j * 0.6, hash(i * 13 + j * 7)]);
|
|
561
|
+
return out;
|
|
562
|
+
}, []);
|
|
563
|
+
return (
|
|
564
|
+
<group>
|
|
565
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.02, 0]} receiveShadow>
|
|
566
|
+
<planeGeometry args={[40, 40]} /><meshStandardMaterial color={escurecer(c.chao, 0.45)} roughness={0.9} />
|
|
567
|
+
</mesh>
|
|
568
|
+
{pecas.map(([x, z, k], i) => (
|
|
569
|
+
<mesh key={i} position={[x, -0.03, z]} geometry={geo(0.88, 0.1, 0.56, 0.035)} receiveShadow>
|
|
570
|
+
<meshStandardMaterial color={k > 0.5 ? c.chao : c.chao2} roughness={0.42} metalness={0.05} />
|
|
571
|
+
</mesh>
|
|
572
|
+
))}
|
|
573
|
+
</group>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
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]] };
|
|
582
|
+
function lugarDe(el, i) {
|
|
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];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function cameraDe(pl, t, part, aspecto) {
|
|
611
|
+
const vert = aspecto < 1;
|
|
612
|
+
const el = part.elenco;
|
|
613
|
+
const dois = el.length > 1;
|
|
614
|
+
const quem = el[pl.alvo] ? pl.alvo : 0;
|
|
615
|
+
const cabeca = (i) => { const p = lugarDe(el, i); return [p[0], alturaDaCabeca(el[i]), p[2]]; };
|
|
616
|
+
const frente = (i) => [Math.sin(giroDe(el, i)), 0, Math.cos(giroDe(el, i))];
|
|
617
|
+
const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
|
|
618
|
+
const fovK = vert ? 1.45 : 1;
|
|
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' };
|
|
622
|
+
if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
|
|
623
|
+
// O plano aberto de cima, como o da referência: o set inteiro e a parede de telas.
|
|
624
|
+
fov = (pl.tipo === 'largo' ? 38 : 34) * fovK;
|
|
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];
|
|
631
|
+
} else if (pl.tipo === 'ombro' && dois) {
|
|
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);
|
|
636
|
+
const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
|
|
637
|
+
pos = [h[0] + (d[0] / n) * 0.9, h[1] + 0.25, h[2] + 1.6];
|
|
638
|
+
alvo = [s[0], s[1] - 0.1, s[2]]; fov = 32 * fovK;
|
|
639
|
+
} else {
|
|
640
|
+
/* O close da referência é de BAIXO e perto, com a lente aberta: o rosto
|
|
641
|
+
enorme e o resto derretendo no desfoque. */
|
|
642
|
+
const dist = (pl.tipo === 'detalhe' ? 1.25 : pl.tipo === 'close' ? 1.7 : 2.7) * (vert ? 1.25 : 1);
|
|
643
|
+
fov = (pl.tipo === 'detalhe' ? 30 : pl.tipo === 'close' ? 32 : 34) * fovK;
|
|
644
|
+
const h = cabeca(quem), f = frente(quem);
|
|
645
|
+
const lado = pl.lado * 0.42;
|
|
646
|
+
const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
|
|
647
|
+
const baixo = pl.tipo === 'medio' ? 0.05 : -0.32;
|
|
648
|
+
pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
|
|
649
|
+
alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
|
|
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]];
|
|
653
|
+
const m = suave(k);
|
|
654
|
+
if (pl.mov === 'entra' || pl.mov === 'sai') {
|
|
655
|
+
const s = pl.mov === 'entra' ? m : 1 - m;
|
|
656
|
+
pos = pos.map((v, i) => lerp(v, alvo[i], 0.12 * s));
|
|
657
|
+
} else if (pl.mov === 'lateral') {
|
|
658
|
+
pos = [pos[0] + (m - 0.5) * 0.45 * pl.lado, pos[1], pos[2]];
|
|
659
|
+
}
|
|
660
|
+
const mao = [Math.sin(t * 1.3) * 0.01 + Math.sin(t * 3.1) * 0.004, Math.sin(t * 1.1 + 1) * 0.008 + Math.sin(t * 2.7) * 0.003, 0];
|
|
661
|
+
pos = pos.map((v, i) => v + (mao[i] || 0));
|
|
662
|
+
alvo = alvo.map((v, i) => v + (mao[i] || 0) * 0.5);
|
|
663
|
+
/* O FOCO é no ROSTO, não no centro da cabeça: a cara fica 0,3 m mais perto
|
|
664
|
+
da câmera que o alvo. No plano aberto, no meio do set. A abertura é
|
|
665
|
+
pequena (a lente de perto já borra muito por metro), e o teto do
|
|
666
|
+
desfoque é que dá o fundo derretido da referência. */
|
|
667
|
+
const aoAlvo = Math.hypot(pos[0] - alvo[0], pos[1] - alvo[1], pos[2] - alvo[2]);
|
|
668
|
+
const deRosto = pl.tipo !== 'aberto' && pl.tipo !== 'largo';
|
|
669
|
+
const foco = deRosto ? Math.max(0.4, aoAlvo - 0.28) : aoAlvo;
|
|
670
|
+
const perto = pl.tipo === 'detalhe' || pl.tipo === 'close';
|
|
671
|
+
return { pos, alvo, fov, foco, abertura: perto ? 0.012 : pl.tipo === 'ombro' ? 0.01 : pl.tipo === 'medio' ? 0.008 : 0.003, desfoqueMax: perto ? 0.03 : pl.tipo === 'ombro' ? 0.024 : 0.012 };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* ── POR CIMA: a legenda da referência ─────────────────────────────────── */
|
|
675
|
+
const FONTE = "'Inter','SF Pro Display','Helvetica Neue',Arial,sans-serif";
|
|
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;
|
|
679
|
+
function pedacos(palavras) {
|
|
680
|
+
const out = [];
|
|
681
|
+
let cur = [];
|
|
682
|
+
for (const w of palavras) {
|
|
683
|
+
cur.push(w);
|
|
684
|
+
const letras = cur.reduce((n, x) => n + String(x.t).length + 1, 0);
|
|
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
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (cur.length) out.push(cur);
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
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
|
+
|
|
710
|
+
function Legenda({ part, t, W, H }) {
|
|
711
|
+
const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.35);
|
|
712
|
+
if (!f) return null;
|
|
713
|
+
const grupos = pedacos(f.palavras);
|
|
714
|
+
const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.2) || grupos[grupos.length - 1];
|
|
715
|
+
const entra = suave((t - (g[0].de - 0.08)) / 0.18);
|
|
716
|
+
const sai = 1 - suave((t - (f.ate + 0.15)) / 0.2);
|
|
717
|
+
const vertical = H > W;
|
|
718
|
+
const util = W * 0.84;
|
|
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);
|
|
721
|
+
return (
|
|
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)`,
|
|
725
|
+
textShadow: '0 2px 12px rgba(0,0,0,0.55), 0 1px 2px rgba(0,0,0,0.6)' }}>{texto}</div>
|
|
726
|
+
</div>
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function Destaque({ part, t, W, H, acento }) {
|
|
731
|
+
const f = part.falas.find((x) => x.destaque && t >= x.de + 0.3 && t <= Math.min(x.ate + 0.3, x.de + 3));
|
|
732
|
+
if (!f) return null;
|
|
733
|
+
const k = t - (f.de + 0.3);
|
|
734
|
+
const fim = Math.min(f.ate + 0.3, f.de + 3) - (f.de + 0.3);
|
|
735
|
+
const s = suave(k / 0.3) * (1 - suave((k - fim + 0.3) / 0.3));
|
|
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);
|
|
738
|
+
return (
|
|
739
|
+
<div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.12 : 0.08), display: 'flex', justifyContent: 'center' }}>
|
|
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,
|
|
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>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/* ── A PEÇA ────────────────────────────────────────────────────────────── */
|
|
747
|
+
export function VoxelCena({ el, t }) {
|
|
748
|
+
const part = el.partitura || {};
|
|
749
|
+
const W = el.largura || part.largura || 1080, H = el.altura || part.altura || 1920;
|
|
750
|
+
const c = { parede: '#07061a', chao: '#4338ca', chao2: '#3b32b8', acento: '#ff7a45', luz: '#ffd2b0', tela: '#101734', recorte: '#8b7bff', ...(part.cenario || {}) };
|
|
751
|
+
const elenco = part.elenco || [];
|
|
752
|
+
const falas = part.falas || [];
|
|
753
|
+
const planos = part.planos || [{ de: 0, ate: part.duracao || 10, tipo: 'aberto', alvo: 0, mov: 'fixo', lado: 1 }];
|
|
754
|
+
const dois = elenco.length > 1;
|
|
755
|
+
const pl = planos.find((x) => t >= x.de && t < x.ate) || planos[planos.length - 1];
|
|
756
|
+
const cam = cameraDe(pl, t, part, W / H);
|
|
757
|
+
|
|
758
|
+
const falandoAgora = (i) => falas.some((f) => f.quem === i && t >= f.de && t <= f.ate);
|
|
759
|
+
const emocaoDe = (i) => {
|
|
760
|
+
let e = 'neutro';
|
|
761
|
+
for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
|
|
762
|
+
return e;
|
|
763
|
+
};
|
|
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);
|
|
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);
|
|
779
|
+
const emo = figurante ? 'feliz' : emocaoDe(i);
|
|
780
|
+
const e = EMO[emo] || EMO.neutro;
|
|
781
|
+
const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.6;
|
|
782
|
+
// Quem ouve vira um pouco para quem fala; quem fala olha para a câmera de vez em quando.
|
|
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);
|
|
786
|
+
return {
|
|
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],
|
|
791
|
+
};
|
|
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);
|
|
808
|
+
const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);
|
|
809
|
+
|
|
810
|
+
return (
|
|
811
|
+
<div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden', background: '#000' }}>
|
|
812
|
+
<div style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
|
|
813
|
+
<ThreeCanvas width={W} height={H} shadows camera={{ position: [0, 2, 6], fov: 36 }}
|
|
814
|
+
gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.0 }}>
|
|
815
|
+
<Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
|
|
816
|
+
<Ambiente />
|
|
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} />}
|
|
833
|
+
{elenco.map((p, i) => (
|
|
834
|
+
<group key={i} position={lugarDe(elenco, i)} rotation={[0, giroDe(elenco, i), 0]}>
|
|
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))}
|
|
841
|
+
</group>
|
|
842
|
+
))}
|
|
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} />
|
|
845
|
+
</ThreeCanvas>
|
|
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%)' }} />
|
|
847
|
+
<Destaque part={part} t={t} W={W} H={H} acento={c.acento} />
|
|
848
|
+
{part.legenda !== false && <Legenda part={part} t={t} W={W} H={H} />}
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export const VOXEL = { voxel: VoxelCena };
|