primocode 10.3.0-beta.0 → 10.4.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,582 @@
1
+ /* kit/personagem.jsx — o vídeo GRAVADO em 3D: o set, os personagens e a câmera.
2
+ *
3
+ * {"tipo":"kit","peca":"personagem","partitura":{…},"largura":1920,"altura":1080}
4
+ *
5
+ * A partitura vem pronta do lib/estudio/personagem.js (quem fala quando, os
6
+ * planos, a aparência, o cenário). Aqui só se desenha, quadro a quadro:
7
+ * - a boca segue a voz LETRA POR LETRA (a vogal abre, o "m" fecha, o "o" arredonda);
8
+ * - piscada, sobrancelha, sorriso e cabeça que vira para quem fala;
9
+ * - gesto de mão nas palavras fortes;
10
+ * - a câmera corta entre aberto, ombro, close e detalhe, com o balanço de mão;
11
+ * - por cima: legenda grossa palavra por palavra, o nome de quem fala, o
12
+ * cartão de destaque, o REC e o grão de filme.
13
+ * Tudo é geometria do three.js: nada é baixado. */
14
+ import React, { useMemo } from 'react';
15
+ import { ThreeCanvas } from '@remotion/three';
16
+ import * as THREE from 'three';
17
+ import { useThree } from '@react-three/fiber';
18
+ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
19
+
20
+ const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
21
+ const lerp = (a, b, k) => a + (b - a) * k;
22
+ const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
23
+ const mola = (x) => { const k = clamp(x); return 1 - Math.pow(1 - k, 3) * Math.cos(k * 4.2); };
24
+ const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
25
+
26
+ function Ambiente() {
27
+ const { gl, scene } = useThree();
28
+ useMemo(() => {
29
+ const pm = new THREE.PMREMGenerator(gl);
30
+ scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
31
+ scene.environmentIntensity = 0.35;
32
+ pm.dispose();
33
+ }, [gl, scene]);
34
+ return null;
35
+ }
36
+
37
+ function Camera({ pos, alvo, fov }) {
38
+ const { camera } = useThree();
39
+ camera.position.set(pos[0], pos[1], pos[2]);
40
+ camera.fov = fov;
41
+ camera.near = 0.05;
42
+ camera.far = 60;
43
+ camera.lookAt(alvo[0], alvo[1], alvo[2]);
44
+ camera.updateProjectionMatrix();
45
+ return null;
46
+ }
47
+
48
+ const Mat = ({ c, r = 0.6, m = 0, e, ei = 1, sheen, cc = 0 }) => (
49
+ <meshPhysicalMaterial color={c} roughness={r} metalness={m} clearcoat={cc} clearcoatRoughness={0.3}
50
+ {...(sheen ? { sheen: 0.6, sheenColor: sheen, sheenRoughness: 0.5 } : {})}
51
+ {...(e ? { emissive: e, emissiveIntensity: ei } : {})} />
52
+ );
53
+
54
+ /* ── A BOCA ───────────────────────────────────────────────────────────────
55
+ Cada letra vira um formato: a = abre (quanto) e w = largura (o "i" estica,
56
+ o "o" e o "u" arredondam, m/b/p fecham). Dentro da letra a boca respira
57
+ (fecha um pouco entre uma e outra), e três amostras seguidas suavizam. */
58
+ const VISEMA = (ch) => {
59
+ if (/[aáàãâ]/.test(ch)) return [1, 0.6];
60
+ if (/[eéê]/.test(ch)) return [0.6, 0.85];
61
+ if (/[iíy]/.test(ch)) return [0.32, 1];
62
+ if (/[oóôõ]/.test(ch)) return [0.78, 0.3];
63
+ if (/[uúw]/.test(ch)) return [0.45, 0.15];
64
+ if (/[mbp]/.test(ch)) return [0, 0.5];
65
+ if (/[fv]/.test(ch)) return [0.12, 0.62];
66
+ if (/[lnrdt]/.test(ch)) return [0.3, 0.6];
67
+ return [0.22, 0.55];
68
+ };
69
+
70
+ function bocaEm(falas, quem, t) {
71
+ for (const f of falas) {
72
+ if (f.quem !== quem || t < f.de - 0.05 || t > f.ate + 0.05) continue;
73
+ const w = f.palavras.find((x) => t >= x.de && t <= x.ate);
74
+ if (!w) return [0, 0.55];
75
+ const letras = String(w.t).toLowerCase().replace(/[^a-záàãâéêíóôõúçy]/g, '') || 'a';
76
+ const p = clamp((t - w.de) / Math.max(0.05, w.ate - w.de), 0, 0.999);
77
+ const pos = p * letras.length;
78
+ const [a, l] = VISEMA(letras[Math.floor(pos)]);
79
+ return [a * (0.62 + 0.38 * Math.sin(Math.PI * (pos % 1))), l];
80
+ }
81
+ return [0, 0.55];
82
+ }
83
+
84
+ function boca(falas, quem, t) {
85
+ const amostras = [0, 1 / 60, 1 / 30].map((d) => bocaEm(falas, quem, t - d));
86
+ return [amostras.reduce((s, x) => s + x[0], 0) / 3, amostras.reduce((s, x) => s + x[1], 0) / 3];
87
+ }
88
+
89
+ // O gesto: uma onda em cada palavra comprida (as fortes), alternando a mão.
90
+ function gesto(falas, quem, t) {
91
+ let esq = 0, dir = 0;
92
+ for (const f of falas) {
93
+ if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
94
+ f.palavras.forEach((w, k) => {
95
+ if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
96
+ const x = (t - (w.de - 0.15)) / 0.9;
97
+ if (x < 0 || x > 1) return;
98
+ const b = Math.sin(Math.PI * x);
99
+ if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
100
+ });
101
+ }
102
+ return [esq, dir];
103
+ }
104
+
105
+ function piscada(quem, t) {
106
+ // A cada 2,5 a 5 s, uma piscada de 0,16 s (às vezes dupla).
107
+ let base = 0.6 + quem * 1.3, k = 0;
108
+ while (base < t + 1) {
109
+ const d = t - base;
110
+ if (d >= 0 && d < 0.16) return Math.sin((d / 0.16) * Math.PI);
111
+ if (hash(quem * 31 + k) < 0.2) { const d2 = t - (base + 0.3); if (d2 >= 0 && d2 < 0.16) return Math.sin((d2 / 0.16) * Math.PI); }
112
+ base += 2.5 + hash(quem * 17 + k) * 2.5;
113
+ k++;
114
+ }
115
+ return 0;
116
+ }
117
+
118
+ const EMO = {
119
+ neutro: { sobr: 0, sorri: 0.15, palp: 0 },
120
+ feliz: { sobr: 0.25, sorri: 0.9, palp: 0.1 },
121
+ animado: { sobr: 0.55, sorri: 0.7, palp: 0 },
122
+ serio: { sobr: -0.35, sorri: 0, palp: 0.3 },
123
+ surpreso: { sobr: 0.9, sorri: 0, palp: -0.2 },
124
+ pensativo: { sobr: -0.1, sorri: 0.05, palp: 0.35, inclina: 0.12 },
125
+ curioso: { sobr: 0.45, sorri: 0.3, palp: 0, inclina: 0.1 },
126
+ };
127
+
128
+ /* ── A PESSOA ────────────────────────────────────────────────────────────── */
129
+ function Cabelo({ estilo, cor }) {
130
+ const m = <Mat c={cor} r={0.85} sheen="#ffffff" />;
131
+ const cap = (
132
+ <mesh position={[0, 0.03, -0.02]} scale={[0.97, 1.02, 1.0]} rotation={[-0.3, 0, 0]}>
133
+ <sphereGeometry args={[0.445, 48, 32, 0, Math.PI * 2, 0, Math.PI * 0.45]} />{m}
134
+ </mesh>
135
+ );
136
+ if (estilo === 'careca') return null;
137
+ if (estilo === 'cacheado') {
138
+ const pts = [];
139
+ for (let i = 0; i < 150; i++) {
140
+ const y = 1 - (i / 149) * 2, r = Math.sqrt(Math.max(0, 1 - y * y)), a = i * 2.39996;
141
+ const z = Math.sin(a) * r;
142
+ if (y < 0.38 + 0.36 * z) continue; // a testa e o rosto ficam livres; atrás desce mais
143
+ pts.push([Math.cos(a) * r * 0.44, y * 0.44 + 0.03, z * 0.44 - 0.02]);
144
+ }
145
+ return <group>{cap}{pts.map((p, i) => <mesh key={i} position={p}><sphereGeometry args={[0.068 + hash(i) * 0.022, 14, 10]} />{m}</mesh>)}</group>;
146
+ }
147
+ return (
148
+ <group>
149
+ {cap}
150
+ {estilo === 'topete' && <mesh position={[0.02, 0.4, 0.2]} rotation={[0.5, 0, 0.2]} scale={[1.5, 0.7, 1.1]}><sphereGeometry args={[0.15, 24, 16]} />{m}</mesh>}
151
+ {estilo === 'coque' && <mesh position={[0, 0.42, -0.22]}><sphereGeometry args={[0.15, 24, 16]} />{m}</mesh>}
152
+ {estilo === 'longo' && (
153
+ <>
154
+ <mesh position={[0, -0.28, -0.2]} scale={[1.05, 1, 0.55]}><capsuleGeometry args={[0.38, 0.55, 8, 24]} />{m}</mesh>
155
+ {[-1, 1].map((s) => <mesh key={s} position={[s * 0.38, -0.12, 0.06]} scale={[0.5, 1, 0.6]}><capsuleGeometry args={[0.12, 0.45, 8, 16]} />{m}</mesh>)}
156
+ </>
157
+ )}
158
+ </group>
159
+ );
160
+ }
161
+
162
+ function Olho({ x, olhar, pisca, palp, corIris, pele }) {
163
+ // A pálpebra: meia esfera que gira por cima do olho (aberta = -0,15; fechada = 1,5).
164
+ const a = lerp(-0.5 + palp * 0.7, 1.5, clamp(pisca));
165
+ return (
166
+ <group position={[x, 0.08, 0.335]}>
167
+ <mesh><sphereGeometry args={[0.083, 32, 24]} /><Mat c="#f7f5f2" r={0.15} cc={1} /></mesh>
168
+ <group rotation={[olhar[1], olhar[0], 0]}>
169
+ <mesh position={[0, 0, 0.058]}><sphereGeometry args={[0.042, 24, 16]} /><Mat c={corIris} r={0.2} cc={1} /></mesh>
170
+ <mesh position={[0, 0, 0.083]}><sphereGeometry args={[0.022, 16, 12]} /><Mat c="#050505" r={0.1} /></mesh>
171
+ <mesh position={[0.018, 0.02, 0.098]}><sphereGeometry args={[0.009, 8, 8]} /><meshBasicMaterial color="#ffffff" /></mesh>
172
+ </group>
173
+ <mesh rotation={[a, 0, 0]}><sphereGeometry args={[0.091, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2]} /><Mat c={pele} r={0.6} sheen="#ff9a8a" /></mesh>
174
+ </group>
175
+ );
176
+ }
177
+
178
+ function Rosto({ p, est }) {
179
+ const pele = p.pele;
180
+ const labio = new THREE.Color(pele).lerp(new THREE.Color('#8a3b3b'), 0.35).getStyle();
181
+ const e = EMO[est.emocao] || EMO.neutro;
182
+ const sobr = e.sobr + est.enfase * 0.35;
183
+ const [abre, larg] = est.boca;
184
+ const sorri = e.sorri * (1 - abre * 0.6);
185
+ const corIris = new THREE.Color(p.corCabelo).lerp(new THREE.Color('#5a3a22'), 0.5).getStyle();
186
+ const olho = (x) => <Olho key={x} x={x} olhar={est.olhar} pisca={est.pisca} palp={e.palp} corIris={corIris} pele={pele} />;
187
+ return (
188
+ <group>
189
+ <mesh scale={[0.92, 1.02, 0.95]}><sphereGeometry args={[0.42, 64, 48]} /><Mat c={pele} r={0.55} sheen="#ff9a8a" /></mesh>
190
+ {[-1, 1].map((s) => <mesh key={s} position={[s * 0.385, -0.01, -0.02]} scale={[0.45, 1, 0.75]}><sphereGeometry args={[0.09, 24, 16]} /><Mat c={pele} r={0.6} sheen="#ff9a8a" /></mesh>)}
191
+ <mesh position={[0, -0.04, 0.4]} scale={[0.85, 1.1, 1]}><sphereGeometry args={[0.062, 24, 16]} /><Mat c={pele} r={0.5} sheen="#ff9a8a" /></mesh>
192
+ {olho(-0.14)}{olho(0.14)}
193
+ {[-1, 1].map((s) => (
194
+ <mesh key={s} position={[s * 0.145, 0.205 + sobr * 0.035 + (s > 0 && e.inclina ? e.inclina * 0.1 : 0), 0.37]}
195
+ rotation={[0, 0, Math.PI / 2 + s * (-0.12 + (est.emocao === 'serio' ? 0.25 : 0) - sobr * 0.12)]}>
196
+ <capsuleGeometry args={[0.019, 0.1, 6, 12]} /><Mat c={p.corCabelo} r={0.9} />
197
+ </mesh>
198
+ ))}
199
+ <group position={[0, -0.175, 0.37]} rotation={[0.42, 0, 0]}>
200
+ <mesh scale={[0.06 + 0.035 * larg, 0.008 + 0.06 * abre, 1]}><circleGeometry args={[1, 32]} /><meshBasicMaterial color="#2a0d10" /></mesh>
201
+ {abre > 0.25 && <mesh position={[0, 0.025 + 0.02 * abre, 0.002]} scale={[0.05 + 0.025 * larg, 0.012, 1]}><circleGeometry args={[1, 24]} /><meshBasicMaterial color="#f3efe9" /></mesh>}
202
+ <mesh scale={[0.062 + 0.036 * larg, 0.012 + 0.062 * abre, 0.04]}><torusGeometry args={[1, 0.15, 10, 40]} /><Mat c={labio} r={0.45} /></mesh>
203
+ {sorri > 0.2 && (
204
+ <mesh position={[0, 0.01, 0.004]} rotation={[0, 0, Math.PI]} scale={[0.09, 0.04 * sorri, 0.04]}>
205
+ <torusGeometry args={[1, 0.16, 8, 32, Math.PI]} /><Mat c={labio} r={0.45} />
206
+ </mesh>
207
+ )}
208
+ </group>
209
+ {p.barba && <mesh scale={[0.95, 1.03, 0.97]}><sphereGeometry args={[0.43, 48, 24, 0, Math.PI * 2, Math.PI * 0.69, Math.PI * 0.31]} /><Mat c={p.corCabelo} r={0.95} /></mesh>}
210
+ {p.oculos && (
211
+ <group position={[0, 0.08, 0.425]}>
212
+ {[-1, 1].map((s) => <mesh key={s} position={[s * 0.14, 0, 0]}><torusGeometry args={[0.095, 0.011, 10, 40]} /><Mat c="#15161a" r={0.3} m={0.5} /></mesh>)}
213
+ <mesh rotation={[0, 0, Math.PI / 2]}><cylinderGeometry args={[0.008, 0.008, 0.09, 8]} /><Mat c="#15161a" r={0.3} m={0.5} /></mesh>
214
+ </group>
215
+ )}
216
+ <group position={[0, 0, 0]}><Cabelo estilo={p.cabelo} cor={p.corCabelo} /></group>
217
+ </group>
218
+ );
219
+ }
220
+
221
+ /* ── O ROBÔ ──────────────────────────────────────────────────────────────
222
+ Cabeça branca, o visor escuro de vidro (uma segunda elipse que fura a
223
+ frente), os olhos acesos que piscam e a boca de barras que dança com a voz. */
224
+ function RostoRobo({ p, est, acento }) {
225
+ const [abre] = est.boca;
226
+ const e = EMO[est.emocao] || EMO.neutro;
227
+ const olhoY = 1 - clamp(est.pisca) * 0.88;
228
+ const feliz = e.sorri > 0.5 && abre < 0.2;
229
+ return (
230
+ <group>
231
+ <mesh scale={[1.1, 0.9, 0.95]}><sphereGeometry args={[0.42, 64, 48]} /><Mat c={p.cor} r={0.25} m={0.1} cc={1} /></mesh>
232
+ <mesh position={[0, 0, 0.04]} scale={[1.0, 0.75, 0.95]}><sphereGeometry args={[0.4, 64, 48]} /><Mat c="#07090d" r={0.08} m={0.4} cc={1} /></mesh>
233
+ {[-1, 1].map((s) => (
234
+ <group key={s} position={[s * 0.13, 0.05 + e.sobr * 0.02, 0.405]} rotation={[0, s * 0.18, 0]}>
235
+ <mesh scale={[1, olhoY * (feliz ? 0.55 : 1), 1]} rotation={[0, 0, Math.PI / 2 * (feliz ? 1 : 0)]}>
236
+ <capsuleGeometry args={[0.038, feliz ? 0.07 : 0.05, 6, 16]} /><meshBasicMaterial color={acento} />
237
+ </mesh>
238
+ </group>
239
+ ))}
240
+ <group position={[0, -0.1, 0.392]}>
241
+ {[-2, -1, 0, 1, 2].map((k) => {
242
+ const h = 0.012 + abre * 0.085 * (1 - Math.abs(k) * 0.18) * (0.75 + 0.25 * Math.sin(est.t * 23 + k * 1.7));
243
+ return <mesh key={k} position={[k * 0.034, 0, -Math.abs(k) * 0.006]}><boxGeometry args={[0.02, h, 0.02]} /><meshBasicMaterial color={acento} /></mesh>;
244
+ })}
245
+ </group>
246
+ {[-1, 1].map((s) => (
247
+ <group key={s} position={[s * 0.47, 0, 0]} rotation={[0, 0, Math.PI / 2]}>
248
+ <mesh><cylinderGeometry args={[0.11, 0.11, 0.07, 32]} /><Mat c={p.cor} r={0.3} m={0.2} cc={1} /></mesh>
249
+ <mesh position={[0, s * 0.037, 0]}><torusGeometry args={[0.075, 0.012, 8, 32]} /><meshBasicMaterial color={acento} /></mesh>
250
+ </group>
251
+ ))}
252
+ <mesh position={[0, 0.42, 0]}><cylinderGeometry args={[0.012, 0.012, 0.16, 8]} /><Mat c="#9aa0aa" r={0.3} m={0.8} /></mesh>
253
+ <mesh position={[0, 0.51, 0]}><sphereGeometry args={[0.035, 16, 12]} /><Mat c={acento} e={acento} ei={1 + (est.falando ? 1.5 + Math.sin(est.t * 9) : 0)} /></mesh>
254
+ </group>
255
+ );
256
+ }
257
+
258
+ /* ── O CORPO, OS BRAÇOS E O PERSONAGEM INTEIRO ───────────────────────────── */
259
+ function Braco({ lado, g, cor, pele, robo, t }) {
260
+ // Repouso: o antebraço deitado na mesa. Gesto: a mão sobe e mexe um pouco.
261
+ const ombroX = lerp(-0.3, -0.62, g) + Math.sin(t * 6 + lado) * 0.05 * g;
262
+ const ombroZ = lado * lerp(0.08, 0.28, g);
263
+ const cotovelo = lerp(-1.17, -1.75, g) + Math.sin(t * 7.3 + lado * 2) * 0.12 * g;
264
+ const pulso = Math.sin(t * 9 + lado) * 0.25 * g;
265
+ const tecido = robo ? <Mat c={cor} r={0.3} m={0.1} cc={1} /> : <Mat c={cor} r={0.85} sheen="#ffffff" />;
266
+ return (
267
+ <group position={[lado * 0.44, 1.18, 0]} rotation={[ombroX, 0, ombroZ]}>
268
+ <mesh position={[0, -0.16, 0]}><capsuleGeometry args={[0.095, 0.24, 8, 16]} />{tecido}</mesh>
269
+ <group position={[0, -0.32, 0]} rotation={[cotovelo, 0, 0]}>
270
+ <mesh position={[0, -0.15, 0]}><capsuleGeometry args={[0.082, 0.22, 8, 16]} />{tecido}</mesh>
271
+ <group position={[0, -0.33, 0]} rotation={[pulso, 0, 0]}>
272
+ <mesh scale={[1, 1.15, 0.7]}><sphereGeometry args={[0.083, 20, 14]} />{robo ? <Mat c="#2a2d35" r={0.35} m={0.6} /> : <Mat c={pele} r={0.55} sheen="#ff9a8a" />}</mesh>
273
+ </group>
274
+ </group>
275
+ </group>
276
+ );
277
+ }
278
+
279
+ function Personagem({ p, est, acento, pos, giro }) {
280
+ const robo = p.tipo === 'robo';
281
+ const corpo = robo ? p.cor : p.roupa;
282
+ const resp = 1 + Math.sin(est.t * 1.6 + pos[0]) * 0.012;
283
+ const [cy, cp, cr] = est.cabeca;
284
+ return (
285
+ <group position={pos} rotation={[0, giro, 0]}>
286
+ <group scale={[1, resp, 1]}>
287
+ <mesh position={[0, 0.92, 0]} scale={[1.18, 1, 0.78]} castShadow>
288
+ <capsuleGeometry args={[0.34, 0.42, 12, 32]} />
289
+ {robo ? <Mat c={corpo} r={0.28} m={0.1} cc={1} /> : <Mat c={corpo} r={0.85} sheen="#ffffff" />}
290
+ </mesh>
291
+ {robo
292
+ ? <mesh position={[0, 1.0, 0.265]}><circleGeometry args={[0.07, 32]} /><meshBasicMaterial color={acento} /></mesh>
293
+ : <mesh position={[0, 1.27, 0]} rotation={[Math.PI / 2, 0, 0]}><torusGeometry args={[0.15, 0.04, 12, 32]} /><Mat c={new THREE.Color(corpo).multiplyScalar(0.7).getStyle()} r={0.85} /></mesh>}
294
+ <mesh position={[0, 1.3, 0]}><cylinderGeometry args={[0.12, 0.13, 0.25, 24]} />{robo ? <Mat c="#3a3e48" r={0.4} m={0.6} /> : <Mat c={p.pele} r={0.6} sheen="#ff9a8a" />}</mesh>
295
+ </group>
296
+ <Braco lado={-1} g={est.gesto[0]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
297
+ <Braco lado={1} g={est.gesto[1]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
298
+ <group position={[0, 1.62, 0]} rotation={[cp, cy, cr]} castShadow>
299
+ {robo ? <RostoRobo p={p} est={est} acento={acento} /> : <Rosto p={p} est={est} />}
300
+ </group>
301
+ </group>
302
+ );
303
+ }
304
+
305
+ /* ── O SET ───────────────────────────────────────────────────────────────── */
306
+ function Palco({ c, dois, t }) {
307
+ const parede = -2.6;
308
+ const paineis = [-4.2, -2.8, -1.4, 0, 1.4, 2.8, 4.2];
309
+ return (
310
+ <group>
311
+ <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow><planeGeometry args={[40, 40]} /><Mat c={c.chao} r={0.55} /></mesh>
312
+ <mesh position={[0, 4, parede]} receiveShadow><planeGeometry args={[40, 10]} /><Mat c={c.parede} r={0.9} /></mesh>
313
+ {paineis.map((x, i) => (
314
+ <group key={i}>
315
+ <mesh position={[x, 1.9, parede + 0.06]} receiveShadow><boxGeometry args={[1.2, 3.2, 0.1]} /><Mat c={c.parede2} r={0.95} /></mesh>
316
+ <mesh position={[x + 0.66, 1.9, parede + 0.08]}><boxGeometry args={[0.03, 3.2, 0.03]} /><meshBasicMaterial color={c.acento} /></mesh>
317
+ </group>
318
+ ))}
319
+ {/* o neon da parede: um anel e um traço na cor de acento, piscando de leve */}
320
+ <mesh position={[1.7, 2.55, parede + 0.14]}><torusGeometry args={[0.32, 0.025, 12, 64]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
321
+ <mesh position={[1.7, 2.55, parede + 0.14]}><circleGeometry args={[0.06, 24]} /><meshBasicMaterial color={hash(Math.floor(t * 1.5)) > 0.1 ? '#ff3b3b' : '#551515'} toneMapped={false} /></mesh>
322
+ <mesh position={[-1.9, 2.75, parede + 0.14]}><boxGeometry args={[1.3, 0.04, 0.03]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
323
+ {/* a estante com livros e a planta */}
324
+ <group position={[-3.0, 0, parede + 0.45]}>
325
+ {[0.9, 1.6].map((y) => <mesh key={y} position={[0, y, 0]} castShadow receiveShadow><boxGeometry args={[1.6, 0.05, 0.4]} /><Mat c="#5b4632" r={0.7} /></mesh>)}
326
+ {[0, 1, 2, 3, 4, 5].map((k) => <mesh key={k} position={[-0.6 + k * 0.17, 1.05 + hash(k) * 0.04, 0]} castShadow><boxGeometry args={[0.12, 0.26 + hash(k + 9) * 0.12, 0.3]} /><Mat c={['#c94f3d', '#2f6db5', '#e8c35a', '#3d8a5f', '#ece6da', '#4a3d6e'][k]} r={0.8} /></mesh>)}
327
+ </group>
328
+ <group position={[3.1, 0, parede + 0.6]}>
329
+ <mesh position={[0, 0.3, 0]} castShadow><cylinderGeometry args={[0.28, 0.22, 0.6, 32]} /><Mat c="#d8d2c8" r={0.6} /></mesh>
330
+ {Array.from({ length: 9 }, (_, k) => {
331
+ const a = k * 2.4, h = 0.75 + hash(k) * 0.7;
332
+ return <mesh key={k} position={[Math.cos(a) * 0.18, h, Math.sin(a) * 0.18]} rotation={[Math.cos(a) * 0.4, 0, Math.sin(a) * 0.4]} castShadow><sphereGeometry args={[0.2 + hash(k + 3) * 0.08, 16, 12]} /><Mat c={k % 2 ? '#2f7a43' : '#3f9a55'} r={0.7} /></mesh>;
333
+ })}
334
+ </group>
335
+ {/* a luminária de chão, acesa */}
336
+ <group position={[-3.6, 0, 0.2]}>
337
+ <mesh position={[0, 0.9, 0]}><cylinderGeometry args={[0.02, 0.02, 1.8, 8]} /><Mat c="#222" r={0.4} m={0.6} /></mesh>
338
+ <mesh position={[0, 1.85, 0]}><sphereGeometry args={[0.18, 24, 16]} /><meshBasicMaterial color={c.luz} toneMapped={false} /></mesh>
339
+ </group>
340
+ {/* a mesa */}
341
+ <group position={[0, 0, 0.42]}>
342
+ <mesh position={[0, 0.76, 0]} scale={[dois ? 1.75 : 1.1, 1, 0.62]} castShadow receiveShadow><cylinderGeometry args={[1, 1, 0.05, 64]} /><Mat c="#3b2a1f" r={0.35} cc={0.6} /></mesh>
343
+ <mesh position={[0, 0.38, -0.05]} scale={[dois ? 1.5 : 0.92, 1, 0.5]} castShadow receiveShadow><cylinderGeometry args={[1, 1, 0.76, 64]} /><Mat c="#16171b" r={0.5} m={0.2} /></mesh>
344
+ <mesh position={[0, 0.05, -0.05]} scale={[dois ? 1.51 : 0.93, 1, 0.51]}><cylinderGeometry args={[1, 1, 0.02, 64, 1, true]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
345
+ {/* a xícara e o papel: a mesa de quem está gravando */}
346
+ <mesh position={[dois ? 0.15 : 0.55, 0.85, 0.22]} castShadow><cylinderGeometry args={[0.055, 0.045, 0.13, 24]} /><Mat c="#ece6da" r={0.3} cc={1} /></mesh>
347
+ <mesh position={[dois ? -0.2 : -0.5, 0.79, 0.25]} rotation={[-Math.PI / 2, 0, 0.3]}><planeGeometry args={[0.3, 0.4]} /><Mat c="#f4f1ea" r={0.9} /></mesh>
348
+ </group>
349
+ </group>
350
+ );
351
+ }
352
+
353
+ function Microfone({ x, z, giro }) {
354
+ return (
355
+ <group position={[x, 0, z]} rotation={[0, giro, 0]}>
356
+ <mesh position={[0.26, 0.8, 0.62]}><cylinderGeometry args={[0.07, 0.08, 0.02, 24]} /><Mat c="#1b1c1f" r={0.3} m={0.8} /></mesh>
357
+ <mesh position={[0.26, 1.0, 0.62]}><cylinderGeometry args={[0.01, 0.01, 0.4, 8]} /><Mat c="#1b1c1f" r={0.3} m={0.8} /></mesh>
358
+ <mesh position={[0.22, 1.24, 0.57]} rotation={[-0.9, 0, 0.35]} castShadow><capsuleGeometry args={[0.042, 0.11, 8, 16]} /><Mat c="#24262b" r={0.35} m={0.7} /></mesh>
359
+ </group>
360
+ );
361
+ }
362
+
363
+ /* ── A CÂMERA ────────────────────────────────────────────────────────────── */
364
+ function cameraDe(pl, t, part, aspecto) {
365
+ const vert = aspecto < 1;
366
+ const el = part.elenco;
367
+ const dois = el.length > 1;
368
+ const quem = el[pl.alvo] ? pl.alvo : 0;
369
+ const lugar = (i) => (dois ? [(el[i].lado || 0) * 0.95, 0, 0] : [0, 0, 0]);
370
+ const giroDe = (i) => (dois ? (el[i].lado < 0 ? 0.95 : -0.95) : 0);
371
+ const cabeca = (i) => { const p = lugar(i); return [p[0], 1.6, p[2]]; };
372
+ const frente = (i) => [Math.sin(giroDe(i)), 0, Math.cos(giroDe(i))];
373
+ const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
374
+ const fovK = vert ? 1.5 : 1;
375
+ let pos, alvo, fov;
376
+ if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
377
+ fov = (pl.tipo === 'largo' ? 40 : 36) * fovK;
378
+ const dist = Math.max(pl.tipo === 'largo' ? 5.4 : 4.4, 1.9 / (Math.tan((fov * Math.PI) / 360) * aspecto));
379
+ pos = [0, 1.75, dist]; alvo = [0, 1.25, 0];
380
+ } else if (pl.tipo === 'ombro' && dois) {
381
+ // Atrás do ombro de quem ouve, afastada para o lado da câmera: a nuca dele fica na borda do quadro.
382
+ // Na linha de quem fala até quem ouve, 1,25 m além dele e 1 m para o lado da câmera: só a borda dele entra no quadro.
383
+ const o = 1 - quem, h = cabeca(o), s = cabeca(quem);
384
+ const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
385
+ const lat = [-d[1] / n, d[0] / n];
386
+ const latZ = lat[1] >= 0 ? lat : [-lat[0], -lat[1]];
387
+ pos = [h[0] + (d[0] / n) * 1.25 + latZ[0] * 1.0, 1.92, h[2] + (d[1] / n) * 1.25 + latZ[1] * 1.0];
388
+ alvo = cabeca(quem); alvo = [alvo[0], 1.5, alvo[2]]; fov = 30 * fovK;
389
+ } else {
390
+ // No vertical o quadro é estreito: a câmera recua para o rosto caber de lado a lado.
391
+ const dist = (pl.tipo === 'detalhe' ? 1.15 : pl.tipo === 'close' ? 1.75 : 2.6) * (vert ? 1.55 : 1);
392
+ fov = (pl.tipo === 'detalhe' ? 24 : pl.tipo === 'close' ? 28 : 32) * fovK;
393
+ const h = cabeca(quem), f = frente(quem);
394
+ // Um pouco de lado (o "três quartos") e da altura dos olhos.
395
+ const lado = pl.lado * 0.35;
396
+ const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
397
+ pos = [h[0] + dir[0] * dist, pl.tipo === 'medio' ? 1.62 : 1.66, h[2] + dir[2] * dist];
398
+ alvo = [h[0], pl.tipo === 'medio' ? 1.35 : pl.tipo === 'close' ? 1.52 : 1.58, h[2]];
399
+ }
400
+ // O movimento dentro do plano: entra, sai, desliza — e o balanço de câmera na mão.
401
+ const m = suave(k);
402
+ if (pl.mov === 'entra' || pl.mov === 'sai') {
403
+ const s = pl.mov === 'entra' ? m : 1 - m;
404
+ pos = pos.map((v, i) => lerp(v, alvo[i], 0.1 * s));
405
+ } else if (pl.mov === 'lateral') {
406
+ pos = [pos[0] + (m - 0.5) * 0.3 * pl.lado, pos[1], pos[2]];
407
+ }
408
+ const mao = [Math.sin(t * 1.3) * 0.012 + Math.sin(t * 3.1) * 0.005, Math.sin(t * 1.1 + 1) * 0.009 + Math.sin(t * 2.7) * 0.004, 0];
409
+ return { pos: pos.map((v, i) => v + (mao[i] || 0)), alvo: alvo.map((v, i) => v + (mao[i] || 0) * 0.5), fov };
410
+ }
411
+
412
+ /* ── POR CIMA: legenda, nome, destaque, REC e grão ──────────────────────── */
413
+ const FONTE = "'Archivo Black','Arial Black','Helvetica Neue',Impact,system-ui,sans-serif";
414
+
415
+ function pedacos(palavras) {
416
+ const out = [];
417
+ let cur = [];
418
+ for (const w of palavras) {
419
+ cur.push(w);
420
+ const letras = cur.reduce((n, x) => n + String(x.t).length + 1, 0);
421
+ if (cur.length >= 4 || letras > 22 || /[.,!?;:]$/.test(w.t)) { out.push(cur); cur = []; }
422
+ }
423
+ if (cur.length) out.push(cur);
424
+ return out;
425
+ }
426
+
427
+ function Legenda({ part, t, W, H, acento }) {
428
+ const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.25);
429
+ if (!f) return null;
430
+ const grupos = pedacos(f.palavras);
431
+ const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.15) || grupos[grupos.length - 1];
432
+ const t0 = g[0].de;
433
+ const tam = Math.round(Math.min(W, H) * (H > W ? 0.075 : 0.058));
434
+ return (
435
+ <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.7 : 0.8), display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: `0 ${tam * 0.42}px`, padding: `0 ${W * 0.08}px` }}>
436
+ {g.map((w, i) => {
437
+ const on = t >= w.de;
438
+ const agora = t >= w.de && t < w.ate;
439
+ const k = mola((t - Math.max(t0, w.de - 0.06)) / 0.22);
440
+ return (
441
+ <span key={i} style={{ fontFamily: FONTE, fontWeight: 900, fontSize: tam, lineHeight: 1.05, textTransform: 'uppercase', letterSpacing: '-0.01em',
442
+ color: agora ? acento : '#ffffff', opacity: on ? 1 : 0.0, transform: `translateY(${(1 - k) * tam * 0.35}px) scale(${agora ? 1.04 : 1})`,
443
+ textShadow: '0 4px 0 rgba(0,0,0,0.55), 0 0 24px rgba(0,0,0,0.45)', WebkitTextStroke: `${Math.max(2, tam * 0.05)}px rgba(0,0,0,0.65)`, paintOrder: 'stroke fill' }}>{w.t}</span>
444
+ );
445
+ })}
446
+ </div>
447
+ );
448
+ }
449
+
450
+ function Nome({ part, t, W, H, acento }) {
451
+ if (!part.elenco.some((e) => e.rotulo)) return null;
452
+ // O nome entra na primeira fala de cada um, por 2,4 s.
453
+ const vistos = new Set();
454
+ let tag = null;
455
+ for (const f of part.falas) {
456
+ if (vistos.has(f.quem)) continue;
457
+ vistos.add(f.quem);
458
+ if (t >= f.de && t <= f.de + 2.4) tag = { f, k: t - f.de };
459
+ }
460
+ if (!tag) return null;
461
+ const p = part.elenco[tag.f.quem];
462
+ const entra = suave(tag.k / 0.35), sai = 1 - suave((tag.k - 2.0) / 0.4);
463
+ const tam = Math.round(Math.min(W, H) * 0.032);
464
+ return (
465
+ <div style={{ position: 'absolute', left: W * 0.06, top: H * (H > W ? 0.6 : 0.66), opacity: entra * sai, transform: `translateX(${(1 - entra) * -40}px)`, display: 'flex', alignItems: 'stretch' }}>
466
+ <div style={{ width: tam * 0.3, background: acento }} />
467
+ <div style={{ background: 'rgba(8,10,14,0.82)', padding: `${tam * 0.35}px ${tam * 0.7}px`, fontFamily: FONTE, fontSize: tam, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.04em' }}>{p.rotulo}</div>
468
+ </div>
469
+ );
470
+ }
471
+
472
+ function Destaque({ part, t, W, H, acento }) {
473
+ const f = part.falas.find((x) => x.destaque && t >= x.de + 0.3 && t <= Math.min(x.ate + 0.3, x.de + 3.2));
474
+ if (!f) return null;
475
+ const k = t - (f.de + 0.3);
476
+ const fim = Math.min(f.ate + 0.3, f.de + 3.2) - (f.de + 0.3);
477
+ const s = mola(k / 0.4) * (1 - suave((k - fim + 0.3) / 0.3));
478
+ const tam = Math.round(Math.min(W, H) * (H > W ? 0.085 : 0.075));
479
+ return (
480
+ <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.1 : 0.07), display: 'flex', justifyContent: 'center' }}>
481
+ <div style={{ transform: `scale(${s}) rotate(${(1 - s) * -4}deg)`, opacity: clamp(s * 1.4), background: 'rgba(8,10,14,0.78)', border: `${Math.max(3, tam * 0.06)}px solid ${acento}`,
482
+ borderRadius: tam * 0.3, padding: `${tam * 0.25}px ${tam * 0.55}px`, fontFamily: FONTE, fontSize: tam, color: '#fff', textTransform: 'uppercase', boxShadow: `0 0 ${tam}px ${acento}55` }}>{f.destaque}</div>
483
+ </div>
484
+ );
485
+ }
486
+
487
+ function Rec({ t, W, H }) {
488
+ const tam = Math.round(Math.min(W, H) * 0.024);
489
+ const s = Math.floor(t), q = Math.floor((t % 1) * 30);
490
+ const tc = `00:${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}:${String(q).padStart(2, '0')}`;
491
+ return (
492
+ <div style={{ position: 'absolute', left: W * 0.05, top: H * 0.045, display: 'flex', alignItems: 'center', gap: tam * 0.5, fontFamily: "'SF Mono',Menlo,monospace", fontSize: tam, color: 'rgba(255,255,255,0.85)', letterSpacing: '0.08em' }}>
493
+ <div style={{ width: tam * 0.7, height: tam * 0.7, borderRadius: '50%', background: '#ff3030', opacity: Math.floor(t * 1.2) % 2 ? 0.25 : 1 }} />
494
+ <span>REC</span><span style={{ opacity: 0.7 }}>{tc}</span>
495
+ </div>
496
+ );
497
+ }
498
+
499
+ function Filme({ t, W, H, grao }) {
500
+ return (
501
+ <>
502
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, rgba(0,0,0,0) 55%, rgba(0,0,0,0.5) 100%)' }} />
503
+ {grao && (
504
+ <svg width={W} height={H} style={{ position: 'absolute', inset: 0, opacity: 0.08, mixBlendMode: 'overlay' }}>
505
+ <filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed={Math.floor(t * 24) % 50} /></filter>
506
+ <rect width="100%" height="100%" filter="url(#grao)" />
507
+ </svg>
508
+ )}
509
+ </>
510
+ );
511
+ }
512
+
513
+ /* ── A PEÇA ──────────────────────────────────────────────────────────────── */
514
+ export function PersonagemCena({ el, t }) {
515
+ const part = el.partitura || {};
516
+ const W = el.largura || part.largura || 1920, H = el.altura || part.altura || 1080;
517
+ const c = part.cenario || {};
518
+ const acento = c.acento || '#3ddc97';
519
+ const elenco = part.elenco || [];
520
+ const falas = part.falas || [];
521
+ const planos = part.planos || [{ de: 0, ate: part.duracao || 10, tipo: 'aberto', alvo: 0, mov: 'fixo', lado: 1 }];
522
+ const dois = elenco.length > 1;
523
+ const pl = planos.find((x) => t >= x.de && t < x.ate) || planos[planos.length - 1];
524
+ const cam = cameraDe(pl, t, part, W / H);
525
+
526
+ const falandoAgora = (i) => falas.some((f) => f.quem === i && t >= f.de && t <= f.ate);
527
+ const emocaoDe = (i) => {
528
+ let e = 'neutro';
529
+ for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
530
+ return e;
531
+ };
532
+ const estado = (i) => {
533
+ const fala = falandoAgora(i);
534
+ const outro = dois && falandoAgora(1 - i);
535
+ const enfase = gesto(falas, i, t);
536
+ const nod = Math.sin(t * (fala ? 5.2 : 1.4) + i) * (fala ? 0.035 : 0.015) + (outro ? Math.max(0, Math.sin(t * 2.1 + i)) * 0.04 : 0);
537
+ const emo = emocaoDe(i);
538
+ const e = EMO[emo] || EMO.neutro;
539
+ const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.08;
540
+ return {
541
+ t, falando: fala, emocao: emo, boca: boca(falas, i, t), pisca: piscada(i, t),
542
+ gesto: enfase, enfase: Math.max(enfase[0], enfase[1]),
543
+ olhar: dois ? [micro, 0.02] : [micro * 0.5, 0.04],
544
+ cabeca: [Math.sin(t * 0.7 + i * 2) * 0.08 + (outro ? 0.05 : 0), nod + (fala ? 0 : 0.03), (e.inclina || 0) * (i ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
545
+ };
546
+ };
547
+ const posDe = (i) => (dois ? [(elenco[i].lado || 0) * 0.95, 0, 0] : [0, 0, 0]);
548
+ const giroDe = (i) => (dois ? (elenco[i].lado < 0 ? 0.95 : -0.95) : 0);
549
+ const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);
550
+
551
+ return (
552
+ <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden', background: '#000' }}>
553
+ <div style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
554
+ <ThreeCanvas width={W} height={H} shadows camera={{ position: [0, 1.7, 4.5], fov: 36 }}
555
+ gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.05 }}>
556
+ <Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
557
+ <Ambiente />
558
+ <color attach="background" args={[c.parede || '#111']} />
559
+ <fog attach="fog" args={[c.parede || '#111', 7, 16]} />
560
+ <hemisphereLight args={['#dfe8ff', c.chao || '#222', 0.45]} />
561
+ <ambientLight intensity={0.15} />
562
+ <spotLight position={[2.4, 4.6, 4.2]} angle={0.55} penumbra={0.7} intensity={95} distance={20} decay={2} color={c.luz || '#fff'} castShadow
563
+ shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0004} />
564
+ <directionalLight position={[-3, 2.5, 3]} intensity={0.7} color="#cfe0ff" />
565
+ <pointLight position={[-2.2, 2.4, -1.4]} intensity={14} distance={7} color={acento} />
566
+ <pointLight position={[2.2, 2.4, -1.4]} intensity={14} distance={7} color={acento} />
567
+ <pointLight position={[-3.6, 1.85, 0.2]} intensity={6} distance={5} color={c.luz || '#ffd9a8'} />
568
+ <Palco c={c} dois={dois} t={t} />
569
+ {elenco.map((p, i) => <Personagem key={i} p={p} est={estado(i)} acento={acento} pos={posDe(i)} giro={giroDe(i)} />)}
570
+ {c.microfones && elenco.map((p, i) => <Microfone key={'m' + i} x={posDe(i)[0]} z={posDe(i)[2]} giro={giroDe(i)} />)}
571
+ </ThreeCanvas>
572
+ <Filme t={t} W={W} H={H} grao={!part.filme || part.filme.grao !== false} />
573
+ <Rec t={t} W={W} H={H} />
574
+ <Nome part={part} t={t} W={W} H={H} acento={acento} />
575
+ <Destaque part={part} t={t} W={W} H={H} acento={acento} />
576
+ {part.legenda !== false && <Legenda part={part} t={t} W={W} H={H} acento={acento} />}
577
+ </div>
578
+ </div>
579
+ );
580
+ }
581
+
582
+ export const PERSONAGEM = { personagem: PersonagemCena };
@@ -59,9 +59,11 @@ const PECAS = {
59
59
  gerativo: { precisa: ['partitura'] },
60
60
  // O estilo fluido (kit/fluido.jsx): a forma que vira a próxima, numa peça só (fluido.js).
61
61
  fluido: { precisa: ['partitura'] },
62
+ // O vídeo gravado em 3D (kit/personagem.jsx): o set, os personagens e a câmera (personagem.js).
63
+ personagem: { precisa: ['partitura'] },
62
64
  };
63
65
 
64
- const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'index.jsx'];
66
+ const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'personagem.jsx', 'index.jsx'];
65
67
 
66
68
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
67
69
  function arquivos() {
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Tipos de parte: abertura, logo, titulo, numero, lista, passos, contraste,
18
18
  * citacao, imagem, grafico, cores, produto, cta. Qualquer parte pode pedir
19
- * a peça que a conduz com "visual": orbe, vetor, estrela, esfera, grade,
19
+ * a peça que a conduz com "visual": orbe, vetor, esfera, grade,
20
20
  * site, padrao, cromo, cartoes, painel, cores.
21
21
  *
22
22
  * Puro: roteiro, tempos da voz, glifos da fonte (glifos.js) e arquivos
@@ -41,7 +41,10 @@ const luz = (hex) => {
41
41
  };
42
42
  const sobre = (c) => (luz(c) > 0.6 ? '#111114' : '#ffffff');
43
43
  const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
44
- const VISUAIS = ['orbe', 'vetor', 'estrela', 'esfera', 'grade', 'site', 'padrao', 'cromo', 'cartoes', 'painel', 'cores'];
44
+ /* Sem a `estrela`: o asterisco laranja de raios redondos girando É o logo do
45
+ Claude, e entrava em todo vídeo de lançamento como se fosse nosso. A peça
46
+ continua no VISUAL abaixo só para projeto antigo que já a pede renderizar. */
47
+ const VISUAIS = ['orbe', 'vetor', 'esfera', 'grade', 'site', 'padrao', 'cromo', 'cartoes', 'painel', 'cores'];
45
48
 
46
49
  /**
47
50
  * @param {object} op