morfeo 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/morfeo.mjs +26 -1
- package/index.d.ts +17 -4
- package/lib/config.mjs +84 -6
- package/lib/diccionario.mjs +129 -0
- package/lib/guion.mjs +34 -12
- package/lib/recursos.mjs +299 -0
- package/lib/render.mjs +87 -27
- package/package.json +2 -2
- package/plantilla/videos.mjs +1 -0
- package/remotion/Short.tsx +6 -6
- package/remotion/motor/Video.tsx +8 -6
- package/remotion/motor/capas.tsx +88 -3
- package/remotion/motor/escenarios/Bosque.tsx +157 -0
- package/remotion/motor/escenarios/Cuarto.tsx +103 -0
- package/remotion/motor/escenarios/Noche.tsx +163 -0
- package/remotion/motor/escenarios/index.tsx +26 -0
- package/remotion/motor/fondo.tsx +18 -7
- package/remotion/motor/modelo3d.tsx +4 -1
- package/remotion/motor/objetos3d.tsx +548 -0
- package/remotion/motor/plantillas.tsx +10 -4
- package/remotion/motor/tema.ts +14 -8
- package/remotion/motor/widgets.tsx +5 -5
package/remotion/motor/capas.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {H, W, backOut, clamp, easeIO, easeOut, lerp} from '../estilo';
|
|
|
7
7
|
import {azar, cuando, formatoNumero, partesResaltadas, recurso, type CtxEscena} from './util';
|
|
8
8
|
import {Particulas} from './fondo';
|
|
9
9
|
import {Escena3D, Modelo3D} from './modelo3d';
|
|
10
|
+
import {Escenario} from './escenarios';
|
|
10
11
|
import {componente, nombresComponentes} from './registro';
|
|
11
12
|
import {Barra, Chat, Estado, Globo, ImagenCambia, Medidor, Noticia, Onda, Seguir, Votacion} from './widgets';
|
|
12
13
|
|
|
@@ -14,6 +15,9 @@ export type Capa = Record<string, any> & {tipo: string};
|
|
|
14
15
|
|
|
15
16
|
const ENTRADA_DUR = 0.5;
|
|
16
17
|
|
|
18
|
+
// el personaje de una serie sigue en pantalla de escena en escena: no "reaparece" en cada una
|
|
19
|
+
const entradaPorDefecto = (c: Capa) => (c.tipo === 'personaje' ? 'ninguna' : 'escala');
|
|
20
|
+
|
|
17
21
|
/** Posición, escala, giro y opacidad de una capa en el instante t (entrada, movimiento, salida). */
|
|
18
22
|
function animar(c: Capa, ctx: CtxEscena, relativo: boolean) {
|
|
19
23
|
const t = ctx.t;
|
|
@@ -26,7 +30,7 @@ function animar(c: Capa, ctx: CtxEscena, relativo: boolean) {
|
|
|
26
30
|
let r = c.rotar ?? 0;
|
|
27
31
|
let o = c.opacidad ?? 1;
|
|
28
32
|
if (t < t0) return null; // con entrada 'ninguna' aparece de golpe en su momento
|
|
29
|
-
switch (c.entrada ??
|
|
33
|
+
switch (c.entrada ?? entradaPorDefecto(c)) {
|
|
30
34
|
case 'aparecer':
|
|
31
35
|
o *= easeOut(p);
|
|
32
36
|
break;
|
|
@@ -185,6 +189,8 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
185
189
|
return <Tarjeta c={c} ctx={ctx} />;
|
|
186
190
|
case 'telefono':
|
|
187
191
|
return <Telefono c={c} ctx={ctx} />;
|
|
192
|
+
case 'ventana':
|
|
193
|
+
return <Ventana c={c} ctx={ctx} />;
|
|
188
194
|
case 'forma':
|
|
189
195
|
return <Forma c={c} ctx={ctx} />;
|
|
190
196
|
case 'icono': {
|
|
@@ -214,6 +220,15 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
214
220
|
);
|
|
215
221
|
case 'grupo':
|
|
216
222
|
return <Grupo c={c} ctx={ctx} />;
|
|
223
|
+
case 'escenario': {
|
|
224
|
+
const {tipo, nombre, x, y, en, hasta, entrada, salida, z, ...props} = c;
|
|
225
|
+
const t0 = cuando(en, ctx, 0);
|
|
226
|
+
return (
|
|
227
|
+
<div style={{width: W, height: H}}>
|
|
228
|
+
<Escenario nombre={nombre} {...props} t={ctx.t - t0} dur={ctx.dur} cuando={(e: unknown) => cuando(e, ctx) - t0} />
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
217
232
|
case 'escena3d':
|
|
218
233
|
return <Escena3D c={c} ctx={ctx} />;
|
|
219
234
|
case 'personaje':
|
|
@@ -260,7 +275,7 @@ const TextoCapa: React.FC<{c: Capa; ctx: CtxEscena; fuente: string}> = ({c, ctx,
|
|
|
260
275
|
const vel = typeof c.escribir === 'number' ? c.escribir : 22; // letras por segundo
|
|
261
276
|
const n = Math.max(0, Math.floor((ctx.t - t0) * vel));
|
|
262
277
|
let cuenta = 0;
|
|
263
|
-
texto = texto.replace(/[^*]/g, (ch) => (cuenta++ < n ? ch : ch === '\n' ? '\n' : ''));
|
|
278
|
+
texto = texto.replace(/[^*]/g, (ch) => (cuenta++ < n ? ch : ch === '\n' ? '\n' : '')).replace(/\*\*/g, '');
|
|
264
279
|
}
|
|
265
280
|
const color = c.color ?? T.colores.texto;
|
|
266
281
|
const acento = c.acento ?? T.colores.acento;
|
|
@@ -398,7 +413,9 @@ const Telefono: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
398
413
|
const borde = ancho * 0.035;
|
|
399
414
|
return (
|
|
400
415
|
<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'}}>
|
|
401
|
-
<div style={{position: 'relative', width: '100%', height: '100%', borderRadius: ancho * 0.1, overflow: 'hidden', background: c.pantalla ??
|
|
416
|
+
<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)`}}>
|
|
417
|
+
{/* sin imagen ni capas: una pantalla de app genérica en vez de un rectángulo negro */}
|
|
418
|
+
{!c.src && !c.capas?.length ? <PantallaGenerica ancho={ancho} t={ctx.t} /> : null}
|
|
402
419
|
{c.src ? (
|
|
403
420
|
/\.(mp4|webm|mov)$/i.test(c.src) ? (
|
|
404
421
|
<OffthreadVideo src={recurso(c.src)} muted style={{width: '100%', height: '100%', objectFit: 'cover'}} />
|
|
@@ -418,6 +435,74 @@ const Telefono: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
418
435
|
);
|
|
419
436
|
};
|
|
420
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Ventana circular (escotilla, lupa, mirilla): muestra una imagen o capas adentro, con borde metálico y remaches.
|
|
440
|
+
* {tipo: 'ventana', diametro: 640, src: 'interior.png'} o {tipo: 'ventana', capas: [...]} (coordenadas desde el centro)
|
|
441
|
+
* borde: 'metal' | 'ninguno' | '#color' · remaches · zoom: [inicio, fin] para acercar el contenido · fondo: color detrás
|
|
442
|
+
*/
|
|
443
|
+
const Ventana: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
444
|
+
const d = c.diametro ?? 640;
|
|
445
|
+
const aro = c.borde === 'ninguno' ? 0 : c.grosor ?? Math.round(d * 0.06);
|
|
446
|
+
const metal = c.borde && c.borde !== 'metal' && c.borde !== 'ninguno' ? c.borde : null;
|
|
447
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
448
|
+
const k = clamp((ctx.t - t0) / Math.max(0.5, ctx.dur - t0));
|
|
449
|
+
const zoom = Array.isArray(c.zoom) ? lerp(c.zoom[0], c.zoom[1], easeIO(k)) : c.zoom ?? 1;
|
|
450
|
+
const n = c.remaches === false ? 0 : c.remaches ?? 16;
|
|
451
|
+
const total = d + aro * 2;
|
|
452
|
+
return (
|
|
453
|
+
<div style={{position: 'relative', width: total, height: total}}>
|
|
454
|
+
<div
|
|
455
|
+
style={{
|
|
456
|
+
position: 'absolute',
|
|
457
|
+
inset: 0,
|
|
458
|
+
borderRadius: '50%',
|
|
459
|
+
background: metal ?? 'conic-gradient(from 210deg, #8C96A3, #E3E8EE, #7A8592, #C9D0D8, #6E7884, #DDE3EA, #8C96A3)',
|
|
460
|
+
boxShadow: '0 24px 50px rgba(0,0,0,0.45), inset 0 0 0 3px rgba(0,0,0,0.35)',
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
<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)'}}>
|
|
464
|
+
<div style={{position: 'absolute', inset: 0, transform: `scale(${zoom})`}}>
|
|
465
|
+
{c.src ? <Img src={recurso(c.src)} style={{width: '100%', height: '100%', objectFit: 'cover'}} /> : null}
|
|
466
|
+
<div style={{position: 'absolute', left: '50%', top: '50%', width: 0, height: 0}}>
|
|
467
|
+
{(c.capas ?? []).map((h: Capa, i: number) => (
|
|
468
|
+
<CapaVista key={i} c={h} ctx={ctx} relativo i={i} />
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
{/* brillo del vidrio */}
|
|
473
|
+
<div style={{position: 'absolute', inset: 0, borderRadius: '50%', background: 'radial-gradient(circle at 30% 25%, rgba(255,255,255,0.22), transparent 40%)'}} />
|
|
474
|
+
</div>
|
|
475
|
+
{Array.from({length: n}, (_, i) => {
|
|
476
|
+
const a = (i / n) * Math.PI * 2;
|
|
477
|
+
const r = d / 2 + aro / 2;
|
|
478
|
+
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)'}} />;
|
|
479
|
+
})}
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/** Pantalla de relleno: barra de app, un video con botón de play y renglones de texto. */
|
|
485
|
+
const PantallaGenerica: React.FC<{ancho: number; t: number}> = ({ancho, t}) => {
|
|
486
|
+
const u = ancho / 100;
|
|
487
|
+
const pulso = 1 + 0.05 * Math.sin(t * 4);
|
|
488
|
+
return (
|
|
489
|
+
<div style={{position: 'absolute', inset: 0, padding: `${12 * u}px ${7 * u}px`, display: 'flex', flexDirection: 'column', gap: 4 * u}}>
|
|
490
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 3 * u}}>
|
|
491
|
+
<div style={{width: 9 * u, height: 9 * u, borderRadius: '50%', background: 'rgba(255,255,255,0.85)'}} />
|
|
492
|
+
<div style={{flex: 1, height: 3 * u, borderRadius: 99, background: 'rgba(255,255,255,0.6)'}} />
|
|
493
|
+
</div>
|
|
494
|
+
<div style={{position: 'relative', height: 60 * u, borderRadius: 5 * u, background: 'rgba(0,0,0,0.28)', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
495
|
+
<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})`}}>
|
|
496
|
+
<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}} />
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
{[90, 70, 80, 50].map((w, i) => (
|
|
500
|
+
<div key={i} style={{width: `${w}%`, height: 4 * u, borderRadius: 99, background: 'rgba(255,255,255,0.55)'}} />
|
|
501
|
+
))}
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
};
|
|
505
|
+
|
|
421
506
|
const Forma: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
422
507
|
const T = ctx.tema;
|
|
423
508
|
const color = c.color ?? T.colores.acento;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Bosque de noche ilustrado, 1080×1920, por capas: cielo, estrellas, luna con halo, montañas,
|
|
2
|
+
// tres filas de pinos, colina con pasto y luciérnagas. Con `paneo` las capas se mueven a distinta
|
|
3
|
+
// velocidad (paralaje). La luna puede tener cara y cantar: luna: {cara: true, cantar: 'palabra', bajar: 'palabra'}.
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
t: number;
|
|
8
|
+
dur: number;
|
|
9
|
+
cuando: (en: unknown) => number;
|
|
10
|
+
paneo?: number;
|
|
11
|
+
luna?: {x?: number; y?: number; cara?: boolean; cantar?: string; bajar?: string; a?: [number, number]};
|
|
12
|
+
luciernagas?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const W = 1080;
|
|
16
|
+
const H = 1920;
|
|
17
|
+
const clamp = (x: number) => Math.max(0, Math.min(1, x));
|
|
18
|
+
const suave = (x: number) => {
|
|
19
|
+
const c = clamp(x);
|
|
20
|
+
return c * c * (3 - 2 * c);
|
|
21
|
+
};
|
|
22
|
+
const azar = (n: number) => {
|
|
23
|
+
const x = Math.sin(n * 127.1) * 43758.5453;
|
|
24
|
+
return x - Math.floor(x);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Fila de pinos como un solo trazado (más liviano que muchos elementos). */
|
|
28
|
+
function pinos(base: number, alto: number, ancho: number, n: number, semilla: number) {
|
|
29
|
+
let d = '';
|
|
30
|
+
for (let i = 0; i < n; i++) {
|
|
31
|
+
const cx = (i / (n - 1)) * (W + 400) - 200 + (azar(semilla + i) - 0.5) * ancho * 0.6;
|
|
32
|
+
const h = alto * (0.75 + azar(semilla + i * 3) * 0.5);
|
|
33
|
+
const w = ancho * (0.8 + azar(semilla + i * 7) * 0.4);
|
|
34
|
+
// tres pisos de ramas
|
|
35
|
+
for (let p = 0; p < 3; p++) {
|
|
36
|
+
const top = base - h + p * h * 0.26;
|
|
37
|
+
const bot = base - h * 0.32 + p * h * 0.22;
|
|
38
|
+
const ww = w * (0.55 + p * 0.25);
|
|
39
|
+
d += `M${cx} ${top} L${cx + ww / 2} ${bot} L${cx - ww / 2} ${bot} Z `;
|
|
40
|
+
}
|
|
41
|
+
d += `M${cx - w * 0.06} ${base - h * 0.12} h${w * 0.12} v${h * 0.14} h${-w * 0.12} Z `;
|
|
42
|
+
}
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Bosque({t, dur, cuando, paneo = 0, luna = {}, luciernagas = true}: Props) {
|
|
47
|
+
const k = suave(t / Math.max(1, dur));
|
|
48
|
+
const p = (f: number) => -paneo * f * k; // paralaje: lo lejano se mueve menos
|
|
49
|
+
// luna: posición (puede bajar) y canto
|
|
50
|
+
const lx0 = luna.x ?? 800;
|
|
51
|
+
const ly0 = luna.y ?? 470;
|
|
52
|
+
const kb = luna.bajar ? suave((t - cuando(luna.bajar)) / 1.8) : 0;
|
|
53
|
+
const lx = lx0 + ((luna.a?.[0] ?? lx0) - lx0) * kb;
|
|
54
|
+
const ly = ly0 + ((luna.a?.[1] ?? ly0) - ly0) * kb;
|
|
55
|
+
const canta = luna.cantar ? t >= cuando(luna.cantar) : false;
|
|
56
|
+
const boca = canta ? 6 + Math.abs(Math.sin(t * 9)) * 14 : 0;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
60
|
+
<defs>
|
|
61
|
+
<linearGradient id="cielo" x1="0" y1="0" x2="0" y2="1">
|
|
62
|
+
<stop offset="0" stopColor="#0B1033" />
|
|
63
|
+
<stop offset="0.5" stopColor="#252063" />
|
|
64
|
+
<stop offset="0.78" stopColor="#5B3C7E" />
|
|
65
|
+
<stop offset="1" stopColor="#8E5A86" />
|
|
66
|
+
</linearGradient>
|
|
67
|
+
<radialGradient id="halo">
|
|
68
|
+
<stop offset="0" stopColor="rgba(255,240,200,0.55)" />
|
|
69
|
+
<stop offset="1" stopColor="rgba(255,240,200,0)" />
|
|
70
|
+
</radialGradient>
|
|
71
|
+
<linearGradient id="colina" x1="0" y1="0" x2="0" y2="1">
|
|
72
|
+
<stop offset="0" stopColor="#1F3B3A" />
|
|
73
|
+
<stop offset="1" stopColor="#0E1E22" />
|
|
74
|
+
</linearGradient>
|
|
75
|
+
</defs>
|
|
76
|
+
<rect width={W} height={H} fill="url(#cielo)" />
|
|
77
|
+
{/* estrellas que titilan */}
|
|
78
|
+
{Array.from({length: 70}, (_, i) => {
|
|
79
|
+
const x = azar(i) * W;
|
|
80
|
+
const y = azar(i + 100) * 1100;
|
|
81
|
+
const r = 1.2 + azar(i + 200) * 2.4;
|
|
82
|
+
const o = 0.35 + 0.65 * Math.abs(Math.sin(t * (1 + azar(i + 300) * 2) + i));
|
|
83
|
+
return <circle key={i} cx={x + p(0.05)} cy={y} r={r} fill="#FFFFFF" opacity={o} />;
|
|
84
|
+
})}
|
|
85
|
+
{/* luna */}
|
|
86
|
+
<g transform={`translate(${lx + p(0.1)} ${ly})`}>
|
|
87
|
+
<circle r={260} fill="url(#halo)" />
|
|
88
|
+
<circle r={110} fill="#FFF4D6" />
|
|
89
|
+
<circle cx={-34} cy={-26} r={16} fill="rgba(210,190,150,0.45)" />
|
|
90
|
+
<circle cx={38} cy={30} r={22} fill="rgba(210,190,150,0.4)" />
|
|
91
|
+
<circle cx={18} cy={-48} r={9} fill="rgba(210,190,150,0.4)" />
|
|
92
|
+
{luna.cara ? (
|
|
93
|
+
<g>
|
|
94
|
+
{canta ? (
|
|
95
|
+
<>
|
|
96
|
+
<path d="M-42 -8 Q-30 -18 -18 -8" stroke="#6B5236" strokeWidth={6} fill="none" strokeLinecap="round" />
|
|
97
|
+
<path d="M18 -8 Q30 -18 42 -8" stroke="#6B5236" strokeWidth={6} fill="none" strokeLinecap="round" />
|
|
98
|
+
</>
|
|
99
|
+
) : (
|
|
100
|
+
<>
|
|
101
|
+
<circle cx={-30} cy={-10} r={8} fill="#6B5236" />
|
|
102
|
+
<circle cx={30} cy={-10} r={8} fill="#6B5236" />
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
<ellipse cx={0} cy={34} rx={canta ? 14 : 16} ry={canta ? boca : 4} fill={canta ? '#7A3B3B' : '#6B5236'} />
|
|
106
|
+
<circle cx={-58} cy={18} r={12} fill="rgba(255,150,150,0.45)" />
|
|
107
|
+
<circle cx={58} cy={18} r={12} fill="rgba(255,150,150,0.45)" />
|
|
108
|
+
</g>
|
|
109
|
+
) : null}
|
|
110
|
+
</g>
|
|
111
|
+
{/* notas musicales cuando canta */}
|
|
112
|
+
{canta
|
|
113
|
+
? Array.from({length: 6}, (_, i) => {
|
|
114
|
+
const fase = ((t - cuando(luna.cantar) + i * 0.45) % 2.7) / 2.7;
|
|
115
|
+
const x = lx - 160 - fase * 260 + Math.sin(fase * 9 + i) * 40;
|
|
116
|
+
const y = ly + 60 + fase * 420;
|
|
117
|
+
return (
|
|
118
|
+
<text key={i} x={x} y={y} fontSize={70} fill="#FFE9A8" opacity={Math.sin(fase * Math.PI)} fontFamily="sans-serif">
|
|
119
|
+
{i % 2 ? '♪' : '♫'}
|
|
120
|
+
</text>
|
|
121
|
+
);
|
|
122
|
+
})
|
|
123
|
+
: null}
|
|
124
|
+
{/* montañas lejanas */}
|
|
125
|
+
<path transform={`translate(${p(0.15)} 0)`} d="M-200 1250 L80 980 L300 1150 L560 920 L820 1120 L1040 960 L1300 1180 L1300 1500 L-200 1500 Z" fill="#2E2A5A" opacity={0.9} />
|
|
126
|
+
{/* pinos lejanos, medios y cercanos */}
|
|
127
|
+
<path transform={`translate(${p(0.3)} 0)`} d={pinos(1330, 300, 150, 12, 1)} fill="#263A55" />
|
|
128
|
+
<path transform={`translate(${p(0.55)} 0)`} d={pinos(1420, 380, 200, 9, 20)} fill="#1A2E3F" />
|
|
129
|
+
{/* colina */}
|
|
130
|
+
<path transform={`translate(${p(0.8)} 0)`} d="M-300 1500 Q300 1380 700 1460 Q1050 1520 1400 1440 L1400 1920 L-300 1920 Z" fill="url(#colina)" />
|
|
131
|
+
{/* pasto */}
|
|
132
|
+
<g transform={`translate(${p(0.8)} 0)`}>
|
|
133
|
+
{Array.from({length: 60}, (_, i) => {
|
|
134
|
+
const x = -200 + i * 26;
|
|
135
|
+
const y = 1470 + Math.sin(i * 0.7) * 22;
|
|
136
|
+
const viento = Math.sin(t * 2 + i * 0.4) * 5;
|
|
137
|
+
return <path key={i} d={`M${x} ${y + 30} Q${x + viento} ${y + 10} ${x + 4 + viento * 1.6} ${y - 6}`} stroke="#2E5A4E" strokeWidth={5} fill="none" strokeLinecap="round" />;
|
|
138
|
+
})}
|
|
139
|
+
</g>
|
|
140
|
+
<path transform={`translate(${p(1)} 0)`} d={pinos(1950, 620, 320, 5, 40)} fill="#0C1A24" />
|
|
141
|
+
{/* luciérnagas */}
|
|
142
|
+
{luciernagas
|
|
143
|
+
? Array.from({length: 14}, (_, i) => {
|
|
144
|
+
const x = azar(i + 500) * W + Math.sin(t * 0.8 + i) * 40;
|
|
145
|
+
const y = 1250 + azar(i + 600) * 450 + Math.cos(t * 0.6 + i) * 30;
|
|
146
|
+
const o = 0.3 + 0.7 * Math.abs(Math.sin(t * 1.7 + i * 2));
|
|
147
|
+
return (
|
|
148
|
+
<g key={i} opacity={o}>
|
|
149
|
+
<circle cx={x} cy={y} r={14} fill="rgba(255,236,120,0.25)" />
|
|
150
|
+
<circle cx={x} cy={y} r={4} fill="#FFF3A0" />
|
|
151
|
+
</g>
|
|
152
|
+
);
|
|
153
|
+
})
|
|
154
|
+
: null}
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Cuarto de Neto (serie). Dos partes para meter al personaje en medio:
|
|
2
|
+
// {tipo: 'componente', nombre: 'Cuarto'} pared, ventana, póster, calendario
|
|
3
|
+
// {tipo: 'componente', nombre: 'Cuarto', props: {parte: 'frente'}} escritorio, monitor, teclado RGB y ramen
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
type Props = {t: number; parte?: 'fondo' | 'frente'; dia?: number};
|
|
7
|
+
|
|
8
|
+
const W = 1080;
|
|
9
|
+
const H = 1920;
|
|
10
|
+
|
|
11
|
+
function Fondo({t, dia}: {t: number; dia?: number}) {
|
|
12
|
+
return (
|
|
13
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
14
|
+
<defs>
|
|
15
|
+
<pattern id="rayas" width="90" height="10" patternUnits="userSpaceOnUse">
|
|
16
|
+
<rect width="90" height="10" fill="#5B3FA0" />
|
|
17
|
+
<rect width="45" height="10" fill="#6447AE" />
|
|
18
|
+
</pattern>
|
|
19
|
+
<linearGradient id="noche" x1="0" y1="0" x2="0" y2="1">
|
|
20
|
+
<stop offset="0" stopColor="#0E1840" />
|
|
21
|
+
<stop offset="1" stopColor="#2D2A6E" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
<rect width={W} height={H} fill="url(#rayas)" />
|
|
25
|
+
{/* zoclo y piso */}
|
|
26
|
+
<rect y={1520} width={W} height={400} fill="#3B2A63" />
|
|
27
|
+
<rect y={1510} width={W} height={18} fill="#2A1D4A" />
|
|
28
|
+
{/* ventana */}
|
|
29
|
+
<g transform="translate(70 360)">
|
|
30
|
+
<rect width={420} height={520} rx={10} fill="url(#noche)" stroke="#2A1D4A" strokeWidth={18} />
|
|
31
|
+
<circle cx={300} cy={130} r={50} fill="#FFF4D6" />
|
|
32
|
+
<circle cx={322} cy={118} r={46} fill="#141E4E" />
|
|
33
|
+
{Array.from({length: 14}, (_, i) => (
|
|
34
|
+
<circle key={i} cx={40 + ((i * 97) % 340)} cy={60 + ((i * 53) % 400)} r={3} fill="#fff" opacity={0.4 + 0.6 * Math.abs(Math.sin(t * 1.5 + i))} />
|
|
35
|
+
))}
|
|
36
|
+
<rect x={200} y={0} width={18} height={520} fill="#2A1D4A" />
|
|
37
|
+
<rect x={0} y={250} width={420} height={18} fill="#2A1D4A" />
|
|
38
|
+
{/* cortinas que se mecen */}
|
|
39
|
+
<path d={`M-40 -20 Q${30 + Math.sin(t) * 8} 260 -10 560 L-60 560 L-60 -20 Z`} fill="#C0304A" />
|
|
40
|
+
<path d={`M460 -20 Q${390 + Math.sin(t + 1) * 8} 260 430 560 L480 560 L480 -20 Z`} fill="#C0304A" />
|
|
41
|
+
</g>
|
|
42
|
+
{/* póster de Ari */}
|
|
43
|
+
<g transform="translate(700 330) rotate(4)">
|
|
44
|
+
<rect width={300} height={380} rx={8} fill="#FFFFFF" />
|
|
45
|
+
<rect x={18} y={18} width={264} height={290} fill="#FF9EC7" />
|
|
46
|
+
<circle cx={150} cy={150} r={70} fill="#FFE1EF" />
|
|
47
|
+
<path d="M150 120 l-28 -28 a20 20 0 0 1 28 -18 a20 20 0 0 1 28 18z" fill="#FF3B7F" transform={`translate(0 ${Math.sin(t * 3) * 4})`} />
|
|
48
|
+
<text x={150} y={355} textAnchor="middle" fontSize={44} fontWeight={900} fill="#FF3B7F" fontFamily="Montserrat, sans-serif">
|
|
49
|
+
ARI
|
|
50
|
+
</text>
|
|
51
|
+
<rect x={120} y={-14} width={60} height={26} fill="rgba(255,255,255,0.7)" />
|
|
52
|
+
</g>
|
|
53
|
+
{/* calendario con días tachados */}
|
|
54
|
+
<g transform="translate(560 780)">
|
|
55
|
+
<rect width={220} height={230} rx={8} fill="#FFFFFF" />
|
|
56
|
+
<rect width={220} height={56} rx={8} fill="#E5484D" />
|
|
57
|
+
<text x={110} y={40} textAnchor="middle" fontSize={32} fontWeight={900} fill="#fff" fontFamily="Montserrat, sans-serif">
|
|
58
|
+
{dia != null ? `DÍA ${dia}` : 'NOV'}
|
|
59
|
+
</text>
|
|
60
|
+
{Array.from({length: 20}, (_, i) => (
|
|
61
|
+
<text key={i} x={22 + (i % 5) * 42} y={90 + Math.floor(i / 5) * 38} fontSize={30} fontWeight={900} fill="#E5484D" fontFamily="sans-serif">
|
|
62
|
+
✕
|
|
63
|
+
</text>
|
|
64
|
+
))}
|
|
65
|
+
</g>
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Frente({t}: {t: number}) {
|
|
71
|
+
const colores = ['#FF4D6D', '#FFD60A', '#4CC9F0', '#80ED99', '#B517FF', '#FF8C42'];
|
|
72
|
+
return (
|
|
73
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
74
|
+
{/* luz del monitor sobre el escritorio */}
|
|
75
|
+
<ellipse cx={540} cy={1600} rx={520} ry={120} fill="rgba(120,200,255,0.12)" />
|
|
76
|
+
{/* escritorio */}
|
|
77
|
+
<rect x={-20} y={1640} width={1120} height={60} rx={10} fill="#2B2238" />
|
|
78
|
+
<rect x={-20} y={1690} width={1120} height={260} fill="#1E1828" />
|
|
79
|
+
{/* teclado RGB */}
|
|
80
|
+
<g transform="translate(250 1590)">
|
|
81
|
+
<rect width={580} height={70} rx={14} fill="#15121C" />
|
|
82
|
+
{Array.from({length: 24}, (_, i) => (
|
|
83
|
+
<rect key={i} x={14 + (i % 12) * 46} y={10 + Math.floor(i / 12) * 28} width={40} height={22} rx={5} fill={colores[Math.floor(i + t * 6) % colores.length]} opacity={0.9} />
|
|
84
|
+
))}
|
|
85
|
+
</g>
|
|
86
|
+
{/* ramen */}
|
|
87
|
+
<g transform="translate(70 1500)">
|
|
88
|
+
<path d="M0 60 L130 60 L112 150 L18 150 Z" fill="#FFFFFF" stroke="#D9D3C7" strokeWidth={4} />
|
|
89
|
+
<rect x={10} y={80} width={110} height={26} fill="#E5484D" />
|
|
90
|
+
<path d={`M50 60 Q${44 + Math.sin(t * 2) * 6} 20 56 -10`} stroke="rgba(255,255,255,0.5)" strokeWidth={6} fill="none" strokeLinecap="round" />
|
|
91
|
+
<rect x={84} y={-30} width={8} height={110} rx={4} fill="#C99A5B" transform="rotate(14 88 30)" />
|
|
92
|
+
<rect x={98} y={-30} width={8} height={110} rx={4} fill="#C99A5B" transform="rotate(20 102 30)" />
|
|
93
|
+
</g>
|
|
94
|
+
{/* mouse */}
|
|
95
|
+
<ellipse cx={920} cy={1630} rx={38} ry={22} fill="#15121C" />
|
|
96
|
+
<rect x={916} y={1612} width={8} height={14} rx={3} fill={colores[Math.floor(t * 4) % colores.length]} />
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function Cuarto({t, parte = 'fondo', dia}: Props) {
|
|
102
|
+
return parte === 'frente' ? <Frente t={t} /> : <Fondo t={t} dia={dia} />;
|
|
103
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Escenarios nocturnos para historias, 1080×1920:
|
|
2
|
+
// lugar: 'departamento' sala oscura: ventana con ciudad, sillón, lámpara y tele que parpadea
|
|
3
|
+
// lugar: 'tunel' túnel del metro: arcos, rieles, luces que pasan y el tren que llega (llegar: 'palabra')
|
|
4
|
+
// lugar: 'pasillo' pasillo con la puerta: luz bajo la puerta y golpes que la sacuden (golpe: 'palabra')
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
type Props = {t: number; cuando: (en: unknown) => number; lugar?: 'departamento' | 'tunel' | 'pasillo'; llegar?: string; golpe?: string};
|
|
8
|
+
|
|
9
|
+
const W = 1080;
|
|
10
|
+
const H = 1920;
|
|
11
|
+
const clamp = (x: number) => Math.max(0, Math.min(1, x));
|
|
12
|
+
const suave = (x: number) => {
|
|
13
|
+
const c = clamp(x);
|
|
14
|
+
return c * c * (3 - 2 * c);
|
|
15
|
+
};
|
|
16
|
+
const azar = (n: number) => {
|
|
17
|
+
const x = Math.sin(n * 91.7) * 43758.5453;
|
|
18
|
+
return x - Math.floor(x);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function Departamento({t}: {t: number}) {
|
|
22
|
+
const tele = 0.55 + 0.45 * Math.abs(Math.sin(t * 7) * Math.sin(t * 3.3));
|
|
23
|
+
return (
|
|
24
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
25
|
+
<rect width={W} height={H} fill="#121A2B" />
|
|
26
|
+
<rect y={1480} width={W} height={440} fill="#0C111C" />
|
|
27
|
+
{/* ventana con ciudad */}
|
|
28
|
+
<g transform="translate(110 520)">
|
|
29
|
+
<rect width={460} height={560} fill="#1B2A4A" stroke="#0A0F1A" strokeWidth={16} />
|
|
30
|
+
{Array.from({length: 8}, (_, i) => {
|
|
31
|
+
const x = 10 + i * 58;
|
|
32
|
+
const h = 160 + azar(i) * 260;
|
|
33
|
+
return (
|
|
34
|
+
<g key={i}>
|
|
35
|
+
<rect x={x} y={560 - h} width={50} height={h} fill="#0E1730" />
|
|
36
|
+
{Array.from({length: 10}, (_, j) =>
|
|
37
|
+
azar(i * 10 + j) > 0.55 ? <rect key={j} x={x + 8 + (j % 3) * 14} y={560 - h + 14 + Math.floor(j / 3) * 26} width={8} height={12} fill="#FFD27A" opacity={0.5 + 0.5 * Math.abs(Math.sin(t + i + j))} /> : null,
|
|
38
|
+
)}
|
|
39
|
+
</g>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
<rect x={222} width={16} height={560} fill="#0A0F1A" />
|
|
43
|
+
</g>
|
|
44
|
+
{/* luz de la lámpara */}
|
|
45
|
+
<circle cx={880} cy={980} r={420} fill="rgba(255,190,110,0.12)" />
|
|
46
|
+
<rect x={872} y={900} width={16} height={600} fill="#2A2420" />
|
|
47
|
+
<path d="M800 900 L960 900 L920 800 L840 800 Z" fill="#E8B35C" />
|
|
48
|
+
<ellipse cx={880} cy={1500} rx={90} ry={16} fill="#1C1814" />
|
|
49
|
+
{/* tele que parpadea */}
|
|
50
|
+
<rect x={600} y={1180} width={260} height={160} rx={8} fill="#0A0F1A" />
|
|
51
|
+
<rect x={612} y={1192} width={236} height={136} fill={`rgba(120,170,255,${0.35 * tele})`} />
|
|
52
|
+
<circle cx={730} cy={1260} r={360} fill={`rgba(120,170,255,${0.06 * tele})`} />
|
|
53
|
+
{/* sillón */}
|
|
54
|
+
<g transform="translate(140 1300)">
|
|
55
|
+
<rect width={560} height={180} rx={40} fill="#3A2E3A" />
|
|
56
|
+
<rect y={-120} width={560} height={150} rx={40} fill="#45374A" />
|
|
57
|
+
<rect x={-30} y={-60} width={90} height={250} rx={36} fill="#3A2E3A" />
|
|
58
|
+
<rect x={500} y={-60} width={90} height={250} rx={36} fill="#3A2E3A" />
|
|
59
|
+
</g>
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Tunel({t, tTren}: {t: number; tTren: number}) {
|
|
65
|
+
const k = suave((t - tTren) / 2.4); // el tren se acerca
|
|
66
|
+
const escala = 0.2 + k * 0.85;
|
|
67
|
+
return (
|
|
68
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
69
|
+
<defs>
|
|
70
|
+
<radialGradient id="fondoTunel" cx="0.5" cy="0.48" r="0.6">
|
|
71
|
+
<stop offset="0" stopColor="#2C3446" />
|
|
72
|
+
<stop offset="1" stopColor="#07090E" />
|
|
73
|
+
</radialGradient>
|
|
74
|
+
</defs>
|
|
75
|
+
<rect width={W} height={H} fill="url(#fondoTunel)" />
|
|
76
|
+
{/* arcos del túnel hacia el fondo */}
|
|
77
|
+
{Array.from({length: 7}, (_, i) => {
|
|
78
|
+
const d = ((i + (t * 0.6) % 1) / 7);
|
|
79
|
+
const s = 0.15 + (1 - d) * 1.1;
|
|
80
|
+
return <ellipse key={i} cx={540} cy={900} rx={520 * s} ry={600 * s} fill="none" stroke="#3B4560" strokeWidth={10 * s} opacity={0.25 + (1 - d) * 0.6} />;
|
|
81
|
+
})}
|
|
82
|
+
{/* luces de pared que pasan */}
|
|
83
|
+
{Array.from({length: 6}, (_, i) => {
|
|
84
|
+
const d = ((i / 6 + t * 0.35) % 1);
|
|
85
|
+
const s = 0.1 + d * 1.2;
|
|
86
|
+
return (
|
|
87
|
+
<g key={i} opacity={d}>
|
|
88
|
+
<circle cx={540 - 470 * s} cy={900 - 120 * s} r={10 * s + 3} fill="#FFE29A" />
|
|
89
|
+
<circle cx={540 + 470 * s} cy={900 - 120 * s} r={10 * s + 3} fill="#FFE29A" />
|
|
90
|
+
</g>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
{/* rieles */}
|
|
94
|
+
<path d="M520 960 L250 1920 M560 960 L830 1920" stroke="#5A6378" strokeWidth={14} />
|
|
95
|
+
{Array.from({length: 10}, (_, i) => {
|
|
96
|
+
const d = (i / 10 + t * 0.5) % 1;
|
|
97
|
+
const y = 960 + d * d * 960;
|
|
98
|
+
const a = 30 + d * 420;
|
|
99
|
+
return <rect key={i} x={540 - a} y={y} width={a * 2} height={6 + d * 18} fill="#2A2F3C" />;
|
|
100
|
+
})}
|
|
101
|
+
{/* tren que llega con faros */}
|
|
102
|
+
{t >= tTren ? (
|
|
103
|
+
<g transform={`translate(540 ${930 + k * 160}) scale(${escala})`}>
|
|
104
|
+
<circle r={700} fill="rgba(255,240,200,0.08)" />
|
|
105
|
+
<rect x={-330} y={-380} width={660} height={700} rx={70} fill="#D9DEE8" />
|
|
106
|
+
<rect x={-280} y={-310} width={560} height={240} rx={26} fill="#1A2233" />
|
|
107
|
+
<rect x={-330} y={-40} width={660} height={60} fill="#E5484D" />
|
|
108
|
+
<text x={0} y={190} textAnchor="middle" fontSize={130} fontWeight={900} fill="#1A2233" fontFamily="Montserrat, sans-serif">
|
|
109
|
+
B
|
|
110
|
+
</text>
|
|
111
|
+
<circle cx={-210} cy={220} r={46} fill="#FFF6C8" />
|
|
112
|
+
<circle cx={210} cy={220} r={46} fill="#FFF6C8" />
|
|
113
|
+
<circle cx={-210} cy={220} r={120} fill="rgba(255,246,200,0.25)" />
|
|
114
|
+
<circle cx={210} cy={220} r={120} fill="rgba(255,246,200,0.25)" />
|
|
115
|
+
</g>
|
|
116
|
+
) : null}
|
|
117
|
+
</svg>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Pasillo({t, tGolpe}: {t: number; tGolpe: number}) {
|
|
122
|
+
// tres golpes: la puerta se sacude y la luz de abajo parpadea
|
|
123
|
+
const g = t - tGolpe;
|
|
124
|
+
const golpe = g >= 0 && g < 1.6 ? [0, 0.45, 0.9].reduce((s, d) => s + Math.max(0, 1 - Math.abs(g - d) * 8), 0) : 0;
|
|
125
|
+
const sacudida = Math.sin(t * 80) * 10 * golpe;
|
|
126
|
+
const luz = 0.6 + 0.4 * Math.abs(Math.sin(t * 2.3)) - golpe * 0.3;
|
|
127
|
+
return (
|
|
128
|
+
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
|
|
129
|
+
<rect width={W} height={H} fill="#151B26" />
|
|
130
|
+
{/* paredes en perspectiva */}
|
|
131
|
+
<path d="M0 0 L250 380 L250 1500 L0 1920 Z" fill="#1C2433" />
|
|
132
|
+
<path d="M1080 0 L830 380 L830 1500 L1080 1920 Z" fill="#1C2433" />
|
|
133
|
+
<path d="M250 1500 L830 1500 L1080 1920 L0 1920 Z" fill="#2A2018" />
|
|
134
|
+
{/* tablas del piso */}
|
|
135
|
+
{Array.from({length: 7}, (_, i) => (
|
|
136
|
+
<path key={i} d={`M${250 + i * 97} 1500 L${-60 + i * 200} 1920`} stroke="#1E1610" strokeWidth={4} />
|
|
137
|
+
))}
|
|
138
|
+
{/* cuadros en la pared */}
|
|
139
|
+
<rect x={60} y={700} width={120} height={160} fill="#2A3346" stroke="#3A4560" strokeWidth={8} transform="skewY(18)" />
|
|
140
|
+
<rect x={900} y={980} width={120} height={160} fill="#2A3346" stroke="#3A4560" strokeWidth={8} transform="skewY(-18)" />
|
|
141
|
+
{/* puerta */}
|
|
142
|
+
<g transform={`translate(${sacudida} 0)`}>
|
|
143
|
+
<rect x={320} y={540} width={440} height={960} fill="#0F1520" />
|
|
144
|
+
<rect x={340} y={560} width={400} height={930} fill="#22314A" stroke="#33466A" strokeWidth={10} />
|
|
145
|
+
<rect x={380} y={610} width={320} height={360} fill="none" stroke="#33466A" strokeWidth={8} />
|
|
146
|
+
<rect x={380} y={1030} width={320} height={400} fill="none" stroke="#33466A" strokeWidth={8} />
|
|
147
|
+
<circle cx={690} cy={1060} r={18} fill="#D9B45A" />
|
|
148
|
+
<circle cx={540} cy={760} r={14} fill="#0A0E16" stroke="#3A4560" strokeWidth={5} />
|
|
149
|
+
</g>
|
|
150
|
+
{/* luz por debajo de la puerta */}
|
|
151
|
+
<rect x={340} y={1488} width={400} height={12} fill="#FFD66B" opacity={luz} />
|
|
152
|
+
<path d={`M340 1500 L740 1500 L${900} 1920 L${180} 1920 Z`} fill={`rgba(255,214,107,${0.12 * luz})`} />
|
|
153
|
+
{/* sombra de pies bajo la puerta */}
|
|
154
|
+
{g > 0 ? <rect x={500} y={1489} width={80} height={10} fill="#0A0A0A" opacity={0.85} /> : null}
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function Noche({t, cuando, lugar = 'departamento', llegar, golpe}: Props) {
|
|
160
|
+
if (lugar === 'tunel') return <Tunel t={t} tTren={llegar ? cuando(llegar) : 0.3} />;
|
|
161
|
+
if (lugar === 'pasillo') return <Pasillo t={t} tGolpe={golpe ? cuando(golpe) : 1} />;
|
|
162
|
+
return <Departamento t={t} />;
|
|
163
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Escenarios ilustrados incluidos (1080×1920), para que un personaje nunca quede sobre un fondo vacío.
|
|
2
|
+
// fondo: {tipo: 'escenario', nombre: 'cuarto'} como fondo de escena (o del estilo)
|
|
3
|
+
// {tipo: 'escenario', nombre: 'cuarto', parte: 'frente'} como capa (por ejemplo, el escritorio delante del personaje)
|
|
4
|
+
// Todos se animan con el tiempo de la escena (estrellas, cortinas, luces, tren, golpes en la puerta...).
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import {Bosque} from './Bosque';
|
|
7
|
+
import {Cuarto} from './Cuarto';
|
|
8
|
+
import {Noche} from './Noche';
|
|
9
|
+
|
|
10
|
+
type P = {t: number; dur: number; cuando: (en: unknown) => number} & Record<string, any>;
|
|
11
|
+
|
|
12
|
+
export const ESCENARIOS: Record<string, React.FC<P>> = {
|
|
13
|
+
bosque: (p) => <Bosque {...p} />,
|
|
14
|
+
cuarto: (p) => <Cuarto {...p} />,
|
|
15
|
+
departamento: (p) => <Noche {...p} lugar="departamento" />,
|
|
16
|
+
tunel: (p) => <Noche {...p} lugar="tunel" />,
|
|
17
|
+
pasillo: (p) => <Noche {...p} lugar="pasillo" />,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const NOMBRES_ESCENARIOS = Object.keys(ESCENARIOS);
|
|
21
|
+
|
|
22
|
+
export const Escenario: React.FC<P & {nombre: string}> = ({nombre, ...p}) => {
|
|
23
|
+
const Comp = ESCENARIOS[nombre];
|
|
24
|
+
if (!Comp) throw new Error(`Escenario "${nombre}" no existe. Hay: ${NOMBRES_ESCENARIOS.join(', ')}`);
|
|
25
|
+
return <Comp {...p} />;
|
|
26
|
+
};
|
package/remotion/motor/fondo.tsx
CHANGED
|
@@ -4,9 +4,10 @@ import React from 'react';
|
|
|
4
4
|
import {AbsoluteFill, Img, OffthreadVideo} from 'remotion';
|
|
5
5
|
import {H, W} from '../estilo';
|
|
6
6
|
import type {Fondo as TFondo, Particulas as TParticulas, Tema} from './tema';
|
|
7
|
-
import {azar, recurso} from './util';
|
|
7
|
+
import {azar, cuando, recurso, type CtxEscena} from './util';
|
|
8
|
+
import {Escenario} from './escenarios';
|
|
8
9
|
|
|
9
|
-
const Base: React.FC<{f: TFondo}> = ({f}) => {
|
|
10
|
+
const Base: React.FC<{f: TFondo; ctx?: CtxEscena}> = ({f, ctx}) => {
|
|
10
11
|
switch (f.tipo) {
|
|
11
12
|
case 'color':
|
|
12
13
|
return <AbsoluteFill style={{background: f.color}} />;
|
|
@@ -19,6 +20,17 @@ const Base: React.FC<{f: TFondo}> = ({f}) => {
|
|
|
19
20
|
{f.oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${f.oscurecer})`}} /> : null}
|
|
20
21
|
</AbsoluteFill>
|
|
21
22
|
);
|
|
23
|
+
case 'escenario': {
|
|
24
|
+
// escenario ilustrado animado: usa el tiempo de la escena (sin ctx, por ejemplo en la miniatura de otro lado, queda quieto)
|
|
25
|
+
const {tipo, nombre, oscurecer, ...props} = f as any;
|
|
26
|
+
const c = ctx ?? ({t: 0, dur: 1, habla: [0, 1], palabras: [], semilla: '', tema: undefined as any} as CtxEscena);
|
|
27
|
+
return (
|
|
28
|
+
<AbsoluteFill>
|
|
29
|
+
<Escenario nombre={nombre} {...props} t={c.t} dur={c.dur} cuando={(en: unknown) => cuando(en, c)} />
|
|
30
|
+
{oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${oscurecer})`}} /> : null}
|
|
31
|
+
</AbsoluteFill>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
22
34
|
case 'video':
|
|
23
35
|
return (
|
|
24
36
|
<AbsoluteFill>
|
|
@@ -110,8 +122,8 @@ export const Particulas: React.FC<{p: TParticulas; t: number; semilla: string}>
|
|
|
110
122
|
};
|
|
111
123
|
|
|
112
124
|
/** Ruido fino (película) o papel: un SVG fractal estable. */
|
|
113
|
-
const Textura: React.FC<{tipo: 'papel' | 'grano'}> = ({tipo}) => (
|
|
114
|
-
<AbsoluteFill style={{opacity: tipo === 'papel' ? 0.
|
|
125
|
+
export const Textura: React.FC<{tipo: 'papel' | 'grano'}> = ({tipo}) => (
|
|
126
|
+
<AbsoluteFill style={{opacity: tipo === 'papel' ? 0.22 : 0.1, mixBlendMode: 'multiply', pointerEvents: 'none'}}>
|
|
115
127
|
<svg width={W} height={H}>
|
|
116
128
|
<filter id={`tx-${tipo}`}>
|
|
117
129
|
<feTurbulence type="fractalNoise" baseFrequency={tipo === 'papel' ? 0.9 : 1.6} numOctaves={tipo === 'papel' ? 3 : 2} seed={3} />
|
|
@@ -123,12 +135,12 @@ const Textura: React.FC<{tipo: 'papel' | 'grano'}> = ({tipo}) => (
|
|
|
123
135
|
);
|
|
124
136
|
|
|
125
137
|
/** El fondo completo de una escena (el de la escena gana sobre el del estilo). */
|
|
126
|
-
export const FondoTema: React.FC<{tema: Tema; fondo?: TFondo; t: number; semilla: string}> = ({tema, fondo, t, semilla}) => {
|
|
138
|
+
export const FondoTema: React.FC<{tema: Tema; fondo?: TFondo; t: number; semilla: string; ctx?: CtxEscena}> = ({tema, fondo, t, semilla, ctx}) => {
|
|
127
139
|
const f = fondo ?? tema.fondo;
|
|
128
140
|
const foco = tema.foco;
|
|
129
141
|
return (
|
|
130
142
|
<AbsoluteFill>
|
|
131
|
-
<Base f={f} />
|
|
143
|
+
<Base f={f} ctx={ctx} />
|
|
132
144
|
{tema.rayos ? <Rayos color={tema.rayos.color} fuerza={tema.rayos.fuerza ?? 1} t={t} /> : null}
|
|
133
145
|
{foco ? (
|
|
134
146
|
<AbsoluteFill
|
|
@@ -139,7 +151,6 @@ export const FondoTema: React.FC<{tema: Tema; fondo?: TFondo; t: number; semilla
|
|
|
139
151
|
}}
|
|
140
152
|
/>
|
|
141
153
|
) : null}
|
|
142
|
-
{tema.textura && tema.textura !== 'ninguna' ? <Textura tipo={tema.textura} /> : null}
|
|
143
154
|
{tema.particulas ? <Particulas p={tema.particulas} t={t} semilla={semilla} /> : null}
|
|
144
155
|
</AbsoluteFill>
|
|
145
156
|
);
|