morfeo 0.2.0 → 0.3.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 +1 -0
- package/index.d.ts +17 -2
- package/lib/config.mjs +2 -2
- package/lib/estructura.mjs +15 -4
- package/lib/instalar.mjs +18 -2
- package/lib/whisper.mjs +18 -2
- package/package.json +1 -1
- package/remotion/kit/diorama.tsx +489 -0
- package/remotion/kit/index.ts +1 -0
- package/remotion/kit/tres.tsx +123 -7
- package/remotion/motor/Video.tsx +13 -7
- package/remotion/motor/capas.tsx +2 -0
- package/remotion/motor/fichas.tsx +255 -0
- package/remotion/motor/modelo3d.tsx +1 -1
- package/remotion/motor/subtitulos.tsx +2 -2
- package/remotion/motor/tema.ts +20 -1
package/remotion/motor/Video.tsx
CHANGED
|
@@ -129,26 +129,32 @@ const Rotulo: React.FC<{e: Escena; t: number; tema: Tema}> = ({e, t, tema}) => {
|
|
|
129
129
|
const limpio = texto.replace(/\*/g, '');
|
|
130
130
|
const fuente = tema.fuentes.titulo;
|
|
131
131
|
// cabe en 940 px: se achica si hace falta (el ancho se mide con la fuente ya cargada)
|
|
132
|
-
let tam = e.p.rotuloTam ?? 104;
|
|
132
|
+
let tam = e.p.rotuloTam ?? tema.rotulo?.tam ?? 104;
|
|
133
133
|
try {
|
|
134
134
|
const w = ancho(may ? limpio.toUpperCase() : limpio, fuente, tam, 900);
|
|
135
135
|
if (w > 940) tam = Math.max(56, Math.floor((tam * 940) / w));
|
|
136
136
|
} catch {}
|
|
137
137
|
const claro = tema.colores.texto !== '#FFFFFF';
|
|
138
|
-
const
|
|
138
|
+
const estilo = e.p.rotuloEstilo ?? tema.rotulo?.estilo ?? 'normal';
|
|
139
|
+
const contorno = estilo === 'contorno' || (estilo === 'normal' && !claro && (S.contorno ?? 0) > 0);
|
|
140
|
+
const grosor = estilo === 'contorno' ? Math.max(8, Math.round(tam * 0.11)) : S.contorno;
|
|
141
|
+
const pastilla = estilo === 'pastilla';
|
|
142
|
+
const inclinar = e.p.rotuloInclinar ?? tema.rotulo?.inclinar ?? (pastilla ? -3 : 0);
|
|
143
|
+
const y = tema.rotulo?.y ?? (abajo ? 1470 : 330);
|
|
139
144
|
return (
|
|
140
|
-
<div style={{position: 'absolute', left: 50, width: W - 100, top:
|
|
145
|
+
<div style={{position: 'absolute', left: 50, width: W - 100, top: y, transform: `translate(0, -50%) scale(${0.86 + 0.14 * resorte(k, 0.3)}) rotate(${inclinar}deg)`, opacity: clamp(k * 3), textAlign: 'center', pointerEvents: 'none'}}>
|
|
141
146
|
<span
|
|
142
147
|
style={{
|
|
143
148
|
fontFamily: fuente,
|
|
144
149
|
fontWeight: 900,
|
|
145
|
-
fontSize: tam,
|
|
150
|
+
fontSize: pastilla ? tam * 0.82 : tam,
|
|
146
151
|
lineHeight: 1.05,
|
|
147
|
-
color: tema.colores.texto,
|
|
152
|
+
color: pastilla || estilo === 'contorno' ? '#FFFFFF' : tema.colores.texto,
|
|
148
153
|
textTransform: may ? 'uppercase' : undefined,
|
|
149
|
-
WebkitTextStroke: contorno ? `${
|
|
154
|
+
WebkitTextStroke: contorno ? `${grosor}px #1B1B1F` : undefined,
|
|
150
155
|
paintOrder: 'stroke fill',
|
|
151
|
-
textShadow: claro ? undefined : '0 6px 22px rgba(0,0,0,0.55)',
|
|
156
|
+
textShadow: pastilla ? undefined : estilo === 'contorno' ? '0 7px 0 rgba(0,0,0,0.28)' : claro ? undefined : '0 6px 22px rgba(0,0,0,0.55)',
|
|
157
|
+
...(pastilla ? {display: 'inline-block', background: '#1E1F24', padding: `${tam * 0.12}px ${tam * 0.32}px`, borderRadius: tam * 0.22, boxShadow: '0 10px 24px rgba(0,0,0,0.3)', border: '4px solid rgba(255,255,255,0.9)'} : {}),
|
|
152
158
|
}}
|
|
153
159
|
>
|
|
154
160
|
{partesResaltadas(texto).map(([p, r], i) => (
|
package/remotion/motor/capas.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {Img, OffthreadVideo} from 'remotion';
|
|
|
5
5
|
import {Avatar, Icono, tamIcono, useMarca} from '../piezas';
|
|
6
6
|
import {H, W, backOut, clamp, easeIO, easeOut, easeOutExpo, lerp, resorte} from '../estilo';
|
|
7
7
|
import {ANOTACIONES, ANOTACIONES_SIN_ENTRADA, Anotacion} from './anotaciones';
|
|
8
|
+
import {FICHAS, Fichas} from './fichas';
|
|
8
9
|
import {azar, cuando, formatoNumero, partesResaltadas, recurso, type CtxEscena} from './util';
|
|
9
10
|
import {Particulas} from './fondo';
|
|
10
11
|
import {Escena3D, Modelo3D} from './modelo3d';
|
|
@@ -278,6 +279,7 @@ const Contenido: React.FC<{c: Capa; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
278
279
|
}
|
|
279
280
|
default:
|
|
280
281
|
if (ANOTACIONES.includes(c.tipo)) return <Anotacion c={c} ctx={ctx} />;
|
|
282
|
+
if (FICHAS.includes(c.tipo)) return <Fichas c={c} ctx={ctx} />;
|
|
281
283
|
return <div style={{color: 'red', fontSize: 40, fontFamily: 'sans-serif', background: '#fff', padding: 10}}>capa desconocida: {c.tipo}</div>;
|
|
282
284
|
}
|
|
283
285
|
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Tarjetas de datos para explicar con cosas que se entienden de un vistazo (estilo maqueta/infografía):
|
|
2
|
+
// reloj (carátula que se llena: horas del día, minutos), termometro, recipientes (garrafones o vasos que se llenan),
|
|
3
|
+
// ficha (credencial con foto y campos), checklist (se palomea en las palabras), mapa (plano con ruta y distancia).
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import {Img} from 'remotion';
|
|
6
|
+
import {clamp, easeOut, lerp, resorte} from '../estilo';
|
|
7
|
+
import {cuando, formatoNumero, recurso, type CtxEscena} from './util';
|
|
8
|
+
|
|
9
|
+
type C = Record<string, any>;
|
|
10
|
+
const tarjeta = (c: C, extra: React.CSSProperties = {}): React.CSSProperties => ({
|
|
11
|
+
background: c.fondo ?? '#FFFFFF',
|
|
12
|
+
border: `${c.grosor ?? 6}px solid ${c.borde ?? '#1B1B1F'}`,
|
|
13
|
+
borderRadius: 30,
|
|
14
|
+
boxShadow: '0 14px 30px rgba(0,0,0,0.28)',
|
|
15
|
+
fontFamily: 'Montserrat, sans-serif',
|
|
16
|
+
color: c.colorTexto ?? '#1B1B1F',
|
|
17
|
+
...extra,
|
|
18
|
+
});
|
|
19
|
+
/** 0..1 desde que empieza la capa (o desde `en`) durante `duracion` segundos. */
|
|
20
|
+
const avance = (c: C, ctx: CtxEscena, dur = 1.2, retraso = 0.25) => easeOut(clamp((ctx.t - cuando(c.en, ctx, 0) - retraso) / (c.duracion ?? dur)));
|
|
21
|
+
|
|
22
|
+
/** Reloj: un sector que se llena de `de` a `a` (horas de 0 a `max`, 24 por defecto). `segmentos` pinta tramos de colores. */
|
|
23
|
+
export const Reloj: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
24
|
+
const tam = c.tam ?? 300;
|
|
25
|
+
const max = c.max ?? 24;
|
|
26
|
+
const k = avance(c, ctx, c.duracion ?? 1.4);
|
|
27
|
+
const de = c.de ?? 0;
|
|
28
|
+
const a = lerp(de, c.a ?? c.valor ?? max / 4, k);
|
|
29
|
+
const r = tam / 2 - 22;
|
|
30
|
+
const cx = tam / 2;
|
|
31
|
+
const sector = (h0: number, h1: number, color: string, key: number) => {
|
|
32
|
+
if (h1 <= h0) return null;
|
|
33
|
+
const a0 = (h0 / max) * Math.PI * 2 - Math.PI / 2;
|
|
34
|
+
const a1 = (Math.min(h1, h0 + max * 0.9999) / max) * Math.PI * 2 - Math.PI / 2;
|
|
35
|
+
const p = (ang: number) => `${cx + Math.cos(ang) * r} ${cx + Math.sin(ang) * r}`;
|
|
36
|
+
return <path key={key} d={`M ${cx} ${cx} L ${p(a0)} A ${r} ${r} 0 ${a1 - a0 > Math.PI ? 1 : 0} 1 ${p(a1)} Z`} fill={color} />;
|
|
37
|
+
};
|
|
38
|
+
const segmentos: [number, number, string][] = c.segmentos ?? [[de, a, c.color ?? '#2D7BFF']];
|
|
39
|
+
const ang = (a / max) * 360;
|
|
40
|
+
return (
|
|
41
|
+
<div style={tarjeta(c, {width: tam + 40, padding: 20, borderRadius: 34, textAlign: 'center'})}>
|
|
42
|
+
<svg width={tam} height={tam}>
|
|
43
|
+
<circle cx={cx} cy={cx} r={r + 10} fill="#FFFFFF" stroke="#1B1B1F" strokeWidth={8} />
|
|
44
|
+
{segmentos.map(([h0, h1, color], i) => sector(h0, Math.min(h1, a), color, i))}
|
|
45
|
+
{Array.from({length: 12}, (_, i) => {
|
|
46
|
+
const g = (i / 12) * Math.PI * 2;
|
|
47
|
+
return <line key={i} x1={cx + Math.cos(g) * (r - 4)} y1={cx + Math.sin(g) * (r - 4)} x2={cx + Math.cos(g) * (r + 6)} y2={cx + Math.sin(g) * (r + 6)} stroke="#1B1B1F" strokeWidth={i % 3 ? 3 : 6} />;
|
|
48
|
+
})}
|
|
49
|
+
<line x1={cx} y1={cx} x2={cx + Math.cos(((ang - 90) * Math.PI) / 180) * r * 0.85} y2={cx + Math.sin(((ang - 90) * Math.PI) / 180) * r * 0.85} stroke="#1B1B1F" strokeWidth={8} strokeLinecap="round" />
|
|
50
|
+
<circle cx={cx} cy={cx} r={11} fill="#1B1B1F" />
|
|
51
|
+
</svg>
|
|
52
|
+
{c.etiqueta !== false ? <div style={{fontWeight: 900, fontSize: tam * 0.12, marginTop: 6}}>{c.etiqueta ?? `${max} H`}</div> : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Cronómetro: cuenta de `de` a `a` segundos (o minutos con `unidad: 'min'`) y su aro avanza. */
|
|
58
|
+
export const Cronometro: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
59
|
+
const tam = c.tam ?? 280;
|
|
60
|
+
const k = avance(c, ctx, c.duracion ?? 1.6);
|
|
61
|
+
const v = lerp(c.de ?? 0, c.a ?? 60, k);
|
|
62
|
+
const total = c.max ?? c.a ?? 60;
|
|
63
|
+
const r = tam / 2 - 26;
|
|
64
|
+
const cx = tam / 2;
|
|
65
|
+
const largo = 2 * Math.PI * r;
|
|
66
|
+
const mm = Math.floor(v / 60);
|
|
67
|
+
const ss = Math.floor(v % 60);
|
|
68
|
+
const texto = c.unidad === 'min' ? `${Math.round(v)} min` : `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
|
|
69
|
+
return (
|
|
70
|
+
<div style={{position: 'relative', width: tam, height: tam + 40}}>
|
|
71
|
+
<div style={{position: 'absolute', left: tam / 2 - 26, top: 0, width: 52, height: 34, background: c.color ?? '#E5484D', borderRadius: 10, border: '5px solid #1B1B1F'}} />
|
|
72
|
+
<svg width={tam} height={tam} style={{position: 'absolute', top: 36}}>
|
|
73
|
+
<circle cx={cx} cy={cx} r={r + 16} fill="#FFFFFF" stroke="#1B1B1F" strokeWidth={8} />
|
|
74
|
+
<circle cx={cx} cy={cx} r={r} fill="none" stroke="#E9ECF2" strokeWidth={18} />
|
|
75
|
+
<circle cx={cx} cy={cx} r={r} fill="none" stroke={c.color ?? '#E5484D'} strokeWidth={18} strokeDasharray={`${largo * clamp(v / total)} ${largo}`} transform={`rotate(-90 ${cx} ${cx})`} strokeLinecap="round" />
|
|
76
|
+
<text x={cx} y={cx + tam * 0.06} textAnchor="middle" fontFamily="Montserrat, sans-serif" fontWeight={900} fontSize={tam * 0.17} fill="#1B1B1F">
|
|
77
|
+
{texto}
|
|
78
|
+
</text>
|
|
79
|
+
</svg>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Termómetro que sube hasta `valor` (de `min` a `max`). */
|
|
85
|
+
export const Termometro: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
86
|
+
const alto = c.alto ?? 420;
|
|
87
|
+
const min = c.min ?? 0;
|
|
88
|
+
const max = c.max ?? 50;
|
|
89
|
+
const k = avance(c, ctx, 1.3);
|
|
90
|
+
const v = lerp(c.de ?? min, c.valor ?? 38, k);
|
|
91
|
+
const nivel = clamp((v - min) / (max - min));
|
|
92
|
+
const color = c.color ?? (nivel > 0.66 ? '#E5484D' : nivel > 0.4 ? '#F5B841' : '#2D7BFF');
|
|
93
|
+
const tubo = alto - 110;
|
|
94
|
+
return (
|
|
95
|
+
<div style={tarjeta(c, {width: 170, padding: '24px 0 18px', display: 'flex', flexDirection: 'column', alignItems: 'center'})}>
|
|
96
|
+
<div style={{position: 'relative', width: 60, height: tubo, background: '#EEF1F6', border: '6px solid #1B1B1F', borderRadius: 30, overflow: 'hidden'}}>
|
|
97
|
+
<div style={{position: 'absolute', left: 0, right: 0, bottom: 0, height: `${nivel * 100}%`, background: color}} />
|
|
98
|
+
</div>
|
|
99
|
+
<div style={{width: 92, height: 92, marginTop: -14, borderRadius: '50%', background: color, border: '6px solid #1B1B1F', zIndex: 1}} />
|
|
100
|
+
{c.mostrar !== false ? (
|
|
101
|
+
<div style={{fontWeight: 900, fontSize: 40, marginTop: 10}}>
|
|
102
|
+
{formatoNumero(v, c.decimales ?? 0)}
|
|
103
|
+
{c.unidad ?? '°C'}
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Recipientes (garrafones, vasos, cubetas) que se llenan uno tras otro hasta `valor` de `n`; `total` debajo ("50 L"). */
|
|
111
|
+
export const Recipientes: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
112
|
+
const n = c.n ?? 3;
|
|
113
|
+
const valor = c.valor ?? n;
|
|
114
|
+
const k = avance(c, ctx, c.duracion ?? 0.6 * n);
|
|
115
|
+
const lleno = valor * k;
|
|
116
|
+
const color = c.color ?? '#3B8CFF';
|
|
117
|
+
const forma = c.forma ?? 'garrafon';
|
|
118
|
+
const w = c.tamPieza ?? 110;
|
|
119
|
+
const h = forma === 'vaso' ? w * 1.2 : w * 1.6;
|
|
120
|
+
return (
|
|
121
|
+
<div style={tarjeta(c, {padding: '26px 30px 18px', display: 'inline-flex', flexDirection: 'column', alignItems: 'center'})}>
|
|
122
|
+
<div style={{display: 'flex', gap: 22, alignItems: 'flex-end'}}>
|
|
123
|
+
{Array.from({length: n}, (_, i) => {
|
|
124
|
+
const nivel = clamp(lleno - i);
|
|
125
|
+
return (
|
|
126
|
+
<div key={i} style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
|
|
127
|
+
{forma === 'garrafon' ? <div style={{width: w * 0.32, height: w * 0.22, background: color, border: '5px solid #1B1B1F', borderRadius: 6, marginBottom: -5}} /> : null}
|
|
128
|
+
<div style={{position: 'relative', width: w, height: h, border: '6px solid #1B1B1F', borderRadius: forma === 'vaso' ? '8px 8px 24px 24px' : `${w * 0.3}px ${w * 0.3}px 18px 18px`, background: '#F4F8FF', overflow: 'hidden'}}>
|
|
129
|
+
<div style={{position: 'absolute', left: 0, right: 0, bottom: 0, height: `${nivel * 100}%`, background: color, opacity: 0.9}} />
|
|
130
|
+
<div style={{position: 'absolute', left: '18%', top: '12%', width: '14%', height: '60%', background: 'rgba(255,255,255,0.55)', borderRadius: 20}} />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
{c.total ? <div style={{fontWeight: 900, fontSize: 44, marginTop: 12}}>{c.total}</div> : null}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ficha o credencial: cabecera de color, foto (imagen/recurso) a la izquierda y campos [etiqueta, valor].
|
|
143
|
+
* campos: [['NOMBRE', 'Buey prieto'], ['SEXO', 'Macho']]
|
|
144
|
+
*/
|
|
145
|
+
export const Ficha: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
146
|
+
const ancho = c.ancho ?? 640;
|
|
147
|
+
const campos: [string, string][] = c.campos ?? [];
|
|
148
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
149
|
+
return (
|
|
150
|
+
<div style={tarjeta(c, {width: ancho, overflow: 'hidden', padding: 0, borderRadius: 26})}>
|
|
151
|
+
<div style={{background: c.color ?? '#7A4FD1', color: '#FFFFFF', fontWeight: 900, fontSize: 30, letterSpacing: 1.5, textAlign: 'center', padding: '12px 10px', textTransform: 'uppercase'}}>{c.titulo ?? 'Credencial'}</div>
|
|
152
|
+
<div style={{display: 'flex', gap: 22, padding: 22, alignItems: 'center'}}>
|
|
153
|
+
{c.foto ? (
|
|
154
|
+
<div style={{width: ancho * 0.3, height: ancho * 0.3, borderRadius: 18, border: '5px solid #1B1B1F', background: c.fondoFoto ?? '#EDE6F7', overflow: 'hidden', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
155
|
+
<Img src={recurso(c.foto)} style={{width: '90%', height: '90%', objectFit: 'contain'}} />
|
|
156
|
+
</div>
|
|
157
|
+
) : null}
|
|
158
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
|
159
|
+
{campos.map(([et, v], i) => {
|
|
160
|
+
const k = clamp((ctx.t - t0 - 0.3 - i * 0.18) / 0.3);
|
|
161
|
+
return (
|
|
162
|
+
<div key={i} style={{opacity: k, transform: `translateX(${(1 - k) * 20}px)`}}>
|
|
163
|
+
<div style={{fontSize: 20, fontWeight: 700, color: '#8A8A99', letterSpacing: 1, textTransform: 'uppercase'}}>{et}</div>
|
|
164
|
+
<div style={{fontSize: 34, fontWeight: 900, lineHeight: 1.1, textTransform: 'uppercase'}}>{v}</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Lista que se palomea: items [{texto, en: 'palabra'}] (o texto suelto). Los que no tienen `en` se palomean en orden.
|
|
176
|
+
* Con `pendiente: true` un item queda gris y sin palomita (lo que falta).
|
|
177
|
+
*/
|
|
178
|
+
export const Checklist: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
179
|
+
const items: {texto: string; en?: unknown; pendiente?: boolean}[] = (c.items ?? []).map((x: any) => (typeof x === 'string' ? {texto: x} : x));
|
|
180
|
+
const t0 = cuando(c.en, ctx, 0);
|
|
181
|
+
const tam = c.tam ?? 46;
|
|
182
|
+
return (
|
|
183
|
+
<div style={tarjeta(c, {padding: '22px 32px', display: 'inline-flex', flexDirection: 'column', gap: 10})}>
|
|
184
|
+
{c.titulo ? <div style={{fontWeight: 900, fontSize: tam * 0.7, color: '#8A8A99', letterSpacing: 1, textTransform: 'uppercase'}}>{c.titulo}</div> : null}
|
|
185
|
+
{items.map((it, i) => {
|
|
186
|
+
const tc = it.en != null ? cuando(it.en, ctx, 0) : t0 + 0.4 + i * (c.cada ?? 0.45);
|
|
187
|
+
const k = it.pendiente ? 0 : clamp((ctx.t - tc) / 0.25);
|
|
188
|
+
const color = it.pendiente ? '#B9BCC6' : c.color ?? '#2BB673';
|
|
189
|
+
return (
|
|
190
|
+
<div key={i} style={{display: 'flex', alignItems: 'center', gap: 18, fontWeight: 900, fontSize: tam, color: it.pendiente ? '#B9BCC6' : '#1B1B1F', textTransform: 'uppercase'}}>
|
|
191
|
+
<span style={{color}}>{it.texto}</span>
|
|
192
|
+
<svg width={tam * 0.9} height={tam * 0.9} viewBox="0 0 40 40" style={{transform: `scale(${resorte(k, 0.4)})`}}>
|
|
193
|
+
<path d="M 6 21 L 16 31 L 35 9" stroke={color} strokeWidth={7} fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
194
|
+
</svg>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Mapa mini: un plano inclinado con una ruta que se dibuja de `de` a `a` (coordenadas 0..1 del plano) y la distancia.
|
|
204
|
+
* `ruta`: puntos intermedios [[x, y], ...]; `distancia`: '850 M'.
|
|
205
|
+
*/
|
|
206
|
+
export const Mapa: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
207
|
+
const ancho = c.ancho ?? 620;
|
|
208
|
+
const alto = ancho * 0.62;
|
|
209
|
+
const k = avance(c, ctx, c.duracion ?? 1.4, 0.4);
|
|
210
|
+
const pts: [number, number][] = c.ruta ?? [
|
|
211
|
+
[0.12, 0.7],
|
|
212
|
+
[0.35, 0.45],
|
|
213
|
+
[0.6, 0.55],
|
|
214
|
+
[0.88, 0.25],
|
|
215
|
+
];
|
|
216
|
+
const d = pts.map(([x, y], i) => `${i ? 'L' : 'M'} ${x * ancho} ${y * alto}`).join(' ');
|
|
217
|
+
const largo = pts.reduce((a, [x, y], i) => (i ? a + Math.hypot((x - pts[i - 1][0]) * ancho, (y - pts[i - 1][1]) * alto) : 0), 0);
|
|
218
|
+
const fin = pts[pts.length - 1];
|
|
219
|
+
return (
|
|
220
|
+
<div style={{position: 'relative', width: ancho, height: alto, transform: `perspective(1400px) rotateX(${c.inclinar ?? 38}deg) rotateZ(-8deg)`, transformOrigin: 'center'}}>
|
|
221
|
+
<svg width={ancho} height={alto} style={{background: c.fondo ?? '#7CC46A', borderRadius: 18, border: '8px solid #FFFFFF', boxShadow: '0 30px 40px rgba(0,0,0,0.25)'}}>
|
|
222
|
+
{Array.from({length: 40}, (_, i) => (
|
|
223
|
+
<circle key={i} cx={((i * 97) % 100) / 100 * ancho} cy={((i * 61) % 100) / 100 * alto} r={10 + (i % 4) * 4} fill="#5BA84C" opacity={0.7} />
|
|
224
|
+
))}
|
|
225
|
+
<path d={d} stroke={c.color ?? '#2D7BFF'} strokeWidth={22} fill="none" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={`${largo * k} ${largo}`} />
|
|
226
|
+
{k > 0.98 ? <circle cx={fin[0] * ancho} cy={fin[1] * alto} r={18} fill="#E5484D" stroke="#FFFFFF" strokeWidth={6} /> : null}
|
|
227
|
+
</svg>
|
|
228
|
+
{c.distancia ? (
|
|
229
|
+
<div style={{position: 'absolute', left: 0, right: 0, top: alto * 0.08, textAlign: 'center', fontFamily: 'Montserrat, sans-serif', fontWeight: 900, fontSize: 54, color: '#FFFFFF', textShadow: '0 4px 0 rgba(0,0,0,0.35)', opacity: k}}>
|
|
230
|
+
{c.distancia}
|
|
231
|
+
</div>
|
|
232
|
+
) : null}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const FICHAS = ['reloj', 'cronometro', 'termometro', 'recipientes', 'ficha', 'checklist', 'mapa'];
|
|
238
|
+
export const Fichas: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
239
|
+
switch (c.tipo) {
|
|
240
|
+
case 'reloj':
|
|
241
|
+
return <Reloj c={c} ctx={ctx} />;
|
|
242
|
+
case 'cronometro':
|
|
243
|
+
return <Cronometro c={c} ctx={ctx} />;
|
|
244
|
+
case 'termometro':
|
|
245
|
+
return <Termometro c={c} ctx={ctx} />;
|
|
246
|
+
case 'recipientes':
|
|
247
|
+
return <Recipientes c={c} ctx={ctx} />;
|
|
248
|
+
case 'ficha':
|
|
249
|
+
return <Ficha c={c} ctx={ctx} />;
|
|
250
|
+
case 'checklist':
|
|
251
|
+
return <Checklist c={c} ctx={ctx} />;
|
|
252
|
+
default:
|
|
253
|
+
return <Mapa c={c} ctx={ctx} />;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
@@ -19,7 +19,7 @@ import {OBJETOS_3D, Objeto3D} from './objetos3d';
|
|
|
19
19
|
const RAD = Math.PI / 180;
|
|
20
20
|
type Gltf = {scene: THREE.Group; animations: THREE.AnimationClip[]};
|
|
21
21
|
const cache = new Map<string, Promise<Gltf>>();
|
|
22
|
-
const cargarGltf = (url: string) => {
|
|
22
|
+
export const cargarGltf = (url: string) => {
|
|
23
23
|
if (!cache.has(url)) cache.set(url, new GLTFLoader().loadAsync(url).then((g) => ({scene: g.scene, animations: g.animations})));
|
|
24
24
|
return cache.get(url)!;
|
|
25
25
|
};
|
|
@@ -74,7 +74,7 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
|
|
|
74
74
|
if (!g || t < g[0][1] - 0.12) return null;
|
|
75
75
|
const entrada = clamp((t - (g[0][1] - 0.12)) / 0.18);
|
|
76
76
|
const clave = claveDe(g, claves);
|
|
77
|
-
const texto = (w: string) => (S.mayusculas ? w.toUpperCase() : w);
|
|
77
|
+
const texto = (w: string) => (S.mayusculas === 'inicial' ? w.charAt(0).toUpperCase() + w.slice(1) : S.mayusculas ? w.toUpperCase() : w);
|
|
78
78
|
|
|
79
79
|
if (S.tipo === 'palabra') {
|
|
80
80
|
const [w] = g[0];
|
|
@@ -82,7 +82,7 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
|
|
|
82
82
|
const acento = claveDe([g[0]], claves) === 0;
|
|
83
83
|
return (
|
|
84
84
|
<div style={{position: 'absolute', left: 40, width: W - 80, top: S.y, transform: `translateY(-50%) scale(${resorte(entrada, 0.25)})`, textAlign: 'center'}}>
|
|
85
|
-
<span style={{fontFamily: tema.fuentes.texto, fontWeight: 900, fontSize: S.tam * 1.25, color: acento ? tema.colores.acento : S.color, WebkitTextStroke: `${S.contorno ?? 9}px #000`, paintOrder: 'stroke fill', textShadow: '0 6px 0 rgba(0,0,0,0.35)'}}>
|
|
85
|
+
<span style={{fontFamily: tema.fuentes.texto, fontWeight: 900, fontSize: S.tam * 1.25, color: acento ? tema.colores.acento : S.color, WebkitTextStroke: S.contorno === 0 ? undefined : `${S.contorno ?? 9}px #000`, paintOrder: 'stroke fill', textShadow: S.contorno === 0 ? '0 3px 3px rgba(0,0,0,0.45), 0 6px 22px rgba(0,0,0,0.35)' : '0 6px 0 rgba(0,0,0,0.35)'}}>
|
|
86
86
|
{texto(w)}
|
|
87
87
|
</span>
|
|
88
88
|
</div>
|
package/remotion/motor/tema.ts
CHANGED
|
@@ -38,7 +38,9 @@ export type Tema = {
|
|
|
38
38
|
* palabra: una palabra a la vez · caja: la frase completa en una etiqueta clara.
|
|
39
39
|
*/
|
|
40
40
|
tipo: 'karaoke' | 'seleccion' | 'pop' | 'bloque' | 'palabra' | 'caja' | 'ninguno';
|
|
41
|
-
|
|
41
|
+
/** true = MAYÚSCULAS · 'inicial' = Primera Letra (como en los videos de maqueta) */
|
|
42
|
+
mayusculas?: boolean | 'inicial';
|
|
43
|
+
/** 0 = sin contorno: sombra suave en su lugar */
|
|
42
44
|
contorno?: number;
|
|
43
45
|
fondo?: string; // caja detrás de la frase (karaoke)
|
|
44
46
|
y: number; // centro vertical (px de 1920)
|
|
@@ -58,6 +60,8 @@ export type Tema = {
|
|
|
58
60
|
desenfoqueBordes?: number; // px de blur arriba y abajo (efecto maqueta / profundidad)
|
|
59
61
|
vinetaColor?: string;
|
|
60
62
|
};
|
|
63
|
+
/** Rótulo grande de cada escena: 'normal' (color del texto), 'contorno' (blanco con contorno oscuro, clave en acento) o 'pastilla' (caja oscura inclinada). */
|
|
64
|
+
rotulo?: {estilo?: 'normal' | 'contorno' | 'pastilla'; inclinar?: number; y?: number; tam?: number};
|
|
61
65
|
marca: 'esquina' | 'arriba' | 'ninguna';
|
|
62
66
|
/** Movimiento suave de cámara por defecto en cada escena (zoom de 1 a este valor). */
|
|
63
67
|
zoomLento: number;
|
|
@@ -197,6 +201,21 @@ export const ESTILOS: Record<string, Tema> = {
|
|
|
197
201
|
marca: 'esquina',
|
|
198
202
|
zoomLento: 1.04,
|
|
199
203
|
},
|
|
204
|
+
// Maqueta 3D low-poly explicada (diorama con corte de tierra): una palabra a la vez al centro, rótulos con contorno,
|
|
205
|
+
// etiquetas con flecha pegadas a los objetos y cortes rápidos entre ángulos del mismo mundo
|
|
206
|
+
maqueta: {
|
|
207
|
+
fondo: {tipo: 'degradado', colores: ['#BFE3FF', '#EAF6FF']},
|
|
208
|
+
vineta: 0.06,
|
|
209
|
+
colores: {texto: '#FFFFFF', acento: '#FFC928', acento2: '#2D7BFF', suave: '#E8EEF6', tarjeta: '#FFFFFF', tarjetaTexto: '#1B1B1F', borde: '#1B1B1F'},
|
|
210
|
+
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
211
|
+
tituloMayusculas: true,
|
|
212
|
+
subtitulos: {tipo: 'palabra', y: 1010, tam: 52, maxPalabras: 1, mayusculas: 'inicial', contorno: 0, color: '#FFFFFF', pendiente: '#FFFFFF', resalte: ''},
|
|
213
|
+
rotulo: {estilo: 'contorno', inclinar: -2, y: 330, tam: 92},
|
|
214
|
+
transicion: 'corte',
|
|
215
|
+
movimiento: {entrada: 0.4, salida: 0.25, transicion: 0.2, curva: 'resorte'},
|
|
216
|
+
marca: 'ninguna',
|
|
217
|
+
zoomLento: 1.03,
|
|
218
|
+
},
|
|
200
219
|
// Oscuro y limpio, para lo que sea
|
|
201
220
|
minimal: {
|
|
202
221
|
fondo: {tipo: 'degradado', colores: ['#15151C', '#08080C']},
|