primocode 10.4.7 → 10.5.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.
@@ -0,0 +1,609 @@
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
+
34
+ const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
35
+ const lerp = (a, b, k) => a + (b - a) * k;
36
+ const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
37
+ const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
38
+
39
+ /* ── O BLOCO ─────────────────────────────────────────────────────────────
40
+ Toda peça é uma caixa de quina arredondada. A geometria é guardada pelo
41
+ tamanho: o mesmo ladrilho repetido 200 vezes é UMA geometria. */
42
+ const GEOS = new Map();
43
+ function geo(w, h, d, r) {
44
+ const raio = Math.min(r, w / 2 - 0.001, h / 2 - 0.001, d / 2 - 0.001);
45
+ const k = `${w}|${h}|${d}|${raio}`;
46
+ if (!GEOS.has(k)) GEOS.set(k, new RoundedBoxGeometry(w, h, d, 3, Math.max(0.001, raio)));
47
+ return GEOS.get(k);
48
+ }
49
+ const Bloco = ({ s, p = [0, 0, 0], rot, c, r = 0.03, rough = 0.62, metal = 0, e, ei = 1, sombra = true }) => (
50
+ <mesh position={p} rotation={rot} geometry={geo(s[0], s[1], s[2], r)} castShadow={sombra} receiveShadow>
51
+ <meshStandardMaterial color={c} roughness={rough} metalness={metal} {...(e ? { emissive: e, emissiveIntensity: ei } : {})} />
52
+ </mesh>
53
+ );
54
+ // O que é chapado no rosto (olho, boca, sobrancelha): fino, sem quina e sem sombra.
55
+ const Chapa = ({ s, p, c, rot }) => (
56
+ <mesh position={p} rotation={rot}><boxGeometry args={s} /><meshStandardMaterial color={c} roughness={0.7} /></mesh>
57
+ );
58
+
59
+ /* ── A CENA POR TRÁS: a luz de ambiente e a câmera ───────────────────────── */
60
+ function Ambiente() {
61
+ const { gl, scene } = useThree();
62
+ useMemo(() => {
63
+ const pm = new THREE.PMREMGenerator(gl);
64
+ scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
65
+ scene.environmentIntensity = 0.22;
66
+ pm.dispose();
67
+ }, [gl, scene]);
68
+ return null;
69
+ }
70
+
71
+ function Camera({ pos, alvo, fov }) {
72
+ const { camera } = useThree();
73
+ camera.position.set(pos[0], pos[1], pos[2]);
74
+ camera.fov = fov;
75
+ camera.near = 0.05;
76
+ camera.far = 60;
77
+ camera.lookAt(alvo[0], alvo[1], alvo[2]);
78
+ camera.updateProjectionMatrix();
79
+ return null;
80
+ }
81
+
82
+ /* A LENTE. O compositor desenha a cena, o brilho das telas e o desfoque de
83
+ profundidade, nesta ordem. O useFrame com prioridade 1 tira o desenho do
84
+ R3F e passa para o compositor — o Remotion chama o advance() a cada quadro,
85
+ e é ele que dispara isto. */
86
+ function Lente({ W, H, foco, abertura, desfoqueMax, brilho }) {
87
+ const { gl, scene, camera } = useThree();
88
+ const fx = useMemo(() => {
89
+ const comp = new EffectComposer(gl);
90
+ comp.setPixelRatio(1);
91
+ comp.setSize(W, H);
92
+ comp.addPass(new RenderPass(scene, camera));
93
+ const bloom = new UnrealBloomPass(new THREE.Vector2(W, H), 0.55, 0.65, 0.92);
94
+ comp.addPass(bloom);
95
+ const bokeh = new BokehPass(scene, camera, { focus: 3, aperture: 0.004, maxblur: 0.01 });
96
+ comp.addPass(bokeh);
97
+ comp.addPass(new OutputPass());
98
+ return { comp, bokeh, bloom };
99
+ }, [gl, scene, camera, W, H]);
100
+ fx.bokeh.uniforms.focus.value = foco;
101
+ fx.bokeh.uniforms.aperture.value = abertura;
102
+ fx.bokeh.uniforms.maxblur.value = desfoqueMax;
103
+ fx.bloom.strength = brilho;
104
+ useFrame(() => { fx.comp.render(); }, 1);
105
+ return null;
106
+ }
107
+
108
+ /* ── A BOCA, O GESTO E A PISCADA (os mesmos do kit/personagem.jsx) ──────── */
109
+ const VISEMA = (ch) => {
110
+ if (/[aáàãâ]/.test(ch)) return [1, 0.6];
111
+ if (/[eéê]/.test(ch)) return [0.6, 0.85];
112
+ if (/[iíy]/.test(ch)) return [0.32, 1];
113
+ if (/[oóôõ]/.test(ch)) return [0.78, 0.3];
114
+ if (/[uúw]/.test(ch)) return [0.45, 0.15];
115
+ if (/[mbp]/.test(ch)) return [0, 0.5];
116
+ if (/[fv]/.test(ch)) return [0.12, 0.62];
117
+ if (/[lnrdt]/.test(ch)) return [0.3, 0.6];
118
+ return [0.22, 0.55];
119
+ };
120
+
121
+ function bocaEm(falas, quem, t) {
122
+ for (const f of falas) {
123
+ if (f.quem !== quem || t < f.de - 0.05 || t > f.ate + 0.05) continue;
124
+ const w = f.palavras.find((x) => t >= x.de && t <= x.ate);
125
+ if (!w) return [0, 0.55];
126
+ const letras = String(w.t).toLowerCase().replace(/[^a-záàãâéêíóôõúçy]/g, '') || 'a';
127
+ const p = clamp((t - w.de) / Math.max(0.05, w.ate - w.de), 0, 0.999);
128
+ const pos = p * letras.length;
129
+ const [a, l] = VISEMA(letras[Math.floor(pos)]);
130
+ return [a * (0.62 + 0.38 * Math.sin(Math.PI * (pos % 1))), l];
131
+ }
132
+ return [0, 0.55];
133
+ }
134
+
135
+ function boca(falas, quem, t) {
136
+ const amostras = [0, 1 / 60, 1 / 30].map((d) => bocaEm(falas, quem, t - d));
137
+ return [amostras.reduce((s, x) => s + x[0], 0) / 3, amostras.reduce((s, x) => s + x[1], 0) / 3];
138
+ }
139
+
140
+ function gesto(falas, quem, t) {
141
+ let esq = 0, dir = 0;
142
+ for (const f of falas) {
143
+ if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
144
+ f.palavras.forEach((w, k) => {
145
+ if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
146
+ const x = (t - (w.de - 0.15)) / 1.0;
147
+ if (x < 0 || x > 1) return;
148
+ const b = Math.sin(Math.PI * x);
149
+ if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
150
+ });
151
+ }
152
+ return [esq, dir];
153
+ }
154
+
155
+ function piscada(quem, t) {
156
+ let base = 0.6 + quem * 1.3, k = 0;
157
+ while (base < t + 1) {
158
+ const d = t - base;
159
+ if (d >= 0 && d < 0.14) return Math.sin((d / 0.14) * Math.PI);
160
+ 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); }
161
+ base += 2.4 + hash(quem * 17 + k) * 2.6;
162
+ k++;
163
+ }
164
+ return 0;
165
+ }
166
+
167
+ const EMO = {
168
+ neutro: { sobr: 0, inclina: 0 },
169
+ feliz: { sobr: 0.3, inclina: 0.04 },
170
+ animado: { sobr: 0.6, inclina: 0 },
171
+ serio: { sobr: -0.5, inclina: 0 },
172
+ surpreso: { sobr: 0.9, inclina: 0 },
173
+ pensativo: { sobr: -0.15, inclina: 0.12 },
174
+ curioso: { sobr: 0.45, inclina: 0.1 },
175
+ };
176
+
177
+ /* ── A PESSOA DE BLOCOS ─────────────────────────────────────────────────
178
+ A cabeça é um cubo de 0,62: o rosto mora na face da frente (z = 0,31). */
179
+ const CAB = 0.62;
180
+ const FACE = CAB / 2 + 0.004;
181
+ const escurecer = (c, k) => new THREE.Color(c).multiplyScalar(k).getStyle();
182
+
183
+ function Cabelo({ estilo, cor }) {
184
+ const s = CAB;
185
+ 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} />;
186
+ 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} />;
187
+ 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} />);
188
+ if (estilo === 'careca') return null;
189
+ if (estilo === 'cacheado') {
190
+ return (
191
+ <group>
192
+ {nuca}{lados}
193
+ {[...Array(9)].map((_, i) => {
194
+ const x = ((i % 3) - 1) * 0.22, z = (Math.floor(i / 3) - 1) * 0.22;
195
+ 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} />;
196
+ })}
197
+ </group>
198
+ );
199
+ }
200
+ return (
201
+ <group>
202
+ {topo}{nuca}{lados}
203
+ {/* a franja: um degrau na testa, o que dá a cara do boneco da referência */}
204
+ <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} />
205
+ {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} />}
206
+ {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} />}
207
+ {estilo === 'longo' && (
208
+ <>
209
+ <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} />
210
+ {[-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} />)}
211
+ </>
212
+ )}
213
+ </group>
214
+ );
215
+ }
216
+
217
+ function RostoPixel({ p, est }) {
218
+ const e = EMO[est.emocao] || EMO.neutro;
219
+ const sobr = e.sobr + est.enfase * 0.4;
220
+ const [abre, larg] = est.boca;
221
+ const pisca = clamp(est.pisca);
222
+ const escuro = '#141016';
223
+ const olhoH = 0.14 * (1 - pisca * 0.9);
224
+ // O olhar: a pupila anda dentro do branco do olho.
225
+ const ox = clamp(est.olhar[0], -1, 1) * 0.025, oy = clamp(est.olhar[1], -1, 1) * 0.02;
226
+ return (
227
+ <group>
228
+ {[-1, 1].map((k) => (
229
+ <group key={k} position={[k * 0.135, 0.035, FACE]}>
230
+ <Chapa s={[0.13, olhoH, 0.012]} p={[0, 0, 0]} c="#d6d2cc" />
231
+ {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} />}
232
+ {/* a sobrancelha grossa: sobe e desce com a emoção, inclina no sério */}
233
+ <Chapa s={[0.17, 0.05, 0.02]} p={[0, 0.125 + sobr * 0.03, 0.004]}
234
+ rot={[0, 0, k * (est.emocao === 'serio' ? 0.22 : est.emocao === 'surpreso' ? -0.08 : 0.04) - k * sobr * 0.06]} c={p.corCabelo} />
235
+ </group>
236
+ ))}
237
+ {/* o nariz: um degrauzinho de pele mais escura */}
238
+ <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} />
239
+ {/* a boca: um retângulo escuro que abre com a vogal e estica no "i" */}
240
+ <Chapa s={[0.1 + 0.1 * larg, 0.028 + 0.1 * abre, 0.012]} p={[0, -0.165 - 0.03 * abre, FACE]} c="#3a1416" />
241
+ {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" />}
242
+ {p.barba && (
243
+ <>
244
+ <Chapa s={[CAB + 0.006, 0.17, 0.008]} p={[0, -CAB / 2 + 0.085, FACE - 0.002]} c={escurecer(p.corCabelo, 1)} />
245
+ {[-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} />)}
246
+ </>
247
+ )}
248
+ {p.oculos && (
249
+ <group position={[0, 0.035, FACE + 0.03]}>
250
+ {[-1, 1].map((k) => (
251
+ <group key={k} position={[k * 0.135, 0, 0]}>
252
+ {[[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) => (
253
+ <Chapa key={i} s={[w, h, 0.02]} p={[x, y, 0]} c="#111114" />
254
+ ))}
255
+ </group>
256
+ ))}
257
+ <Chapa s={[0.07, 0.022, 0.02]} p={[0, 0.03, 0]} c="#111114" />
258
+ </group>
259
+ )}
260
+ </group>
261
+ );
262
+ }
263
+
264
+ function BracoBloco({ lado, g, cor, pele, t }) {
265
+ // 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;
269
+ return (
270
+ <group position={[lado * 0.47, 1.4, 0]} rotation={[ombroX, 0, ombroZ]}>
271
+ <Bloco s={[0.2, 0.36, 0.2]} p={[0, -0.17, 0]} c={cor} r={0.04} rough={0.8} />
272
+ <group position={[0, -0.34, 0]} rotation={[cotovelo, 0, 0]}>
273
+ <Bloco s={[0.19, 0.3, 0.19]} p={[0, -0.14, 0]} c={cor} r={0.04} rough={0.8} />
274
+ <Bloco s={[0.17, 0.17, 0.17]} p={[0, -0.36, 0]} c={pele} r={0.035} />
275
+ </group>
276
+ </group>
277
+ );
278
+ }
279
+
280
+ function PessoaBloco({ p, est }) {
281
+ const resp = Math.sin(est.t * 1.7) * 0.008;
282
+ const [cy, cp, cr] = est.cabeca;
283
+ const calca = escurecer(p.roupa, 0.35);
284
+ return (
285
+ <group>
286
+ {/* 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
+ ))}
293
+ <group position={[0, resp, 0]}>
294
+ {/* o tronco, com a faixa branca da camisa da referência */}
295
+ <Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
296
+ <Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
297
+ <Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
298
+ </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} />
301
+ <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} />
305
+ </group>
306
+ </group>
307
+ );
308
+ }
309
+
310
+ /* ── 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. */
314
+ function Mascote({ p, est }) {
315
+ const cor = p.cor || '#d97757';
316
+ 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;
319
+ const pisca = clamp(est.pisca);
320
+ 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} />;
322
+ 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" />)}
330
+ </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
+ </group>
333
+ );
334
+ }
335
+
336
+ /* ── O SET ─────────────────────────────────────────────────────────────── */
337
+
338
+ /* Um cartão de tela: o painel escuro com o ícone, o título do assunto, duas
339
+ linhas de texto, a barra de progresso e a bolinha verde de "rodando". É
340
+ desenhado num canvas 2D e vira textura que BRILHA (o bloom pega). */
341
+ function texturaCartao(titulo, linha, cor, acento, k) {
342
+ const cv = document.createElement('canvas');
343
+ cv.width = 512; cv.height = 320;
344
+ const g = cv.getContext('2d');
345
+ const r = 26;
346
+ g.fillStyle = cor; g.beginPath(); g.roundRect(6, 6, 500, 308, r); g.fill();
347
+ g.strokeStyle = 'rgba(160,175,255,0.35)'; g.lineWidth = 3; g.stroke();
348
+ g.fillStyle = acento; g.beginPath(); g.roundRect(34, 34, 44, 44, 8); g.fill();
349
+ g.fillStyle = '#eef1ff'; g.font = '600 34px Inter, "Helvetica Neue", Arial, sans-serif';
350
+ g.fillText(String(titulo).slice(0, 18), 96, 68);
351
+ g.fillStyle = '#3ddc84'; g.beginPath(); g.arc(462, 56, 10, 0, Math.PI * 2); g.fill();
352
+ g.fillStyle = 'rgba(205,212,255,0.75)'; g.font = '400 24px Inter, "Helvetica Neue", Arial, sans-serif';
353
+ g.fillText(String(linha).slice(0, 30), 36, 132);
354
+ g.fillStyle = 'rgba(205,212,255,0.28)';
355
+ g.beginPath(); g.roundRect(36, 160, 300 + hash(k) * 120, 16, 8); g.fill();
356
+ g.beginPath(); g.roundRect(36, 192, 200 + hash(k + 4) * 160, 16, 8); g.fill();
357
+ g.fillStyle = 'rgba(205,212,255,0.18)'; g.beginPath(); g.roundRect(36, 262, 440, 10, 5); g.fill();
358
+ g.fillStyle = '#8fa2ff'; g.beginPath(); g.roundRect(36, 262, 120 + hash(k + 9) * 300, 10, 5); g.fill();
359
+ const tx = new THREE.CanvasTexture(cv);
360
+ tx.colorSpace = THREE.SRGBColorSpace;
361
+ tx.anisotropy = 4;
362
+ return tx;
363
+ }
364
+
365
+ function ParedeDeTelas({ c, paineis, t }) {
366
+ const texturas = useMemo(() => {
367
+ const lista = paineis && paineis.length ? paineis : [{ titulo: 'Agora', linha: '' }];
368
+ return Array.from({ length: 16 }, (_, i) => {
369
+ const pn = lista[i % lista.length];
370
+ return texturaCartao(pn.titulo, pn.linha || '', c.tela, c.acento, i);
371
+ });
372
+ }, [paineis, c.tela, c.acento]);
373
+ // Duas fileiras de seis telas, num arco suave atrás do set.
374
+ const telas = [];
375
+ for (let fila = 0; fila < 2; fila++) {
376
+ for (let i = 0; i < 8; i++) {
377
+ const x = (i - 3.5) * 1.62;
378
+ const ang = -x * 0.07;
379
+ const z = -3.4 + Math.abs(x) * 0.12;
380
+ telas.push({ k: fila * 8 + i, x, y: 1.25 + fila * 1.08, z, ang });
381
+ }
382
+ }
383
+ return (
384
+ <group>
385
+ {/* a estrutura escura onde as telas moram */}
386
+ <Bloco s={[16, 3.2, 0.3]} p={[0, 1.78, -3.75]} c={c.parede} r={0.05} rough={0.9} sombra={false} />
387
+ {telas.map((s) => {
388
+ const vivo = 0.82 + 0.18 * Math.sin(t * 2 + s.k * 1.7);
389
+ return (
390
+ <group key={s.k} position={[s.x, s.y, s.z]} rotation={[0, s.ang, 0]}>
391
+ <Bloco s={[1.54, 0.98, 0.07]} c="#0a0c18" r={0.04} rough={0.4} sombra={false} />
392
+ <mesh position={[0, 0, 0.04]}>
393
+ <planeGeometry args={[1.46, 0.9]} />
394
+ <meshStandardMaterial map={texturas[s.k]} emissiveMap={texturas[s.k]} emissive="#ffffff" emissiveIntensity={0.55 * vivo} roughness={0.35} toneMapped />
395
+ </mesh>
396
+ </group>
397
+ );
398
+ })}
399
+ {/* a fileira de luzinhas em cima da parede, como na referência */}
400
+ {Array.from({ length: 9 }, (_, i) => (
401
+ <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} />
402
+ ))}
403
+ </group>
404
+ );
405
+ }
406
+
407
+ function ChaoDeLadrilhos({ c }) {
408
+ // Ladrilhos de 0,9 com junta escura: a grade que dá escala ao set.
409
+ const pecas = useMemo(() => {
410
+ const out = [];
411
+ 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)]);
412
+ return out;
413
+ }, []);
414
+ return (
415
+ <group>
416
+ <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.02, 0]} receiveShadow>
417
+ <planeGeometry args={[40, 40]} /><meshStandardMaterial color={escurecer(c.chao, 0.45)} roughness={0.9} />
418
+ </mesh>
419
+ {pecas.map(([x, z, k], i) => (
420
+ <mesh key={i} position={[x, -0.03, z]} geometry={geo(0.88, 0.1, 0.56, 0.035)} receiveShadow>
421
+ <meshStandardMaterial color={k > 0.5 ? c.chao : c.chao2} roughness={0.42} metalness={0.05} />
422
+ </mesh>
423
+ ))}
424
+ </group>
425
+ );
426
+ }
427
+
428
+ /* ── A CÂMERA ──────────────────────────────────────────────────────────── */
429
+ function lugarDe(el, i) {
430
+ const dois = el.length > 1;
431
+ return dois ? [(el[i].lado || 0) * 0.78, 0, 0] : [0, 0, 0];
432
+ }
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
+
436
+ function cameraDe(pl, t, part, aspecto) {
437
+ const vert = aspecto < 1;
438
+ const el = part.elenco;
439
+ const dois = el.length > 1;
440
+ const quem = el[pl.alvo] ? pl.alvo : 0;
441
+ const cabeca = (i) => { const p = lugarDe(el, i); return [p[0], alturaDaCabeca(el[i]), p[2]]; };
442
+ const frente = (i) => [Math.sin(giroDe(el, i)), 0, Math.cos(giroDe(el, i))];
443
+ const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
444
+ const fovK = vert ? 1.45 : 1;
445
+ let pos, alvo, fov;
446
+ if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
447
+ // O plano aberto de cima, como o da referência: o set inteiro e a parede de telas.
448
+ 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];
452
+ } else if (pl.tipo === 'ombro' && dois) {
453
+ const o = 1 - quem, h = cabeca(o), s = cabeca(quem);
454
+ const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
455
+ pos = [h[0] + (d[0] / n) * 0.9, h[1] + 0.25, h[2] + 1.6];
456
+ alvo = [s[0], s[1] - 0.1, s[2]]; fov = 32 * fovK;
457
+ } else {
458
+ /* O close da referência é de BAIXO e perto, com a lente aberta: o rosto
459
+ enorme e o resto derretendo no desfoque. */
460
+ const dist = (pl.tipo === 'detalhe' ? 1.25 : pl.tipo === 'close' ? 1.7 : 2.7) * (vert ? 1.25 : 1);
461
+ fov = (pl.tipo === 'detalhe' ? 30 : pl.tipo === 'close' ? 32 : 34) * fovK;
462
+ const h = cabeca(quem), f = frente(quem);
463
+ const lado = pl.lado * 0.42;
464
+ const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
465
+ const baixo = pl.tipo === 'medio' ? 0.05 : -0.32;
466
+ pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
467
+ alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
468
+ }
469
+ const m = suave(k);
470
+ if (pl.mov === 'entra' || pl.mov === 'sai') {
471
+ const s = pl.mov === 'entra' ? m : 1 - m;
472
+ pos = pos.map((v, i) => lerp(v, alvo[i], 0.12 * s));
473
+ } else if (pl.mov === 'lateral') {
474
+ pos = [pos[0] + (m - 0.5) * 0.45 * pl.lado, pos[1], pos[2]];
475
+ }
476
+ 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];
477
+ pos = pos.map((v, i) => v + (mao[i] || 0));
478
+ alvo = alvo.map((v, i) => v + (mao[i] || 0) * 0.5);
479
+ /* O FOCO é no ROSTO, não no centro da cabeça: a cara fica 0,3 m mais perto
480
+ da câmera que o alvo. No plano aberto, no meio do set. A abertura é
481
+ pequena (a lente de perto já borra muito por metro), e o teto do
482
+ desfoque é que dá o fundo derretido da referência. */
483
+ const aoAlvo = Math.hypot(pos[0] - alvo[0], pos[1] - alvo[1], pos[2] - alvo[2]);
484
+ const deRosto = pl.tipo !== 'aberto' && pl.tipo !== 'largo';
485
+ const foco = deRosto ? Math.max(0.4, aoAlvo - 0.28) : aoAlvo;
486
+ const perto = pl.tipo === 'detalhe' || pl.tipo === 'close';
487
+ 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 };
488
+ }
489
+
490
+ /* ── POR CIMA: a legenda da referência ─────────────────────────────────── */
491
+ const FONTE = "'Inter','SF Pro Display','Helvetica Neue',Arial,sans-serif";
492
+
493
+ function pedacos(palavras) {
494
+ const out = [];
495
+ let cur = [];
496
+ for (const w of palavras) {
497
+ cur.push(w);
498
+ 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 = []; }
500
+ }
501
+ if (cur.length) out.push(cur);
502
+ return out;
503
+ }
504
+
505
+ function Legenda({ part, t, W, H }) {
506
+ const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.35);
507
+ if (!f) return null;
508
+ const grupos = pedacos(f.palavras);
509
+ const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.2) || grupos[grupos.length - 1];
510
+ const entra = suave((t - (g[0].de - 0.08)) / 0.18);
511
+ const sai = 1 - suave((t - (f.ate + 0.15)) / 0.2);
512
+ const tam = Math.round(H > W ? W * 0.052 : H * 0.05);
513
+ const texto = g.map((w) => w.t).join(' ');
514
+ 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)`,
518
+ textShadow: '0 2px 12px rgba(0,0,0,0.55), 0 1px 2px rgba(0,0,0,0.6)' }}>{texto}</div>
519
+ </div>
520
+ );
521
+ }
522
+
523
+ function Destaque({ part, t, W, H, acento }) {
524
+ const f = part.falas.find((x) => x.destaque && t >= x.de + 0.3 && t <= Math.min(x.ate + 0.3, x.de + 3));
525
+ if (!f) return null;
526
+ const k = t - (f.de + 0.3);
527
+ const fim = Math.min(f.ate + 0.3, f.de + 3) - (f.de + 0.3);
528
+ 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);
530
+ return (
531
+ <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,
533
+ 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
+ </div>
535
+ );
536
+ }
537
+
538
+ /* ── A PEÇA ────────────────────────────────────────────────────────────── */
539
+ export function VoxelCena({ el, t }) {
540
+ const part = el.partitura || {};
541
+ const W = el.largura || part.largura || 1080, H = el.altura || part.altura || 1920;
542
+ const c = { parede: '#07061a', chao: '#4338ca', chao2: '#3b32b8', acento: '#ff7a45', luz: '#ffd2b0', tela: '#101734', recorte: '#8b7bff', ...(part.cenario || {}) };
543
+ const elenco = part.elenco || [];
544
+ const falas = part.falas || [];
545
+ const planos = part.planos || [{ de: 0, ate: part.duracao || 10, tipo: 'aberto', alvo: 0, mov: 'fixo', lado: 1 }];
546
+ const dois = elenco.length > 1;
547
+ const pl = planos.find((x) => t >= x.de && t < x.ate) || planos[planos.length - 1];
548
+ const cam = cameraDe(pl, t, part, W / H);
549
+
550
+ const falandoAgora = (i) => falas.some((f) => f.quem === i && t >= f.de && t <= f.ate);
551
+ const emocaoDe = (i) => {
552
+ let e = 'neutro';
553
+ for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
554
+ return e;
555
+ };
556
+ const estado = (i) => {
557
+ const fala = falandoAgora(i);
558
+ const outro = dois && falandoAgora(1 - i);
559
+ const enfase = gesto(falas, i, t);
560
+ 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);
562
+ const e = EMO[emo] || EMO.neutro;
563
+ const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.6;
564
+ // 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;
566
+ 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],
571
+ };
572
+ };
573
+ const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);
574
+
575
+ return (
576
+ <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden', background: '#000' }}>
577
+ <div style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
578
+ <ThreeCanvas width={W} height={H} shadows camera={{ position: [0, 2, 6], fov: 36 }}
579
+ gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.0 }}>
580
+ <Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
581
+ <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} />
594
+ {elenco.map((p, i) => (
595
+ <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)} />}
597
+ </group>
598
+ ))}
599
+ <Lente W={W} H={H} foco={cam.foco} abertura={cam.abertura} desfoqueMax={cam.desfoqueMax} brilho={0.5} />
600
+ </ThreeCanvas>
601
+ <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
+ <Destaque part={part} t={t} W={W} H={H} acento={c.acento} />
603
+ {part.legenda !== false && <Legenda part={part} t={t} W={W} H={H} />}
604
+ </div>
605
+ </div>
606
+ );
607
+ }
608
+
609
+ export const VOXEL = { voxel: VoxelCena };
@@ -61,9 +61,13 @@ const PECAS = {
61
61
  fluido: { precisa: ['partitura'] },
62
62
  // O vídeo gravado em 3D (kit/personagem.jsx): o set, os personagens e a câmera (personagem.js).
63
63
  personagem: { precisa: ['partitura'] },
64
+ // O mesmo vídeo gravado, no visual de blocos da referência (kit/voxel.jsx): o padrão desde a 10.5.
65
+ voxel: { precisa: ['partitura'] },
66
+ // A animação pronta da biblioteca (kit/animacao.jsx, colecao.js): o Lottie tocado quadro a quadro.
67
+ animacao: { precisa: ['src'] },
64
68
  };
65
69
 
66
- const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'personagem.jsx', 'index.jsx'];
70
+ const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'personagem.jsx', 'voxel.jsx', 'animacao.jsx', 'index.jsx'];
67
71
 
68
72
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
69
73
  function arquivos() {