morfeo 0.3.0 → 0.3.3
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/bases/2d/Mundo.tsx +43 -0
- package/bases/2d/guion.mjs +32 -0
- package/bases/3d/Mundo.tsx +45 -0
- package/bases/3d/guion.mjs +26 -0
- package/bin/morfeo.mjs +23 -4
- package/index.d.ts +10 -3
- package/lib/config.mjs +48 -8
- package/lib/entorno.mjs +22 -2
- package/lib/estructura.mjs +1 -1
- package/lib/guion.mjs +2 -2
- package/lib/hoja.mjs +3 -0
- package/lib/nuevo-video.mjs +65 -0
- package/lib/palabras.mjs +79 -0
- package/lib/render.mjs +211 -88
- package/lib/salida.mjs +84 -0
- package/lib/whisper.mjs +47 -3
- package/package.json +5 -4
- package/plantilla/morfeo.config.mjs +3 -1
- package/python/musica.py +40 -7
- package/python/preparar.py +10 -1
- package/remotion/kit/datos.tsx +180 -0
- package/remotion/kit/escenarios2d.tsx +250 -0
- package/remotion/kit/index.ts +9 -2
- package/remotion/kit/mundo3d.tsx +251 -0
- package/remotion/kit/personas.tsx +439 -0
- package/remotion/kit/tres.tsx +11 -6
- package/remotion/motor/capas.tsx +27 -2
- package/remotion/motor/modelo3d.tsx +8 -3
package/python/musica.py
CHANGED
|
@@ -175,20 +175,50 @@ def efecto(nombre, rng):
|
|
|
175
175
|
if nombre == "tick": return 0.4 * tick()
|
|
176
176
|
if nombre == "glitch": return 0.3 * glitch(rng)
|
|
177
177
|
if nombre == "ding": return 0.2 * bell(nf(88), 0.8)
|
|
178
|
+
if nombre == "golpe":
|
|
179
|
+
x = 0.22 * pop(180, 90, 0.16)
|
|
180
|
+
add(x, 0.1 * lp(rng.standard_normal(int(0.06 * SR)), 1200), 0)
|
|
181
|
+
return x
|
|
178
182
|
return 0.14 * pop()
|
|
179
183
|
|
|
180
184
|
|
|
185
|
+
# sonido por defecto según cómo entra o qué es la capa (sonido: false lo apaga; sonido: "x" lo cambia)
|
|
186
|
+
SONIDO_TIPO = {"sello": "golpe", "dato": "tick", "contador": "tick", "nombre": "pop", "accion": "pop", "chip": "pop", "insignia": "pop"}
|
|
187
|
+
SONIDO_ENTRADA = {"rebote": "pop", "caer": "golpe", "deslizar-izq": "whoosh", "deslizar-der": "whoosh", "deslizar-arriba": "whoosh", "deslizar-abajo": "whoosh", "girar": "whoosh"}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def sonido_de(c):
|
|
191
|
+
s = c.get("sonido")
|
|
192
|
+
if s is False or s == "ninguno": return None
|
|
193
|
+
if s: return s
|
|
194
|
+
return SONIDO_TIPO.get(c.get("tipo")) or SONIDO_ENTRADA.get(c.get("entrada"))
|
|
195
|
+
|
|
196
|
+
|
|
181
197
|
def capas_con_sonido(sp):
|
|
182
|
-
"""
|
|
198
|
+
"""[(capa, efecto, explícito)] de la escena y de sus grupos; las de enMundo y las del HUD no suenan solas."""
|
|
183
199
|
out, pila = [], list(sp.get("capas") or [])
|
|
184
200
|
while pila:
|
|
185
201
|
c = pila.pop(0)
|
|
186
202
|
if not isinstance(c, dict): continue
|
|
187
|
-
|
|
203
|
+
ef = sonido_de(c)
|
|
204
|
+
if ef: out.append((c, ef, bool(c.get("sonido"))))
|
|
188
205
|
pila.extend(c.get("capas") or [])
|
|
189
206
|
return out
|
|
190
207
|
|
|
191
208
|
|
|
209
|
+
def momento(en, ev, si, m):
|
|
210
|
+
"""Segundos (relativos a la escena) de `en`: número, palabra, "palabra+0.3", "inicio", "voz" o "fin" (como cuando() del motor)."""
|
|
211
|
+
if en is None: return 0.3
|
|
212
|
+
if isinstance(en, (int, float)): return float(en)
|
|
213
|
+
import re
|
|
214
|
+
g = re.match(r"^(.*?)([+-]\d+(?:\.\d+)?)?$", str(en))
|
|
215
|
+
base, ajuste = (g.group(1) or "").strip(), float(g.group(2) or 0)
|
|
216
|
+
if base in ("", "inicio"): return ajuste
|
|
217
|
+
if base == "voz": return m["start"] - m["t0"] + ajuste
|
|
218
|
+
if base == "fin": return m["start"] - m["t0"] + m["speech"] + ajuste
|
|
219
|
+
return ev(si, base) + ajuste
|
|
220
|
+
|
|
221
|
+
|
|
192
222
|
def mix(vid, style, outdir, TIM, SPEC, durs, ev):
|
|
193
223
|
"""Mezcla voz + música (con ducking) + efectos y escribe mix.wav. style=None: sin música."""
|
|
194
224
|
dur = sum(durs); N = int(dur * SR); rng = np.random.default_rng(11)
|
|
@@ -206,11 +236,14 @@ def mix(vid, style, outdir, TIM, SPEC, durs, ev):
|
|
|
206
236
|
if ty == "text" and sp.get("strike"):
|
|
207
237
|
for i in range(len(sp["rows"])): add(sfx, 0.2 * whoosh(rng, 0.15), t0 + m["speech"] * 0.55 + i * 0.12)
|
|
208
238
|
if ty == "cta": add(sfx, 0.4 * tick(), t0 + 1.6)
|
|
209
|
-
# motor de capas: cada capa (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
239
|
+
# motor de capas: cada capa suena cuando entra (las explícitas siempre; las automáticas, más bajas y sin amontonarse)
|
|
240
|
+
if sp.get("rotulo"): add(sfx, 0.5 * efecto("whoosh", rng), t0 + momento(sp.get("rotuloEn", 0.12), ev, si, m) - 0.05)
|
|
241
|
+
usados = []
|
|
242
|
+
for capa, ef, explicito in sorted(capas_con_sonido(sp), key=lambda x: momento(x[0].get("en"), ev, si, m)):
|
|
243
|
+
tc = t0 + momento(capa.get("en"), ev, si, m)
|
|
244
|
+
if not explicito and any(abs(tc - u) < 0.25 for u in usados): continue
|
|
245
|
+
usados.append(tc)
|
|
246
|
+
add(sfx, (1.0 if explicito else 0.6) * efecto(ef, rng), tc)
|
|
214
247
|
voice, _ = sf.read(os.path.join(outdir, "voz.wav"))
|
|
215
248
|
voice = resample_poly(voice, 147, 80)
|
|
216
249
|
v = np.zeros(N); v[: min(N, len(voice))] = voice[:N]
|
package/python/preparar.py
CHANGED
|
@@ -228,16 +228,25 @@ def preparar(job):
|
|
|
228
228
|
if not voz_vigente(v, outdir):
|
|
229
229
|
print(" voz...", flush=True); hacer_voz(v, outdir, job["modelos"])
|
|
230
230
|
if job.get("soloVoz"): return
|
|
231
|
+
# pidió Whisper y la transcripción no existe (voz nueva): se sale con 10 para que morfeo transcriba y vuelva a llamar
|
|
232
|
+
if job.get("conWhisper") and not os.path.exists(os.path.join(outdir, "whisper.json")): sys.exit(10)
|
|
231
233
|
TIM = json.load(open(os.path.join(outdir, "tiempos.json"), encoding="utf-8"))["escenas"]
|
|
232
234
|
wj = os.path.join(outdir, "whisper.json"); ww = palabras_whisper(wj) if os.path.exists(wj) else None
|
|
233
235
|
WORDS, reales = [], 0
|
|
234
236
|
for seg in TIM:
|
|
235
237
|
w, ok = tiempos_palabras(seg, ww); WORDS.append(w); reales += ok
|
|
236
238
|
|
|
239
|
+
def norm(x):
|
|
240
|
+
# igual que norm() de remotion/motor/util.ts: sin acentos, minúsculas, solo letras y números
|
|
241
|
+
import unicodedata
|
|
242
|
+
x = unicodedata.normalize("NFD", str(x))
|
|
243
|
+
return "".join(ch for ch in x if not unicodedata.combining(ch)).lower().translate({ord(c): None for c in "¿?.,:;¡!\"'«»()"}).replace(" ", "")
|
|
244
|
+
|
|
237
245
|
def ev(si, prefix):
|
|
238
246
|
seg = TIM[si]
|
|
247
|
+
p = norm(prefix)
|
|
239
248
|
for w, a, b in WORDS[si]:
|
|
240
|
-
if w
|
|
249
|
+
if norm(w).startswith(p): return a - seg["t0"]
|
|
241
250
|
return seg["speech"] * 0.5
|
|
242
251
|
|
|
243
252
|
durs = [seg["dur"] + (0.6 if i == len(TIM) - 1 else 0) for i, seg in enumerate(TIM)]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Gráficas 2D que se dibujan solas (van DENTRO de un <svg>): barras, línea, comparación y lupa.
|
|
2
|
+
// `t` es el tiempo y `en` el momento en que empiezan (segundos); todo se anima con el tiempo.
|
|
3
|
+
//
|
|
4
|
+
// <GraficaBarras x={140} y={700} ancho={800} alto={500} t={t} en={1} datos={[['0 m', 100], ['1 m', 50], ['2 m', 25]]} />
|
|
5
|
+
// <GraficaLinea x={140} y={700} ancho={800} alto={420} t={t} puntos={[100, 50, 25, 12]} etiquetas={['0', '1', '2', '3']} />
|
|
6
|
+
// <Comparacion x={540} y={1300} t={t} a={['ANTES', 10]} b={['DESPUÉS', 27.4]} unidad="kg" />
|
|
7
|
+
// <Lupa x={540} y={900} radio={220} t={t}> ...lo que se ve aumentado... </Lupa>
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
const k = (t: number, en: number, dur: number) => {
|
|
11
|
+
const x = Math.max(0, Math.min(1, (t - en) / dur));
|
|
12
|
+
return 1 - (1 - x) ** 3;
|
|
13
|
+
};
|
|
14
|
+
const fmt = (v: number, dec = 0) => v.toLocaleString('es-MX', {minimumFractionDigits: dec, maximumFractionDigits: dec});
|
|
15
|
+
|
|
16
|
+
type Comun = {x: number; y: number; t: number; en?: number; color?: string; texto?: string; fuente?: string};
|
|
17
|
+
|
|
18
|
+
/** Barras horizontales: [etiqueta, valor]. La más grande ocupa todo el ancho; el valor cuenta mientras crece. */
|
|
19
|
+
export const GraficaBarras: React.FC<Comun & {ancho?: number; alto?: number; datos: [string, number][]; unidad?: string; colores?: string[]; decimales?: number; maximo?: number}> = ({
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
t,
|
|
23
|
+
en = 0,
|
|
24
|
+
ancho = 820,
|
|
25
|
+
alto = 480,
|
|
26
|
+
datos,
|
|
27
|
+
unidad = '',
|
|
28
|
+
color = '#F2B632',
|
|
29
|
+
colores,
|
|
30
|
+
texto = '#FFFFFF',
|
|
31
|
+
fuente = 'JetBrains Mono, monospace',
|
|
32
|
+
decimales = 0,
|
|
33
|
+
maximo,
|
|
34
|
+
}) => {
|
|
35
|
+
const max = maximo ?? Math.max(...datos.map(([, v]) => v), 1);
|
|
36
|
+
const fila = alto / datos.length;
|
|
37
|
+
const etiqueta = 150;
|
|
38
|
+
return (
|
|
39
|
+
<g transform={`translate(${x} ${y})`}>
|
|
40
|
+
{datos.map(([e, v], i) => {
|
|
41
|
+
const p = k(t, en + i * 0.18, 0.9);
|
|
42
|
+
const w = (ancho - etiqueta - 150) * (v / max) * p;
|
|
43
|
+
return (
|
|
44
|
+
<g key={i} transform={`translate(0 ${i * fila})`} opacity={Math.min(1, p * 4)}>
|
|
45
|
+
<text x={etiqueta - 20} y={fila * 0.42} textAnchor="end" dominantBaseline="middle" fontFamily={fuente} fontSize={fila * 0.32} fill={texto} opacity={0.75}>
|
|
46
|
+
{e}
|
|
47
|
+
</text>
|
|
48
|
+
<rect x={etiqueta} y={fila * 0.18} width={Math.max(4, w)} height={fila * 0.48} rx={fila * 0.1} fill={colores?.[i] ?? color} />
|
|
49
|
+
<text x={etiqueta + w + 16} y={fila * 0.42} dominantBaseline="middle" fontFamily={fuente} fontWeight={700} fontSize={fila * 0.32} fill={texto}>
|
|
50
|
+
{fmt(v * p, decimales)}
|
|
51
|
+
{unidad}
|
|
52
|
+
</text>
|
|
53
|
+
</g>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</g>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Línea que se dibuja de izquierda a derecha, con puntos y etiquetas en el eje. */
|
|
61
|
+
export const GraficaLinea: React.FC<Comun & {ancho?: number; alto?: number; puntos: number[]; etiquetas?: string[]; minimo?: number; maximo?: number; relleno?: boolean; dur?: number}> = ({
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
t,
|
|
65
|
+
en = 0,
|
|
66
|
+
ancho = 820,
|
|
67
|
+
alto = 420,
|
|
68
|
+
puntos,
|
|
69
|
+
etiquetas = [],
|
|
70
|
+
color = '#F07A3A',
|
|
71
|
+
texto = '#FFFFFF',
|
|
72
|
+
fuente = 'JetBrains Mono, monospace',
|
|
73
|
+
minimo,
|
|
74
|
+
maximo,
|
|
75
|
+
relleno = true,
|
|
76
|
+
dur = 1.4,
|
|
77
|
+
}) => {
|
|
78
|
+
const mn = minimo ?? Math.min(0, ...puntos);
|
|
79
|
+
const mx = maximo ?? Math.max(...puntos);
|
|
80
|
+
const px = (i: number) => (i / Math.max(1, puntos.length - 1)) * ancho;
|
|
81
|
+
const py = (v: number) => alto - ((v - mn) / Math.max(1e-6, mx - mn)) * alto;
|
|
82
|
+
const p = k(t, en, dur);
|
|
83
|
+
const d = puntos.map((v, i) => `${i ? 'L' : 'M'} ${px(i)} ${py(v)}`).join(' ');
|
|
84
|
+
const largo = puntos.slice(1).reduce((s, v, i) => s + Math.hypot(px(i + 1) - px(i), py(v) - py(puntos[i])), 0);
|
|
85
|
+
const id = `linea-${Math.round(x)}-${Math.round(y)}`;
|
|
86
|
+
return (
|
|
87
|
+
<g transform={`translate(${x} ${y})`}>
|
|
88
|
+
<line x1={0} y1={alto} x2={ancho} y2={alto} stroke={texto} strokeOpacity={0.35} strokeWidth={3} />
|
|
89
|
+
<line x1={0} y1={0} x2={0} y2={alto} stroke={texto} strokeOpacity={0.35} strokeWidth={3} />
|
|
90
|
+
{relleno ? (
|
|
91
|
+
<>
|
|
92
|
+
<defs>
|
|
93
|
+
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
94
|
+
<stop offset="0" stopColor={color} stopOpacity={0.35} />
|
|
95
|
+
<stop offset="1" stopColor={color} stopOpacity={0} />
|
|
96
|
+
</linearGradient>
|
|
97
|
+
<clipPath id={`${id}-c`}>
|
|
98
|
+
<rect x={0} y={-50} width={ancho * p} height={alto + 100} />
|
|
99
|
+
</clipPath>
|
|
100
|
+
</defs>
|
|
101
|
+
<path d={`${d} L ${ancho} ${alto} L 0 ${alto} Z`} fill={`url(#${id})`} clipPath={`url(#${id}-c)`} />
|
|
102
|
+
</>
|
|
103
|
+
) : null}
|
|
104
|
+
<path d={d} fill="none" stroke={color} strokeWidth={7} strokeLinecap="round" strokeLinejoin="round" strokeDasharray={largo} strokeDashoffset={largo * (1 - p)} />
|
|
105
|
+
{puntos.map((v, i) =>
|
|
106
|
+
px(i) <= ancho * p + 1 ? <circle key={i} cx={px(i)} cy={py(v)} r={11} fill={color} stroke="#fff" strokeWidth={4} /> : null,
|
|
107
|
+
)}
|
|
108
|
+
{etiquetas.map((e, i) => (
|
|
109
|
+
<text key={i} x={px(i)} y={alto + 44} textAnchor="middle" fontFamily={fuente} fontSize={28} fill={texto} opacity={0.7}>
|
|
110
|
+
{e}
|
|
111
|
+
</text>
|
|
112
|
+
))}
|
|
113
|
+
</g>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Dos columnas que crecen hasta su valor (antes / después, A contra B), con el número contando encima. */
|
|
118
|
+
export const Comparacion: React.FC<Comun & {a: [string, number]; b: [string, number]; unidad?: string; alto?: number; colorA?: string; colorB?: string; decimales?: number}> = ({
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
t,
|
|
122
|
+
en = 0,
|
|
123
|
+
a,
|
|
124
|
+
b,
|
|
125
|
+
unidad = '',
|
|
126
|
+
alto = 520,
|
|
127
|
+
colorA = '#8A8F9C',
|
|
128
|
+
colorB = '#2FD08A',
|
|
129
|
+
texto = '#FFFFFF',
|
|
130
|
+
fuente = 'Montserrat, sans-serif',
|
|
131
|
+
decimales = 0,
|
|
132
|
+
}) => {
|
|
133
|
+
const max = Math.max(a[1], b[1], 1e-6);
|
|
134
|
+
const col = (lbl: string, v: number, c: string, dx: number, retraso: number) => {
|
|
135
|
+
const p = k(t, en + retraso, 1.1);
|
|
136
|
+
const h = (alto * v * p) / max;
|
|
137
|
+
return (
|
|
138
|
+
<g transform={`translate(${dx} 0)`}>
|
|
139
|
+
<rect x={-90} y={-h} width={180} height={Math.max(2, h)} rx={18} fill={c} />
|
|
140
|
+
<text x={0} y={-h - 28} textAnchor="middle" fontFamily={fuente} fontWeight={900} fontSize={64} fill={texto}>
|
|
141
|
+
{fmt(v * p, decimales)}
|
|
142
|
+
<tspan fontSize={34}>{unidad}</tspan>
|
|
143
|
+
</text>
|
|
144
|
+
<text x={0} y={56} textAnchor="middle" fontFamily={fuente} fontWeight={800} fontSize={36} fill={texto} opacity={0.8}>
|
|
145
|
+
{lbl}
|
|
146
|
+
</text>
|
|
147
|
+
</g>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
return (
|
|
151
|
+
<g transform={`translate(${x} ${y})`}>
|
|
152
|
+
<line x1={-280} y1={0} x2={280} y2={0} stroke={texto} strokeOpacity={0.4} strokeWidth={4} />
|
|
153
|
+
{col(a[0], a[1], colorA, -150, 0)}
|
|
154
|
+
{col(b[0], b[1], colorB, 150, 0.5)}
|
|
155
|
+
</g>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Lupa: aro con mango; dentro se ve `children` (lo que dibujes, normalmente más grande). Entra con un pequeño rebote. */
|
|
160
|
+
export const Lupa: React.FC<{x: number; y: number; radio?: number; t: number; en?: number; fondo?: string; children?: React.ReactNode}> = ({x, y, radio = 220, t, en = 0, fondo = '#F4E6D8', children}) => {
|
|
161
|
+
const p = k(t, en, 0.5);
|
|
162
|
+
const s = 0.6 + 0.4 * p + Math.sin(Math.min(1, (t - en) / 0.5) * Math.PI) * 0.05;
|
|
163
|
+
const id = `lupa-${Math.round(x)}-${Math.round(y)}`;
|
|
164
|
+
if (p <= 0) return null;
|
|
165
|
+
return (
|
|
166
|
+
<g transform={`translate(${x} ${y}) scale(${s})`} opacity={Math.min(1, p * 3)}>
|
|
167
|
+
<defs>
|
|
168
|
+
<clipPath id={id}>
|
|
169
|
+
<circle r={radio} />
|
|
170
|
+
</clipPath>
|
|
171
|
+
</defs>
|
|
172
|
+
<rect x={radio * 0.62} y={radio * 0.62} width={radio * 0.3} height={radio * 0.95} rx={radio * 0.12} transform={`rotate(-45 ${radio * 0.77} ${radio * 0.62})`} fill="#5A3A28" />
|
|
173
|
+
<circle r={radio} fill={fondo} />
|
|
174
|
+
<g clipPath={`url(#${id})`}>{children}</g>
|
|
175
|
+
<circle r={radio} fill="none" stroke="#E9EDF2" strokeWidth={radio * 0.11} />
|
|
176
|
+
<circle r={radio * 1.05} fill="none" stroke="rgba(0,0,0,0.25)" strokeWidth={4} />
|
|
177
|
+
<path d={`M ${-radio * 0.55} ${-radio * 0.35} A ${radio * 0.7} ${radio * 0.7} 0 0 1 ${-radio * 0.2} ${-radio * 0.62}`} stroke="rgba(255,255,255,0.7)" strokeWidth={10} fill="none" strokeLinecap="round" />
|
|
178
|
+
</g>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Escenarios 2D listos (van DENTRO de un <svg>, normalmente como fondo de un `mundo` de 2160×1920).
|
|
2
|
+
// Todos aceptan `ancho` (del mundo), `suelo` (altura del piso), `t` (para lo que se mueve) y `camara` (paralaje).
|
|
3
|
+
// Combínalos con Persona2D, Multitud y Globo (kit/personas.tsx) y con las anotaciones del motor.
|
|
4
|
+
//
|
|
5
|
+
// <CiudadNoche t={t} camara={camara} /> <Cuarto pared="#2E5A4C" /> <Campo t={t} /> <Calle t={t} />
|
|
6
|
+
// <Oficina /> <Mar t={t} profundo />
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {Cielo, ConoLuz, Estrellas, Montanas, Paralaje, Pasto, Pinos, Suelo, alturaSuelo, azarN, type Camara} from './ilustrado';
|
|
9
|
+
|
|
10
|
+
type Base = {ancho?: number; suelo?: number; t?: number; camara?: Camara; semilla?: number};
|
|
11
|
+
|
|
12
|
+
/** Edificios con ventanas (algunas encendidas que parpadean poco a poco). */
|
|
13
|
+
const Edificios: React.FC<{x0: number; x1: number; base: number; alto: number; color: string; luz: string; t: number; semilla: number; n?: number}> = ({x0, x1, base, alto, color, luz, t, semilla, n = 9}) => {
|
|
14
|
+
const ancho = (x1 - x0) / n;
|
|
15
|
+
return (
|
|
16
|
+
<g>
|
|
17
|
+
{Array.from({length: n}, (_, i) => {
|
|
18
|
+
const s = semilla + i * 13;
|
|
19
|
+
const w = ancho * (0.7 + azarN(s) * 0.28);
|
|
20
|
+
const h = alto * (0.45 + azarN(s + 1) * 0.55);
|
|
21
|
+
const x = x0 + i * ancho + (ancho - w) / 2;
|
|
22
|
+
const cols = Math.max(2, Math.floor(w / 46));
|
|
23
|
+
const filas = Math.max(3, Math.floor(h / 62));
|
|
24
|
+
return (
|
|
25
|
+
<g key={i}>
|
|
26
|
+
<rect x={x} y={base - h} width={w} height={h + 400} fill={color} />
|
|
27
|
+
{Array.from({length: cols * filas}, (_, j) => {
|
|
28
|
+
const c = j % cols;
|
|
29
|
+
const f = Math.floor(j / cols);
|
|
30
|
+
const on = azarN(s + j * 3) > 0.55;
|
|
31
|
+
if (!on) return null;
|
|
32
|
+
const brillo = 0.75 + 0.25 * Math.sin(t * (0.3 + azarN(s + j) * 0.5) + j);
|
|
33
|
+
return <rect key={j} x={x + 14 + c * ((w - 28) / cols)} y={base - h + 22 + f * 62} width={(w - 28) / cols - 14} height={34} rx={3} fill={luz} opacity={brillo} />;
|
|
34
|
+
})}
|
|
35
|
+
</g>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</g>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Ciudad de noche: cielo, estrellas, edificios en tres planos con ventanas, calle. */
|
|
43
|
+
export const CiudadNoche: React.FC<Base & {luz?: string}> = ({ancho = 2160, suelo = 1500, t = 0, camara, semilla = 0, luz = '#FFD27A'}) => (
|
|
44
|
+
<g>
|
|
45
|
+
<Paralaje camara={camara} profundidad={0.9}>
|
|
46
|
+
<Cielo id="cn-cielo" colores={['#070A1F', '#141B45', '#2B2A63', '#4A3A73']} />
|
|
47
|
+
<Estrellas t={t} n={80} y1={900} semilla={semilla} />
|
|
48
|
+
</Paralaje>
|
|
49
|
+
<Paralaje camara={camara} profundidad={0.55}>
|
|
50
|
+
<Edificios x0={-500} x1={ancho + 500} base={suelo - 60} alto={900} color="#1B2350" luz="#6C77C9" t={t} semilla={semilla + 1} n={14} />
|
|
51
|
+
</Paralaje>
|
|
52
|
+
<Paralaje camara={camara} profundidad={0.25}>
|
|
53
|
+
<Edificios x0={-400} x1={ancho + 400} base={suelo - 10} alto={1050} color="#141A3A" luz={luz} t={t} semilla={semilla + 2} n={10} />
|
|
54
|
+
</Paralaje>
|
|
55
|
+
<rect x={-800} y={suelo} width={ancho + 1600} height={800} fill="#20222C" />
|
|
56
|
+
<rect x={-800} y={suelo} width={ancho + 1600} height={14} fill="#3A3D4A" />
|
|
57
|
+
{Array.from({length: Math.ceil((ancho + 1600) / 240)}, (_, i) => (
|
|
58
|
+
<rect key={i} x={-800 + i * 240} y={suelo + 150} width={130} height={14} rx={7} fill="#E8C35A" opacity={0.7} />
|
|
59
|
+
))}
|
|
60
|
+
</g>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** Interior: pared con zoclo, piso de madera, ventana, cuadro y lámpara con su cono de luz. */
|
|
64
|
+
export const Cuarto: React.FC<Base & {pared?: string; piso?: string; noche?: boolean; ventana?: boolean; lampara?: boolean; cuadro?: boolean}> = ({ancho = 2160, suelo = 1500, t = 0, pared = '#2E5A4C', piso = '#7A4A2A', noche = true, ventana = true, lampara = true, cuadro = true}) => (
|
|
65
|
+
<g>
|
|
66
|
+
<rect x={-800} y={-800} width={ancho + 1600} height={suelo + 800} fill={pared} />
|
|
67
|
+
{/* papel tapiz: rombos sutiles */}
|
|
68
|
+
<g opacity={0.08} fill="#000">
|
|
69
|
+
{Array.from({length: 40 * 30}, (_, i) => {
|
|
70
|
+
const c = i % 40;
|
|
71
|
+
const f = Math.floor(i / 40);
|
|
72
|
+
return <path key={i} d={`M ${-800 + c * 110 + (f % 2) * 55} ${-800 + f * 80} l 18 26 l -18 26 l -18 -26 Z`} />;
|
|
73
|
+
})}
|
|
74
|
+
</g>
|
|
75
|
+
<rect x={-800} y={suelo - 140} width={ancho + 1600} height={140} fill="rgba(0,0,0,0.22)" />
|
|
76
|
+
<rect x={-800} y={suelo - 150} width={ancho + 1600} height={14} fill="rgba(255,255,255,0.12)" />
|
|
77
|
+
<rect x={-800} y={suelo} width={ancho + 1600} height={900} fill={piso} />
|
|
78
|
+
{Array.from({length: 30}, (_, i) => (
|
|
79
|
+
<rect key={i} x={-800} y={suelo + 30 + i * 46} width={ancho + 1600} height={3} fill="rgba(0,0,0,0.18)" />
|
|
80
|
+
))}
|
|
81
|
+
{ventana ? (
|
|
82
|
+
<g transform={`translate(${ancho * 0.72} ${suelo - 1000})`}>
|
|
83
|
+
<rect x={-20} y={-20} width={420} height={520} rx={10} fill="#E9E1D2" />
|
|
84
|
+
<rect width={380} height={480} fill={noche ? '#1A2350' : '#9FD4F5'} />
|
|
85
|
+
{noche ? <circle cx={270} cy={120} r={46} fill="#FFF4D6" /> : <circle cx={270} cy={120} r={54} fill="#FFE27A" />}
|
|
86
|
+
<rect x={186} width={8} height={480} fill="#E9E1D2" />
|
|
87
|
+
<rect y={236} width={380} height={8} fill="#E9E1D2" />
|
|
88
|
+
</g>
|
|
89
|
+
) : null}
|
|
90
|
+
{cuadro ? (
|
|
91
|
+
<g transform={`translate(${ancho * 0.3} ${suelo - 1050})`}>
|
|
92
|
+
<rect x={-16} y={-16} width={352} height={262} fill="#B8873A" />
|
|
93
|
+
<rect width={320} height={230} fill="#8FD0C4" />
|
|
94
|
+
<path d="M 0 230 L 90 120 L 170 190 L 240 110 L 320 230 Z" fill="#3E8A55" />
|
|
95
|
+
<circle cx={250} cy={60} r={26} fill="#F5D46A" />
|
|
96
|
+
</g>
|
|
97
|
+
) : null}
|
|
98
|
+
{lampara ? (
|
|
99
|
+
<g transform={`translate(${ancho * 0.12} ${suelo})`}>
|
|
100
|
+
<ConoLuz id="cuarto-cono" x={0} y={-640} ancho={720} largo={700} fuerza={0.4 + 0.03 * Math.sin(t * 2)} />
|
|
101
|
+
<rect x={-6} y={-600} width={12} height={600} fill="#2A2420" />
|
|
102
|
+
<ellipse cx={0} cy={0} rx={70} ry={14} fill="#2A2420" />
|
|
103
|
+
<path d="M -90 -640 L 90 -640 L 60 -760 L -60 -760 Z" fill="#F2D49B" />
|
|
104
|
+
</g>
|
|
105
|
+
) : null}
|
|
106
|
+
</g>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/** Campo abierto de día: cielo, sol, nubes que avanzan, montañas, árboles y pasto. */
|
|
110
|
+
export const Campo: React.FC<Base & {tarde?: boolean}> = ({ancho = 2160, suelo = 1500, t = 0, camara, semilla = 0, tarde = false}) => (
|
|
111
|
+
<g>
|
|
112
|
+
<Paralaje camara={camara} profundidad={0.9}>
|
|
113
|
+
<Cielo id="campo-cielo" colores={tarde ? ['#F7B267', '#F4845F', '#F27059'] : ['#7EC8F2', '#B6E2F7', '#E8F6FC']} />
|
|
114
|
+
<circle cx={ancho * 0.75} cy={380} r={110} fill={tarde ? '#FFE3A3' : '#FFF1A8'} />
|
|
115
|
+
{[0, 1, 2, 3].map((i) => {
|
|
116
|
+
const x = ((azarN(semilla + i) * (ancho + 800) + t * (18 + i * 6)) % (ancho + 1200)) - 600;
|
|
117
|
+
const y = 220 + azarN(semilla + i + 9) * 380;
|
|
118
|
+
return (
|
|
119
|
+
<g key={i} transform={`translate(${x} ${y}) scale(${0.8 + azarN(i) * 0.6})`} fill="#fff" opacity={0.92}>
|
|
120
|
+
<circle cx={0} cy={0} r={60} />
|
|
121
|
+
<circle cx={70} cy={-20} r={75} />
|
|
122
|
+
<circle cx={150} cy={0} r={55} />
|
|
123
|
+
<rect x={0} y={0} width={150} height={55} />
|
|
124
|
+
</g>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</Paralaje>
|
|
128
|
+
<Paralaje camara={camara} profundidad={0.6}>
|
|
129
|
+
<Montanas base={suelo - 220} alto={500} semilla={semilla + 3} color={tarde ? '#B5656B' : '#8DB7C9'} />
|
|
130
|
+
</Paralaje>
|
|
131
|
+
<Paralaje camara={camara} profundidad={0.3}>
|
|
132
|
+
<Pinos base={suelo - 60} alto={420} ancho={190} n={18} semilla={semilla + 7} color={tarde ? '#4E5B3A' : '#3E8A55'} />
|
|
133
|
+
</Paralaje>
|
|
134
|
+
<Suelo y={suelo} colores={tarde ? ['#8DA05A', '#55623A'] : ['#7CC45A', '#3E8A3E']} id="campo-suelo" />
|
|
135
|
+
<Pasto y={suelo} t={t} n={180} color={tarde ? '#6E7F44' : '#5AA845'} semilla={semilla + 11} />
|
|
136
|
+
</g>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/** Calle de día con banqueta, casas al fondo y postes. */
|
|
140
|
+
export const Calle: React.FC<Base & {noche?: boolean}> = ({ancho = 2160, suelo = 1500, t = 0, camara, semilla = 0, noche = false}) => {
|
|
141
|
+
const casas = ['#F2E6D0', '#F7D9C4', '#DDE8F0', '#F4E3A1', '#E8D5F0'];
|
|
142
|
+
return (
|
|
143
|
+
<g>
|
|
144
|
+
<Paralaje camara={camara} profundidad={0.9}>
|
|
145
|
+
<Cielo id="calle-cielo" colores={noche ? ['#0B1033', '#232A63'] : ['#8FD0F5', '#DDF2FC']} />
|
|
146
|
+
</Paralaje>
|
|
147
|
+
<Paralaje camara={camara} profundidad={0.3}>
|
|
148
|
+
<g>
|
|
149
|
+
{Array.from({length: 8}, (_, i) => {
|
|
150
|
+
const x = -400 + i * ((ancho + 800) / 8);
|
|
151
|
+
const w = 300 + azarN(semilla + i) * 60;
|
|
152
|
+
const h = 320 + azarN(semilla + i + 3) * 140;
|
|
153
|
+
return (
|
|
154
|
+
<g key={i}>
|
|
155
|
+
<rect x={x} y={suelo - 120 - h} width={w} height={h} fill={casas[i % casas.length]} />
|
|
156
|
+
<path d={`M ${x - 20} ${suelo - 120 - h} L ${x + w / 2} ${suelo - 120 - h - 150} L ${x + w + 20} ${suelo - 120 - h} Z`} fill={['#D9533B', '#8A3B2A', '#4A5A7A'][i % 3]} />
|
|
157
|
+
<rect x={x + w * 0.4} y={suelo - 120 - 170} width={70} height={170} fill="#7A4E2D" />
|
|
158
|
+
<rect x={x + w * 0.1} y={suelo - 120 - h + 60} width={80} height={70} fill={noche ? '#FFD27A' : '#9AD4F5'} />
|
|
159
|
+
<rect x={x + w * 0.65} y={suelo - 120 - h + 60} width={80} height={70} fill={noche ? '#FFD27A' : '#9AD4F5'} />
|
|
160
|
+
</g>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</g>
|
|
164
|
+
</Paralaje>
|
|
165
|
+
<rect x={-800} y={suelo - 120} width={ancho + 1600} height={120} fill="#C9C4BA" />
|
|
166
|
+
<rect x={-800} y={suelo - 124} width={ancho + 1600} height={10} fill="#E2DDD3" />
|
|
167
|
+
<rect x={-800} y={suelo} width={ancho + 1600} height={900} fill="#3E4250" />
|
|
168
|
+
{Array.from({length: Math.ceil((ancho + 1600) / 260)}, (_, i) => (
|
|
169
|
+
<rect key={i} x={-800 + i * 260 - ((t * 0) % 260)} y={suelo + 210} width={140} height={16} rx={8} fill="#F2D04A" />
|
|
170
|
+
))}
|
|
171
|
+
{[0.15, 0.55, 0.92].map((f, i) => (
|
|
172
|
+
<g key={i} transform={`translate(${ancho * f} ${suelo - 120})`}>
|
|
173
|
+
<rect x={-8} y={-620} width={16} height={620} fill="#5A5F6B" />
|
|
174
|
+
<rect x={-8} y={-620} width={110} height={12} fill="#5A5F6B" />
|
|
175
|
+
<ellipse cx={100} cy={-602} rx={24} ry={10} fill={noche ? '#FFE9A8' : '#C9CED6'} />
|
|
176
|
+
</g>
|
|
177
|
+
))}
|
|
178
|
+
</g>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Oficina: pared clara, escritorio con monitor, planta y repisa. */
|
|
183
|
+
export const Oficina: React.FC<Base> = ({ancho = 2160, suelo = 1500, t = 0}) => (
|
|
184
|
+
<g>
|
|
185
|
+
<rect x={-800} y={-800} width={ancho + 1600} height={suelo + 800} fill="#E8E4DC" />
|
|
186
|
+
<rect x={-800} y={suelo} width={ancho + 1600} height={900} fill="#B9A58A" />
|
|
187
|
+
<rect x={-800} y={suelo - 10} width={ancho + 1600} height={10} fill="#9C8A70" />
|
|
188
|
+
<g transform={`translate(${ancho * 0.62} ${suelo})`}>
|
|
189
|
+
<rect x={-360} y={-420} width={720} height={36} rx={8} fill="#8A5A3B" />
|
|
190
|
+
<rect x={-330} y={-384} width={30} height={384} fill="#6B4630" />
|
|
191
|
+
<rect x={300} y={-384} width={30} height={384} fill="#6B4630" />
|
|
192
|
+
<rect x={-150} y={-720} width={300} height={200} rx={14} fill="#22252E" />
|
|
193
|
+
<rect x={-136} y={-706} width={272} height={172} rx={8} fill="#3B7BE0" opacity={0.85 + 0.1 * Math.sin(t * 2)} />
|
|
194
|
+
<rect x={-20} y={-520} width={40} height={100} fill="#22252E" />
|
|
195
|
+
<rect x={-90} y={-430} width={180} height={14} rx={5} fill="#22252E" />
|
|
196
|
+
</g>
|
|
197
|
+
<g transform={`translate(${ancho * 0.2} ${suelo})`}>
|
|
198
|
+
<path d="M -70 0 L 70 0 L 55 -150 L -55 -150 Z" fill="#C9653E" />
|
|
199
|
+
{[-50, -20, 10, 40].map((x, i) => (
|
|
200
|
+
<ellipse key={i} cx={x} cy={-230 - (i % 2) * 40} rx={36} ry={90} fill="#3E8A55" transform={`rotate(${(i - 1.5) * 18} ${x} -160)`} />
|
|
201
|
+
))}
|
|
202
|
+
</g>
|
|
203
|
+
<g transform={`translate(${ancho * 0.3} ${suelo - 1100})`}>
|
|
204
|
+
<rect x={-200} y={0} width={400} height={20} fill="#8A5A3B" />
|
|
205
|
+
{['#E04A4A', '#2F6FE0', '#F2B632', '#2FA36B'].map((c, i) => (
|
|
206
|
+
<rect key={i} x={-180 + i * 50} y={-130 + (i % 2) * 20} width={40} height={130 - (i % 2) * 20} fill={c} />
|
|
207
|
+
))}
|
|
208
|
+
</g>
|
|
209
|
+
</g>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/** Mar: superficie con olas o fondo marino con rayos de luz y burbujas (`profundo`). */
|
|
213
|
+
export const Mar: React.FC<Base & {profundo?: boolean}> = ({ancho = 2160, suelo = 1500, t = 0, camara, semilla = 0, profundo = false}) => {
|
|
214
|
+
if (profundo)
|
|
215
|
+
return (
|
|
216
|
+
<g>
|
|
217
|
+
<Cielo id="mar-fondo" colores={['#0E5AA0', '#073A70', '#031A38']} />
|
|
218
|
+
<g style={{mixBlendMode: 'screen'}} opacity={0.18} filter="url(#kit-lejos)">
|
|
219
|
+
{Array.from({length: 9}, (_, i) => {
|
|
220
|
+
const x = -300 + i * ((ancho + 600) / 9) + Math.sin(t * 0.4 + i) * 40;
|
|
221
|
+
return <polygon key={i} points={`${x},-400 ${x + 90},-400 ${x + 260},${suelo + 200} ${x + 60},${suelo + 200}`} fill="#BFE3FF" />;
|
|
222
|
+
})}
|
|
223
|
+
</g>
|
|
224
|
+
{Array.from({length: 40}, (_, i) => {
|
|
225
|
+
const s = semilla + i;
|
|
226
|
+
const y = ((suelo + 300 - ((t * (40 + azarN(s) * 60) + azarN(s + 1) * 2000) % 2400)) as number);
|
|
227
|
+
return <circle key={i} cx={azarN(s + 2) * ancho + Math.sin(t + i) * 12} cy={y} r={3 + azarN(s + 3) * 7} fill="none" stroke="rgba(200,235,255,0.5)" strokeWidth={2} />;
|
|
228
|
+
})}
|
|
229
|
+
<path d={`M -800 ${suelo + 120} ${Array.from({length: 30}, (_, i) => `L ${-800 + i * 140} ${suelo + 100 + Math.sin(i * 1.3) * 30}`).join(' ')} L ${ancho + 800} ${suelo + 900} L -800 ${suelo + 900} Z`} fill="#0A2440" />
|
|
230
|
+
</g>
|
|
231
|
+
);
|
|
232
|
+
const ola = (y: number, amp: number, fase: number, color: string) => {
|
|
233
|
+
let d = `M -800 ${y}`;
|
|
234
|
+
for (let x = -800; x <= ancho + 800; x += 40) d += ` L ${x} ${y + Math.sin(x / 120 + t * 1.6 + fase) * amp}`;
|
|
235
|
+
return <path d={`${d} L ${ancho + 800} ${y + 1200} L -800 ${y + 1200} Z`} fill={color} />;
|
|
236
|
+
};
|
|
237
|
+
return (
|
|
238
|
+
<g>
|
|
239
|
+
<Paralaje camara={camara} profundidad={0.9}>
|
|
240
|
+
<Cielo id="mar-cielo" colores={['#7EC8F2', '#CDEBFA']} />
|
|
241
|
+
<circle cx={ancho * 0.25} cy={420} r={100} fill="#FFF1A8" />
|
|
242
|
+
</Paralaje>
|
|
243
|
+
{ola(suelo - 180, 10, 0, '#4FA7E0')}
|
|
244
|
+
{ola(suelo - 80, 16, 1.4, '#2E86C9')}
|
|
245
|
+
{ola(suelo + 40, 22, 2.6, '#1E6AA8')}
|
|
246
|
+
</g>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export {alturaSuelo};
|
package/remotion/kit/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Kits de Morfeo: piezas para armar las escenas a medida de un video dentro de tus `componentes`.
|
|
2
2
|
// import {Cielo, Pinos, Paralaje} from 'morfeo/kit'; (2D ilustrado, dentro de un <svg>)
|
|
3
|
-
// import {
|
|
3
|
+
// import {Persona2D, Multitud, Globo, CiudadNoche, Cuarto} from 'morfeo/kit'; (personajes y escenarios 2D)
|
|
4
|
+
// import {Lienzo3D, Persona3D, Ciudad, Terreno, Tubo, Mesa, Pila} from 'morfeo/kit'; (3D: de día o con luz cálida)
|
|
5
|
+
// import {Maqueta, Modelo, Persona, Coche, Trafico, Corte} from 'morfeo/kit'; (diorama low-poly)
|
|
6
|
+
// import {GraficaBarras, GraficaLinea, Comparacion, Lupa} from 'morfeo/kit'; (datos 2D)
|
|
4
7
|
export * from './ilustrado';
|
|
8
|
+
export * from './personas';
|
|
9
|
+
export * from './escenarios2d';
|
|
5
10
|
export * from './tres';
|
|
6
|
-
export
|
|
11
|
+
export * from './mundo3d';
|
|
12
|
+
export * from './datos';
|
|
7
13
|
export * from './diorama';
|
|
14
|
+
export {clamp, easeIO, easeOut, easeOutExpo, backOut, resorte, ruido, lerp, mezclaColor} from '../estilo';
|