morfeo 0.5.0 → 0.6.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/README.md +1 -1
- package/bin/morfeo.mjs +27 -3
- package/index.d.ts +20 -1
- package/lib/calidad.mjs +127 -0
- package/lib/config.mjs +10 -1
- package/lib/estructura.mjs +34 -4
- package/lib/formulas.mjs +2 -1
- package/lib/hoja.mjs +2 -2
- package/lib/recursos.mjs +6 -2
- package/lib/render.mjs +42 -6
- package/package.json +4 -2
- package/plantilla/AGENTS.md +11 -5
- package/plantilla/gustos.md +19 -0
- package/python/musica.py +43 -2
- package/python/preparar.py +1 -1
- package/remotion/kit/ambientes.tsx +635 -0
- package/remotion/kit/casa.tsx +130 -0
- package/remotion/kit/index.ts +3 -0
- package/remotion/kit/tres.tsx +150 -11
- package/remotion/motor/Video.tsx +33 -2
- package/remotion/motor/capas.tsx +24 -0
- package/remotion/motor/util.ts +2 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Piezas 3D de casa y de electrónica, sacadas de los mundos que más se repetían en videos reales (consola, escalada,
|
|
2
|
+
// control): chip con patitas, ventilador de pie y de PC, escritorio, puerta, audífonos, nube de polvo o humo.
|
|
3
|
+
// Todas dependen solo de props y de `t` (sin useFrame). Las nubes se apagan solas fuera de su tramo (`en`..`hasta`).
|
|
4
|
+
//
|
|
5
|
+
// <Chip pos={[0, 0.62, 0]} tam={[0.7, 0.7]} calor={0.5} /> <Ventilador pos={[1.7, 0, -1.6]} t={t} />
|
|
6
|
+
// <Nube3D pos={[0.7, 0.9, -2]} t={t} en={12} hasta={16} />
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {azarN} from './ilustrado';
|
|
9
|
+
import {Bloque, grupo, type Pieza, type V3} from './tres';
|
|
10
|
+
|
|
11
|
+
/** Chip negro con patitas plateadas a los lados. `calor` 0..1 lo pone al rojo. */
|
|
12
|
+
export const Chip: React.FC<Pieza & {tam?: [number, number]; calor?: number; color?: string}> = ({tam = [0.6, 0.6], calor = 0, color = '#17171C', ...p}) => (
|
|
13
|
+
<group {...grupo(p)}>
|
|
14
|
+
<mesh position={[0, 0.05, 0]} castShadow>
|
|
15
|
+
<boxGeometry args={[tam[0], 0.1, tam[1]]} />
|
|
16
|
+
<meshStandardMaterial color={color} roughness={0.6} emissive="#FF4A1C" emissiveIntensity={calor * 0.9} />
|
|
17
|
+
</mesh>
|
|
18
|
+
{Array.from({length: 6}, (_, i) =>
|
|
19
|
+
[-1, 1].map((l) => <Bloque key={`${i}${l}`} pos={[-tam[0] / 2 + 0.08 + (i * (tam[0] - 0.16)) / 5, 0.02, l * (tam[1] / 2 + 0.03)]} tam={[0.04, 0.03, 0.06]} color="#C9CCD3" metal={0.8} rugosidad={0.3} />),
|
|
20
|
+
)}
|
|
21
|
+
</group>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** Aspas que giran en el plano XY (miran hacia +z). */
|
|
25
|
+
export const Aspas: React.FC<{giro: number; radio: number; n?: number; color?: string}> = ({giro, radio, n = 4, color = '#E8ECF2'}) => (
|
|
26
|
+
<group rotation={[0, 0, giro]}>
|
|
27
|
+
{Array.from({length: n}, (_, k) => (
|
|
28
|
+
<Bloque key={k} pos={[(radio / 2) * Math.cos((k * Math.PI * 2) / n), (radio / 2) * Math.sin((k * Math.PI * 2) / n), 0]} rot={[0, 0, (k * 360) / n]} tam={[radio, radio * 0.32, 0.02]} color={color} />
|
|
29
|
+
))}
|
|
30
|
+
</group>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ventilador. `tipo: 'pie'` (de pedestal, ~1.3 u, mira hacia +z; gíralo con `rot`) o `'pc'` (disipador con
|
|
35
|
+
* ventilador encima, para chips). `velocidad` en vueltas por segundo.
|
|
36
|
+
*/
|
|
37
|
+
export const Ventilador: React.FC<Pieza & {t: number; tipo?: 'pie' | 'pc'; velocidad?: number; color?: string}> = ({t, tipo = 'pie', velocidad = 2.2, color = '#D9DDE3', ...p}) => {
|
|
38
|
+
const giro = t * velocidad * Math.PI * 2;
|
|
39
|
+
if (tipo === 'pc')
|
|
40
|
+
return (
|
|
41
|
+
<group {...grupo(p)}>
|
|
42
|
+
{Array.from({length: 7}, (_, i) => (
|
|
43
|
+
<Bloque key={i} pos={[-0.3 + i * 0.1, 0.12, 0]} tam={[0.03, 0.24, 0.7]} color="#B9BEC8" metal={0.7} rugosidad={0.35} />
|
|
44
|
+
))}
|
|
45
|
+
<group position={[0, 0.3, 0]}>
|
|
46
|
+
<mesh>
|
|
47
|
+
<cylinderGeometry args={[0.34, 0.34, 0.06, 24]} />
|
|
48
|
+
<meshStandardMaterial color="#222228" />
|
|
49
|
+
</mesh>
|
|
50
|
+
<group position={[0, 0.04, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
51
|
+
<Aspas giro={giro} radio={0.3} n={5} color="#3A3A42" />
|
|
52
|
+
</group>
|
|
53
|
+
</group>
|
|
54
|
+
</group>
|
|
55
|
+
);
|
|
56
|
+
return (
|
|
57
|
+
<group {...grupo(p)}>
|
|
58
|
+
<Bloque pos={[0, 0.03, 0]} tam={[0.5, 0.06, 0.5]} color={color} radio={0.05} />
|
|
59
|
+
<Bloque pos={[0, 0.6, 0]} tam={[0.05, 1.1, 0.05]} color="#B9BEC8" metal={0.5} />
|
|
60
|
+
<group position={[0, 1.2, 0]}>
|
|
61
|
+
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
|
62
|
+
<torusGeometry args={[0.3, 0.025, 8, 28]} />
|
|
63
|
+
<meshStandardMaterial color={color} />
|
|
64
|
+
</mesh>
|
|
65
|
+
<Aspas giro={giro} radio={0.26} color="#7FB2FF" />
|
|
66
|
+
</group>
|
|
67
|
+
</group>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Escritorio de madera (cara de arriba a `alto`). */
|
|
72
|
+
export const Escritorio: React.FC<Pieza & {ancho?: number; fondo?: number; alto?: number; color?: string; patas?: string}> = ({ancho = 2.4, fondo = 0.9, alto = 0.78, color = '#C49A6C', patas = '#8A6A48', ...p}) => (
|
|
73
|
+
<group {...grupo(p)}>
|
|
74
|
+
<Bloque pos={[0, alto - 0.035, 0]} tam={[ancho, 0.07, fondo]} color={color} rugosidad={0.8} />
|
|
75
|
+
{[-1, 1].map((l) => (
|
|
76
|
+
<Bloque key={l} pos={[l * (ancho / 2 - 0.1), (alto - 0.07) / 2, 0]} tam={[0.07, alto - 0.07, fondo - 0.1]} color={patas} />
|
|
77
|
+
))}
|
|
78
|
+
</group>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/** Puerta en una pared (mira hacia +z; para la pared izquierda usa rot [0, 90, 0]). `abierta` 0..1. */
|
|
82
|
+
export const Puerta: React.FC<Pieza & {abierta?: number; color?: string; marco?: string}> = ({abierta = 0, color = '#8A6A48', marco = '#E9E1D2', ...p}) => (
|
|
83
|
+
<group {...grupo(p)}>
|
|
84
|
+
<Bloque pos={[0, 1.07, 0]} tam={[1.14, 2.2, 0.04]} color={marco} />
|
|
85
|
+
<group position={[-0.5, 0, 0.04]} rotation={[0, -abierta * 1.4, 0]}>
|
|
86
|
+
<Bloque pos={[0.5, 1.03, 0]} tam={[1.0, 2.06, 0.05]} color={color} />
|
|
87
|
+
<Bloque pos={[0.88, 1.0, 0.04]} tam={[0.12, 0.05, 0.05]} color="#D4B04A" metal={0.6} />
|
|
88
|
+
</group>
|
|
89
|
+
</group>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/** Audífonos de diadema (para la cabeza de una Persona3D: centro de la cabeza, escala ~1.2). */
|
|
93
|
+
export const Audifonos: React.FC<Pieza & {color?: string; diadema?: string}> = ({color = '#E5484D', diadema = '#1B1B20', ...p}) => (
|
|
94
|
+
<group {...grupo(p)}>
|
|
95
|
+
<mesh>
|
|
96
|
+
<torusGeometry args={[0.36, 0.035, 8, 24, Math.PI]} />
|
|
97
|
+
<meshStandardMaterial color={diadema} />
|
|
98
|
+
</mesh>
|
|
99
|
+
{[-1, 1].map((l) => (
|
|
100
|
+
<mesh key={l} position={[l * 0.37, -0.02, 0]} rotation={[0, 0, Math.PI / 2]}>
|
|
101
|
+
<cylinderGeometry args={[0.14, 0.14, 0.1, 18]} />
|
|
102
|
+
<meshStandardMaterial color={color} />
|
|
103
|
+
</mesh>
|
|
104
|
+
))}
|
|
105
|
+
</group>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Nube que sale de un punto (polvo, humo, vapor): crece en ~2 s desde `en` y desaparece en `hasta` (segundos del
|
|
110
|
+
* video). Fuera de ese tramo no dibuja nada, así que no se queda en el cierre.
|
|
111
|
+
*/
|
|
112
|
+
export const Nube3D: React.FC<{pos: V3; t: number; en: number; hasta?: number; n?: number; color?: string; tam?: number; alcance?: number}> = ({pos, t, en, hasta, n = 40, color = '#9A948A', tam = 0.03, alcance = 0.8}) => {
|
|
113
|
+
if (t < en || (hasta != null && t > hasta)) return null;
|
|
114
|
+
const k = Math.min(1, (t - en) / 2.2);
|
|
115
|
+
const sale = hasta != null ? Math.min(1, Math.max(0, (hasta - t) / 0.5)) : 1;
|
|
116
|
+
return (
|
|
117
|
+
<group>
|
|
118
|
+
{Array.from({length: n}, (_, i) => {
|
|
119
|
+
const dir = azarN(i * 3.3) * Math.PI * 2;
|
|
120
|
+
const r = k * (0.3 + azarN(i * 5.1) * 0.6) * alcance;
|
|
121
|
+
return (
|
|
122
|
+
<mesh key={i} position={[pos[0] + Math.cos(dir) * r, pos[1] + k * (0.2 + azarN(i * 1.7) * 0.5) * alcance, pos[2] + Math.sin(dir) * r * 0.6]}>
|
|
123
|
+
<sphereGeometry args={[tam * (0.7 + azarN(i * 9.1) * 1.2), 6, 5]} />
|
|
124
|
+
<meshStandardMaterial color={color} transparent opacity={0.75 * sale} roughness={1} />
|
|
125
|
+
</mesh>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</group>
|
|
129
|
+
);
|
|
130
|
+
};
|
package/remotion/kit/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// import {Persona2D, Multitud, Globo, CiudadNoche, Cuarto} from 'morfeo/kit'; (personajes y escenarios 2D)
|
|
4
4
|
// import {Lienzo3D, Persona3D, Ciudad, Terreno, Tubo, Mesa, Pila} from 'morfeo/kit'; (3D: de día o con luz cálida)
|
|
5
5
|
// import {Maqueta, Modelo, Persona, Coche, Trafico, Corte} from 'morfeo/kit'; (diorama low-poly)
|
|
6
|
+
// import {Ambiente, Tele3D, Gato3D} from 'morfeo/kit'; (ambientes 3D completos: cuarto-gamer, sala, arcade)
|
|
6
7
|
// import {GraficaBarras, GraficaLinea, Comparacion, Lupa} from 'morfeo/kit'; (datos 2D)
|
|
7
8
|
export * from './ilustrado';
|
|
8
9
|
export * from './personas';
|
|
@@ -13,3 +14,5 @@ export * from './datos';
|
|
|
13
14
|
export * from './diorama';
|
|
14
15
|
export {clamp, easeIO, easeOut, easeOutExpo, backOut, resorte, ruido, lerp, mezclaColor} from '../estilo';
|
|
15
16
|
export * from './armar';
|
|
17
|
+
export * from './ambientes';
|
|
18
|
+
export * from './casa';
|
package/remotion/kit/tres.tsx
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// La cámara se mueve por `tomas`: una por escena; viaja desde donde terminó la anterior y luego deriva despacio.
|
|
7
7
|
// Nada usa useFrame: todo depende de las props (`t`, `tEscena`), como pide Remotion.
|
|
8
8
|
import {useThree} from '@react-three/fiber';
|
|
9
|
+
import {Bloom, DepthOfField, EffectComposer, TiltShift2, Vignette} from '@react-three/postprocessing';
|
|
9
10
|
import {ThreeCanvas} from '@remotion/three';
|
|
11
|
+
import {continueRender, delayRender} from 'remotion';
|
|
10
12
|
import React, {useEffect, useLayoutEffect, useMemo, useRef} from 'react';
|
|
11
13
|
import * as THREE from 'three';
|
|
12
14
|
import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|
@@ -69,10 +71,63 @@ export function camara3D(tomas: (Toma3D | undefined | null)[], escena: number, t
|
|
|
69
71
|
return {pos: mezcla(o.pos, actual.pos, kv), mirar: mezcla(o.mirar, actual.mirar, kv), fov: o.fov + (actual.fov - o.fov) * kv};
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Toma calculada para que un objeto de tamaño `tam` (u) quepa en el cuadro vertical sin pegarse ni salirse.
|
|
76
|
+
* tipo: 'detalle' (llena el ancho) · 'media' (con aire alrededor) · 'general' (con su entorno)
|
|
77
|
+
* desde: 'frente' | 'izq' | 'der' | 'arriba' | 'atras' o un vector propio (de dónde se mira)
|
|
78
|
+
* La toma se acerca un poco durante la escena (`acercar`, 0.08 = 8 %).
|
|
79
|
+
*/
|
|
80
|
+
export function tomaA(objetivo: V3, o: {tipo?: 'detalle' | 'media' | 'general'; tam?: number; desde?: 'frente' | 'izq' | 'der' | 'arriba' | 'atras' | V3; fov?: number; acercar?: number; corte?: boolean} = {}): Toma3D {
|
|
81
|
+
const fov = o.fov ?? 40;
|
|
82
|
+
const DIR: Record<string, V3> = {frente: [0, 0.35, 1], izq: [-1, 0.35, 0.35], der: [1, 0.35, 0.35], arriba: [0, 1.7, 0.55], atras: [0, 0.35, -1]};
|
|
83
|
+
const d0 = Array.isArray(o.desde) ? o.desde : DIR[o.desde ?? 'frente'];
|
|
84
|
+
const n = Math.hypot(...d0) || 1;
|
|
85
|
+
const dir: V3 = [d0[0] / n, d0[1] / n, d0[2] / n];
|
|
86
|
+
// en vertical el ancho manda: campo horizontal = 2·atan(tan(fov/2)·9/16)
|
|
87
|
+
const mitadH = Math.atan(Math.tan(((fov / 2) * Math.PI) / 180) * (9 / 16));
|
|
88
|
+
const ancho = (o.tam ?? 1) * {detalle: 1.35, media: 2.4, general: 4.5}[o.tipo ?? 'media'];
|
|
89
|
+
const d = ancho / (2 * Math.tan(mitadH));
|
|
90
|
+
const pos: V3 = [objetivo[0] + dir[0] * d, objetivo[1] + dir[1] * d, objetivo[2] + dir[2] * d];
|
|
91
|
+
return {pos, mirar: objetivo, fov, corte: o.corte, a: {pos: mezcla(pos, objetivo, o.acercar ?? 0.08)}};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lente despejado: si algo queda pegado a la cámara (a menos de `cerca` u) entre ella y lo que mira, la cámara avanza
|
|
96
|
+
* hasta pasarlo. Evita los cuadros tapados por una cabeza, una bocina o una pared en primer plano.
|
|
97
|
+
*/
|
|
98
|
+
function despejar(scene: THREE.Scene, desde: THREE.Vector3, hacia: THREE.Vector3, cerca = 0.9): THREE.Vector3 | null {
|
|
99
|
+
const dir = hacia.clone().sub(desde);
|
|
100
|
+
const dist = dir.length();
|
|
101
|
+
if (dist < 0.5) return null;
|
|
102
|
+
dir.normalize();
|
|
103
|
+
const solido = (h: THREE.Intersection) => {
|
|
104
|
+
const m = h.object as THREE.Mesh;
|
|
105
|
+
if (!(m as any).isMesh || !m.visible || m.userData?.sinOclusion) return false;
|
|
106
|
+
const mat = (Array.isArray(m.material) ? m.material[0] : m.material) as THREE.Material | undefined;
|
|
107
|
+
return !!mat && !(mat.transparent && (mat as any).opacity < 0.6);
|
|
108
|
+
};
|
|
109
|
+
scene.updateMatrixWorld(true);
|
|
110
|
+
const ray = new THREE.Raycaster(desde, dir, 0.02, Math.min(cerca, dist - 0.4));
|
|
111
|
+
if (!ray.intersectObjects(scene.children, true).some(solido)) return null;
|
|
112
|
+
// la cara de esos objetos que mira hacia el objetivo: desde ahí ya no tapan
|
|
113
|
+
const vuelta = new THREE.Raycaster(hacia, dir.clone().negate(), 0, dist);
|
|
114
|
+
let salida = 0;
|
|
115
|
+
for (const h of vuelta.intersectObjects(scene.children, true)) {
|
|
116
|
+
const desdeCam = dist - h.distance;
|
|
117
|
+
if (solido(h) && desdeCam < cerca + 1.2) salida = Math.max(salida, desdeCam);
|
|
118
|
+
}
|
|
119
|
+
if (!salida || salida > dist - 0.4) return null;
|
|
120
|
+
return desde.clone().add(dir.multiplyScalar(salida + 0.12));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const Camara: React.FC<{estado: Estado; despejado?: boolean}> = ({estado, despejado}) => {
|
|
124
|
+
const {camera, scene} = useThree();
|
|
74
125
|
useLayoutEffect(() => {
|
|
75
126
|
camera.position.set(...estado.pos);
|
|
127
|
+
if (despejado) {
|
|
128
|
+
const nueva = despejar(scene, new THREE.Vector3(...estado.pos), new THREE.Vector3(...estado.mirar));
|
|
129
|
+
if (nueva) camera.position.copy(nueva);
|
|
130
|
+
}
|
|
76
131
|
camera.lookAt(...estado.mirar);
|
|
77
132
|
const p = camera as THREE.PerspectiveCamera;
|
|
78
133
|
if (p.isPerspectiveCamera) {
|
|
@@ -139,13 +194,14 @@ const Recorte: React.FC = () => {
|
|
|
139
194
|
|
|
140
195
|
/**
|
|
141
196
|
* Etiqueta pegada a un punto 3D: pastilla de color con una flecha que apunta al objeto. Sigue al objeto cuando la
|
|
142
|
-
* cámara se mueve. `escena` (1, 2...) la limita a esa escena; `en`/`hasta` son segundos desde que empieza
|
|
197
|
+
* cámara se mueve. `escena` (1, 2...) la limita a esa escena; `en`/`hasta` son segundos desde que empieza, o una
|
|
198
|
+
* palabra (`en: 'memoria'`) si el Lienzo3D recibe `cuando`: así sale junto con el objeto que llega en esa palabra.
|
|
143
199
|
*/
|
|
144
200
|
export type Etiqueta3D = {
|
|
145
201
|
pos: V3;
|
|
146
202
|
texto: string;
|
|
147
203
|
escena?: number | number[];
|
|
148
|
-
en?: number;
|
|
204
|
+
en?: number | string;
|
|
149
205
|
hasta?: number;
|
|
150
206
|
color?: string;
|
|
151
207
|
colorTexto?: string;
|
|
@@ -157,12 +213,12 @@ export type Etiqueta3D = {
|
|
|
157
213
|
inclinar?: number;
|
|
158
214
|
};
|
|
159
215
|
|
|
160
|
-
const EtiquetasPantalla: React.FC<{etiquetas: Etiqueta3D[]; estado: Estado; escena: number; tEscena: number}> = ({etiquetas, estado, escena, tEscena}) => (
|
|
216
|
+
const EtiquetasPantalla: React.FC<{etiquetas: Etiqueta3D[]; estado: Estado; escena: number; tEscena: number; enDe: (en: Etiqueta3D['en']) => number}> = ({etiquetas, estado, escena, tEscena, enDe}) => (
|
|
161
217
|
<>
|
|
162
218
|
{etiquetas.map((e, i) => {
|
|
163
219
|
const escenas = e.escena == null ? null : ([] as number[]).concat(e.escena);
|
|
164
220
|
if (escenas && !escenas.includes(escena)) return null;
|
|
165
|
-
const t = tEscena - (e.en
|
|
221
|
+
const t = tEscena - enDe(e.en);
|
|
166
222
|
if (t < 0 || (e.hasta != null && tEscena > e.hasta)) return null;
|
|
167
223
|
const p = proyectar(estado, e.pos);
|
|
168
224
|
if (!p.visible) return null;
|
|
@@ -195,20 +251,87 @@ const EtiquetasPantalla: React.FC<{etiquetas: Etiqueta3D[]; estado: Estado; esce
|
|
|
195
251
|
</>
|
|
196
252
|
);
|
|
197
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Postproceso del lienzo (todo determinista: sin ruido ni nada que dependa del reloj).
|
|
256
|
+
* 'cine' foco en lo que mira la toma (el fondo se desenfoca), brillo en pantallas y luces, viñeta
|
|
257
|
+
* 'miniatura' desenfoque arriba y abajo (efecto maqueta / tilt-shift), brillo y viñeta
|
|
258
|
+
* 'suave' solo brillo y viñeta
|
|
259
|
+
* Con objeto ajustas cada efecto: {foco: true | número (rango de foco), brillo: 0..2, vinieta: 0..1, miniatura: 0..1}.
|
|
260
|
+
*/
|
|
261
|
+
export type Post3D = 'cine' | 'miniatura' | 'suave' | false | {foco?: boolean | number; brillo?: number; vinieta?: number; miniatura?: number};
|
|
262
|
+
const PRESETS_POST: Record<string, {foco?: boolean | number; brillo?: number; vinieta?: number; miniatura?: number}> = {
|
|
263
|
+
cine: {foco: true, brillo: 0.35, vinieta: 0.5},
|
|
264
|
+
miniatura: {miniatura: 0.55, brillo: 0.3, vinieta: 0.45},
|
|
265
|
+
suave: {brillo: 0.3, vinieta: 0.45},
|
|
266
|
+
};
|
|
267
|
+
const Post: React.FC<{post: Post3D; estado: Estado}> = ({post, estado}) => {
|
|
268
|
+
if (!post) return null;
|
|
269
|
+
const o = typeof post === 'string' ? PRESETS_POST[post] ?? {} : post;
|
|
270
|
+
const dist = Math.hypot(estado.pos[0] - estado.mirar[0], estado.pos[1] - estado.mirar[1], estado.pos[2] - estado.mirar[2]);
|
|
271
|
+
const rango = typeof o.foco === 'number' ? o.foco : Math.max(1.2, dist * 0.55);
|
|
272
|
+
const efectos: React.ReactElement[] = [];
|
|
273
|
+
if (o.foco) efectos.push(<DepthOfField key="f" target={estado.mirar} worldFocusRange={rango} bokehScale={4} resolutionScale={0.6} />);
|
|
274
|
+
if (o.miniatura) efectos.push(<TiltShift2 key="m" blur={0.12 * o.miniatura} taper={0.6} start={[0.5, 0.0]} end={[0.5, 1.0]} />);
|
|
275
|
+
if (o.brillo) efectos.push(<Bloom key="b" intensity={o.brillo} luminanceThreshold={0.85} luminanceSmoothing={0.2} mipmapBlur />);
|
|
276
|
+
if (o.vinieta) efectos.push(<Vignette key="v" offset={0.3} darkness={o.vinieta} />);
|
|
277
|
+
if (!efectos.length) return null;
|
|
278
|
+
return (
|
|
279
|
+
<>
|
|
280
|
+
<EffectComposer multisampling={4}>{efectos}</EffectComposer>
|
|
281
|
+
<RedibujarAlMontar />
|
|
282
|
+
</>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Remotion dibuja el primer cuadro de cada pestaña antes de que el compositor de efectos termine de montarse (sale
|
|
288
|
+
* negro: 1 cuadro por pestaña al inicio del video). Este componente detiene la captura (delayRender) hasta que el
|
|
289
|
+
* compositor ya dibujó dos veces con sus efectos montados; solo pasa al montar, no en cada cuadro.
|
|
290
|
+
*/
|
|
291
|
+
const RedibujarAlMontar: React.FC = () => {
|
|
292
|
+
const {advance} = useThree();
|
|
293
|
+
const [espera] = React.useState(() => delayRender('Esperando el postproceso 3D'));
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
let vivo = true;
|
|
296
|
+
const r1 = requestAnimationFrame(() => {
|
|
297
|
+
if (!vivo) return;
|
|
298
|
+
advance(performance.now());
|
|
299
|
+
requestAnimationFrame(() => {
|
|
300
|
+
if (!vivo) return;
|
|
301
|
+
advance(performance.now());
|
|
302
|
+
continueRender(espera);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
return () => {
|
|
306
|
+
vivo = false;
|
|
307
|
+
cancelAnimationFrame(r1);
|
|
308
|
+
continueRender(espera);
|
|
309
|
+
};
|
|
310
|
+
}, [advance, espera]);
|
|
311
|
+
return null;
|
|
312
|
+
};
|
|
313
|
+
|
|
198
314
|
/**
|
|
199
315
|
* El lienzo del mundo 3D (1080×1920). `luz`:
|
|
200
316
|
* 'dia' cielo claro, sol con sombras suaves (maquetas, cortes de terreno, low-poly)
|
|
201
317
|
* 'calida' cuarto oscuro con un foco cálido desde arriba y su cono visible (mesa, dinero, objetos)
|
|
202
318
|
* `hora` (0–24 o 'atardecer', 'noche'...) cambia el cielo y el sol de la luz 'dia'; anímala para pasar de día a noche.
|
|
203
319
|
* `etiquetas`: pastillas con flecha pegadas a puntos 3D (siguen al objeto con la cámara).
|
|
320
|
+
* 'noche' interior de noche: luz ambiente tenue y azulada; las luces las pone el set (lámpara, tele, neón)
|
|
321
|
+
* `post`: postproceso ('cine', 'miniatura', 'suave'); ver Post3D.
|
|
204
322
|
*/
|
|
205
323
|
export const Lienzo3D: React.FC<{
|
|
206
324
|
tomas: (Toma3D | undefined | null)[];
|
|
207
325
|
escena?: number;
|
|
208
326
|
tEscena?: number;
|
|
209
327
|
inicios?: number[];
|
|
210
|
-
luz?: 'dia' | 'calida';
|
|
328
|
+
luz?: 'dia' | 'calida' | 'noche';
|
|
211
329
|
hora?: number | string;
|
|
330
|
+
post?: Post3D;
|
|
331
|
+
/** La cámara avanza si algo le tapa el lente (cabeza, pared, objeto en primer plano). */
|
|
332
|
+
despejado?: boolean;
|
|
333
|
+
/** El `cuando` del componente: deja usar palabras en `en` de las etiquetas (sale cuando llega su objeto). */
|
|
334
|
+
cuando?: (en: unknown) => number;
|
|
212
335
|
fondo?: string;
|
|
213
336
|
niebla?: [number, number];
|
|
214
337
|
sol?: V3;
|
|
@@ -217,17 +340,17 @@ export const Lienzo3D: React.FC<{
|
|
|
217
340
|
/** Encima del 3D, en coordenadas de pantalla (texto, tarjetas). */
|
|
218
341
|
encima?: React.ReactNode;
|
|
219
342
|
children: React.ReactNode;
|
|
220
|
-
}> = ({tomas, escena = 1, tEscena = 0, inicios, luz = 'dia', hora, fondo, niebla, sol = [9, 15, 8], foco = [0, 10, 1.5], etiquetas, encima, children}) => {
|
|
343
|
+
}> = ({tomas, escena = 1, tEscena = 0, inicios, luz = 'dia', hora, fondo, niebla, sol = [9, 15, 8], foco = [0, 10, 1.5], etiquetas, encima, post = false, despejado = false, cuando, children}) => {
|
|
221
344
|
const estado = camara3D(tomas, escena, tEscena, inicios);
|
|
222
345
|
const cielo = cieloDeHora(hora ?? 12);
|
|
223
|
-
const color = fondo ?? (luz === 'dia' ? cielo.fondo : '#120A06');
|
|
346
|
+
const color = fondo ?? (luz === 'dia' ? cielo.fondo : luz === 'noche' ? '#0B0E1A' : '#120A06');
|
|
224
347
|
const posSol: V3 = [sol[0], sol[1] * cielo.altura + 1.5, sol[2]];
|
|
225
348
|
return (
|
|
226
349
|
<div style={{position: 'relative', width: 1080, height: 1920}}>
|
|
227
350
|
<ThreeCanvas width={1080} height={1920} shadows gl={{antialias: true}} camera={{fov: estado.fov, position: estado.pos, near: 0.1, far: 400}}>
|
|
228
351
|
<color attach="background" args={[color]} />
|
|
229
352
|
{niebla ? <fog attach="fog" args={[color, niebla[0], niebla[1]]} /> : null}
|
|
230
|
-
<Camara estado={estado} />
|
|
353
|
+
<Camara estado={estado} despejado={despejado} />
|
|
231
354
|
<Recorte />
|
|
232
355
|
{luz === 'dia' ? (
|
|
233
356
|
<>
|
|
@@ -235,6 +358,12 @@ export const Lienzo3D: React.FC<{
|
|
|
235
358
|
<hemisphereLight args={[cielo.arriba, cielo.abajo, cielo.fuerzaHemi]} />
|
|
236
359
|
<directionalLight position={posSol} color={cielo.sol} intensity={cielo.fuerzaSol} castShadow shadow-mapSize={[2048, 2048]} shadow-camera-left={-26} shadow-camera-right={26} shadow-camera-top={26} shadow-camera-bottom={-26} shadow-camera-far={90} shadow-bias={-0.0006} />
|
|
237
360
|
</>
|
|
361
|
+
) : luz === 'noche' ? (
|
|
362
|
+
<>
|
|
363
|
+
<Entorno fuerza={0.12} />
|
|
364
|
+
<ambientLight color="#3A4470" intensity={0.55} />
|
|
365
|
+
<hemisphereLight args={['#6A78B8', '#1A1424', 0.35]} />
|
|
366
|
+
</>
|
|
238
367
|
) : (
|
|
239
368
|
<>
|
|
240
369
|
<Entorno fuerza={0.22} />
|
|
@@ -248,8 +377,18 @@ export const Lienzo3D: React.FC<{
|
|
|
248
377
|
</>
|
|
249
378
|
)}
|
|
250
379
|
{children}
|
|
380
|
+
<Post post={post} estado={estado} />
|
|
251
381
|
</ThreeCanvas>
|
|
252
|
-
{etiquetas?.length ?
|
|
382
|
+
{etiquetas?.length ? (
|
|
383
|
+
<EtiquetasPantalla
|
|
384
|
+
etiquetas={etiquetas}
|
|
385
|
+
estado={estado}
|
|
386
|
+
escena={escena}
|
|
387
|
+
tEscena={tEscena}
|
|
388
|
+
// palabra → segundos dentro de la escena (+0.35: el objeto ya cayó cuando sale la pastilla)
|
|
389
|
+
enDe={(en) => (typeof en === 'string' && cuando ? cuando(en) + 0.35 - (inicios?.[escena - 1] ?? 0) : typeof en === 'number' ? en : 0.2)}
|
|
390
|
+
/>
|
|
391
|
+
) : null}
|
|
253
392
|
{encima}
|
|
254
393
|
</div>
|
|
255
394
|
);
|
package/remotion/motor/Video.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {FondoTema, Post, Textura, Vineta} from './fondo';
|
|
|
10
10
|
import {capasDe} from './plantillas';
|
|
11
11
|
import {Subtitulos} from './subtitulos';
|
|
12
12
|
import {mezclar, temaDe, type Tema} from './tema';
|
|
13
|
-
import {azar, cuando, partesResaltadas, type CtxEscena} from './util';
|
|
13
|
+
import {azar, cuando, norm, partesResaltadas, type CtxEscena} from './util';
|
|
14
14
|
|
|
15
15
|
const TRANSICION = 0.35;
|
|
16
16
|
// estilos con fondo claro (cielo, papel): los modos de imagen invierten para que el fondo quede oscuro
|
|
@@ -105,13 +105,42 @@ export function camaraMundo(d: Datos, si: number, t: number): Toma {
|
|
|
105
105
|
return {x: lerp(o.x, actual.x, kv), y: lerp(o.y, actual.y, kv), zoom: Math.exp(lerp(Math.log(o.zoom), Math.log(actual.zoom), kv)), giro: lerp(o.giro, actual.giro, kv)};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Cortes de cámara dentro de una escena (segundos): en la primera palabra clave, si la escena dura más de 3,5 s y no
|
|
110
|
+
* fija `plano`; en escenas de más de 7 s sin claves, a la mitad. `cortes: false` en la escena los apaga; `cortes: [2.1]` los fija.
|
|
111
|
+
*/
|
|
112
|
+
function cortesDe(e: Escena): number[] {
|
|
113
|
+
if (e.p.cortes === false || e.tipo === 'cta') return [];
|
|
114
|
+
if (Array.isArray(e.p.cortes)) return e.p.cortes.filter((c: unknown) => typeof c === 'number');
|
|
115
|
+
if (e.p.plano || e.dur < 3.5) return [];
|
|
116
|
+
const claves: string[] = (e.p.claves ?? []).map((c: string) => norm(c)).filter(Boolean);
|
|
117
|
+
const ok = (c: number) => c > 1.4 && c < e.dur - 1.1;
|
|
118
|
+
for (const [w, a] of e.palabras ?? []) if (claves.some((c) => norm(w).startsWith(c)) && ok(a)) return [a];
|
|
119
|
+
return e.dur > 7 ? [e.dur / 2] : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Golpe de cámara (énfasis): cuando cae un rótulo `sello` o `pixel`, todo el cuadro da un pequeño zoom con sacudida
|
|
124
|
+
* que se apaga en ~0,35 s. `golpe: false` en la escena (o `tema.movimiento.golpes: false`) lo quita.
|
|
125
|
+
*/
|
|
126
|
+
function golpeDe(e: Escena, t: number, tema: Tema): string | undefined {
|
|
127
|
+
const est = e.p.rotuloEstilo;
|
|
128
|
+
if (!e.p.rotulo || e.p.golpe === false || (tema.movimiento as any)?.golpes === false || (est !== 'sello' && est !== 'pixel' && e.p.golpe !== true)) return undefined;
|
|
129
|
+
const ctx: CtxEscena = {t, dur: e.dur, habla: e.habla, palabras: e.palabras ?? [], tema, semilla: ''};
|
|
130
|
+
const t0 = cuando(e.p.rotuloEn, ctx, 0.12) + (est === 'sello' ? 0.12 : 0);
|
|
131
|
+
const x = t - t0;
|
|
132
|
+
if (x < 0 || x > 0.5) return undefined;
|
|
133
|
+
const k = Math.exp(-x * 9);
|
|
134
|
+
return `scale(${1 + 0.045 * k}) translate(${Math.sin(x * 70) * 9 * k}px, ${Math.cos(x * 55) * 6 * k}px)`;
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
/** El mundo del video: capas que persisten de escena en escena, vistas por la cámara. Las capas `enMundo` de la escena actual viven aquí. */
|
|
109
138
|
const Mundo: React.FC<{d: Datos; si: number; t: number; T: number; tema: Tema}> = ({d, si, t, T, tema}) => {
|
|
110
139
|
const marca = useMarca();
|
|
111
140
|
const e = d.escenas[si];
|
|
112
141
|
const cam = camaraMundo(d, si, t);
|
|
113
142
|
const palabras = d.escenas.flatMap((x) => (x.palabras ?? []).map(([w, a, b]) => [w, a + x.t0, b + x.t0] as [string, number, number]));
|
|
114
|
-
const ctxMundo: CtxEscena = {t: T, dur: d.duracion, habla: [0, d.duracion], palabras, tema, semilla: `${d.id}-mundo`, inicios: d.escenas.map((x) => x.t0), escena: si + 1, tEscena: t, camara: cam};
|
|
143
|
+
const ctxMundo: CtxEscena = {t: T, dur: d.duracion, habla: [0, d.duracion], palabras, tema, semilla: `${d.id}-mundo`, inicios: d.escenas.map((x) => x.t0), escena: si + 1, tEscena: t, camara: cam, escenasInfo: d.escenas.map((x) => ({say: x.p.say, plano: x.p.plano, capas: x.tipo === 'cta' || (x.p.capas ?? []).some((c: any) => !c.enMundo), cortes: cortesDe(x)}))};
|
|
115
144
|
const temaEsc = e.p.tema ? mezclar(tema, e.p.tema) : tema;
|
|
116
145
|
const ctxEsc: CtxEscena = {t, dur: e.dur, habla: e.habla, palabras: e.palabras ?? [], tema: temaEsc, semilla: `${d.id}-${e.t0}`};
|
|
117
146
|
const propias = capasDe(e.tipo, e.p, temaEsc, marca).filter((c: any) => c.enMundo);
|
|
@@ -432,6 +461,7 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
|
|
|
432
461
|
}
|
|
433
462
|
return (
|
|
434
463
|
<AbsoluteFill style={{background: '#000', overflow: 'hidden'}}>
|
|
464
|
+
<AbsoluteFill style={{transform: golpeDe(e, t, tema)}}>
|
|
435
465
|
<Modo d={d} si={si} t={t} T={T} claro={LOOKS_CLAROS.includes(d.estilo ?? '') || tema.colores.texto !== '#FFFFFF'}>
|
|
436
466
|
{conMundo && enTransicion && prev ? (
|
|
437
467
|
<AbsoluteFill style={estiloPrevio}>
|
|
@@ -452,6 +482,7 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
|
|
|
452
482
|
<EscenaVista e={e} t={t} tema={tema} semilla={semilla} sinFondo={conMundo} />
|
|
453
483
|
</AbsoluteFill>
|
|
454
484
|
</Modo>
|
|
485
|
+
</AbsoluteFill>
|
|
455
486
|
{destello > 0 ? <AbsoluteFill style={{background: '#fff', opacity: destello}} /> : null}
|
|
456
487
|
<Post tema={tema} />
|
|
457
488
|
{/* la textura va quieta, fuera de la cámara: si se moviera con el zoom, cada cuadro sería distinto y el video pesaría 10 veces más */}
|
package/remotion/motor/capas.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {Particulas} from './fondo';
|
|
|
12
12
|
import {Escena3D, Modelo3D} from './modelo3d';
|
|
13
13
|
import {Escenario} from './escenarios';
|
|
14
14
|
import {componente, nombresComponentes} from './registro';
|
|
15
|
+
import {Ambiente, reaccionesAuto} from '../kit/ambientes';
|
|
15
16
|
import {Barra, Chat, Estado, Globo, ImagenCambia, Medidor, Noticia, Onda, Seguir, Votacion} from './widgets';
|
|
16
17
|
|
|
17
18
|
export type Capa = Record<string, any> & {tipo: string};
|
|
@@ -272,6 +273,29 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
272
273
|
return <Seguir c={c} ctx={ctx} />;
|
|
273
274
|
case 'modelo':
|
|
274
275
|
return <Modelo3D c={c} ctx={ctx} />;
|
|
276
|
+
case 'ambiente': {
|
|
277
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
278
|
+
const info = ctx.escenasInfo;
|
|
279
|
+
const enMundo = ctx.escena != null && !!info;
|
|
280
|
+
const reacciones = c.reacciones ?? (c.persona && info && c.reaccionar !== false ? reaccionesAuto(info, ctx.inicios ?? []) : []);
|
|
281
|
+
return (
|
|
282
|
+
<Ambiente
|
|
283
|
+
nombre={c.nombre}
|
|
284
|
+
t={ctx.t - t0}
|
|
285
|
+
escena={enMundo ? ctx.escena : 1}
|
|
286
|
+
tEscena={enMundo ? ctx.tEscena : ctx.t}
|
|
287
|
+
inicios={enMundo ? ctx.inicios : [0]}
|
|
288
|
+
planos={c.planos ?? (enMundo ? info!.map((e) => e.plano as any) : [c.plano])}
|
|
289
|
+
conCapas={enMundo ? info!.map((e) => !!e.capas) : []}
|
|
290
|
+
cortes={enMundo && c.cortes !== false ? info!.map((e) => e.cortes ?? []) : []}
|
|
291
|
+
persona={c.persona}
|
|
292
|
+
reacciones={reacciones.map((r: any) => ({...r, en: r.en - t0}))}
|
|
293
|
+
post={c.post}
|
|
294
|
+
etiquetas={c.etiquetas}
|
|
295
|
+
cuando={(en: unknown) => cuando(en, ctx) - t0}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
275
299
|
case 'componente': {
|
|
276
300
|
const Comp = componente(c.nombre);
|
|
277
301
|
if (!Comp) throw new Error(`No hay un componente llamado "${c.nombre}". Regístralo en morfeo.config.mjs → componentes. Disponibles: ${nombresComponentes().join(', ') || 'ninguno'}`);
|
package/remotion/motor/util.ts
CHANGED
|
@@ -13,9 +13,10 @@ export type CtxEscena = {
|
|
|
13
13
|
escena?: number; // en el `mundo`: número de la escena actual (1 = la primera)
|
|
14
14
|
camara?: {x: number; y: number; zoom: number; giro: number}; // en el `mundo`: la cámara de este cuadro (para paralaje)
|
|
15
15
|
tEscena?: number; // en el `mundo`: segundos desde que empezó la escena actual
|
|
16
|
+
escenasInfo?: {say?: string; plano?: unknown; capas?: boolean; cortes?: number[]}[]; // en el `mundo`: lo que dice cada escena y su `plano` (ambientes)
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
const norm = (s: string) =>
|
|
19
|
+
export const norm = (s: string) =>
|
|
19
20
|
s
|
|
20
21
|
.normalize('NFD')
|
|
21
22
|
.replace(/[̀-ͯ]/g, '')
|