morfeo 0.1.1 → 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/README.md +1 -0
- package/bin/morfeo.mjs +26 -1
- package/index.d.ts +13 -4
- package/lib/config.mjs +38 -3
- package/lib/diccionario.mjs +132 -0
- package/lib/guion.mjs +25 -7
- package/lib/recursos.mjs +310 -0
- package/lib/render.mjs +29 -3
- package/package.json +1 -1
- package/plantilla/videos.mjs +1 -0
- package/remotion/motor/Video.tsx +1 -1
- package/remotion/motor/capas.tsx +10 -0
- package/remotion/motor/escenarios/Bosque.tsx +157 -0
- package/remotion/motor/escenarios/Cuarto.tsx +103 -0
- package/remotion/motor/escenarios/Noche.tsx +163 -0
- package/remotion/motor/escenarios/index.tsx +26 -0
- package/remotion/motor/fondo.tsx +16 -4
- package/remotion/motor/modelo3d.tsx +51 -6
- package/remotion/motor/objetos3d.tsx +548 -0
- package/remotion/motor/plantillas.tsx +7 -3
- package/remotion/motor/subtitulos.tsx +2 -2
- package/remotion/motor/tema.ts +5 -4
- package/remotion/motor/widgets.tsx +1 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// Catálogo de objetos 3D hechos en código (Three.js), para no depender de emojis:
|
|
2
|
+
// {tipo: 'modelo', forma: 'corazon'} (todas las capas `modelo` aceptan color, metal, rugosidad, girar3d...)
|
|
3
|
+
// Cada objeto mide ~2 unidades y viene centrado. Materiales con brillo: se ven bien con los reflejos del entorno.
|
|
4
|
+
import React, {useMemo} from 'react';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
|
|
7
|
+
export const OBJETOS_3D = [
|
|
8
|
+
'corazon', 'estrella', 'rayo', 'diamante', 'trofeo', 'cohete', 'foco', 'reloj', 'planeta', 'arbol',
|
|
9
|
+
'nube', 'regalo', 'llave', 'candado', 'casa', 'libro', 'globo', 'medalla', 'escudo', 'pin',
|
|
10
|
+
// construcción (aceptan opciones: ver más abajo)
|
|
11
|
+
'tuberia', 'muro', 'viga', 'columna', 'escalera', 'losa', 'tanque', 'suelo',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type M = {color?: string; metal?: number; rugosidad?: number};
|
|
15
|
+
const Mat: React.FC<{c: string; m?: number; r?: number; e?: number; o?: number} & M> = ({c, m = 0.1, r = 0.4, e = 0, o, color, metal, rugosidad}) => (
|
|
16
|
+
<meshStandardMaterial color={color ?? c} metalness={metal ?? m} roughness={rugosidad ?? r} emissive={color ?? c} emissiveIntensity={e} transparent={o != null} opacity={o ?? 1} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** Extrusión con bisel a partir de una figura 2D. */
|
|
20
|
+
const Extruido: React.FC<{figura: THREE.Shape; prof?: number} & M & {c: string; m?: number; r?: number; e?: number}> = ({figura, prof = 0.45, ...mat}) => {
|
|
21
|
+
const geo = useMemo(() => {
|
|
22
|
+
const g = new THREE.ExtrudeGeometry(figura, {depth: prof, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.1, bevelSegments: 6, curveSegments: 24});
|
|
23
|
+
g.center();
|
|
24
|
+
return g;
|
|
25
|
+
}, [figura, prof]);
|
|
26
|
+
return (
|
|
27
|
+
<mesh geometry={geo}>
|
|
28
|
+
<Mat {...mat} />
|
|
29
|
+
</mesh>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const figuraCorazon = () => {
|
|
34
|
+
const s = new THREE.Shape();
|
|
35
|
+
s.moveTo(0, -1);
|
|
36
|
+
s.bezierCurveTo(-0.2, -0.75, -1.1, -0.35, -1.1, 0.25);
|
|
37
|
+
s.bezierCurveTo(-1.1, 0.8, -0.45, 1.05, 0, 0.6);
|
|
38
|
+
s.bezierCurveTo(0.45, 1.05, 1.1, 0.8, 1.1, 0.25);
|
|
39
|
+
s.bezierCurveTo(1.1, -0.35, 0.2, -0.75, 0, -1);
|
|
40
|
+
return s;
|
|
41
|
+
};
|
|
42
|
+
const figuraEstrella = (puntas = 5, ext = 1.1, int = 0.48) => {
|
|
43
|
+
const s = new THREE.Shape();
|
|
44
|
+
for (let i = 0; i < puntas * 2; i++) {
|
|
45
|
+
const r = i % 2 ? int : ext;
|
|
46
|
+
const a = (i / (puntas * 2)) * Math.PI * 2 + Math.PI / 2;
|
|
47
|
+
if (i === 0) s.moveTo(Math.cos(a) * r, Math.sin(a) * r);
|
|
48
|
+
else s.lineTo(Math.cos(a) * r, Math.sin(a) * r);
|
|
49
|
+
}
|
|
50
|
+
s.closePath();
|
|
51
|
+
return s;
|
|
52
|
+
};
|
|
53
|
+
const figuraRayo = () => {
|
|
54
|
+
const s = new THREE.Shape();
|
|
55
|
+
[[0.25, 1.1], [-0.55, -0.05], [-0.05, -0.05], [-0.3, -1.1], [0.6, 0.15], [0.1, 0.15], [0.45, 1.1]].forEach(([x, y], i) => (i ? s.lineTo(x, y) : s.moveTo(x, y)));
|
|
56
|
+
s.closePath();
|
|
57
|
+
return s;
|
|
58
|
+
};
|
|
59
|
+
const figuraEscudo = () => {
|
|
60
|
+
const s = new THREE.Shape();
|
|
61
|
+
s.moveTo(0, 1.05);
|
|
62
|
+
s.bezierCurveTo(0.45, 0.85, 0.8, 0.9, 0.95, 0.85);
|
|
63
|
+
s.bezierCurveTo(1.0, 0.0, 0.6, -0.7, 0, -1.1);
|
|
64
|
+
s.bezierCurveTo(-0.6, -0.7, -1.0, 0.0, -0.95, 0.85);
|
|
65
|
+
s.bezierCurveTo(-0.8, 0.9, -0.45, 0.85, 0, 1.05);
|
|
66
|
+
return s;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Torno: perfil [radio, altura] girado alrededor del eje Y (copas, focos, globos). */
|
|
70
|
+
const Torno: React.FC<{perfil: [number, number][]} & M & {c: string; m?: number; r?: number; e?: number; o?: number}> = ({perfil, ...mat}) => {
|
|
71
|
+
const geo = useMemo(() => new THREE.LatheGeometry(perfil.map(([x, y]) => new THREE.Vector2(x, y)), 48), [perfil]);
|
|
72
|
+
return (
|
|
73
|
+
<mesh geometry={geo}>
|
|
74
|
+
<Mat {...mat} />
|
|
75
|
+
</mesh>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Tubería por puntos con codos redondeados. Opciones: puntos [[x,y,z]...], radio, material ('pvc' | 'cobre' | 'acero' | 'vidrio'),
|
|
81
|
+
* agua (true: gotas que fluyen por dentro; con vidrio se ven), velocidad, colorAgua.
|
|
82
|
+
*/
|
|
83
|
+
const Tuberia: React.FC<{t: number; o: Record<string, any>} & M> = ({t, o, ...m}) => {
|
|
84
|
+
const puntos: [number, number, number][] = o.puntos ?? [[-1.7, 1.2, 0], [-1.7, -0.4, 0], [-0.7, -0.9, 0], [0.3, -0.4, 0], [0.3, 0.3, 0], [1.8, 0.3, 0]];
|
|
85
|
+
const radio = o.radio ?? 0.16;
|
|
86
|
+
const curva = useMemo(() => new THREE.CatmullRomCurve3(puntos.map((p) => new THREE.Vector3(...p)), false, 'catmullrom', 0.08), [JSON.stringify(puntos)]);
|
|
87
|
+
const geo = useMemo(() => new THREE.TubeGeometry(curva, 220, radio, 24, false), [curva, radio]);
|
|
88
|
+
const mats: Record<string, {c: string; m: number; r: number; o?: number}> = {pvc: {c: '#E8ECEF', m: 0.05, r: 0.35}, cobre: {c: '#C77A45', m: 0.85, r: 0.3}, acero: {c: '#9AA4B2', m: 0.85, r: 0.3}, vidrio: {c: '#7FB8DE', m: 0.15, r: 0.05, o: 0.5}};
|
|
89
|
+
const mat = mats[o.material ?? (o.agua ? 'vidrio' : 'pvc')] ?? mats.pvc;
|
|
90
|
+
const n = o.gotas ?? 28;
|
|
91
|
+
const vel = o.velocidad ?? 0.18;
|
|
92
|
+
return (
|
|
93
|
+
<group>
|
|
94
|
+
<mesh geometry={geo}>
|
|
95
|
+
<Mat c={mat.c} m={mat.m} r={mat.r} o={mat.o} {...m} />
|
|
96
|
+
</mesh>
|
|
97
|
+
{/* bridas en las uniones */}
|
|
98
|
+
{puntos.slice(1, -1).map((p, i) => (
|
|
99
|
+
<mesh key={i} position={p}>
|
|
100
|
+
<sphereGeometry args={[radio * 1.18, 20, 20]} />
|
|
101
|
+
<Mat c={mat.c} m={mat.m} r={mat.r} o={mat.o} />
|
|
102
|
+
</mesh>
|
|
103
|
+
))}
|
|
104
|
+
{o.agua
|
|
105
|
+
? Array.from({length: n}, (_, i) => {
|
|
106
|
+
const k = (((t * vel + i / n) % 1) + 1) % 1;
|
|
107
|
+
const p = curva.getPointAt(k);
|
|
108
|
+
return (
|
|
109
|
+
<mesh key={i} position={[p.x, p.y, p.z]}>
|
|
110
|
+
<sphereGeometry args={[radio * 0.62, 14, 14]} />
|
|
111
|
+
<Mat c={o.colorAgua ?? '#2E86FF'} e={0.25} r={0.15} />
|
|
112
|
+
</mesh>
|
|
113
|
+
);
|
|
114
|
+
})
|
|
115
|
+
: null}
|
|
116
|
+
</group>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Muro de ladrillos. Opciones: ancho, alto (en ladrillos: columnas, filas), colorLadrillo, colorMezcla. */
|
|
121
|
+
const Muro: React.FC<{o: Record<string, any>} & M> = ({o, ...m}) => {
|
|
122
|
+
const cols = o.columnas ?? 8;
|
|
123
|
+
const filas = o.filas ?? 9;
|
|
124
|
+
const [lw, lh] = [0.4, 0.18];
|
|
125
|
+
const ladrillos: [number, number][] = [];
|
|
126
|
+
for (let f = 0; f < filas; f++) for (let c = 0; c < cols + (f % 2); c++) ladrillos.push([(c - cols / 2 + (f % 2 ? -0.5 : 0) + 0.5) * (lw + 0.03), (f - filas / 2 + 0.5) * (lh + 0.03)]);
|
|
127
|
+
const ancho = cols * (lw + 0.03);
|
|
128
|
+
return (
|
|
129
|
+
<group>
|
|
130
|
+
<mesh position={[0, 0, -0.04]}>
|
|
131
|
+
<boxGeometry args={[ancho + 0.1, filas * (lh + 0.03) + 0.02, 0.12]} />
|
|
132
|
+
<Mat c={o.colorMezcla ?? '#CFC6B8'} r={0.9} />
|
|
133
|
+
</mesh>
|
|
134
|
+
{ladrillos.map(([x, y], i) => (
|
|
135
|
+
<mesh key={i} position={[Math.max(-ancho / 2 + lw / 4, Math.min(ancho / 2 - lw / 4, x)), y, 0.03]}>
|
|
136
|
+
<boxGeometry args={[Math.abs(x) > ancho / 2 - lw / 2 ? lw / 2 : lw, lh, 0.2]} />
|
|
137
|
+
<Mat c={o.colorLadrillo ?? ['#B5523B', '#A84A35', '#BF5E45'][i % 3]} r={0.85} {...m} />
|
|
138
|
+
</mesh>
|
|
139
|
+
))}
|
|
140
|
+
</group>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const Objeto3D: React.FC<{forma: string; t: number; opciones?: Record<string, any>} & M> = ({forma, t, opciones: o = {}, ...m}) => {
|
|
145
|
+
const figuras = useMemo(() => ({corazon: figuraCorazon(), estrella: figuraEstrella(), rayo: figuraRayo(), escudo: figuraEscudo(), medalla: figuraEstrella(12, 0.95, 0.82)}), []);
|
|
146
|
+
switch (forma) {
|
|
147
|
+
case 'corazon':
|
|
148
|
+
return <Extruido figura={figuras.corazon} c="#FF3B5C" r={0.3} m={0.15} {...m} />;
|
|
149
|
+
case 'estrella':
|
|
150
|
+
return <Extruido figura={figuras.estrella} c="#FFC93C" r={0.25} m={0.45} {...m} />;
|
|
151
|
+
case 'rayo':
|
|
152
|
+
return <Extruido figura={figuras.rayo} c="#FFD43B" r={0.3} m={0.3} e={0.15} {...m} />;
|
|
153
|
+
case 'escudo':
|
|
154
|
+
return (
|
|
155
|
+
<group>
|
|
156
|
+
<Extruido figura={figuras.escudo} c="#2D6BFF" r={0.3} m={0.35} {...m} />
|
|
157
|
+
<group position={[0, 0.05, 0.35]} scale={0.45}>
|
|
158
|
+
<Extruido figura={figuras.estrella} c="#FFFFFF" prof={0.15} r={0.3} />
|
|
159
|
+
</group>
|
|
160
|
+
</group>
|
|
161
|
+
);
|
|
162
|
+
case 'medalla':
|
|
163
|
+
return (
|
|
164
|
+
<group>
|
|
165
|
+
<mesh position={[-0.25, 0.95, -0.1]} rotation={[0, 0, 0.35]}>
|
|
166
|
+
<boxGeometry args={[0.35, 1.0, 0.05]} />
|
|
167
|
+
<Mat c="#2D6BFF" />
|
|
168
|
+
</mesh>
|
|
169
|
+
<mesh position={[0.25, 0.95, -0.1]} rotation={[0, 0, -0.35]}>
|
|
170
|
+
<boxGeometry args={[0.35, 1.0, 0.05]} />
|
|
171
|
+
<Mat c="#E5484D" />
|
|
172
|
+
</mesh>
|
|
173
|
+
<group position={[0, -0.25, 0]} scale={0.85}>
|
|
174
|
+
<Extruido figura={figuras.medalla} c="#E8B04A" m={0.75} r={0.28} prof={0.2} {...m} />
|
|
175
|
+
</group>
|
|
176
|
+
</group>
|
|
177
|
+
);
|
|
178
|
+
case 'diamante':
|
|
179
|
+
return (
|
|
180
|
+
<group rotation={[0.25, 0, 0]}>
|
|
181
|
+
<mesh position={[0, 0.35, 0]}>
|
|
182
|
+
<cylinderGeometry args={[0.7, 1.05, 0.5, 8]} />
|
|
183
|
+
<Mat c="#7FE3FF" m={0.6} r={0.05} {...m} />
|
|
184
|
+
</mesh>
|
|
185
|
+
<mesh position={[0, -0.5, 0]} rotation={[Math.PI, 0, 0]}>
|
|
186
|
+
<coneGeometry args={[1.05, 1.2, 8]} />
|
|
187
|
+
<Mat c="#4CC9F0" m={0.6} r={0.05} {...m} />
|
|
188
|
+
</mesh>
|
|
189
|
+
</group>
|
|
190
|
+
);
|
|
191
|
+
case 'trofeo':
|
|
192
|
+
return (
|
|
193
|
+
<group position={[0, -0.1, 0]}>
|
|
194
|
+
<Torno perfil={[[0, 0.15], [0.62, 0.2], [0.7, 0.6], [0.6, 1.1], [0.15, 1.15], [0.12, 0.05], [0.35, -0.55], [0.0, -0.55]]} c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
195
|
+
<mesh position={[-0.72, 0.75, 0]} rotation={[0, 0, Math.PI / 2]}>
|
|
196
|
+
<torusGeometry args={[0.28, 0.07, 12, 24, Math.PI]} />
|
|
197
|
+
<Mat c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
198
|
+
</mesh>
|
|
199
|
+
<mesh position={[0.72, 0.75, 0]} rotation={[0, 0, -Math.PI / 2]}>
|
|
200
|
+
<torusGeometry args={[0.28, 0.07, 12, 24, Math.PI]} />
|
|
201
|
+
<Mat c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
202
|
+
</mesh>
|
|
203
|
+
<mesh position={[0, -0.75, 0]}>
|
|
204
|
+
<boxGeometry args={[1.1, 0.35, 0.8]} />
|
|
205
|
+
<Mat c="#3A2A1A" r={0.6} />
|
|
206
|
+
</mesh>
|
|
207
|
+
</group>
|
|
208
|
+
);
|
|
209
|
+
case 'cohete':
|
|
210
|
+
return (
|
|
211
|
+
<group rotation={[0, 0, -0.5]}>
|
|
212
|
+
<mesh>
|
|
213
|
+
<cylinderGeometry args={[0.42, 0.42, 1.4, 32]} />
|
|
214
|
+
<Mat c="#F2F4F7" r={0.3} {...m} />
|
|
215
|
+
</mesh>
|
|
216
|
+
<mesh position={[0, 1.0, 0]}>
|
|
217
|
+
<coneGeometry args={[0.42, 0.65, 32]} />
|
|
218
|
+
<Mat c="#E5484D" r={0.3} />
|
|
219
|
+
</mesh>
|
|
220
|
+
<mesh position={[0, 0.25, 0.4]}>
|
|
221
|
+
<circleGeometry args={[0.18, 32]} />
|
|
222
|
+
<Mat c="#2D6BFF" m={0.5} r={0.1} />
|
|
223
|
+
</mesh>
|
|
224
|
+
{[0, 1, 2].map((i) => (
|
|
225
|
+
<mesh key={i} position={[Math.cos((i * Math.PI * 2) / 3) * 0.45, -0.6, Math.sin((i * Math.PI * 2) / 3) * 0.45]} rotation={[0, -(i * Math.PI * 2) / 3, 0]}>
|
|
226
|
+
<boxGeometry args={[0.35, 0.5, 0.06]} />
|
|
227
|
+
<Mat c="#E5484D" />
|
|
228
|
+
</mesh>
|
|
229
|
+
))}
|
|
230
|
+
{/* fuego que parpadea con el tiempo */}
|
|
231
|
+
<mesh position={[0, -0.95 - 0.08 * Math.sin(t * 30), 0]} rotation={[Math.PI, 0, 0]}>
|
|
232
|
+
<coneGeometry args={[0.28, 0.6 + 0.15 * Math.abs(Math.sin(t * 23)), 24]} />
|
|
233
|
+
<Mat c="#FF8A00" e={1} r={0.6} />
|
|
234
|
+
</mesh>
|
|
235
|
+
</group>
|
|
236
|
+
);
|
|
237
|
+
case 'foco':
|
|
238
|
+
return (
|
|
239
|
+
<group>
|
|
240
|
+
<Torno perfil={[[0, 1.15], [0.55, 1.0], [0.8, 0.55], [0.7, 0.05], [0.35, -0.35], [0.33, -0.45], [0, -0.45]]} c="#FFE27A" e={0.35 + 0.15 * Math.sin(t * 3)} r={0.15} o={0.92} {...m} />
|
|
241
|
+
<mesh position={[0, -0.75, 0]}>
|
|
242
|
+
<cylinderGeometry args={[0.34, 0.28, 0.6, 24]} />
|
|
243
|
+
<Mat c="#9AA4B2" m={0.8} r={0.3} />
|
|
244
|
+
</mesh>
|
|
245
|
+
</group>
|
|
246
|
+
);
|
|
247
|
+
case 'reloj':
|
|
248
|
+
return (
|
|
249
|
+
<group rotation={[Math.PI / 2, 0, 0]}>
|
|
250
|
+
<mesh>
|
|
251
|
+
<cylinderGeometry args={[1, 1, 0.25, 48]} />
|
|
252
|
+
<Mat c="#FFFFFF" r={0.4} {...m} />
|
|
253
|
+
</mesh>
|
|
254
|
+
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
|
255
|
+
<torusGeometry args={[1, 0.1, 16, 48]} />
|
|
256
|
+
<Mat c="#E5484D" r={0.3} m={0.3} />
|
|
257
|
+
</mesh>
|
|
258
|
+
{/* manecillas que giran con el tiempo */}
|
|
259
|
+
<group position={[0, 0.14, 0]} rotation={[0, -t * 0.6, 0]}>
|
|
260
|
+
<mesh position={[0, 0, -0.3]}>
|
|
261
|
+
<boxGeometry args={[0.08, 0.04, 0.6]} />
|
|
262
|
+
<Mat c="#141414" />
|
|
263
|
+
</mesh>
|
|
264
|
+
</group>
|
|
265
|
+
<group position={[0, 0.15, 0]} rotation={[0, -t * 4, 0]}>
|
|
266
|
+
<mesh position={[0, 0, -0.42]}>
|
|
267
|
+
<boxGeometry args={[0.05, 0.04, 0.84]} />
|
|
268
|
+
<Mat c="#141414" />
|
|
269
|
+
</mesh>
|
|
270
|
+
</group>
|
|
271
|
+
</group>
|
|
272
|
+
);
|
|
273
|
+
case 'planeta':
|
|
274
|
+
return (
|
|
275
|
+
<group rotation={[0.4, 0, 0.3]}>
|
|
276
|
+
<mesh>
|
|
277
|
+
<sphereGeometry args={[0.8, 48, 48]} />
|
|
278
|
+
<Mat c="#8B7BFF" r={0.5} {...m} />
|
|
279
|
+
</mesh>
|
|
280
|
+
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
|
281
|
+
<torusGeometry args={[1.2, 0.08, 8, 64]} />
|
|
282
|
+
<Mat c="#FFC93C" r={0.4} />
|
|
283
|
+
</mesh>
|
|
284
|
+
</group>
|
|
285
|
+
);
|
|
286
|
+
case 'arbol':
|
|
287
|
+
return (
|
|
288
|
+
<group position={[0, -0.2, 0]}>
|
|
289
|
+
<mesh position={[0, -0.75, 0]}>
|
|
290
|
+
<cylinderGeometry args={[0.18, 0.24, 0.6, 16]} />
|
|
291
|
+
<Mat c="#8A5636" r={0.8} />
|
|
292
|
+
</mesh>
|
|
293
|
+
{[0, 1, 2].map((i) => (
|
|
294
|
+
<mesh key={i} position={[0, -0.2 + i * 0.5, 0]}>
|
|
295
|
+
<coneGeometry args={[0.95 - i * 0.22, 0.85, 24]} />
|
|
296
|
+
<Mat c={['#2E8B57', '#36A267', '#46B978'][i]} r={0.6} {...m} />
|
|
297
|
+
</mesh>
|
|
298
|
+
))}
|
|
299
|
+
</group>
|
|
300
|
+
);
|
|
301
|
+
case 'nube':
|
|
302
|
+
return (
|
|
303
|
+
<group>
|
|
304
|
+
{[[-0.6, -0.1, 0.5], [0, 0.15, 0.7], [0.6, -0.05, 0.55], [0.25, -0.25, 0.45], [-0.25, -0.25, 0.45]].map(([x, y, r], i) => (
|
|
305
|
+
<mesh key={i} position={[x, y, 0]}>
|
|
306
|
+
<sphereGeometry args={[r, 32, 32]} />
|
|
307
|
+
<Mat c="#F4F7FB" r={0.7} {...m} />
|
|
308
|
+
</mesh>
|
|
309
|
+
))}
|
|
310
|
+
</group>
|
|
311
|
+
);
|
|
312
|
+
case 'regalo':
|
|
313
|
+
return (
|
|
314
|
+
<group>
|
|
315
|
+
<mesh position={[0, -0.25, 0]}>
|
|
316
|
+
<boxGeometry args={[1.4, 1.1, 1.4]} />
|
|
317
|
+
<Mat c="#E5484D" r={0.4} {...m} />
|
|
318
|
+
</mesh>
|
|
319
|
+
<mesh position={[0, 0.38, 0]}>
|
|
320
|
+
<boxGeometry args={[1.55, 0.25, 1.55]} />
|
|
321
|
+
<Mat c="#E5484D" r={0.4} {...m} />
|
|
322
|
+
</mesh>
|
|
323
|
+
<mesh position={[0, 0, 0]}>
|
|
324
|
+
<boxGeometry args={[0.25, 1.4, 1.6]} />
|
|
325
|
+
<Mat c="#FFC93C" m={0.4} r={0.3} />
|
|
326
|
+
</mesh>
|
|
327
|
+
<mesh position={[0, 0, 0]}>
|
|
328
|
+
<boxGeometry args={[1.6, 1.4, 0.25]} />
|
|
329
|
+
<Mat c="#FFC93C" m={0.4} r={0.3} />
|
|
330
|
+
</mesh>
|
|
331
|
+
{[-1, 1].map((s) => (
|
|
332
|
+
<mesh key={s} position={[s * 0.28, 0.68, 0]} rotation={[0, 0, s * 0.6]}>
|
|
333
|
+
<torusGeometry args={[0.25, 0.08, 12, 24]} />
|
|
334
|
+
<Mat c="#FFC93C" m={0.4} r={0.3} />
|
|
335
|
+
</mesh>
|
|
336
|
+
))}
|
|
337
|
+
</group>
|
|
338
|
+
);
|
|
339
|
+
case 'llave':
|
|
340
|
+
return (
|
|
341
|
+
<group rotation={[0, 0, -0.6]}>
|
|
342
|
+
<mesh position={[0, 0.7, 0]}>
|
|
343
|
+
<torusGeometry args={[0.38, 0.13, 16, 32]} />
|
|
344
|
+
<Mat c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
345
|
+
</mesh>
|
|
346
|
+
<mesh position={[0, -0.25, 0]}>
|
|
347
|
+
<cylinderGeometry args={[0.1, 0.1, 1.4, 16]} />
|
|
348
|
+
<Mat c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
349
|
+
</mesh>
|
|
350
|
+
{[-0.6, -0.85].map((y) => (
|
|
351
|
+
<mesh key={y} position={[0.17, y, 0]}>
|
|
352
|
+
<boxGeometry args={[0.25, 0.12, 0.12]} />
|
|
353
|
+
<Mat c="#E8B04A" m={0.8} r={0.25} {...m} />
|
|
354
|
+
</mesh>
|
|
355
|
+
))}
|
|
356
|
+
</group>
|
|
357
|
+
);
|
|
358
|
+
case 'candado':
|
|
359
|
+
return (
|
|
360
|
+
<group>
|
|
361
|
+
<mesh position={[0, 0.45, 0]}>
|
|
362
|
+
<torusGeometry args={[0.5, 0.12, 16, 32, Math.PI]} />
|
|
363
|
+
<Mat c="#B8C2CC" m={0.9} r={0.2} />
|
|
364
|
+
</mesh>
|
|
365
|
+
<mesh position={[0, -0.35, 0]}>
|
|
366
|
+
<boxGeometry args={[1.4, 1.1, 0.5]} />
|
|
367
|
+
<Mat c="#E8B04A" m={0.75} r={0.28} {...m} />
|
|
368
|
+
</mesh>
|
|
369
|
+
<mesh position={[0, -0.3, 0.26]}>
|
|
370
|
+
<cylinderGeometry args={[0.12, 0.12, 0.05, 24]} />
|
|
371
|
+
<Mat c="#3A2A1A" />
|
|
372
|
+
</mesh>
|
|
373
|
+
</group>
|
|
374
|
+
);
|
|
375
|
+
case 'casa':
|
|
376
|
+
return (
|
|
377
|
+
<group position={[0, -0.3, 0]}>
|
|
378
|
+
<mesh>
|
|
379
|
+
<boxGeometry args={[1.5, 1.1, 1.3]} />
|
|
380
|
+
<Mat c="#F2E6D0" r={0.6} {...m} />
|
|
381
|
+
</mesh>
|
|
382
|
+
<mesh position={[0, 0.95, 0]} rotation={[0, Math.PI / 4, 0]}>
|
|
383
|
+
<coneGeometry args={[1.25, 0.8, 4]} />
|
|
384
|
+
<Mat c="#E5484D" r={0.5} />
|
|
385
|
+
</mesh>
|
|
386
|
+
<mesh position={[0, -0.25, 0.66]}>
|
|
387
|
+
<boxGeometry args={[0.35, 0.6, 0.04]} />
|
|
388
|
+
<Mat c="#8A5636" r={0.6} />
|
|
389
|
+
</mesh>
|
|
390
|
+
<mesh position={[0.45, 0.15, 0.66]}>
|
|
391
|
+
<boxGeometry args={[0.3, 0.3, 0.04]} />
|
|
392
|
+
<Mat c="#7FE3FF" e={0.3} />
|
|
393
|
+
</mesh>
|
|
394
|
+
</group>
|
|
395
|
+
);
|
|
396
|
+
case 'libro':
|
|
397
|
+
return (
|
|
398
|
+
<group rotation={[0.3, 0.5, 0]}>
|
|
399
|
+
<mesh>
|
|
400
|
+
<boxGeometry args={[1.4, 1.8, 0.35]} />
|
|
401
|
+
<Mat c="#2D6BFF" r={0.5} {...m} />
|
|
402
|
+
</mesh>
|
|
403
|
+
<mesh position={[0.06, 0, 0]}>
|
|
404
|
+
<boxGeometry args={[1.32, 1.72, 0.3]} />
|
|
405
|
+
<Mat c="#FFFDF5" r={0.8} />
|
|
406
|
+
</mesh>
|
|
407
|
+
<mesh position={[0, 0, 0.18]}>
|
|
408
|
+
<boxGeometry args={[1.4, 1.8, 0.02]} />
|
|
409
|
+
<Mat c="#2D6BFF" r={0.5} {...m} />
|
|
410
|
+
</mesh>
|
|
411
|
+
</group>
|
|
412
|
+
);
|
|
413
|
+
case 'globo':
|
|
414
|
+
return (
|
|
415
|
+
<group position={[0, 0.2, 0]}>
|
|
416
|
+
<mesh scale={[1, 1.18, 1]}>
|
|
417
|
+
<sphereGeometry args={[0.8, 48, 48]} />
|
|
418
|
+
<Mat c="#FF4D8D" r={0.2} m={0.1} {...m} />
|
|
419
|
+
</mesh>
|
|
420
|
+
<mesh position={[0, -1.0, 0]}>
|
|
421
|
+
<coneGeometry args={[0.1, 0.15, 12]} />
|
|
422
|
+
<Mat c="#FF4D8D" />
|
|
423
|
+
</mesh>
|
|
424
|
+
<mesh position={[0.05, -1.55, 0]} rotation={[0, 0, 0.1 * Math.sin(t * 2)]}>
|
|
425
|
+
<cylinderGeometry args={[0.01, 0.01, 1.0, 6]} />
|
|
426
|
+
<Mat c="#FFFFFF" />
|
|
427
|
+
</mesh>
|
|
428
|
+
</group>
|
|
429
|
+
);
|
|
430
|
+
case 'pin':
|
|
431
|
+
return (
|
|
432
|
+
<group>
|
|
433
|
+
<Torno perfil={[[0, -1.1], [0.15, -0.6], [0.55, 0.0], [0.7, 0.45], [0.55, 0.9], [0.25, 1.08], [0, 1.1]]} c="#E5484D" r={0.35} {...m} />
|
|
434
|
+
<mesh position={[0, 0.45, 0.58]}>
|
|
435
|
+
<sphereGeometry args={[0.22, 24, 24]} />
|
|
436
|
+
<Mat c="#FFFFFF" />
|
|
437
|
+
</mesh>
|
|
438
|
+
</group>
|
|
439
|
+
);
|
|
440
|
+
case 'tuberia':
|
|
441
|
+
return <Tuberia t={t} o={o} {...m} />;
|
|
442
|
+
case 'muro':
|
|
443
|
+
return <Muro o={o} {...m} />;
|
|
444
|
+
case 'viga': {
|
|
445
|
+
const L = o.largo ?? 3.4;
|
|
446
|
+
const c = {c: '#8C96A3', m: 0.8, r: 0.35};
|
|
447
|
+
return (
|
|
448
|
+
<group rotation={[0, 0, o.vertical ? Math.PI / 2 : 0]}>
|
|
449
|
+
<mesh position={[0, 0.42, 0]}>
|
|
450
|
+
<boxGeometry args={[L, 0.08, 0.5]} />
|
|
451
|
+
<Mat {...c} {...m} />
|
|
452
|
+
</mesh>
|
|
453
|
+
<mesh position={[0, -0.42, 0]}>
|
|
454
|
+
<boxGeometry args={[L, 0.08, 0.5]} />
|
|
455
|
+
<Mat {...c} {...m} />
|
|
456
|
+
</mesh>
|
|
457
|
+
<mesh>
|
|
458
|
+
<boxGeometry args={[L, 0.8, 0.07]} />
|
|
459
|
+
<Mat {...c} {...m} />
|
|
460
|
+
</mesh>
|
|
461
|
+
</group>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
case 'columna':
|
|
465
|
+
return (
|
|
466
|
+
<group>
|
|
467
|
+
<mesh>
|
|
468
|
+
<cylinderGeometry args={[0.32, 0.36, 2.6, 32]} />
|
|
469
|
+
<Mat c="#CFCAC0" r={0.8} {...m} />
|
|
470
|
+
</mesh>
|
|
471
|
+
{[1.35, -1.35].map((y) => (
|
|
472
|
+
<mesh key={y} position={[0, y, 0]}>
|
|
473
|
+
<boxGeometry args={[0.9, 0.2, 0.9]} />
|
|
474
|
+
<Mat c="#BDB7AC" r={0.85} {...m} />
|
|
475
|
+
</mesh>
|
|
476
|
+
))}
|
|
477
|
+
</group>
|
|
478
|
+
);
|
|
479
|
+
case 'escalera': {
|
|
480
|
+
const n = o.escalones ?? 8;
|
|
481
|
+
return (
|
|
482
|
+
<group position={[-0.9, -1, 0]}>
|
|
483
|
+
{Array.from({length: n}, (_, i) => (
|
|
484
|
+
<mesh key={i} position={[i * 0.24, i * 0.22 + 0.11, 0]}>
|
|
485
|
+
<boxGeometry args={[0.26, 0.22 * (i + 1), 1.2]} />
|
|
486
|
+
<Mat c="#CFCAC0" r={0.8} {...m} />
|
|
487
|
+
</mesh>
|
|
488
|
+
)).map((el, i) => React.cloneElement(el, {position: [i * 0.24, (0.22 * (i + 1)) / 2, 0]}))}
|
|
489
|
+
</group>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
case 'losa':
|
|
493
|
+
return (
|
|
494
|
+
<group rotation={[0.5, 0.6, 0]}>
|
|
495
|
+
<mesh>
|
|
496
|
+
<boxGeometry args={[3, 0.25, 2.2]} />
|
|
497
|
+
<Mat c="#C9C4BA" r={0.9} {...m} />
|
|
498
|
+
</mesh>
|
|
499
|
+
{[-1, 0, 1].map((k) => (
|
|
500
|
+
<mesh key={k} position={[k * 0.9, 0.13, 0]}>
|
|
501
|
+
<boxGeometry args={[0.04, 0.02, 2.2]} />
|
|
502
|
+
<Mat c="#8C96A3" m={0.7} />
|
|
503
|
+
</mesh>
|
|
504
|
+
))}
|
|
505
|
+
</group>
|
|
506
|
+
);
|
|
507
|
+
case 'tanque':
|
|
508
|
+
return (
|
|
509
|
+
<group>
|
|
510
|
+
<mesh position={[0, 0.2, 0]}>
|
|
511
|
+
<cylinderGeometry args={[0.8, 0.8, 1.5, 40]} />
|
|
512
|
+
<Mat c="#3E8ED0" r={0.4} m={0.2} {...m} />
|
|
513
|
+
</mesh>
|
|
514
|
+
<mesh position={[0, 0.95, 0]}>
|
|
515
|
+
<sphereGeometry args={[0.8, 40, 20, 0, Math.PI * 2, 0, Math.PI / 2]} />
|
|
516
|
+
<Mat c="#3E8ED0" r={0.4} m={0.2} {...m} />
|
|
517
|
+
</mesh>
|
|
518
|
+
{[0, 1, 2, 3].map((i) => (
|
|
519
|
+
<mesh key={i} position={[Math.cos((i * Math.PI) / 2 + 0.8) * 0.6, -0.9, Math.sin((i * Math.PI) / 2 + 0.8) * 0.6]}>
|
|
520
|
+
<cylinderGeometry args={[0.05, 0.05, 0.8, 8]} />
|
|
521
|
+
<Mat c="#8C96A3" m={0.8} />
|
|
522
|
+
</mesh>
|
|
523
|
+
))}
|
|
524
|
+
</group>
|
|
525
|
+
);
|
|
526
|
+
case 'suelo': {
|
|
527
|
+
// corte de terreno por capas: pasto, tierra, roca (para túneles, cimientos, drenaje)
|
|
528
|
+
const capas: [string, number][] = o.capas ?? [['#4FAF5A', 0.18], ['#8A5636', 0.9], ['#6E4A30', 0.6], ['#8C8C86', 0.7]];
|
|
529
|
+
let y = 1.2;
|
|
530
|
+
return (
|
|
531
|
+
<group rotation={[0.25, -0.5, 0]}>
|
|
532
|
+
{capas.map(([color, alto], i) => {
|
|
533
|
+
const el = (
|
|
534
|
+
<mesh key={i} position={[0, y - alto / 2, 0]}>
|
|
535
|
+
<boxGeometry args={[3.2, alto, 2]} />
|
|
536
|
+
<Mat c={color} r={0.95} />
|
|
537
|
+
</mesh>
|
|
538
|
+
);
|
|
539
|
+
y -= alto;
|
|
540
|
+
return el;
|
|
541
|
+
})}
|
|
542
|
+
</group>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
default:
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
@@ -31,7 +31,8 @@ export function capasDe(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
|
31
31
|
case 'dato': {
|
|
32
32
|
const y = p.y ?? 940;
|
|
33
33
|
const out: Capa[] = [];
|
|
34
|
-
if (p.
|
|
34
|
+
if (p.objeto) out.push({tipo: 'modelo', forma: p.objeto, ancho: 520, alto: 520, x: 540, y: y - 400, girar3d: [0, 35, 0], entrada: 'rebote'});
|
|
35
|
+
else if (p.imagen) out.push({tipo: 'imagen', src: p.imagen, ancho: p.anchoImagen ?? 520, x: 540, y: y - 380, entrada: 'escala', flotar: true});
|
|
35
36
|
out.push({
|
|
36
37
|
tipo: 'contador',
|
|
37
38
|
de: p.de ?? 0,
|
|
@@ -98,8 +99,11 @@ export function capasDe(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
|
98
99
|
const x = n === 1 ? 540 : 270 + (i * 540) / (n - 1);
|
|
99
100
|
const en = o.en ?? 0.15 + i * 0.25;
|
|
100
101
|
out.push({tipo: 'forma', forma: 'rect', ancho: 400, alto: 440, color: o.fondo ?? '#FFFFFF', radio: 28, x, y: yC, rotar: n === 2 ? (i === 0 ? -5 : 5) : 0, entrada: 'rebote', en, borde: o.color ?? tema.colores.acento, grosor: 10, sonido: 'pop'});
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
// ilustración: objeto 3D > imagen > ícono > emoji (último recurso)
|
|
103
|
+
if (o.objeto) out.push({tipo: 'modelo', forma: o.objeto, ancho: 280, alto: 280, x, y: yC - 75, girar3d: [0, 45, 0], entrada: 'rebote', en: en + 0.1});
|
|
104
|
+
else if (o.imagen) out.push({tipo: 'imagen', src: o.imagen, ancho: 220, x, y: yC - 70, entrada: 'escala', en: en + 0.1});
|
|
105
|
+
else if (o.emoji) out.push({tipo: 'emoji', texto: o.emoji, tam: 150, x, y: yC - 70, entrada: 'rebote', en: en + 0.1});
|
|
106
|
+
else out.push({tipo: 'icono', nombre: o.icono ?? 'question', tam: 160, x, y: yC - 70, entrada: 'rebote', en: en + 0.1});
|
|
103
107
|
out.push({tipo: 'texto', texto: o.texto, fuente: 'texto', tam: 50, peso: 900, mayusculas: true, color: '#141414', x, y: yC + 90, maxAncho: 360, entrada: 'aparecer', en: en + 0.15, sombra: false});
|
|
104
108
|
if (o.sub) out.push({tipo: 'texto', texto: o.sub, fuente: 'texto', tam: 26, peso: 700, color: o.color ?? tema.colores.acento, x, y: yC + 155, maxAncho: 340, entrada: 'aparecer', en: en + 0.2, mayusculas: true, sombra: false});
|
|
105
109
|
});
|
|
@@ -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
|
);
|
package/remotion/motor/tema.ts
CHANGED
|
@@ -7,7 +7,8 @@ export type Fondo =
|
|
|
7
7
|
| {tipo: 'color'; color: string}
|
|
8
8
|
| {tipo: 'degradado'; colores: string[]; angulo?: number}
|
|
9
9
|
| {tipo: 'imagen'; src: string; oscurecer?: number; desenfoque?: number}
|
|
10
|
-
| {tipo: 'video'; src: string; oscurecer?: number}
|
|
10
|
+
| {tipo: 'video'; src: string; oscurecer?: number}
|
|
11
|
+
| ({tipo: 'escenario'; nombre: string; oscurecer?: number} & Record<string, any>);
|
|
11
12
|
|
|
12
13
|
export type Particulas = {tipo: 'burbujas' | 'polvo' | 'estrellas' | 'nieve' | 'confeti' | 'ninguna'; cantidad?: number; color?: string};
|
|
13
14
|
|
|
@@ -80,7 +81,7 @@ export const ESTILOS: Record<string, Tema> = {
|
|
|
80
81
|
},
|
|
81
82
|
// Escenarios ilustrados (bosque, ciudad): el fondo suele ser una imagen tuya; cielo de atardecer por defecto
|
|
82
83
|
ilustrado: {
|
|
83
|
-
fondo: {tipo: '
|
|
84
|
+
fondo: {tipo: 'escenario', nombre: 'bosque', paneo: 60},
|
|
84
85
|
vineta: 0.3,
|
|
85
86
|
particulas: {tipo: 'polvo', cantidad: 18},
|
|
86
87
|
colores: {texto: '#FFFFFF', acento: '#FFE27A', acento2: '#FF6B6B', suave: 'rgba(255,255,255,0.55)', tarjeta: 'rgba(20,14,30,0.75)', tarjetaTexto: '#FFFFFF', borde: '#FFFFFF'},
|
|
@@ -93,7 +94,7 @@ export const ESTILOS: Record<string, Tema> = {
|
|
|
93
94
|
},
|
|
94
95
|
// Historias de misterio: interiores oscuros, siluetas, letreros "DÍA 10"
|
|
95
96
|
historia: {
|
|
96
|
-
fondo: {tipo: '
|
|
97
|
+
fondo: {tipo: 'escenario', nombre: 'departamento'},
|
|
97
98
|
vineta: 0.6,
|
|
98
99
|
foco: {color: 'rgba(255,214,140,0.22)', x: 540, y: 700, radio: 700},
|
|
99
100
|
particulas: {tipo: 'polvo', cantidad: 14},
|
|
@@ -133,7 +134,7 @@ export const ESTILOS: Record<string, Tema> = {
|
|
|
133
134
|
},
|
|
134
135
|
// Series animadas con personaje (capítulos, retos, "comenta"): cuarto morado, subtítulos en bloque al centro
|
|
135
136
|
serie: {
|
|
136
|
-
fondo: {tipo: '
|
|
137
|
+
fondo: {tipo: 'escenario', nombre: 'cuarto'},
|
|
137
138
|
vineta: 0.25,
|
|
138
139
|
colores: {texto: '#FFFFFF', acento: '#FF3B5C', acento2: '#FF3B5C', suave: 'rgba(255,255,255,0.7)', tarjeta: '#FFFFFF', tarjetaTexto: '#141414', borde: '#141414'},
|
|
139
140
|
fuentes: {titulo: MONT, texto: MONT, mono: MONO},
|
|
@@ -157,7 +157,7 @@ export const Chat: React.FC<{c: C; ctx: CtxEscena}> = ({c, ctx}) => {
|
|
|
157
157
|
const visibles = msgs.slice(Math.max(0, n - (c.maximo ?? 6)), n);
|
|
158
158
|
const colores = ['#FF7AB6', '#7AD7FF', '#B6FF7A', '#FFD27A', '#C9A2FF'];
|
|
159
159
|
return (
|
|
160
|
-
<div style={{width: c.ancho ??
|
|
160
|
+
<div style={{width: c.ancho ?? 480, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 10, fontFamily: T.fuentes.texto, fontSize: c.tam ?? 36}}>
|
|
161
161
|
{visibles.map((m, i) => (
|
|
162
162
|
<div key={n - visibles.length + i} style={{background: 'rgba(0,0,0,0.45)', color: '#fff', borderRadius: 12, padding: '6px 14px', opacity: i === visibles.length - 1 ? clamp((ctx.t - t0 - (n - 1) * cada) / 0.2) : 1}}>
|
|
163
163
|
<b style={{color: colores[(m.usuario ?? '').length % colores.length]}}>{m.usuario}</b> {m.texto}
|