morfeo 0.3.4 → 0.4.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/bases/biografia/Mundo.tsx +35 -0
- package/bases/biografia/guion.mjs +34 -0
- package/bases/construiste/Mundo.tsx +273 -0
- package/bases/construiste/guion.mjs +84 -0
- package/bases/escalada/Mundo.tsx +45 -0
- package/bases/escalada/guion.mjs +24 -0
- package/bases/mito/Mundo.tsx +55 -0
- package/bases/mito/guion.mjs +29 -0
- package/bases/serie/guion.mjs +32 -0
- package/bin/morfeo.mjs +14 -6
- package/index.d.ts +26 -0
- package/lib/config.mjs +16 -4
- package/lib/estructura.mjs +9 -3
- package/lib/formulas.mjs +179 -0
- package/lib/guion.mjs +29 -3
- package/lib/nuevo-video.mjs +12 -3
- package/lib/palabras.mjs +1 -1
- package/package.json +1 -1
- package/python/preparar.py +1 -1
- package/remotion/kit/armar.tsx +245 -0
- package/remotion/kit/index.ts +1 -0
- package/remotion/motor/Video.tsx +45 -11
- package/remotion/motor/capas.tsx +2 -0
- package/remotion/motor/historias.tsx +440 -0
- package/remotion/motor/tierra-datos.ts +2 -0
- package/remotion/tipos.ts +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Piezas 3D para los formatos "construiste un X" y "escalada absurda" (un objeto que suma partes escena por escena),
|
|
2
|
+
// para multitudes ("30 000 hombres") y para la mano en primera persona que aprieta un control o sostiene algo.
|
|
3
|
+
// Todo depende de `t` (segundos) y de props; nada de useFrame.
|
|
4
|
+
//
|
|
5
|
+
// <Aparece t={t} en={cuando('ruedas')} desde="arriba"><Ruedas /></Aparece> (cae, rebota y suena por su cuenta)
|
|
6
|
+
// <Multitud3D t={t} n={400} ancho={16} fondo={10} colores={['#2F6FE0', '#F2B632']} destacar={[0, 7]} />
|
|
7
|
+
// <Mano3D t={t} pos={[0.5, -1.2, 3]} sostiene="control" aprieta={cuando('botón')} />
|
|
8
|
+
import React, {useMemo} from 'react';
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
import {azarN} from './ilustrado';
|
|
11
|
+
import {grupo, type Pieza, type V3} from './tres';
|
|
12
|
+
|
|
13
|
+
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
|
14
|
+
const suave = (x: number) => {
|
|
15
|
+
const c = clamp01(x);
|
|
16
|
+
return c * c * (3 - 2 * c);
|
|
17
|
+
};
|
|
18
|
+
/** Rebote al caer: llega en 0.55 del tramo, rebota dos veces y se queda. */
|
|
19
|
+
const caida = (k: number) => {
|
|
20
|
+
if (k >= 1) return 1;
|
|
21
|
+
if (k < 0.55) return (k / 0.55) ** 2;
|
|
22
|
+
const r = (k - 0.55) / 0.45;
|
|
23
|
+
return 1 - Math.abs(Math.sin(r * Math.PI * 2)) * (1 - r) * 0.12;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Mate: React.FC<{c: string; r?: number}> = ({c, r = 0.75}) => <meshStandardMaterial color={c} roughness={r} flatShading />;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Una parte que llega en su momento: cae desde arriba con rebote (`desde: 'arriba'`), crece desde cero (`'crece'`),
|
|
30
|
+
* entra de lado (`'izq'`/`'der'`) o sube del suelo (`'abajo'`). Antes de `en` no se ve. `dur` en segundos, `altura` de caída.
|
|
31
|
+
*/
|
|
32
|
+
export const Aparece: React.FC<Pieza & {t: number; en: number; desde?: 'arriba' | 'crece' | 'izq' | 'der' | 'abajo'; dur?: number; altura?: number; hasta?: number; children: React.ReactNode}> = ({t, en, desde = 'arriba', dur = 0.55, altura = 4, hasta, children, ...p}) => {
|
|
33
|
+
if (t < en) return null;
|
|
34
|
+
const k = clamp01((t - en) / dur);
|
|
35
|
+
const fuera = hasta != null ? suave((t - hasta) / 0.35) : 0;
|
|
36
|
+
if (fuera >= 1) return null;
|
|
37
|
+
const g = grupo(p);
|
|
38
|
+
let [x, y, z] = g.position;
|
|
39
|
+
let esc = g.scale * (1 - fuera);
|
|
40
|
+
if (desde === 'arriba') y += (1 - caida(k)) * altura;
|
|
41
|
+
else if (desde === 'abajo') y -= (1 - suave(k)) * altura;
|
|
42
|
+
else if (desde === 'izq') x -= (1 - suave(k)) * altura;
|
|
43
|
+
else if (desde === 'der') x += (1 - suave(k)) * altura;
|
|
44
|
+
else esc *= k < 1 ? Math.sin(suave(k) * Math.PI * 0.62) / Math.sin(Math.PI * 0.62) : 1;
|
|
45
|
+
// pequeño aplastamiento al tocar el suelo
|
|
46
|
+
const apl = desde === 'arriba' && k > 0.5 && k < 0.8 ? Math.sin(((k - 0.5) / 0.3) * Math.PI) * 0.12 : 0;
|
|
47
|
+
return (
|
|
48
|
+
<group position={[x, y, z]} rotation={g.rotation} scale={[esc * (1 + apl * 0.5), esc * (1 - apl), esc * (1 + apl * 0.5)]}>
|
|
49
|
+
{children}
|
|
50
|
+
</group>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Multitud de figuras simples (cuerpo + cabeza) en una rejilla con algo de desorden, en un solo `instancedMesh` (miles
|
|
56
|
+
* sin costo). `n`, `ancho`×`fondo` (unidades), `colores` del cuerpo, `destacar: [índices]` en `colorDestacado`, `brinca`
|
|
57
|
+
* para que salten. Aparece de adelante hacia atrás desde `en`.
|
|
58
|
+
*/
|
|
59
|
+
export const Multitud3D: React.FC<Pieza & {t: number; n?: number; ancho?: number; fondo?: number; colores?: string[]; colorDestacado?: string; destacar?: number[]; brinca?: boolean; en?: number; altura?: number}> = ({t, n = 300, ancho = 14, fondo = 10, colores = ['#2F6FE0', '#3D7DF0', '#2A5FC8'], colorDestacado = '#F2B632', destacar = [], brinca = false, en = 0, altura = 1.1, ...p}) => {
|
|
60
|
+
const cols = Math.max(1, Math.round(Math.sqrt((n * ancho) / fondo)));
|
|
61
|
+
const filas = Math.ceil(n / cols);
|
|
62
|
+
const datos = useMemo(() => {
|
|
63
|
+
let semilla = n * 3 + 7;
|
|
64
|
+
const r = () => azarN(semilla++);
|
|
65
|
+
return Array.from({length: n}, (_, i) => {
|
|
66
|
+
const c = i % cols;
|
|
67
|
+
const f = Math.floor(i / cols);
|
|
68
|
+
const x = (c / Math.max(1, cols - 1) - 0.5) * ancho + (r() - 0.5) * (ancho / cols) * 0.5;
|
|
69
|
+
const z = (f / Math.max(1, filas - 1) - 0.5) * fondo + (r() - 0.5) * (fondo / filas) * 0.5;
|
|
70
|
+
return {x, z, fase: r() * 6.28, escala: 0.9 + r() * 0.2, color: destacar.includes(i) ? colorDestacado : colores[Math.floor(r() * colores.length)], fila: f};
|
|
71
|
+
});
|
|
72
|
+
}, [n, cols, filas, ancho, fondo, colores.join(), colorDestacado, destacar.join()]);
|
|
73
|
+
const cuerpo = useMemo(() => new THREE.CapsuleGeometry(0.22 * altura, 0.5 * altura, 3, 6), [altura]);
|
|
74
|
+
const cabeza = useMemo(() => new THREE.IcosahedronGeometry(0.2 * altura, 0), [altura]);
|
|
75
|
+
const m = useMemo(() => new THREE.Matrix4(), []);
|
|
76
|
+
const color = useMemo(() => new THREE.Color(), []);
|
|
77
|
+
const refC = React.useRef<THREE.InstancedMesh>(null);
|
|
78
|
+
const refH = React.useRef<THREE.InstancedMesh>(null);
|
|
79
|
+
// las matrices se escriben en cada render (depende de t), no en un efecto
|
|
80
|
+
const aplicar = () => {
|
|
81
|
+
if (!refC.current || !refH.current) return;
|
|
82
|
+
datos.forEach((d, i) => {
|
|
83
|
+
const k = clamp01((t - en - d.fila * 0.04) / 0.35);
|
|
84
|
+
const s = d.escala * (k < 1 ? caida(k) : 1);
|
|
85
|
+
const salto = brinca ? Math.max(0, Math.sin(t * 6 + d.fase)) * 0.25 : 0;
|
|
86
|
+
m.compose(new THREE.Vector3(d.x, (0.47 * altura + salto) * s, d.z), new THREE.Quaternion(), new THREE.Vector3(s, s, s));
|
|
87
|
+
refC.current!.setMatrixAt(i, m);
|
|
88
|
+
refC.current!.setColorAt(i, color.set(d.color));
|
|
89
|
+
m.compose(new THREE.Vector3(d.x, (1.05 * altura + salto) * s, d.z), new THREE.Quaternion(), new THREE.Vector3(s, s, s));
|
|
90
|
+
refH.current!.setMatrixAt(i, m);
|
|
91
|
+
refH.current!.setColorAt(i, color.set('#F2C9A4'));
|
|
92
|
+
});
|
|
93
|
+
refC.current.instanceMatrix.needsUpdate = true;
|
|
94
|
+
refH.current.instanceMatrix.needsUpdate = true;
|
|
95
|
+
if (refC.current.instanceColor) refC.current.instanceColor.needsUpdate = true;
|
|
96
|
+
if (refH.current.instanceColor) refH.current.instanceColor.needsUpdate = true;
|
|
97
|
+
};
|
|
98
|
+
React.useLayoutEffect(aplicar);
|
|
99
|
+
return (
|
|
100
|
+
<group {...grupo(p)}>
|
|
101
|
+
<instancedMesh ref={refC} args={[cuerpo, undefined, n]} castShadow>
|
|
102
|
+
<meshStandardMaterial roughness={0.7} flatShading />
|
|
103
|
+
</instancedMesh>
|
|
104
|
+
<instancedMesh ref={refH} args={[cabeza, undefined, n]} castShadow>
|
|
105
|
+
<meshStandardMaterial roughness={0.7} flatShading />
|
|
106
|
+
</instancedMesh>
|
|
107
|
+
</group>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Control remoto o de portón: caja oscura con botones. */
|
|
112
|
+
const Control: React.FC<{apretado: number}> = ({apretado}) => (
|
|
113
|
+
<group>
|
|
114
|
+
<mesh castShadow>
|
|
115
|
+
<boxGeometry args={[0.55, 0.95, 0.16]} />
|
|
116
|
+
<Mate c="#26282E" />
|
|
117
|
+
</mesh>
|
|
118
|
+
<mesh position={[0, 0.2, 0.09 - apretado * 0.03]}>
|
|
119
|
+
<cylinderGeometry args={[0.13, 0.13, 0.06, 16]} />
|
|
120
|
+
<meshStandardMaterial color="#E5484D" roughness={0.4} emissive="#E5484D" emissiveIntensity={apretado * 0.8} />
|
|
121
|
+
</mesh>
|
|
122
|
+
{[-0.13, 0.13].map((x) => (
|
|
123
|
+
<mesh key={x} position={[x, -0.15, 0.09]} rotation={[Math.PI / 2, 0, 0]}>
|
|
124
|
+
<cylinderGeometry args={[0.07, 0.07, 0.04, 12]} />
|
|
125
|
+
<Mate c="#D7D9DE" />
|
|
126
|
+
</mesh>
|
|
127
|
+
))}
|
|
128
|
+
</group>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/** Teléfono: losa negra con pantalla encendida. */
|
|
132
|
+
const Movil: React.FC<{pantalla?: string}> = ({pantalla = '#3A6FF2'}) => (
|
|
133
|
+
<group>
|
|
134
|
+
<mesh castShadow>
|
|
135
|
+
<boxGeometry args={[0.55, 1.1, 0.08]} />
|
|
136
|
+
<Mate c="#111214" r={0.4} />
|
|
137
|
+
</mesh>
|
|
138
|
+
<mesh position={[0, 0, 0.045]}>
|
|
139
|
+
<planeGeometry args={[0.48, 1.0]} />
|
|
140
|
+
<meshStandardMaterial color={pantalla} emissive={pantalla} emissiveIntensity={0.7} />
|
|
141
|
+
</mesh>
|
|
142
|
+
</group>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mano en primera persona (entra desde abajo del cuadro): palma, cuatro dedos que rodean lo que sostiene y pulgar que
|
|
147
|
+
* aprieta en `aprieta` (segundos). `sostiene: 'control' | 'movil' | 'nada'`, `piel`, `manga` (color de la manga).
|
|
148
|
+
* Ponla cerca de la cámara (2–4 u) y abajo del centro.
|
|
149
|
+
*/
|
|
150
|
+
export const Mano3D: React.FC<Pieza & {t: number; sostiene?: 'control' | 'movil' | 'nada'; aprieta?: number; piel?: string; manga?: string; en?: number; pantalla?: string}> = ({t, sostiene = 'control', aprieta, piel = '#E8B48F', manga = '#2F6FE0', en = 0, pantalla, ...p}) => {
|
|
151
|
+
const entra = suave((t - en) / 0.45);
|
|
152
|
+
const dt = aprieta != null ? t - aprieta : -1;
|
|
153
|
+
const apretado = dt >= 0 && dt < 0.5 ? Math.sin((dt / 0.5) * Math.PI) : 0;
|
|
154
|
+
const g = grupo(p);
|
|
155
|
+
const [x, y, z] = g.position;
|
|
156
|
+
const tiembla = Math.sin(t * 2.1) * 0.01;
|
|
157
|
+
return (
|
|
158
|
+
<group position={[x, y - (1 - entra) * 1.6 + tiembla, z]} rotation={g.rotation} scale={g.scale}>
|
|
159
|
+
{/* manga y muñeca */}
|
|
160
|
+
<mesh position={[0, -0.95, -0.05]} rotation={[0.25, 0, 0]} castShadow>
|
|
161
|
+
<cylinderGeometry args={[0.24, 0.3, 0.9, 8]} />
|
|
162
|
+
<Mate c={manga} />
|
|
163
|
+
</mesh>
|
|
164
|
+
<mesh position={[0, -0.45, 0]} castShadow>
|
|
165
|
+
<cylinderGeometry args={[0.17, 0.2, 0.3, 8]} />
|
|
166
|
+
<Mate c={piel} />
|
|
167
|
+
</mesh>
|
|
168
|
+
{/* palma detrás del objeto */}
|
|
169
|
+
<mesh position={[0, -0.05, -0.14]} castShadow>
|
|
170
|
+
<boxGeometry args={[0.6, 0.62, 0.2]} />
|
|
171
|
+
<Mate c={piel} />
|
|
172
|
+
</mesh>
|
|
173
|
+
{/* lo que sostiene */}
|
|
174
|
+
<group position={[0, 0.15, 0]}>{sostiene === 'control' ? <Control apretado={apretado} /> : sostiene === 'movil' ? <Movil pantalla={pantalla} /> : null}</group>
|
|
175
|
+
{/* dedos que rodean por la derecha */}
|
|
176
|
+
{[0.22, 0.02, -0.18, -0.36].map((yy, i) => (
|
|
177
|
+
<mesh key={i} position={[0.33, yy + 0.1, 0.02]} rotation={[0, 0, 0.1]} castShadow>
|
|
178
|
+
<boxGeometry args={[0.14, 0.15, 0.24]} />
|
|
179
|
+
<Mate c={piel} />
|
|
180
|
+
</mesh>
|
|
181
|
+
))}
|
|
182
|
+
{/* pulgar: baja sobre el botón al apretar */}
|
|
183
|
+
<group position={[-0.32, -0.05, 0.05]} rotation={[0, 0, -0.9 + apretado * 0.35]}>
|
|
184
|
+
<mesh position={[0, 0.25, 0.1 - apretado * 0.04]} castShadow>
|
|
185
|
+
<boxGeometry args={[0.16, 0.42, 0.16]} />
|
|
186
|
+
<Mate c={piel} />
|
|
187
|
+
</mesh>
|
|
188
|
+
</group>
|
|
189
|
+
</group>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Partes listas para armar objetos que crecen: rueda, motor y asiento (low-poly, se combinan con Aparece). */
|
|
194
|
+
export const Rueda: React.FC<Pieza & {radio?: number; t?: number; gira?: number; color?: string}> = ({radio = 0.35, t = 0, gira = 0, color = '#1E1F24', ...p}) => (
|
|
195
|
+
<group {...grupo(p)}>
|
|
196
|
+
<group rotation={[0, 0, -t * gira]}>
|
|
197
|
+
<mesh rotation={[Math.PI / 2, 0, 0]} castShadow>
|
|
198
|
+
<cylinderGeometry args={[radio, radio, radio * 0.6, 14]} />
|
|
199
|
+
<Mate c={color} r={0.9} />
|
|
200
|
+
</mesh>
|
|
201
|
+
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0.01]}>
|
|
202
|
+
<cylinderGeometry args={[radio * 0.5, radio * 0.5, radio * 0.62, 8]} />
|
|
203
|
+
<Mate c="#B9BEC8" r={0.5} />
|
|
204
|
+
</mesh>
|
|
205
|
+
</group>
|
|
206
|
+
</group>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
export const Motor: React.FC<Pieza & {t?: number; color?: string}> = ({t = 0, color = '#F2B632', ...p}) => {
|
|
210
|
+
const vibra = Math.sin(t * 60) * 0.01;
|
|
211
|
+
return (
|
|
212
|
+
<group {...grupo(p)} position={[(p.pos?.[0] ?? 0) + vibra, p.pos?.[1] ?? 0, p.pos?.[2] ?? 0]}>
|
|
213
|
+
<mesh castShadow>
|
|
214
|
+
<boxGeometry args={[0.7, 0.5, 0.5]} />
|
|
215
|
+
<Mate c={color} />
|
|
216
|
+
</mesh>
|
|
217
|
+
<mesh position={[0, 0.32, 0]} castShadow>
|
|
218
|
+
<boxGeometry args={[0.4, 0.14, 0.34]} />
|
|
219
|
+
<Mate c="#2A2C33" />
|
|
220
|
+
</mesh>
|
|
221
|
+
<mesh position={[0.42, 0.1, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
|
|
222
|
+
<cylinderGeometry args={[0.06, 0.06, 0.2, 8]} />
|
|
223
|
+
<Mate c="#6B7080" />
|
|
224
|
+
</mesh>
|
|
225
|
+
</group>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const Asiento: React.FC<Pieza & {color?: string}> = ({color = '#E5484D', ...p}) => (
|
|
230
|
+
<group {...grupo(p)}>
|
|
231
|
+
<mesh position={[0, 0, 0]} castShadow>
|
|
232
|
+
<boxGeometry args={[0.6, 0.14, 0.55]} />
|
|
233
|
+
<Mate c={color} />
|
|
234
|
+
</mesh>
|
|
235
|
+
<mesh position={[0, 0.32, -0.24]} castShadow>
|
|
236
|
+
<boxGeometry args={[0.6, 0.6, 0.12]} />
|
|
237
|
+
<Mate c={color} />
|
|
238
|
+
</mesh>
|
|
239
|
+
<mesh position={[0, -0.25, 0]} castShadow>
|
|
240
|
+
<cylinderGeometry args={[0.05, 0.05, 0.4, 6]} />
|
|
241
|
+
<Mate c="#6B7080" />
|
|
242
|
+
</mesh>
|
|
243
|
+
</group>
|
|
244
|
+
);
|
|
245
|
+
|
package/remotion/kit/index.ts
CHANGED
package/remotion/motor/Video.tsx
CHANGED
|
@@ -53,6 +53,21 @@ const EscenaVista: React.FC<{e: Escena; t: number; tema: Tema; semilla: string;
|
|
|
53
53
|
);
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/** Lugar (mundo) de la escena `i`: el suyo, o el de la última escena anterior que dijo uno, o el primero de `lugares`. */
|
|
57
|
+
export function lugarDe(d: Datos, i: number): string | null {
|
|
58
|
+
const nombres = Object.keys(d.lugares ?? {});
|
|
59
|
+
if (!nombres.length) return null;
|
|
60
|
+
for (let j = i; j >= 0; j--) {
|
|
61
|
+
const l = d.escenas[j]?.p.lugar;
|
|
62
|
+
if (typeof l === 'string' && d.lugares![l]) return l;
|
|
63
|
+
}
|
|
64
|
+
return d.mundo?.length ? null : nombres[0];
|
|
65
|
+
}
|
|
66
|
+
const capasDeLugar = (d: Datos, i: number) => {
|
|
67
|
+
const l = lugarDe(d, i);
|
|
68
|
+
return (l ? d.lugares![l] : d.mundo) ?? [];
|
|
69
|
+
};
|
|
70
|
+
|
|
56
71
|
type Toma = {x: number; y: number; zoom: number; giro: number};
|
|
57
72
|
const TOMA0: Toma = {x: 540, y: 960, zoom: 1, giro: 0};
|
|
58
73
|
|
|
@@ -62,8 +77,10 @@ const TOMA0: Toma = {x: 540, y: 960, zoom: 1, giro: 0};
|
|
|
62
77
|
* Sin `toma`, la escena conserva la anterior. `corte: true` salta sin viaje.
|
|
63
78
|
*/
|
|
64
79
|
export function camaraMundo(d: Datos, si: number, t: number): Toma {
|
|
80
|
+
// cada lugar tiene su propia cámara: al buscar la toma anterior no se cruza a otro lugar
|
|
65
81
|
const destino = (i: number): Toma => {
|
|
66
|
-
|
|
82
|
+
const l = lugarDe(d, i);
|
|
83
|
+
for (let j = i; j >= 0 && lugarDe(d, j) === l; j--) if (d.escenas[j].p.toma) return {...TOMA0, ...d.escenas[j].p.toma};
|
|
67
84
|
return TOMA0;
|
|
68
85
|
};
|
|
69
86
|
const final = (i: number): Toma => {
|
|
@@ -76,7 +93,8 @@ export function camaraMundo(d: Datos, si: number, t: number): Toma {
|
|
|
76
93
|
const tm = e.p.toma ?? {};
|
|
77
94
|
const a = destino(si);
|
|
78
95
|
const b = final(si);
|
|
79
|
-
const
|
|
96
|
+
const cambiaLugar = si > 0 && lugarDe(d, si) !== lugarDe(d, si - 1);
|
|
97
|
+
const viaje = tm.corte || si === 0 || cambiaLugar ? 0 : tm.duracion ?? Math.min(1.1, e.dur * 0.55);
|
|
80
98
|
const kd = clamp((t - viaje * 0.6) / Math.max(0.01, e.dur - viaje * 0.6));
|
|
81
99
|
const kq = tm.a ? easeIO(kd) : kd;
|
|
82
100
|
const actual: Toma = {x: lerp(a.x, b.x, kq), y: lerp(a.y, b.y, kq), zoom: lerp(a.zoom, b.zoom, kq), giro: lerp(a.giro, b.giro, kq)};
|
|
@@ -99,7 +117,7 @@ const Mundo: React.FC<{d: Datos; si: number; t: number; T: number; tema: Tema}>
|
|
|
99
117
|
const propias = capasDe(e.tipo, e.p, temaEsc, marca).filter((c: any) => c.enMundo);
|
|
100
118
|
return (
|
|
101
119
|
<AbsoluteFill style={{transform: `translate(540px, 960px) scale(${cam.zoom}) rotate(${cam.giro}deg) translate(${-cam.x}px, ${-cam.y}px)`, transformOrigin: '0 0'}}>
|
|
102
|
-
<Capas capas={d
|
|
120
|
+
<Capas capas={capasDeLugar(d, si) as any} ctx={ctxMundo} />
|
|
103
121
|
{propias.length ? <Capas capas={propias} ctx={ctxEsc} /> : null}
|
|
104
122
|
</AbsoluteFill>
|
|
105
123
|
);
|
|
@@ -198,7 +216,9 @@ const RotuloSello: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
|
|
|
198
216
|
const w = ancho(limpio, tema.fuentes.titulo, tam, 900);
|
|
199
217
|
if (w > 820) tam = Math.max(44, Math.floor((tam * 820) / w));
|
|
200
218
|
} catch {}
|
|
201
|
-
|
|
219
|
+
// lejos de los subtítulos: si van a media altura, el sello se pone arriba de ellos
|
|
220
|
+
const S = tema.subtitulos;
|
|
221
|
+
const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && Math.abs(S.y - 1180) < 260 ? S.y - 300 : 1180);
|
|
202
222
|
return (
|
|
203
223
|
<div style={{position: 'absolute', left: 0, width: W, top: y, display: 'flex', justifyContent: 'center', pointerEvents: 'none', opacity: clamp(caida * 4)}}>
|
|
204
224
|
<div
|
|
@@ -238,7 +258,9 @@ const RotuloPaso: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
|
|
|
238
258
|
const k = easeIO(clamp(t / 0.35));
|
|
239
259
|
const color = colorRotulo(e.p.rotuloColor ?? tema.rotulo?.color, tema.colores.acento);
|
|
240
260
|
const tam = e.p.rotuloTam ?? tema.rotulo?.tam ?? 64;
|
|
241
|
-
|
|
261
|
+
// si los subtítulos van arriba, el titular baja para no encimarse con ellos
|
|
262
|
+
const S = tema.subtitulos;
|
|
263
|
+
const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && S.y < 600 ? S.y + 120 : 250);
|
|
242
264
|
const n = e.p.rotuloPaso;
|
|
243
265
|
return (
|
|
244
266
|
<div style={{position: 'absolute', left: 56, right: 56, top: y, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, pointerEvents: 'none'}}>
|
|
@@ -305,7 +327,8 @@ const RotuloPixel: React.FC<{texto: string; t: number; tema: Tema; e: Escena}> =
|
|
|
305
327
|
const w = ancho(limpio, PIXEL, tam, 400);
|
|
306
328
|
if (w > 900) tam = Math.max(30, Math.floor((tam * 900) / w));
|
|
307
329
|
} catch {}
|
|
308
|
-
const
|
|
330
|
+
const S = tema.subtitulos;
|
|
331
|
+
const y = e.p.rotuloY ?? tema.rotulo?.y ?? (S.tipo !== 'ninguno' && S.y < 600 ? S.y + 150 : 230);
|
|
309
332
|
return (
|
|
310
333
|
<div style={{position: 'absolute', left: 0, width: W, top: y, display: 'flex', justifyContent: 'center', pointerEvents: 'none'}}>
|
|
311
334
|
<div
|
|
@@ -368,10 +391,12 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
|
|
|
368
391
|
const e = d.escenas[si];
|
|
369
392
|
const t = T - e.t0;
|
|
370
393
|
const semilla = d.id;
|
|
371
|
-
const conMundo = !!d.mundo?.length;
|
|
394
|
+
const conMundo = !!d.mundo?.length || Object.keys(d.lugares ?? {}).length > 0;
|
|
372
395
|
const DUR = tema.movimiento?.transicion ?? TRANSICION;
|
|
373
|
-
// con mundo, la transición es el viaje de la cámara: las capas de pantalla solo entran con su propia animación
|
|
374
|
-
|
|
396
|
+
// con mundo, la transición es el viaje de la cámara: las capas de pantalla solo entran con su propia animación.
|
|
397
|
+
// Al cambiar de lugar sí hay transición (la de la escena; corte si no dice), y el lugar anterior sale con ella.
|
|
398
|
+
const cambiaLugar = conMundo && si > 0 && lugarDe(d, si) !== lugarDe(d, si - 1);
|
|
399
|
+
const trans = conMundo ? (cambiaLugar ? ((e.p.transicion as string) ?? 'corte') : 'corte') : ((e.p.transicion ?? tema.transicion) as string);
|
|
375
400
|
const enTransicion = si > 0 && t < DUR && trans !== 'corte';
|
|
376
401
|
const lin = clamp(t / DUR);
|
|
377
402
|
const k = easeOut(lin);
|
|
@@ -408,10 +433,19 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
|
|
|
408
433
|
return (
|
|
409
434
|
<AbsoluteFill style={{background: '#000', overflow: 'hidden'}}>
|
|
410
435
|
<Modo d={d} si={si} t={t} T={T} claro={LOOKS_CLAROS.includes(d.estilo ?? '') || tema.colores.texto !== '#FFFFFF'}>
|
|
411
|
-
{conMundo
|
|
436
|
+
{conMundo && enTransicion && prev ? (
|
|
437
|
+
<AbsoluteFill style={estiloPrevio}>
|
|
438
|
+
<Mundo d={d} si={si - 1} t={prev.dur + t} T={T} tema={tema} />
|
|
439
|
+
</AbsoluteFill>
|
|
440
|
+
) : null}
|
|
441
|
+
{conMundo ? (
|
|
442
|
+
<AbsoluteFill style={enTransicion ? estiloActual : undefined}>
|
|
443
|
+
<Mundo d={d} si={si} t={t} T={T} tema={tema} />
|
|
444
|
+
</AbsoluteFill>
|
|
445
|
+
) : null}
|
|
412
446
|
{enTransicion && prev ? (
|
|
413
447
|
<AbsoluteFill style={estiloPrevio}>
|
|
414
|
-
<EscenaVista e={prev} t={prev.dur + t} tema={tema} semilla={semilla} />
|
|
448
|
+
<EscenaVista e={prev} t={prev.dur + t} tema={tema} semilla={semilla} sinFondo={conMundo} />
|
|
415
449
|
</AbsoluteFill>
|
|
416
450
|
) : null}
|
|
417
451
|
<AbsoluteFill style={estiloActual}>
|
package/remotion/motor/capas.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {Avatar, Icono, tamIcono, useMarca} from '../piezas';
|
|
|
6
6
|
import {H, W, ancho, backOut, clamp, easeIO, easeOut, easeOutExpo, lerp, resorte} from '../estilo';
|
|
7
7
|
import {ANOTACIONES, ANOTACIONES_SIN_ENTRADA, Anotacion} from './anotaciones';
|
|
8
8
|
import {FICHAS, Fichas} from './fichas';
|
|
9
|
+
import {HISTORIAS, Historias} from './historias';
|
|
9
10
|
import {azar, cuando, formatoNumero, partesResaltadas, recurso, type CtxEscena} from './util';
|
|
10
11
|
import {Particulas} from './fondo';
|
|
11
12
|
import {Escena3D, Modelo3D} from './modelo3d';
|
|
@@ -280,6 +281,7 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
280
281
|
default:
|
|
281
282
|
if (ANOTACIONES.includes(c.tipo)) return <Anotacion c={c} ctx={ctx} />;
|
|
282
283
|
if (FICHAS.includes(c.tipo)) return <Fichas c={c} ctx={ctx} />;
|
|
284
|
+
if (HISTORIAS.includes(c.tipo)) return <Historias c={c} ctx={ctx} />;
|
|
283
285
|
return <div style={{color: 'red', fontSize: 40, fontFamily: 'sans-serif', background: '#fff', padding: 10}}>capa desconocida: {c.tipo}</div>;
|
|
284
286
|
}
|
|
285
287
|
};
|