morfeo 0.5.1 → 0.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.
@@ -0,0 +1,635 @@
1
+ // Ambientes 3D listos: sets completos con utilería, luz propia, vida de fondo (tele que parpadea, LED que cambia,
2
+ // lluvia en la ventana, polvo en la luz, gato que respira) y planos de cámara con nombre. Sirven como `mundo` de
3
+ // cualquier video (también 2D): así ninguna escena queda con un fondo vacío.
4
+ //
5
+ // <Ambiente nombre="cuarto-gamer" t={t} escena={escena} tEscena={tEscena} inicios={inicios} persona />
6
+ // capa del motor: {tipo: 'ambiente', nombre: 'cuarto-gamer'} · escena: {plano: 'tele'}
7
+ //
8
+ // Sin `plano`, cada escena toma el siguiente de `orden` (otro ángulo en cada escena). Nada usa useFrame.
9
+ import React from 'react';
10
+ import * as THREE from 'three';
11
+ import {azarN} from './ilustrado';
12
+ import {Persona3D} from './mundo3d';
13
+ import type {Expresion, Pose} from './personas';
14
+ import {Bloque, Lienzo3D, type Etiqueta3D, type Post3D, type Toma3D, type V3} from './tres';
15
+
16
+ export type NombreAmbiente = 'cuarto-gamer' | 'sala' | 'arcade';
17
+ export type Reaccion = {en: number; expresion?: Expresion; pose?: Pose};
18
+
19
+ type Def = {
20
+ luz: 'noche' | 'calida' | 'dia';
21
+ fondo: string;
22
+ niebla: [number, number];
23
+ post: Post3D;
24
+ /** Planos con nombre (la toma de cada escena). */
25
+ planos: Record<string, Toma3D>;
26
+ /** Orden de planos cuando la escena no dice ninguno. */
27
+ orden: string[];
28
+ /** Orden para escenas con capas 2D al centro: planos abiertos (sin la cara de la persona tapando la capa). */
29
+ ordenFondo: string[];
30
+ /** Puntos con nombre (para etiquetas y para apuntar): tele, consola, persona… */
31
+ anclas: Record<string, V3>;
32
+ /** Dónde y cómo va la persona. */
33
+ persona: {pos: V3; rot: V3; pose: Pose};
34
+ Set: React.FC<{t: number}>;
35
+ };
36
+
37
+ const hsl = (h: number, s: number, l: number) => '#' + new THREE.Color().setHSL(((h % 1) + 1) % 1, s, l).getHexString();
38
+ const Emisivo: React.FC<{pos: V3; tam: V3; color: string; fuerza?: number; rot?: V3}> = ({pos, tam, color, fuerza = 1, rot}) => (
39
+ <mesh position={pos} rotation={rot}>
40
+ <boxGeometry args={tam} />
41
+ <meshStandardMaterial color={color} emissive={color} emissiveIntensity={fuerza} toneMapped={false} />
42
+ </mesh>
43
+ );
44
+
45
+ /** Cuarto: piso, pared del fondo, pared izquierda y techo bajo (abierto hacia la cámara). */
46
+ const Cuarto: React.FC<{ancho: number; fondo: number; alto: number; pared: string; lateral?: string; piso: string; techo?: boolean}> = ({ancho, fondo, alto, pared, lateral, piso, techo = true}) => (
47
+ <group>
48
+ <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
49
+ <planeGeometry args={[ancho, fondo]} />
50
+ <meshStandardMaterial color={piso} roughness={0.85} />
51
+ </mesh>
52
+ <Bloque pos={[0, alto / 2, -fondo / 2]} tam={[ancho, alto, 0.2]} color={pared} rugosidad={0.95} />
53
+ <Bloque pos={[-ancho / 2, alto / 2, 0]} tam={[0.2, alto, fondo]} color={lateral ?? pared} rugosidad={0.95} />
54
+ <Bloque pos={[ancho / 2, alto / 2, 0]} tam={[0.2, alto, fondo]} color={lateral ?? pared} rugosidad={0.95} />
55
+ {techo ? <Bloque pos={[0, alto + 0.1, 0]} tam={[ancho, 0.2, fondo]} color="#1A1C26" /> : null}
56
+ {/* zoclo */}
57
+ <Bloque pos={[0, 0.06, -fondo / 2 + 0.12]} tam={[ancho, 0.12, 0.04]} color="#E9E1D2" />
58
+ </group>
59
+ );
60
+
61
+ /** Ventana de noche con lluvia: marco, vidrio oscuro con edificios lejanos y gotas que bajan. */
62
+ const VentanaLluvia: React.FC<{pos: V3; ancho?: number; alto?: number; t: number; semilla?: number}> = ({pos, ancho = 1.8, alto = 1.5, t, semilla = 0}) => (
63
+ <group position={pos}>
64
+ <Bloque pos={[0, 0, 0]} tam={[ancho + 0.14, alto + 0.14, 0.06]} color="#E9E1D2" />
65
+ <mesh position={[0, 0, 0.035]}>
66
+ <planeGeometry args={[ancho, alto]} />
67
+ <meshStandardMaterial color="#141C3A" emissive="#1B2550" emissiveIntensity={0.5} />
68
+ </mesh>
69
+ {/* edificios lejanos con ventanas encendidas */}
70
+ {Array.from({length: 7}, (_, i) => {
71
+ const h = 0.3 + azarN(semilla + i * 3.1) * 0.7;
72
+ const x = -ancho / 2 + 0.13 + i * ((ancho - 0.26) / 6);
73
+ return (
74
+ <group key={i}>
75
+ <mesh position={[x, -alto / 2 + h / 2, 0.04]}>
76
+ <planeGeometry args={[0.22, h]} />
77
+ <meshStandardMaterial color="#0A0F22" />
78
+ </mesh>
79
+ {Array.from({length: 3}, (_, j) =>
80
+ azarN(semilla + i * 7 + j) > 0.45 ? <Emisivo key={j} pos={[x + (j - 1) * 0.06, -alto / 2 + 0.08 + azarN(i + j * 5) * (h - 0.12), 0.045]} tam={[0.03, 0.04, 0.001]} color="#FFD27A" fuerza={1.4} /> : null,
81
+ )}
82
+ </group>
83
+ );
84
+ })}
85
+ {/* gotas de lluvia bajando por el vidrio */}
86
+ {Array.from({length: 22}, (_, i) => {
87
+ const v = 0.25 + azarN(semilla + i * 2.3) * 0.35;
88
+ const f = (t * v + azarN(semilla + i * 9.7)) % 1;
89
+ return <Emisivo key={`g${i}`} pos={[-ancho / 2 + 0.05 + azarN(semilla + i * 4.4) * (ancho - 0.1), alto / 2 - 0.05 - f * (alto - 0.1), 0.05]} tam={[0.008, 0.07, 0.002]} color="#9FC3FF" fuerza={0.6} />;
90
+ })}
91
+ {/* cortinas */}
92
+ <Bloque pos={[-ancho / 2 - 0.15, -0.05, 0.12]} tam={[0.3, alto + 0.4, 0.05]} color="#3B2F55" />
93
+ <Bloque pos={[ancho / 2 + 0.15, -0.05, 0.12]} tam={[0.3, alto + 0.4, 0.05]} color="#3B2F55" />
94
+ </group>
95
+ );
96
+
97
+ /** Polvo flotando en el aire (se ve en la luz): puntitos que suben y bajan despacio. */
98
+ const Polvo: React.FC<{centro: V3; tam: V3; t: number; n?: number; color?: string}> = ({centro, tam, t, n = 40, color = '#FFE6B8'}) => (
99
+ <group>
100
+ {Array.from({length: n}, (_, i) => {
101
+ const x = centro[0] + (azarN(i * 3.7) - 0.5) * tam[0] + Math.sin(t * 0.3 + i) * 0.08;
102
+ const y = centro[1] + (((azarN(i * 5.3) + t * 0.02 * (0.5 + azarN(i))) % 1) - 0.5) * tam[1];
103
+ const z = centro[2] + (azarN(i * 7.1) - 0.5) * tam[2] + Math.cos(t * 0.25 + i) * 0.08;
104
+ return (
105
+ <mesh key={i} position={[x, y, z]}>
106
+ <sphereGeometry args={[0.008 + azarN(i * 1.9) * 0.008, 5, 4]} />
107
+ <meshBasicMaterial color={color} transparent opacity={0.55} toneMapped={false} />
108
+ </mesh>
109
+ );
110
+ })}
111
+ </group>
112
+ );
113
+
114
+ /** Tele o monitor encendido: la pantalla cambia de color como un juego y alumbra lo que tiene enfrente. */
115
+ export const Tele3D: React.FC<{pos: V3; ancho?: number; t: number; fase?: number; luz?: number; rot?: V3}> = ({pos, ancho = 1.5, t, fase = 0, luz = 2.2, rot}) => {
116
+ const alto = ancho * 0.58;
117
+ // "juego": un cielo que cambia lento, un piso, un personaje que brinca y destellos de vez en cuando
118
+ const cielo = hsl(0.58 + Math.sin(t * 0.25 + fase) * 0.08, 0.7, 0.45);
119
+ const destello = Math.max(0, Math.sin(t * 2.3 + fase)) ** 12;
120
+ const brinco = Math.abs(Math.sin(t * 3 + fase)) * alto * 0.18;
121
+ return (
122
+ <group position={pos} rotation={rot}>
123
+ <Bloque pos={[0, 0, 0]} tam={[ancho + 0.08, alto + 0.08, 0.07]} color="#15161B" radio={0.02} />
124
+ <mesh position={[0, 0, 0.037]}>
125
+ <planeGeometry args={[ancho, alto]} />
126
+ <meshStandardMaterial color={cielo} emissive={cielo} emissiveIntensity={0.6 + 0.6 * destello} />
127
+ </mesh>
128
+ <Emisivo pos={[0, -alto * 0.36, 0.04]} tam={[ancho, alto * 0.28, 0.001]} color="#3CB371" fuerza={0.9} />
129
+ <Emisivo pos={[-ancho * 0.2, -alto * 0.14 + brinco, 0.042]} tam={[alto * 0.12, alto * 0.16, 0.001]} color="#FF4D4D" fuerza={1.2} />
130
+ <Emisivo pos={[ancho * 0.25, alto * 0.25, 0.042]} tam={[alto * 0.14, alto * 0.08, 0.001]} color="#FFFFFF" fuerza={0.9} />
131
+ <pointLight position={[0, 0, 0.6]} color={cielo} intensity={luz * (0.85 + 0.15 * Math.sin(t * 9 + fase) + destello)} distance={5} decay={1.5} />
132
+ </group>
133
+ );
134
+ };
135
+
136
+ /** Consola blanca con su luz encendida y un control encima de la mesa. */
137
+ const ConsolaYControl: React.FC<{pos: V3; t: number}> = ({pos, t}) => (
138
+ <group position={pos}>
139
+ <Bloque pos={[0, 0.06, 0]} tam={[0.5, 0.12, 0.36]} color="#E9ECF1" radio={0.02} />
140
+ <Emisivo pos={[-0.18, 0.07, 0.182]} tam={[0.05, 0.015, 0.004]} color="#3DDC84" fuerza={1 + 0.5 * Math.sin(t * 2)} />
141
+ <group position={[0.55, 0.03, 0.25]} rotation={[0, -0.4, 0]}>
142
+ <Bloque pos={[0, 0, 0]} tam={[0.32, 0.06, 0.16]} color="#2A2A30" radio={0.03} />
143
+ <Bloque pos={[-0.13, -0.005, 0.04]} tam={[0.1, 0.06, 0.12]} color="#2A2A30" radio={0.03} />
144
+ <Bloque pos={[0.13, -0.005, 0.04]} tam={[0.1, 0.06, 0.12]} color="#2A2A30" radio={0.03} />
145
+ <Emisivo pos={[0.08, 0.035, -0.02]} tam={[0.03, 0.01, 0.03]} color="#E5484D" fuerza={0.6} />
146
+ <Emisivo pos={[0.12, 0.035, 0.01]} tam={[0.03, 0.01, 0.03]} color="#3D7BFF" fuerza={0.6} />
147
+ </group>
148
+ </group>
149
+ );
150
+
151
+ /** Silla gamer (respaldo alto) mirando hacia −z. */
152
+ const SillaGamer: React.FC<{pos: V3; color?: string}> = ({pos, color = '#C62E3A'}) => (
153
+ <group position={pos}>
154
+ <Bloque pos={[0, 0.42, 0.05]} tam={[0.62, 0.1, 0.58]} color="#1E1F26" radio={0.04} />
155
+ <Bloque pos={[0, 0.98, 0.34]} tam={[0.6, 1.05, 0.1]} color="#1E1F26" radio={0.05} />
156
+ <Bloque pos={[0, 1.0, 0.285]} tam={[0.3, 0.9, 0.02]} color={color} />
157
+ <Bloque pos={[0, 0.2, 0.05]} tam={[0.06, 0.36, 0.06]} color="#2A2A30" metal={0.6} />
158
+ {[0, 1, 2, 3, 4].map((k) => (
159
+ <Bloque key={k} pos={[Math.cos((k * Math.PI * 2) / 5) * 0.28, 0.03, 0.05 + Math.sin((k * Math.PI * 2) / 5) * 0.28]} rot={[0, (-k * 360) / 5, 0]} tam={[0.3, 0.04, 0.05]} color="#2A2A30" />
160
+ ))}
161
+ </group>
162
+ );
163
+
164
+ /** Estante de pared con cajas de juegos de colores, una planta y una figura. */
165
+ const Estante: React.FC<{pos: V3; rot?: V3; semilla?: number}> = ({pos, rot, semilla = 0}) => {
166
+ const colores = ['#E5484D', '#3D7BFF', '#F2B632', '#30A46C', '#8B5BD6', '#EDEDED', '#FF8A3D'];
167
+ return (
168
+ <group position={pos} rotation={rot}>
169
+ {[0, 0.62].map((y, f) => (
170
+ <group key={f} position={[0, y, 0]}>
171
+ <Bloque pos={[0, 0, 0]} tam={[1.5, 0.05, 0.32]} color="#8A6A48" />
172
+ {Array.from({length: 9}, (_, i) =>
173
+ f === 1 && i > 5 ? null : <Bloque key={i} pos={[-0.66 + i * 0.075, 0.2, 0]} rot={[0, 0, i === 8 ? -12 : 0]} tam={[0.06, 0.34 + azarN(semilla + i + f * 9) * 0.04, 0.24]} color={colores[Math.floor(azarN(semilla + i * 3 + f) * colores.length)]} />,
174
+ )}
175
+ </group>
176
+ ))}
177
+ {/* planta y figura */}
178
+ <group position={[0.45, 0.65, 0]}>
179
+ <Bloque pos={[0, 0.08, 0]} tam={[0.16, 0.16, 0.16]} color="#C47A4A" />
180
+ {[0, 1, 2, 3].map((k) => (
181
+ <mesh key={k} position={[Math.cos(k * 1.6) * 0.05, 0.26 + k * 0.02, Math.sin(k * 1.6) * 0.05]} rotation={[0.3 * Math.cos(k), k, 0.4]}>
182
+ <coneGeometry args={[0.06, 0.24, 4]} />
183
+ <meshStandardMaterial color="#3E9B5A" flatShading />
184
+ </mesh>
185
+ ))}
186
+ </group>
187
+ <group position={[0.28, 0.66, 0]}>
188
+ <mesh position={[0, 0.12, 0]}>
189
+ <octahedronGeometry args={[0.1, 0]} />
190
+ <meshStandardMaterial color="#F2B632" metalness={0.6} roughness={0.3} flatShading />
191
+ </mesh>
192
+ </group>
193
+ </group>
194
+ );
195
+ };
196
+
197
+ /** Póster genérico (formas, sin marcas): planeta con anillo o nave. */
198
+ const Poster: React.FC<{pos: V3; tipo?: 'planeta' | 'nave' | 'pixel'; rot?: V3}> = ({pos, tipo = 'planeta', rot}) => (
199
+ <group position={pos} rotation={rot}>
200
+ <Bloque pos={[0, 0, 0]} tam={[0.7, 1.0, 0.02]} color={tipo === 'nave' ? '#1E2340' : tipo === 'pixel' ? '#2B1640' : '#2A1F3D'} />
201
+ {tipo === 'planeta' ? (
202
+ <group position={[0, 0.08, 0.02]}>
203
+ <mesh>
204
+ <circleGeometry args={[0.2, 20]} />
205
+ <meshStandardMaterial color="#FF8A3D" />
206
+ </mesh>
207
+ <mesh rotation={[0, 0, 0.3]}>
208
+ <ringGeometry args={[0.26, 0.3, 28]} />
209
+ <meshStandardMaterial color="#FFD34D" side={THREE.DoubleSide} />
210
+ </mesh>
211
+ </group>
212
+ ) : tipo === 'nave' ? (
213
+ <group position={[0, 0.05, 0.02]}>
214
+ <mesh rotation={[0, 0, Math.PI]}>
215
+ <circleGeometry args={[0.2, 3]} />
216
+ <meshStandardMaterial color="#5FD4FF" />
217
+ </mesh>
218
+ <Bloque pos={[0, -0.28, 0]} tam={[0.08, 0.14, 0.005]} color="#FF5A3D" />
219
+ </group>
220
+ ) : (
221
+ <group position={[0, 0.05, 0.02]}>
222
+ {[
223
+ [0, 2],
224
+ [1, 1],
225
+ [1, 2],
226
+ [1, 3],
227
+ [2, 0],
228
+ [2, 1],
229
+ [2, 2],
230
+ [2, 3],
231
+ [2, 4],
232
+ [3, 1],
233
+ [3, 3],
234
+ ].map(([f, c], i) => (
235
+ <Bloque key={i} pos={[(c - 2) * 0.09, (1.5 - f) * 0.09, 0]} tam={[0.085, 0.085, 0.005]} color="#5ADC6E" />
236
+ ))}
237
+ </group>
238
+ )}
239
+ <Bloque pos={[0, -0.4, 0.015]} tam={[0.5, 0.06, 0.005]} color="#E9E1D2" />
240
+ </group>
241
+ );
242
+
243
+ /** Tira de LED que cambia de color despacio (y alumbra la pared). */
244
+ const TiraLED: React.FC<{pos: V3; largo: number; t: number; fase?: number; luz?: number}> = ({pos, largo, t, fase = 0, luz = 1.6}) => {
245
+ const c = hsl(0.75 + t * 0.03 + fase, 0.9, 0.55);
246
+ return (
247
+ <group position={pos}>
248
+ <Emisivo pos={[0, 0, 0]} tam={[largo, 0.03, 0.03]} color={c} fuerza={2} />
249
+ <pointLight position={[0, -0.1, 0.3]} color={c} intensity={luz} distance={3.5} decay={1.5} />
250
+ </group>
251
+ );
252
+ };
253
+
254
+ /** Gato low-poly echado: respira y mueve la cola. */
255
+ export const Gato3D: React.FC<{pos: V3; rot?: V3; t: number; color?: string; escala?: number}> = ({pos, rot = [0, 0, 0], t, color = '#E8A55C', escala = 1}) => {
256
+ const resp = 1 + Math.sin(t * 2.2) * 0.03;
257
+ const cola = Math.sin(t * 1.6) * 0.5;
258
+ return (
259
+ <group position={pos} rotation={rot.map((g) => (g * Math.PI) / 180) as V3} scale={escala}>
260
+ <mesh position={[0, 0.13, 0]} scale={[1.35, 0.75 * resp, 0.85]}>
261
+ <icosahedronGeometry args={[0.16, 0]} />
262
+ <meshStandardMaterial color={color} flatShading />
263
+ </mesh>
264
+ <mesh position={[0.22, 0.2, 0]}>
265
+ <icosahedronGeometry args={[0.1, 0]} />
266
+ <meshStandardMaterial color={color} flatShading />
267
+ </mesh>
268
+ {[-1, 1].map((l) => (
269
+ <mesh key={l} position={[0.25, 0.3, l * 0.05]} rotation={[0, 0, -0.2]}>
270
+ <coneGeometry args={[0.035, 0.08, 4]} />
271
+ <meshStandardMaterial color={color} flatShading />
272
+ </mesh>
273
+ ))}
274
+ <group position={[-0.22, 0.1, 0]} rotation={[cola, 0, 0.6]}>
275
+ <mesh position={[0, 0.12, 0]}>
276
+ <cylinderGeometry args={[0.025, 0.035, 0.26, 5]} />
277
+ <meshStandardMaterial color={color} flatShading />
278
+ </mesh>
279
+ </group>
280
+ </group>
281
+ );
282
+ };
283
+
284
+ // ── Sets ────────────────────────────────────────────────────────────────────────────────
285
+
286
+ const SetCuartoGamer: React.FC<{t: number}> = ({t}) => (
287
+ <group>
288
+ <Cuarto ancho={8} fondo={6} alto={3.6} pared="#2C3550" lateral="#26304A" piso="#5A3E2B" />
289
+ {/* tapete */}
290
+ <mesh position={[-0.9, 0.006, -0.9]} rotation={[-Math.PI / 2, 0, 0]}>
291
+ <circleGeometry args={[1.3, 28]} />
292
+ <meshStandardMaterial color="#3B2F55" roughness={1} />
293
+ </mesh>
294
+ {/* escritorio, tele, consola */}
295
+ <Bloque pos={[-1.0, 0.76, -2.4]} tam={[2.4, 0.07, 0.9]} color="#C49A6C" rugosidad={0.8} />
296
+ {[-2.1, 0.1].map((x) => (
297
+ <Bloque key={x} pos={[x, 0.37, -2.4]} tam={[0.07, 0.74, 0.8]} color="#8A6A48" />
298
+ ))}
299
+ <Bloque pos={[-1.0, 0.86, -2.68]} tam={[0.35, 0.12, 0.2]} color="#222228" />
300
+ <Tele3D pos={[-1.0, 1.42, -2.72]} ancho={1.55} t={t} />
301
+ <ConsolaYControl pos={[-0.45, 0.8, -2.35]} t={t} />
302
+ {/* cables que bajan del escritorio */}
303
+ <Bloque pos={[-1.15, 0.4, -2.82]} tam={[0.025, 0.8, 0.025]} color="#111" />
304
+ <Bloque pos={[-0.95, 0.4, -2.8]} tam={[0.025, 0.8, 0.025]} color="#111" />
305
+ {/* LED detrás del escritorio y en el techo */}
306
+ <TiraLED pos={[-1.0, 0.72, -2.84]} largo={2.2} t={t} />
307
+ <TiraLED pos={[0, 3.52, -2.86]} largo={7.6} t={t} fase={0.3} luz={1.2} />
308
+ {/* silla */}
309
+ <SillaGamer pos={[-1.0, 0, -0.95]} />
310
+ {/* pósters y estante */}
311
+ <Poster pos={[-2.9, 2.3, -2.88]} tipo="planeta" />
312
+ <Poster pos={[0.65, 2.45, -2.88]} tipo="pixel" />
313
+ <Estante pos={[-3.82, 1.25, -1.2]} rot={[0, 90, 0].map((g) => (g * Math.PI) / 180) as V3} />
314
+ {/* ventana con lluvia y cama */}
315
+ <VentanaLluvia pos={[2.3, 2.1, -2.88]} t={t} />
316
+ <Bloque pos={[2.75, 0.25, -1.6]} tam={[1.7, 0.5, 2.4]} color="#4A3A2A" />
317
+ <Bloque pos={[2.75, 0.56, -1.4]} tam={[1.6, 0.14, 2.0]} color="#3D7BFF" />
318
+ <Bloque pos={[2.75, 0.62, -2.6]} tam={[1.0, 0.16, 0.35]} color="#EDEDED" radio={0.06} />
319
+ {/* lámpara de techo cálida */}
320
+ <mesh position={[0.4, 3.45, -0.6]}>
321
+ <cylinderGeometry args={[0.28, 0.38, 0.16, 16]} />
322
+ <meshStandardMaterial color="#F2D49B" emissive="#F2D49B" emissiveIntensity={0.6} />
323
+ </mesh>
324
+ <pointLight position={[0.4, 3.1, -0.6]} intensity={5} distance={9} decay={1.6} color="#FFD9A0" castShadow shadow-mapSize={[1024, 1024]} shadow-bias={-0.0005} />
325
+ <Polvo centro={[-0.6, 1.8, -1.4]} tam={[2.6, 2.4, 2]} t={t} />
326
+ </group>
327
+ );
328
+
329
+ const SetSala: React.FC<{t: number}> = ({t}) => (
330
+ <group>
331
+ <Cuarto ancho={9} fondo={7} alto={3.6} pared="#4A3B5C" lateral="#40334F" piso="#6E4A30" />
332
+ <mesh position={[0, 0.006, -1.0]} rotation={[-Math.PI / 2, 0, 0]}>
333
+ <planeGeometry args={[3.6, 2.6]} />
334
+ <meshStandardMaterial color="#8A3B3B" roughness={1} />
335
+ </mesh>
336
+ {/* mueble y tele */}
337
+ <Bloque pos={[0, 0.3, -3.1]} tam={[2.6, 0.6, 0.5]} color="#3A2A20" />
338
+ <Tele3D pos={[0, 1.25, -3.2]} ancho={1.9} t={t} fase={1.3} luz={2.6} />
339
+ <ConsolaYControl pos={[0.75, 0.6, -3.05]} t={t} />
340
+ <TiraLED pos={[0, 0.58, -3.32]} largo={2.4} t={t} fase={0.5} />
341
+ {/* sillón mirando a la tele */}
342
+ <group position={[0, 0, 0.9]}>
343
+ <Bloque pos={[0, 0.25, 0]} tam={[2.4, 0.5, 0.9]} color="#2F6FA0" radio={0.08} />
344
+ <Bloque pos={[0, 0.75, 0.38]} tam={[2.4, 0.7, 0.2]} color="#2F6FA0" radio={0.08} />
345
+ {[-1.25, 1.25].map((x) => (
346
+ <Bloque key={x} pos={[x, 0.45, 0]} tam={[0.22, 0.6, 0.9]} color="#285E88" radio={0.06} />
347
+ ))}
348
+ <Bloque pos={[-0.7, 0.6, 0.05]} tam={[0.5, 0.18, 0.5]} color="#F2B632" radio={0.08} />
349
+ </group>
350
+ {/* mesa de centro con tazas */}
351
+ <Bloque pos={[0, 0.38, -1.0]} tam={[1.4, 0.06, 0.7]} color="#C49A6C" />
352
+ {[-0.6, 0.6].map((x) => (
353
+ <Bloque key={x} pos={[x, 0.18, -1.0]} tam={[0.06, 0.36, 0.6]} color="#8A6A48" />
354
+ ))}
355
+ <mesh position={[0.3, 0.47, -0.95]}>
356
+ <cylinderGeometry args={[0.06, 0.05, 0.12, 12]} />
357
+ <meshStandardMaterial color="#EDEDED" />
358
+ </mesh>
359
+ {/* lámpara de pie, planta, cuadros, ventana */}
360
+ <group position={[-3.4, 0, -2.4]}>
361
+ <Bloque pos={[0, 0.8, 0]} tam={[0.05, 1.6, 0.05]} color="#2A2A30" />
362
+ <mesh position={[0, 1.7, 0]}>
363
+ <cylinderGeometry args={[0.2, 0.3, 0.32, 14, 1, true]} />
364
+ <meshStandardMaterial color="#F2D49B" emissive="#F2D49B" emissiveIntensity={0.8} side={THREE.DoubleSide} />
365
+ </mesh>
366
+ <pointLight position={[0, 1.6, 0.2]} intensity={3.2} distance={6} decay={1.5} color="#FFC88A" castShadow shadow-mapSize={[1024, 1024]} shadow-bias={-0.0005} />
367
+ </group>
368
+ <group position={[3.6, 0, -2.6]}>
369
+ <Bloque pos={[0, 0.2, 0]} tam={[0.4, 0.4, 0.4]} color="#C47A4A" />
370
+ {[0, 1, 2, 3, 4].map((k) => (
371
+ <mesh key={k} position={[Math.cos(k * 1.3) * 0.12, 0.65 + k * 0.08, Math.sin(k * 1.3) * 0.12]} rotation={[0.3 * Math.cos(k), k, 0.5]}>
372
+ <coneGeometry args={[0.14, 0.6, 4]} />
373
+ <meshStandardMaterial color="#3E9B5A" flatShading />
374
+ </mesh>
375
+ ))}
376
+ </group>
377
+ <Poster pos={[-2.0, 2.2, -3.38]} tipo="nave" />
378
+ <Poster pos={[2.0, 2.2, -3.38]} tipo="planeta" />
379
+ <VentanaLluvia pos={[-4.38, 1.9, -0.6]} t={t} semilla={4} />
380
+ <Gato3D pos={[1.25, 0.75, 0.95]} rot={[0, -30, 0]} t={t} escala={0.9} />
381
+ <pointLight position={[0, 3.2, 0]} intensity={1.6} distance={9} decay={1.6} color="#FFD9A0" />
382
+ <Polvo centro={[-2.4, 1.6, -1.8]} tam={[2.2, 2.2, 2]} t={t} />
383
+ </group>
384
+ );
385
+
386
+ const COLORES_ARCADE = ['#FF3B6B', '#3DB8FF', '#FFD23D', '#5ADC6E', '#B25BFF'];
387
+ const Maquinita: React.FC<{pos: V3; rot?: number; t: number; i: number}> = ({pos, rot = 0, t, i}) => {
388
+ const c = COLORES_ARCADE[i % COLORES_ARCADE.length];
389
+ const pant = hsl(0.1 * i + t * 0.05, 0.75, 0.5);
390
+ return (
391
+ <group position={pos} rotation={[0, rot, 0]}>
392
+ <Bloque pos={[0, 0.9, 0]} tam={[0.8, 1.8, 0.8]} color="#1C1C26" />
393
+ <Bloque pos={[-0.41, 0.9, 0]} tam={[0.03, 1.82, 0.82]} color={c} />
394
+ <Bloque pos={[0.41, 0.9, 0]} tam={[0.03, 1.82, 0.82]} color={c} />
395
+ <Emisivo pos={[0, 1.95, 0.32]} tam={[0.76, 0.22, 0.04]} color={c} fuerza={1.8} />
396
+ <mesh position={[0, 1.42, 0.37]} rotation={[-0.25, 0, 0]}>
397
+ <planeGeometry args={[0.6, 0.48]} />
398
+ <meshStandardMaterial color={pant} emissive={pant} emissiveIntensity={1.1 + 0.3 * Math.sin(t * 7 + i)} toneMapped={false} />
399
+ </mesh>
400
+ <Emisivo pos={[(Math.sin(t * 2 + i) * 0.2), 1.42 + Math.cos(t * 3 + i) * 0.1, 0.4]} tam={[0.06, 0.06, 0.005]} color="#FFFFFF" fuerza={1.5} rot={[-0.25, 0, 0]} />
401
+ <Bloque pos={[0, 1.02, 0.5]} tam={[0.78, 0.08, 0.3]} color="#2A2A36" />
402
+ <Emisivo pos={[-0.18, 1.08, 0.52]} tam={[0.05, 0.05, 0.05]} color="#FF3B30" fuerza={1} />
403
+ <Emisivo pos={[0.12, 1.08, 0.55]} tam={[0.05, 0.05, 0.05]} color="#3DB8FF" fuerza={1} />
404
+ <Emisivo pos={[0.22, 1.08, 0.5]} tam={[0.05, 0.05, 0.05]} color="#FFD23D" fuerza={1} />
405
+ <pointLight position={[0, 1.4, 0.9]} color={pant} intensity={1.4} distance={2.6} decay={1.5} />
406
+ </group>
407
+ );
408
+ };
409
+
410
+ const SetArcade: React.FC<{t: number}> = ({t}) => (
411
+ <group>
412
+ <Cuarto ancho={10} fondo={8} alto={3.8} pared="#1A1030" lateral="#160D2A" piso="#1E1A3A" />
413
+ {/* alfombra con figuras que brillan */}
414
+ {Array.from({length: 60}, (_, i) => (
415
+ <Emisivo key={i} pos={[(azarN(i * 2.1) - 0.5) * 9, 0.004, (azarN(i * 3.3) - 0.5) * 7]} tam={[0.12, 0.002, 0.12]} rot={[0, azarN(i) * 3, 0]} color={COLORES_ARCADE[i % 5]} fuerza={0.35} />
416
+ ))}
417
+ {[-3, -1.8, -0.6, 0.6, 1.8, 3].map((x, i) => (
418
+ <Maquinita key={x} pos={[x, 0, -3.4]} t={t} i={i} />
419
+ ))}
420
+ {[-2, -0.6, 0.8].map((z, i) => (
421
+ <Maquinita key={z} pos={[-4.4, 0, z]} rot={Math.PI / 2} t={t} i={i + 6} />
422
+ ))}
423
+ {/* letreros de neón: rayo y estrella */}
424
+ <group position={[2.6, 2.9, -3.88]}>
425
+ {[
426
+ [0, 0.3, 0.5, -30],
427
+ [0.05, 0, 0.3, 60],
428
+ [0.1, -0.3, 0.5, -30],
429
+ ].map(([x, y, l, r], i) => (
430
+ <Emisivo key={i} pos={[x, y, 0]} tam={[l, 0.06, 0.04]} rot={[0, 0, (r * Math.PI) / 180]} color="#FFD23D" fuerza={2.2 * (0.75 + 0.25 * (Math.sin(t * 13) > -0.8 ? 1 : 0))} />
431
+ ))}
432
+ <pointLight position={[0, 0, 0.6]} color="#FFD23D" intensity={2} distance={4} />
433
+ </group>
434
+ <group position={[-2.4, 2.9, -3.88]}>
435
+ {[0, 1, 2, 3, 4].map((k) => (
436
+ <Emisivo key={k} pos={[Math.cos((k * Math.PI * 2) / 5 + Math.PI / 2) * 0.22, Math.sin((k * Math.PI * 2) / 5 + Math.PI / 2) * 0.22, 0]} tam={[0.42, 0.05, 0.04]} rot={[0, 0, (k * 72 + 90 + 54) * (Math.PI / 180)]} color="#FF3B6B" fuerza={2.2} />
437
+ ))}
438
+ <pointLight position={[0, 0, 0.6]} color="#FF3B6B" intensity={2} distance={4} />
439
+ </group>
440
+ {/* focos del techo */}
441
+ {[-3, 0, 3].map((x) => (
442
+ <group key={x} position={[x, 3.7, -1]}>
443
+ <Emisivo pos={[0, 0, 0]} tam={[1.2, 0.04, 0.12]} color="#B9A8FF" fuerza={1.2} />
444
+ <pointLight position={[0, -0.3, 0]} intensity={1.3} distance={6} decay={1.6} color="#B9A8FF" />
445
+ </group>
446
+ ))}
447
+ <Polvo centro={[0, 1.8, -1.5]} tam={[6, 2.5, 3]} t={t} color="#D8C8FF" />
448
+ </group>
449
+ );
450
+
451
+ const AMBIENTES: Record<NombreAmbiente, Def> = {
452
+ 'cuarto-gamer': {
453
+ luz: 'noche',
454
+ fondo: '#0B0E1A',
455
+ niebla: [10, 26],
456
+ post: 'cine',
457
+ planos: {
458
+ general: {pos: [2.6, 2.6, 3.4], mirar: [-0.7, 1.15, -1.6], fov: 50, a: {pos: [2.1, 2.4, 2.8]}},
459
+ persona: {pos: [0.7, 1.55, -2.15], mirar: [-1.0, 1.2, -0.95], fov: 44, a: {pos: [0.55, 1.5, -2.05]}},
460
+ escritorio: {pos: [0.2, 1.8, 0.5], mirar: [-1.0, 1.25, -2.5], fov: 42, a: {pos: [0.05, 1.7, 0.2]}},
461
+ tele: {pos: [0.5, 1.75, 0.3], mirar: [-1.0, 1.25, -2.72], fov: 42, a: {pos: [0.35, 1.7, 0.0]}},
462
+ consola: {pos: [1.0, 1.9, -0.5], mirar: [-0.5, 0.95, -2.35], fov: 42, a: {pos: [0.85, 1.82, -0.75]}},
463
+ estante: {pos: [-1.9, 1.75, 0.8], mirar: [-3.8, 1.55, -1.2], fov: 42, a: {pos: [-2.1, 1.7, 0.5]}},
464
+ ventana: {pos: [0.9, 1.75, 0.8], mirar: [2.3, 1.9, -2.88], fov: 44, a: {pos: [1.05, 1.75, 0.5]}},
465
+ contrapicado: {pos: [0.9, 0.45, -2.2], mirar: [-1.0, 1.35, -0.95], fov: 50},
466
+ cenital: {pos: [-0.9, 3.4, 0.4], mirar: [-1.0, 0.7, -1.5], fov: 52, a: {pos: [-0.9, 3.3, 0.1]}},
467
+ },
468
+ orden: ['general', 'persona', 'escritorio', 'tele', 'contrapicado', 'consola', 'estante', 'ventana', 'cenital'],
469
+ ordenFondo: ['general', 'escritorio', 'estante', 'ventana', 'cenital', 'consola'],
470
+ anclas: {tele: [-1.0, 1.42, -2.72], consola: [-0.45, 0.92, -2.35], control: [0.1, 0.86, -2.1], escritorio: [-1.0, 0.8, -2.4], persona: [-1.0, 1.3, -0.95], estante: [-3.8, 1.5, -1.2], ventana: [2.3, 2.1, -2.88], cama: [2.75, 0.6, -1.6], silla: [-1.0, 0.9, -0.95]},
471
+ persona: {pos: [-1.0, 0, -0.98], rot: [0, 180, 0], pose: 'sentado'},
472
+ Set: SetCuartoGamer,
473
+ },
474
+ sala: {
475
+ luz: 'noche',
476
+ fondo: '#0E0B16',
477
+ niebla: [11, 28],
478
+ post: 'cine',
479
+ planos: {
480
+ general: {pos: [3.0, 2.7, 4.2], mirar: [0, 0.9, -1.2], fov: 50, a: {pos: [2.5, 2.5, 3.6]}},
481
+ persona: {pos: [0.5, 1.3, -1.6], mirar: [0, 1.15, 0.9], fov: 44, a: {pos: [0.4, 1.28, -1.4]}},
482
+ tele: {pos: [0.7, 1.6, -0.3], mirar: [0, 1.15, -3.2], fov: 42, a: {pos: [0.6, 1.55, -0.6]}},
483
+ sillon: {pos: [2.6, 1.6, -0.6], mirar: [0, 0.8, 0.9], fov: 42, a: {pos: [2.3, 1.5, -0.4]}},
484
+ gato: {pos: [2.4, 1.45, -0.5], mirar: [1.2, 0.9, 0.95], fov: 40, a: {pos: [2.25, 1.4, -0.35]}},
485
+ espalda: {pos: [0.6, 1.9, 3.0], mirar: [0, 1.1, -2.6], fov: 44, a: {pos: [0.5, 1.8, 2.6]}},
486
+ lampara: {pos: [-1.6, 1.6, 0.4], mirar: [-3.4, 1.4, -2.4], fov: 44},
487
+ ventana: {pos: [-1.8, 1.7, 1.6], mirar: [-4.38, 1.9, -0.6], fov: 44, a: {pos: [-2.0, 1.7, 1.3]}},
488
+ },
489
+ orden: ['general', 'persona', 'espalda', 'tele', 'sillon', 'gato', 'ventana', 'lampara'],
490
+ ordenFondo: ['general', 'espalda', 'ventana', 'gato', 'lampara', 'sillon'],
491
+ anclas: {tele: [0, 1.25, -3.2], consola: [0.75, 0.68, -3.05], persona: [0, 1.2, 0.9], sillon: [0, 0.6, 0.9], gato: [1.25, 0.95, 0.95], mesa: [0, 0.45, -1.0], ventana: [-4.38, 1.9, -0.6], lampara: [-3.4, 1.6, -2.4]},
492
+ persona: {pos: [0, 0.05, 0.88], rot: [0, 180, 0], pose: 'sentado'},
493
+ Set: SetSala,
494
+ },
495
+ arcade: {
496
+ luz: 'noche',
497
+ fondo: '#08061A',
498
+ niebla: [9, 24],
499
+ post: 'cine',
500
+ planos: {
501
+ general: {pos: [3.2, 2.6, 3.6], mirar: [-0.4, 1.3, -2.2], fov: 50, a: {pos: [2.6, 2.4, 3.0]}},
502
+ persona: {pos: [1.7, 1.6, -2.7], mirar: [0.6, 1.4, -0.95], fov: 44, a: {pos: [1.6, 1.58, -2.55]}},
503
+ maquina: {pos: [1.3, 1.75, -1.2], mirar: [0.6, 1.35, -3.4], fov: 42, a: {pos: [1.2, 1.7, -1.45]}},
504
+ pasillo: {pos: [-3.6, 1.6, 2.6], mirar: [-4.0, 1.2, -2.2], fov: 46, a: {pos: [-3.5, 1.6, 2.1]}},
505
+ neon: {pos: [1.4, 2.4, -1.2], mirar: [2.6, 2.9, -3.88], fov: 42},
506
+ contrapicado: {pos: [1.8, 0.5, 0.4], mirar: [0.4, 1.3, -3.0], fov: 52},
507
+ espalda: {pos: [2.3, 1.9, 1.3], mirar: [-0.2, 1.35, -3.4], fov: 46, a: {pos: [2.15, 1.85, 1.0]}},
508
+ },
509
+ orden: ['general', 'persona', 'maquina', 'espalda', 'pasillo', 'contrapicado'],
510
+ ordenFondo: ['general', 'espalda', 'pasillo', 'maquina'],
511
+ anclas: {maquina: [0.6, 1.42, -3.0], persona: [0.6, 1.6, -0.9], neon: [2.6, 2.9, -3.88], pasillo: [-4.4, 1.4, -0.6]},
512
+ persona: {pos: [0.6, 0, -0.95], rot: [0, 180, 0], pose: 'parado'},
513
+ Set: SetArcade,
514
+ },
515
+ };
516
+
517
+ export const NOMBRES_AMBIENTES = Object.keys(AMBIENTES) as NombreAmbiente[];
518
+ export const planosDe = (nombre: NombreAmbiente) => Object.keys(AMBIENTES[nombre]?.planos ?? {});
519
+ export const anclaDe = (nombre: NombreAmbiente, ancla: string): V3 | undefined => AMBIENTES[nombre]?.anclas[ancla];
520
+
521
+ /**
522
+ * El plano de cada escena: el que dice (nombre o toma propia) o el siguiente de `orden` sin repetir el anterior.
523
+ * Las escenas con capas 2D al centro (`conCapas[i]`) usan `ordenFondo`: planos abiertos, sin la cara tapando la capa.
524
+ */
525
+ export function tomasAmbiente(nombre: NombreAmbiente, planos: (string | Toma3D | null | undefined)[], n: number, conCapas: boolean[] = []): Toma3D[] {
526
+ const def = AMBIENTES[nombre];
527
+ const out: Toma3D[] = [];
528
+ const k = {orden: 0, ordenFondo: 0};
529
+ for (let i = 0; i < n; i++) {
530
+ const p = planos[i];
531
+ if (p && typeof p === 'object') out.push(p);
532
+ else if (typeof p === 'string' && def.planos[p]) out.push(def.planos[p]);
533
+ else {
534
+ const lista = conCapas[i] ? 'ordenFondo' : 'orden';
535
+ let toma = def.planos[def[lista][k[lista]++ % def[lista].length]];
536
+ // no repetir el mismo plano que la escena anterior
537
+ if (i > 0 && out[i - 1] === toma) toma = def.planos[def[lista][k[lista]++ % def[lista].length]];
538
+ out.push(toma);
539
+ }
540
+ }
541
+ return out;
542
+ }
543
+
544
+ /**
545
+ * Reacciones de la persona según lo que dice cada escena: pregunta → pensativa, exclamación o cifra → sorpresa,
546
+ * "felicidades"/"ganaste" → brazos arriba y feliz. `escenas` = [{say}] e `inicios` = segundo en que empieza cada una.
547
+ */
548
+ export function reaccionesAuto(escenas: {say?: string}[], inicios: number[]): Reaccion[] {
549
+ const out: Reaccion[] = [];
550
+ escenas.forEach((e, i) => {
551
+ const s = (e.say ?? '').toLowerCase();
552
+ const en = (inicios[i] ?? 0) + 0.3;
553
+ if (/felicidades|ganaste|lo lograste|victoria|récord|record/.test(s)) out.push({en, expresion: 'feliz', pose: 'brazosArriba'});
554
+ else if (/[¡!]/.test(s) || /\d/.test(s)) out.push({en, expresion: 'sorpresa'});
555
+ else if (/[¿?]/.test(s)) out.push({en, expresion: 'pensativo'});
556
+ else out.push({en, expresion: 'neutral'});
557
+ });
558
+ return out;
559
+ }
560
+
561
+ export type PropsAmbiente = {
562
+ nombre: NombreAmbiente;
563
+ t: number;
564
+ escena?: number;
565
+ tEscena?: number;
566
+ inicios?: number[];
567
+ /** Plano por escena (nombre o toma propia); los que falten se eligen solos. */
568
+ planos?: (string | Toma3D | null | undefined)[];
569
+ /** Por escena: ¿lleva capas 2D al centro? (esas usan planos abiertos). */
570
+ conCapas?: boolean[];
571
+ /** La persona del set: true, o su ropa/pelo/piel. */
572
+ persona?: boolean | {ropa?: string; pelo?: string; piel?: string; semilla?: number};
573
+ reacciones?: Reaccion[];
574
+ post?: Post3D;
575
+ /** Etiquetas 3D; `ancla` en vez de `pos` apunta a un objeto del set por nombre ('tele', 'consola'...). */
576
+ etiquetas?: (Omit<Etiqueta3D, 'pos'> & {pos?: V3; ancla?: string})[];
577
+ encima?: React.ReactNode;
578
+ /** El `cuando` del motor: deja usar palabras en `en` de las etiquetas. */
579
+ cuando?: (en: unknown) => number;
580
+ children?: React.ReactNode;
581
+ };
582
+
583
+ /**
584
+ * Tramos de cámara: cada escena empieza con su plano y, en cada `corte` (segundos dentro de la escena, normalmente la
585
+ * palabra clave), salta en seco a otro ángulo del mismo set. Así una escena larga no se queda en una sola toma.
586
+ */
587
+ function tramos(nombre: NombreAmbiente, base: Toma3D[], inicios: number[], cortes: number[][], conCapas: boolean[]) {
588
+ const def = AMBIENTES[nombre];
589
+ const out: {ini: number; toma: Toma3D; escena: number}[] = [];
590
+ base.forEach((toma, i) => {
591
+ const ini = inicios[i] ?? (out.length ? out[out.length - 1].ini + 3 : 0);
592
+ out.push({ini, toma, escena: i + 1});
593
+ const lista = (conCapas[i] ? def.ordenFondo : def.orden).map((n) => def.planos[n]);
594
+ let j = Math.max(0, lista.indexOf(toma));
595
+ for (const c of cortes[i] ?? []) {
596
+ j = (j + 2) % lista.length;
597
+ if (lista[j] === toma || lista[j] === base[i + 1]) j = (j + 1) % lista.length;
598
+ out.push({ini: ini + c, toma: {...lista[j], corte: true}, escena: i + 1});
599
+ }
600
+ });
601
+ return out;
602
+ }
603
+
604
+ /** Un ambiente completo dentro de su propio Lienzo3D (un solo contexto WebGL). `children` va dentro del set. */
605
+ export const Ambiente: React.FC<PropsAmbiente & {cortes?: number[][]}> = ({nombre, t, escena = 1, tEscena = 0, inicios = [0], planos = [], conCapas = [], cortes = [], persona = false, reacciones = [], post, etiquetas = [], encima, cuando, children}) => {
606
+ const def = AMBIENTES[nombre];
607
+ if (!def) throw new Error(`No hay un ambiente "${nombre}". Opciones: ${NOMBRES_AMBIENTES.join(', ')}`);
608
+ const base = tomasAmbiente(nombre, planos, Math.max(inicios.length, escena), conCapas);
609
+ const tr = tramos(nombre, base, inicios, cortes, conCapas);
610
+ const T = (inicios[escena - 1] ?? 0) + tEscena;
611
+ let k = 0;
612
+ for (let i = 0; i < tr.length; i++) if (tr[i].ini <= T + 1e-6) k = i;
613
+ const p = typeof persona === 'object' ? persona : {};
614
+ // etiquetas: su `escena`/`en` son de la escena; se reparten en sus tramos con el tiempo corrido
615
+ const etq: Etiqueta3D[] = etiquetas.flatMap((e) => {
616
+ const pos = e.pos ?? (e.ancla ? def.anclas[e.ancla] : undefined);
617
+ if (!pos) return [];
618
+ const escenas = e.escena == null ? null : ([] as number[]).concat(e.escena);
619
+ return tr.flatMap((s, i) => {
620
+ if (escenas && !escenas.includes(s.escena)) return [];
621
+ const corrido = s.ini - (inicios[s.escena - 1] ?? 0);
622
+ return [{...e, pos, escena: i + 1, en: typeof e.en === 'string' ? e.en : Math.max(0, (e.en ?? 0.2) - corrido), hasta: e.hasta != null ? e.hasta - corrido : undefined}];
623
+ });
624
+ });
625
+ const Set = def.Set;
626
+ return (
627
+ <Lienzo3D tomas={tr.map((s) => s.toma)} escena={k + 1} tEscena={T - tr[k].ini} inicios={tr.map((s) => s.ini)} luz={def.luz} fondo={def.fondo} niebla={def.niebla} post={post ?? def.post} etiquetas={etq} encima={encima} despejado cuando={cuando}>
628
+ <Set t={t} />
629
+ {persona ? (
630
+ <Persona3D pos={def.persona.pos} rot={def.persona.rot} t={t} pose={def.persona.pose} ropa={p.ropa ?? '#2F6FE0'} pelo={p.pelo} piel={p.piel} semilla={p.semilla ?? 2} estados={reacciones} />
631
+ ) : null}
632
+ {children}
633
+ </Lienzo3D>
634
+ );
635
+ };