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,149 @@
|
|
|
1
|
+
// Fondos del motor de capas: color, degradado, imagen o video, con rayos de luz, foco, viñeta,
|
|
2
|
+
// textura y partículas (burbujas, polvo, estrellas, nieve, confeti).
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {AbsoluteFill, Img, OffthreadVideo} from 'remotion';
|
|
5
|
+
import {H, W} from '../estilo';
|
|
6
|
+
import type {Fondo as TFondo, Particulas as TParticulas, Tema} from './tema';
|
|
7
|
+
import {azar, recurso} from './util';
|
|
8
|
+
|
|
9
|
+
const Base: React.FC<{f: TFondo}> = ({f}) => {
|
|
10
|
+
switch (f.tipo) {
|
|
11
|
+
case 'color':
|
|
12
|
+
return <AbsoluteFill style={{background: f.color}} />;
|
|
13
|
+
case 'degradado':
|
|
14
|
+
return <AbsoluteFill style={{background: `linear-gradient(${f.angulo ?? 180}deg, ${f.colores.join(', ')})`}} />;
|
|
15
|
+
case 'imagen':
|
|
16
|
+
return (
|
|
17
|
+
<AbsoluteFill>
|
|
18
|
+
<Img src={recurso(f.src)} style={{width: '100%', height: '100%', objectFit: 'cover', filter: f.desenfoque ? `blur(${f.desenfoque}px)` : undefined}} />
|
|
19
|
+
{f.oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${f.oscurecer})`}} /> : null}
|
|
20
|
+
</AbsoluteFill>
|
|
21
|
+
);
|
|
22
|
+
case 'video':
|
|
23
|
+
return (
|
|
24
|
+
<AbsoluteFill>
|
|
25
|
+
<OffthreadVideo src={recurso(f.src)} muted style={{width: '100%', height: '100%', objectFit: 'cover'}} />
|
|
26
|
+
{f.oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${f.oscurecer})`}} /> : null}
|
|
27
|
+
</AbsoluteFill>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const Rayos: React.FC<{color: string; fuerza: number; t: number}> = ({color, fuerza, t}) => (
|
|
33
|
+
<AbsoluteFill style={{overflow: 'hidden', mixBlendMode: 'screen'}}>
|
|
34
|
+
{Array.from({length: 7}, (_, i) => {
|
|
35
|
+
const x = 80 + i * 150 + Math.sin(t * 0.4 + i) * 40;
|
|
36
|
+
const ancho = 70 + (i % 3) * 50;
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
key={i}
|
|
40
|
+
style={{
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
left: x,
|
|
43
|
+
top: -200,
|
|
44
|
+
width: ancho,
|
|
45
|
+
height: H * 0.95,
|
|
46
|
+
background: `linear-gradient(${color}, transparent)`,
|
|
47
|
+
transform: `rotate(${12 + Math.sin(t * 0.3 + i * 2) * 3}deg)`,
|
|
48
|
+
transformOrigin: 'top center',
|
|
49
|
+
opacity: fuerza * (0.5 + 0.5 * Math.sin(t * 0.7 + i * 1.7)),
|
|
50
|
+
filter: 'blur(14px)',
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</AbsoluteFill>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export const Particulas: React.FC<{p: TParticulas; t: number; semilla: string}> = ({p, t, semilla}) => {
|
|
59
|
+
if (!p || p.tipo === 'ninguna') return null;
|
|
60
|
+
const n = p.cantidad ?? 24;
|
|
61
|
+
return (
|
|
62
|
+
<AbsoluteFill style={{pointerEvents: 'none'}}>
|
|
63
|
+
{Array.from({length: n}, (_, i) => {
|
|
64
|
+
const r1 = azar(`${semilla}${p.tipo}a${i}`);
|
|
65
|
+
const r2 = azar(`${semilla}${p.tipo}b${i}`);
|
|
66
|
+
const r3 = azar(`${semilla}${p.tipo}c${i}`);
|
|
67
|
+
let x = r1 * W;
|
|
68
|
+
let y = r2 * H;
|
|
69
|
+
let tam = 4 + r3 * 10;
|
|
70
|
+
let estilo: React.CSSProperties = {};
|
|
71
|
+
switch (p.tipo) {
|
|
72
|
+
case 'burbujas': {
|
|
73
|
+
const vel = 60 + r3 * 90;
|
|
74
|
+
y = (((r2 * H - t * vel) % H) + H) % H;
|
|
75
|
+
x += Math.sin(t * 1.5 + i) * 14;
|
|
76
|
+
tam = 6 + r3 * 18;
|
|
77
|
+
estilo = {border: `2px solid ${p.color ?? 'rgba(200,235,255,0.55)'}`, borderRadius: '50%', background: 'rgba(255,255,255,0.06)'};
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'polvo':
|
|
81
|
+
x += Math.sin(t * 0.3 + i) * 30;
|
|
82
|
+
y += Math.cos(t * 0.25 + i) * 30;
|
|
83
|
+
tam = 3 + r3 * 5;
|
|
84
|
+
estilo = {borderRadius: '50%', background: p.color ?? 'rgba(255,240,210,0.5)', filter: 'blur(1px)', opacity: 0.4 + 0.6 * Math.abs(Math.sin(t + i))};
|
|
85
|
+
break;
|
|
86
|
+
case 'estrellas':
|
|
87
|
+
tam = 2 + r3 * 4;
|
|
88
|
+
estilo = {borderRadius: '50%', background: p.color ?? '#fff', opacity: 0.3 + 0.7 * Math.abs(Math.sin(t * 2 + i * 3))};
|
|
89
|
+
break;
|
|
90
|
+
case 'nieve': {
|
|
91
|
+
const vel = 40 + r3 * 70;
|
|
92
|
+
y = (r2 * H + t * vel) % H;
|
|
93
|
+
x += Math.sin(t + i) * 24;
|
|
94
|
+
estilo = {borderRadius: '50%', background: p.color ?? '#fff', opacity: 0.8};
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'confeti': {
|
|
98
|
+
const vel = 200 + r3 * 240;
|
|
99
|
+
y = ((r2 * H + t * vel) % (H + 100)) - 50;
|
|
100
|
+
tam = 14 + r3 * 10;
|
|
101
|
+
const colores = ['#FF4D6D', '#FFD60A', '#4CC9F0', '#80ED99', '#B517FF'];
|
|
102
|
+
estilo = {background: p.color ?? colores[i % colores.length], borderRadius: 3, transform: `rotate(${t * 300 * (r1 - 0.5)}deg)`, height: tam * 0.5};
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return <div key={i} style={{position: 'absolute', left: x, top: y, width: tam, height: estilo.height ?? tam, ...estilo}} />;
|
|
107
|
+
})}
|
|
108
|
+
</AbsoluteFill>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** 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.35 : 0.12, mixBlendMode: 'multiply', pointerEvents: 'none'}}>
|
|
115
|
+
<svg width={W} height={H}>
|
|
116
|
+
<filter id={`tx-${tipo}`}>
|
|
117
|
+
<feTurbulence type="fractalNoise" baseFrequency={tipo === 'papel' ? 0.9 : 1.6} numOctaves={tipo === 'papel' ? 3 : 2} seed={3} />
|
|
118
|
+
<feColorMatrix type="saturate" values="0" />
|
|
119
|
+
</filter>
|
|
120
|
+
<rect width={W} height={H} filter={`url(#tx-${tipo})`} />
|
|
121
|
+
</svg>
|
|
122
|
+
</AbsoluteFill>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/** 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}) => {
|
|
127
|
+
const f = fondo ?? tema.fondo;
|
|
128
|
+
const foco = tema.foco;
|
|
129
|
+
return (
|
|
130
|
+
<AbsoluteFill>
|
|
131
|
+
<Base f={f} />
|
|
132
|
+
{tema.rayos ? <Rayos color={tema.rayos.color} fuerza={tema.rayos.fuerza ?? 1} t={t} /> : null}
|
|
133
|
+
{foco ? (
|
|
134
|
+
<AbsoluteFill
|
|
135
|
+
style={{
|
|
136
|
+
background: `radial-gradient(circle at ${foco.x ?? 540}px ${foco.y ?? 800}px, ${foco.color} 0%, transparent ${foco.radio ?? 700}px)`,
|
|
137
|
+
opacity: foco.fuerza ?? 1,
|
|
138
|
+
mixBlendMode: 'screen',
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
) : null}
|
|
142
|
+
{tema.textura && tema.textura !== 'ninguna' ? <Textura tipo={tema.textura} /> : null}
|
|
143
|
+
{tema.particulas ? <Particulas p={tema.particulas} t={t} semilla={semilla} /> : null}
|
|
144
|
+
</AbsoluteFill>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const Vineta: React.FC<{fuerza: number}> = ({fuerza}) =>
|
|
149
|
+
fuerza > 0 ? <AbsoluteFill style={{background: `radial-gradient(ellipse at center, transparent 45%, rgba(0,0,0,${fuerza}) 100%)`, pointerEvents: 'none'}} /> : null;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Capa {tipo: 'modelo'}: un objeto 3D (Three.js) dentro de su propio lienzo transparente.
|
|
2
|
+
// src: 'moneda.glb' tu modelo (carpeta assets/)
|
|
3
|
+
// forma: 'moneda' | 'billete' | 'cubo' | 'esfera' | 'lingote' objetos incluidos (sin archivos)
|
|
4
|
+
// color, metal (0..1), rugosidad, ancho/alto del lienzo, tamano (escala del objeto)
|
|
5
|
+
// rotacion: [x, y, z] grados · girar3d: [x, y, z] grados por segundo
|
|
6
|
+
// cantidad: 30 + crecer: true -> una pila que va creciendo (monedas, billetes, lingotes)
|
|
7
|
+
// Todo se anima con el tiempo de la escena (nunca con useFrame), como pide Remotion.
|
|
8
|
+
import {useThree} from '@react-three/fiber';
|
|
9
|
+
import {ThreeCanvas} from '@remotion/three';
|
|
10
|
+
import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react';
|
|
11
|
+
import {continueRender, delayRender} from 'remotion';
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
14
|
+
import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|
15
|
+
import {clamp, easeOut} from '../estilo';
|
|
16
|
+
import {cuando, recurso, type CtxEscena} from './util';
|
|
17
|
+
|
|
18
|
+
const RAD = Math.PI / 180;
|
|
19
|
+
type Gltf = {scene: THREE.Group; animations: THREE.AnimationClip[]};
|
|
20
|
+
const cache = new Map<string, Promise<Gltf>>();
|
|
21
|
+
const cargarGltf = (url: string) => {
|
|
22
|
+
if (!cache.has(url)) cache.set(url, new GLTFLoader().loadAsync(url).then((g) => ({scene: g.scene, animations: g.animations})));
|
|
23
|
+
return cache.get(url)!;
|
|
24
|
+
};
|
|
25
|
+
const cargar = (url: string) => cargarGltf(url).then((g) => g.scene);
|
|
26
|
+
|
|
27
|
+
const Glb: React.FC<{src: string}> = ({src}) => {
|
|
28
|
+
const url = recurso(src);
|
|
29
|
+
const [escena, setEscena] = useState<THREE.Group | null>(null);
|
|
30
|
+
const [handle] = useState(() => delayRender(`Cargando modelo ${src}`));
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
cargar(url)
|
|
33
|
+
.then((s) => {
|
|
34
|
+
// centrado y escalado a ~2 unidades para que cualquier modelo quepa en el lienzo
|
|
35
|
+
const copia = s.clone(true);
|
|
36
|
+
const caja = new THREE.Box3().setFromObject(copia);
|
|
37
|
+
const tam = caja.getSize(new THREE.Vector3());
|
|
38
|
+
const centro = caja.getCenter(new THREE.Vector3());
|
|
39
|
+
const k = 2 / Math.max(tam.x, tam.y, tam.z, 0.0001);
|
|
40
|
+
copia.position.sub(centro.multiplyScalar(k));
|
|
41
|
+
copia.scale.setScalar(k);
|
|
42
|
+
setEscena(copia);
|
|
43
|
+
continueRender(handle);
|
|
44
|
+
})
|
|
45
|
+
.catch((e) => {
|
|
46
|
+
continueRender(handle);
|
|
47
|
+
throw new Error(`No pude cargar el modelo 3D ${src}: ${e.message}`);
|
|
48
|
+
});
|
|
49
|
+
}, [url, handle, src]);
|
|
50
|
+
return escena ? <primitive object={escena} /> : null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Reflejos de un cuarto iluminado: sin esto, los metales (monedas, lingotes) se ven negros. */
|
|
54
|
+
const Entorno: React.FC<{fuerza: number}> = ({fuerza}) => {
|
|
55
|
+
const {gl, scene} = useThree();
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const pmrem = new THREE.PMREMGenerator(gl);
|
|
58
|
+
const tex = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
59
|
+
scene.environment = tex;
|
|
60
|
+
scene.environmentIntensity = fuerza;
|
|
61
|
+
return () => {
|
|
62
|
+
tex.dispose();
|
|
63
|
+
pmrem.dispose();
|
|
64
|
+
};
|
|
65
|
+
}, [gl, scene, fuerza]);
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Geometría y material de las formas incluidas. */
|
|
70
|
+
const Forma: React.FC<{forma: string; color?: string; metal?: number; rugosidad?: number; acostada?: boolean}> = ({forma, color, metal, rugosidad, acostada}) => {
|
|
71
|
+
const mat = (c: string, m: number, r: number) => <meshStandardMaterial color={color ?? c} metalness={metal ?? m} roughness={rugosidad ?? r} />;
|
|
72
|
+
switch (forma) {
|
|
73
|
+
case 'moneda':
|
|
74
|
+
return (
|
|
75
|
+
<mesh rotation={[acostada ? 0 : Math.PI / 2, 0, 0]}>
|
|
76
|
+
<cylinderGeometry args={[1, 1, 0.14, 64]} />
|
|
77
|
+
{mat('#E2AE4A', 0.75, 0.3)}
|
|
78
|
+
</mesh>
|
|
79
|
+
);
|
|
80
|
+
case 'billete':
|
|
81
|
+
return (
|
|
82
|
+
<mesh>
|
|
83
|
+
<boxGeometry args={n2(acostada)} />
|
|
84
|
+
{mat('#6FAF7A', 0.05, 0.75)}
|
|
85
|
+
</mesh>
|
|
86
|
+
);
|
|
87
|
+
case 'lingote':
|
|
88
|
+
return (
|
|
89
|
+
<mesh>
|
|
90
|
+
<boxGeometry args={[1.8, 0.55, 0.8]} />
|
|
91
|
+
{mat('#E8B84A', 0.95, 0.22)}
|
|
92
|
+
</mesh>
|
|
93
|
+
);
|
|
94
|
+
case 'esfera':
|
|
95
|
+
return (
|
|
96
|
+
<mesh>
|
|
97
|
+
<sphereGeometry args={[1, 48, 48]} />
|
|
98
|
+
{mat('#7FA8FF', 0.2, 0.4)}
|
|
99
|
+
</mesh>
|
|
100
|
+
);
|
|
101
|
+
default:
|
|
102
|
+
return (
|
|
103
|
+
<mesh>
|
|
104
|
+
<boxGeometry args={[1.4, 1.4, 1.4]} />
|
|
105
|
+
{mat('#8B7BFF', 0.1, 0.45)}
|
|
106
|
+
</mesh>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const n2 = (acostada?: boolean): [number, number, number] => (acostada ? [2.3, 0.02, 1.05] : [2.3, 1.05, 0.02]);
|
|
112
|
+
|
|
113
|
+
/** Separación vertical de cada forma al apilarla. */
|
|
114
|
+
const ALTO_PILA: Record<string, number> = {moneda: 0.16, billete: 0.03, lingote: 0.58, cubo: 1.45, esfera: 1.9};
|
|
115
|
+
|
|
116
|
+
export const Modelo3D: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
117
|
+
const ancho = c.ancho ?? 600;
|
|
118
|
+
const alto = c.alto ?? ancho;
|
|
119
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
120
|
+
const t = Math.max(0, ctx.t - t0);
|
|
121
|
+
const [rx, ry, rz] = (c.rotacion ?? [0, 0, 0]).map((g: number) => g * RAD);
|
|
122
|
+
const [gx, gy, gz] = (c.girar3d ?? [0, 25, 0]).map((g: number) => g * RAD);
|
|
123
|
+
const n = Math.max(1, c.cantidad ?? 1);
|
|
124
|
+
// pila que crece: de 1 a n en `duracion` (por defecto, mientras habla)
|
|
125
|
+
const visibles = c.crecer ? Math.max(1, Math.round(1 + (n - 1) * easeOut(clamp(t / (c.duracion ?? Math.max(0.5, ctx.habla[1] - t0)))))) : n;
|
|
126
|
+
const paso = ALTO_PILA[c.forma ?? 'cubo'] ?? 1;
|
|
127
|
+
const pila = useMemo(() => Array.from({length: n}, (_, i) => [Math.sin(i * 12.9) * 0.05, i * paso, Math.cos(i * 7.3) * 0.05, Math.sin(i * 3.1) * 0.3] as const), [n, paso]);
|
|
128
|
+
const altoPila = (visibles - 1) * paso;
|
|
129
|
+
return (
|
|
130
|
+
<div style={{width: ancho, height: alto}}>
|
|
131
|
+
<ThreeCanvas width={ancho} height={alto} gl={{alpha: true, antialias: true}} camera={{fov: 35, position: [0, c.camaraY ?? 1.2, c.distancia ?? 6]}}>
|
|
132
|
+
<Entorno fuerza={c.reflejos ?? 1} />
|
|
133
|
+
<ambientLight intensity={c.luzAmbiente ?? 0.55} />
|
|
134
|
+
<directionalLight position={[3, 5, 4]} intensity={c.luz ?? 1.4} />
|
|
135
|
+
<directionalLight position={[0, 1, 6]} intensity={(c.luz ?? 1.4) * 0.7} />
|
|
136
|
+
<directionalLight position={[-4, 2, -3]} intensity={0.4} color="#9fc4ff" />
|
|
137
|
+
<group scale={c.tamano ?? 1} rotation={[rx + gx * t, ry + gy * t, rz + gz * t]} position={[0, n > 1 ? -altoPila / 2 : 0, 0]}>
|
|
138
|
+
{c.src ? (
|
|
139
|
+
<Glb src={c.src} />
|
|
140
|
+
) : (
|
|
141
|
+
pila.slice(0, visibles).map(([x, y, z, giro], i) => (
|
|
142
|
+
<group key={i} position={[x, y, z]} rotation={[0, n > 1 ? giro : 0, 0]}>
|
|
143
|
+
<Forma forma={c.forma ?? 'cubo'} color={c.color} metal={c.metal} rugosidad={c.rugosidad} acostada={n > 1} />
|
|
144
|
+
</group>
|
|
145
|
+
))
|
|
146
|
+
)}
|
|
147
|
+
</group>
|
|
148
|
+
</ThreeCanvas>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------------------------
|
|
154
|
+
// Escena 3D completa: tu .glb (por ejemplo, exportado de Blender) ocupando todo el cuadro.
|
|
155
|
+
// {tipo: 'escena3d', src: 'casa.glb',
|
|
156
|
+
// camara: [{en: 0, pos: [8, 4, 10], mirar: [0, 0, 0]}, {en: 'tubería', pos: [2, 1, 3], mirar: [0, -1, 0]}],
|
|
157
|
+
// animacion: true | 'NombreDelClip', velocidad: 1,
|
|
158
|
+
// mostrar: [{nodo: 'Agua', en: 'viertes'}], ocultar: [{nodo: 'Pared', en: 'dentro'}],
|
|
159
|
+
// fondo: '#87CEEB' | null, niebla: [cerca, lejos]}
|
|
160
|
+
// Coordenadas del propio archivo (no se reescala). La cámara va suave de un punto al siguiente.
|
|
161
|
+
type Toma = {en?: unknown; pos: [number, number, number]; mirar?: [number, number, number]; fov?: number};
|
|
162
|
+
|
|
163
|
+
const Camara: React.FC<{tomas: Toma[]; ctx: CtxEscena}> = ({tomas, ctx}) => {
|
|
164
|
+
const {camera} = useThree();
|
|
165
|
+
const lista = tomas.map((k) => ({...k, t: cuando(k.en, ctx, 0)})).sort((a, b) => a.t - b.t);
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < lista.length - 1 && ctx.t >= lista[i + 1].t) i++;
|
|
168
|
+
const a = lista[i];
|
|
169
|
+
const b = lista[Math.min(i + 1, lista.length - 1)];
|
|
170
|
+
const k = b === a ? 0 : (() => {
|
|
171
|
+
const x = clamp((ctx.t - a.t) / Math.max(0.3, b.t - a.t));
|
|
172
|
+
return x * x * (3 - 2 * x);
|
|
173
|
+
})();
|
|
174
|
+
// entre dos tomas la cámara se mueve durante todo el intervalo (y se queda quieta en la última)
|
|
175
|
+
const L = (u: number[], v: number[]) => u.map((n, j) => n + (v[j] - n) * k) as [number, number, number];
|
|
176
|
+
const pos = L(a.pos, b.pos);
|
|
177
|
+
const mirar = L(a.mirar ?? [0, 0, 0], b.mirar ?? a.mirar ?? [0, 0, 0]);
|
|
178
|
+
const fov = (a.fov ?? 40) + ((b.fov ?? a.fov ?? 40) - (a.fov ?? 40)) * k;
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
camera.position.set(...pos);
|
|
181
|
+
camera.lookAt(...mirar);
|
|
182
|
+
if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) {
|
|
183
|
+
(camera as THREE.PerspectiveCamera).fov = fov;
|
|
184
|
+
camera.updateProjectionMatrix();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const ModeloEscena: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
191
|
+
const url = recurso(c.src);
|
|
192
|
+
const [g, setG] = useState<{scene: THREE.Group; mixer: THREE.AnimationMixer | null} | null>(null);
|
|
193
|
+
const [handle] = useState(() => delayRender(`Cargando escena 3D ${c.src}`));
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
cargarGltf(url)
|
|
196
|
+
.then((gl) => {
|
|
197
|
+
const scene = gl.scene.clone(true);
|
|
198
|
+
let mixer: THREE.AnimationMixer | null = null;
|
|
199
|
+
if (c.animacion && gl.animations.length) {
|
|
200
|
+
mixer = new THREE.AnimationMixer(scene);
|
|
201
|
+
const clips = c.animacion === true ? gl.animations : gl.animations.filter((a) => [].concat(c.animacion).includes(a.name as never));
|
|
202
|
+
for (const clip of clips) mixer.clipAction(clip).play();
|
|
203
|
+
}
|
|
204
|
+
scene.traverse((o: any) => {
|
|
205
|
+
if (o.isMesh && c.sombras !== false) {
|
|
206
|
+
o.castShadow = true;
|
|
207
|
+
o.receiveShadow = true;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
setG({scene, mixer});
|
|
211
|
+
continueRender(handle);
|
|
212
|
+
})
|
|
213
|
+
.catch((e) => {
|
|
214
|
+
continueRender(handle);
|
|
215
|
+
throw new Error(`No pude cargar la escena 3D ${c.src}: ${e.message}`);
|
|
216
|
+
});
|
|
217
|
+
}, [url, handle]);
|
|
218
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
219
|
+
useLayoutEffect(() => {
|
|
220
|
+
if (!g) return;
|
|
221
|
+
// animaciones del archivo: se posicionan en el tiempo exacto del fotograma (nunca corren solas)
|
|
222
|
+
g.mixer?.setTime(Math.max(0, ctx.t - t0) * (c.velocidad ?? 1));
|
|
223
|
+
const visible = (lista: any[] | undefined, valor: boolean) => {
|
|
224
|
+
for (const v of lista ?? []) {
|
|
225
|
+
const nodo = g.scene.getObjectByName(v.nodo);
|
|
226
|
+
if (nodo) nodo.visible = ctx.t >= cuando(v.en, ctx, 0) ? valor : !valor;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
visible(c.mostrar, true);
|
|
230
|
+
visible(c.ocultar, false);
|
|
231
|
+
});
|
|
232
|
+
return g ? <primitive object={g.scene} /> : null;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const Escena3D: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
236
|
+
const ancho = c.ancho ?? 1080;
|
|
237
|
+
const alto = c.alto ?? 1920;
|
|
238
|
+
const tomas: Toma[] = c.camara ?? [{pos: [6, 4, 8], mirar: [0, 0, 0]}];
|
|
239
|
+
return (
|
|
240
|
+
<div style={{width: ancho, height: alto}}>
|
|
241
|
+
<ThreeCanvas width={ancho} height={alto} shadows gl={{alpha: c.fondo == null, antialias: true}} camera={{fov: tomas[0].fov ?? 40, position: tomas[0].pos}}>
|
|
242
|
+
{c.fondo ? <color attach="background" args={[c.fondo]} /> : null}
|
|
243
|
+
{c.niebla ? <fog attach="fog" args={[c.fondo ?? '#ffffff', c.niebla[0], c.niebla[1]]} /> : null}
|
|
244
|
+
<Entorno fuerza={c.reflejos ?? 0.8} />
|
|
245
|
+
<hemisphereLight args={['#ffffff', '#8a7a66', c.luzAmbiente ?? 0.9]} />
|
|
246
|
+
<directionalLight position={c.sol ?? [8, 14, 6]} intensity={c.luz ?? 2.2} castShadow shadow-mapSize={[2048, 2048]} />
|
|
247
|
+
<Camara tomas={tomas} ctx={ctx} />
|
|
248
|
+
<ModeloEscena c={c} ctx={ctx} />
|
|
249
|
+
</ThreeCanvas>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Escenas listas para usar en cualquier estilo del motor de capas. Cada una se convierte en capas,
|
|
2
|
+
// así que todas aceptan además `capas: [...]` para agregar lo que quieras encima.
|
|
3
|
+
// capas solo tus capas
|
|
4
|
+
// titular etiqueta "// paso 1", titular con *palabra* resaltada y número de paso
|
|
5
|
+
// dato un número grande que cuenta (2.000 m, 230 dB, $5,3 M) con texto e imagen opcional
|
|
6
|
+
// lista tarjeta con renglones que se palomean
|
|
7
|
+
// telefono un teléfono con tu imagen o video
|
|
8
|
+
// comparar A contra B (antes/después)
|
|
9
|
+
// eleccion dos (o más) opciones para que el público comente: cartas VS o botones
|
|
10
|
+
// cta cierre con tu marca
|
|
11
|
+
import type {Marca} from '../tipos';
|
|
12
|
+
import type {Capa} from './capas';
|
|
13
|
+
import type {Tema} from './tema';
|
|
14
|
+
|
|
15
|
+
type P = Record<string, any>;
|
|
16
|
+
|
|
17
|
+
export const PLANTILLAS = ['capas', 'titular', 'dato', 'lista', 'telefono', 'comparar', 'eleccion', 'cta'];
|
|
18
|
+
|
|
19
|
+
export function capasDe(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
20
|
+
const extra: Capa[] = p.capas ?? [];
|
|
21
|
+
switch (tipo) {
|
|
22
|
+
case 'titular': {
|
|
23
|
+
const y = p.y ?? 560;
|
|
24
|
+
const out: Capa[] = [];
|
|
25
|
+
if (p.etiqueta) out.push({tipo: 'etiqueta', texto: p.etiqueta, x: 80, y: y - 92, ancla: 'izquierda', entrada: 'aparecer'});
|
|
26
|
+
out.push({tipo: 'texto', texto: p.texto ?? '', x: 80, y, ancla: 'izquierda', alinear: 'left', tam: p.tam ?? 104, maxAncho: p.numero != null ? 800 : 920, entrada: 'subir', en: p.en});
|
|
27
|
+
if (p.numero != null) out.push({tipo: 'numero', texto: String(p.numero), x: 1000, y, ancla: 'derecha', entrada: 'rebote', en: 0.15});
|
|
28
|
+
if (p.sub) out.push({tipo: 'texto', texto: p.sub, fuente: 'texto', tam: 44, peso: 600, color: tema.colores.suave, x: 80, y: y + 110, ancla: 'izquierda', alinear: 'left', entrada: 'aparecer', en: 0.3});
|
|
29
|
+
return [...out, ...extra];
|
|
30
|
+
}
|
|
31
|
+
case 'dato': {
|
|
32
|
+
const y = p.y ?? 940;
|
|
33
|
+
const out: Capa[] = [];
|
|
34
|
+
if (p.imagen) out.push({tipo: 'imagen', src: p.imagen, ancho: p.anchoImagen ?? 520, x: 540, y: y - 380, entrada: 'escala', flotar: true});
|
|
35
|
+
out.push({
|
|
36
|
+
tipo: 'contador',
|
|
37
|
+
de: p.de ?? 0,
|
|
38
|
+
a: p.valor ?? p.a ?? 0,
|
|
39
|
+
prefijo: p.prefijo,
|
|
40
|
+
sufijo: p.sufijo,
|
|
41
|
+
decimales: p.decimales,
|
|
42
|
+
curva: p.curva,
|
|
43
|
+
formato: p.formato ?? 'grande',
|
|
44
|
+
tam: p.tam,
|
|
45
|
+
x: 540,
|
|
46
|
+
y,
|
|
47
|
+
en: p.en ?? 'voz',
|
|
48
|
+
duracion: p.duracion,
|
|
49
|
+
entrada: 'rebote',
|
|
50
|
+
});
|
|
51
|
+
if (p.texto) out.push({tipo: 'texto', texto: p.texto, fuente: 'texto', tam: 54, peso: 700, color: tema.colores.texto, x: 540, y: y + 150, entrada: 'subir', en: (p.en ?? 0) === 'voz' ? 0.4 : 0.4});
|
|
52
|
+
return [...out, ...extra];
|
|
53
|
+
}
|
|
54
|
+
case 'lista':
|
|
55
|
+
return [{tipo: 'tarjeta', titulo: p.titulo, cabecera: p.cabecera, items: p.items ?? [], ancho: p.ancho ?? 760, x: 540, y: p.y ?? 1000, entrada: 'subir', checks: p.checks}, ...extra];
|
|
56
|
+
case 'telefono':
|
|
57
|
+
return [{tipo: 'telefono', src: p.src, capas: p.capasPantalla, ancho: p.ancho ?? 500, x: 540, y: p.y ?? 1080, entrada: 'deslizar-arriba'}, ...(p.texto ? [{tipo: 'texto', texto: p.texto, x: 540, y: 520, tam: 90, entrada: 'subir'}] : []), ...extra];
|
|
58
|
+
case 'comparar': {
|
|
59
|
+
const lado = (d: any, x: number, color: string, en: any): Capa[] => {
|
|
60
|
+
const v = typeof d === 'string' ? {texto: d} : d ?? {};
|
|
61
|
+
const out: Capa[] = [];
|
|
62
|
+
if (v.imagen) out.push({tipo: 'imagen', src: v.imagen, ancho: 340, x, y: 860, entrada: 'escala', en});
|
|
63
|
+
out.push({tipo: 'texto', texto: v.texto ?? '', tam: v.tam ?? 84, color: v.color ?? color, x, y: v.imagen ? 1110 : 960, maxAncho: 440, entrada: 'rebote', en});
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
const enB = p.at ?? p.en ?? 'fin-0.6';
|
|
67
|
+
return [
|
|
68
|
+
...lado(p.a, 280, tema.colores.texto, 0.1),
|
|
69
|
+
{tipo: 'forma', forma: 'flecha', ancho: 150, x: 540, y: 960, color: tema.colores.acento, en: enB, entrada: 'ninguna'},
|
|
70
|
+
...lado(p.b, 800, tema.colores.acento, enB),
|
|
71
|
+
...extra,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
case 'eleccion': {
|
|
75
|
+
const ops: P[] = (p.opciones ?? []).map((o: any) => (typeof o === 'string' ? {texto: o} : o));
|
|
76
|
+
const out: Capa[] = [];
|
|
77
|
+
const yTit = p.y ?? 640;
|
|
78
|
+
if (p.pregunta) out.push({tipo: 'texto', texto: p.pregunta, x: 540, y: yTit, tam: p.tamPregunta ?? 84, maxAncho: 900, entrada: 'subir'});
|
|
79
|
+
if ((p.formato ?? 'cartas') === 'botones') {
|
|
80
|
+
ops.forEach((o, i) =>
|
|
81
|
+
out.push({
|
|
82
|
+
tipo: 'texto', texto: o.texto, fuente: 'texto', tam: 56, peso: 900, mayusculas: true,
|
|
83
|
+
color: i === 0 ? (tema.colores.texto === '#FFFFFF' ? '#141414' : '#FFFFFF') : o.color ?? tema.colores.acento2,
|
|
84
|
+
fondo: i === 0 ? tema.colores.texto : 'transparent', borde: i === 0 ? undefined : o.color ?? tema.colores.acento2,
|
|
85
|
+
relleno: '18px 0', radio: 99, maxAncho: 620, ancho: 620,
|
|
86
|
+
x: 540, y: yTit + 300 + i * 170, entrada: 'rebote', en: o.en ?? 0.3 + i * 0.2, sombra: false,
|
|
87
|
+
...(i > 0 ? {} : {}),
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
ops.forEach((o, i) => o.sub && out.push({tipo: 'texto', texto: o.sub, fuente: 'texto', tam: 28, peso: 600, color: tema.colores.suave, x: 540, y: yTit + 370 + i * 170, entrada: 'aparecer', en: 0.5 + i * 0.2}));
|
|
91
|
+
} else {
|
|
92
|
+
const n = Math.max(1, ops.length);
|
|
93
|
+
// debajo de los subtítulos del centro (series) o al centro si los subtítulos van arriba/abajo
|
|
94
|
+
const yC = p.yCartas ?? (tema.subtitulos.y > 900 && tema.subtitulos.y < 1250 ? 1380 : 1060);
|
|
95
|
+
ops.forEach((o, i) => {
|
|
96
|
+
const x = n === 1 ? 540 : 270 + (i * 540) / (n - 1);
|
|
97
|
+
const en = o.en ?? 0.15 + i * 0.25;
|
|
98
|
+
out.push({tipo: 'forma', forma: 'rect', ancho: 400, alto: 440, color: o.fondo ?? '#FFFFFF', radio: 28, x, y: yC, rotar: n === 2 ? (i === 0 ? -5 : 5) : 0, entrada: 'rebote', en, borde: o.color ?? tema.colores.acento, grosor: 10, sonido: 'pop'});
|
|
99
|
+
if (o.imagen) out.push({tipo: 'imagen', src: o.imagen, ancho: 220, x, y: yC - 70, entrada: 'escala', en: en + 0.1});
|
|
100
|
+
else out.push({tipo: o.icono ? 'icono' : 'emoji', texto: o.emoji ?? '❓', nombre: o.icono, tam: o.icono ? 160 : 150, x, y: yC - 70, entrada: 'rebote', en: en + 0.1});
|
|
101
|
+
out.push({tipo: 'texto', texto: o.texto, fuente: 'texto', tam: 50, peso: 900, mayusculas: true, color: '#141414', x, y: yC + 90, maxAncho: 360, entrada: 'aparecer', en: en + 0.15, sombra: false});
|
|
102
|
+
if (o.sub) out.push({tipo: 'texto', texto: o.sub, fuente: 'texto', tam: 26, peso: 700, color: o.color ?? tema.colores.acento, x, y: yC + 155, maxAncho: 340, entrada: 'aparecer', en: en + 0.2, mayusculas: true, sombra: false});
|
|
103
|
+
});
|
|
104
|
+
if (n === 2) out.push({tipo: 'texto', texto: 'VS', tam: 120, color: tema.colores.acento, contorno: 10, colorContorno: '#141414', x: 540, y: yC, entrada: 'rebote', en: 0.55, mayusculas: true});
|
|
105
|
+
}
|
|
106
|
+
if (p.comenta !== false) out.push({tipo: 'texto', texto: `💬 ${p.comenta ?? '¡COMENTA!'}`, fuente: 'texto', tam: 50, peso: 900, color: '#141414', fondo: '#FFD84A', relleno: '14px 34px', radio: 99, x: 540, y: p.yComenta ?? (tema.subtitulos.y > 1350 ? 1300 : 1640), entrada: 'rebote', en: p.enComenta ?? 'fin-0.8', latir: true, sombra: false, mayusculas: true, sonido: 'ding'});
|
|
107
|
+
return [...out, ...extra];
|
|
108
|
+
}
|
|
109
|
+
case 'cta': {
|
|
110
|
+
const nombre = marca.nombre;
|
|
111
|
+
return [
|
|
112
|
+
{tipo: 'marca', tam: 330, x: 540, y: 760, entrada: 'rebote', latir: true},
|
|
113
|
+
...(nombre ? [{tipo: 'texto', texto: nombre, x: 540, y: 1030, tam: 120, color: tema.colores.texto, entrada: 'rebote', en: 0.25, mayusculas: false}] : []),
|
|
114
|
+
{tipo: 'texto', texto: marca.seguir, fuente: 'texto', tam: 58, peso: 800, color: '#FFFFFF', fondo: tema.colores.acento2, relleno: '18px 60px', radio: 99, x: 540, y: 1190, entrada: 'rebote', en: 0.6, hasta: 1.7, salida: 'ninguna', sombra: false},
|
|
115
|
+
{tipo: 'texto', texto: marca.siguiendo, fuente: 'texto', tam: 58, peso: 800, color: '#FFFFFF', fondo: '#3C3C46', relleno: '18px 60px', radio: 99, x: 540, y: 1190, entrada: 'ninguna', en: 1.7, sombra: false},
|
|
116
|
+
...(marca.redes ? [{tipo: 'texto', texto: marca.redes, fuente: 'texto', tam: 36, peso: 600, color: tema.colores.suave, x: 540, y: 1310, entrada: 'aparecer', en: 0.9, mayusculas: false}] : []),
|
|
117
|
+
...extra,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
return extra;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Componentes propios del proyecto (escenas o piezas en React/TSX) para la capa {tipo: 'componente'}.
|
|
2
|
+
// Morfeo los registra solo a partir de `componentes` en morfeo.config.mjs:
|
|
3
|
+
// componentes: {Mapa: './escenas/Mapa.tsx'}
|
|
4
|
+
// Cada componente recibe {t, dur, palabras, tema, cuando, ...props}: t = segundos desde que empezó la escena.
|
|
5
|
+
import type React from 'react';
|
|
6
|
+
|
|
7
|
+
const COMPONENTES: Record<string, React.FC<any>> = {};
|
|
8
|
+
|
|
9
|
+
export const registrarComponentes = (c: Record<string, React.FC<any>>) => Object.assign(COMPONENTES, c);
|
|
10
|
+
export const componente = (nombre: string) => COMPONENTES[nombre];
|
|
11
|
+
export const nombresComponentes = () => Object.keys(COMPONENTES);
|
|
@@ -0,0 +1,137 @@
|
|
|
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).
|
|
4
|
+
// 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).
|
|
6
|
+
// karaoke con `fondo`: la frase dentro de una caja oscura (infografías).
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {W, backOut, clamp} from '../estilo';
|
|
9
|
+
import type {Tema} from './tema';
|
|
10
|
+
|
|
11
|
+
type Palabra = [string, number, number];
|
|
12
|
+
|
|
13
|
+
/** Parte las palabras de la escena en frases de hasta `max` palabras, cortando en la puntuación. */
|
|
14
|
+
function frases(palabras: Palabra[], max: number): Palabra[][] {
|
|
15
|
+
const out: Palabra[][] = [];
|
|
16
|
+
let cur: Palabra[] = [];
|
|
17
|
+
for (const p of palabras) {
|
|
18
|
+
cur.push(p);
|
|
19
|
+
if (cur.length >= max || /[.,:;?!]$/.test(p[0])) {
|
|
20
|
+
out.push(cur);
|
|
21
|
+
cur = [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (cur.length) out.push(cur);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; dur: number}> = ({tema, palabras, t, dur}) => {
|
|
29
|
+
const S = tema.subtitulos;
|
|
30
|
+
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;
|
|
32
|
+
const grupos = frases(palabras, max);
|
|
33
|
+
const gi = grupos.findIndex((g, i) => t < (grupos[i + 1]?.[0][1] ?? dur) - 0.02);
|
|
34
|
+
const g = grupos[gi];
|
|
35
|
+
if (!g || t < g[0][1] - 0.12) return null;
|
|
36
|
+
const entrada = clamp((t - (g[0][1] - 0.12)) / 0.18);
|
|
37
|
+
|
|
38
|
+
if (S.tipo === 'palabra') {
|
|
39
|
+
const [w] = g[0];
|
|
40
|
+
const acento = w.replace(/[^\p{L}\p{N}]/gu, '').length >= 7;
|
|
41
|
+
return (
|
|
42
|
+
<div style={{position: 'absolute', left: 40, width: W - 80, top: S.y, transform: `translateY(-50%) scale(${backOut(entrada)})`, textAlign: 'center'}}>
|
|
43
|
+
<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}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (S.tipo === 'bloque') {
|
|
51
|
+
return (
|
|
52
|
+
<div style={{position: 'absolute', left: 90, width: W - 180, top: S.y, transform: `translateY(-50%) scale(${0.9 + 0.1 * backOut(entrada)})`, opacity: entrada, display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: '6px 18px'}}>
|
|
53
|
+
{g.map(([w, a, b], i) => {
|
|
54
|
+
const actual = t >= a && t < b + 0.06;
|
|
55
|
+
return (
|
|
56
|
+
<span
|
|
57
|
+
key={i}
|
|
58
|
+
style={{
|
|
59
|
+
fontFamily: tema.fuentes.texto,
|
|
60
|
+
fontWeight: 900,
|
|
61
|
+
fontSize: S.tam * 1.15,
|
|
62
|
+
lineHeight: 1.15,
|
|
63
|
+
textTransform: 'uppercase',
|
|
64
|
+
color: actual && !S.resalte ? tema.colores.acento : S.color,
|
|
65
|
+
background: actual && S.resalte ? S.resalte : undefined,
|
|
66
|
+
padding: '0 12px',
|
|
67
|
+
borderRadius: 10,
|
|
68
|
+
WebkitTextStroke: actual && S.resalte ? undefined : `${S.contorno ?? 8}px #1a1a1a`,
|
|
69
|
+
paintOrder: 'stroke fill',
|
|
70
|
+
transform: `scale(${actual ? 1.06 : 1})`,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{w}
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (S.tipo === 'pop') {
|
|
82
|
+
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 22px'}}>
|
|
84
|
+
{g.map(([w, a, b], i) => {
|
|
85
|
+
const activa = t >= a && t < b + 0.05;
|
|
86
|
+
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', transform: `scale(${activa ? 1.1 : 1})`}}>
|
|
88
|
+
{w}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
style={{
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
left: 80,
|
|
101
|
+
width: W - 160,
|
|
102
|
+
top: S.y,
|
|
103
|
+
transform: `translateY(-50%) translateY(${(1 - entrada) * 16}px)`,
|
|
104
|
+
opacity: entrada,
|
|
105
|
+
textAlign: 'center',
|
|
106
|
+
fontFamily: tema.fuentes.texto,
|
|
107
|
+
fontSize: S.tam,
|
|
108
|
+
fontWeight: 700,
|
|
109
|
+
lineHeight: 1.35,
|
|
110
|
+
textShadow: S.color === '#FFFFFF' && !S.fondo ? '0 3px 12px rgba(0,0,0,0.5)' : undefined,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<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
|
+
})}
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|