morfeo 0.0.0-stage → 0.1.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.
- 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 +246 -0
- package/lib/config.mjs +241 -0
- package/lib/entorno.mjs +93 -0
- package/lib/guion.mjs +309 -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 +278 -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 +119 -0
- package/remotion/motor/capas.tsx +547 -0
- package/remotion/motor/fondo.tsx +149 -0
- package/remotion/motor/modelo3d.tsx +252 -0
- package/remotion/motor/plantillas.tsx +123 -0
- package/remotion/motor/registro.ts +11 -0
- package/remotion/motor/subtitulos.tsx +137 -0
- package/remotion/motor/tema.ts +202 -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,202 @@
|
|
|
1
|
+
// Estilos del motor de capas. Un estilo es un "tema": fondo, colores, fuentes, subtítulos y transiciones.
|
|
2
|
+
// Cada video elige uno (estilo: 'documental') y puede cambiar lo que quiera con `tema: {...}`.
|
|
3
|
+
// En morfeo.config.mjs puedes crear los tuyos: estilos: {'mi-estilo': {base: 'explicativo', colores: {...}}}.
|
|
4
|
+
import {ANTON, MONO, MONT} from '../estilo';
|
|
5
|
+
|
|
6
|
+
export type Fondo =
|
|
7
|
+
| {tipo: 'color'; color: string}
|
|
8
|
+
| {tipo: 'degradado'; colores: string[]; angulo?: number}
|
|
9
|
+
| {tipo: 'imagen'; src: string; oscurecer?: number; desenfoque?: number}
|
|
10
|
+
| {tipo: 'video'; src: string; oscurecer?: number};
|
|
11
|
+
|
|
12
|
+
export type Particulas = {tipo: 'burbujas' | 'polvo' | 'estrellas' | 'nieve' | 'confeti' | 'ninguna'; cantidad?: number; color?: string};
|
|
13
|
+
|
|
14
|
+
export type Tema = {
|
|
15
|
+
fondo: Fondo;
|
|
16
|
+
/** Efectos sobre el fondo */
|
|
17
|
+
rayos?: {color: string; fuerza?: number}; // rayos de luz desde arriba (agua, bosque)
|
|
18
|
+
foco?: {color: string; x?: number; y?: number; radio?: number; fuerza?: number}; // luz puntual (mesa, escenario)
|
|
19
|
+
vineta?: number; // oscurecer bordes 0..1
|
|
20
|
+
textura?: 'papel' | 'grano' | 'ninguna';
|
|
21
|
+
particulas?: Particulas;
|
|
22
|
+
colores: {
|
|
23
|
+
texto: string;
|
|
24
|
+
acento: string; // palabra resaltada, números importantes
|
|
25
|
+
acento2: string;
|
|
26
|
+
suave: string; // texto secundario
|
|
27
|
+
tarjeta: string; // fondo de tarjetas/chips
|
|
28
|
+
tarjetaTexto: string;
|
|
29
|
+
borde: string;
|
|
30
|
+
};
|
|
31
|
+
fuentes: {titulo: string; texto: string; mono: string};
|
|
32
|
+
tituloMayusculas: boolean;
|
|
33
|
+
subtitulos: {
|
|
34
|
+
tipo: 'karaoke' | 'pop' | 'bloque' | 'palabra' | 'ninguno';
|
|
35
|
+
mayusculas?: boolean;
|
|
36
|
+
contorno?: number;
|
|
37
|
+
fondo?: string; // caja detrás de la frase (karaoke)
|
|
38
|
+
y: number; // centro vertical (px de 1920)
|
|
39
|
+
tam: number;
|
|
40
|
+
color: string; // palabras ya dichas
|
|
41
|
+
pendiente: string; // palabras por decir
|
|
42
|
+
resalte: string; // caja de la palabra actual ('' = sin caja)
|
|
43
|
+
maxPalabras: number;
|
|
44
|
+
};
|
|
45
|
+
transicion: 'corte' | 'fundido' | 'deslizar' | 'zoom';
|
|
46
|
+
marca: 'esquina' | 'arriba' | 'ninguna';
|
|
47
|
+
/** Movimiento suave de cámara por defecto en cada escena (zoom de 1 a este valor). */
|
|
48
|
+
zoomLento: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SUB_BASE = {tipo: 'karaoke' as const, y: 330, tam: 54, maxPalabras: 7};
|
|
52
|
+
|
|
53
|
+
export const ESTILOS: Record<string, Tema> = {
|
|
54
|
+
// Fondo papel claro, titulares negros con acento rojo, etiquetas mono, tarjetas de interfaz (tutoriales)
|
|
55
|
+
explicativo: {
|
|
56
|
+
fondo: {tipo: 'color', color: '#ECE6DC'},
|
|
57
|
+
textura: 'papel',
|
|
58
|
+
vineta: 0.12,
|
|
59
|
+
colores: {texto: '#141414', acento: '#E0302A', acento2: '#2D6BFF', suave: '#8A8378', tarjeta: '#FFFFFF', tarjetaTexto: '#141414', borde: '#141414'},
|
|
60
|
+
fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
|
|
61
|
+
tituloMayusculas: true,
|
|
62
|
+
subtitulos: {...SUB_BASE, y: 300, color: '#141414', pendiente: 'rgba(20,20,20,0.28)', resalte: ''},
|
|
63
|
+
transicion: 'deslizar',
|
|
64
|
+
marca: 'esquina',
|
|
65
|
+
zoomLento: 1.03,
|
|
66
|
+
},
|
|
67
|
+
// Fondo azul profundo con rayos de luz y burbujas; datos en chips (naturaleza, ciencia, océano)
|
|
68
|
+
documental: {
|
|
69
|
+
fondo: {tipo: 'degradado', colores: ['#0E4A86', '#062A52', '#020C1C']},
|
|
70
|
+
rayos: {color: 'rgba(160,210,255,0.16)', fuerza: 1},
|
|
71
|
+
vineta: 0.45,
|
|
72
|
+
particulas: {tipo: 'burbujas', cantidad: 26},
|
|
73
|
+
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'},
|
|
74
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
75
|
+
tituloMayusculas: false,
|
|
76
|
+
subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.42)', resalte: '#2C7BE5'},
|
|
77
|
+
transicion: 'fundido',
|
|
78
|
+
marca: 'esquina',
|
|
79
|
+
zoomLento: 1.06,
|
|
80
|
+
},
|
|
81
|
+
// Escenarios ilustrados (bosque, ciudad): el fondo suele ser una imagen tuya; cielo de atardecer por defecto
|
|
82
|
+
ilustrado: {
|
|
83
|
+
fondo: {tipo: 'degradado', colores: ['#5B3A7A', '#C7746A', '#F2B27A']},
|
|
84
|
+
vineta: 0.3,
|
|
85
|
+
particulas: {tipo: 'polvo', cantidad: 18},
|
|
86
|
+
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'},
|
|
87
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
88
|
+
tituloMayusculas: false,
|
|
89
|
+
subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '#3D5AFE'},
|
|
90
|
+
transicion: 'corte',
|
|
91
|
+
marca: 'esquina',
|
|
92
|
+
zoomLento: 1.08,
|
|
93
|
+
},
|
|
94
|
+
// Historias de misterio: interiores oscuros, siluetas, letreros "DÍA 10"
|
|
95
|
+
historia: {
|
|
96
|
+
fondo: {tipo: 'degradado', colores: ['#1C2A2A', '#0E1414']},
|
|
97
|
+
vineta: 0.6,
|
|
98
|
+
foco: {color: 'rgba(255,214,140,0.22)', x: 540, y: 700, radio: 700},
|
|
99
|
+
particulas: {tipo: 'polvo', cantidad: 14},
|
|
100
|
+
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'},
|
|
101
|
+
fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
|
|
102
|
+
tituloMayusculas: true,
|
|
103
|
+
subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.4)', resalte: ''},
|
|
104
|
+
transicion: 'corte',
|
|
105
|
+
marca: 'esquina',
|
|
106
|
+
zoomLento: 1.05,
|
|
107
|
+
},
|
|
108
|
+
// Objetos sobre una mesa con foco de luz, contadores tipo LCD (dinero, crecimiento, comparaciones)
|
|
109
|
+
mesa: {
|
|
110
|
+
fondo: {tipo: 'degradado', colores: ['#2A1A10', '#120A06'], angulo: 180},
|
|
111
|
+
foco: {color: 'rgba(255,200,120,0.30)', x: 540, y: 820, radio: 760, fuerza: 1},
|
|
112
|
+
vineta: 0.7,
|
|
113
|
+
textura: 'grano',
|
|
114
|
+
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'},
|
|
115
|
+
fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
|
|
116
|
+
tituloMayusculas: true,
|
|
117
|
+
subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.4)', resalte: '#2C7BE5'},
|
|
118
|
+
transicion: 'fundido',
|
|
119
|
+
marca: 'esquina',
|
|
120
|
+
zoomLento: 1.04,
|
|
121
|
+
},
|
|
122
|
+
// El de los shorts clásicos (morado oscuro, Anton, amarillo): para capas dentro de 'datos-curiosos'
|
|
123
|
+
'datos-curiosos': {
|
|
124
|
+
fondo: {tipo: 'degradado', colores: ['#180E30', '#080614']},
|
|
125
|
+
vineta: 0,
|
|
126
|
+
colores: {texto: '#FFFFFF', acento: '#FFD400', acento2: '#FF4050', suave: '#E6E1FF', tarjeta: '#1E1E2A', tarjetaTexto: '#FFFFFF', borde: '#FFD400'},
|
|
127
|
+
fuentes: {titulo: ANTON, texto: MONT, mono: MONO},
|
|
128
|
+
tituloMayusculas: true,
|
|
129
|
+
subtitulos: {...SUB_BASE, tipo: 'pop', y: 1420, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: ''},
|
|
130
|
+
transicion: 'corte',
|
|
131
|
+
marca: 'arriba',
|
|
132
|
+
zoomLento: 1,
|
|
133
|
+
},
|
|
134
|
+
// Series animadas con personaje (capítulos, retos, "comenta"): cuarto morado, subtítulos en bloque al centro
|
|
135
|
+
serie: {
|
|
136
|
+
fondo: {tipo: 'degradado', colores: ['#5B3FA0', '#3A2470']},
|
|
137
|
+
vineta: 0.25,
|
|
138
|
+
colores: {texto: '#FFFFFF', acento: '#FF3B5C', acento2: '#FF3B5C', suave: 'rgba(255,255,255,0.7)', tarjeta: '#FFFFFF', tarjetaTexto: '#141414', borde: '#141414'},
|
|
139
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
140
|
+
tituloMayusculas: true,
|
|
141
|
+
subtitulos: {tipo: 'bloque', y: 1080, tam: 62, maxPalabras: 4, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: '#FF3B5C', contorno: 8},
|
|
142
|
+
transicion: 'corte',
|
|
143
|
+
marca: 'ninguna',
|
|
144
|
+
zoomLento: 1.02,
|
|
145
|
+
},
|
|
146
|
+
// Infografía plana (cuerpo, ciencia, comparaciones): gris claro, encabezado y subtítulos abajo en caja
|
|
147
|
+
infografia: {
|
|
148
|
+
fondo: {tipo: 'degradado', colores: ['#EEEDEA', '#E2E1DC']},
|
|
149
|
+
vineta: 0.08,
|
|
150
|
+
colores: {texto: '#1B1B1F', acento: '#E5484D', acento2: '#2D6BFF', suave: '#8A8A93', tarjeta: '#FFFFFF', tarjetaTexto: '#1B1B1F', borde: '#1B1B1F'},
|
|
151
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
152
|
+
tituloMayusculas: true,
|
|
153
|
+
subtitulos: {tipo: 'karaoke', y: 1500, tam: 48, maxPalabras: 4, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.45)', resalte: '', fondo: '#1B1B1F'},
|
|
154
|
+
transicion: 'corte',
|
|
155
|
+
marca: 'esquina',
|
|
156
|
+
zoomLento: 1.0,
|
|
157
|
+
},
|
|
158
|
+
// Escenas 3D (construcción, cómo funciona por dentro): una palabra grande a la vez
|
|
159
|
+
'3d': {
|
|
160
|
+
fondo: {tipo: 'degradado', colores: ['#BFE3FF', '#EAF6FF']},
|
|
161
|
+
vineta: 0.1,
|
|
162
|
+
colores: {texto: '#FFFFFF', acento: '#FFD43B', acento2: '#FF5A36', suave: 'rgba(255,255,255,0.7)', tarjeta: 'rgba(20,20,20,0.75)', tarjetaTexto: '#FFFFFF', borde: '#141414'},
|
|
163
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
164
|
+
tituloMayusculas: true,
|
|
165
|
+
subtitulos: {tipo: 'palabra', y: 1000, tam: 76, maxPalabras: 1, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: '', contorno: 10},
|
|
166
|
+
transicion: 'corte',
|
|
167
|
+
marca: 'esquina',
|
|
168
|
+
zoomLento: 1.0,
|
|
169
|
+
},
|
|
170
|
+
// Oscuro y limpio, para lo que sea
|
|
171
|
+
minimal: {
|
|
172
|
+
fondo: {tipo: 'degradado', colores: ['#15151C', '#08080C']},
|
|
173
|
+
vineta: 0.3,
|
|
174
|
+
colores: {texto: '#FFFFFF', acento: '#8B7BFF', acento2: '#FF6B8B', suave: 'rgba(255,255,255,0.5)', tarjeta: '#1E1E28', tarjetaTexto: '#FFFFFF', borde: '#33334A'},
|
|
175
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
176
|
+
tituloMayusculas: false,
|
|
177
|
+
subtitulos: {...SUB_BASE, color: '#FFFFFF', pendiente: 'rgba(255,255,255,0.35)', resalte: '#8B7BFF'},
|
|
178
|
+
transicion: 'fundido',
|
|
179
|
+
marca: 'esquina',
|
|
180
|
+
zoomLento: 1.03,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const esObjeto = (x: unknown): x is Record<string, any> => !!x && typeof x === 'object' && !Array.isArray(x);
|
|
185
|
+
/** Mezcla profunda: `b` gana sobre `a` (los arreglos se reemplazan completos). */
|
|
186
|
+
export const mezclar = <T,>(a: T, b: any): T => {
|
|
187
|
+
if (!esObjeto(b)) return a;
|
|
188
|
+
const out: any = {...a};
|
|
189
|
+
for (const [k, v] of Object.entries(b)) out[k] = esObjeto(v) && esObjeto(out[k]) && !('tipo' in v) ? mezclar(out[k], v) : v;
|
|
190
|
+
return out;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Tema final: estilo base (o uno propio del config, que puede heredar con `base`) + ajustes del video. */
|
|
194
|
+
export function temaDe(estilo: string | undefined, ajustes: Record<string, any> | undefined, propios: Record<string, any> = {}): Tema {
|
|
195
|
+
const resolver = (nombre: string, n = 0): Tema => {
|
|
196
|
+
if (ESTILOS[nombre]) return ESTILOS[nombre];
|
|
197
|
+
const p = propios[nombre];
|
|
198
|
+
if (p && n < 5) return mezclar(resolver(p.base ?? 'minimal', n + 1), p);
|
|
199
|
+
return ESTILOS.minimal;
|
|
200
|
+
};
|
|
201
|
+
return mezclar(resolver(estilo ?? 'minimal'), ajustes ?? {});
|
|
202
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Utilidades del motor de capas: tiempos por palabra, rutas de recursos y formato de números.
|
|
2
|
+
import {staticFile} from 'remotion';
|
|
3
|
+
import type {Tema} from './tema';
|
|
4
|
+
|
|
5
|
+
export type CtxEscena = {
|
|
6
|
+
t: number; // segundos desde que empezó la escena
|
|
7
|
+
dur: number;
|
|
8
|
+
habla: [number, number];
|
|
9
|
+
palabras: [string, number, number][];
|
|
10
|
+
tema: Tema;
|
|
11
|
+
semilla: string;
|
|
12
|
+
inicios?: number[]; // inicio de cada escena (para el HUD: cambios en {escena: n})
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const norm = (s: string) =>
|
|
16
|
+
s
|
|
17
|
+
.normalize('NFD')
|
|
18
|
+
.replace(/[̀-ͯ]/g, '')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]/g, '');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Momento (s, relativo a la escena) de `en`: un número de segundos, una palabra de lo que dice la voz
|
|
24
|
+
* (se busca la primera que empiece así; "orca" encuentra "orcas"), "inicio", "voz" (cuando empieza a hablar)
|
|
25
|
+
* o "fin" (cuando termina de hablar). Con "palabra+0.3" se suma un ajuste.
|
|
26
|
+
*/
|
|
27
|
+
export function cuando(en: unknown, c: CtxEscena, porDefecto = 0): number {
|
|
28
|
+
if (en == null) return porDefecto;
|
|
29
|
+
if (typeof en === 'number') return en;
|
|
30
|
+
if (typeof en !== 'string') return porDefecto;
|
|
31
|
+
const m = en.match(/^(.*?)([+-]\d+(?:\.\d+)?)?$/);
|
|
32
|
+
const base = (m?.[1] ?? en).trim();
|
|
33
|
+
const ajuste = m?.[2] ? Number(m[2]) : 0;
|
|
34
|
+
if (base === 'inicio' || base === '') return ajuste;
|
|
35
|
+
if (base === 'voz') return c.habla[0] + ajuste;
|
|
36
|
+
if (base === 'fin') return c.habla[1] + ajuste;
|
|
37
|
+
const n = norm(base);
|
|
38
|
+
const p = c.palabras.find(([w]) => norm(w).startsWith(n));
|
|
39
|
+
if (p) return p[1] + ajuste;
|
|
40
|
+
return (c.habla[0] + c.habla[1]) / 2 + ajuste; // no la encontró: a media frase
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Ruta de un recurso: URL tal cual, o archivo de la carpeta assets/ del proyecto. */
|
|
44
|
+
export const recurso = (src: string) => (/^(https?:|data:)/.test(src) ? src : staticFile(`assets/${src.replace(/^\.?\/?(assets\/)?/, '')}`));
|
|
45
|
+
|
|
46
|
+
/** 1234567.5 -> "1.234.567,5" (separadores en español; configurables). */
|
|
47
|
+
export function formatoNumero(v: number, decimales = 0, miles = '.', decimal = ',') {
|
|
48
|
+
const fijo = Math.abs(v).toFixed(decimales);
|
|
49
|
+
const [ent, dec] = fijo.split('.');
|
|
50
|
+
const conMiles = ent.replace(/\B(?=(\d{3})+(?!\d))/g, miles);
|
|
51
|
+
return `${v < 0 ? '-' : ''}${conMiles}${dec ? decimal + dec : ''}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** "Hola *mundo*" -> [["Hola ", false], ["mundo", true]] */
|
|
55
|
+
export const partesResaltadas = (s: string): [string, boolean][] =>
|
|
56
|
+
s
|
|
57
|
+
.split(/(\*[^*]+\*)/)
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((p) => (p.startsWith('*') && p.endsWith('*') ? [p.slice(1, -1), true] : [p, false]));
|
|
60
|
+
|
|
61
|
+
/** Número pseudoaleatorio estable en [0, 1) a partir de una semilla. */
|
|
62
|
+
export const azar = (semilla: string) => {
|
|
63
|
+
let h = 2166136261;
|
|
64
|
+
for (let i = 0; i < semilla.length; i++) h = Math.imul(h ^ semilla.charCodeAt(i), 16777619);
|
|
65
|
+
return ((h >>> 0) % 100000) / 100000;
|
|
66
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Capas de interfaz para series, infografías e historias interactivas:
|
|
2
|
+
// barra (progreso con %), medidor (aguja), votacion (resultados), estado (barra del teléfono: hora, batería, día),
|
|
3
|
+
// noticia (cintillo de noticiero), chat (comentarios en vivo), onda (audio), globo (diálogo), seguir (botón de seguir).
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import {Img} from 'remotion';
|
|
6
|
+
import {backOut, clamp, easeOut, lerp} from '../estilo';
|
|
7
|
+
import {Avatar, useMarca} from '../piezas';
|
|
8
|
+
import {azar, cuando, formatoNumero, recurso, type CtxEscena} from './util';
|
|
9
|
+
|
|
10
|
+
type C = Record<string, any>;
|
|
11
|
+
const oscuro = (ctx: CtxEscena) => ctx.tema.colores.texto === '#FFFFFF';
|
|
12
|
+
|
|
13
|
+
/** Valor animado desde el inicio de la capa (o desde el valor anterior si viene de un cambio del HUD). */
|
|
14
|
+
const animado = (c: C, ctx: CtxEscena, campo = 'valor', dur = 0.8) => {
|
|
15
|
+
const t0 = cuando(c.en, ctx, 0) + (c.__cambio ?? 0);
|
|
16
|
+
const k = easeOut(clamp((ctx.t - t0) / (c.duracion ?? dur)));
|
|
17
|
+
return lerp(c[`__${campo}Previo`] ?? c.desde ?? 0, c[campo] ?? 0, k);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const Barra: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
21
|
+
const T = ctx.tema;
|
|
22
|
+
const ancho = c.ancho ?? 560;
|
|
23
|
+
const v = clamp(animado(c, ctx) / (c.max ?? 100));
|
|
24
|
+
const color = c.color ?? T.colores.acento;
|
|
25
|
+
return (
|
|
26
|
+
<div style={{width: ancho, background: c.fondo ?? T.colores.tarjeta, borderRadius: 16, padding: '14px 20px', boxShadow: '0 10px 26px rgba(0,0,0,0.25)', border: `3px solid ${c.borde ?? 'rgba(0,0,0,0.08)'}`, fontFamily: T.fuentes.texto}}>
|
|
27
|
+
{c.titulo || c.mostrar !== false ? (
|
|
28
|
+
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10, color: T.colores.tarjetaTexto}}>
|
|
29
|
+
<span style={{fontSize: c.tamTitulo ?? 28, fontWeight: 800, letterSpacing: 1, textTransform: 'uppercase'}}>{c.titulo ?? ''}</span>
|
|
30
|
+
{c.mostrar !== false ? <span style={{fontSize: 30, fontWeight: 800, color}}>{formatoNumero(v * (c.max ?? 100), c.decimales ?? 0)}{c.unidad ?? '%'}</span> : null}
|
|
31
|
+
</div>
|
|
32
|
+
) : null}
|
|
33
|
+
<div style={{height: c.alto ?? 24, background: c.pista ?? 'rgba(127,127,127,0.25)', borderRadius: 99, overflow: 'hidden'}}>
|
|
34
|
+
<div style={{width: `${v * 100}%`, height: '100%', background: color, borderRadius: 99, boxShadow: `0 0 14px ${color}`}} />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Medidor: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
41
|
+
const T = ctx.tema;
|
|
42
|
+
const tam = c.tam ?? 420;
|
|
43
|
+
const min = c.min ?? 0;
|
|
44
|
+
const max = c.max ?? 100;
|
|
45
|
+
const v = animado(c, ctx, 'valor', 1.1);
|
|
46
|
+
const k = clamp((v - min) / (max - min));
|
|
47
|
+
const ang = -225 + k * 270; // de -225° a 45°
|
|
48
|
+
const r = tam / 2 - 26;
|
|
49
|
+
const punto = (a: number, rr = r) => [tam / 2 + Math.cos((a * Math.PI) / 180) * rr, tam / 2 + Math.sin((a * Math.PI) / 180) * rr];
|
|
50
|
+
const arco = (a0: number, a1: number, color: string) => {
|
|
51
|
+
const [x0, y0] = punto(a0);
|
|
52
|
+
const [x1, y1] = punto(a1);
|
|
53
|
+
return <path d={`M ${x0} ${y0} A ${r} ${r} 0 ${a1 - a0 > 180 ? 1 : 0} 1 ${x1} ${y1}`} stroke={color} strokeWidth={22} fill="none" strokeLinecap="round" />;
|
|
54
|
+
};
|
|
55
|
+
const zonas: [number, string][] = c.zonas ?? [[0.6, '#2BB673'], [0.85, '#F5B841'], [1, '#E5484D']];
|
|
56
|
+
let previo = 0;
|
|
57
|
+
const [nx, ny] = punto(ang, r - 40);
|
|
58
|
+
return (
|
|
59
|
+
<div style={{position: 'relative', width: tam, height: tam, background: c.fondo ?? '#FFFFFF', borderRadius: '50%', border: `6px solid ${c.borde ?? '#1B1B1F'}`, boxShadow: '0 16px 30px rgba(0,0,0,0.25)'}}>
|
|
60
|
+
<svg width={tam} height={tam} style={{position: 'absolute', left: -6, top: -6}}>
|
|
61
|
+
{zonas.map(([hasta, color], i) => {
|
|
62
|
+
const el = arco(-225 + previo * 270, -225 + hasta * 270, color);
|
|
63
|
+
previo = hasta;
|
|
64
|
+
return <React.Fragment key={i}>{el}</React.Fragment>;
|
|
65
|
+
})}
|
|
66
|
+
<line x1={tam / 2} y1={tam / 2} x2={nx} y2={ny} stroke="#1B1B1F" strokeWidth={10} strokeLinecap="round" />
|
|
67
|
+
<circle cx={tam / 2} cy={tam / 2} r={18} fill="#1B1B1F" />
|
|
68
|
+
</svg>
|
|
69
|
+
<div style={{position: 'absolute', left: 0, right: 0, bottom: tam * 0.16, textAlign: 'center', fontFamily: T.fuentes.texto, fontWeight: 900, fontSize: tam * 0.13, color: '#1B1B1F'}}>
|
|
70
|
+
{formatoNumero(v, c.decimales ?? 0)}
|
|
71
|
+
<div style={{fontSize: tam * 0.055, fontWeight: 700, color: '#666'}}>{c.unidad ?? ''}</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Votacion: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
78
|
+
const T = ctx.tema;
|
|
79
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
80
|
+
const k = easeOut(clamp((ctx.t - t0 - 0.2) / 1));
|
|
81
|
+
const opciones: C[] = c.opciones ?? [];
|
|
82
|
+
return (
|
|
83
|
+
<div style={{width: c.ancho ?? 820, fontFamily: T.fuentes.texto, color: T.colores.texto}}>
|
|
84
|
+
{c.cabecera ? <div style={{textAlign: 'center', fontFamily: T.fuentes.mono, fontSize: 26, letterSpacing: 4, color: T.colores.suave, marginBottom: 18}}>{c.cabecera}</div> : null}
|
|
85
|
+
{c.titulo ? <div style={{textAlign: 'center', fontSize: 64, fontWeight: 900, lineHeight: 1.1, marginBottom: 40, textTransform: 'uppercase'}}>{c.titulo}</div> : null}
|
|
86
|
+
{opciones.map((o, i) => (
|
|
87
|
+
<div key={i} style={{marginBottom: 34}}>
|
|
88
|
+
<div style={{display: 'flex', justifyContent: 'space-between', fontSize: 50, fontWeight: 900}}>
|
|
89
|
+
<span>{o.texto}</span>
|
|
90
|
+
<span style={{color: o.color ?? (i === 0 ? T.colores.acento2 : T.colores.suave)}}>{Math.round((o.valor ?? 0) * k)}%</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div style={{height: 16, background: 'rgba(127,127,127,0.25)', borderRadius: 99, marginTop: 10}}>
|
|
93
|
+
<div style={{width: `${(o.valor ?? 0) * k}%`, height: '100%', background: o.color ?? (i === 0 ? T.colores.texto : T.colores.suave), borderRadius: 99}} />
|
|
94
|
+
</div>
|
|
95
|
+
{o.sub ? <div style={{fontSize: 26, color: T.colores.suave, marginTop: 8}}>{o.sub}</div> : null}
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Barra de estado del teléfono a lo ancho: día, hora, batería y aviso. */
|
|
103
|
+
export const Estado: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
104
|
+
const T = ctx.tema;
|
|
105
|
+
const color = c.color ?? (oscuro(ctx) ? '#FFFFFF' : '#141414');
|
|
106
|
+
const bat = c.bateria ?? 80;
|
|
107
|
+
return (
|
|
108
|
+
<div style={{width: c.ancho ?? 1000, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', fontFamily: T.fuentes.mono, color}}>
|
|
109
|
+
<div>
|
|
110
|
+
{c.dia ? <div style={{fontSize: 24, opacity: 0.7, letterSpacing: 2}}>{c.dia}</div> : null}
|
|
111
|
+
<div style={{fontSize: c.tam ?? 64, fontWeight: 800, lineHeight: 1}}>{c.hora ?? '12:00'}</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div style={{textAlign: 'right', fontSize: 22, letterSpacing: 2, opacity: 0.85}}>
|
|
114
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'flex-end'}}>
|
|
115
|
+
BATERÍA
|
|
116
|
+
<div style={{width: 46, height: 22, border: `3px solid ${color}`, borderRadius: 5, padding: 2, boxSizing: 'border-box'}}>
|
|
117
|
+
<div style={{width: `${bat}%`, height: '100%', background: bat < 20 ? '#E5484D' : color, borderRadius: 2}} />
|
|
118
|
+
</div>
|
|
119
|
+
{bat}%
|
|
120
|
+
</div>
|
|
121
|
+
{c.aviso ? (
|
|
122
|
+
<div style={{marginTop: 8}}>
|
|
123
|
+
{c.aviso} {c.contador != null ? <span style={{background: '#E5484D', color: '#fff', borderRadius: 6, padding: '0 8px', marginLeft: 6}}>{c.contador}</span> : null}
|
|
124
|
+
</div>
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Rótulo de noticiero: etiqueta roja, titular y cintillo que corre. */
|
|
132
|
+
export const Noticia: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
133
|
+
const T = ctx.tema;
|
|
134
|
+
const ancho = c.ancho ?? 900;
|
|
135
|
+
const corre = (ctx.t * (c.velocidad ?? 140)) % (ancho * 2);
|
|
136
|
+
return (
|
|
137
|
+
<div style={{width: ancho, fontFamily: T.fuentes.texto, boxShadow: '0 12px 30px rgba(0,0,0,0.35)'}}>
|
|
138
|
+
{c.vivo ? <div style={{display: 'inline-block', background: '#E5484D', color: '#fff', fontWeight: 900, fontSize: 24, padding: '4px 12px', marginBottom: 6}}>EN VIVO</div> : null}
|
|
139
|
+
<div style={{background: '#E5484D', color: '#fff', fontWeight: 900, fontSize: 30, padding: '8px 18px', letterSpacing: 1}}>{c.etiqueta ?? 'ÚLTIMA HORA'}</div>
|
|
140
|
+
<div style={{background: '#FFFFFF', color: '#111', fontWeight: 900, fontSize: 40, padding: '14px 18px', lineHeight: 1.15}}>{c.titular}</div>
|
|
141
|
+
{c.cintillo ? (
|
|
142
|
+
<div style={{background: '#B5121B', color: '#fff', fontWeight: 700, fontSize: 26, padding: '8px 0', overflow: 'hidden', whiteSpace: 'nowrap'}}>
|
|
143
|
+
<span style={{display: 'inline-block', transform: `translateX(${ancho - corre}px)`}}>{c.cintillo}</span>
|
|
144
|
+
</div>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Chat en vivo: los mensajes van apareciendo de abajo hacia arriba. */
|
|
151
|
+
export const Chat: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
152
|
+
const T = ctx.tema;
|
|
153
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
154
|
+
const cada = c.cada ?? 0.45;
|
|
155
|
+
const msgs: C[] = c.mensajes ?? [];
|
|
156
|
+
const n = Math.max(0, Math.min(msgs.length, Math.floor((ctx.t - t0) / cada) + 1));
|
|
157
|
+
const visibles = msgs.slice(Math.max(0, n - (c.maximo ?? 6)), n);
|
|
158
|
+
const colores = ['#FF7AB6', '#7AD7FF', '#B6FF7A', '#FFD27A', '#C9A2FF'];
|
|
159
|
+
return (
|
|
160
|
+
<div style={{width: c.ancho ?? 420, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 8, fontFamily: T.fuentes.texto, fontSize: c.tam ?? 28}}>
|
|
161
|
+
{visibles.map((m, i) => (
|
|
162
|
+
<div key={n - visibles.length + i} style={{background: 'rgba(0,0,0,0.45)', color: '#fff', borderRadius: 12, padding: '6px 14px', opacity: i === visibles.length - 1 ? clamp((ctx.t - t0 - (n - 1) * cada) / 0.2) : 1}}>
|
|
163
|
+
<b style={{color: colores[(m.usuario ?? '').length % colores.length]}}>{m.usuario}</b> {m.texto}
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** Onda de audio: barras que se mueven mientras habla (o siempre, con `siempre`). */
|
|
171
|
+
export const Onda: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
172
|
+
const n = c.barras ?? 28;
|
|
173
|
+
const alto = c.alto ?? 90;
|
|
174
|
+
const hablando = c.siempre || (ctx.t >= ctx.habla[0] && ctx.t <= ctx.habla[1]);
|
|
175
|
+
return (
|
|
176
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 5, height: alto, width: c.ancho ?? 380}}>
|
|
177
|
+
{Array.from({length: n}, (_, i) => {
|
|
178
|
+
const base = 0.15 + 0.85 * Math.abs(Math.sin(ctx.t * (6 + azar(`o${i}`) * 9) + i));
|
|
179
|
+
const h = hablando ? base : 0.12;
|
|
180
|
+
return <div key={i} style={{flex: 1, height: `${h * 100}%`, background: c.color ?? '#B9C3FF', borderRadius: 99}} />;
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Globo de diálogo o etiqueta con pico. */
|
|
187
|
+
export const Globo: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
188
|
+
const T = ctx.tema;
|
|
189
|
+
const fondo = c.fondo ?? '#FFFFFF';
|
|
190
|
+
const cola = c.cola ?? 'abajo';
|
|
191
|
+
const pico: React.CSSProperties =
|
|
192
|
+
cola === 'izq'
|
|
193
|
+
? {left: -22, top: '50%', marginTop: -14, borderWidth: '14px 24px 14px 0', borderColor: `transparent ${fondo} transparent transparent`}
|
|
194
|
+
: cola === 'der'
|
|
195
|
+
? {right: -22, top: '50%', marginTop: -14, borderWidth: '14px 0 14px 24px', borderColor: `transparent transparent transparent ${fondo}`}
|
|
196
|
+
: cola === 'arriba'
|
|
197
|
+
? {top: -22, left: '30%', borderWidth: '0 16px 24px 16px', borderColor: `transparent transparent ${fondo} transparent`}
|
|
198
|
+
: {bottom: -22, left: '30%', borderWidth: '24px 16px 0 16px', borderColor: `${fondo} transparent transparent transparent`};
|
|
199
|
+
return (
|
|
200
|
+
<div style={{position: 'relative', background: fondo, color: c.color ?? '#141414', borderRadius: 18, padding: '14px 26px', fontFamily: T.fuentes.texto, fontWeight: 800, fontSize: c.tam ?? 40, maxWidth: c.maxAncho ?? 600, boxShadow: '0 10px 24px rgba(0,0,0,0.25)', border: c.borde ? `4px solid ${c.borde}` : undefined}}>
|
|
201
|
+
{c.texto}
|
|
202
|
+
{cola !== 'ninguna' ? <div style={{position: 'absolute', width: 0, height: 0, borderStyle: 'solid', ...pico}} /> : null}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Tarjeta de "seguir": avatar, usuario y botón que cambia a "Siguiendo" (con dedo que lo pulsa). */
|
|
208
|
+
export const Seguir: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
209
|
+
const marca = useMarca();
|
|
210
|
+
const T = ctx.tema;
|
|
211
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
212
|
+
const tc = t0 + (c.pulsar ?? 1);
|
|
213
|
+
const pulsado = ctx.t >= tc;
|
|
214
|
+
const dedo = clamp((ctx.t - t0 - 0.3) / (tc - t0 - 0.3));
|
|
215
|
+
return (
|
|
216
|
+
<div style={{position: 'relative', display: 'flex', alignItems: 'center', gap: 18, background: '#FFFFFF', borderRadius: 24, padding: '16px 22px', boxShadow: '0 14px 30px rgba(0,0,0,0.3)', fontFamily: T.fuentes.texto}}>
|
|
217
|
+
<div style={{position: 'relative', width: 84, height: 84}}>
|
|
218
|
+
<Avatar x={0} y={0} size={76} aro={4} />
|
|
219
|
+
</div>
|
|
220
|
+
<div style={{color: '#111'}}>
|
|
221
|
+
<div style={{fontSize: 34, fontWeight: 800}}>{c.usuario ?? `@${marca.nombre.replace(/\s+/g, '').toLowerCase()}`}</div>
|
|
222
|
+
{c.texto ? <div style={{fontSize: 22, color: '#666'}}>{c.texto}</div> : null}
|
|
223
|
+
</div>
|
|
224
|
+
<div style={{marginLeft: 16, background: pulsado ? '#EDEDED' : T.colores.acento2, color: pulsado ? '#111' : '#fff', fontWeight: 800, fontSize: 28, borderRadius: 12, padding: '12px 22px', transform: `scale(${pulsado ? backOut(clamp((ctx.t - tc) / 0.3)) : 1})`}}>
|
|
225
|
+
{pulsado ? `${marca.siguiendo.charAt(0)}${marca.siguiendo.slice(1).toLowerCase()} ✓` : marca.seguir}
|
|
226
|
+
</div>
|
|
227
|
+
{dedo < 1 || ctx.t < tc + 0.5 ? <div style={{position: 'absolute', right: 10 - (1 - dedo) * 120, bottom: -60 - (1 - dedo) * 120, fontSize: 80, opacity: ctx.t > tc + 0.5 ? 0 : 1}}>👆</div> : null}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Imagen que se transforma en otra (rayos X, antes/después): fundido, barrido o círculo que se abre. */
|
|
233
|
+
export const ImagenCambia: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
234
|
+
const ca = c.cambiarA;
|
|
235
|
+
const tc = cuando(ca.en, ctx, ctx.habla[1] * 0.5);
|
|
236
|
+
const k = easeOut(clamp((ctx.t - tc) / (ca.duracion ?? 0.6)));
|
|
237
|
+
const ancho = c.ancho ?? 500;
|
|
238
|
+
const efecto = ca.efecto ?? 'fundido';
|
|
239
|
+
const clip = efecto === 'barrido' ? `inset(0 ${(1 - k) * 100}% 0 0)` : efecto === 'circulo' ? `circle(${k * 75}% at 50% 50%)` : undefined;
|
|
240
|
+
return (
|
|
241
|
+
<div style={{position: 'relative', width: ancho}}>
|
|
242
|
+
<Img src={recurso(c.src)} style={{width: ancho, display: 'block'}} />
|
|
243
|
+
<Img src={recurso(ca.src)} style={{position: 'absolute', left: 0, top: 0, width: ancho, display: 'block', opacity: efecto === 'fundido' ? k : k > 0 ? 1 : 0, clipPath: clip}} />
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export {Img};
|