morfeo 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,21 @@ const EscenaVista: React.FC<{e: Escena; t: number; tema: Tema; semilla: string;
53
53
  );
54
54
  };
55
55
 
56
+ /** Lugar (mundo) de la escena `i`: el suyo, o el de la última escena anterior que dijo uno, o el primero de `lugares`. */
57
+ export function lugarDe(d: Datos, i: number): string | null {
58
+ const nombres = Object.keys(d.lugares ?? {});
59
+ if (!nombres.length) return null;
60
+ for (let j = i; j >= 0; j--) {
61
+ const l = d.escenas[j]?.p.lugar;
62
+ if (typeof l === 'string' && d.lugares![l]) return l;
63
+ }
64
+ return d.mundo?.length ? null : nombres[0];
65
+ }
66
+ const capasDeLugar = (d: Datos, i: number) => {
67
+ const l = lugarDe(d, i);
68
+ return (l ? d.lugares![l] : d.mundo) ?? [];
69
+ };
70
+
56
71
  type Toma = {x: number; y: number; zoom: number; giro: number};
57
72
  const TOMA0: Toma = {x: 540, y: 960, zoom: 1, giro: 0};
58
73
 
@@ -62,8 +77,10 @@ const TOMA0: Toma = {x: 540, y: 960, zoom: 1, giro: 0};
62
77
  * Sin `toma`, la escena conserva la anterior. `corte: true` salta sin viaje.
63
78
  */
64
79
  export function camaraMundo(d: Datos, si: number, t: number): Toma {
80
+ // cada lugar tiene su propia cámara: al buscar la toma anterior no se cruza a otro lugar
65
81
  const destino = (i: number): Toma => {
66
- for (let j = i; j >= 0; j--) if (d.escenas[j].p.toma) return {...TOMA0, ...d.escenas[j].p.toma};
82
+ const l = lugarDe(d, i);
83
+ for (let j = i; j >= 0 && lugarDe(d, j) === l; j--) if (d.escenas[j].p.toma) return {...TOMA0, ...d.escenas[j].p.toma};
67
84
  return TOMA0;
68
85
  };
69
86
  const final = (i: number): Toma => {
@@ -76,7 +93,8 @@ export function camaraMundo(d: Datos, si: number, t: number): Toma {
76
93
  const tm = e.p.toma ?? {};
77
94
  const a = destino(si);
78
95
  const b = final(si);
79
- const viaje = tm.corte || si === 0 ? 0 : tm.duracion ?? Math.min(1.1, e.dur * 0.55);
96
+ const cambiaLugar = si > 0 && lugarDe(d, si) !== lugarDe(d, si - 1);
97
+ const viaje = tm.corte || si === 0 || cambiaLugar ? 0 : tm.duracion ?? Math.min(1.1, e.dur * 0.55);
80
98
  const kd = clamp((t - viaje * 0.6) / Math.max(0.01, e.dur - viaje * 0.6));
81
99
  const kq = tm.a ? easeIO(kd) : kd;
82
100
  const actual: Toma = {x: lerp(a.x, b.x, kq), y: lerp(a.y, b.y, kq), zoom: lerp(a.zoom, b.zoom, kq), giro: lerp(a.giro, b.giro, kq)};
@@ -99,7 +117,7 @@ const Mundo: React.FC<{d: Datos; si: number; t: number; T: number; tema: Tema}>
99
117
  const propias = capasDe(e.tipo, e.p, temaEsc, marca).filter((c: any) => c.enMundo);
100
118
  return (
101
119
  <AbsoluteFill style={{transform: `translate(540px, 960px) scale(${cam.zoom}) rotate(${cam.giro}deg) translate(${-cam.x}px, ${-cam.y}px)`, transformOrigin: '0 0'}}>
102
- <Capas capas={d.mundo as any} ctx={ctxMundo} />
120
+ <Capas capas={capasDeLugar(d, si) as any} ctx={ctxMundo} />
103
121
  {propias.length ? <Capas capas={propias} ctx={ctxEsc} /> : null}
104
122
  </AbsoluteFill>
105
123
  );
@@ -198,7 +216,9 @@ const RotuloSello: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
198
216
  const w = ancho(limpio, tema.fuentes.titulo, tam, 900);
199
217
  if (w > 820) tam = Math.max(44, Math.floor((tam * 820) / w));
200
218
  } catch {}
201
- const y = e.p.rotuloY ?? tema.rotulo?.y ?? 1180;
219
+ // lejos de los subtítulos: si van a media altura, el sello se pone arriba de ellos
220
+ const S = tema.subtitulos;
221
+ const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && Math.abs(S.y - 1180) < 260 ? S.y - 300 : 1180);
202
222
  return (
203
223
  <div style={{position: 'absolute', left: 0, width: W, top: y, display: 'flex', justifyContent: 'center', pointerEvents: 'none', opacity: clamp(caida * 4)}}>
204
224
  <div
@@ -238,7 +258,9 @@ const RotuloPaso: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
238
258
  const k = easeIO(clamp(t / 0.35));
239
259
  const color = colorRotulo(e.p.rotuloColor ?? tema.rotulo?.color, tema.colores.acento);
240
260
  const tam = e.p.rotuloTam ?? tema.rotulo?.tam ?? 64;
241
- const y = e.p.rotuloY ?? tema.rotulo?.y ?? 250;
261
+ // si los subtítulos van arriba, el titular baja para no encimarse con ellos
262
+ const S = tema.subtitulos;
263
+ const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && S.y < 600 ? S.y + 120 : 250);
242
264
  const n = e.p.rotuloPaso;
243
265
  return (
244
266
  <div style={{position: 'absolute', left: 56, right: 56, top: y, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, pointerEvents: 'none'}}>
@@ -305,7 +327,8 @@ const RotuloPixel: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
305
327
  const w = ancho(limpio, PIXEL, tam, 400);
306
328
  if (w > 900) tam = Math.max(30, Math.floor((tam * 900) / w));
307
329
  } catch {}
308
- const y = e.p.rotuloY ?? tema.rotulo?.y ?? 230;
330
+ const S = tema.subtitulos;
331
+ const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && S.y < 600 ? S.y + 150 : 230);
309
332
  return (
310
333
  <div style={{position: 'absolute', left: 0, width: W, top: y, display: 'flex', justifyContent: 'center', pointerEvents: 'none'}}>
311
334
  <div
@@ -360,7 +383,7 @@ const MarcaEsquina: React.FC<{tema: Tema}> = ({tema}) => {
360
383
  );
361
384
  };
362
385
 
363
- export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string, any>; miniatura?: boolean}> = ({d, T, estilos, miniatura}) => {
386
+ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string, any>; miniatura?: boolean; sinMarca?: boolean}> = ({d, T, estilos, miniatura, sinMarca}) => {
364
387
  const tema = temaDe(d.estilo, d.tema, estilos);
365
388
  const n = d.escenas.length;
366
389
  let si = d.escenas.findIndex((e) => T < e.t0 + e.dur);
@@ -368,10 +391,12 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
368
391
  const e = d.escenas[si];
369
392
  const t = T - e.t0;
370
393
  const semilla = d.id;
371
- const conMundo = !!d.mundo?.length;
394
+ const conMundo = !!d.mundo?.length || Object.keys(d.lugares ?? {}).length > 0;
372
395
  const DUR = tema.movimiento?.transicion ?? TRANSICION;
373
- // con mundo, la transición es el viaje de la cámara: las capas de pantalla solo entran con su propia animación
374
- const trans = conMundo ? 'corte' : ((e.p.transicion ?? tema.transicion) as string);
396
+ // con mundo, la transición es el viaje de la cámara: las capas de pantalla solo entran con su propia animación.
397
+ // Al cambiar de lugar sí hay transición (la de la escena; corte si no dice), y el lugar anterior sale con ella.
398
+ const cambiaLugar = conMundo && si > 0 && lugarDe(d, si) !== lugarDe(d, si - 1);
399
+ const trans = conMundo ? (cambiaLugar ? ((e.p.transicion as string) ?? 'corte') : 'corte') : ((e.p.transicion ?? tema.transicion) as string);
375
400
  const enTransicion = si > 0 && t < DUR && trans !== 'corte';
376
401
  const lin = clamp(t / DUR);
377
402
  const k = easeOut(lin);
@@ -408,10 +433,19 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
408
433
  return (
409
434
  <AbsoluteFill style={{background: '#000', overflow: 'hidden'}}>
410
435
  <Modo d={d} si={si} t={t} T={T} claro={LOOKS_CLAROS.includes(d.estilo ?? '') || tema.colores.texto !== '#FFFFFF'}>
411
- {conMundo ? <Mundo d={d} si={si} t={t} T={T} tema={tema} /> : null}
436
+ {conMundo && enTransicion && prev ? (
437
+ <AbsoluteFill style={estiloPrevio}>
438
+ <Mundo d={d} si={si - 1} t={prev.dur + t} T={T} tema={tema} />
439
+ </AbsoluteFill>
440
+ ) : null}
441
+ {conMundo ? (
442
+ <AbsoluteFill style={enTransicion ? estiloActual : undefined}>
443
+ <Mundo d={d} si={si} t={t} T={T} tema={tema} />
444
+ </AbsoluteFill>
445
+ ) : null}
412
446
  {enTransicion && prev ? (
413
447
  <AbsoluteFill style={estiloPrevio}>
414
- <EscenaVista e={prev} t={prev.dur + t} tema={tema} semilla={semilla} />
448
+ <EscenaVista e={prev} t={prev.dur + t} tema={tema} semilla={semilla} sinFondo={conMundo} />
415
449
  </AbsoluteFill>
416
450
  ) : null}
417
451
  <AbsoluteFill style={estiloActual}>
@@ -425,18 +459,18 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
425
459
  <Vineta fuerza={tema.vineta ?? 0} color={tema.post?.vinetaColor} />
426
460
  {d.hud?.length ? <Hud d={d} T={T} tema={tema} /> : null}
427
461
  {d.serie && e.tipo !== 'cta' ? <Serie texto={d.serie} tema={tema} /> : null}
428
- {e.tipo !== 'cta' ? <Rotulo e={e} t={t} tema={e.p.tema ? mezclar(tema, e.p.tema) : tema} /> : null}
462
+ {e.tipo !== 'cta' && !sinMarca ? <Rotulo e={e} t={t} tema={e.p.tema ? mezclar(tema, e.p.tema) : tema} /> : null}
429
463
  {e.tipo !== 'cta' && !miniatura ? <Subtitulos tema={e.p.tema ? mezclar(tema, e.p.tema) : tema} palabras={e.palabras ?? []} t={t} dur={e.dur} claves={e.p.claves} /> : null}
430
- {e.tipo !== 'cta' ? <MarcaEsquina tema={tema} /> : null}
464
+ {e.tipo !== 'cta' && !sinMarca ? <MarcaEsquina tema={tema} /> : null}
431
465
  </AbsoluteFill>
432
466
  );
433
467
  };
434
468
 
435
469
  /** Componente raíz de un video del motor de capas (con su audio). */
436
- export const VideoConAudio: React.FC<{d: Datos; frame: number; marca: any; estilos?: Record<string, any>; miniatura?: boolean}> = ({d, frame, marca, estilos, miniatura}) => (
470
+ export const VideoConAudio: React.FC<{d: Datos; frame: number; marca: any; estilos?: Record<string, any>; miniatura?: boolean; sinMarca?: boolean}> = ({d, frame, marca, estilos, miniatura, sinMarca}) => (
437
471
  <MarcaContexto.Provider value={marca}>
438
472
  <AbsoluteFill>
439
- <VideoMotor d={d} T={frame / FPS} estilos={estilos} miniatura={miniatura} />
473
+ <VideoMotor d={d} T={frame / FPS} estilos={estilos} miniatura={miniatura} sinMarca={sinMarca} />
440
474
  <Audio src={staticFile(d.audio)} />
441
475
  </AbsoluteFill>
442
476
  </MarcaContexto.Provider>
@@ -6,6 +6,7 @@ import {Avatar, Icono, tamIcono, useMarca} from '../piezas';
6
6
  import {H, W, ancho, backOut, clamp, easeIO, easeOut, easeOutExpo, lerp, resorte} from '../estilo';
7
7
  import {ANOTACIONES, ANOTACIONES_SIN_ENTRADA, Anotacion} from './anotaciones';
8
8
  import {FICHAS, Fichas} from './fichas';
9
+ import {HISTORIAS, Historias} from './historias';
9
10
  import {azar, cuando, formatoNumero, partesResaltadas, recurso, type CtxEscena} from './util';
10
11
  import {Particulas} from './fondo';
11
12
  import {Escena3D, Modelo3D} from './modelo3d';
@@ -280,6 +281,7 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
280
281
  default:
281
282
  if (ANOTACIONES.includes(c.tipo)) return <Anotacion c={c} ctx={ctx} />;
282
283
  if (FICHAS.includes(c.tipo)) return <Fichas c={c} ctx={ctx} />;
284
+ if (HISTORIAS.includes(c.tipo)) return <Historias c={c} ctx={ctx} />;
283
285
  return <div style={{color: 'red', fontSize: 40, fontFamily: 'sans-serif', background: '#fff', padding: 10}}>capa desconocida: {c.tipo}</div>;
284
286
  }
285
287
  };