morfeo 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +210 -2
  3. package/assets/fuentes/Anton.ttf +0 -0
  4. package/assets/fuentes/JetBrainsMono.ttf +0 -0
  5. package/assets/fuentes/Montserrat.ttf +0 -0
  6. package/bin/morfeo.mjs +184 -0
  7. package/bin/postinstall.mjs +10 -0
  8. package/index.d.ts +250 -0
  9. package/lib/config.mjs +284 -0
  10. package/lib/entorno.mjs +93 -0
  11. package/lib/guion.mjs +313 -0
  12. package/lib/imagenes.mjs +99 -0
  13. package/lib/index.mjs +15 -0
  14. package/lib/instalar.mjs +193 -0
  15. package/lib/navegador.mjs +64 -0
  16. package/lib/personajes.mjs +94 -0
  17. package/lib/render.mjs +312 -0
  18. package/lib/voces.mjs +122 -0
  19. package/lib/whisper.mjs +45 -0
  20. package/package.json +84 -5
  21. package/plantilla/assets/.gitkeep +0 -0
  22. package/plantilla/env.example +7 -0
  23. package/plantilla/escenas/Barras.tsx +15 -0
  24. package/plantilla/gitignore +4 -0
  25. package/plantilla/guiones/cachalote.mjs +24 -0
  26. package/plantilla/guiones/tutorial.mjs +16 -0
  27. package/plantilla/morfeo.config.mjs +43 -0
  28. package/plantilla/package.json +13 -0
  29. package/plantilla/videos.mjs +25 -0
  30. package/python/musica.py +221 -0
  31. package/python/preparar.py +268 -0
  32. package/python/requirements.txt +5 -0
  33. package/remotion/Root.tsx +35 -0
  34. package/remotion/Short.tsx +278 -0
  35. package/remotion/escenas.tsx +430 -0
  36. package/remotion/estilo.ts +73 -0
  37. package/remotion/iconos.ts +326 -0
  38. package/remotion/index.ts +4 -0
  39. package/remotion/motor/Video.tsx +121 -0
  40. package/remotion/motor/capas.tsx +622 -0
  41. package/remotion/motor/fondo.tsx +148 -0
  42. package/remotion/motor/modelo3d.tsx +252 -0
  43. package/remotion/motor/plantillas.tsx +125 -0
  44. package/remotion/motor/registro.ts +11 -0
  45. package/remotion/motor/subtitulos.tsx +137 -0
  46. package/remotion/motor/tema.ts +207 -0
  47. package/remotion/motor/util.ts +66 -0
  48. package/remotion/motor/widgets.tsx +248 -0
  49. package/remotion/piezas.tsx +181 -0
  50. package/remotion/tipos.ts +43 -0
  51. package/remotion/tres/Capa3D.tsx +153 -0
@@ -0,0 +1,430 @@
1
+ // Un componente por cada tipo de escena de videos.py. Todos son funciones puras del tiempo `t`
2
+ // (segundos desde que empieza la escena), así se pueden congelar o repetir sin problema.
3
+ import React from 'react';
4
+ import {random} from 'remotion';
5
+ import {
6
+ AMARILLO,
7
+ ANTON,
8
+ BLANCO,
9
+ FPS,
10
+ MONO,
11
+ MORADO,
12
+ NEGRO,
13
+ ROJO,
14
+ VERDE,
15
+ W,
16
+ ajustar,
17
+ ancho,
18
+ backOut,
19
+ easeIO,
20
+ easeOut,
21
+ clamp,
22
+ lerp,
23
+ mezclaColor,
24
+ } from './estilo';
25
+ import {Avatar, Pastilla, Resplandor, Texto, tamIcono, useMarca} from './piezas';
26
+ import type {Escena, Fila} from './tipos';
27
+
28
+ export type PropsEscena = {e: Escena; t: number; si: number; resplandor: string};
29
+
30
+ const temblor = (t: number, semilla: string, amt = 10): [number, number] => {
31
+ const f = Math.floor(t * FPS);
32
+ if (f % 9 >= 2) return [0, 0];
33
+ return [(random(`${semilla}x${f}`) * 2 - 1) * amt, (random(`${semilla}y${f}`) * 2 - 1) * amt * 0.6];
34
+ };
35
+
36
+ const Lienzo: React.FC<{children: React.ReactNode}> = ({children}) => (
37
+ <svg style={{position: 'absolute', left: 0, top: 0, overflow: 'visible'}} width={W} height={1920}>
38
+ {children}
39
+ </svg>
40
+ );
41
+
42
+ // ---------- title: el gancho ----------
43
+ const Title: React.FC<PropsEscena> = ({e, t, si}) => {
44
+ const p = e.p;
45
+ const marca = useMarca();
46
+ const rows: Fila[] = p.rows;
47
+ let y = 400;
48
+ const filas = rows.map(([s, col, size0], i) => {
49
+ const size = ajustar(s, size0);
50
+ const sc = backOut((t - i * 0.15) / 0.45);
51
+ const ultima = i === rows.length - 1;
52
+ const [jx, jy] = ultima ? temblor(t, `t${si}`) : [0, 0];
53
+ y += i ? size * 0.55 : 0;
54
+ const el = <Texto key={i} x={W / 2 + jx} y={y + jy} s={s} size={size} color={col} stroke={ultima ? 12 : 9} escala={sc} rot={ultima ? -3 : 0} />;
55
+ y += size * 0.62;
56
+ return el;
57
+ });
58
+ const ic = p.icon ?? 'question';
59
+ const s4 = backOut((t - 0.6) / 0.5);
60
+ return (
61
+ <>
62
+ <Pastilla cx={W / 2} cy={230} s={p.tag ?? marca.etiqueta} fondo={ROJO} />
63
+ {filas}
64
+ </>
65
+ );
66
+ };
67
+
68
+ // ---------- year: año que cuenta hacia arriba ----------
69
+ const Year: React.FC<PropsEscena> = ({e, t}) => {
70
+ const p = e.p;
71
+ const pr = easeOut(t / 0.9);
72
+ const yr = p.year - 9 + Math.round(9 * pr);
73
+ const s = backOut((t - 1.1) / 0.45);
74
+ const s2 = backOut((t - 1.5) / 0.45);
75
+ return (
76
+ <>
77
+ <Pastilla cx={W / 2} cy={230} s={p.pre ?? 'EN'} fondo={MORADO} />
78
+ <Texto x={W / 2} y={470} s={String(yr)} size={280} color={AMARILLO} stroke={12} escala={1 + 0.08 * (1 - pr)} />
79
+ {p.sub && <Texto x={W / 2} y={820} s={p.sub} size={ajustar(p.sub, 150)} color={BLANCO} stroke={10} escala={s} rot={-2} />}
80
+ </>
81
+ );
82
+ };
83
+
84
+ // ---------- text: renglones grandes (con tachado opcional) ----------
85
+ const Text: React.FC<PropsEscena> = ({e, t}) => {
86
+ const rows: Fila[] = e.p.rows;
87
+ const sizes = rows.map(([s, , z]) => ajustar(s, z));
88
+ const total = sizes.reduce((a, s) => a + s * 1.05, 0);
89
+ let y = 760 - total / 2;
90
+ const finVoz = e.habla[1];
91
+ const textos: React.ReactNode[] = [];
92
+ const rayas: React.ReactNode[] = [];
93
+ rows.forEach(([s, col], i) => {
94
+ const size = sizes[i];
95
+ y += size * 0.525;
96
+ textos.push(<Texto key={i} x={W / 2} y={y} s={s} size={size} color={col} stroke={11} escala={backOut((t - i * 0.18) / 0.42)} rot={i % 2 ? -2 : 2} />);
97
+ if (e.p.strike) {
98
+ const k = easeOut((t - (finVoz * 0.55 + i * 0.12)) / 0.25);
99
+ if (k > 0) {
100
+ const ww = ancho(s, ANTON, size) + 60;
101
+ const x0 = W / 2 - ww / 2;
102
+ rayas.push(<line key={i} x1={x0} y1={y + size * 0.15} x2={x0 + ww * k} y2={y + size * 0.15 - size * 0.3 * k} stroke={ROJO} strokeWidth={22} strokeLinecap="round" />);
103
+ }
104
+ }
105
+ y += size * 0.525;
106
+ });
107
+ return (
108
+ <>
109
+ {textos}
110
+ <Lienzo>{rayas}</Lienzo>
111
+ </>
112
+ );
113
+ };
114
+
115
+ // ---------- icon: ícono grande con brillo + etiqueta ----------
116
+ const Icon: React.FC<PropsEscena> = ({e, t}) => {
117
+ const p = e.p;
118
+ const color = p.color ?? AMARILLO;
119
+ return (
120
+ <>
121
+ <Texto x={W / 2} y={1180} s={p.label} size={ajustar(p.label, 140)} color={color} stroke={11} escala={backOut((t - 0.35) / 0.42)} rot={-2} />
122
+ </>
123
+ );
124
+ };
125
+
126
+ // ---------- compare: A -> B con destello ----------
127
+ const Compare: React.FC<PropsEscena> = ({e, t}) => {
128
+ const p = e.p;
129
+ const at = e.ev.at !== undefined ? e.ev.at - 0.2 : 1.2;
130
+ const [[a, ca], [b, cb]] = [p.a, p.b];
131
+ const [sa, sb] = p.sizes ?? [170, 200];
132
+ const fa = ajustar(a, sa);
133
+ const pa = easeOut((t - 0.45) / 0.5);
134
+ const y0 = 470 + fa * 0.55;
135
+ const y1 = lerp(y0, 860, pa);
136
+ const s2 = backOut((t - at) / 0.45);
137
+ const cy = 1100;
138
+ return (
139
+ <>
140
+ <Texto x={W / 2} y={470} s={a} size={fa} color={ca} stroke={12} escala={backOut(t / 0.4)} rot={3} />
141
+ <Lienzo>
142
+ {pa > 0 && <rect x={W / 2 - 16} y={y0} width={32} height={y1 - y0} fill={BLANCO} />}
143
+ {pa > 0.95 && <polygon points={`${W / 2 - 50},850 ${W / 2 + 50},850 ${W / 2},920`} fill={BLANCO} />}
144
+ {s2 > 0.01 &&
145
+ Array.from({length: 12}, (_, i) => {
146
+ const ang = (i * Math.PI) / 6 + t * 0.6;
147
+ const r0 = 230;
148
+ const r1 = 230 + 120 * s2;
149
+ return (
150
+ <line key={i} x1={W / 2 + Math.cos(ang) * r0} y1={cy + Math.sin(ang) * r0} x2={W / 2 + Math.cos(ang) * r1} y2={cy + Math.sin(ang) * r1} stroke={cb} strokeWidth={14} strokeLinecap="round" />
151
+ );
152
+ })}
153
+ </Lienzo>
154
+ {s2 > 0.01 && <Texto x={W / 2} y={cy} s={b} size={ajustar(b, sb)} color={cb} stroke={12} escala={s2} rot={-3} />}
155
+ </>
156
+ );
157
+ };
158
+
159
+ // ---------- card: ventana de código cuyo valor cambia con glitch ----------
160
+ const Card: React.FC<PropsEscena> = ({e, t, si}) => {
161
+ const p = e.p;
162
+ const tswap = e.ev.at ?? e.habla[1] * 0.5;
163
+ const [x0, y0, x1, y1] = [90, 330, 990, 1110];
164
+ const swapped = t >= tswap;
165
+ const k = t - tswap;
166
+ const fl = p.flash ?? '¡CAMBIÓ!';
167
+ const jx = swapped && k < 0.5 ? (random(`c${si}-${Math.floor(t * FPS)}`) * 2 - 1) * 8 : 0;
168
+ return (
169
+ <>
170
+ <div style={{position: 'absolute', left: x0 + 12, top: y0 + 16, width: x1 - x0, height: y1 - y0, borderRadius: 34, background: NEGRO}} />
171
+ <div
172
+ style={{
173
+ position: 'absolute',
174
+ left: x0,
175
+ top: y0,
176
+ width: x1 - x0,
177
+ height: y1 - y0,
178
+ borderRadius: 34,
179
+ background: '#161624',
180
+ border: '4px solid #46466E',
181
+ boxSizing: 'border-box',
182
+ }}
183
+ />
184
+ {['#FF5F56', '#FFBD2E', '#27C93F'].map((c, i) => (
185
+ <div key={c} style={{position: 'absolute', left: x0 + 40 + i * 50, top: y0 + 34, width: 32, height: 32, borderRadius: 16, background: c}} />
186
+ ))}
187
+ <Texto x={x0 + 230} y={y0 + 50} s={p.file ?? 'main.c'} size={30} fuente="mono" peso={500} color="#9696BE" ancla="izquierda" sombra={false} />
188
+ {(p.rows as [string, string | null, string | null][]).map(([lab, antes, despues], i) => {
189
+ const y = y0 + 190 + i * 170;
190
+ if (antes === null) return <Texto key={i} x={x0 + 60} y={y} s={lab} size={44} fuente="mono" peso={500} color="#6E78A0" ancla="izquierda" sombra={false} />;
191
+ const v = (swapped ? despues : antes) ?? '';
192
+ const vx = x0 + 60 + ancho(lab, MONO, 60, 700) + 18;
193
+ let fv = 60;
194
+ while (vx + ancho(v, MONO, fv, 700) > x1 - 40 && fv > 20) fv -= 4;
195
+ const wv = ancho(v, MONO, fv, 700);
196
+ return (
197
+ <React.Fragment key={i}>
198
+ <Texto x={x0 + 60} y={y} s={lab} size={60} fuente="mono" color="#78C8FF" ancla="izquierda" sombra={false} />
199
+ {swapped && k < 1.6 && <div style={{position: 'absolute', left: vx - 14, top: y - 46, width: wv + 28, height: 92, borderRadius: 12, background: '#781428'}} />}
200
+ <Texto x={vx} y={y} s={v} size={fv} fuente="mono" color={swapped ? ROJO : '#FFC878'} ancla="izquierda" sombra={false} />
201
+ </React.Fragment>
202
+ );
203
+ })}
204
+ {swapped && <Texto x={W / 2 + jx} y={1240} s={fl} size={ajustar(fl, 130)} color={ROJO} stroke={10} escala={backOut(k / 0.35)} rot={4} />}
205
+ </>
206
+ );
207
+ };
208
+
209
+ // ---------- dialog: ventana "¿...?" donde el cursor da clic en NO ----------
210
+ const Cursor: React.FC<{cx: number; cy: number}> = ({cx, cy}) => {
211
+ const pts: [number, number][] = [
212
+ [0, 0],
213
+ [0, 70],
214
+ [18, 54],
215
+ [32, 86],
216
+ [46, 80],
217
+ [32, 48],
218
+ [56, 48],
219
+ ];
220
+ const str = (dx: number) => pts.map(([x, y]) => `${cx + x + dx},${cy + y + dx}`).join(' ');
221
+ return (
222
+ <Lienzo>
223
+ <polygon points={str(5)} fill={NEGRO} />
224
+ <polygon points={str(0)} fill={BLANCO} stroke={NEGRO} strokeWidth={3} strokeLinejoin="round" />
225
+ </Lienzo>
226
+ );
227
+ };
228
+
229
+ const Dialog: React.FC<PropsEscena> = ({e, t}) => {
230
+ const p = e.p;
231
+ const tclick = (e.ev.click ?? e.habla[1] * 0.6) - 0.15;
232
+ const s = backOut(t / 0.4);
233
+ const [cx, cy] = [W / 2, 700];
234
+ const [bx0, bx1, by0, by1] = [cx - (cx - 150) * s, cx + (930 - cx) * s, cy - (cy - 420) * s, cy + (980 - cy) * s];
235
+ const pressed = t >= tclick;
236
+ const kc = t - tclick;
237
+ const off = pressed && kc < 0.15 ? 6 : 0;
238
+ const cp = easeIO((t - 0.4) / Math.max(0.1, tclick - 0.5));
239
+ const [q1, q2] = p.q;
240
+ const r = 30 + kc * 260;
241
+ return (
242
+ <>
243
+ <div style={{position: 'absolute', left: bx0 + 12, top: by0 + 16, width: bx1 - bx0, height: by1 - by0, borderRadius: 30, background: NEGRO}} />
244
+ <div style={{position: 'absolute', left: bx0, top: by0, width: bx1 - bx0, height: by1 - by0, borderRadius: 30, background: '#F0F0F8', overflow: 'hidden'}}>
245
+ {s >= 0.98 && <div style={{height: 90, background: '#3C3C5A'}} />}
246
+ </div>
247
+ {s >= 0.98 && (
248
+ <>
249
+ <Texto x={W / 2} y={by0 + 46} s={p.title ?? 'ADVERTENCIA'} size={38} fuente="mont" peso={900} color={BLANCO} sombra={false} />
250
+ <Texto x={W / 2} y={640} s={q1} size={62} fuente="mont" color="#1E1E32" sombra={false} />
251
+ <Texto x={W / 2} y={720} s={q2} size={62} fuente="mont" color="#1E1E32" sombra={false} />
252
+ <div style={{position: 'absolute', left: 210, top: 820, width: 290, height: 110, borderRadius: 24, background: '#C8C8D2'}} />
253
+ <Texto x={355} y={875} s="SÍ" size={64} color="#5A5A64" sombra={false} />
254
+ <div style={{position: 'absolute', left: 580, top: 820 + off, width: 290, height: 110, borderRadius: 24, background: pressed ? VERDE : '#28AA50'}} />
255
+ <Texto x={725} y={875 + off} s="NO" size={64} color={BLANCO} sombra={false} />
256
+ {pressed && kc < 0.5 && (
257
+ <Lienzo>
258
+ <circle cx={740} cy={880} r={r} fill="none" stroke={VERDE} strokeWidth={8} opacity={1 - kc * 2} />
259
+ </Lienzo>
260
+ )}
261
+ <Cursor cx={lerp(300, 740, cp)} cy={lerp(1150, 890, cp)} />
262
+ {pressed && <Texto x={W / 2} y={1150} s={p.result ?? '¡SE QUEDA!'} size={130} color={VERDE} stroke={10} escala={backOut(kc / 0.4)} rot={-4} />}
263
+ </>
264
+ )}
265
+ </>
266
+ );
267
+ };
268
+
269
+ // ---------- aliens: fila de invasores pixel ----------
270
+ const Aliens: React.FC<PropsEscena> = ({e, t}) => {
271
+ const {n, label} = e.p;
272
+ const muchos = n > 11;
273
+ return (
274
+ <>
275
+ <div style={{position: 'absolute', left: W / 2 - 60, top: 1180, width: 120, height: 40, background: VERDE}} />
276
+ <div style={{position: 'absolute', left: W / 2 - 14, top: 1150, width: 28, height: 35, background: VERDE}} />
277
+ <Texto x={W / 2} y={330} s={label} size={ajustar(label, 150)} color={muchos ? AMARILLO : ROJO} stroke={11} escala={backOut(t / 0.4)} rot={-2} />
278
+ </>
279
+ );
280
+ };
281
+
282
+ // ---------- memory: cartucho + barra de memoria ----------
283
+ const Memory: React.FC<PropsEscena> = ({e, t}) => {
284
+ const s = backOut(t / 0.4);
285
+ const [cx, cy] = [W / 2, 640];
286
+ const [w, h] = [520 * s, 560 * s];
287
+ const k = easeIO((t - 0.8) / 1.8);
288
+ const [x0, x1, y] = [140, 940, 1060];
289
+ return (
290
+ <>
291
+ {s > 0.02 && (
292
+ <>
293
+ <div style={{position: 'absolute', left: cx - w / 2 + 14, top: cy - h / 2 + 16, width: w, height: h, background: NEGRO}} />
294
+ <div style={{position: 'absolute', left: cx - w / 2, top: cy - h / 2, width: w, height: h, background: '#787887'}} />
295
+ <div style={{position: 'absolute', left: cx - w / 2 + 50 * s, top: cy - h / 2 + 60 * s, width: w - 100 * s, height: h / 2 + 80 * s - 60 * s, background: '#32323C'}} />
296
+ {Array.from({length: 8}, (_, i) => (
297
+ <div key={i} style={{position: 'absolute', left: cx - w / 2 + (40 + i * 60) * s, top: cy + h / 2 - 60 * s, width: 30 * s, height: 60 * s, background: '#C8AA50'}} />
298
+ ))}
299
+ {s > 0.9 && <Texto x={cx} y={cy - 40} s={e.p.label ?? '40 KB'} size={140} color={AMARILLO} stroke={8} />}
300
+ </>
301
+ )}
302
+ <div style={{position: 'absolute', left: x0, top: y, width: x1 - x0, height: 70, borderRadius: 20, background: '#28283C'}} />
303
+ {k > 0 && <div style={{position: 'absolute', left: x0 + 8, top: y + 8, width: (x1 - x0 - 16) * k * 0.97, height: 54, borderRadius: 14, background: mezclaColor(VERDE, ROJO, k)}} />}
304
+ <Texto x={W / 2} y={y + 130} s={`MEMORIA ${Math.floor(97 * k)}%`} size={44} fuente="mont" peso={900} color={BLANCO} sombra={false} />
305
+ </>
306
+ );
307
+ };
308
+
309
+ // ---------- morph: la nube se vuelve arbusto ----------
310
+ const Morph: React.FC<PropsEscena> = ({e, t}) => {
311
+ const at = (e.ev.at ?? e.habla[1] * 0.5) - 0.1;
312
+ const k = easeIO((t - at) / 0.9);
313
+ const gy = 1180;
314
+ return (
315
+ <>
316
+ <div style={{position: 'absolute', left: 0, top: gy + 60, width: W, height: 340, background: '#784628'}} />
317
+ <div style={{position: 'absolute', left: 0, top: gy + 40, width: W, height: 30, background: '#46AA46'}} />
318
+ {k > 0.98 ? (
319
+ <Texto x={W / 2} y={450} s="¿ARBUSTO?" size={150} color={VERDE} stroke={11} escala={backOut((t - at - 0.9) / 0.4)} rot={-3} />
320
+ ) : (
321
+ <Texto x={W / 2} y={330} s="MISMO DIBUJO" size={120} color={BLANCO} stroke={10} escala={backOut(t / 0.4)} />
322
+ )}
323
+ </>
324
+ );
325
+ };
326
+
327
+ // ---------- cta: cierre con avatar y botón de seguir ----------
328
+ const Cta: React.FC<PropsEscena> = ({t, resplandor}) => {
329
+ const marca = useMarca();
330
+ const s = backOut(t / 0.45);
331
+ const pulso = 1 + 0.03 * Math.sin(t * 6);
332
+ const av = Math.round(380 * Math.max(0.05, s) * pulso);
333
+ const s2 = backOut((t - 0.6) / 0.4);
334
+ const [bw, bh] = [460 * s2, 130 * s2];
335
+ const cy = 1060 + (t > 1.6 && t < 1.8 ? 8 : 0);
336
+ const siguiendo = t > 1.7;
337
+ const s3 = easeOut((t - 0.9) / 0.4);
338
+ return (
339
+ <>
340
+ {s > 0.02 && (
341
+ <>
342
+ <Resplandor cx={W / 2} cy={560} rx={270} ry={270} color={resplandor} fuerza={0.55 * Math.min(1, s)} />
343
+ <Avatar x={W / 2 - av / 2 - 10} y={560 - av / 2 - 10} size={av} aro={10} />
344
+ </>
345
+ )}
346
+ {marca.nombre && <Texto x={W / 2} y={880} s={marca.nombre} size={Math.min(170, Math.floor((170 * (W - 120)) / ancho(marca.nombre, ANTON, 170)))} color={BLANCO} stroke={12} escala={backOut((t - 0.25) / 0.4)} />}
347
+ {s2 > 0.01 && (
348
+ <>
349
+ <div style={{position: 'absolute', left: W / 2 - bw / 2 + 10, top: cy - bh / 2 + 14, width: bw, height: bh, borderRadius: bh / 2, background: NEGRO}} />
350
+ <div style={{position: 'absolute', left: W / 2 - bw / 2, top: cy - bh / 2, width: bw, height: bh, borderRadius: bh / 2, background: siguiendo ? '#3C3C46' : ROJO}} />
351
+ {s2 > 0.9 && <Texto x={W / 2} y={cy} s={siguiendo ? marca.siguiendo : marca.seguir} size={72} color={BLANCO} sombra={false} />}
352
+ </>
353
+ )}
354
+ {s3 > 0 && marca.redes && (
355
+ <Texto x={W / 2} y={1210 + 20 * (1 - s3)} s={marca.redes} size={40} fuente="mont" color="#E6E1FF" sombra={false} opacidad={s3} />
356
+ )}
357
+ </>
358
+ );
359
+ };
360
+
361
+ export const ESCENAS: Record<string, React.FC<PropsEscena>> = {
362
+ title: Title,
363
+ year: Year,
364
+ text: Text,
365
+ icon: Icon,
366
+ compare: Compare,
367
+ card: Card,
368
+ dialog: Dialog,
369
+ aliens: Aliens,
370
+ memory: Memory,
371
+ morph: Morph,
372
+ cta: Cta,
373
+ };
374
+
375
+ // ---------- lo que va en 3D (Three.js) ----------
376
+ // Cada escena dice qué íconos voxel lleva y dónde (en píxeles de la pantalla), más los halos de luz detrás.
377
+ export type Icono3D = {nombre: string; cx: number; cy: number; u: number; color?: string; giro?: number; fase?: number};
378
+ export type Halo = {cx: number; cy: number; r: number; color: string; fuerza: number};
379
+
380
+ export const capa3D = (e: Escena, t: number): {iconos: Icono3D[]; halos: Halo[]} => {
381
+ const p = e.p;
382
+ const iconos: Icono3D[] = [];
383
+ const halos: Halo[] = [];
384
+ switch (e.tipo) {
385
+ case 'title': {
386
+ const ic = p.icon ?? 'question';
387
+ const s4 = backOut((t - 0.6) / 0.5);
388
+ if (s4 > 0.01) iconos.push({nombre: ic, cx: W / 2, cy: 1080 + Math.sin(t * 4) * 14, u: tamIcono(ic, 190) * s4, giro: 1 - clamp((t - 0.6) / 0.6)});
389
+ break;
390
+ }
391
+ case 'year': {
392
+ const s2 = backOut((t - 1.5) / 0.45);
393
+ if (p.icon && s2 > 0.01) iconos.push({nombre: p.icon, cx: W / 2, cy: 1100, u: tamIcono(p.icon, 180) * s2, giro: 1 - clamp((t - 1.5) / 0.6)});
394
+ break;
395
+ }
396
+ case 'icon': {
397
+ const s = backOut(t / 0.45);
398
+ const color = p.color ?? AMARILLO;
399
+ halos.push({cx: W / 2, cy: 750, r: 330, color, fuerza: 0.43 * Math.min(1, s)});
400
+ if (s > 0.01) iconos.push({nombre: p.icon, cx: W / 2, cy: 760 + Math.sin(t * 4) * 18, u: tamIcono(p.icon, 430) * s, giro: 1 - clamp(t / 0.7)});
401
+ break;
402
+ }
403
+ case 'aliens': {
404
+ const {n, speed} = p;
405
+ const muchos = n > 11;
406
+ const cols = muchos ? 11 : n;
407
+ const u = muchos ? 9 : 22;
408
+ const cw = 7 * u + (muchos ? u * 3 : u * 5);
409
+ const span = cols * cw;
410
+ const xOff = Math.sin(t * speed) * ((980 - span) / 2);
411
+ const yBase = 520 + (muchos ? t * 30 : t * 60);
412
+ const alt = Math.floor(t * speed * 1.5) % 2;
413
+ for (let k = 0; k < n; k++) {
414
+ const r = Math.floor(k / cols);
415
+ const c = k % cols;
416
+ iconos.push({nombre: 'alien', cx: W / 2 - span / 2 + cw * (c + 0.5) + xOff, cy: yBase + r * (8 * u + 24) + (alt ? u * 0.4 : 0), u, fase: k * 0.7});
417
+ }
418
+ break;
419
+ }
420
+ case 'morph': {
421
+ const at = (e.ev.at ?? e.habla[1] * 0.5) - 0.1;
422
+ const k = easeIO((t - at) / 0.9);
423
+ iconos.push({nombre: 'cloud', cx: W / 2, cy: lerp(620, 1150, k), u: 60, color: mezclaColor('#FAFAFF', '#46C850', easeIO((t - at + 0.2) / 0.5))});
424
+ break;
425
+ }
426
+ default:
427
+ break;
428
+ }
429
+ return {iconos, halos};
430
+ };
@@ -0,0 +1,73 @@
1
+ // La fórmula visual de los shorts: medidas, paleta, fuentes y curvas de animación.
2
+ import {loadFont} from '@remotion/fonts';
3
+ import {measureText} from '@remotion/layout-utils';
4
+ import {staticFile} from 'remotion';
5
+
6
+ export const W = 1080;
7
+ export const H = 1920;
8
+ export const FPS = 30;
9
+
10
+ // Paleta
11
+ export const AMARILLO = '#FFD400'; // lo importante
12
+ export const ROJO = '#FF4050'; // errores y problemas
13
+ export const VERDE = '#5ADC6E'; // soluciones
14
+ export const BLANCO = '#FFFFFF';
15
+ export const CIAN = '#50DCFF';
16
+ export const MORADO = '#5A3CAA';
17
+ export const NEGRO = '#000000';
18
+
19
+ // Zonas seguras de Shorts / TikTok / Reels (lo que tapa la interfaz de cada app)
20
+ export const SEGURO = {arriba: 200, abajo: 370, derecha: 160, izquierda: 60};
21
+ export const SUB_Y = 1420; // altura de los subtítulos
22
+ export const SUB_ANCHO = 880; // ancho máximo: no se mete bajo los botones de la derecha
23
+
24
+ // Fondo: degradado oscuro + resplandor; cambia según el número del video
25
+ // [arriba, abajo, resplandor]
26
+ export const FONDOS: [string, string, string][] = [
27
+ ['#180E30', '#080614', '#462882'],
28
+ ['#0A1830', '#040814', '#1E508C'],
29
+ ['#280C1E', '#0C040E', '#82285A'],
30
+ ['#0A221E', '#040C0C', '#1E6E5A'],
31
+ ['#1E160A', '#0A0804', '#825A1E'],
32
+ ];
33
+ export const fondoDe = (indice: number) => FONDOS[indice % FONDOS.length];
34
+
35
+ // Fuentes (licencia libre OFL)
36
+ export const ANTON = 'Anton';
37
+ export const MONT = 'Montserrat';
38
+ export const MONO = 'JetBrains Mono';
39
+ export const fuentesListas = Promise.all([
40
+ loadFont({family: ANTON, url: staticFile('fuentes/Anton.ttf')}),
41
+ loadFont({family: MONT, url: staticFile('fuentes/Montserrat.ttf'), weight: '100 900'}),
42
+ loadFont({family: MONO, url: staticFile('fuentes/JetBrainsMono.ttf'), weight: '100 800'}),
43
+ ]);
44
+
45
+ // Curvas
46
+ export const clamp = (x: number, a = 0, b = 1) => Math.max(a, Math.min(b, x));
47
+ export const easeOut = (x: number) => 1 - (1 - clamp(x)) ** 3;
48
+ export const easeIO = (x: number) => {
49
+ const c = clamp(x);
50
+ return 3 * c * c - 2 * c * c * c;
51
+ };
52
+ /** Rebote "back-out": crece, se pasa un poco y regresa. */
53
+ export const backOut = (x: number, s = 1.9) => {
54
+ const c = clamp(x) - 1;
55
+ return c * c * ((s + 1) * c + s) + 1;
56
+ };
57
+ export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
58
+ export const mezclaColor = (c1: string, c2: string, t: number) => {
59
+ const p = (c: string) => [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16));
60
+ const a = p(c1);
61
+ const b = p(c2);
62
+ return `rgb(${a.map((v, i) => Math.round(lerp(v, b[i], clamp(t)))).join(',')})`;
63
+ };
64
+
65
+ // Medidas de texto
66
+ export const ancho = (text: string, fontFamily: string, fontSize: number, fontWeight: number | string = 400) =>
67
+ measureText({text, fontFamily, fontSize, fontWeight: String(fontWeight), validateFontIsLoaded: true}).width;
68
+
69
+ /** Achica el tamaño hasta que el texto quepa en `maxw` (igual que el motor anterior). */
70
+ export const ajustar = (s: string, size: number, maxw = 980) => {
71
+ while (ancho(s, ANTON, size) > maxw && size > 40) size -= 6;
72
+ return size;
73
+ };