morfeo 0.1.1 → 0.1.2

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.
@@ -0,0 +1,103 @@
1
+ // Cuarto de Neto (serie). Dos partes para meter al personaje en medio:
2
+ // {tipo: 'componente', nombre: 'Cuarto'} pared, ventana, póster, calendario
3
+ // {tipo: 'componente', nombre: 'Cuarto', props: {parte: 'frente'}} escritorio, monitor, teclado RGB y ramen
4
+ import React from 'react';
5
+
6
+ type Props = {t: number; parte?: 'fondo' | 'frente'; dia?: number};
7
+
8
+ const W = 1080;
9
+ const H = 1920;
10
+
11
+ function Fondo({t, dia}: {t: number; dia?: number}) {
12
+ return (
13
+ <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
14
+ <defs>
15
+ <pattern id="rayas" width="90" height="10" patternUnits="userSpaceOnUse">
16
+ <rect width="90" height="10" fill="#5B3FA0" />
17
+ <rect width="45" height="10" fill="#6447AE" />
18
+ </pattern>
19
+ <linearGradient id="noche" x1="0" y1="0" x2="0" y2="1">
20
+ <stop offset="0" stopColor="#0E1840" />
21
+ <stop offset="1" stopColor="#2D2A6E" />
22
+ </linearGradient>
23
+ </defs>
24
+ <rect width={W} height={H} fill="url(#rayas)" />
25
+ {/* zoclo y piso */}
26
+ <rect y={1520} width={W} height={400} fill="#3B2A63" />
27
+ <rect y={1510} width={W} height={18} fill="#2A1D4A" />
28
+ {/* ventana */}
29
+ <g transform="translate(70 360)">
30
+ <rect width={420} height={520} rx={10} fill="url(#noche)" stroke="#2A1D4A" strokeWidth={18} />
31
+ <circle cx={300} cy={130} r={50} fill="#FFF4D6" />
32
+ <circle cx={322} cy={118} r={46} fill="#141E4E" />
33
+ {Array.from({length: 14}, (_, i) => (
34
+ <circle key={i} cx={40 + ((i * 97) % 340)} cy={60 + ((i * 53) % 400)} r={3} fill="#fff" opacity={0.4 + 0.6 * Math.abs(Math.sin(t * 1.5 + i))} />
35
+ ))}
36
+ <rect x={200} y={0} width={18} height={520} fill="#2A1D4A" />
37
+ <rect x={0} y={250} width={420} height={18} fill="#2A1D4A" />
38
+ {/* cortinas que se mecen */}
39
+ <path d={`M-40 -20 Q${30 + Math.sin(t) * 8} 260 -10 560 L-60 560 L-60 -20 Z`} fill="#C0304A" />
40
+ <path d={`M460 -20 Q${390 + Math.sin(t + 1) * 8} 260 430 560 L480 560 L480 -20 Z`} fill="#C0304A" />
41
+ </g>
42
+ {/* póster de Ari */}
43
+ <g transform="translate(700 330) rotate(4)">
44
+ <rect width={300} height={380} rx={8} fill="#FFFFFF" />
45
+ <rect x={18} y={18} width={264} height={290} fill="#FF9EC7" />
46
+ <circle cx={150} cy={150} r={70} fill="#FFE1EF" />
47
+ <path d="M150 120 l-28 -28 a20 20 0 0 1 28 -18 a20 20 0 0 1 28 18z" fill="#FF3B7F" transform={`translate(0 ${Math.sin(t * 3) * 4})`} />
48
+ <text x={150} y={355} textAnchor="middle" fontSize={44} fontWeight={900} fill="#FF3B7F" fontFamily="Montserrat, sans-serif">
49
+ ARI
50
+ </text>
51
+ <rect x={120} y={-14} width={60} height={26} fill="rgba(255,255,255,0.7)" />
52
+ </g>
53
+ {/* calendario con días tachados */}
54
+ <g transform="translate(560 780)">
55
+ <rect width={220} height={230} rx={8} fill="#FFFFFF" />
56
+ <rect width={220} height={56} rx={8} fill="#E5484D" />
57
+ <text x={110} y={40} textAnchor="middle" fontSize={32} fontWeight={900} fill="#fff" fontFamily="Montserrat, sans-serif">
58
+ {dia != null ? `DÍA ${dia}` : 'NOV'}
59
+ </text>
60
+ {Array.from({length: 20}, (_, i) => (
61
+ <text key={i} x={22 + (i % 5) * 42} y={90 + Math.floor(i / 5) * 38} fontSize={30} fontWeight={900} fill="#E5484D" fontFamily="sans-serif">
62
+ ✕
63
+ </text>
64
+ ))}
65
+ </g>
66
+ </svg>
67
+ );
68
+ }
69
+
70
+ function Frente({t}: {t: number}) {
71
+ const colores = ['#FF4D6D', '#FFD60A', '#4CC9F0', '#80ED99', '#B517FF', '#FF8C42'];
72
+ return (
73
+ <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
74
+ {/* luz del monitor sobre el escritorio */}
75
+ <ellipse cx={540} cy={1600} rx={520} ry={120} fill="rgba(120,200,255,0.12)" />
76
+ {/* escritorio */}
77
+ <rect x={-20} y={1640} width={1120} height={60} rx={10} fill="#2B2238" />
78
+ <rect x={-20} y={1690} width={1120} height={260} fill="#1E1828" />
79
+ {/* teclado RGB */}
80
+ <g transform="translate(250 1590)">
81
+ <rect width={580} height={70} rx={14} fill="#15121C" />
82
+ {Array.from({length: 24}, (_, i) => (
83
+ <rect key={i} x={14 + (i % 12) * 46} y={10 + Math.floor(i / 12) * 28} width={40} height={22} rx={5} fill={colores[Math.floor(i + t * 6) % colores.length]} opacity={0.9} />
84
+ ))}
85
+ </g>
86
+ {/* ramen */}
87
+ <g transform="translate(70 1500)">
88
+ <path d="M0 60 L130 60 L112 150 L18 150 Z" fill="#FFFFFF" stroke="#D9D3C7" strokeWidth={4} />
89
+ <rect x={10} y={80} width={110} height={26} fill="#E5484D" />
90
+ <path d={`M50 60 Q${44 + Math.sin(t * 2) * 6} 20 56 -10`} stroke="rgba(255,255,255,0.5)" strokeWidth={6} fill="none" strokeLinecap="round" />
91
+ <rect x={84} y={-30} width={8} height={110} rx={4} fill="#C99A5B" transform="rotate(14 88 30)" />
92
+ <rect x={98} y={-30} width={8} height={110} rx={4} fill="#C99A5B" transform="rotate(20 102 30)" />
93
+ </g>
94
+ {/* mouse */}
95
+ <ellipse cx={920} cy={1630} rx={38} ry={22} fill="#15121C" />
96
+ <rect x={916} y={1612} width={8} height={14} rx={3} fill={colores[Math.floor(t * 4) % colores.length]} />
97
+ </svg>
98
+ );
99
+ }
100
+
101
+ export function Cuarto({t, parte = 'fondo', dia}: Props) {
102
+ return parte === 'frente' ? <Frente t={t} /> : <Fondo t={t} dia={dia} />;
103
+ }
@@ -0,0 +1,163 @@
1
+ // Escenarios nocturnos para historias, 1080×1920:
2
+ // lugar: 'departamento' sala oscura: ventana con ciudad, sillón, lámpara y tele que parpadea
3
+ // lugar: 'tunel' túnel del metro: arcos, rieles, luces que pasan y el tren que llega (llegar: 'palabra')
4
+ // lugar: 'pasillo' pasillo con la puerta: luz bajo la puerta y golpes que la sacuden (golpe: 'palabra')
5
+ import React from 'react';
6
+
7
+ type Props = {t: number; cuando: (en: unknown) => number; lugar?: 'departamento' | 'tunel' | 'pasillo'; llegar?: string; golpe?: string};
8
+
9
+ const W = 1080;
10
+ const H = 1920;
11
+ const clamp = (x: number) => Math.max(0, Math.min(1, x));
12
+ const suave = (x: number) => {
13
+ const c = clamp(x);
14
+ return c * c * (3 - 2 * c);
15
+ };
16
+ const azar = (n: number) => {
17
+ const x = Math.sin(n * 91.7) * 43758.5453;
18
+ return x - Math.floor(x);
19
+ };
20
+
21
+ function Departamento({t}: {t: number}) {
22
+ const tele = 0.55 + 0.45 * Math.abs(Math.sin(t * 7) * Math.sin(t * 3.3));
23
+ return (
24
+ <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
25
+ <rect width={W} height={H} fill="#121A2B" />
26
+ <rect y={1480} width={W} height={440} fill="#0C111C" />
27
+ {/* ventana con ciudad */}
28
+ <g transform="translate(110 520)">
29
+ <rect width={460} height={560} fill="#1B2A4A" stroke="#0A0F1A" strokeWidth={16} />
30
+ {Array.from({length: 8}, (_, i) => {
31
+ const x = 10 + i * 58;
32
+ const h = 160 + azar(i) * 260;
33
+ return (
34
+ <g key={i}>
35
+ <rect x={x} y={560 - h} width={50} height={h} fill="#0E1730" />
36
+ {Array.from({length: 10}, (_, j) =>
37
+ azar(i * 10 + j) > 0.55 ? <rect key={j} x={x + 8 + (j % 3) * 14} y={560 - h + 14 + Math.floor(j / 3) * 26} width={8} height={12} fill="#FFD27A" opacity={0.5 + 0.5 * Math.abs(Math.sin(t + i + j))} /> : null,
38
+ )}
39
+ </g>
40
+ );
41
+ })}
42
+ <rect x={222} width={16} height={560} fill="#0A0F1A" />
43
+ </g>
44
+ {/* luz de la lámpara */}
45
+ <circle cx={880} cy={980} r={420} fill="rgba(255,190,110,0.12)" />
46
+ <rect x={872} y={900} width={16} height={600} fill="#2A2420" />
47
+ <path d="M800 900 L960 900 L920 800 L840 800 Z" fill="#E8B35C" />
48
+ <ellipse cx={880} cy={1500} rx={90} ry={16} fill="#1C1814" />
49
+ {/* tele que parpadea */}
50
+ <rect x={600} y={1180} width={260} height={160} rx={8} fill="#0A0F1A" />
51
+ <rect x={612} y={1192} width={236} height={136} fill={`rgba(120,170,255,${0.35 * tele})`} />
52
+ <circle cx={730} cy={1260} r={360} fill={`rgba(120,170,255,${0.06 * tele})`} />
53
+ {/* sillón */}
54
+ <g transform="translate(140 1300)">
55
+ <rect width={560} height={180} rx={40} fill="#3A2E3A" />
56
+ <rect y={-120} width={560} height={150} rx={40} fill="#45374A" />
57
+ <rect x={-30} y={-60} width={90} height={250} rx={36} fill="#3A2E3A" />
58
+ <rect x={500} y={-60} width={90} height={250} rx={36} fill="#3A2E3A" />
59
+ </g>
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ function Tunel({t, tTren}: {t: number; tTren: number}) {
65
+ const k = suave((t - tTren) / 2.4); // el tren se acerca
66
+ const escala = 0.2 + k * 0.85;
67
+ return (
68
+ <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
69
+ <defs>
70
+ <radialGradient id="fondoTunel" cx="0.5" cy="0.48" r="0.6">
71
+ <stop offset="0" stopColor="#2C3446" />
72
+ <stop offset="1" stopColor="#07090E" />
73
+ </radialGradient>
74
+ </defs>
75
+ <rect width={W} height={H} fill="url(#fondoTunel)" />
76
+ {/* arcos del túnel hacia el fondo */}
77
+ {Array.from({length: 7}, (_, i) => {
78
+ const d = ((i + (t * 0.6) % 1) / 7);
79
+ const s = 0.15 + (1 - d) * 1.1;
80
+ return <ellipse key={i} cx={540} cy={900} rx={520 * s} ry={600 * s} fill="none" stroke="#3B4560" strokeWidth={10 * s} opacity={0.25 + (1 - d) * 0.6} />;
81
+ })}
82
+ {/* luces de pared que pasan */}
83
+ {Array.from({length: 6}, (_, i) => {
84
+ const d = ((i / 6 + t * 0.35) % 1);
85
+ const s = 0.1 + d * 1.2;
86
+ return (
87
+ <g key={i} opacity={d}>
88
+ <circle cx={540 - 470 * s} cy={900 - 120 * s} r={10 * s + 3} fill="#FFE29A" />
89
+ <circle cx={540 + 470 * s} cy={900 - 120 * s} r={10 * s + 3} fill="#FFE29A" />
90
+ </g>
91
+ );
92
+ })}
93
+ {/* rieles */}
94
+ <path d="M520 960 L250 1920 M560 960 L830 1920" stroke="#5A6378" strokeWidth={14} />
95
+ {Array.from({length: 10}, (_, i) => {
96
+ const d = (i / 10 + t * 0.5) % 1;
97
+ const y = 960 + d * d * 960;
98
+ const a = 30 + d * 420;
99
+ return <rect key={i} x={540 - a} y={y} width={a * 2} height={6 + d * 18} fill="#2A2F3C" />;
100
+ })}
101
+ {/* tren que llega con faros */}
102
+ {t >= tTren ? (
103
+ <g transform={`translate(540 ${930 + k * 160}) scale(${escala})`}>
104
+ <circle r={700} fill="rgba(255,240,200,0.08)" />
105
+ <rect x={-330} y={-380} width={660} height={700} rx={70} fill="#D9DEE8" />
106
+ <rect x={-280} y={-310} width={560} height={240} rx={26} fill="#1A2233" />
107
+ <rect x={-330} y={-40} width={660} height={60} fill="#E5484D" />
108
+ <text x={0} y={190} textAnchor="middle" fontSize={130} fontWeight={900} fill="#1A2233" fontFamily="Montserrat, sans-serif">
109
+ B
110
+ </text>
111
+ <circle cx={-210} cy={220} r={46} fill="#FFF6C8" />
112
+ <circle cx={210} cy={220} r={46} fill="#FFF6C8" />
113
+ <circle cx={-210} cy={220} r={120} fill="rgba(255,246,200,0.25)" />
114
+ <circle cx={210} cy={220} r={120} fill="rgba(255,246,200,0.25)" />
115
+ </g>
116
+ ) : null}
117
+ </svg>
118
+ );
119
+ }
120
+
121
+ function Pasillo({t, tGolpe}: {t: number; tGolpe: number}) {
122
+ // tres golpes: la puerta se sacude y la luz de abajo parpadea
123
+ const g = t - tGolpe;
124
+ const golpe = g >= 0 && g < 1.6 ? [0, 0.45, 0.9].reduce((s, d) => s + Math.max(0, 1 - Math.abs(g - d) * 8), 0) : 0;
125
+ const sacudida = Math.sin(t * 80) * 10 * golpe;
126
+ const luz = 0.6 + 0.4 * Math.abs(Math.sin(t * 2.3)) - golpe * 0.3;
127
+ return (
128
+ <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
129
+ <rect width={W} height={H} fill="#151B26" />
130
+ {/* paredes en perspectiva */}
131
+ <path d="M0 0 L250 380 L250 1500 L0 1920 Z" fill="#1C2433" />
132
+ <path d="M1080 0 L830 380 L830 1500 L1080 1920 Z" fill="#1C2433" />
133
+ <path d="M250 1500 L830 1500 L1080 1920 L0 1920 Z" fill="#2A2018" />
134
+ {/* tablas del piso */}
135
+ {Array.from({length: 7}, (_, i) => (
136
+ <path key={i} d={`M${250 + i * 97} 1500 L${-60 + i * 200} 1920`} stroke="#1E1610" strokeWidth={4} />
137
+ ))}
138
+ {/* cuadros en la pared */}
139
+ <rect x={60} y={700} width={120} height={160} fill="#2A3346" stroke="#3A4560" strokeWidth={8} transform="skewY(18)" />
140
+ <rect x={900} y={980} width={120} height={160} fill="#2A3346" stroke="#3A4560" strokeWidth={8} transform="skewY(-18)" />
141
+ {/* puerta */}
142
+ <g transform={`translate(${sacudida} 0)`}>
143
+ <rect x={320} y={540} width={440} height={960} fill="#0F1520" />
144
+ <rect x={340} y={560} width={400} height={930} fill="#22314A" stroke="#33466A" strokeWidth={10} />
145
+ <rect x={380} y={610} width={320} height={360} fill="none" stroke="#33466A" strokeWidth={8} />
146
+ <rect x={380} y={1030} width={320} height={400} fill="none" stroke="#33466A" strokeWidth={8} />
147
+ <circle cx={690} cy={1060} r={18} fill="#D9B45A" />
148
+ <circle cx={540} cy={760} r={14} fill="#0A0E16" stroke="#3A4560" strokeWidth={5} />
149
+ </g>
150
+ {/* luz por debajo de la puerta */}
151
+ <rect x={340} y={1488} width={400} height={12} fill="#FFD66B" opacity={luz} />
152
+ <path d={`M340 1500 L740 1500 L${900} 1920 L${180} 1920 Z`} fill={`rgba(255,214,107,${0.12 * luz})`} />
153
+ {/* sombra de pies bajo la puerta */}
154
+ {g > 0 ? <rect x={500} y={1489} width={80} height={10} fill="#0A0A0A" opacity={0.85} /> : null}
155
+ </svg>
156
+ );
157
+ }
158
+
159
+ export function Noche({t, cuando, lugar = 'departamento', llegar, golpe}: Props) {
160
+ if (lugar === 'tunel') return <Tunel t={t} tTren={llegar ? cuando(llegar) : 0.3} />;
161
+ if (lugar === 'pasillo') return <Pasillo t={t} tGolpe={golpe ? cuando(golpe) : 1} />;
162
+ return <Departamento t={t} />;
163
+ }
@@ -0,0 +1,26 @@
1
+ // Escenarios ilustrados incluidos (1080×1920), para que un personaje nunca quede sobre un fondo vacío.
2
+ // fondo: {tipo: 'escenario', nombre: 'cuarto'} como fondo de escena (o del estilo)
3
+ // {tipo: 'escenario', nombre: 'cuarto', parte: 'frente'} como capa (por ejemplo, el escritorio delante del personaje)
4
+ // Todos se animan con el tiempo de la escena (estrellas, cortinas, luces, tren, golpes en la puerta...).
5
+ import React from 'react';
6
+ import {Bosque} from './Bosque';
7
+ import {Cuarto} from './Cuarto';
8
+ import {Noche} from './Noche';
9
+
10
+ type P = {t: number; dur: number; cuando: (en: unknown) => number} & Record<string, any>;
11
+
12
+ export const ESCENARIOS: Record<string, React.FC<P>> = {
13
+ bosque: (p) => <Bosque {...p} />,
14
+ cuarto: (p) => <Cuarto {...p} />,
15
+ departamento: (p) => <Noche {...p} lugar="departamento" />,
16
+ tunel: (p) => <Noche {...p} lugar="tunel" />,
17
+ pasillo: (p) => <Noche {...p} lugar="pasillo" />,
18
+ };
19
+
20
+ export const NOMBRES_ESCENARIOS = Object.keys(ESCENARIOS);
21
+
22
+ export const Escenario: React.FC<P & {nombre: string}> = ({nombre, ...p}) => {
23
+ const Comp = ESCENARIOS[nombre];
24
+ if (!Comp) throw new Error(`Escenario "${nombre}" no existe. Hay: ${NOMBRES_ESCENARIOS.join(', ')}`);
25
+ return <Comp {...p} />;
26
+ };
@@ -4,9 +4,10 @@ import React from 'react';
4
4
  import {AbsoluteFill, Img, OffthreadVideo} from 'remotion';
5
5
  import {H, W} from '../estilo';
6
6
  import type {Fondo as TFondo, Particulas as TParticulas, Tema} from './tema';
7
- import {azar, recurso} from './util';
7
+ import {azar, cuando, recurso, type CtxEscena} from './util';
8
+ import {Escenario} from './escenarios';
8
9
 
9
- const Base: React.FC<{f: TFondo}> = ({f}) => {
10
+ const Base: React.FC<{f: TFondo; ctx?: CtxEscena}> = ({f, ctx}) => {
10
11
  switch (f.tipo) {
11
12
  case 'color':
12
13
  return <AbsoluteFill style={{background: f.color}} />;
@@ -19,6 +20,17 @@ const Base: React.FC<{f: TFondo}> = ({f}) => {
19
20
  {f.oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${f.oscurecer})`}} /> : null}
20
21
  </AbsoluteFill>
21
22
  );
23
+ case 'escenario': {
24
+ // escenario ilustrado animado: usa el tiempo de la escena (sin ctx, por ejemplo en la miniatura de otro lado, queda quieto)
25
+ const {tipo, nombre, oscurecer, ...props} = f as any;
26
+ const c = ctx ?? ({t: 0, dur: 1, habla: [0, 1], palabras: [], semilla: '', tema: undefined as any} as CtxEscena);
27
+ return (
28
+ <AbsoluteFill>
29
+ <Escenario nombre={nombre} {...props} t={c.t} dur={c.dur} cuando={(en: unknown) => cuando(en, c)} />
30
+ {oscurecer ? <AbsoluteFill style={{background: `rgba(0,0,0,${oscurecer})`}} /> : null}
31
+ </AbsoluteFill>
32
+ );
33
+ }
22
34
  case 'video':
23
35
  return (
24
36
  <AbsoluteFill>
@@ -123,12 +135,12 @@ export const Textura: React.FC<{tipo: 'papel' | 'grano'}> = ({tipo}) => (
123
135
  );
124
136
 
125
137
  /** El fondo completo de una escena (el de la escena gana sobre el del estilo). */
126
- export const FondoTema: React.FC<{tema: Tema; fondo?: TFondo; t: number; semilla: string}> = ({tema, fondo, t, semilla}) => {
138
+ export const FondoTema: React.FC<{tema: Tema; fondo?: TFondo; t: number; semilla: string; ctx?: CtxEscena}> = ({tema, fondo, t, semilla, ctx}) => {
127
139
  const f = fondo ?? tema.fondo;
128
140
  const foco = tema.foco;
129
141
  return (
130
142
  <AbsoluteFill>
131
- <Base f={f} />
143
+ <Base f={f} ctx={ctx} />
132
144
  {tema.rayos ? <Rayos color={tema.rayos.color} fuerza={tema.rayos.fuerza ?? 1} t={t} /> : null}
133
145
  {foco ? (
134
146
  <AbsoluteFill
@@ -14,6 +14,7 @@ import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
14
14
  import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment.js';
15
15
  import {clamp, easeOut} from '../estilo';
16
16
  import {cuando, recurso, type CtxEscena} from './util';
17
+ import {OBJETOS_3D, Objeto3D} from './objetos3d';
17
18
 
18
19
  const RAD = Math.PI / 180;
19
20
  type Gltf = {scene: THREE.Group; animations: THREE.AnimationClip[]};
@@ -88,7 +89,7 @@ const Forma: React.FC<{forma: string; color?: string; metal?: number; rugosidad?
88
89
  return (
89
90
  <mesh>
90
91
  <boxGeometry args={[1.8, 0.55, 0.8]} />
91
- {mat('#E8B84A', 0.95, 0.22)}
92
+ {mat('#E8B84A', 0.7, 0.28)}
92
93
  </mesh>
93
94
  );
94
95
  case 'esfera':
@@ -137,6 +138,8 @@ export const Modelo3D: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c,
137
138
  <group scale={c.tamano ?? 1} rotation={[rx + gx * t, ry + gy * t, rz + gz * t]} position={[0, n > 1 ? -altoPila / 2 : 0, 0]}>
138
139
  {c.src ? (
139
140
  <Glb src={c.src} />
141
+ ) : (OBJETOS_3D as readonly string[]).includes(c.forma) ? (
142
+ <Objeto3D forma={c.forma} t={t} opciones={c} color={c.color} metal={c.metal} rugosidad={c.rugosidad} />
140
143
  ) : (
141
144
  pila.slice(0, visibles).map(([x, y, z, giro], i) => (
142
145
  <group key={i} position={[x, y, z]} rotation={[0, n > 1 ? giro : 0, 0]}>