morfeo 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/morfeo.mjs +35 -1
- package/index.d.ts +57 -5
- package/lib/config.mjs +11 -8
- 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/instalar.mjs +18 -2
- package/lib/personajes.mjs +1 -1
- package/lib/recursos.mjs +2 -1
- package/lib/render.mjs +9 -2
- package/lib/whisper.mjs +18 -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/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/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/lib/whisper.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Whisper (opcional): tiempos reales de cada palabra para los subtítulos.
|
|
2
2
|
import {downloadWhisperModel, installWhisperCpp, toCaptions, transcribe} from '@remotion/install-whisper-cpp';
|
|
3
|
-
import {execFileSync} from 'node:child_process';
|
|
3
|
+
import {execFileSync, spawnSync} from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {WHISPER, pythonVenv} from './entorno.mjs';
|
|
@@ -10,8 +10,24 @@ export const MODELO_WHISPER = 'small';
|
|
|
10
10
|
|
|
11
11
|
export const whisperListo = () => fs.existsSync(path.join(WHISPER, `ggml-${MODELO_WHISPER}.bin`)) && fs.existsSync(path.join(WHISPER, '.listo'));
|
|
12
12
|
|
|
13
|
+
/** Herramientas que whisper.cpp necesita para compilarse (en Windows se descarga ya compilado). */
|
|
14
|
+
export function faltaParaWhisper() {
|
|
15
|
+
if (process.platform === 'win32') return [];
|
|
16
|
+
return ['git', 'make', 'cmake'].filter((cmd) => spawnSync('which', [cmd], {stdio: 'ignore'}).status !== 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Mismo lugar donde Remotion busca el ejecutable desde whisper.cpp 1.7.4.
|
|
20
|
+
const compilado = () => fs.existsSync(path.join(WHISPER, 'build', 'bin', process.platform === 'win32' ? 'whisper-cli.exe' : 'whisper-cli'));
|
|
21
|
+
|
|
13
22
|
export async function instalarWhisper() {
|
|
14
|
-
|
|
23
|
+
// Un intento anterior que falló a medias deja la carpeta sin el ejecutable, y Remotion se niega a seguir: se limpia.
|
|
24
|
+
if (fs.existsSync(WHISPER) && !compilado()) fs.rmSync(WHISPER, {recursive: true, force: true});
|
|
25
|
+
try {
|
|
26
|
+
await installWhisperCpp({to: WHISPER, version: VERSION_CPP, printOutput: true});
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (!compilado()) fs.rmSync(WHISPER, {recursive: true, force: true});
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
15
31
|
await downloadWhisperModel({model: MODELO_WHISPER, folder: WHISPER, printOutput: true});
|
|
16
32
|
fs.writeFileSync(path.join(WHISPER, '.listo'), VERSION_CPP);
|
|
17
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morfeo",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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';
|