morfeo 0.1.2 → 0.1.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/index.d.ts +2 -2
- package/lib/diccionario.mjs +5 -2
- package/lib/recursos.mjs +22 -11
- package/package.json +1 -1
- package/remotion/motor/modelo3d.tsx +47 -5
- package/remotion/motor/subtitulos.tsx +2 -2
package/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type Capa = CapaBase &
|
|
|
77
77
|
| {tipo: 'marca'; tam?: number}
|
|
78
78
|
| {tipo: 'particulas'; particulas?: 'burbujas' | 'polvo' | 'estrellas' | 'nieve' | 'confeti'; cantidad?: number; color?: string}
|
|
79
79
|
| {tipo: 'grupo'; capas?: Capa[]; capa?: Capa; repetir?: number; disposicion?: 'circulo' | 'fila' | 'columna' | 'cuadricula' | 'pila' | 'aleatorio'; radio?: number; espacio?: number; espacioV?: number; columnas?: number; orientar?: boolean | number; retraso?: number; area?: [number, number]; girarAzar?: number; desorden?: number}
|
|
80
|
-
| {tipo: 'modelo'; src?: string; forma?: 'moneda' | 'billete' | 'cubo' | 'esfera' | 'lingote' | Objeto3D; color?: string; metal?: number; rugosidad?: number; ancho?: number; alto?: number; tamano?: number; rotacion?: [number, number, number]; girar3d?: [number, number, number]; cantidad?: number; crecer?: boolean; duracion?: number; distancia?: number; camaraY?: number; luz?: number; reflejos?: number; /** tuberia */ puntos?: [number, number, number][]; radio?: number; material?: 'pvc' | 'cobre' | 'acero' | 'vidrio'; agua?: boolean; velocidad?: number; colorAgua?: string; gotas?: number; /** muro */ columnas?: number; filas?: number; colorLadrillo?: string; colorMezcla?: string; /** viga */ largo?: number; vertical?: boolean; /** escalera */ escalones?: number; /** suelo */ capas?: [string, number][]}
|
|
80
|
+
| {tipo: 'modelo'; src?: string; forma?: 'moneda' | 'billete' | 'cubo' | 'esfera' | 'lingote' | Objeto3D; color?: string; metal?: number; rugosidad?: number; ancho?: number; alto?: number; tamano?: number; rotacion?: [number, number, number]; girar3d?: [number, number, number]; cantidad?: number; crecer?: boolean; duracion?: number; distancia?: number; camaraY?: number; luz?: number; reflejos?: number; /** levanta hacia la cámara los modelos planos acostados (por defecto en recursos) */ levantar?: boolean; /** tuberia */ puntos?: [number, number, number][]; radio?: number; material?: 'pvc' | 'cobre' | 'acero' | 'vidrio'; agua?: boolean; velocidad?: number; colorAgua?: string; gotas?: number; /** muro */ columnas?: number; filas?: number; colorLadrillo?: string; colorMezcla?: string; /** viga */ largo?: number; vertical?: boolean; /** escalera */ escalones?: number; /** suelo */ capas?: [string, number][]}
|
|
81
81
|
| {tipo: 'componente'; nombre: string; props?: Record<string, unknown>}
|
|
82
82
|
| {tipo: 'personaje'; nombre: string; pose?: string; poses?: {en: Momento; pose: string}[]; hablar?: boolean; ancho?: number}
|
|
83
83
|
| {tipo: 'escena3d'; src: string; camara?: {en?: Momento; pos: [number, number, number]; mirar?: [number, number, number]; fov?: number}[]; animacion?: boolean | string | string[]; velocidad?: number; mostrar?: {nodo: string; en: Momento}[]; ocultar?: {nodo: string; en: Momento}[]; fondo?: string | null; niebla?: [number, number]; luz?: number; luzAmbiente?: number; sol?: [number, number, number]; reflejos?: number; sombras?: boolean; ancho?: number; alto?: number}
|
|
@@ -90,7 +90,7 @@ export type Capa = CapaBase &
|
|
|
90
90
|
| {tipo: 'onda'; barras?: number; ancho?: number; alto?: number; color?: string; siempre?: boolean}
|
|
91
91
|
| {tipo: 'globo'; texto: string; cola?: 'abajo' | 'arriba' | 'izq' | 'der' | 'ninguna'; fondo?: string; color?: string; borde?: string; tam?: number; maxAncho?: number}
|
|
92
92
|
| {tipo: 'seguir'; usuario?: string; texto?: string; pulsar?: number}
|
|
93
|
-
| {tipo: 'recurso'; buscar: string; estilo?: '2d' | '3d' | 'realista'; variante?: 'color' | 'clay' | 'glass' | 'kawaii' | 'retro' | 'pastel' | 'sticker' | 'skeuo' | 'plush' | 'anime' | 'luxe' | 'chrome' | 'liquid' | 'bauhaus' | 'brutal' | 'gothic' | 'linea'; fuente?: 'kenney' | 'polyhaven' | 'iconify' | 'polypizza'; indice?: number; color?: string; ancho?: number; alto?: number; girar3d?: [number, number, number]; rotacion?: [number, number, number]}
|
|
93
|
+
| {tipo: 'recurso'; buscar: string; estilo?: '2d' | '3d' | 'realista'; variante?: 'color' | 'clay' | 'glass' | 'kawaii' | 'retro' | 'pastel' | 'sticker' | 'skeuo' | 'plush' | 'anime' | 'luxe' | 'chrome' | 'liquid' | 'bauhaus' | 'brutal' | 'gothic' | 'linea' | 'plano'; fuente?: 'kenney' | 'polyhaven' | 'iconify' | 'polypizza'; indice?: number; color?: string; ancho?: number; alto?: number; girar3d?: [number, number, number]; rotacion?: [number, number, number]}
|
|
94
94
|
| {tipo: 'escenario'; nombre: Escenario; parte?: 'fondo' | 'frente'; [opcion: string]: unknown}
|
|
95
95
|
| {tipo: 'ventana'; diametro?: number; src?: string; capas?: Capa[]; borde?: 'metal' | 'ninguno' | (string & {}); grosor?: number; remaches?: number | false; zoom?: number | [number, number]; fondo?: string}
|
|
96
96
|
);
|
package/lib/diccionario.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export const TEMAS = {
|
|
|
19
19
|
casa: ['house', 'home'], cuarto: ['bedroom', 'room'], cama: ['bed'], sillon: ['sofa', 'couch', 'armchair'], sofa: ['sofa', 'couch'],
|
|
20
20
|
silla: ['chair'], mesa: ['table'], escritorio: ['desk'], lampara: ['lamp'], librero: ['bookcase', 'shelf'], refrigerador: ['fridge', 'refrigerator'],
|
|
21
21
|
estufa: ['stove', 'oven'], horno: ['oven'], lavabo: ['sink', 'bathroom sink'], fregadero: ['kitchen sink', 'sink'], inodoro: ['toilet'], banera: ['bathtub'],
|
|
22
|
-
regadera: ['shower'], espejo: ['mirror'], puerta: ['door'], ventana: ['window'], televisor: ['television', 'tv'], planta: ['plant', 'potted plant'],
|
|
22
|
+
regadera: ['shower'], espejo: ['mirror'], puerta: ['door'], ventana: ['window'], televisor: ['television', 'tv', 'monitor'], television: ['television', 'tv', 'monitor'], tele: ['television', 'tv', 'monitor'], tv: ['tv', 'television', 'monitor'], planta: ['plant', 'potted plant'],
|
|
23
23
|
alfombra: ['rug'], ropero: ['wardrobe', 'closet'], buro: ['nightstand'], cajon: ['drawer', 'cabinet'], lavadora: ['washer', 'washing machine'], microondas: ['microwave'], licuadora: ['blender'], ventilador: ['fan'], almohada: ['pillow'], cortina: ['curtain'], reloj: ['clock'], llave: ['key'], candado: ['padlock', 'lock'], cocina: ['kitchen'], bano: ['bathroom'],
|
|
24
24
|
},
|
|
25
25
|
construccion: {
|
|
@@ -69,7 +69,8 @@ export const TEMAS = {
|
|
|
69
69
|
tecnologia: {
|
|
70
70
|
computadora: ['computer', 'laptop'], laptop: ['laptop'], celular: ['phone', 'smartphone'], telefono: ['phone'], robot: ['robot'], chip: ['chip', 'microchip'],
|
|
71
71
|
bateria: ['battery'], enchufe: ['plug', 'socket'], foco: ['light bulb', 'bulb'], camara: ['camera'], audifonos: ['headphones'], microfono: ['microphone'],
|
|
72
|
-
control: ['gamepad', 'controller'], consola: ['console', '
|
|
72
|
+
control: ['gamepad', 'controller'], consola: ['game console', 'console', 'gamepad'], consola_portatil: ['handheld console', 'nintendo switch', 'tablet', 'gamepad'],
|
|
73
|
+
tablet: ['tablet'], monitor: ['monitor', 'screen'], pantalla: ['screen', 'monitor'], disco: ['disc', 'cd', 'compact disc'], cd: ['disc', 'cd', 'compact disc'], dvd: ['disc', 'dvd'], memoria_usb: ['usb', 'flash drive'], antena: ['antenna'], satelite: ['satellite'], dron: ['drone'], servidor: ['server'],
|
|
73
74
|
wifi: ['wifi'], nube_de_datos: ['cloud computing', 'cloud'], codigo: ['code'], teclado: ['keyboard'], raton_pc: ['computer mouse'],
|
|
74
75
|
},
|
|
75
76
|
espacio: {
|
|
@@ -97,6 +98,8 @@ export const TEMAS = {
|
|
|
97
98
|
juegos: {
|
|
98
99
|
videojuego: ['video game', 'gamepad'], dado: ['dice'], carta: ['playing card', 'card'], ajedrez: ['chess'], moneda_juego: ['coin'], corona: ['crown'], espada: ['sword'],
|
|
99
100
|
escudo: ['shield'], cofre_juego: ['chest'], gema: ['gem', 'crystal'], pocion: ['potion'], bandera: ['flag'], meta: ['finish line', 'flag'],
|
|
101
|
+
cartucho: ['game cartridge', 'cartridge', 'sd card', 'floppy disk'], joystick: ['joystick', 'gamepad'], arcade: ['arcade machine', 'arcade', 'gamepad'], jefe_final: ['monster', 'dragon', 'skull'],
|
|
102
|
+
vida: ['heart', 'life'], nivel: ['level', 'block', 'platform'], bloque: ['block', 'cube', 'brick'], llave_juego: ['key'], hongo: ['mushroom'], estrella_juego: ['star'],
|
|
100
103
|
},
|
|
101
104
|
fiestas: {
|
|
102
105
|
regalo: ['gift', 'present'], globo: ['balloon'], pastel_cumpleanos: ['birthday cake', 'cake'], arbol_navidad: ['christmas tree'], nieve: ['snow', 'snowman'],
|
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,16 +290,18 @@ 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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morfeo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -25,7 +25,41 @@ const cargarGltf = (url: string) => {
|
|
|
25
25
|
};
|
|
26
26
|
const cargar = (url: string) => cargarGltf(url).then((g) => g.scene);
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Caja del "cuerpo" del modelo: si casi todos los vértices se juntan en una zona pequeña y el resto son
|
|
30
|
+
* hilos sueltos (el cable de un control, la cuerda de un globo), encuadra esa zona y no la caja completa.
|
|
31
|
+
*/
|
|
32
|
+
function cajaPrincipal(obj: THREE.Object3D): THREE.Box3 {
|
|
33
|
+
const completa = new THREE.Box3().setFromObject(obj);
|
|
34
|
+
const pts: number[] = [];
|
|
35
|
+
const v = new THREE.Vector3();
|
|
36
|
+
obj.updateMatrixWorld(true);
|
|
37
|
+
obj.traverse((o) => {
|
|
38
|
+
const m = o as THREE.Mesh;
|
|
39
|
+
const pos = m.isMesh ? (m.geometry?.attributes?.position as THREE.BufferAttribute | undefined) : undefined;
|
|
40
|
+
if (!pos) return;
|
|
41
|
+
const paso = Math.max(1, Math.floor(pos.count / 20000));
|
|
42
|
+
for (let i = 0; i < pos.count; i += paso) {
|
|
43
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(m.matrixWorld);
|
|
44
|
+
pts.push(v.x, v.y, v.z);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (pts.length < 300) return completa;
|
|
48
|
+
const N = 10;
|
|
49
|
+
const mn = completa.min.toArray();
|
|
50
|
+
const tam = completa.getSize(new THREE.Vector3()).toArray();
|
|
51
|
+
const celda = (i: number) => [0, 1, 2].map((k) => Math.min(N - 1, Math.floor(((pts[i + k] - mn[k]) / (tam[k] || 1)) * N))).join();
|
|
52
|
+
const cuenta = new Map<string, number>();
|
|
53
|
+
for (let i = 0; i < pts.length; i += 3) cuenta.set(celda(i), (cuenta.get(celda(i)) ?? 0) + 1);
|
|
54
|
+
const max = Math.max(...cuenta.values());
|
|
55
|
+
const caja = new THREE.Box3();
|
|
56
|
+
for (let i = 0; i < pts.length; i += 3) if ((cuenta.get(celda(i)) ?? 0) >= max * 0.2) caja.expandByPoint(v.set(pts[i], pts[i + 1], pts[i + 2]));
|
|
57
|
+
const lado = (b: THREE.Box3) => Math.max(...b.getSize(new THREE.Vector3()).toArray());
|
|
58
|
+
// solo si el cuerpo es mucho más chico que todo: en modelos normales la caja densa es casi la completa
|
|
59
|
+
return lado(caja) < 0.5 * lado(completa) ? caja.expandByScalar(lado(caja) * 0.08) : completa;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Glb: React.FC<{src: string; levantar?: boolean}> = ({src, levantar}) => {
|
|
29
63
|
const url = recurso(src);
|
|
30
64
|
const [escena, setEscena] = useState<THREE.Group | null>(null);
|
|
31
65
|
const [handle] = useState(() => delayRender(`Cargando modelo ${src}`));
|
|
@@ -34,20 +68,28 @@ const Glb: React.FC<{src: string}> = ({src}) => {
|
|
|
34
68
|
.then((s) => {
|
|
35
69
|
// centrado y escalado a ~2 unidades para que cualquier modelo quepa en el lienzo
|
|
36
70
|
const copia = s.clone(true);
|
|
37
|
-
const caja = new THREE.Box3().setFromObject(copia);
|
|
71
|
+
const caja = levantar ? cajaPrincipal(copia) : new THREE.Box3().setFromObject(copia);
|
|
38
72
|
const tam = caja.getSize(new THREE.Vector3());
|
|
39
73
|
const centro = caja.getCenter(new THREE.Vector3());
|
|
40
74
|
const k = 2 / Math.max(tam.x, tam.y, tam.z, 0.0001);
|
|
41
75
|
copia.position.sub(centro.multiplyScalar(k));
|
|
42
76
|
copia.scale.setScalar(k);
|
|
43
|
-
|
|
77
|
+
// objetos planos acostados (un control, un libro, un plato): de frente se ven como una raya,
|
|
78
|
+
// así que se levantan hacia la cámara
|
|
79
|
+
const plano = levantar && tam.y < 0.3 * Math.max(tam.x, tam.z);
|
|
80
|
+
if (plano) {
|
|
81
|
+
const g = new THREE.Group();
|
|
82
|
+
g.add(copia);
|
|
83
|
+
g.rotation.x = 65 * RAD;
|
|
84
|
+
setEscena(g);
|
|
85
|
+
} else setEscena(copia);
|
|
44
86
|
continueRender(handle);
|
|
45
87
|
})
|
|
46
88
|
.catch((e) => {
|
|
47
89
|
continueRender(handle);
|
|
48
90
|
throw new Error(`No pude cargar el modelo 3D ${src}: ${e.message}`);
|
|
49
91
|
});
|
|
50
|
-
}, [url, handle, src]);
|
|
92
|
+
}, [url, handle, src, levantar]);
|
|
51
93
|
return escena ? <primitive object={escena} /> : null;
|
|
52
94
|
};
|
|
53
95
|
|
|
@@ -137,7 +179,7 @@ export const Modelo3D: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c,
|
|
|
137
179
|
<directionalLight position={[-4, 2, -3]} intensity={0.4} color="#9fc4ff" />
|
|
138
180
|
<group scale={c.tamano ?? 1} rotation={[rx + gx * t, ry + gy * t, rz + gz * t]} position={[0, n > 1 ? -altoPila / 2 : 0, 0]}>
|
|
139
181
|
{c.src ? (
|
|
140
|
-
<Glb src={c.src} />
|
|
182
|
+
<Glb src={c.src} levantar={c.levantar} />
|
|
141
183
|
) : (OBJETOS_3D as readonly string[]).includes(c.forma) ? (
|
|
142
184
|
<Objeto3D forma={c.forma} t={t} opciones={c} color={c.color} metal={c.metal} rugosidad={c.rugosidad} />
|
|
143
185
|
) : (
|
|
@@ -80,11 +80,11 @@ export const Subtitulos: React.FC<{tema: Tema; palabras: Palabra[]; t: number; d
|
|
|
80
80
|
|
|
81
81
|
if (S.tipo === 'pop') {
|
|
82
82
|
return (
|
|
83
|
-
<div style={{position: 'absolute', left: 60, width: W - 120, top: S.y, transform: `translateY(-50%) scale(${backOut(entrada)})`, display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap:
|
|
83
|
+
<div style={{position: 'absolute', left: 60, width: W - 120, top: S.y, transform: `translateY(-50%) scale(${backOut(entrada)})`, display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: `0 ${Math.round(S.tam * 1.5 * 0.4 + 20)}px`}}>
|
|
84
84
|
{g.map(([w, a, b], i) => {
|
|
85
85
|
const activa = t >= a && t < b + 0.05;
|
|
86
86
|
return (
|
|
87
|
-
<span key={i} style={{fontFamily: tema.fuentes.titulo, fontWeight: 900, fontSize: S.tam * 1.5, color: activa ? tema.colores.acento : S.color, WebkitTextStroke: '10px #000', paintOrder: 'stroke fill', textTransform: 'uppercase', transform: `scale(${activa ? 1.1 : 1})`}}>
|
|
87
|
+
<span key={i} style={{fontFamily: tema.fuentes.titulo, fontWeight: 900, fontSize: S.tam * 1.5, color: activa ? tema.colores.acento : S.color, WebkitTextStroke: '10px #000', paintOrder: 'stroke fill', textTransform: 'uppercase', display: 'inline-block', transformOrigin: 'center bottom', transform: `scale(${activa ? 1.1 : 1})`}}>
|
|
88
88
|
{w}
|
|
89
89
|
</span>
|
|
90
90
|
);
|