morfeo 0.1.3 → 0.2.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.
@@ -1,15 +1,26 @@
1
1
  // Subtítulos del motor de capas.
2
- // karaoke: frase corta arriba; lo ya dicho en blanco, lo que falta tenue y la palabra actual resaltada.
3
- // pop: 1–3 palabras grandes en mayúsculas, la actual en color de acento (estilo TikTok clásico).
2
+ // karaoke: frase corta arriba; lo ya dicho en un color, lo que falta tenue y la palabra actual resaltada.
3
+ // seleccion: las palabras aparecen al decirse; lo dicho queda tenue, la actual en claro y la palabra clave de la
4
+ // frase se queda dentro de una caja de selección con asas (documentales ilustrados).
5
+ // pop: 1–3 palabras grandes en mayúsculas; la palabra clave en color de acento y la actual da un golpe de escala.
4
6
  // bloque: 2 renglones centrados en mayúsculas; la palabra actual dentro de una caja de color (series animadas).
5
- // palabra: una sola palabra grande a la vez; las largas en color de acento (videos 3D).
7
+ // palabra: una sola palabra grande a la vez; la palabra clave en color de acento (videos 3D).
8
+ // caja: la frase completa dentro de una etiqueta clara (historias, anime).
6
9
  // karaoke con `fondo`: la frase dentro de una caja oscura (infografías).
10
+ // Palabra clave de una frase: la que la escena marque en `claves: ['cerebro']`; si no, una cifra; si no, la más larga.
7
11
  import React from 'react';
8
- import {W, backOut, clamp} from '../estilo';
12
+ import {W, backOut, clamp, easeOut, resorte} from '../estilo';
9
13
  import type {Tema} from './tema';
10
14
 
11
15
  type Palabra = [string, number, number];
12
16
 
17
+ const limpia = (w: string) =>
18
+ w
19
+ .normalize('NFD')
20
+ .replace(/[̀-ͯ]/g, '')
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]/g, '');
23
+
13
24
  /** Parte las palabras de la escena en frases de hasta `max` palabras, cortando en la puntuación. */
14
25
  function frases(palabras: Palabra[], max: number): Palabra[][] {
15
26
  const out: Palabra[][] = [];
@@ -25,23 +36,54 @@ function frases(palabras: Palabra[], max: number): Palabra[][] {
25
36
  return out;
26
37
  }
27
38
 
28
- export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; dur: number}> = ({tema, palabras, t, dur}) => {
39
+ /** Índice de la palabra clave de la frase (-1 = ninguna). */
40
+ function claveDe(g: Palabra[], claves?: string[]): number {
41
+ if (claves?.length) {
42
+ const cs = claves.map(limpia).filter(Boolean);
43
+ const i = g.findIndex(([w]) => cs.some((c) => limpia(w).startsWith(c)));
44
+ if (i >= 0) return i;
45
+ }
46
+ const cifra = g.findIndex(([w]) => /\d/.test(w));
47
+ if (cifra >= 0) return cifra;
48
+ let mejor = -1;
49
+ let largo = 7; // solo palabras de 8 letras o más
50
+ g.forEach(([w], i) => {
51
+ const n = limpia(w).length;
52
+ if (n > largo) {
53
+ largo = n;
54
+ mejor = i;
55
+ }
56
+ });
57
+ return mejor;
58
+ }
59
+
60
+ /** Golpe de escala al decir una palabra: sube de golpe y se asienta. */
61
+ const golpe = (t: number, a: number, fuerza: number) => (t < a ? 1 : 1 + fuerza * (1 - easeOut((t - a) / 0.22)));
62
+
63
+ const Asa: React.FC<{x: string; y: string; color: string}> = ({x, y, color}) => (
64
+ <span style={{position: 'absolute', left: x, top: y, width: 11, height: 11, marginLeft: -6, marginTop: -6, background: '#fff', border: `2.5px solid ${color}`, boxSizing: 'border-box'}} />
65
+ );
66
+
67
+ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; dur: number; claves?: string[]}> = ({tema, palabras, t, dur, claves}) => {
29
68
  const S = tema.subtitulos;
30
69
  if (S.tipo === 'ninguno' || !palabras.length) return null;
31
- const max = S.tipo === 'pop' ? 3 : S.tipo === 'bloque' ? Math.min(S.maxPalabras, 5) : S.tipo === 'palabra' ? 1 : S.maxPalabras;
70
+ const max = S.tipo === 'pop' ? Math.min(S.maxPalabras, 4) : S.tipo === 'bloque' ? Math.min(S.maxPalabras, 5) : S.tipo === 'palabra' ? 1 : S.maxPalabras;
32
71
  const grupos = frases(palabras, max);
33
72
  const gi = grupos.findIndex((g, i) => t < (grupos[i + 1]?.[0][1] ?? dur) - 0.02);
34
73
  const g = grupos[gi];
35
74
  if (!g || t < g[0][1] - 0.12) return null;
36
75
  const entrada = clamp((t - (g[0][1] - 0.12)) / 0.18);
76
+ const clave = claveDe(g, claves);
77
+ const texto = (w: string) => (S.mayusculas ? w.toUpperCase() : w);
37
78
 
38
79
  if (S.tipo === 'palabra') {
39
80
  const [w] = g[0];
40
- const acento = w.replace(/[^\p{L}\p{N}]/gu, '').length >= 7;
81
+ // con una palabra por frase, la clave se decide sobre toda la escena: las marcadas, las cifras y las largas
82
+ const acento = claveDe([g[0]], claves) === 0;
41
83
  return (
42
- <div style={{position: 'absolute', left: 40, width: W - 80, top: S.y, transform: `translateY(-50%) scale(${backOut(entrada)})`, textAlign: 'center'}}>
84
+ <div style={{position: 'absolute', left: 40, width: W - 80, top: S.y, transform: `translateY(-50%) scale(${resorte(entrada, 0.25)})`, textAlign: 'center'}}>
43
85
  <span style={{fontFamily: tema.fuentes.texto, fontWeight: 900, fontSize: S.tam * 1.25, color: acento ? tema.colores.acento : S.color, WebkitTextStroke: `${S.contorno ?? 9}px #000`, paintOrder: 'stroke fill', textShadow: '0 6px 0 rgba(0,0,0,0.35)'}}>
44
- {S.mayusculas ? w.toUpperCase() : w}
86
+ {texto(w)}
45
87
  </span>
46
88
  </div>
47
89
  );
@@ -67,7 +109,8 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
67
109
  borderRadius: 10,
68
110
  WebkitTextStroke: actual && S.resalte ? undefined : `${S.contorno ?? 8}px #1a1a1a`,
69
111
  paintOrder: 'stroke fill',
70
- transform: `scale(${actual ? 1.06 : 1})`,
112
+ display: 'inline-block',
113
+ transform: `scale(${golpe(t, a, 0.1)})`,
71
114
  }}
72
115
  >
73
116
  {w}
@@ -79,14 +122,94 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
79
122
  }
80
123
 
81
124
  if (S.tipo === 'pop') {
125
+ const tam = S.tam * 1.5;
82
126
  return (
83
- <div style={{position: 'absolute', left: 60, width: W - 120, top: S.y, transform: `translateY(-50%) scale(${backOut(entrada)})`, display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: `0 ${Math.round(S.tam * 1.5 * 0.4 + 20)}px`}}>
84
- {g.map(([w, a, b], i) => {
85
- const activa = t >= a && t < b + 0.05;
127
+ <div style={{position: 'absolute', left: 60, width: W - 120, top: S.y, transform: `translateY(-50%) scale(${resorte(entrada, 0.2)})`, display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: `0 ${Math.round(tam * 0.3)}px`}}>
128
+ {g.map(([w, a], i) => (
129
+ <span
130
+ key={i}
131
+ style={{
132
+ fontFamily: tema.fuentes.titulo,
133
+ fontWeight: 900,
134
+ fontSize: tam,
135
+ lineHeight: 1.12,
136
+ color: i === clave ? tema.colores.acento : S.color,
137
+ WebkitTextStroke: `${S.contorno ?? 10}px #000`,
138
+ paintOrder: 'stroke fill',
139
+ textShadow: '0 6px 0 rgba(0,0,0,0.4)',
140
+ textTransform: 'uppercase',
141
+ display: 'inline-block',
142
+ transformOrigin: 'center bottom',
143
+ opacity: t >= a - 0.04 ? 1 : 0.55,
144
+ transform: `scale(${golpe(t, a, 0.14)})`,
145
+ }}
146
+ >
147
+ {w}
148
+ </span>
149
+ ))}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ if (S.tipo === 'caja') {
155
+ return (
156
+ <div style={{position: 'absolute', left: 70, width: W - 140, top: S.y, transform: `translateY(-50%) scale(${0.94 + 0.06 * resorte(entrada, 0.2)})`, opacity: clamp(entrada * 2), textAlign: 'center'}}>
157
+ <span
158
+ style={{
159
+ display: 'inline-block',
160
+ background: S.fondo ?? '#EAF3FF',
161
+ color: S.color,
162
+ fontFamily: tema.fuentes.texto,
163
+ fontWeight: 800,
164
+ fontSize: S.tam,
165
+ lineHeight: 1.25,
166
+ padding: '14px 30px',
167
+ borderRadius: 18,
168
+ boxShadow: '0 8px 22px rgba(0,0,0,0.28)',
169
+ }}
170
+ >
171
+ {g.map(([w, a], i) => (
172
+ <React.Fragment key={i}>
173
+ <span style={{opacity: t >= a - 0.04 ? 1 : 0.38, color: i === clave && S.resalte ? S.resalte : undefined}}>{texto(w)}</span>{' '}
174
+ </React.Fragment>
175
+ ))}
176
+ </span>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ if (S.tipo === 'seleccion') {
182
+ const marco = S.resalte || '#4C8DFF';
183
+ const dichas = g.filter(([, a]) => t >= a - 0.03);
184
+ if (!dichas.length) return null;
185
+ return (
186
+ <div style={{position: 'absolute', left: 90, width: W - 180, top: S.y, transform: 'translateY(-50%)', textAlign: 'center', fontFamily: tema.fuentes.texto, fontSize: S.tam, lineHeight: 1.42, textShadow: '0 2px 10px rgba(0,0,0,0.45)'}}>
187
+ {dichas.map(([w, a, b], i) => {
188
+ const actual = t < b + 0.08 || i === dichas.length - 1;
189
+ const esClave = i === clave;
190
+ const k = resorte(clamp((t - a) / 0.3), 0.3);
86
191
  return (
87
- <span key={i} style={{fontFamily: tema.fuentes.titulo, fontWeight: 900, fontSize: S.tam * 1.5, color: activa ? tema.colores.acento : S.color, WebkitTextStroke: '10px #000', paintOrder: 'stroke fill', textTransform: 'uppercase', display: 'inline-block', transformOrigin: 'center bottom', transform: `scale(${activa ? 1.1 : 1})`}}>
88
- {w}
89
- </span>
192
+ <React.Fragment key={i}>
193
+ <span
194
+ style={{
195
+ position: 'relative',
196
+ display: 'inline-block',
197
+ color: actual || esClave ? S.color : S.pendiente,
198
+ fontWeight: actual || esClave ? 800 : 600,
199
+ opacity: clamp((t - a + 0.03) / 0.1),
200
+ padding: esClave ? '0 10px' : undefined,
201
+ margin: esClave ? '0 2px' : undefined,
202
+ }}
203
+ >
204
+ {esClave ? (
205
+ <span style={{position: 'absolute', inset: '4px 0', border: `3px solid ${marco}`, background: `${marco}33`, borderRadius: 3, transform: `scale(${0.7 + 0.3 * k})`, opacity: clamp(k * 2)}}>
206
+ <Asa x="0%" y="0%" color={marco} />
207
+ <Asa x="100%" y="100%" color={marco} />
208
+ </span>
209
+ ) : null}
210
+ <span style={{position: 'relative'}}>{texto(w)}</span>
211
+ </span>{' '}
212
+ </React.Fragment>
90
213
  );
91
214
  })}
92
215
  </div>
@@ -111,26 +234,26 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
111
234
  }}
112
235
  >
113
236
  <span style={S.fondo ? {background: S.fondo, padding: '10px 22px', borderRadius: 14, boxDecorationBreak: 'clone', WebkitBoxDecorationBreak: 'clone', lineHeight: 1.6} : undefined}>
114
- {g.map(([w, a, b], i) => {
115
- const dicha = t >= a;
116
- const actual = t >= a && t < b + 0.06;
117
- return (
118
- <React.Fragment key={i}>
119
- <span
120
- style={{
121
- color: dicha ? S.color : S.pendiente,
122
- fontWeight: actual ? 800 : 700,
123
- background: actual && S.resalte ? S.resalte : undefined,
124
- borderRadius: 10,
125
- padding: actual && S.resalte ? '2px 10px' : undefined,
126
- margin: actual && S.resalte ? '0 -10px' : undefined,
127
- }}
128
- >
129
- {w}
130
- </span>{' '}
131
- </React.Fragment>
132
- );
133
- })}
237
+ {g.map(([w, a, b], i) => {
238
+ const dicha = t >= a;
239
+ const actual = t >= a && t < b + 0.06;
240
+ return (
241
+ <React.Fragment key={i}>
242
+ <span
243
+ style={{
244
+ color: dicha ? S.color : S.pendiente,
245
+ fontWeight: actual ? 800 : 700,
246
+ background: actual && S.resalte ? S.resalte : undefined,
247
+ borderRadius: 10,
248
+ padding: actual && S.resalte ? '2px 10px' : undefined,
249
+ margin: actual && S.resalte ? '0 -10px' : undefined,
250
+ }}
251
+ >
252
+ {texto(w)}
253
+ </span>{' '}
254
+ </React.Fragment>
255
+ );
256
+ })}
134
257
  </span>
135
258
  </div>
136
259
  );
@@ -32,7 +32,12 @@ export type Tema = {
32
32
  fuentes: {titulo: string; texto: string; mono: string};
33
33
  tituloMayusculas: boolean;
34
34
  subtitulos: {
35
- tipo: 'karaoke' | 'pop' | 'bloque' | 'palabra' | 'ninguno';
35
+ /**
36
+ * karaoke: frase arriba, lo dicho en un color y lo pendiente tenue · seleccion: igual, pero solo la palabra clave
37
+ * de cada frase lleva caja de selección con asas · pop: 1–3 palabras grandes · bloque: 2 renglones con caja ·
38
+ * palabra: una palabra a la vez · caja: la frase completa en una etiqueta clara.
39
+ */
40
+ tipo: 'karaoke' | 'seleccion' | 'pop' | 'bloque' | 'palabra' | 'caja' | 'ninguno';
36
41
  mayusculas?: boolean;
37
42
  contorno?: number;
38
43
  fondo?: string; // caja detrás de la frase (karaoke)
@@ -43,7 +48,16 @@ export type Tema = {
43
48
  resalte: string; // caja de la palabra actual ('' = sin caja)
44
49
  maxPalabras: number;
45
50
  };
46
- transicion: 'corte' | 'fundido' | 'deslizar' | 'zoom';
51
+ transicion: 'corte' | 'fundido' | 'deslizar' | 'zoom' | 'barrido' | 'empuje' | 'mascara' | 'destello';
52
+ /** Ritmo de las animaciones: duraciones (s) y curva de las entradas. Sin esto: 0.5 / 0.35 / 0.35 y 'suave'. */
53
+ movimiento?: {entrada?: number; salida?: number; transicion?: number; curva?: 'suave' | 'resorte' | 'seca'};
54
+ /** Acabado sobre todo el cuadro: tinte de color, cono de luz y desenfoque de bordes (profundidad). */
55
+ post?: {
56
+ tinte?: {color: string; fuerza?: number; modo?: 'multiply' | 'screen' | 'overlay' | 'soft-light'};
57
+ luz?: {x?: number; y?: number; ancho?: number; largo?: number; color?: string; fuerza?: number; angulo?: number};
58
+ desenfoqueBordes?: number; // px de blur arriba y abajo (efecto maqueta / profundidad)
59
+ vinetaColor?: string;
60
+ };
47
61
  marca: 'esquina' | 'arriba' | 'ninguna';
48
62
  /** Movimiento suave de cámara por defecto en cada escena (zoom de 1 a este valor). */
49
63
  zoomLento: number;
@@ -62,8 +76,9 @@ export const ESTILOS: Record<string, Tema> = {
62
76
  tituloMayusculas: true,
63
77
  subtitulos: {...SUB_BASE, y: 300, color: '#141414', pendiente: 'rgba(20,20,20,0.28)', resalte: ''},
64
78
  transicion: 'deslizar',
79
+ movimiento: {entrada: 0.45, salida: 0.25, transicion: 0.42, curva: 'seca'},
65
80
  marca: 'esquina',
66
- zoomLento: 1.03,
81
+ zoomLento: 1.02,
67
82
  },
68
83
  // Fondo azul profundo con rayos de luz y burbujas; datos en chips (naturaleza, ciencia, océano)
69
84
  documental: {
@@ -74,10 +89,12 @@ export const ESTILOS: Record<string, Tema> = {
74
89
  colores: {texto: '#FFFFFF', acento: '#7FE3FF', acento2: '#FF5A5A', suave: 'rgba(255,255,255,0.55)', tarjeta: 'rgba(8,20,40,0.78)', tarjetaTexto: '#FFFFFF', borde: '#7FE3FF'},
75
90
  fuentes: {titulo: MONT, texto: MONT, mono: MONO},
76
91
  tituloMayusculas: false,
77
- subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.42)', resalte: '#2C7BE5'},
92
+ subtitulos: {...SUB_BASE, tipo: 'seleccion', y: 310, tam: 50, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.5)', resalte: '#4C8DFF'},
78
93
  transicion: 'fundido',
94
+ movimiento: {entrada: 0.6, salida: 0.4, transicion: 0.5, curva: 'suave'},
95
+ post: {tinte: {color: '#1E6BFF', fuerza: 0.14, modo: 'soft-light'}},
79
96
  marca: 'esquina',
80
- zoomLento: 1.06,
97
+ zoomLento: 1.07,
81
98
  },
82
99
  // Escenarios ilustrados (bosque, ciudad): el fondo suele ser una imagen tuya; cielo de atardecer por defecto
83
100
  ilustrado: {
@@ -87,8 +104,10 @@ export const ESTILOS: Record<string, Tema> = {
87
104
  colores: {texto: '#FFFFFF', acento: '#FFE27A', acento2: '#FF6B6B', suave: 'rgba(255,255,255,0.55)', tarjeta: 'rgba(20,14,30,0.75)', tarjetaTexto: '#FFFFFF', borde: '#FFFFFF'},
88
105
  fuentes: {titulo: MONT, texto: MONT, mono: MONO},
89
106
  tituloMayusculas: false,
90
- subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '#3D5AFE'},
107
+ subtitulos: {...SUB_BASE, tipo: 'seleccion', y: 310, tam: 50, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.5)', resalte: '#4C8DFF'},
91
108
  transicion: 'corte',
109
+ movimiento: {entrada: 0.55, salida: 0.3, transicion: 0.4, curva: 'resorte'},
110
+ post: {tinte: {color: '#FF9A5A', fuerza: 0.1, modo: 'soft-light'}},
92
111
  marca: 'esquina',
93
112
  zoomLento: 1.08,
94
113
  },
@@ -101,10 +120,12 @@ export const ESTILOS: Record<string, Tema> = {
101
120
  colores: {texto: '#FFFFFF', acento: '#FFD66B', acento2: '#FF4A4A', suave: 'rgba(255,255,255,0.5)', tarjeta: 'rgba(0,0,0,0.6)', tarjetaTexto: '#FFD66B', borde: '#FFD66B'},
102
121
  fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
103
122
  tituloMayusculas: true,
104
- subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.4)', resalte: ''},
123
+ subtitulos: {...SUB_BASE, tipo: 'seleccion', y: 310, tam: 50, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '#FFD66B'},
105
124
  transicion: 'corte',
125
+ movimiento: {entrada: 0.6, salida: 0.35, transicion: 0.45, curva: 'suave'},
126
+ post: {luz: {x: 700, ancho: 900, largo: 1700, color: '#FFE2A8', fuerza: 0.22, angulo: 12}, vinetaColor: '#05060F'},
106
127
  marca: 'esquina',
107
- zoomLento: 1.05,
128
+ zoomLento: 1.06,
108
129
  },
109
130
  // Objetos sobre una mesa con foco de luz, contadores tipo LCD (dinero, crecimiento, comparaciones)
110
131
  mesa: {
@@ -115,10 +136,12 @@ export const ESTILOS: Record<string, Tema> = {
115
136
  colores: {texto: '#FFFFFF', acento: '#FFD24A', acento2: '#5CFF7A', suave: 'rgba(255,255,255,0.5)', tarjeta: 'rgba(10,8,6,0.85)', tarjetaTexto: '#FFD24A', borde: '#3A2A1A'},
116
137
  fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
117
138
  tituloMayusculas: true,
118
- subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.4)', resalte: '#2C7BE5'},
139
+ subtitulos: {...SUB_BASE, tipo: 'seleccion', y: 310, tam: 50, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '#4C8DFF'},
119
140
  transicion: 'fundido',
141
+ movimiento: {entrada: 0.55, salida: 0.35, transicion: 0.5, curva: 'resorte'},
142
+ post: {luz: {x: 540, ancho: 820, largo: 1500, color: '#FFD9A0', fuerza: 0.3}, desenfoqueBordes: 5, vinetaColor: '#0A0503'},
120
143
  marca: 'esquina',
121
- zoomLento: 1.04,
144
+ zoomLento: 1.05,
122
145
  },
123
146
  // El de los shorts clásicos (morado oscuro, Anton, amarillo): para capas dentro de 'datos-curiosos'
124
147
  'datos-curiosos': {
@@ -129,6 +152,7 @@ export const ESTILOS: Record<string, Tema> = {
129
152
  tituloMayusculas: true,
130
153
  subtitulos: {...SUB_BASE, tipo: 'pop', y: 1420, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: ''},
131
154
  transicion: 'corte',
155
+ movimiento: {entrada: 0.45, salida: 0.25, transicion: 0.3, curva: 'resorte'},
132
156
  marca: 'arriba',
133
157
  zoomLento: 1,
134
158
  },
@@ -141,8 +165,9 @@ export const ESTILOS: Record<string, Tema> = {
141
165
  tituloMayusculas: true,
142
166
  subtitulos: {tipo: 'bloque', y: 1080, tam: 62, maxPalabras: 4, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: '#FF3B5C', contorno: 8},
143
167
  transicion: 'corte',
168
+ movimiento: {entrada: 0.45, salida: 0.25, transicion: 0.3, curva: 'resorte'},
144
169
  marca: 'ninguna',
145
- zoomLento: 1.02,
170
+ zoomLento: 1.03,
146
171
  },
147
172
  // Infografía plana (cuerpo, ciencia, comparaciones): gris claro, encabezado y subtítulos abajo en caja
148
173
  infografia: {
@@ -152,22 +177,25 @@ export const ESTILOS: Record<string, Tema> = {
152
177
  fuentes: {titulo: MONT, texto: MONT, mono: MONO},
153
178
  tituloMayusculas: true,
154
179
  subtitulos: {tipo: 'karaoke', y: 1500, tam: 48, maxPalabras: 4, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '', fondo: '#1B1B1F'},
155
- transicion: 'corte',
180
+ transicion: 'empuje',
181
+ movimiento: {entrada: 0.42, salida: 0.25, transicion: 0.45, curva: 'seca'},
156
182
  marca: 'esquina',
157
- zoomLento: 1.0,
183
+ zoomLento: 1.02,
158
184
  },
159
185
  // Escenas 3D (construcción, cómo funciona por dentro): una palabra grande a la vez
160
186
  '3d': {
161
187
  fondo: {tipo: 'degradado', colores: ['#BFE3FF', '#EAF6FF']},
162
188
  vineta: 0.1,
163
189
  // cielo claro: el texto propio va oscuro; los subtítulos siguen blancos con contorno negro
164
- colores: {texto: '#14233A', acento: '#FF8A00', acento2: '#FF5A36', suave: '#4F6278', tarjeta: 'rgba(20,20,20,0.78)', tarjetaTexto: '#FFFFFF', borde: '#141414'},
190
+ colores: {texto: '#14233A', acento: '#FFC400', acento2: '#FF5A36', suave: '#4F6278', tarjeta: 'rgba(20,20,20,0.78)', tarjetaTexto: '#FFFFFF', borde: '#141414'},
165
191
  fuentes: {titulo: MONT, texto: MONT, mono: MONO},
166
192
  tituloMayusculas: true,
167
- subtitulos: {tipo: 'palabra', y: 1400, tam: 76, maxPalabras: 1, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: '', contorno: 10},
193
+ subtitulos: {tipo: 'pop', y: 1380, tam: 50, maxPalabras: 3, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: '', contorno: 10},
168
194
  transicion: 'corte',
195
+ movimiento: {entrada: 0.5, salida: 0.3, transicion: 0.35, curva: 'resorte'},
196
+ post: {desenfoqueBordes: 4},
169
197
  marca: 'esquina',
170
- zoomLento: 1.0,
198
+ zoomLento: 1.04,
171
199
  },
172
200
  // Oscuro y limpio, para lo que sea
173
201
  minimal: {
@@ -178,8 +206,9 @@ export const ESTILOS: Record<string, Tema> = {
178
206
  tituloMayusculas: false,
179
207
  subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.35)', resalte: '#8B7BFF'},
180
208
  transicion: 'fundido',
209
+ movimiento: {entrada: 0.5, salida: 0.3, transicion: 0.4, curva: 'suave'},
181
210
  marca: 'esquina',
182
- zoomLento: 1.03,
211
+ zoomLento: 1.04,
183
212
  },
184
213
  };
185
214
 
@@ -10,6 +10,9 @@ export type CtxEscena = {
10
10
  tema: Tema;
11
11
  semilla: string;
12
12
  inicios?: number[]; // inicio de cada escena (para el HUD: cambios en {escena: n})
13
+ escena?: number; // en el `mundo`: número de la escena actual (1 = la primera)
14
+ camara?: {x: number; y: number; zoom: number; giro: number}; // en el `mundo`: la cámara de este cuadro (para paralaje)
15
+ tEscena?: number; // en el `mundo`: segundos desde que empezó la escena actual
13
16
  };
14
17
 
15
18
  const norm = (s: string) =>
package/remotion/tipos.ts CHANGED
@@ -21,6 +21,8 @@ export type Datos = {
21
21
  estilo?: string; // 'datos-curiosos' (clásico) o un estilo del motor de capas
22
22
  tema?: Record<string, any>; // ajustes del estilo para este video
23
23
  hud?: Record<string, any>[]; // capas fijas sobre todo el video (día, barra de progreso, hora...)
24
+ mundo?: Record<string, any>[]; // capas del mundo: persisten todo el video y la cámara las recorre con la `toma` de cada escena
25
+ serie?: string; // pastilla fija arriba: 'GYM CON CIENCIA · PARTE 5'
24
26
  audio: string;
25
27
  whisper: number;
26
28
  escenas: Escena[];