morfeo 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +210 -2
- package/assets/fuentes/Anton.ttf +0 -0
- package/assets/fuentes/JetBrainsMono.ttf +0 -0
- package/assets/fuentes/Montserrat.ttf +0 -0
- package/bin/morfeo.mjs +184 -0
- package/bin/postinstall.mjs +10 -0
- package/index.d.ts +250 -0
- package/lib/config.mjs +284 -0
- package/lib/entorno.mjs +93 -0
- package/lib/guion.mjs +313 -0
- package/lib/imagenes.mjs +99 -0
- package/lib/index.mjs +15 -0
- package/lib/instalar.mjs +193 -0
- package/lib/navegador.mjs +64 -0
- package/lib/personajes.mjs +94 -0
- package/lib/render.mjs +312 -0
- package/lib/voces.mjs +122 -0
- package/lib/whisper.mjs +45 -0
- package/package.json +84 -5
- package/plantilla/assets/.gitkeep +0 -0
- package/plantilla/env.example +7 -0
- package/plantilla/escenas/Barras.tsx +15 -0
- package/plantilla/gitignore +4 -0
- package/plantilla/guiones/cachalote.mjs +24 -0
- package/plantilla/guiones/tutorial.mjs +16 -0
- package/plantilla/morfeo.config.mjs +43 -0
- package/plantilla/package.json +13 -0
- package/plantilla/videos.mjs +25 -0
- package/python/musica.py +221 -0
- package/python/preparar.py +268 -0
- package/python/requirements.txt +5 -0
- package/remotion/Root.tsx +35 -0
- package/remotion/Short.tsx +278 -0
- package/remotion/escenas.tsx +430 -0
- package/remotion/estilo.ts +73 -0
- package/remotion/iconos.ts +326 -0
- package/remotion/index.ts +4 -0
- package/remotion/motor/Video.tsx +121 -0
- package/remotion/motor/capas.tsx +622 -0
- package/remotion/motor/fondo.tsx +148 -0
- package/remotion/motor/modelo3d.tsx +252 -0
- package/remotion/motor/plantillas.tsx +125 -0
- package/remotion/motor/registro.ts +11 -0
- package/remotion/motor/subtitulos.tsx +137 -0
- package/remotion/motor/tema.ts +207 -0
- package/remotion/motor/util.ts +66 -0
- package/remotion/motor/widgets.tsx +248 -0
- package/remotion/piezas.tsx +181 -0
- package/remotion/tipos.ts +43 -0
- package/remotion/tres/Capa3D.tsx +153 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// Motor de capas: cada escena es una lista de capas (imagen, texto, contador, chip, tarjeta, teléfono,
|
|
2
|
+
// forma, grupo...) con su posición y sus animaciones. Las coordenadas son del lienzo 1080×1920.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {Img, OffthreadVideo} from 'remotion';
|
|
5
|
+
import {Avatar, Icono, tamIcono, useMarca} from '../piezas';
|
|
6
|
+
import {H, W, backOut, clamp, easeIO, easeOut, lerp} from '../estilo';
|
|
7
|
+
import {azar, cuando, formatoNumero, partesResaltadas, recurso, type CtxEscena} from './util';
|
|
8
|
+
import {Particulas} from './fondo';
|
|
9
|
+
import {Escena3D, Modelo3D} from './modelo3d';
|
|
10
|
+
import {componente, nombresComponentes} from './registro';
|
|
11
|
+
import {Barra, Chat, Estado, Globo, ImagenCambia, Medidor, Noticia, Onda, Seguir, Votacion} from './widgets';
|
|
12
|
+
|
|
13
|
+
export type Capa = Record<string, any> & {tipo: string};
|
|
14
|
+
|
|
15
|
+
const ENTRADA_DUR = 0.5;
|
|
16
|
+
|
|
17
|
+
// el personaje de una serie sigue en pantalla de escena en escena: no "reaparece" en cada una
|
|
18
|
+
const entradaPorDefecto = (c: Capa) => (c.tipo === 'personaje' ? 'ninguna' : 'escala');
|
|
19
|
+
|
|
20
|
+
/** Posición, escala, giro y opacidad de una capa en el instante t (entrada, movimiento, salida). */
|
|
21
|
+
function animar(c: Capa, ctx: CtxEscena, relativo: boolean) {
|
|
22
|
+
const t = ctx.t;
|
|
23
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
24
|
+
const dur = c.duracionEntrada ?? ENTRADA_DUR;
|
|
25
|
+
const p = dur > 0 ? clamp((t - t0) / dur) : t >= t0 ? 1 : 0;
|
|
26
|
+
let x = c.x ?? (relativo ? 0 : W / 2);
|
|
27
|
+
let y = c.y ?? (relativo ? 0 : H / 2);
|
|
28
|
+
let s = c.escala ?? 1;
|
|
29
|
+
let r = c.rotar ?? 0;
|
|
30
|
+
let o = c.opacidad ?? 1;
|
|
31
|
+
if (t < t0) return null; // con entrada 'ninguna' aparece de golpe en su momento
|
|
32
|
+
switch (c.entrada ?? entradaPorDefecto(c)) {
|
|
33
|
+
case 'aparecer':
|
|
34
|
+
o *= easeOut(p);
|
|
35
|
+
break;
|
|
36
|
+
case 'escala':
|
|
37
|
+
s *= easeOut(p);
|
|
38
|
+
o *= clamp(p * 3);
|
|
39
|
+
break;
|
|
40
|
+
case 'rebote':
|
|
41
|
+
s *= backOut(p);
|
|
42
|
+
break;
|
|
43
|
+
case 'deslizar-izq':
|
|
44
|
+
x -= (1 - easeOut(p)) * W;
|
|
45
|
+
break;
|
|
46
|
+
case 'deslizar-der':
|
|
47
|
+
x += (1 - easeOut(p)) * W;
|
|
48
|
+
break;
|
|
49
|
+
case 'deslizar-arriba':
|
|
50
|
+
y += (1 - easeOut(p)) * H * 0.6;
|
|
51
|
+
break;
|
|
52
|
+
case 'deslizar-abajo':
|
|
53
|
+
y -= (1 - easeOut(p)) * H * 0.6;
|
|
54
|
+
break;
|
|
55
|
+
case 'caer':
|
|
56
|
+
y -= (1 - backOut(p, 1.2)) * 700;
|
|
57
|
+
o *= clamp(p * 4);
|
|
58
|
+
break;
|
|
59
|
+
case 'girar':
|
|
60
|
+
r += (1 - easeOut(p)) * -200;
|
|
61
|
+
s *= easeOut(p);
|
|
62
|
+
break;
|
|
63
|
+
case 'subir':
|
|
64
|
+
y += (1 - easeOut(p)) * 80;
|
|
65
|
+
o *= easeOut(p);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
// movimiento: mover: {a: [x, y], en?, duracion?, curva?: 'suave' | 'lineal'}
|
|
69
|
+
const mv = c.mover;
|
|
70
|
+
if (mv?.a) {
|
|
71
|
+
const m0 = cuando(mv.en, ctx, t0);
|
|
72
|
+
const md = mv.duracion ?? Math.max(0.3, ctx.dur - m0);
|
|
73
|
+
const k = clamp((t - m0) / md);
|
|
74
|
+
const f = mv.curva === 'lineal' ? k : easeIO(k);
|
|
75
|
+
x = lerp(x, mv.a[0] ?? x, f);
|
|
76
|
+
y = lerp(y, mv.a[1] ?? y, f);
|
|
77
|
+
}
|
|
78
|
+
// escalar: {a: 1.5, en?, duracion?}
|
|
79
|
+
const es = c.escalar;
|
|
80
|
+
if (es?.a != null) {
|
|
81
|
+
const e0 = cuando(es.en, ctx, t0);
|
|
82
|
+
const k = easeIO(clamp((t - e0) / (es.duracion ?? 0.6)));
|
|
83
|
+
s *= lerp(1, es.a / (c.escala ?? 1), k);
|
|
84
|
+
}
|
|
85
|
+
if (c.girar) r += c.girar * (t - t0); // grados por segundo
|
|
86
|
+
if (c.flotar) {
|
|
87
|
+
const a = c.flotar === true ? 14 : c.flotar;
|
|
88
|
+
const fase = azar(ctx.semilla + (c.id ?? c.src ?? c.texto ?? '')) * 6.28;
|
|
89
|
+
y += Math.sin(t * 1.8 + fase) * a;
|
|
90
|
+
r += Math.sin(t * 1.3 + fase) * (a / 10);
|
|
91
|
+
}
|
|
92
|
+
if (c.latir) s *= 1 + 0.04 * Math.sin(t * 7);
|
|
93
|
+
// salida: hasta: palabra o segundos; sale con la animación `salida` (aparecer = desvanecer)
|
|
94
|
+
if (c.hasta != null) {
|
|
95
|
+
const h = cuando(c.hasta, ctx, ctx.dur);
|
|
96
|
+
const q = clamp((t - h) / 0.35);
|
|
97
|
+
if (q >= 1) return null;
|
|
98
|
+
if (q > 0) {
|
|
99
|
+
const sal = c.salida ?? 'aparecer';
|
|
100
|
+
if (sal === 'escala') s *= 1 - easeOut(q);
|
|
101
|
+
else if (sal === 'deslizar-izq') x -= easeOut(q) * W;
|
|
102
|
+
else if (sal === 'deslizar-der') x += easeOut(q) * W;
|
|
103
|
+
else if (sal === 'subir') y -= easeOut(q) * 300;
|
|
104
|
+
o *= 1 - q;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (s <= 0.001 || o <= 0.001) return null;
|
|
108
|
+
return {x, y, s, r, o};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sombra = (c: Capa) => (c.sombra === false ? undefined : c.sombra === true || c.sombra == null ? 'drop-shadow(0 18px 24px rgba(0,0,0,0.35))' : c.sombra);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cambios en el tiempo (sobre todo para el HUD): cambios: [{en: 'palabra' | segundos | {escena: 3}, valor: 15}, ...].
|
|
115
|
+
* Los números (valor, a) se animan desde el valor anterior; lo demás cambia de golpe.
|
|
116
|
+
*/
|
|
117
|
+
function aplicarCambios(c: Capa, ctx: CtxEscena): Capa {
|
|
118
|
+
if (!Array.isArray(c.cambios) || !c.cambios.length) return c;
|
|
119
|
+
const momento = (en: any) => (en && typeof en === 'object' && en.escena != null ? ctx.inicios?.[en.escena - 1] ?? 0 : cuando(en, ctx, 0));
|
|
120
|
+
const lista = c.cambios.map((k: any) => ({...k, __t: momento(k.en)})).sort((a: any, b: any) => a.__t - b.__t);
|
|
121
|
+
let actual: Capa = {...c};
|
|
122
|
+
let previo: Capa = c;
|
|
123
|
+
for (const k of lista) {
|
|
124
|
+
if (ctx.t < k.__t) break;
|
|
125
|
+
previo = actual;
|
|
126
|
+
const {en, __t, ...resto} = k;
|
|
127
|
+
actual = {...actual, ...resto, __cambio: __t - cuando(c.en, ctx, 0)};
|
|
128
|
+
if (typeof resto.valor === 'number') actual.__valorPrevio = previo.valor ?? 0;
|
|
129
|
+
if (c.tipo === 'contador' && typeof resto.a === 'number') actual = {...actual, de: previo.a ?? previo.de ?? 0, en: __t, duracion: resto.duracion ?? 0.8};
|
|
130
|
+
}
|
|
131
|
+
return actual;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Una capa con su animación aplicada. */
|
|
135
|
+
export const CapaVista: React.FC<{c: Capa; ctx: CtxEscena; relativo?: boolean; i?: number}> = ({c: c0, ctx, relativo = false, i = 0}) => {
|
|
136
|
+
const c = aplicarCambios(c0, ctx);
|
|
137
|
+
const a = animar(c, ctx, relativo);
|
|
138
|
+
if (!a) return null;
|
|
139
|
+
const ancla = c.ancla === 'izquierda' ? '0%' : c.ancla === 'derecha' ? '-100%' : '-50%';
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
position: 'absolute',
|
|
144
|
+
left: a.x,
|
|
145
|
+
top: a.y,
|
|
146
|
+
transform: `translate(${ancla}, -50%) scale(${a.s}) rotate(${a.r}deg)${c.voltear ? ' scaleX(-1)' : ''}`,
|
|
147
|
+
transformOrigin: c.ancla === 'izquierda' ? 'left center' : c.ancla === 'derecha' ? 'right center' : 'center',
|
|
148
|
+
opacity: a.o,
|
|
149
|
+
filter: [c.tipo === 'imagen' || c.tipo === 'emoji' ? sombra(c) : undefined, c.desenfoque ? `blur(${c.desenfoque}px)` : undefined].filter(Boolean).join(' ') || undefined,
|
|
150
|
+
zIndex: c.z ?? i,
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<Contenido c={c} ctx={ctx} />
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
159
|
+
const T = ctx.tema;
|
|
160
|
+
const fuente = (f?: string) => (f === 'mono' ? T.fuentes.mono : f === 'texto' ? T.fuentes.texto : f === 'titulo' || !f ? T.fuentes.titulo : f);
|
|
161
|
+
switch (c.tipo) {
|
|
162
|
+
case 'imagen':
|
|
163
|
+
if (c.cambiarA) return <ImagenCambia c={c} ctx={ctx} />;
|
|
164
|
+
return <Img src={recurso(c.src)} style={{width: c.ancho ?? 500, height: c.alto ?? 'auto', display: 'block', borderRadius: c.redondo ? '50%' : c.radio ?? 0, objectFit: 'cover'}} />;
|
|
165
|
+
case 'video':
|
|
166
|
+
return <OffthreadVideo src={recurso(c.src)} muted style={{width: c.ancho ?? 600, height: c.alto ?? 'auto', display: 'block', borderRadius: c.radio ?? 0}} />;
|
|
167
|
+
case 'texto':
|
|
168
|
+
return <TextoCapa c={c} ctx={ctx} fuente={fuente(c.fuente)} />;
|
|
169
|
+
case 'contador':
|
|
170
|
+
return <Contador c={c} ctx={ctx} />;
|
|
171
|
+
case 'chip':
|
|
172
|
+
return <Chip c={c} ctx={ctx} />;
|
|
173
|
+
case 'etiqueta':
|
|
174
|
+
return (
|
|
175
|
+
<div style={{fontFamily: T.fuentes.mono, fontSize: c.tam ?? 34, color: c.color ?? T.colores.suave, whiteSpace: 'pre', letterSpacing: 0.5}}>
|
|
176
|
+
{c.texto}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
case 'numero': {
|
|
180
|
+
const tam = c.tam ?? 96;
|
|
181
|
+
return (
|
|
182
|
+
<div style={{width: tam, height: tam, background: c.fondo ?? T.colores.texto, color: c.color ?? (T.colores.texto === '#FFFFFF' ? '#000' : '#fff'), fontFamily: T.fuentes.titulo, fontSize: tam * 0.62, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: c.radio ?? 8}}>
|
|
183
|
+
{c.texto ?? c.valor}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
case 'tarjeta':
|
|
188
|
+
return <Tarjeta c={c} ctx={ctx} />;
|
|
189
|
+
case 'telefono':
|
|
190
|
+
return <Telefono c={c} ctx={ctx} />;
|
|
191
|
+
case 'ventana':
|
|
192
|
+
return <Ventana c={c} ctx={ctx} />;
|
|
193
|
+
case 'forma':
|
|
194
|
+
return <Forma c={c} ctx={ctx} />;
|
|
195
|
+
case 'icono': {
|
|
196
|
+
const u = tamIcono(c.nombre, c.tam ?? 300);
|
|
197
|
+
const lado = (c.tam ?? 300) * 1.2;
|
|
198
|
+
return (
|
|
199
|
+
<div style={{position: 'relative', width: lado, height: lado}}>
|
|
200
|
+
<Icono nombre={c.nombre} cx={lado / 2} cy={lado / 2} u={u} color={c.color} />
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
case 'emoji':
|
|
205
|
+
return <div style={{fontSize: c.tam ?? 220, lineHeight: 1}}>{c.texto}</div>;
|
|
206
|
+
case 'marca': {
|
|
207
|
+
const tam = c.tam ?? 200;
|
|
208
|
+
return (
|
|
209
|
+
<div style={{position: 'relative', width: tam + 20, height: tam + 20}}>
|
|
210
|
+
<Avatar x={0} y={0} size={tam} aro={10} />
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
case 'particulas':
|
|
215
|
+
return (
|
|
216
|
+
<div style={{position: 'relative', width: W, height: H}}>
|
|
217
|
+
<Particulas p={{tipo: c.particulas ?? 'polvo', cantidad: c.cantidad, color: c.color}} t={ctx.t} semilla={ctx.semilla} />
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
case 'grupo':
|
|
221
|
+
return <Grupo c={c} ctx={ctx} />;
|
|
222
|
+
case 'escena3d':
|
|
223
|
+
return <Escena3D c={c} ctx={ctx} />;
|
|
224
|
+
case 'personaje':
|
|
225
|
+
return <Personaje c={c} ctx={ctx} />;
|
|
226
|
+
case 'barra':
|
|
227
|
+
return <Barra c={c} ctx={ctx} />;
|
|
228
|
+
case 'medidor':
|
|
229
|
+
return <Medidor c={c} ctx={ctx} />;
|
|
230
|
+
case 'votacion':
|
|
231
|
+
return <Votacion c={c} ctx={ctx} />;
|
|
232
|
+
case 'estado':
|
|
233
|
+
return <Estado c={c} ctx={ctx} />;
|
|
234
|
+
case 'noticia':
|
|
235
|
+
return <Noticia c={c} ctx={ctx} />;
|
|
236
|
+
case 'chat':
|
|
237
|
+
return <Chat c={c} ctx={ctx} />;
|
|
238
|
+
case 'onda':
|
|
239
|
+
return <Onda c={c} ctx={ctx} />;
|
|
240
|
+
case 'globo':
|
|
241
|
+
return <Globo c={c} ctx={ctx} />;
|
|
242
|
+
case 'seguir':
|
|
243
|
+
return <Seguir c={c} ctx={ctx} />;
|
|
244
|
+
case 'modelo':
|
|
245
|
+
return <Modelo3D c={c} ctx={ctx} />;
|
|
246
|
+
case 'componente': {
|
|
247
|
+
const Comp = componente(c.nombre);
|
|
248
|
+
if (!Comp) throw new Error(`No hay un componente llamado "${c.nombre}". Regístralo en morfeo.config.mjs → componentes. Disponibles: ${nombresComponentes().join(', ') || 'ninguno'}`);
|
|
249
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
250
|
+
return <Comp {...(c.props ?? {})} t={ctx.t - t0} dur={ctx.dur} palabras={ctx.palabras} tema={ctx.tema} cuando={(en: unknown) => cuando(en, ctx) - t0} />;
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
return <div style={{color: 'red', fontSize: 40, fontFamily: 'sans-serif', background: '#fff', padding: 10}}>capa desconocida: {c.tipo}</div>;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const TextoCapa: React.FC<{c: Capa; ctx: CtxEscena; fuente: string}> = ({c, ctx, fuente}) => {
|
|
258
|
+
const T = ctx.tema;
|
|
259
|
+
const may = c.mayusculas ?? (fuente === T.fuentes.titulo && T.tituloMayusculas);
|
|
260
|
+
let texto: string = String(c.texto ?? '');
|
|
261
|
+
if (may) texto = texto.toUpperCase();
|
|
262
|
+
// escribir: aparece letra por letra
|
|
263
|
+
if (c.escribir) {
|
|
264
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
265
|
+
const vel = typeof c.escribir === 'number' ? c.escribir : 22; // letras por segundo
|
|
266
|
+
const n = Math.max(0, Math.floor((ctx.t - t0) * vel));
|
|
267
|
+
let cuenta = 0;
|
|
268
|
+
texto = texto.replace(/[^*]/g, (ch) => (cuenta++ < n ? ch : ch === '\n' ? '\n' : '')).replace(/\*\*/g, '');
|
|
269
|
+
}
|
|
270
|
+
const color = c.color ?? T.colores.texto;
|
|
271
|
+
const acento = c.acento ?? T.colores.acento;
|
|
272
|
+
const contorno = c.contorno ? `${c.contorno}px ${c.colorContorno ?? '#000'}` : undefined;
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
style={{
|
|
276
|
+
fontFamily: fuente,
|
|
277
|
+
fontSize: c.tam ?? 96,
|
|
278
|
+
fontWeight: c.peso ?? (fuente === T.fuentes.titulo ? 800 : 600),
|
|
279
|
+
lineHeight: c.interlineado ?? 1.05,
|
|
280
|
+
color,
|
|
281
|
+
textAlign: c.alinear ?? 'center',
|
|
282
|
+
maxWidth: c.maxAncho ?? 940,
|
|
283
|
+
whiteSpace: 'pre-wrap',
|
|
284
|
+
WebkitTextStroke: contorno,
|
|
285
|
+
paintOrder: 'stroke fill',
|
|
286
|
+
textShadow: c.sombra === false ? undefined : c.sombra ?? (color === '#FFFFFF' || color === '#fff' ? '0 6px 18px rgba(0,0,0,0.45)' : undefined),
|
|
287
|
+
background: c.fondo,
|
|
288
|
+
padding: c.fondo ? c.relleno ?? '10px 26px' : undefined,
|
|
289
|
+
borderRadius: c.fondo || c.borde ? c.radio ?? 14 : undefined,
|
|
290
|
+
border: c.borde ? `${c.grosor ?? 5}px solid ${c.borde}` : undefined,
|
|
291
|
+
width: c.ancho ?? (c.maxAncho === 'auto' ? undefined : 'max-content'),
|
|
292
|
+
boxSizing: 'border-box',
|
|
293
|
+
letterSpacing: c.espaciado,
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{partesResaltadas(texto).map(([s, r], i) =>
|
|
297
|
+
r ? (
|
|
298
|
+
<span key={i} style={{color: acento, ...(c.subrayado ? {textDecoration: 'underline', textDecorationThickness: 8, textUnderlineOffset: 10} : {})}}>
|
|
299
|
+
{s}
|
|
300
|
+
</span>
|
|
301
|
+
) : (
|
|
302
|
+
<React.Fragment key={i}>{s}</React.Fragment>
|
|
303
|
+
),
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const Contador: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
310
|
+
const T = ctx.tema;
|
|
311
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
312
|
+
const dur = c.duracion ?? Math.max(0.6, ctx.habla[1] - t0);
|
|
313
|
+
const k = clamp((ctx.t - t0) / dur);
|
|
314
|
+
const de = c.de ?? 0;
|
|
315
|
+
const a = c.a ?? c.valor ?? 0;
|
|
316
|
+
let v: number;
|
|
317
|
+
if (c.curva === 'exponencial' && de > 0 && a > 0) v = de * Math.pow(a / de, k);
|
|
318
|
+
else v = lerp(de, a, c.curva === 'lineal' ? k : easeOut(k));
|
|
319
|
+
const txt = `${c.prefijo ?? ''}${formatoNumero(v, c.decimales ?? 0, c.miles ?? '.', c.decimal ?? ',')}`;
|
|
320
|
+
const sufijo = c.sufijo ?? '';
|
|
321
|
+
const formato = c.formato ?? 'grande';
|
|
322
|
+
if (formato === 'lcd')
|
|
323
|
+
return (
|
|
324
|
+
<div style={{background: c.fondo ?? '#0B0B0B', border: `4px solid ${T.colores.borde}`, borderRadius: 14, padding: '10px 28px', fontFamily: T.fuentes.mono, fontWeight: 800, fontSize: c.tam ?? 78, color: c.color ?? T.colores.acento, textShadow: `0 0 18px ${c.color ?? T.colores.acento}`, whiteSpace: 'pre'}}>
|
|
325
|
+
{txt}
|
|
326
|
+
{sufijo}
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
if (formato === 'chip') return <Chip c={{...c, texto: txt, unidad: sufijo.trim()}} ctx={ctx} />;
|
|
330
|
+
return (
|
|
331
|
+
<div style={{fontFamily: T.fuentes.titulo, fontWeight: 900, fontSize: c.tam ?? 200, color: c.color ?? T.colores.acento, lineHeight: 1, whiteSpace: 'pre', textShadow: '0 10px 30px rgba(0,0,0,0.35)'}}>
|
|
332
|
+
{txt}
|
|
333
|
+
<span style={{fontSize: (c.tam ?? 200) * 0.45}}>{sufijo}</span>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const Chip: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
339
|
+
const T = ctx.tema;
|
|
340
|
+
const tam = c.tam ?? 64;
|
|
341
|
+
return (
|
|
342
|
+
<div
|
|
343
|
+
style={{
|
|
344
|
+
display: 'flex',
|
|
345
|
+
alignItems: 'baseline',
|
|
346
|
+
gap: 10,
|
|
347
|
+
background: c.fondo ?? T.colores.tarjeta,
|
|
348
|
+
border: `3px solid ${c.borde ?? 'rgba(255,255,255,0.12)'}`,
|
|
349
|
+
borderRadius: 16,
|
|
350
|
+
padding: `${tam * 0.18}px ${tam * 0.4}px`,
|
|
351
|
+
fontFamily: T.fuentes.texto,
|
|
352
|
+
fontWeight: 800,
|
|
353
|
+
fontSize: tam,
|
|
354
|
+
color: c.color ?? T.colores.acento,
|
|
355
|
+
whiteSpace: 'pre',
|
|
356
|
+
boxShadow: '0 12px 30px rgba(0,0,0,0.35)',
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{c.texto}
|
|
360
|
+
{c.unidad ? <span style={{fontSize: tam * 0.5, color: T.colores.suave, fontWeight: 600}}>{c.unidad}</span> : null}
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const Check: React.FC<{hecho: number; color: string; tam: number}> = ({hecho, color, tam}) => (
|
|
366
|
+
<div style={{width: tam, height: tam, borderRadius: tam * 0.25, border: `4px solid ${color}`, background: hecho > 0 ? color : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: tam * 0.7, fontWeight: 900, transform: `scale(${hecho > 0 ? backOut(hecho) : 1})`}}>
|
|
367
|
+
{hecho > 0 ? '✓' : ''}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const Tarjeta: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
372
|
+
const T = ctx.tema;
|
|
373
|
+
const ancho = c.ancho ?? 680;
|
|
374
|
+
const items: any[] = c.items ?? [];
|
|
375
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
376
|
+
return (
|
|
377
|
+
<div style={{width: ancho, background: c.fondo ?? T.colores.tarjeta, color: T.colores.tarjetaTexto, borderRadius: 22, padding: '34px 40px', boxShadow: '0 22px 50px rgba(0,0,0,0.25)', border: c.borde ? `4px solid ${c.borde}` : undefined, fontFamily: T.fuentes.texto}}>
|
|
378
|
+
{c.cabecera ? <div style={{fontFamily: T.fuentes.mono, fontSize: 26, color: T.colores.suave, marginBottom: 10}}>{c.cabecera}</div> : null}
|
|
379
|
+
{c.titulo ? <div style={{fontSize: c.tamTitulo ?? 52, fontWeight: 800, marginBottom: items.length ? 22 : 0, lineHeight: 1.1}}>{partesResaltadas(c.titulo).map(([s, r], i) => (r ? <span key={i} style={{color: T.colores.acento}}>{s}</span> : s))}</div> : null}
|
|
380
|
+
{c.texto ? <div style={{fontSize: 38, lineHeight: 1.3, color: T.colores.tarjetaTexto}}>{c.texto}</div> : null}
|
|
381
|
+
{items.map((it, i) => {
|
|
382
|
+
const item = typeof it === 'string' ? {texto: it} : it;
|
|
383
|
+
// cada renglón aparece en su palabra (o escalonado) y se palomea en `check`
|
|
384
|
+
const ti = cuando(item.en, ctx, t0 + 0.25 + i * 0.18);
|
|
385
|
+
const vis = clamp((ctx.t - ti) / 0.3);
|
|
386
|
+
const tc = item.check === true ? ti + 0.3 : item.check != null && item.check !== false ? cuando(item.check, ctx, ti) : null;
|
|
387
|
+
const hecho = tc == null ? 0 : clamp((ctx.t - tc) / 0.3);
|
|
388
|
+
if (vis <= 0) return null;
|
|
389
|
+
return (
|
|
390
|
+
<div key={i} style={{display: 'flex', alignItems: 'center', gap: 20, fontSize: c.tamItems ?? 42, fontWeight: 700, padding: '10px 0', opacity: vis, transform: `translateX(${(1 - easeOut(vis)) * 40}px)`}}>
|
|
391
|
+
{c.checks !== false ? <Check hecho={hecho} color={item.color ?? T.colores.acento} tam={(c.tamItems ?? 42) * 1.05} /> : null}
|
|
392
|
+
<span>{item.texto}</span>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
})}
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const Telefono: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
401
|
+
const ancho = c.ancho ?? 470;
|
|
402
|
+
const alto = ancho * 2.05;
|
|
403
|
+
const borde = ancho * 0.035;
|
|
404
|
+
return (
|
|
405
|
+
<div style={{width: ancho, height: alto, background: '#0A0A0C', borderRadius: ancho * 0.13, padding: borde, boxShadow: '0 30px 60px rgba(0,0,0,0.45), inset 0 0 0 3px #2A2A30', boxSizing: 'border-box'}}>
|
|
406
|
+
<div style={{position: 'relative', width: '100%', height: '100%', borderRadius: ancho * 0.1, overflow: 'hidden', background: c.pantalla ?? `linear-gradient(160deg, ${ctx.tema.colores.acento2}, ${ctx.tema.colores.acento} 60%, #111)`}}>
|
|
407
|
+
{/* sin imagen ni capas: una pantalla de app genérica en vez de un rectángulo negro */}
|
|
408
|
+
{!c.src && !c.capas?.length ? <PantallaGenerica ancho={ancho} t={ctx.t} /> : null}
|
|
409
|
+
{c.src ? (
|
|
410
|
+
/\.(mp4|webm|mov)$/i.test(c.src) ? (
|
|
411
|
+
<OffthreadVideo src={recurso(c.src)} muted style={{width: '100%', height: '100%', objectFit: 'cover'}} />
|
|
412
|
+
) : (
|
|
413
|
+
<Img src={recurso(c.src)} style={{width: '100%', height: '100%', objectFit: 'cover'}} />
|
|
414
|
+
)
|
|
415
|
+
) : null}
|
|
416
|
+
{/* las capas de la pantalla usan coordenadas relativas a su centro */}
|
|
417
|
+
<div style={{position: 'absolute', left: '50%', top: '50%', width: 0, height: 0}}>
|
|
418
|
+
{(c.capas ?? []).map((h: Capa, i: number) => (
|
|
419
|
+
<CapaVista key={i} c={h} ctx={ctx} relativo i={i} />
|
|
420
|
+
))}
|
|
421
|
+
</div>
|
|
422
|
+
<div style={{position: 'absolute', top: borde, left: '50%', transform: 'translateX(-50%)', width: ancho * 0.28, height: ancho * 0.065, background: '#000', borderRadius: 99}} />
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ventana circular (escotilla, lupa, mirilla): muestra una imagen o capas adentro, con borde metálico y remaches.
|
|
430
|
+
* {tipo: 'ventana', diametro: 640, src: 'interior.png'} o {tipo: 'ventana', capas: [...]} (coordenadas desde el centro)
|
|
431
|
+
* borde: 'metal' | 'ninguno' | '#color' · remaches · zoom: [inicio, fin] para acercar el contenido · fondo: color detrás
|
|
432
|
+
*/
|
|
433
|
+
const Ventana: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
434
|
+
const d = c.diametro ?? 640;
|
|
435
|
+
const aro = c.borde === 'ninguno' ? 0 : c.grosor ?? Math.round(d * 0.06);
|
|
436
|
+
const metal = c.borde && c.borde !== 'metal' && c.borde !== 'ninguno' ? c.borde : null;
|
|
437
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
438
|
+
const k = clamp((ctx.t - t0) / Math.max(0.5, ctx.dur - t0));
|
|
439
|
+
const zoom = Array.isArray(c.zoom) ? lerp(c.zoom[0], c.zoom[1], easeIO(k)) : c.zoom ?? 1;
|
|
440
|
+
const n = c.remaches === false ? 0 : c.remaches ?? 16;
|
|
441
|
+
const total = d + aro * 2;
|
|
442
|
+
return (
|
|
443
|
+
<div style={{position: 'relative', width: total, height: total}}>
|
|
444
|
+
<div
|
|
445
|
+
style={{
|
|
446
|
+
position: 'absolute',
|
|
447
|
+
inset: 0,
|
|
448
|
+
borderRadius: '50%',
|
|
449
|
+
background: metal ?? 'conic-gradient(from 210deg, #8C96A3, #E3E8EE, #7A8592, #C9D0D8, #6E7884, #DDE3EA, #8C96A3)',
|
|
450
|
+
boxShadow: '0 24px 50px rgba(0,0,0,0.45), inset 0 0 0 3px rgba(0,0,0,0.35)',
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
<div style={{position: 'absolute', left: aro, top: aro, width: d, height: d, borderRadius: '50%', overflow: 'hidden', background: c.fondo ?? '#0C1A2A', boxShadow: 'inset 0 0 40px rgba(0,0,0,0.6)'}}>
|
|
454
|
+
<div style={{position: 'absolute', inset: 0, transform: `scale(${zoom})`}}>
|
|
455
|
+
{c.src ? <Img src={recurso(c.src)} style={{width: '100%', height: '100%', objectFit: 'cover'}} /> : null}
|
|
456
|
+
<div style={{position: 'absolute', left: '50%', top: '50%', width: 0, height: 0}}>
|
|
457
|
+
{(c.capas ?? []).map((h: Capa, i: number) => (
|
|
458
|
+
<CapaVista key={i} c={h} ctx={ctx} relativo i={i} />
|
|
459
|
+
))}
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
{/* brillo del vidrio */}
|
|
463
|
+
<div style={{position: 'absolute', inset: 0, borderRadius: '50%', background: 'radial-gradient(circle at 30% 25%, rgba(255,255,255,0.22), transparent 40%)'}} />
|
|
464
|
+
</div>
|
|
465
|
+
{Array.from({length: n}, (_, i) => {
|
|
466
|
+
const a = (i / n) * Math.PI * 2;
|
|
467
|
+
const r = d / 2 + aro / 2;
|
|
468
|
+
return <div key={i} style={{position: 'absolute', left: total / 2 + Math.cos(a) * r - aro * 0.16, top: total / 2 + Math.sin(a) * r - aro * 0.16, width: aro * 0.32, height: aro * 0.32, borderRadius: '50%', background: 'radial-gradient(circle at 35% 35%, #F2F5F8, #6E7884)'}} />;
|
|
469
|
+
})}
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/** Pantalla de relleno: barra de app, un video con botón de play y renglones de texto. */
|
|
475
|
+
const PantallaGenerica: React.FC<{ancho: number; t: number}> = ({ancho, t}) => {
|
|
476
|
+
const u = ancho / 100;
|
|
477
|
+
const pulso = 1 + 0.05 * Math.sin(t * 4);
|
|
478
|
+
return (
|
|
479
|
+
<div style={{position: 'absolute', inset: 0, padding: `${12 * u}px ${7 * u}px`, display: 'flex', flexDirection: 'column', gap: 4 * u}}>
|
|
480
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 3 * u}}>
|
|
481
|
+
<div style={{width: 9 * u, height: 9 * u, borderRadius: '50%', background: 'rgba(255,255,255,0.85)'}} />
|
|
482
|
+
<div style={{flex: 1, height: 3 * u, borderRadius: 99, background: 'rgba(255,255,255,0.6)'}} />
|
|
483
|
+
</div>
|
|
484
|
+
<div style={{position: 'relative', height: 60 * u, borderRadius: 5 * u, background: 'rgba(0,0,0,0.28)', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
485
|
+
<div style={{width: 18 * u, height: 18 * u, borderRadius: '50%', background: 'rgba(255,255,255,0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', transform: `scale(${pulso})`}}>
|
|
486
|
+
<div style={{width: 0, height: 0, borderStyle: 'solid', borderWidth: `${4.5 * u}px 0 ${4.5 * u}px ${7.5 * u}px`, borderColor: 'transparent transparent transparent #111', marginLeft: 1.5 * u}} />
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
{[90, 70, 80, 50].map((w, i) => (
|
|
490
|
+
<div key={i} style={{width: `${w}%`, height: 4 * u, borderRadius: 99, background: 'rgba(255,255,255,0.55)'}} />
|
|
491
|
+
))}
|
|
492
|
+
</div>
|
|
493
|
+
);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const Forma: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
497
|
+
const T = ctx.tema;
|
|
498
|
+
const color = c.color ?? T.colores.acento;
|
|
499
|
+
const w = c.ancho ?? 200;
|
|
500
|
+
const h = c.alto ?? (c.forma === 'linea' || c.forma === 'flecha' ? 10 : w);
|
|
501
|
+
if (c.forma === 'flecha') {
|
|
502
|
+
// largo animado: crece desde la izquierda al entrar
|
|
503
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
504
|
+
const k = easeOut(clamp((ctx.t - t0) / 0.45));
|
|
505
|
+
return (
|
|
506
|
+
<svg width={w} height={60} style={{overflow: 'visible'}}>
|
|
507
|
+
<line x1={0} y1={30} x2={w * k - 20} y2={30} stroke={color} strokeWidth={c.grosor ?? 12} strokeLinecap="round" />
|
|
508
|
+
{k > 0.6 ? <polygon points={`${w * k},30 ${w * k - 46},6 ${w * k - 46},54`} fill={color} /> : null}
|
|
509
|
+
</svg>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return (
|
|
513
|
+
<div
|
|
514
|
+
style={{
|
|
515
|
+
width: w,
|
|
516
|
+
height: h,
|
|
517
|
+
background: c.relleno === false ? 'transparent' : color,
|
|
518
|
+
border: c.borde ? `${c.grosor ?? 6}px ${c.punteado ? 'dashed' : 'solid'} ${c.borde}` : undefined,
|
|
519
|
+
borderRadius: c.forma === 'circulo' ? '50%' : c.radio ?? (c.forma === 'linea' ? h / 2 : 0),
|
|
520
|
+
opacity: c.transparencia ?? 1,
|
|
521
|
+
boxShadow: c.brillo ? `0 0 ${c.brillo}px ${color}` : undefined,
|
|
522
|
+
}}
|
|
523
|
+
/>
|
|
524
|
+
);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Grupo: varias capas que se mueven juntas (sus x, y son relativas al grupo), o una capa repetida:
|
|
529
|
+
* {tipo: 'grupo', repetir: 8, disposicion: 'circulo', radio: 320, capa: {tipo: 'imagen', src: 'ballena.png'}}
|
|
530
|
+
* disposiciones: circulo, fila, columna, cuadricula, pila, aleatorio. `retraso`: segundos entre cada copia.
|
|
531
|
+
*/
|
|
532
|
+
const Grupo: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
533
|
+
let hijos: Capa[] = c.capas ?? [];
|
|
534
|
+
if (c.repetir && c.capa) {
|
|
535
|
+
const n = c.repetir;
|
|
536
|
+
const esp = c.espacio ?? 140;
|
|
537
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
538
|
+
hijos = Array.from({length: n}, (_, i) => {
|
|
539
|
+
let x = 0;
|
|
540
|
+
let y = 0;
|
|
541
|
+
let rot = 0;
|
|
542
|
+
switch (c.disposicion ?? 'fila') {
|
|
543
|
+
case 'circulo': {
|
|
544
|
+
const ang = (i / n) * Math.PI * 2 + ((c.giroInicial ?? 0) * Math.PI) / 180;
|
|
545
|
+
x = Math.cos(ang) * (c.radio ?? 300);
|
|
546
|
+
y = Math.sin(ang) * (c.radio ?? 300);
|
|
547
|
+
if (c.orientar) rot = (ang * 180) / Math.PI + (c.orientar === true ? 0 : c.orientar);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case 'fila':
|
|
551
|
+
x = (i - (n - 1) / 2) * esp;
|
|
552
|
+
break;
|
|
553
|
+
case 'columna':
|
|
554
|
+
y = (i - (n - 1) / 2) * esp;
|
|
555
|
+
break;
|
|
556
|
+
case 'cuadricula': {
|
|
557
|
+
const cols = c.columnas ?? Math.ceil(Math.sqrt(n));
|
|
558
|
+
const filas = Math.ceil(n / cols);
|
|
559
|
+
x = ((i % cols) - (cols - 1) / 2) * esp;
|
|
560
|
+
y = (Math.floor(i / cols) - (filas - 1) / 2) * (c.espacioV ?? esp);
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case 'pila':
|
|
564
|
+
y = -i * (c.espacio ?? 14);
|
|
565
|
+
x = (azar(`${ctx.semilla}p${i}`) - 0.5) * (c.desorden ?? 6);
|
|
566
|
+
break;
|
|
567
|
+
case 'aleatorio': {
|
|
568
|
+
const [aw, ah] = c.area ?? [900, 1400];
|
|
569
|
+
x = (azar(`${ctx.semilla}x${i}`) - 0.5) * aw;
|
|
570
|
+
y = (azar(`${ctx.semilla}y${i}`) - 0.5) * ah;
|
|
571
|
+
rot = c.girarAzar ? (azar(`${ctx.semilla}r${i}`) - 0.5) * c.girarAzar : 0;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return {...c.capa, id: `${c.capa.id ?? 'r'}${i}`, x: x + (c.capa.x ?? 0), y: y + (c.capa.y ?? 0), rotar: (c.capa.rotar ?? 0) + rot, en: c.capa.en ?? t0 + i * (c.retraso ?? 0.06)};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return (
|
|
579
|
+
<div style={{position: 'relative', width: 0, height: 0}}>
|
|
580
|
+
{hijos.map((h, i) => (
|
|
581
|
+
<CapaVista key={i} c={h} ctx={ctx} relativo i={i} />
|
|
582
|
+
))}
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Personaje con poses: pose inicial y cambios en palabras ({en: 'enamorado', pose: 'feliz'}).
|
|
589
|
+
* Al cambiar de pose rebota un poco; con `hablar` se mueve mientras suena la voz.
|
|
590
|
+
*/
|
|
591
|
+
const Personaje: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
592
|
+
const poses: Record<string, string> = c.__poses ?? {};
|
|
593
|
+
let pose = c.pose ?? Object.keys(poses)[0];
|
|
594
|
+
let desde = cuando(c.en, ctx, 0);
|
|
595
|
+
for (const k of [...(c.poses ?? [])].map((k: any) => ({...k, t: cuando(k.en, ctx, 0)})).sort((a: any, b: any) => a.t - b.t)) {
|
|
596
|
+
if (ctx.t >= k.t) {
|
|
597
|
+
pose = k.pose;
|
|
598
|
+
desde = k.t;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const src = poses[pose];
|
|
602
|
+
if (!src) throw new Error(`El personaje "${c.nombre}" no tiene la pose "${pose}". Tiene: ${Object.keys(poses).join(', ')}`);
|
|
603
|
+
const rebote = desde > cuando(c.en, ctx, 0) ? 1 + 0.08 * (1 - easeOut(clamp((ctx.t - desde) / 0.35))) : 1;
|
|
604
|
+
const habla = c.hablar && ctx.t >= ctx.habla[0] && ctx.t <= ctx.habla[1] ? Math.sin(ctx.t * 17) * 0.018 : 0;
|
|
605
|
+
return (
|
|
606
|
+
<Img
|
|
607
|
+
src={recurso(src)}
|
|
608
|
+
style={{width: c.ancho ?? 560, display: 'block', transform: `scale(${rebote}, ${rebote * (1 + habla)})`, transformOrigin: 'bottom center', filter: 'drop-shadow(0 14px 18px rgba(0,0,0,0.25))'}}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/** Todas las capas de una escena. */
|
|
614
|
+
export const Capas: React.FC<{capas: Capa[]; ctx: CtxEscena}> = ({capas, ctx}) => (
|
|
615
|
+
<>
|
|
616
|
+
{capas.map((c, i) => (
|
|
617
|
+
<CapaVista key={i} c={c} ctx={ctx} i={i} />
|
|
618
|
+
))}
|
|
619
|
+
</>
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
export {useMarca};
|