morfeo 0.1.2 → 0.2.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/bin/morfeo.mjs +35 -1
- package/index.d.ts +59 -7
- package/lib/config.mjs +11 -8
- package/lib/diccionario.mjs +5 -2
- package/lib/estructura.mjs +104 -0
- package/lib/guion.mjs +25 -8
- package/lib/hoja.mjs +98 -0
- package/lib/imagenes.mjs +2 -1
- package/lib/personajes.mjs +1 -1
- package/lib/recursos.mjs +24 -12
- package/lib/render.mjs +9 -2
- package/package.json +2 -1
- package/python/preparar.py +1 -1
- package/remotion/estilo.ts +13 -0
- package/remotion/kit/ilustrado.tsx +220 -0
- package/remotion/kit/index.ts +6 -0
- package/remotion/kit/tres.tsx +328 -0
- package/remotion/motor/Video.tsx +171 -20
- package/remotion/motor/anotaciones.tsx +296 -0
- package/remotion/motor/capas.tsx +50 -27
- package/remotion/motor/fondo.tsx +56 -2
- package/remotion/motor/modelo3d.tsx +47 -5
- package/remotion/motor/plantillas.tsx +26 -1
- package/remotion/motor/subtitulos.tsx +159 -36
- package/remotion/motor/tema.ts +46 -17
- package/remotion/motor/util.ts +3 -0
- package/remotion/tipos.ts +2 -0
package/lib/recursos.mjs
CHANGED
|
@@ -39,6 +39,8 @@ export const VARIANTES_2D = {
|
|
|
39
39
|
brutal: ['withicons-brutal'],
|
|
40
40
|
gothic: ['withicons-gothic'],
|
|
41
41
|
linea: ['ph', 'mdi', 'iconoir', 'tabler', 'lucide'],
|
|
42
|
+
// ilustración plana con muchísimos animales y objetos: último respaldo 2D antes de pasar a 3D
|
|
43
|
+
plano: ['fluent-emoji-flat', 'noto'],
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
async function pedirJSON(url, opciones = {}) {
|
|
@@ -252,19 +254,26 @@ export async function resolverRecursos(video, assetsPublico, log = console.log)
|
|
|
252
254
|
const {tipo, buscar, estilo = '3d', variante, fuente, indice: n = 0, color, ...resto} = c;
|
|
253
255
|
if (!buscar) throw new ErrorMorfeo('Una capa {tipo: "recurso"} necesita `buscar` (por ejemplo, buscar: "tubería")');
|
|
254
256
|
let res = await buscarRecursos(buscar, {estilo, variante, fuente});
|
|
255
|
-
// sin resultados
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
// sin resultados: primero se prueba el mismo estilo con otra línea gráfica (2d glass → 2d color → otras
|
|
258
|
+
// variantes), y luego los otros estilos. Así un 2D no termina siendo un modelo realista si hay otro 2D.
|
|
259
|
+
if (!res.length && !fuente) {
|
|
260
|
+
const intentos =
|
|
261
|
+
estilo === '2d'
|
|
262
|
+
? [...Object.keys(VARIANTES_2D).filter((v) => v !== (variante ?? 'color') && v !== 'linea').map((v) => ['2d', v]), ['3d'], ['realista']]
|
|
263
|
+
: [...['3d', 'realista'].filter((e) => e !== estilo).map((e) => [e]), ['2d', variante]];
|
|
264
|
+
for (const [otro, otraVariante] of intentos) {
|
|
265
|
+
res = await buscarRecursos(buscar, {estilo: otro, variante: otraVariante});
|
|
259
266
|
if (res.length) {
|
|
260
|
-
log(` "${buscar}" no está en ${estilo}; uso ${otro}`);
|
|
267
|
+
log(` "${buscar}" no está en ${estilo}${variante ? ' ' + variante : ''}; uso ${otro}${otraVariante && otro === '2d' ? ' ' + otraVariante : ''}`);
|
|
261
268
|
break;
|
|
262
269
|
}
|
|
263
270
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
);
|
|
271
|
+
}
|
|
272
|
+
// nada en ninguna biblioteca: se avisa y la capa se omite (el video se hace igual)
|
|
273
|
+
if (!res.length) {
|
|
274
|
+
log(` ⚠ No encontré "${buscar}" en ninguna biblioteca; omito esa capa. Prueba un sinónimo o escríbelo en inglés: npx morfeo recursos buscar "${buscar}"`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
268
277
|
const r = res[Math.min(n, res.length - 1)];
|
|
269
278
|
const d = await bajarRecurso(r, {color});
|
|
270
279
|
creditos.add(d.credito);
|
|
@@ -281,19 +290,22 @@ export async function resolverRecursos(video, assetsPublico, log = console.log)
|
|
|
281
290
|
log(` recurso "${buscar}" → ${r.nombre} (${r.fuente})`);
|
|
282
291
|
return r.tipo === 'modelo'
|
|
283
292
|
? // modelos externos: cámara más cerca y menos luz ambiente para que no se vean chicos ni deslavados
|
|
284
|
-
{tipo: 'modelo', src, ancho: 620, alto: 620, girar3d: [0, 30, 0], distancia: 4.3, camaraY: 0.9, luzAmbiente: 0.25, reflejos: 0.45, luz: 1.6, ...resto}
|
|
293
|
+
{tipo: 'modelo', src, ancho: 620, alto: 620, girar3d: [0, 30, 0], levantar: true, distancia: 4.3, camaraY: 0.9, luzAmbiente: 0.25, reflejos: 0.45, luz: 1.6, ...resto}
|
|
285
294
|
: {tipo: 'imagen', src, ancho: 460, sombra: true, ...resto};
|
|
286
295
|
void base;
|
|
287
296
|
};
|
|
288
297
|
const recorrer = async (x) => {
|
|
289
|
-
if (Array.isArray(x)) return Promise.all(x.map(recorrer));
|
|
298
|
+
if (Array.isArray(x)) return (await Promise.all(x.map(recorrer))).filter((v) => v !== null);
|
|
290
299
|
if (!x || typeof x !== 'object') return x;
|
|
291
300
|
if (x.tipo === 'recurso') return resolver(x);
|
|
292
301
|
const out = {};
|
|
293
302
|
for (const [k, v] of Object.entries(x)) out[k] = await recorrer(v);
|
|
303
|
+
// un grupo cuya capa repetida no se encontró tampoco se dibuja
|
|
304
|
+
if (x.tipo === 'grupo' && x.capa && out.capa === null) return null;
|
|
294
305
|
return out;
|
|
295
306
|
};
|
|
296
307
|
const scenes = await recorrer(video.scenes);
|
|
297
308
|
const hud = await recorrer(video.hud ?? []);
|
|
298
|
-
|
|
309
|
+
const mundo = await recorrer(video.mundo ?? []);
|
|
310
|
+
return {video: {...video, scenes, hud, mundo}, creditos: [...creditos]};
|
|
299
311
|
}
|
package/lib/render.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {ponerPoses, prepararPersonajes} from './personajes.mjs';
|
|
|
13
13
|
import {resolverRecursos} from './recursos.mjs';
|
|
14
14
|
import {sintetizarVoces} from './voces.mjs';
|
|
15
15
|
import {transcribir, whisperListo} from './whisper.mjs';
|
|
16
|
+
import {consejosEnTexto, revisarEstructura} from './estructura.mjs';
|
|
16
17
|
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
|
|
@@ -121,7 +122,7 @@ function prepararPublico(p) {
|
|
|
121
122
|
return {publico, marca: {...p.marca, logo, cta: undefined}, extras: {fuentes, estilos: p.estilos}};
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
const datosDe = (publico, id) => {
|
|
125
|
+
export const datosDe = (publico, id) => {
|
|
125
126
|
const f = path.join(publico, 'v', id, 'datos.json');
|
|
126
127
|
return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null;
|
|
127
128
|
};
|
|
@@ -201,6 +202,12 @@ export async function render({proyecto = process.cwd(), ids, todos = false, log
|
|
|
201
202
|
}
|
|
202
203
|
if (!whisperListo()) log('(Whisper no está instalado: subtítulos repartidos por frase. Para tiempos exactos: npx morfeo instalar --whisper)');
|
|
203
204
|
const {publico, marca, extras, datos} = await preparar({proyecto, ids: lista, log, _p: p});
|
|
205
|
+
// estructura: consejos medidos en videos virales (no detienen el render; se apagan con render.consejos: false)
|
|
206
|
+
if (p.render.consejos !== false)
|
|
207
|
+
for (const id of lista) {
|
|
208
|
+
const c = revisarEstructura(p.videos[id], datos[id]);
|
|
209
|
+
if (c.length) log(` ${id}: ${c.length} consejo(s) de estructura\n${consejosEnTexto(c)}`);
|
|
210
|
+
}
|
|
204
211
|
|
|
205
212
|
log('Empaquetando la animación...');
|
|
206
213
|
const {bundle} = await import('@remotion/bundler');
|
|
@@ -213,7 +220,7 @@ export async function render({proyecto = process.cwd(), ids, todos = false, log
|
|
|
213
220
|
const hechos = [];
|
|
214
221
|
// cuadros en paralelo: con GPU aguanta más; por software cada pestaña come mucha CPU y memoria
|
|
215
222
|
const nucleos = os.cpus().length;
|
|
216
|
-
const concurrenciaBase = p.render.concurrencia ?? (p.render.gl === 'angle' ? Math.max(2, Math.min(
|
|
223
|
+
const concurrenciaBase = p.render.concurrencia ?? (p.render.gl === 'angle' ? Math.max(2, Math.min(10, Math.floor(nucleos * 0.75))) : Math.max(1, Math.min(3, Math.floor(nucleos / 4))));
|
|
217
224
|
const sinTTY = !process.stdout.isTTY;
|
|
218
225
|
for (const [i, id] of lista.entries()) {
|
|
219
226
|
const t = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morfeo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Genera videos verticales animados (Remotion + Three.js) con voz, música y subtítulos, desde un guion en código o escrito por IA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"types": "./index.d.ts",
|
|
33
33
|
"default": "./lib/index.mjs"
|
|
34
34
|
},
|
|
35
|
+
"./kit": "./remotion/kit/index.ts",
|
|
35
36
|
"./package.json": "./package.json",
|
|
36
37
|
"./remotion/*": "./remotion/*"
|
|
37
38
|
},
|
package/python/preparar.py
CHANGED
|
@@ -255,7 +255,7 @@ def preparar(job):
|
|
|
255
255
|
subs = [dict(a=round(c[0][1], 3), b=round(c[-1][2], 3), w=[[x[0].upper(), round(x[1], 3), round(x[2], 3)] for x in c])
|
|
256
256
|
for ws in WORDS for c in trozos(ws)]
|
|
257
257
|
datos = dict(id=vid, titulo=v.get("title", vid), indice=job.get("indice", 0), duracion=round(sum(durs), 3),
|
|
258
|
-
estilo=v.get("estilo", "datos-curiosos"), tema=v.get("tema") or {}, hud=v.get("hud") or [],
|
|
258
|
+
estilo=v.get("estilo", "datos-curiosos"), tema=v.get("tema") or {}, hud=v.get("hud") or [], mundo=v.get("mundo") or [], serie=v.get("serie"),
|
|
259
259
|
audio=f"v/{vid}/mix.wav", whisper=reales, escenas=escenas, subtitulos=subs)
|
|
260
260
|
json.dump(datos, open(os.path.join(outdir, "datos.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
261
261
|
print(f" tiempos por palabra: {'Whisper' if reales else 'reparto por frase'} ({reales}/{len(TIM)} escenas)", flush=True)
|
package/remotion/estilo.ts
CHANGED
|
@@ -54,6 +54,19 @@ export const backOut = (x: number, s = 1.9) => {
|
|
|
54
54
|
const c = clamp(x) - 1;
|
|
55
55
|
return c * c * ((s + 1) * c + s) + 1;
|
|
56
56
|
};
|
|
57
|
+
/** Resorte amortiguado normalizado a 0..1: llega rápido, se pasa un poco y se asienta. `rebote` 0 = sin pasarse. */
|
|
58
|
+
export const resorte = (x: number, rebote = 0.35) => {
|
|
59
|
+
const c = clamp(x);
|
|
60
|
+
if (c >= 1) return 1;
|
|
61
|
+
return 1 - Math.exp(-7.5 * c) * Math.cos(c * Math.PI * (1.2 + rebote * 3.2));
|
|
62
|
+
};
|
|
63
|
+
/** Frenado fuerte: casi todo el recorrido en el primer tercio (movimiento "seco", editorial). */
|
|
64
|
+
export const easeOutExpo = (x: number) => {
|
|
65
|
+
const c = clamp(x);
|
|
66
|
+
return c >= 1 ? 1 : 1 - 2 ** (-10 * c);
|
|
67
|
+
};
|
|
68
|
+
/** Ruido suave determinista en -1..1 (suma de senos con fases distintas): temblores y derivas de cámara. */
|
|
69
|
+
export const ruido = (t: number, semilla = 0) => (Math.sin(t * 1.7 + semilla) + Math.sin(t * 2.9 + semilla * 2.3) * 0.6 + Math.sin(t * 5.3 + semilla * 4.1) * 0.3) / 1.9;
|
|
57
70
|
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
58
71
|
export const mezclaColor = (c1: string, c2: string, t: number) => {
|
|
59
72
|
const p = (c: string) => [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16));
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Kit ilustrado 2D: piezas SVG para armar el mundo de un video (bosque, noche, campo) en un componente propio.
|
|
2
|
+
// Todas van DENTRO de un <svg> y dependen solo de sus props (y de `t` en segundos): nada se anima solo.
|
|
3
|
+
//
|
|
4
|
+
// import {Filtros, Cielo, Estrellas, Luna, Montanas, Pinos, Suelo, Pasto, Luciernagas, Paralaje, Texturizado} from 'morfeo/kit';
|
|
5
|
+
//
|
|
6
|
+
// Un mundo se dibuja más grande que el cuadro (p. ej. 2160×1920) y la cámara lo recorre con la `toma` de cada escena.
|
|
7
|
+
// `Paralaje` mueve cada plano según la cámara: lo lejano casi no se mueve y lo cercano se mueve de más.
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
export const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
|
11
|
+
export const suave = (x: number) => {
|
|
12
|
+
const c = clamp01(x);
|
|
13
|
+
return c * c * (3 - 2 * c);
|
|
14
|
+
};
|
|
15
|
+
/** Número pseudoaleatorio estable en [0, 1) a partir de un entero. */
|
|
16
|
+
export const azarN = (n: number) => {
|
|
17
|
+
const x = Math.sin(n * 127.1 + 11.7) * 43758.5453;
|
|
18
|
+
return x - Math.floor(x);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Camara = {x: number; y: number; zoom: number; giro: number};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filtros compartidos (ponlo una vez dentro del <svg>):
|
|
25
|
+
* url(#kit-grano) motas oscuras recortadas a la figura (papel, pelaje, tierra)
|
|
26
|
+
* url(#kit-brillo) resplandor suave alrededor (luna, luciérnagas, luces)
|
|
27
|
+
* url(#kit-lejos) desenfoque leve para lo que queda fuera de foco
|
|
28
|
+
*/
|
|
29
|
+
export const Filtros: React.FC<{grano?: number}> = ({grano = 0.55}) => (
|
|
30
|
+
<defs>
|
|
31
|
+
<filter id="kit-grano" x="-5%" y="-5%" width="110%" height="110%">
|
|
32
|
+
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed="7" result="ruido" />
|
|
33
|
+
<feColorMatrix in="ruido" type="matrix" values={`0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ${grano} 0 0 0 ${-grano * 0.32}`} result="motas" />
|
|
34
|
+
<feComposite in="motas" in2="SourceAlpha" operator="in" result="recorte" />
|
|
35
|
+
<feMerge>
|
|
36
|
+
<feMergeNode in="SourceGraphic" />
|
|
37
|
+
<feMergeNode in="recorte" />
|
|
38
|
+
</feMerge>
|
|
39
|
+
</filter>
|
|
40
|
+
<filter id="kit-brillo" x="-60%" y="-60%" width="220%" height="220%">
|
|
41
|
+
<feGaussianBlur stdDeviation="14" result="b" />
|
|
42
|
+
<feMerge>
|
|
43
|
+
<feMergeNode in="b" />
|
|
44
|
+
<feMergeNode in="SourceGraphic" />
|
|
45
|
+
</feMerge>
|
|
46
|
+
</filter>
|
|
47
|
+
<filter id="kit-lejos" x="-10%" y="-10%" width="120%" height="120%">
|
|
48
|
+
<feGaussianBlur stdDeviation="5" />
|
|
49
|
+
</filter>
|
|
50
|
+
</defs>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** Grupo con grano recortado a sus figuras. Úsalo en personajes y objetos, no en fondos enormes (cuesta render). */
|
|
54
|
+
export const Texturizado: React.FC<{children: React.ReactNode}> = ({children}) => <g filter="url(#kit-grano)">{children}</g>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Plano con paralaje. `profundidad`: 0 = pegado al mundo, 1 = fijo en pantalla (infinitamente lejos),
|
|
58
|
+
* negativo = primer plano (se mueve más que el mundo). Sin `camara` no hace nada.
|
|
59
|
+
*/
|
|
60
|
+
export const Paralaje: React.FC<{camara?: Camara; profundidad: number; children: React.ReactNode}> = ({camara, profundidad, children}) => {
|
|
61
|
+
if (!camara || !profundidad) return <g>{children}</g>;
|
|
62
|
+
return <g transform={`translate(${(camara.x - 540) * profundidad} ${(camara.y - 960) * profundidad * 0.6})`}>{children}</g>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Cielo en degradado vertical. Dale margen de sobra (x0, x1, y0) para que la cámara nunca vea el borde. */
|
|
66
|
+
export const Cielo: React.FC<{id?: string; colores: string[]; x0?: number; x1?: number; y0?: number; y1?: number}> = ({id = 'kit-cielo', colores, x0 = -1200, x1 = 3400, y0 = -900, y1 = 1920}) => (
|
|
67
|
+
<>
|
|
68
|
+
<defs>
|
|
69
|
+
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
70
|
+
{colores.map((c, i) => (
|
|
71
|
+
<stop key={i} offset={i / Math.max(1, colores.length - 1)} stopColor={c} />
|
|
72
|
+
))}
|
|
73
|
+
</linearGradient>
|
|
74
|
+
</defs>
|
|
75
|
+
<rect x={x0} y={y0} width={x1 - x0} height={y1 - y0} fill={`url(#${id})`} />
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/** Estrellas que titilan. */
|
|
80
|
+
export const Estrellas: React.FC<{t: number; n?: number; x0?: number; x1?: number; y0?: number; y1?: number; semilla?: number}> = ({t, n = 90, x0 = -400, x1 = 2600, y0 = -300, y1 = 1100, semilla = 0}) => (
|
|
81
|
+
<g>
|
|
82
|
+
{Array.from({length: n}, (_, i) => {
|
|
83
|
+
const s = semilla + i;
|
|
84
|
+
const o = 0.3 + 0.7 * Math.abs(Math.sin(t * (0.8 + azarN(s + 300) * 1.8) + i));
|
|
85
|
+
return <circle key={i} cx={x0 + azarN(s) * (x1 - x0)} cy={y0 + azarN(s + 100) * (y1 - y0)} r={1.2 + azarN(s + 200) * 2.6} fill="#fff" opacity={o} />;
|
|
86
|
+
})}
|
|
87
|
+
</g>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/** Luna con halo y cráteres; con `cara` tiene ojos y boca, y con `canta` (0..1) abre la boca al ritmo. */
|
|
91
|
+
export const Luna: React.FC<{x: number; y: number; r?: number; t?: number; cara?: boolean; canta?: number}> = ({x, y, r = 110, t = 0, cara, canta = 0}) => {
|
|
92
|
+
const u = r / 110;
|
|
93
|
+
const boca = canta > 0 ? (6 + Math.abs(Math.sin(t * 9)) * 14) * canta : 0;
|
|
94
|
+
return (
|
|
95
|
+
<g transform={`translate(${x} ${y}) scale(${u})`}>
|
|
96
|
+
<defs>
|
|
97
|
+
<radialGradient id="kit-halo">
|
|
98
|
+
<stop offset="0" stopColor="rgba(255,240,200,0.6)" />
|
|
99
|
+
<stop offset="1" stopColor="rgba(255,240,200,0)" />
|
|
100
|
+
</radialGradient>
|
|
101
|
+
</defs>
|
|
102
|
+
<circle r={300} fill="url(#kit-halo)" />
|
|
103
|
+
<g filter="url(#kit-grano)">
|
|
104
|
+
<circle r={110} fill="#FFF4D6" />
|
|
105
|
+
<circle cx={-34} cy={-26} r={16} fill="rgba(210,190,150,0.45)" />
|
|
106
|
+
<circle cx={38} cy={30} r={22} fill="rgba(210,190,150,0.4)" />
|
|
107
|
+
<circle cx={18} cy={-48} r={9} fill="rgba(210,190,150,0.4)" />
|
|
108
|
+
</g>
|
|
109
|
+
{cara ? (
|
|
110
|
+
<g>
|
|
111
|
+
{canta > 0.5 ? (
|
|
112
|
+
<>
|
|
113
|
+
<path d="M-42 -8 Q-30 -18 -18 -8" stroke="#6B5236" strokeWidth={6} fill="none" strokeLinecap="round" />
|
|
114
|
+
<path d="M18 -8 Q30 -18 42 -8" stroke="#6B5236" strokeWidth={6} fill="none" strokeLinecap="round" />
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
<>
|
|
118
|
+
<circle cx={-30} cy={-10} r={8} fill="#6B5236" />
|
|
119
|
+
<circle cx={30} cy={-10} r={8} fill="#6B5236" />
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
<ellipse cx={0} cy={34} rx={canta > 0.5 ? 14 : 16} ry={canta > 0.5 ? boca : 4} fill={canta > 0.5 ? '#7A3B3B' : '#6B5236'} />
|
|
123
|
+
<circle cx={-58} cy={18} r={12} fill="rgba(255,150,150,0.45)" />
|
|
124
|
+
<circle cx={58} cy={18} r={12} fill="rgba(255,150,150,0.45)" />
|
|
125
|
+
</g>
|
|
126
|
+
) : null}
|
|
127
|
+
</g>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Cordillera: una silueta de picos irregulares entre x0 y x1, con la base en `base`. */
|
|
132
|
+
export const Montanas: React.FC<{base: number; alto: number; x0?: number; x1?: number; picos?: number; semilla?: number; color: string}> = ({base, alto, x0 = -600, x1 = 2800, picos = 9, semilla = 0, color}) => {
|
|
133
|
+
let d = `M${x0} ${base + 400} L${x0} ${base}`;
|
|
134
|
+
for (let i = 0; i <= picos; i++) {
|
|
135
|
+
const x = x0 + ((x1 - x0) * i) / picos;
|
|
136
|
+
const h = alto * (0.45 + azarN(semilla + i * 5) * 0.55);
|
|
137
|
+
d += ` L${x - (x1 - x0) / picos / 2} ${base - h} L${x} ${base - h * 0.35}`;
|
|
138
|
+
}
|
|
139
|
+
return <path d={`${d} L${x1} ${base + 400} Z`} fill={color} />;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Fila de pinos como un solo trazado (liviano). */
|
|
143
|
+
export const Pinos: React.FC<{base: number; alto: number; ancho: number; n: number; x0?: number; x1?: number; semilla?: number; color: string}> = ({base, alto, ancho, n, x0 = -500, x1 = 2700, semilla = 0, color}) => {
|
|
144
|
+
let d = '';
|
|
145
|
+
for (let i = 0; i < n; i++) {
|
|
146
|
+
const cx = x0 + ((x1 - x0) * i) / Math.max(1, n - 1) + (azarN(semilla + i) - 0.5) * ancho * 0.6;
|
|
147
|
+
const h = alto * (0.75 + azarN(semilla + i * 3) * 0.5);
|
|
148
|
+
const w = ancho * (0.8 + azarN(semilla + i * 7) * 0.4);
|
|
149
|
+
for (let p = 0; p < 3; p++) {
|
|
150
|
+
const top = base - h + p * h * 0.26;
|
|
151
|
+
const bot = base - h * 0.32 + p * h * 0.22;
|
|
152
|
+
const ww = w * (0.55 + p * 0.25);
|
|
153
|
+
d += `M${cx} ${top} L${cx + ww / 2} ${bot} L${cx - ww / 2} ${bot} Z `;
|
|
154
|
+
}
|
|
155
|
+
d += `M${cx - w * 0.06} ${base - h * 0.12} h${w * 0.12} v${h * 0.16} h${-w * 0.12} Z `;
|
|
156
|
+
}
|
|
157
|
+
return <path d={d} fill={color} />;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Suelo ondulado: una loma suave de x0 a x1 que baja hasta `fondo`. `y(x)` da la altura del suelo en un punto. */
|
|
161
|
+
export const alturaSuelo = (x: number, y: number, onda = 28) => y + Math.sin(x / 310) * onda + Math.sin(x / 127 + 1.3) * onda * 0.3;
|
|
162
|
+
export const Suelo: React.FC<{y: number; x0?: number; x1?: number; fondo?: number; onda?: number; id?: string; colores: [string, string]}> = ({y, x0 = -800, x1 = 3000, fondo = 2600, onda = 28, id = 'kit-suelo', colores}) => {
|
|
163
|
+
let d = `M${x0} ${fondo}`;
|
|
164
|
+
for (let x = x0; x <= x1; x += 40) d += ` L${x} ${alturaSuelo(x, y, onda)}`;
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<defs>
|
|
168
|
+
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
169
|
+
<stop offset="0" stopColor={colores[0]} />
|
|
170
|
+
<stop offset="0.55" stopColor={colores[1]} />
|
|
171
|
+
</linearGradient>
|
|
172
|
+
</defs>
|
|
173
|
+
<path d={`${d} L${x1} ${fondo} Z`} fill={`url(#${id})`} />
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Briznas de pasto sobre el suelo, que se mecen con el viento. */
|
|
179
|
+
export const Pasto: React.FC<{y: number; t: number; n?: number; x0?: number; x1?: number; alto?: number; onda?: number; semilla?: number; color: string; grosor?: number}> = ({y, t, n = 160, x0 = -400, x1 = 2600, alto = 46, onda = 28, semilla = 0, color, grosor = 5}) => {
|
|
180
|
+
let d = '';
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
const x = x0 + ((x1 - x0) * (i + azarN(semilla + i) * 0.9)) / n;
|
|
183
|
+
const h = alto * (0.5 + azarN(semilla + i * 3) * 0.8);
|
|
184
|
+
const viento = Math.sin(t * 1.6 + x / 140) * h * 0.22 + (azarN(semilla + i * 9) - 0.5) * h * 0.5;
|
|
185
|
+
const by = alturaSuelo(x, y, onda) + 6;
|
|
186
|
+
d += `M${x} ${by} Q${x + viento * 0.3} ${by - h * 0.6} ${x + viento} ${by - h} `;
|
|
187
|
+
}
|
|
188
|
+
return <path d={d} stroke={color} strokeWidth={grosor} strokeLinecap="round" fill="none" />;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Luciérnagas: puntos cálidos que flotan y parpadean. */
|
|
192
|
+
export const Luciernagas: React.FC<{t: number; n?: number; x0?: number; x1?: number; y0?: number; y1?: number; semilla?: number; color?: string}> = ({t, n = 26, x0 = 0, x1 = 2160, y0 = 1000, y1 = 1700, semilla = 0, color = '#FFE98A'}) => (
|
|
193
|
+
<g filter="url(#kit-brillo)">
|
|
194
|
+
{Array.from({length: n}, (_, i) => {
|
|
195
|
+
const s = semilla + i;
|
|
196
|
+
const x = x0 + azarN(s) * (x1 - x0) + Math.sin(t * (0.4 + azarN(s + 50) * 0.5) + i) * 40;
|
|
197
|
+
const y = y0 + azarN(s + 20) * (y1 - y0) + Math.cos(t * (0.3 + azarN(s + 70) * 0.5) + i * 2) * 30;
|
|
198
|
+
const o = 0.25 + 0.75 * Math.abs(Math.sin(t * (0.9 + azarN(s + 90)) + i * 1.7));
|
|
199
|
+
return <circle key={i} cx={x} cy={y} r={3 + azarN(s + 40) * 3} fill={color} opacity={o} />;
|
|
200
|
+
})}
|
|
201
|
+
</g>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
/** Sombra de contacto: una elipse oscura y suave bajo los pies de algo. */
|
|
205
|
+
export const Sombra: React.FC<{x: number; y: number; ancho: number; fuerza?: number}> = ({x, y, ancho, fuerza = 0.35}) => (
|
|
206
|
+
<ellipse cx={x} cy={y} rx={ancho / 2} ry={ancho * 0.09} fill={`rgba(0,0,0,${fuerza})`} filter="url(#kit-lejos)" />
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/** Cono de luz (lámpara, luna, linterna) desde (x, y) hacia abajo, girado `angulo` grados. */
|
|
210
|
+
export const ConoLuz: React.FC<{id?: string; x: number; y: number; ancho: number; largo: number; angulo?: number; color?: string; fuerza?: number}> = ({id = 'kit-cono', x, y, ancho, largo, angulo = 0, color = '#FFE8B0', fuerza = 0.35}) => (
|
|
211
|
+
<g transform={`rotate(${angulo} ${x} ${y})`} style={{mixBlendMode: 'screen'}}>
|
|
212
|
+
<defs>
|
|
213
|
+
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
214
|
+
<stop offset="0" stopColor={color} stopOpacity={fuerza} />
|
|
215
|
+
<stop offset="1" stopColor={color} stopOpacity="0" />
|
|
216
|
+
</linearGradient>
|
|
217
|
+
</defs>
|
|
218
|
+
<polygon points={`${x - 30},${y} ${x + 30},${y} ${x + ancho / 2},${y + largo} ${x - ancho / 2},${y + largo}`} fill={`url(#${id})`} filter="url(#kit-lejos)" />
|
|
219
|
+
</g>
|
|
220
|
+
);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Kits de Morfeo: piezas para armar las escenas a medida de un video dentro de tus `componentes`.
|
|
2
|
+
// import {Cielo, Pinos, Paralaje} from 'morfeo/kit'; (2D ilustrado, dentro de un <svg>)
|
|
3
|
+
// import {Lienzo3D, Terreno, Tubo, Mesa, Pila} from 'morfeo/kit'; (3D: maqueta de día o mesa con luz cálida)
|
|
4
|
+
export * from './ilustrado';
|
|
5
|
+
export * from './tres';
|
|
6
|
+
export {clamp, easeIO, easeOut, easeOutExpo, backOut, resorte, ruido, lerp, mezclaColor} from '../estilo';
|