@7ots/cli 0.1.0 → 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.
Files changed (50) hide show
  1. package/.env.example +17 -1
  2. package/cli/7ots.mjs +136 -36
  3. package/cli/lib/assistant.mjs +337 -0
  4. package/cli/lib/brain.mjs +10 -7
  5. package/cli/lib/i18n.mjs +87 -17
  6. package/cli/lib/install.mjs +29 -1
  7. package/cli/lib/pet-core.mjs +3 -2
  8. package/cli/lib/pet-server.mjs +30 -0
  9. package/cli/lib/prompts.mjs +200 -8
  10. package/cli/lib/terminal.mjs +27 -6
  11. package/cli/pet/electron/main.cjs +79 -28
  12. package/cli/pet/pet.html +55 -6
  13. package/dist/7ots.esm.js +370 -142
  14. package/dist/7ots.esm.js.map +4 -4
  15. package/dist/7ots.iife.js +370 -142
  16. package/dist/7ots.iife.js.map +4 -4
  17. package/llms.txt +199 -7
  18. package/package.json +1 -2
  19. package/server/channels/apuchat.mjs +8 -5
  20. package/server/channels/apumail.mjs +13 -3
  21. package/server/channels/index.mjs +5 -3
  22. package/server/platform/apuchat.mjs +266 -0
  23. package/server/platform/auth.mjs +39 -10
  24. package/server/platform/connect.mjs +347 -0
  25. package/server/platform/db.mjs +55 -0
  26. package/server/platform/look.mjs +20 -0
  27. package/server/platform/orquesta.mjs +499 -0
  28. package/server/platform/routes.mjs +57 -8
  29. package/server/platform/runtime.mjs +75 -15
  30. package/server/platform/store.mjs +130 -9
  31. package/server/server.mjs +14 -3
  32. package/skills/7ots/SKILL.md +1 -1
  33. package/src/avatar/AvatarStage.js +54 -9
  34. package/src/avatar/three.js +62 -0
  35. package/src/character/AvatarEditor.js +174 -50
  36. package/src/character/Character.js +164 -26
  37. package/src/character/motion.js +95 -34
  38. package/src/character/parts.js +43 -12
  39. package/src/character/skin.js +412 -0
  40. package/src/character/styles.js +1429 -0
  41. package/src/character3d/Character3D.js +1693 -0
  42. package/src/character3d/body.js +154 -0
  43. package/src/character3d/fx.js +323 -0
  44. package/src/character3d/index.js +281 -0
  45. package/src/character3d/materials.js +429 -0
  46. package/src/character3d/stage.js +187 -0
  47. package/src/i18n/messages/character.js +18 -42
  48. package/src/i18n/messages/platform.js +135 -0
  49. package/src/identity/schema.js +16 -8
  50. package/src/index.js +1 -0
@@ -0,0 +1,1429 @@
1
+ /**
2
+ * Estilos artísticos («mundos») del personaje 2D: el mismo ot redibujado como grabado ukiyo-e,
3
+ * plastilina, vitral, Bauhaus, manuscrito iluminado, glitch, boceto a tinta, pop art, acuarela,
4
+ * píxel 8 bits, synthwave, art nouveau, cubismo, crayones, plano técnico, puntillismo o xilografía,
5
+ * con un fondo a juego.
6
+ *
7
+ * const spec = normalizeCharacter({ ...ot, style: 'ukiyoe' }); // se guarda en spec.style
8
+ * el.innerHTML = renderCharacter(spec); // estilo + fondo
9
+ * createCharacter(el, spec); // vivo: respira y parpadea igual
10
+ *
11
+ * Cada estilo aporta, todo procedural (SVG/CSS, sin dependencias):
12
+ * · paleta: remapea los colores de la spec (stylizeSpec) y fija acabado, textura y contorno;
13
+ * · capa interior: algo dibujado dentro del cuerpo (vetas, plomos del vitral, mitades Bauhaus…);
14
+ * · filtro: <filter> sobre todo el personaje (grano, posterizado, luz de plastilina, píxeles…);
15
+ * · fondo: escena en un marco fijo de 300×300 (el suelo empieza en y≈240), determinista con la
16
+ * semilla del ot (styleSeed): el mismo ot siempre tiene el mismo paisaje.
17
+ *
18
+ * 'none' (o ausente, specs antiguas) no cambia nada. Los filtros tienen versión 'lite' (sin ruido
19
+ * ni luces) para cuando el contorno se anima, en equipos modestos o con movimiento reducido.
20
+ */
21
+
22
+ import { luminance, hexToRgb } from './parts.js';
23
+ import { skinHash } from './skin.js';
24
+ import { seeded } from './motion.js';
25
+ import { addCatalog } from '../i18n/index.js';
26
+
27
+ const n = (v) => Math.round(v * 100) / 100;
28
+
29
+ export const STYLES = [
30
+ { id: 'none', label: { es: 'Ninguno', en: 'None', pt: 'Nenhum' } },
31
+ { id: 'ukiyoe', label: { es: 'Ukiyo-e', en: 'Ukiyo-e', pt: 'Ukiyo-e' } },
32
+ { id: 'clay', label: { es: 'Plastilina', en: 'Claymation', pt: 'Massinha' } },
33
+ { id: 'glass', label: { es: 'Vitral', en: 'Stained glass', pt: 'Vitral' } },
34
+ { id: 'bauhaus', label: { es: 'Bauhaus', en: 'Bauhaus', pt: 'Bauhaus' } },
35
+ { id: 'manuscript', label: { es: 'Manuscrito', en: 'Manuscript', pt: 'Iluminura' } },
36
+ { id: 'glitch', label: { es: 'Glitch', en: 'Glitch', pt: 'Glitch' } },
37
+ { id: 'ink', label: { es: 'Tinta', en: 'Ink sketch', pt: 'Nanquim' } },
38
+ { id: 'pop', label: { es: 'Pop art', en: 'Pop art', pt: 'Pop art' } },
39
+ { id: 'watercolor', label: { es: 'Acuarela', en: 'Watercolor', pt: 'Aquarela' } },
40
+ { id: 'pixel', label: { es: 'Píxel 8 bits', en: '8-bit pixel', pt: 'Pixel 8 bits' } },
41
+ { id: 'synthwave', label: { es: 'Synthwave', en: 'Synthwave', pt: 'Synthwave' } },
42
+ { id: 'nouveau', label: { es: 'Art nouveau', en: 'Art nouveau', pt: 'Art nouveau' } },
43
+ { id: 'cubism', label: { es: 'Cubismo', en: 'Cubism', pt: 'Cubismo' } },
44
+ { id: 'crayon', label: { es: 'Crayones', en: 'Crayon', pt: 'Giz de cera' } },
45
+ { id: 'blueprint', label: { es: 'Plano técnico', en: 'Blueprint', pt: 'Planta técnica' } },
46
+ { id: 'pointillism', label: { es: 'Puntillismo', en: 'Pointillism', pt: 'Pontilhismo' } },
47
+ { id: 'woodcut', label: { es: 'Xilografía', en: 'Woodcut', pt: 'Xilogravura' } },
48
+ ];
49
+ export const STYLE_IDS = STYLES.map((s) => s.id);
50
+
51
+ const lang = (l) => ({
52
+ character: { styles: Object.fromEntries(STYLES.map((s) => [s.id, s.label[l]])) },
53
+ editor: {
54
+ artStyle: {
55
+ es: { title: 'Estilo artístico', hint: 'Redibuja el mismo personaje en otro estilo, con un fondo a juego.' },
56
+ en: { title: 'Art style', hint: 'Redraws the same character in another art style, with a matching backdrop.' },
57
+ pt: { title: 'Estilo artístico', hint: 'Redesenha o mesmo personagem em outro estilo, com um fundo combinando.' },
58
+ }[l],
59
+ },
60
+ });
61
+ addCatalog({ es: lang('es'), en: lang('en'), pt: lang('pt') });
62
+
63
+ // ───────────────────────────── color ─────────────────────────────
64
+
65
+ const toHex = (rgb) => `#${rgb.map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('')}`;
66
+
67
+ function toHsl(hex) {
68
+ const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
69
+ const mx = Math.max(r, g, b);
70
+ const mn = Math.min(r, g, b);
71
+ const l = (mx + mn) / 2;
72
+ if (mx === mn) return [0, 0, l];
73
+ const d = mx - mn;
74
+ const s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
75
+ const h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
76
+ return [h * 60, s, l];
77
+ }
78
+
79
+ function fromHsl(h, s, l) {
80
+ const a = s * Math.min(l, 1 - l);
81
+ const f = (k) => {
82
+ const t = (k + h / 30) % 12;
83
+ return 255 * (l - a * Math.max(-1, Math.min(t - 3, 9 - t, 1)));
84
+ };
85
+ return toHex([f(0), f(8), f(4)]);
86
+ }
87
+
88
+ /** Distancia «redmean» entre dos colores (barata y bastante perceptual). */
89
+ function dist(a, b) {
90
+ const [r1, g1, b1] = hexToRgb(a);
91
+ const [r2, g2, b2] = hexToRgb(b);
92
+ const rm = (r1 + r2) / 2;
93
+ return (2 + rm / 256) * (r1 - r2) ** 2 + 4 * (g1 - g2) ** 2 + (2 + (255 - rm) / 256) * (b1 - b2) ** 2;
94
+ }
95
+ const nearest = (hex, list, avoid = []) => {
96
+ const pool = list.filter((c) => !avoid.includes(c));
97
+ return pool.reduce((best, c) => (dist(hex, c) < dist(hex, best) ? c : best), pool[0]);
98
+ };
99
+ const grey = (hex, lo, hi) => {
100
+ const v = Math.round(255 * (lo + luminance(hex) * (hi - lo)));
101
+ return toHex([v, v, v]);
102
+ };
103
+
104
+ const PAL = {
105
+ ukiyoe: ['#1f3a5f', '#3f6e8c', '#c0392b', '#d98e4a', '#e6c27a', '#7a9a6a', '#e9dcc0', '#d9a0a0', '#2b2b2b'],
106
+ glass: ['#1e5aa8', '#b8222a', '#e2a52a', '#2f8a4a', '#6a2c8c', '#2aa0b8', '#d65a1f'],
107
+ bauhaus: ['#d7262b', '#f2b705', '#1f4e9c'],
108
+ manuscript: ['#1d3f8f', '#b3261e', '#2e6b3a', '#c99a2e', '#6b2c5a', '#e9d9b4'],
109
+ pop: ['#ffd400', '#e4002b', '#0072ce', '#ff8ab4', '#00a651', '#ffffff', '#111111'],
110
+ // subconjunto de la paleta de la NES
111
+ pixel: ['#000000', '#fcfcfc', '#bcbcbc', '#7c7c7c', '#a81000', '#f83800', '#e45c10', '#fca044', '#f8b800', '#ac7c00', '#00a800', '#58d854', '#005800', '#0058f8', '#3cbcfc', '#6844fc', '#d800cc', '#f878f8', '#fcd8a8'],
112
+ synthwave: ['#ff2bd6', '#2bf3ff', '#8a2bff', '#ff8a2b', '#ffe12b', '#ff2b6e', '#4b3bff'],
113
+ nouveau: ['#8a9a6b', '#c98a7a', '#c9a24a', '#4f7a74', '#efe2c4', '#b5654a', '#6e4a5e', '#a8b48c', '#d9b98a'],
114
+ cubism: ['#c8a046', '#7a5232', '#7c7a46', '#4e6a86', '#b5583a', '#e6d6b0', '#2e2a26', '#8aa0a8'],
115
+ crayon: ['#e8352b', '#f58a1f', '#f6d32b', '#4caf3c', '#2f6fd6', '#8a4fc4', '#f27bb5', '#8b5a2b', '#5fc8e8', '#222222'],
116
+ };
117
+
118
+ /** Mezcla lineal de dos colores (t=0 → a). */
119
+ const mix = (a, b, t) => {
120
+ const x = hexToRgb(a);
121
+ const y = hexToRgb(b);
122
+ return toHex(x.map((v, i) => v + (y[i] - v) * t));
123
+ };
124
+ /** Mismo tono con saturación y luz acotadas. */
125
+ const clampHsl = (c, s0, s1, l0, l1) => {
126
+ const [h, s, l] = toHsl(c);
127
+ return fromHsl(h, Math.min(s1, Math.max(s0, s)), Math.min(l1, Math.max(l0, l)));
128
+ };
129
+
130
+ /** Primario Bauhaus de un color: por tono (los grises van a negro o crema). */
131
+ function bauhausOf(hex) {
132
+ const [h, s, l] = toHsl(hex);
133
+ if (s < 0.18) return l > 0.62 ? '#f1ead8' : '#151515';
134
+ if (h < 22 || h >= 285) return '#d7262b';
135
+ if (h < 150) return '#f2b705';
136
+ return '#1f4e9c';
137
+ }
138
+
139
+ /**
140
+ * Remapeo de paleta y ajustes de cada estilo. `ink`/`lw`: contorno propio del estilo (lw 0 = sin contorno).
141
+ * `map(color, rol)` recibe el rol: 'body' | 'pattern' | 'iris' | 'cheek' | 'lip' | 'brow' | 'acc'.
142
+ */
143
+ const LOOKS = {
144
+ ukiyoe: {
145
+ finish: 'flat', texture: 'none', ink: '#1b2233', lw: 3.4,
146
+ eyes: { white: '#f3ead6', ink: '#1b1b1b', iris: '#1f3a5f' },
147
+ map: (c, role) => (role === 'cheek' ? '#d9776a' : nearest(c, PAL.ukiyoe, role === 'pattern' ? [] : role === 'body' ? ['#2b2b2b', '#e9dcc0'] : ['#2b2b2b'])),
148
+ },
149
+ clay: {
150
+ finish: 'clay', texture: 'none', ink: '', lw: 0,
151
+ eyes: {},
152
+ map: (c, role) => {
153
+ if (role === 'iris') return c;
154
+ const [h, s, l] = toHsl(c);
155
+ return fromHsl(h, Math.min(0.72, Math.max(0.38, s)), Math.min(0.74, Math.max(0.46, l)));
156
+ },
157
+ },
158
+ glass: {
159
+ finish: 'flat', texture: 'none', ink: '#141014', lw: 5,
160
+ eyes: { white: '#f3ecd0', ink: '#141014' },
161
+ map: (c, role, spec) => (role === 'pattern' ? nearest(c, PAL.glass, [nearest(spec.body.color, PAL.glass)]) : nearest(c, PAL.glass)),
162
+ },
163
+ bauhaus: {
164
+ finish: 'flat', texture: 'none', ink: '#151515', lw: 4.5,
165
+ eyes: { white: '#ffffff', ink: '#151515', iris: '#151515' },
166
+ map: (c, role, spec) => {
167
+ if (role === 'cheek' || role === 'lip') return '#d7262b';
168
+ const p = bauhausOf(c);
169
+ if (role === 'pattern') {
170
+ const b = bauhausOf(spec.body.color);
171
+ return p === b ? PAL.bauhaus[(PAL.bauhaus.indexOf(b) + 1) % 3] || '#f2b705' : p;
172
+ }
173
+ return p;
174
+ },
175
+ },
176
+ manuscript: {
177
+ finish: 'flat', texture: 'none', ink: '#3b2412', lw: 3,
178
+ eyes: { white: '#f5ead0', ink: '#2a1a0e' },
179
+ map: (c, role) => (role === 'cheek' ? '#c8584a' : nearest(c, PAL.manuscript, role === 'body' ? ['#e9d9b4'] : [])),
180
+ },
181
+ glitch: {
182
+ finish: 'flat', texture: 'none', ink: '#0b0b14', lw: 2.4,
183
+ eyes: {},
184
+ map: (c, role) => {
185
+ if (role === 'iris') return c;
186
+ const [h, s, l] = toHsl(c);
187
+ return fromHsl(h, Math.max(0.78, s), Math.min(0.66, Math.max(0.46, l)));
188
+ },
189
+ },
190
+ ink: {
191
+ finish: 'sketch', texture: 'none', ink: '#222222', lw: 2.6,
192
+ eyes: { white: '#ffffff', ink: '#1e1e1e', iris: '#3a3a3a' },
193
+ map: (c, role) => {
194
+ if (role === 'body') return grey(c, 0.88, 0.99);
195
+ if (role === 'pattern') return '#ddd8cc';
196
+ if (role === 'cheek' || role === 'lip') return '#b9b2a6';
197
+ return grey(c, 0.3, 0.92);
198
+ },
199
+ },
200
+ pop: {
201
+ finish: 'flat', texture: 'none', ink: '#111111', lw: 5,
202
+ eyes: { white: '#ffffff', ink: '#111111' },
203
+ map: (c, role) => (role === 'cheek' || role === 'lip' ? '#e4002b' : nearest(c, PAL.pop, role === 'body' ? ['#ffffff', '#111111'] : role === 'brow' ? [] : ['#ffffff'])),
204
+ },
205
+ watercolor: {
206
+ finish: 'flat', texture: 'none', ink: '#5b4a3c', lw: 1.3,
207
+ eyes: { white: '#fffdf6', ink: '#3b3029' },
208
+ map: (c, role) => (role === 'iris' || role === 'brow' ? clampHsl(c, 0.3, 0.6, 0.25, 0.5) : clampHsl(c, 0.42, 0.72, 0.5, 0.72)),
209
+ },
210
+ pixel: {
211
+ finish: 'flat', texture: 'none', ink: '#000000', lw: 4.5,
212
+ eyes: { white: '#fcfcfc', ink: '#000000' },
213
+ map: (c, role) => nearest(c, PAL.pixel, role === 'body' ? ['#000000', '#7c7c7c'] : []),
214
+ },
215
+ synthwave: {
216
+ finish: 'flat', texture: 'none', ink: '#fdf6ff', lw: 3.2,
217
+ eyes: { white: '#fdf6ff', ink: '#1a0b33' },
218
+ map: (c, role, spec) => {
219
+ if (role === 'pattern') return nearest(c, PAL.synthwave, [nearest(spec.body.color, PAL.synthwave)]);
220
+ if (role === 'brow') return '#1a0b33';
221
+ return nearest(c, PAL.synthwave);
222
+ },
223
+ },
224
+ nouveau: {
225
+ finish: 'flat', texture: 'none', ink: '#4a3324', lw: 3,
226
+ eyes: { white: '#f6ecd6', ink: '#3a281c' },
227
+ map: (c, role) => (role === 'cheek' ? '#d08e7e' : nearest(c, PAL.nouveau, role === 'body' ? ['#efe2c4'] : [])),
228
+ },
229
+ cubism: {
230
+ finish: 'flat', texture: 'none', ink: '#2e2a26', lw: 3.4,
231
+ eyes: { white: '#efe4c8', ink: '#2e2a26' },
232
+ map: (c, role) => nearest(c, PAL.cubism, role === 'body' ? ['#e6d6b0', '#2e2a26'] : []),
233
+ },
234
+ crayon: {
235
+ finish: 'flat', texture: 'none', ink: '#2a2a2a', lw: 3,
236
+ eyes: { white: '#ffffff', ink: '#222222' },
237
+ map: (c, role) => (role === 'cheek' ? '#f27bb5' : nearest(c, PAL.crayon, role === 'body' ? ['#222222'] : [])),
238
+ },
239
+ blueprint: {
240
+ finish: 'flat', texture: 'none', ink: '#eaf2ff', lw: 2.2,
241
+ eyes: { white: '#3a70b8', ink: '#16386a', iris: '#eaf2ff' },
242
+ map: (c, role) => {
243
+ if (role === 'body') return '#2e64ad';
244
+ if (role === 'pattern') return '#4a80c6';
245
+ if (role === 'cheek') return '#5a8fd0';
246
+ if (role === 'lip' || role === 'brow') return '#eaf2ff';
247
+ return luminance(c) > 0.55 ? '#bcd3f0' : '#4a80c6';
248
+ },
249
+ },
250
+ pointillism: {
251
+ finish: 'flat', texture: 'none', ink: '', lw: 0,
252
+ eyes: {},
253
+ map: (c, role) => (role === 'iris' || role === 'brow' ? c : clampHsl(c, 0.45, 0.75, 0.5, 0.7)),
254
+ },
255
+ woodcut: {
256
+ finish: 'flat', texture: 'none', ink: '#15120f', lw: 4.5,
257
+ eyes: { white: '#f1e6cf', ink: '#15120f', iris: '#15120f' },
258
+ map: (c, role) => {
259
+ if (role === 'body') return '#ecdfc4';
260
+ if (role === 'cheek' || role === 'lip') return '#b8352a';
261
+ if (role === 'pattern' || role === 'brow') return '#15120f';
262
+ return luminance(c) > 0.5 ? '#ecdfc4' : '#15120f';
263
+ },
264
+ },
265
+ };
266
+
267
+ /** Semilla del estilo para un ot: la misma que su piel (no cambia al arrastrar puntos). */
268
+ export function styleSeed(spec) {
269
+ return skinHash(spec);
270
+ }
271
+
272
+ /**
273
+ * Spec lista para pintar con su estilo: colores remapeados, acabado/textura del estilo y
274
+ * `_style` ({ id, seed, ink, lw }) para el contorno. Sin estilo devuelve la misma spec.
275
+ * Recibe una spec normalizada; el resultado no se guarda (normalizeCharacter descarta `_style`).
276
+ */
277
+ export function stylizeSpec(spec) {
278
+ const id = spec?.style;
279
+ const look = LOOKS[id];
280
+ if (!look) return spec;
281
+ const s = structuredClone(spec);
282
+ const m = (c, role) => (c ? look.map(c, role, spec) : c);
283
+ s.body.color = m(spec.body.color, 'body');
284
+ s.body.patternColor = m(spec.body.patternColor, 'pattern');
285
+ s.eyes.iris = m(spec.eyes.iris, 'iris');
286
+ Object.assign(s.eyes, look.eyes);
287
+ s.cheeks.color = m(spec.cheeks.color, 'cheek');
288
+ s.mouth.lipColor = m(spec.mouth.lipColor, 'lip');
289
+ if (spec.mouth.color) s.mouth.color = m(spec.mouth.color, 'brow');
290
+ if (spec.brows.color) s.brows.color = m(spec.brows.color, 'brow');
291
+ for (const a of s.accessories) {
292
+ a.color = m(a.color, 'acc');
293
+ a.color2 = m(a.color2, 'acc');
294
+ }
295
+ s.finish = look.finish;
296
+ s.texture = look.texture;
297
+ s._style = { id, seed: styleSeed(spec), ink: look.ink, lw: look.lw };
298
+ return s;
299
+ }
300
+
301
+ // ───────────────────────────── geometría ─────────────────────────────
302
+
303
+ /** Recorta un polígono convexo al semiplano de los puntos más cerca de p que de q. */
304
+ function clipHalf(poly, p, q) {
305
+ const nx = q[0] - p[0];
306
+ const ny = q[1] - p[1];
307
+ const c = (nx * (p[0] + q[0])) / 2 + (ny * (p[1] + q[1])) / 2;
308
+ const f = (v) => nx * v[0] + ny * v[1] - c;
309
+ const out = [];
310
+ for (let i = 0; i < poly.length; i++) {
311
+ const a = poly[i];
312
+ const b = poly[(i + 1) % poly.length];
313
+ const fa = f(a);
314
+ const fb = f(b);
315
+ if (fa <= 0) out.push(a);
316
+ if ((fa < 0 && fb > 0) || (fa > 0 && fb < 0)) {
317
+ const t = fa / (fa - fb);
318
+ out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+
324
+ /** Celdas de Voronoi (polígonos) de unos puntos dentro de un rectángulo. O(n²), para n pequeño. */
325
+ export function voronoi(sites, x0, y0, x1, y1) {
326
+ return sites.map((p, i) => {
327
+ let poly = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
328
+ for (let j = 0; j < sites.length && poly.length; j++) if (j !== i) poly = clipHalf(poly, p, sites[j]);
329
+ return poly;
330
+ });
331
+ }
332
+
333
+ /** Puntos en rejilla con jitter (celdas más parejas que el azar puro). */
334
+ function jitterGrid(rnd, x0, y0, w, h, cols, rows, j = 0.8) {
335
+ const pts = [];
336
+ for (let r = 0; r < rows; r++) {
337
+ for (let c = 0; c < cols; c++) {
338
+ pts.push([x0 + ((c + 0.5 + (rnd() - 0.5) * j) * w) / cols, y0 + ((r + 0.5 + (rnd() - 0.5) * j) * h) / rows]);
339
+ }
340
+ }
341
+ return pts;
342
+ }
343
+
344
+ const polyD = (poly) => (poly.length ? `M${poly.map((p) => `${n(p[0])} ${n(p[1])}`).join('L')}Z` : '');
345
+
346
+ // ───────────────────────────── filtros ─────────────────────────────
347
+
348
+ // Región fija en espacio de usuario: cabe el cuerpo con alas, manos y efectos (y no cambia al animar).
349
+ const REGION = 'filterUnits="userSpaceOnUse" x="-160" y="-200" width="560" height="580" color-interpolation-filters="sRGB"';
350
+
351
+ const posterize = (levels, inn = 'SourceGraphic', res = 'p') => {
352
+ const tv = Array.from({ length: levels }, (_, i) => n(i / (levels - 1))).join(' ');
353
+ return `<feComponentTransfer in="${inn}" result="${res}"><feFuncR type="discrete" tableValues="${tv}"/><feFuncG type="discrete" tableValues="${tv}"/><feFuncB type="discrete" tableValues="${tv}"/></feComponentTransfer>`;
354
+ };
355
+
356
+ /** Grano (motas de un color) recortado a la silueta → resultado `res`. */
357
+ const grain = (seed, rgb, k, freq = 0.9, res = 'gr') => {
358
+ const [r, g, b] = hexToRgb(rgb).map((v) => n(v / 255));
359
+ return `<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="2" seed="${seed % 997}" result="${res}n"/><feColorMatrix in="${res}n" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} ${k} 0 0 0 ${n(-k * 0.56)}" result="${res}c"/><feComposite in="${res}c" in2="SourceAlpha" operator="in" result="${res}"/>`;
360
+ };
361
+
362
+ /** Temblor de trazo a mano. */
363
+ const wobble = (seed, inn, scale, freq = 0.045, res = 'wb') =>
364
+ `<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="1" seed="${(seed + 11) % 997}" result="${res}n"/><feDisplacementMap in="${inn}" in2="${res}n" scale="${scale}" xChannelSelector="R" yChannelSelector="G" result="${res}"/>`;
365
+
366
+ function filterBody(id, seed, q, animate) {
367
+ const full = q === 'full';
368
+ switch (id) {
369
+ case 'ukiyoe':
370
+ // grabado: grano de papel y borde de plancha (las tintas planas ya vienen de la paleta)
371
+ return full ? grain(seed, '#5a3d1e', 1.6) + wobble(seed, 'SourceGraphic', 2.6) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
372
+ case 'clay': {
373
+ if (!full) return '';
374
+ // plastilina: bultos (desplazamiento) + luz difusa y brillo sobre la silueta desenfocada
375
+ const light = '<feDistantLight azimuth="235" elevation="52"/>';
376
+ return `${wobble(seed, 'SourceGraphic', 5, 0.035, 'd')}<feGaussianBlur in="d" stdDeviation="5" result="b"/>`
377
+ + `<feDiffuseLighting in="b" surfaceScale="4" diffuseConstant="1.18" lighting-color="#fff" result="dl">${light}</feDiffuseLighting>`
378
+ + '<feComposite in="d" in2="dl" operator="arithmetic" k1="1" result="sh"/>'
379
+ + `<feSpecularLighting in="b" surfaceScale="4" specularConstant=".5" specularExponent="20" lighting-color="#fff" result="sp">${light}</feSpecularLighting>`
380
+ + '<feComposite in="sp" in2="d" operator="in" result="sp2"/><feComposite in="sh" in2="sp2" operator="arithmetic" k2="1" k3=".85"/>';
381
+ }
382
+ case 'glass':
383
+ // vidrio: plomo grueso alrededor, color saturado y posterizado, manchas de luz
384
+ return '<feMorphology in="SourceAlpha" operator="dilate" radius="2.6" result="dl"/><feFlood flood-color="#141014"/><feComposite in2="dl" operator="in" result="lead"/>'
385
+ + '<feColorMatrix in="SourceGraphic" type="saturate" values="1.35" result="s"/>' + posterize(5, 's')
386
+ + (full
387
+ ? `<feTurbulence type="fractalNoise" baseFrequency=".05" numOctaves="2" seed="${seed % 997}" result="ln"/><feColorMatrix in="ln" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 .92 .9 0 0 0 -.38" result="lc"/><feComposite in="lc" in2="SourceAlpha" operator="in" result="hi"/>`
388
+ + '<feMerge><feMergeNode in="lead"/><feMergeNode in="p"/><feMergeNode in="hi"/></feMerge>'
389
+ : '<feMerge><feMergeNode in="lead"/><feMergeNode in="p"/></feMerge>');
390
+ case 'bauhaus':
391
+ return full ? grain(seed, '#3a3226', 1.2, 1.1) + '<feMerge><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>' : '';
392
+ case 'manuscript':
393
+ return full ? wobble(seed, 'SourceGraphic', 1.6, 0.06) + grain(seed, '#6b4a1e', 1.3) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
394
+ case 'glitch': {
395
+ // píxeles: una muestra de 2×2 cada 6 unidades, dilatada hasta tapar el bloque
396
+ let f = '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="6" height="6"/><feTile result="tile"/>'
397
+ + '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feMorphology operator="dilate" radius="2" result="px"/>';
398
+ let src = 'px';
399
+ if (full) {
400
+ // desgarro: bandas horizontales que se desplazan a tirones
401
+ const anim = animate ? '<animate attributeName="scale" values="0;0;0;0;16;0;0;0;-10;0;0;0;0;8;0" dur="3.4s" repeatCount="indefinite" calcMode="discrete"/>' : '';
402
+ f += `<feTurbulence type="fractalNoise" baseFrequency="0 .09" numOctaves="1" seed="${seed % 997}" result="tn"/>`
403
+ + '<feComponentTransfer in="tn" result="tb"><feFuncR type="discrete" tableValues=".5 .5 .5 .1 .5 .5 .5 .95 .5 .5"/><feFuncG type="linear" slope="0" intercept=".5"/></feComponentTransfer>'
404
+ + `<feDisplacementMap in="px" in2="tb" scale="${animate ? 0 : 10}" xChannelSelector="R" yChannelSelector="G" result="tear">${anim}</feDisplacementMap>`;
405
+ src = 'tear';
406
+ }
407
+ const dx = animate ? '<animate attributeName="dx" values="-2.5;-2.5;-2.5;-6;-2.5;-1;-2.5" dur="1.9s" repeatCount="indefinite" calcMode="discrete"/>' : '';
408
+ // separación RGB: rojo a un lado, verde+azul al otro
409
+ f += `<feColorMatrix in="${src}" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="r"/><feOffset in="r" dx="-2.5" result="r2">${dx}</feOffset>`
410
+ + `<feColorMatrix in="${src}" values="0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" result="gb"/><feOffset in="gb" dx="2.5" result="gb2"/>`
411
+ + '<feBlend in="r2" in2="gb2" mode="screen"/>';
412
+ return f;
413
+ }
414
+ case 'ink':
415
+ return '<feColorMatrix in="SourceGraphic" type="saturate" values="0" result="bw"/>' + (full ? wobble(seed, 'bw', 2.2, 0.05) : '');
416
+ case 'pop':
417
+ // viñeta: sombra negra dura desplazada (barata: vale también en lite)
418
+ return '<feFlood flood-color="#111"/><feComposite in2="SourceAlpha" operator="in"/><feOffset dx="5" dy="5" result="sh"/>'
419
+ + '<feMerge><feMergeNode in="sh"/><feMergeNode in="SourceGraphic"/></feMerge>';
420
+ case 'watercolor':
421
+ if (!full) return '';
422
+ // borde que se corre, pigmento irregular (aguada + granulado), cerco oscuro donde se seca el agua, papel
423
+ return `${wobble(seed, 'SourceGraphic', 5, 0.03, 'd')}`
424
+ + `<feTurbulence type="fractalNoise" baseFrequency=".018" numOctaves="3" seed="${(seed + 5) % 997}" result="pn"/><feColorMatrix in="pn" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1" result="pg"/>`
425
+ + '<feComposite in="d" in2="pg" operator="arithmetic" k1="1.5" k2=".3" result="pig"/>'
426
+ + '<feGaussianBlur in="d" stdDeviation="3" result="eb"/><feComposite in="d" in2="eb" operator="out" result="edge"/><feColorMatrix in="edge" values=".5 0 0 0 0 0 .5 0 0 0 0 0 .5 0 0 0 0 0 1 0" result="ed"/>'
427
+ + grain(seed, '#3a2a40', 1.5, 0.55) + '<feMerge><feMergeNode in="pig"/><feMergeNode in="ed"/><feMergeNode in="gr"/></feMerge>'
428
+ + '<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 .9 0"/>';
429
+ case 'pixel':
430
+ // bloques de 8: una muestra de 2×2 cada 8 unidades, dilatada; alfa a todo o nada (sin antialias)
431
+ return '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="8" height="8"/><feTile result="tile"/>'
432
+ + '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feMorphology operator="dilate" radius="3"/>'
433
+ + '<feComponentTransfer><feFuncA type="discrete" tableValues="0 1 1"/></feComponentTransfer>';
434
+ case 'synthwave':
435
+ if (!full) return '';
436
+ // tubo de neón: halo magenta alrededor de la silueta
437
+ return '<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="ga"/><feFlood flood-color="#ff2bd6"/><feComposite in2="ga" operator="in" result="glow"/>'
438
+ + '<feMerge><feMergeNode in="glow"/><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>';
439
+ case 'nouveau':
440
+ return full ? wobble(seed, 'SourceGraphic', 1.4, 0.05) + grain(seed, '#6b5030', 1.1) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
441
+ case 'cubism':
442
+ if (!full) return '';
443
+ // planos fracturados: ruido de baja frecuencia posterizado → cada zona se desplaza distinto
444
+ return `<feTurbulence type="fractalNoise" baseFrequency=".022" numOctaves="1" seed="${seed % 997}" result="cn"/>`
445
+ + '<feComponentTransfer in="cn" result="cd"><feFuncR type="discrete" tableValues=".32 .62 .44 .7 .5"/><feFuncG type="discrete" tableValues=".6 .38 .52 .66 .42"/></feComponentTransfer>'
446
+ + '<feDisplacementMap in="SourceGraphic" in2="cd" scale="20" xChannelSelector="R" yChannelSelector="G" result="fr"/>'
447
+ + grain(seed, '#3a2e22', 1.1) + '<feMerge><feMergeNode in="fr"/><feMergeNode in="gr"/></feMerge>';
448
+ case 'crayon':
449
+ if (!full) return '';
450
+ // cera: trazo tembloroso y el diente del papel asomando en vetas
451
+ return wobble(seed, 'SourceGraphic', 4, 0.06)
452
+ + `<feTurbulence type="fractalNoise" baseFrequency=".9 .22" numOctaves="2" seed="${(seed + 3) % 997}" result="wx"/>`
453
+ + '<feColorMatrix in="wx" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 2.35" result="wa"/><feComposite in="wb" in2="wa" operator="in"/>';
454
+ case 'blueprint':
455
+ return full ? grain(seed, '#ffffff', 0.9, 1.2) + '<feMerge><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>' : '';
456
+ case 'pointillism':
457
+ if (!full) return '';
458
+ // puntos: una muestra cada 5 unidades, difuminada y recortada en círculo, sobre una base tenue
459
+ return '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="5" height="5"/><feTile result="tile"/>'
460
+ + '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feGaussianBlur stdDeviation="1.15"/>'
461
+ + '<feComponentTransfer result="dt"><feFuncA type="linear" slope="6" intercept="-.45"/></feComponentTransfer>'
462
+ + wobble(seed, 'dt', 2.5, 0.25, 'dj')
463
+ + '<feColorMatrix in="SourceGraphic" values=".8 0 0 0 .18 0 .8 0 0 .18 0 0 .8 0 .18 0 0 0 .8 0" result="un"/>'
464
+ + '<feMerge><feMergeNode in="un"/><feMergeNode in="dj"/></feMerge>';
465
+ case 'woodcut':
466
+ return full ? wobble(seed, 'SourceGraphic', 2, 0.05) + grain(seed, '#efe4cc', 1.4, 0.7) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
467
+ default:
468
+ return '';
469
+ }
470
+ }
471
+
472
+ // ───────────────────────────── capa interior ─────────────────────────────
473
+
474
+ /** Lo que el estilo dibuja dentro del cuerpo (recortado a su contorno por quien llama). */
475
+ function innerLayer(s, L, uid, d) {
476
+ const sx = s._style;
477
+ const b = L.box;
478
+ const rnd = seeded(sx.seed ^ 0x5f3759df);
479
+ let defs = '';
480
+ let svg = '';
481
+ switch (sx.id) {
482
+ case 'ukiyoe': {
483
+ // bokashi: degradado de tinta arriba y vetas de la plancha de madera
484
+ defs += `<linearGradient id="${uid}-bk" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b2233" stop-opacity=".32"/><stop offset=".4" stop-color="#1b2233" stop-opacity="0"/></linearGradient>`;
485
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bk)"/>`;
486
+ let grainD = '';
487
+ for (let i = 0; i < 9; i++) {
488
+ const y = b.y + b.h * (0.12 + i * 0.1 + rnd() * 0.04);
489
+ const a = 2 + rnd() * 3;
490
+ grainD += `M${n(b.x - 4)} ${n(y)}C${n(b.x + b.w * 0.3)} ${n(y - a)} ${n(b.x + b.w * 0.6)} ${n(y + a)} ${n(b.x + b.w + 4)} ${n(y)}`;
491
+ }
492
+ svg += `<path d="${grainD}" fill="none" stroke="#1b2233" stroke-width=".8" opacity=".14"/>`;
493
+ break;
494
+ }
495
+ case 'clay': {
496
+ // huellas: medias lunas de dedo apretado en la masa
497
+ for (let i = 0; i < 4; i++) {
498
+ const x = b.x + b.w * (0.2 + rnd() * 0.6);
499
+ const y = b.y + b.h * (0.25 + rnd() * 0.6);
500
+ const r = b.w * (0.05 + rnd() * 0.04);
501
+ svg += `<path d="M${n(x - r)} ${n(y)}A${n(r)} ${n(r * 0.6)} 0 0 0 ${n(x + r)} ${n(y)}" fill="none" stroke="#000" stroke-width="1.4" stroke-linecap="round" opacity=".1"/>`;
502
+ }
503
+ break;
504
+ }
505
+ case 'glass': {
506
+ // cristales: celdas de Voronoi con tono variable y plomo oscuro
507
+ const sites = jitterGrid(rnd, b.x, b.y, b.w, b.h, 3, 4, 0.9);
508
+ const cells = voronoi(sites, b.x - 10, b.y - 10, b.x + b.w + 10, b.y + b.h + 10);
509
+ let lead = '';
510
+ cells.forEach((poly, i) => {
511
+ const p = polyD(poly);
512
+ const k = rnd();
513
+ svg += `<path d="${p}" fill="${k < 0.5 ? '#000' : '#fff'}" opacity="${n(0.06 + (i % 3) * 0.06)}"/>`;
514
+ lead += p;
515
+ });
516
+ svg += `<path d="${lead}" fill="none" stroke="#141014" stroke-width="3.2" stroke-linejoin="round"/>`;
517
+ break;
518
+ }
519
+ case 'bauhaus': {
520
+ // mitades de primarios y un círculo: el cuerpo se vuelve composición
521
+ const c2 = s.body.patternColor;
522
+ const c3 = PAL.bauhaus.find((c) => c !== s.body.color && c !== c2) || '#151515';
523
+ const right = rnd() < 0.5;
524
+ svg += `<rect x="${n(right ? L.cx : b.x - 6)}" y="${n(b.y - 6)}" width="${n(b.w / 2 + 6)}" height="${n(b.h + 12)}" fill="${c2}"/>`;
525
+ svg += `<circle cx="${n(right ? b.x + b.w * 0.18 : b.x + b.w * 0.82)}" cy="${n(b.y + b.h * 0.92)}" r="${n(b.w * 0.26)}" fill="${c3}"/>`;
526
+ svg += `<path d="M${n(L.cx)} ${n(b.y - 6)}V${n(b.y + b.h + 6)}" stroke="#151515" stroke-width="3"/>`;
527
+ break;
528
+ }
529
+ case 'manuscript': {
530
+ // pan de oro: punteado y filete dorado por dentro del contorno
531
+ defs += `<pattern id="${uid}-gd" width="9" height="9" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><circle cx="4.5" cy="4.5" r="1.1" fill="#c99a2e"/></pattern>`;
532
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-gd)" opacity=".55"/>`;
533
+ svg += `<path d="${d}" fill="none" stroke="#c99a2e" stroke-width="7"/><path d="${d}" fill="none" stroke="#f3d27a" stroke-width="2" opacity=".8"/>`;
534
+ break;
535
+ }
536
+ case 'glitch': {
537
+ // líneas de barrido
538
+ defs += `<pattern id="${uid}-sl" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="4" height="1.4" fill="#000" opacity=".28"/></pattern>`;
539
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sl)"/>`;
540
+ break;
541
+ }
542
+ case 'ink': {
543
+ // sombreado cruzado suave en la parte de sombra
544
+ defs += `<pattern id="${uid}-xh" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(-35)"><path d="M0 0V6" stroke="#222" stroke-width=".8"/></pattern>`;
545
+ svg += `<ellipse cx="${n(L.cx + b.w * 0.32)}" cy="${n(b.y + b.h * 0.78)}" rx="${n(b.w * 0.42)}" ry="${n(b.h * 0.36)}" fill="url(#${uid}-xh)" opacity=".35"/>`;
546
+ break;
547
+ }
548
+ case 'pop': {
549
+ // Ben-Day: trama de puntos claros en todo el cuerpo, oscuros en la sombra, y un brillo de cómic
550
+ defs += `<pattern id="${uid}-bl" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="3.5" cy="3.5" r="2" fill="#fff" opacity=".5"/></pattern>`;
551
+ defs += `<pattern id="${uid}-bk" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="3.5" cy="3.5" r="2.4" fill="#111" opacity=".42"/></pattern>`;
552
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bl)"/>`;
553
+ svg += `<path d="${d}" fill="url(#${uid}-bk)" transform="translate(${n(b.w * 0.16)} ${n(b.h * 0.14)})"/>`;
554
+ const gx = b.x + b.w * 0.24;
555
+ const gy = b.y + b.h * 0.18;
556
+ svg += `<path d="M${n(gx)} ${n(gy + 14)}Q${n(gx + 2)} ${n(gy)} ${n(gx + 16)} ${n(gy - 4)}" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round"/><circle cx="${n(gx + 24)}" cy="${n(gy - 6)}" r="2.6" fill="#fff"/>`;
557
+ break;
558
+ }
559
+ case 'watercolor': {
560
+ // floraciones: manchas con el cerco más oscuro, y un hueco de papel sin pintar como brillo
561
+ defs += `<radialGradient id="${uid}-wb"><stop offset="0" stop-color="#000" stop-opacity="0"/><stop offset=".7" stop-color="#000" stop-opacity=".04"/><stop offset=".93" stop-color="#000" stop-opacity=".16"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>`;
562
+ defs += `<radialGradient id="${uid}-wh"><stop offset="0" stop-color="#fffdf6" stop-opacity=".85"/><stop offset=".6" stop-color="#fffdf6" stop-opacity=".5"/><stop offset="1" stop-color="#fffdf6" stop-opacity="0"/></radialGradient>`;
563
+ for (let i = 0; i < 5; i++) {
564
+ svg += `<circle cx="${n(b.x + b.w * (0.15 + rnd() * 0.7))}" cy="${n(b.y + b.h * (0.3 + rnd() * 0.65))}" r="${n(b.w * (0.12 + rnd() * 0.14))}" fill="url(#${uid}-wb)"/>`;
565
+ }
566
+ svg += `<ellipse cx="${n(b.x + b.w * 0.3)}" cy="${n(b.y + b.h * 0.22)}" rx="${n(b.w * 0.16)}" ry="${n(b.h * 0.1)}" fill="url(#${uid}-wh)"/>`;
567
+ svg += `<path d="${d}" fill="#000" opacity=".1" transform="translate(${n(b.w * 0.12)} ${n(b.h * 0.16)})"/>`;
568
+ break;
569
+ }
570
+ case 'pixel': {
571
+ // sombreado de dos tonos (los píxeles del filtro lo escalonan)
572
+ svg += `<path d="${d}" fill="#000" opacity=".26" transform="translate(${n(b.w * 0.14)} ${n(b.h * 0.14)})"/>`;
573
+ svg += `<rect x="${n(b.x + b.w * 0.18)}" y="${n(b.y + b.h * 0.1)}" width="${n(b.w * 0.16)}" height="${n(b.h * 0.08)}" fill="#fff" opacity=".55"/>`;
574
+ break;
575
+ }
576
+ case 'synthwave': {
577
+ // atardecer en el cuerpo: violeta abajo, franjas cromadas y líneas de barrido
578
+ defs += `<linearGradient id="${uid}-sw" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".28"/><stop offset=".35" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#2a0857" stop-opacity=".6"/></linearGradient>`;
579
+ defs += `<pattern id="${uid}-sl" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="4" height="1" fill="#1a0b33" opacity=".22"/></pattern>`;
580
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sw)"/><rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sl)"/>`;
581
+ let bars = '';
582
+ for (let i = 0; i < 4; i++) bars += `<rect x="${n(b.x - 4)}" y="${n(b.y + b.h * (0.62 + i * 0.09))}" width="${n(b.w + 8)}" height="${n(1.5 + i * 1.3)}"/>`;
583
+ svg += `<g fill="#1a0b33" opacity=".45">${bars}</g>`;
584
+ break;
585
+ }
586
+ case 'nouveau': {
587
+ // latigazos dorados que suben como tallos, con capullos
588
+ let curves = '';
589
+ let buds = '';
590
+ for (let i = 0; i < 3; i++) {
591
+ const x = b.x + b.w * (0.2 + i * 0.3 + (rnd() - 0.5) * 0.08);
592
+ const y0 = b.y + b.h + 4;
593
+ const y1 = b.y + b.h * (0.5 + rnd() * 0.15);
594
+ const sw = (i % 2 ? 1 : -1) * b.w * 0.12;
595
+ curves += `M${n(x)} ${n(y0)}C${n(x + sw)} ${n(y0 - (y0 - y1) * 0.4)} ${n(x - sw)} ${n(y1 + (y0 - y1) * 0.3)} ${n(x + sw * 0.4)} ${n(y1)}`;
596
+ buds += `<circle cx="${n(x + sw * 0.4)}" cy="${n(y1 - 3)}" r="3.2"/>`;
597
+ }
598
+ svg += `<path d="${curves}" fill="none" stroke="#c9a24a" stroke-width="2.4" stroke-linecap="round" opacity=".8"/><g fill="#c9a24a" opacity=".85">${buds}</g>`;
599
+ svg += `<path d="${d}" fill="none" stroke="#efe2c4" stroke-width="10" opacity=".25"/>`;
600
+ break;
601
+ }
602
+ case 'cubism': {
603
+ // facetas: celdas de Voronoi muy irregulares, cada una con su propia luz y algún plano de color
604
+ defs += `<linearGradient id="${uid}-c0" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset="1" stop-color="#000" stop-opacity=".3"/></linearGradient>`;
605
+ defs += `<linearGradient id="${uid}-c1" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#000" stop-opacity=".35"/><stop offset="1" stop-color="#fff" stop-opacity=".25"/></linearGradient>`;
606
+ const sites = jitterGrid(rnd, b.x, b.y, b.w, b.h, 3, 3, 1.1);
607
+ const cells = voronoi(sites, b.x - 10, b.y - 10, b.x + b.w + 10, b.y + b.h + 10);
608
+ let lines = '';
609
+ cells.forEach((poly, i) => {
610
+ const p = polyD(poly);
611
+ const k = rnd();
612
+ if (k < 0.38) svg += `<path d="${p}" fill="${PAL.cubism[Math.floor(rnd() * 5)]}" opacity=".65"/>`;
613
+ svg += `<path d="${p}" fill="url(#${uid}-c${i % 2})"/>`;
614
+ lines += p;
615
+ });
616
+ svg += `<path d="${lines}" fill="none" stroke="#2e2a26" stroke-width="1.6" stroke-linejoin="miter" opacity=".75"/>`;
617
+ break;
618
+ }
619
+ case 'crayon': {
620
+ // relleno de niño: zigzag de cera que se sale de las líneas
621
+ let z = '';
622
+ const step = 5.5;
623
+ let up = true;
624
+ for (let x = b.x - 10; x < b.x + b.w + 10; x += step) {
625
+ z += `${z ? 'L' : 'M'}${n(x + rnd() * 2)} ${n(up ? b.y - 6 + rnd() * 8 : b.y + b.h + 6 - rnd() * 8)}`;
626
+ up = !up;
627
+ }
628
+ svg += `<path d="${z}" fill="none" stroke="#000" stroke-width="2.2" stroke-linejoin="round" opacity=".14" transform="rotate(14 ${n(L.cx)} ${n(b.y + b.h / 2)})"/>`;
629
+ svg += `<path d="${d}" fill="none" stroke="#fff" stroke-width="7" opacity=".35"/>`;
630
+ break;
631
+ }
632
+ case 'blueprint': {
633
+ // cuadrícula fina y ejes de construcción (raya-punto) con un círculo guía
634
+ defs += `<pattern id="${uid}-bg" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M10 0H0V10" fill="none" stroke="#eaf2ff" stroke-width=".5" opacity=".35"/></pattern>`;
635
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bg)"/>`;
636
+ const cy = b.y + b.h * 0.55;
637
+ svg += `<path d="M${n(L.cx)} ${n(b.y - 6)}V${n(b.y + b.h + 6)}M${n(b.x - 6)} ${n(cy)}H${n(b.x + b.w + 6)}" stroke="#eaf2ff" stroke-width=".9" stroke-dasharray="9 3 2 3" opacity=".75"/>`;
638
+ svg += `<circle cx="${n(L.cx)}" cy="${n(cy)}" r="${n(Math.min(b.w, b.h) * 0.36)}" fill="none" stroke="#eaf2ff" stroke-width=".8" stroke-dasharray="4 3" opacity=".55"/>`;
639
+ break;
640
+ }
641
+ case 'pointillism': {
642
+ // toques de color: claros, complementarios y oscuros (en lite es lo único que queda del estilo)
643
+ const [h] = toHsl(s.body.color);
644
+ const comp = fromHsl((h + 180) % 360, 0.6, 0.6);
645
+ const ana = fromHsl((h + 40) % 360, 0.7, 0.62);
646
+ defs += `<pattern id="${uid}-pt" width="9" height="9" patternUnits="userSpaceOnUse"><circle cx="1.5" cy="2" r="1.4" fill="#fff" opacity=".6"/><circle cx="6" cy="1.5" r="1.3" fill="${comp}" opacity=".7"/><circle cx="4" cy="6" r="1.4" fill="${ana}" opacity=".8"/><circle cx="8" cy="7" r="1.2" fill="#000" opacity=".22"/><circle cx="1" cy="7.5" r="1.1" fill="${comp}" opacity=".45"/></pattern>`;
647
+ svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-pt)"/>`;
648
+ break;
649
+ }
650
+ case 'woodcut': {
651
+ // tallas: el contorno repetido hacia dentro (líneas de gubia) y un sombreado de rayas
652
+ const cx = L.cx;
653
+ const cy = b.y + b.h * 0.58;
654
+ let rings = '';
655
+ [0.84, 0.7, 0.57, 0.45].forEach((k, i) => {
656
+ rings += `<path d="${d}" transform="translate(${n(cx * (1 - k))} ${n(cy * (1 - k))}) scale(${k})" stroke-width="${n((2.6 - i * 0.45) / k)}"/>`;
657
+ });
658
+ svg += `<g fill="none" stroke="#15120f" opacity=".8">${rings}</g>`;
659
+ defs += `<pattern id="${uid}-wh" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(-40)"><rect width="5" height="2.2" fill="#15120f"/></pattern>`;
660
+ svg += `<path d="${d}" fill="url(#${uid}-wh)" transform="translate(${n(b.w * 0.22)} ${n(b.h * 0.2)})"/>`;
661
+ break;
662
+ }
663
+ }
664
+ return { defs, svg };
665
+ }
666
+
667
+ /**
668
+ * Piezas del estilo para renderLayers: defs (filtro + patrones), capa interior del cuerpo y el
669
+ * `url(#…)` del filtro que se aplica a todo el personaje ('' si no hay).
670
+ * @param {object} s spec ya estilizada (stylizeSpec)
671
+ * @param {object} L layout del cuerpo
672
+ * @param {{ uid:string, d:string, quality?:'full'|'lite'|'off', animate?:boolean }} ctx
673
+ */
674
+ export function styleLayers(s, L, ctx) {
675
+ const sx = s?._style;
676
+ if (!sx) return { defs: '', inner: '', filter: '' };
677
+ const q = ctx.quality === 'off' ? 'lite' : ctx.quality || 'full';
678
+ const fb = filterBody(sx.id, sx.seed, q, !!ctx.animate);
679
+ const il = innerLayer(s, L, ctx.uid, ctx.d);
680
+ const fid = `${ctx.uid}-sty`;
681
+ return {
682
+ defs: il.defs + (fb ? `<filter id="${fid}" ${REGION}>${fb}</filter>` : ''),
683
+ inner: il.svg,
684
+ filter: fb ? `url(#${fid})` : '',
685
+ };
686
+ }
687
+
688
+ // ───────────────────────────── fondos ─────────────────────────────
689
+
690
+ // Marco fijo de 300×300; el suelo arranca hacia y=240 (los pies del ot caen ahí).
691
+ const W = 300;
692
+ const GROUND = 240;
693
+
694
+ const BACKDROPS = {
695
+ ukiyoe(p, rnd) {
696
+ const sunX = 190 + rnd() * 50;
697
+ let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".75" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .35 0 0 0 0 .25 0 0 0 0 .12 0 0 0 1.4 -.72"/></filter></defs>`;
698
+ s += `<rect width="${W}" height="${W}" fill="#ece0c4"/>`;
699
+ s += `<circle cx="${n(sunX)}" cy="62" r="34" fill="#c8402c"/>`;
700
+ // Fuji con su capa de nieve dentada
701
+ const fx = 150 + rnd() * 30;
702
+ s += `<path d="M${n(fx - 95)} ${GROUND}L${n(fx - 16)} 128Q${n(fx)} 118 ${n(fx + 16)} 128L${n(fx + 95)} ${GROUND}Z" fill="#46688a"/>`;
703
+ let snow = `M${n(fx - 16)} 128Q${n(fx)} 118 ${n(fx + 16)} 128L${n(fx + 32)} 152`;
704
+ for (let i = 0; i < 6; i++) snow += `L${n(fx + 32 - (i + 0.5) * 10.7)} ${n(i % 2 ? 150 : 140 + rnd() * 6)}`;
705
+ s += `<path d="${snow}L${n(fx - 32)} 152Z" fill="#f3ead6"/>`;
706
+ // mar con escamas de olas
707
+ s += `<rect y="214" width="${W}" height="${GROUND - 214}" fill="#2f5579"/>`;
708
+ let sc = '';
709
+ for (let y = 218; y < GROUND; y += 7) for (let x = (y % 14 ? 0 : 7); x < W; x += 14) sc += `M${x} ${y + 5}a7 7 0 0 1 14 0`;
710
+ s += `<path d="${sc}" fill="none" stroke="#e9e0c8" stroke-width="1.1" opacity=".75"/>`;
711
+ // la gran ola: lengua oscura, banda clara y garras de espuma
712
+ s += '<path d="M0 240V150C8 92 70 56 124 78C146 88 152 108 140 122C132 102 106 98 96 116C88 132 100 152 124 162C96 178 70 204 78 240Z" fill="#1f3a5f"/>';
713
+ s += '<path d="M8 236C4 170 40 112 92 100" fill="none" stroke="#5d86a8" stroke-width="9" opacity=".8"/><path d="M22 236C22 186 46 140 84 124" fill="none" stroke="#e9e0c8" stroke-width="1.4" opacity=".6"/><path d="M36 236C38 196 56 160 86 146" fill="none" stroke="#e9e0c8" stroke-width="1.4" opacity=".5"/>';
714
+ let claws = '';
715
+ const crest = [[124, 78], [134, 86], [141, 98], [142, 110], [140, 122], [128, 108], [116, 102], [104, 104]];
716
+ for (const [x, y] of crest) claws += `<circle cx="${x}" cy="${y}" r="${n(3 + rnd() * 2.5)}" fill="#f3ead6"/>`;
717
+ for (let i = 0; i < 14; i++) claws += `<circle cx="${n(100 + rnd() * 70)}" cy="${n(60 + rnd() * 60)}" r="${n(0.8 + rnd() * 1.6)}" fill="#f3ead6"/>`;
718
+ s += claws;
719
+ // suelo de madera: tablas, juntas y vetas
720
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#b98a55"/>`;
721
+ let pl = '';
722
+ for (const y of [254, 270, 287]) {
723
+ pl += `M0 ${y}H${W}`;
724
+ for (let x = rnd() * 60; x < W; x += 70 + rnd() * 50) pl += `M${n(x)} ${y}v${y === 287 ? 13 : y === 254 ? -14 : -16}`;
725
+ }
726
+ s += `<path d="M0 ${GROUND}H${W}" stroke="#6e4826" stroke-width="2"/><path d="${pl}" stroke="#8a5f34" stroke-width="1.2" fill="none"/>`;
727
+ // sello vertical rojo con 七つ
728
+ s += '<rect x="262" y="22" width="24" height="62" rx="2" fill="#b8322a"/><text x="274" y="48" font-size="19" text-anchor="middle" fill="#f3e6cc" font-family="\'Noto Serif CJK JP\',\'Hiragino Mincho ProN\',serif">七</text><text x="274" y="74" font-size="17" text-anchor="middle" fill="#f3e6cc" font-family="\'Noto Serif CJK JP\',\'Hiragino Mincho ProN\',serif">つ</text>';
729
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
730
+ return s;
731
+ },
732
+
733
+ clay(p, rnd) {
734
+ const light = '<feDistantLight azimuth="235" elevation="50"/>';
735
+ let s = `<defs><linearGradient id="${p}sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#8fcdf0"/><stop offset="1" stop-color="#dff3fb"/></linearGradient>`
736
+ + `<filter id="${p}c" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency=".03" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="6" xChannelSelector="R" yChannelSelector="G" result="d"/><feGaussianBlur in="d" stdDeviation="4" result="b"/><feDiffuseLighting in="b" surfaceScale="4" diffuseConstant="1.18" lighting-color="#fff" result="l">${light}</feDiffuseLighting><feComposite in="d" in2="l" operator="arithmetic" k1="1" result="sh"/><feSpecularLighting in="b" surfaceScale="4" specularConstant=".45" specularExponent="18" lighting-color="#fff" result="sp">${light}</feSpecularLighting><feComposite in="sp" in2="d" operator="in" result="sp2"/><feComposite in="sh" in2="sp2" operator="arithmetic" k2="1" k3=".8"/></filter></defs>`;
737
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}sky)"/>`;
738
+ const F = `filter="url(#${p}c)"`;
739
+ // sol con rayos
740
+ const sx = 228 + rnd() * 30;
741
+ let rays = '';
742
+ for (let i = 0; i < 12; i++) rays += `<rect x="-4" y="-50" width="8" height="16" rx="4" fill="#ffb52e" transform="translate(${n(sx)} 58) rotate(${i * 30 + rnd() * 6})"/>`;
743
+ s += `<g ${F}>${rays}<circle cx="${n(sx)}" cy="58" r="26" fill="#ffcf3f"/></g>`;
744
+ // nubes gorditas
745
+ for (let k = 0; k < 2; k++) {
746
+ const cx = 40 + k * 110 + rnd() * 30;
747
+ const cy = 40 + rnd() * 40;
748
+ s += `<g ${F} fill="#fbfbff"><circle cx="${n(cx)}" cy="${n(cy)}" r="16"/><circle cx="${n(cx + 18)}" cy="${n(cy - 8)}" r="20"/><circle cx="${n(cx + 38)}" cy="${n(cy)}" r="15"/><rect x="${n(cx)}" y="${n(cy)}" width="38" height="15" rx="7"/></g>`;
749
+ }
750
+ // colinas
751
+ s += `<g ${F}><ellipse cx="60" cy="${GROUND + 8}" rx="120" ry="52" fill="#86c96a"/><ellipse cx="250" cy="${GROUND + 10}" rx="110" ry="44" fill="#9bd27a"/></g>`;
752
+ // árbol robusto
753
+ const tx = 40 + rnd() * 20;
754
+ s += `<g ${F}><path d="M${n(tx - 9)} ${GROUND + 4}C${n(tx - 6)} 200 ${n(tx - 8)} 180 ${n(tx - 4)} 150H${n(tx + 6)}C${n(tx + 9)} 180 ${n(tx + 8)} 200 ${n(tx + 12)} ${GROUND + 4}Z" fill="#8a5a33"/></g>`;
755
+ s += `<g ${F} fill="#4fae4a"><circle cx="${n(tx)}" cy="128" r="30"/><circle cx="${n(tx - 24)}" cy="148" r="20"/><circle cx="${n(tx + 24)}" cy="146" r="22"/><circle cx="${n(tx + 6)}" cy="104" r="20" fill="#5cc254"/></g>`;
756
+ // tierra con borde grumoso y piedritas
757
+ let top = `M-10 ${W}V${GROUND}`;
758
+ for (let x = -10; x < W + 10; x += 22) top += `Q${n(x + 11)} ${n(GROUND - 5 - rnd() * 6)} ${x + 22} ${GROUND}`;
759
+ s += `<g ${F}><path d="${top}V${W}Z" fill="#a8703f"/></g>`;
760
+ let peb = '';
761
+ for (let i = 0; i < 7; i++) peb += `<ellipse cx="${n(rnd() * W)}" cy="${n(GROUND + 14 + rnd() * 50)}" rx="${n(4 + rnd() * 5)}" ry="${n(3 + rnd() * 2)}" fill="${rnd() < 0.5 ? '#8a5730' : '#c08a58'}"/>`;
762
+ s += `<g ${F}>${peb}</g>`;
763
+ return s;
764
+ },
765
+
766
+ glass(p, rnd) {
767
+ const arch = `M40 ${GROUND}V118A140 140 0 0 1 150 12A140 140 0 0 1 260 118V${GROUND}Z`;
768
+ let s = `<defs><clipPath id="${p}a"><path d="${arch}"/></clipPath><radialGradient id="${p}gl" cx=".5" cy=".35" r=".6"><stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>`;
769
+ s += `<rect width="${W}" height="${W}" fill="#1c1622"/>`;
770
+ // sillares de piedra
771
+ let st = '';
772
+ for (let y = 0; y < GROUND; y += 20) for (let x = (y / 20) % 2 ? -15 : 0; x < W; x += 30) st += `M${x} ${y}h30v20`;
773
+ s += `<path d="${st}" fill="none" stroke="#2c2434" stroke-width="1.5"/>`;
774
+ // ventana: cristales de Voronoi con plomo
775
+ const sites = jitterGrid(rnd, 40, 12, 220, GROUND - 12, 6, 8, 0.95);
776
+ const cells = voronoi(sites, 40, 0, 260, GROUND);
777
+ let glass = '';
778
+ let lead = '';
779
+ cells.forEach((poly, i) => {
780
+ const y = sites[i][1] / GROUND;
781
+ // más azules arriba, más cálidos abajo
782
+ const pool = y < 0.45 ? ['#1e5aa8', '#2aa0b8', '#6a2c8c', '#1e5aa8', '#e2a52a'] : ['#b8222a', '#e2a52a', '#2f8a4a', '#d65a1f', '#1e5aa8'];
783
+ const d = polyD(poly);
784
+ glass += `<path d="${d}" fill="${pool[Math.floor(rnd() * pool.length)]}"/>`;
785
+ lead += d;
786
+ });
787
+ s += `<g clip-path="url(#${p}a)">${glass}<rect width="${W}" height="${W}" fill="url(#${p}gl)"/><path d="${lead}" fill="none" stroke="#141014" stroke-width="3.4" stroke-linejoin="round"/><path d="M150 12V${GROUND}M40 128H260" stroke="#141014" stroke-width="5"/><circle cx="150" cy="80" r="30" fill="none" stroke="#141014" stroke-width="5"/></g>`;
788
+ s += `<path d="${arch}" fill="none" stroke="#3a3044" stroke-width="10"/><path d="${arch}" fill="none" stroke="#141014" stroke-width="4"/>`;
789
+ // suelo de piedra con charcos de luz de colores
790
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#2a2230"/><rect y="${GROUND}" width="${W}" height="6" fill="#3a3044"/>`;
791
+ const pools = ['#1e5aa8', '#b8222a', '#e2a52a', '#2f8a4a'];
792
+ for (let i = 0; i < 4; i++) s += `<ellipse cx="${n(70 + i * 55 + rnd() * 15)}" cy="${n(GROUND + 26 + rnd() * 20)}" rx="${n(20 + rnd() * 12)}" ry="7" fill="${pools[i]}" opacity=".35"/>`;
793
+ return s;
794
+ },
795
+
796
+ bauhaus(p, rnd) {
797
+ let s = `<rect width="${W}" height="${W}" fill="#efe7d4"/>`;
798
+ // tira de película arriba
799
+ s += `<rect y="10" width="${W}" height="46" fill="#151515"/>`;
800
+ let holes = '';
801
+ for (let x = 4; x < W; x += 14) holes += `<rect x="${x}" y="14" width="7" height="5" rx="1" fill="#efe7d4"/><rect x="${x}" y="47" width="7" height="5" rx="1" fill="#efe7d4"/>`;
802
+ s += holes;
803
+ const icons = [
804
+ (x) => `<circle cx="${x + 30}" cy="35" r="9" fill="#d7262b"/>`,
805
+ (x) => `<rect x="${x + 21}" y="26" width="18" height="18" fill="#1f4e9c"/>`,
806
+ (x) => `<path d="M${x + 20} 44L${x + 30} 26L${x + 40} 44Z" fill="#f2b705"/>`,
807
+ ];
808
+ for (let i = 0; i < 5; i++) {
809
+ const x = -20 + i * 68 + (rnd() * 6);
810
+ s += `<rect x="${n(x)}" y="22" width="60" height="25" fill="#2b2b2b"/>${icons[(i + Math.floor(rnd() * 3)) % 3](n(x))}`;
811
+ }
812
+ // composición: círculo rojo, rectángulo azul, triángulo amarillo, barras negras
813
+ const left = rnd() < 0.5;
814
+ s += `<circle cx="${left ? 92 : 208}" cy="148" r="64" fill="#d7262b"/>`;
815
+ s += `<rect x="${left ? 196 : 26}" y="78" width="78" height="${GROUND - 78}" fill="#1f4e9c"/>`;
816
+ s += `<path d="M${left ? 170 : 40} ${GROUND}L${left ? 290 : 160} ${GROUND}L${left ? 230 : 100} 132Z" fill="#f2b705"/>`;
817
+ s += `<rect x="0" y="${n(96 + rnd() * 20)}" width="${W}" height="7" fill="#151515" transform="rotate(${left ? -14 : 14} 150 120)"/>`;
818
+ s += '<text x="28" y="230" transform="rotate(-90 28 230)" font-family="Futura,\'Century Gothic\',\'Avenir Next\',\'Helvetica Neue\',Arial,sans-serif" font-weight="900" font-size="30" letter-spacing="2" fill="#151515">BAUHAUS</text>';
819
+ s += `<text x="292" y="${GROUND - 8}" text-anchor="end" font-family="Futura,'Century Gothic','Helvetica Neue',Arial,sans-serif" font-weight="900" font-size="26" fill="#151515">1923</text>`;
820
+ // suelo
821
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#e2d6bb"/><rect y="${GROUND}" width="${W}" height="5" fill="#151515"/><rect x="${left ? 220 : 20}" y="${GROUND + 20}" width="60" height="10" fill="#1f4e9c"/>`;
822
+ return s;
823
+ },
824
+
825
+ manuscript(p, rnd) {
826
+ const letters = 'OASMTE';
827
+ const L = letters[Math.floor(rnd() * letters.length)];
828
+ let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".6" numOctaves="3" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .45 0 0 0 0 .32 0 0 0 0 .14 0 0 0 1.2 -.62"/></filter></defs>`;
829
+ s += `<rect width="${W}" height="${W}" fill="#e9d6aa"/>`;
830
+ // escena en mosaico de teselas (cielo, estrellas doradas, colinas)
831
+ const x0 = 24;
832
+ const y0 = 84;
833
+ const T = 9;
834
+ let mos = '';
835
+ const hill = (x) => 196 + Math.sin(x / 38 + rnd() * 0.02) * 14 + Math.sin(x / 17) * 5;
836
+ for (let y = y0; y < 276; y += T) {
837
+ for (let x = x0; x < 276; x += T) {
838
+ let c;
839
+ if (y + T > 268) c = ['#7a4a26', '#8a5a30', '#6e4220'][Math.floor(rnd() * 3)];
840
+ else if (y > hill(x)) c = ['#2e6b3a', '#3a7d44', '#25592f'][Math.floor(rnd() * 3)];
841
+ else {
842
+ const k = (y - y0) / 120;
843
+ c = rnd() < 0.05 && k < 0.7 ? '#e0b64a' : k < 0.35 ? ['#1d3f8f', '#22489d', '#193780'][Math.floor(rnd() * 3)] : ['#3a64b0', '#4672bb', '#3159a3'][Math.floor(rnd() * 3)];
844
+ }
845
+ mos += `<rect x="${x + 0.6}" y="${y + 0.6}" width="${T - 1.2}" height="${T - 1.2}" fill="${c}"/>`;
846
+ }
847
+ }
848
+ s += `<rect x="${x0 - 2}" y="${y0 - 2}" width="${276 - x0 + 4}" height="${276 - y0 + 2}" fill="#3b2412"/>${mos}`;
849
+ s += `<rect x="${x0 - 3}" y="${y0 - 3}" width="${276 - x0 + 6}" height="${276 - y0 + 4}" fill="none" stroke="#c99a2e" stroke-width="3"/>`;
850
+ // inicial capitular dorada
851
+ s += '<rect x="22" y="20" width="52" height="54" fill="#1d3f8f" stroke="#c99a2e" stroke-width="3"/><path d="M26 70C40 56 30 38 46 30" fill="none" stroke="#2e6b3a" stroke-width="2"/><circle cx="46" cy="30" r="3" fill="#b3261e"/>';
852
+ s += `<text x="48" y="64" text-anchor="middle" font-family="'Cinzel','Trajan Pro','Book Antiqua',Palatino,Georgia,serif" font-weight="700" font-size="44" fill="#e0b64a" stroke="#7a5a16" stroke-width=".8">${L}</text>`;
853
+ // dos columnas de texto (renglones de «palabras»), la primera con rúbrica roja
854
+ let txt = '';
855
+ for (const [cx0, cx1] of [[84, 176], [186, 278]]) {
856
+ for (let r = 0; r < 6; r++) {
857
+ let x = cx0;
858
+ const y = 24 + r * 9;
859
+ while (x < cx1 - 6) {
860
+ const w = Math.min(cx1 - x, 6 + rnd() * 16);
861
+ txt += `<rect x="${n(x)}" y="${y}" width="${n(w)}" height="2.6" rx="1" fill="${r === 0 && x === cx0 ? '#b3261e' : '#3b2412'}" opacity=".78"/>`;
862
+ x += w + 3;
863
+ }
864
+ }
865
+ }
866
+ s += txt;
867
+ // marco: oro, filete rojo y medallones en las esquinas
868
+ s += `<rect x="6" y="6" width="${W - 12}" height="${W - 12}" fill="none" stroke="#c99a2e" stroke-width="6"/><rect x="13" y="13" width="${W - 26}" height="${W - 26}" fill="none" stroke="#b3261e" stroke-width="1.5"/>`;
869
+ for (const [x, y] of [[9, 9], [291, 9], [9, 291], [291, 291]]) s += `<circle cx="${x}" cy="${y}" r="8" fill="#c99a2e"/><circle cx="${x}" cy="${y}" r="3.5" fill="#b3261e"/>`;
870
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
871
+ return s;
872
+ },
873
+
874
+ glitch(p, rnd) {
875
+ // degradado tramado: cada banda se cubre con 25/50/75 % de píxeles del color siguiente
876
+ const cols = ['#120a2a', '#2a0f55', '#5a1478', '#a21a8a', '#ff2bd6'];
877
+ const dens = [[[0, 0]], [[0, 0], [3, 3]], [[0, 0], [3, 3], [3, 0]]];
878
+ let s = '<defs>';
879
+ for (let i = 1; i < cols.length; i++) {
880
+ dens.forEach((pts, k) => {
881
+ s += `<pattern id="${p}d${i}${k}" width="6" height="6" patternUnits="userSpaceOnUse">${pts.map(([x, y]) => `<rect x="${x}" y="${y}" width="3" height="3" fill="${cols[i]}"/>`).join('')}</pattern>`;
882
+ });
883
+ }
884
+ s += `<pattern id="${p}sl" width="3" height="3" patternUnits="userSpaceOnUse"><rect width="3" height="1.2" fill="#000" opacity=".35"/></pattern></defs>`;
885
+ const band = GROUND / (cols.length - 1);
886
+ for (let i = 0; i < cols.length - 1; i++) {
887
+ const y = i * band;
888
+ s += `<rect y="${n(y)}" width="${W}" height="${n(band + 1)}" fill="${cols[i]}"/>`;
889
+ for (let k = 0; k < 3; k++) s += `<rect y="${n(y + (k * band) / 3)}" width="${W}" height="${n(band / 3 + 0.5)}" fill="url(#${p}d${i + 1}${k})"/>`;
890
+ }
891
+ // suelo: rejilla en perspectiva con aberración cromática
892
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#0b0b14"/>`;
893
+ let grid = '';
894
+ for (let i = 0; i <= 12; i++) grid += `M${150 + (i - 6) * 9} ${GROUND}L${150 + (i - 6) * 70} ${W}`;
895
+ for (let j = 1; j < 6; j++) {
896
+ const y = GROUND + (W - GROUND) * (j / 6) ** 1.7;
897
+ grid += `M0 ${n(y)}H${W}`;
898
+ }
899
+ s += `<path d="${grid}" stroke="#ff2b5e" stroke-width="1" fill="none" opacity=".7" transform="translate(-1.5 0)"/><path d="${grid}" stroke="#2bf3ff" stroke-width="1" fill="none" opacity=".8" transform="translate(1.5 0)"/><path d="M0 ${GROUND}H${W}" stroke="#fff" stroke-width="1.5"/>`;
900
+ // bloques de error
901
+ let blocks = '';
902
+ const bc = ['#2bf3ff', '#ff2bd6', '#ffffff', '#f2ff2b'];
903
+ for (let i = 0; i < 9; i++) blocks += `<rect x="${n(rnd() * W - 30)}" y="${n(rnd() * GROUND)}" width="${n(20 + rnd() * 90)}" height="${n(2 + rnd() * 7)}" fill="${bc[Math.floor(rnd() * bc.length)]}" opacity="${n(0.25 + rnd() * 0.5)}"/>`;
904
+ s += blocks;
905
+ const ty = 40 + rnd() * 30;
906
+ const font = 'font-family="\'Courier New\',ui-monospace,monospace" font-weight="700" font-size="22" letter-spacing="3"';
907
+ s += `<text x="148" y="${n(ty)}" ${font} text-anchor="middle" fill="#ff2b5e" opacity=".85">7OTS.EXE</text><text x="152" y="${n(ty)}" ${font} text-anchor="middle" fill="#2bf3ff" opacity=".85">7OTS.EXE</text><text x="150" y="${n(ty)}" ${font} text-anchor="middle" fill="#fff">7OTS.EXE</text>`;
908
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}sl)"/>`;
909
+ return s;
910
+ },
911
+
912
+ ink(p, rnd) {
913
+ let s = `<defs><filter id="${p}w" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".04" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".9" numOctaves="2" seed="3"/><feColorMatrix values="0 0 0 0 .4 0 0 0 0 .38 0 0 0 0 .34 0 0 0 .9 -.52"/></filter></defs>`;
914
+ s += `<rect width="${W}" height="${W}" fill="#f7f3ea"/>`;
915
+ // montañas: dos crestas a línea, la lejana más tenue
916
+ const ridge = (base, amp, k) => {
917
+ let d = `M-10 ${base}`;
918
+ for (let x = 0; x <= W + 10; x += 18 + rnd() * 14) d += `L${n(x)} ${n(base - amp * (0.3 + rnd() * 0.7) * (0.6 + 0.4 * Math.sin(x / (40 + k * 20))))}`;
919
+ return d;
920
+ };
921
+ let lines = `<path d="${ridge(170, 70, 1)}" stroke="#b4ada0" stroke-width="1.2"/><path d="${ridge(205, 55, 2)}" stroke="#7d776c" stroke-width="1.6"/>`;
922
+ // sol a línea y pájaros
923
+ lines += `<circle cx="${n(215 + rnd() * 40)}" cy="${n(55 + rnd() * 15)}" r="20" stroke="#8d877c" stroke-width="1.3"/>`;
924
+ for (let i = 0; i < 3; i++) {
925
+ const x = 60 + rnd() * 120;
926
+ const y = 40 + rnd() * 40;
927
+ lines += `<path d="M${n(x - 6)} ${n(y)}q3 -4 6 0q3 -4 6 0" stroke="#5c574f" stroke-width="1.1"/>`;
928
+ }
929
+ // ola ligera
930
+ lines += '<path d="M-5 238C20 206 60 192 92 204C110 211 112 228 98 232C90 220 76 222 76 232" stroke="#6f6a61" stroke-width="1.6"/><path d="M110 236C140 226 170 228 196 238M200 232C230 222 262 224 300 236" stroke="#9a948a" stroke-width="1.2"/>';
931
+ // suelo: una línea y unos trazos de sombreado
932
+ lines += `<path d="M0 ${GROUND + 2}H${W}" stroke="#3b3833" stroke-width="1.8"/>`;
933
+ let hatch = '';
934
+ for (let i = 0; i < 22; i++) {
935
+ const x = rnd() * W;
936
+ const y = GROUND + 10 + rnd() * 50;
937
+ hatch += `M${n(x)} ${n(y)}l${n(10 + rnd() * 14)} -3`;
938
+ }
939
+ lines += `<path d="${hatch}" stroke="#8d877c" stroke-width="1"/>`;
940
+ s += `<g fill="none" stroke-linecap="round" stroke-linejoin="round" filter="url(#${p}w)">${lines}</g>`;
941
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
942
+ return s;
943
+ },
944
+
945
+ pop(p, rnd) {
946
+ const word = ['POW!', 'WOW!', 'ZAP!', 'BAM!', 'WHAAM!'][Math.floor(rnd() * 5)];
947
+ let s = `<defs><pattern id="${p}d" width="9" height="9" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="4.5" cy="4.5" r="2.6" fill="#e4002b" opacity=".55"/></pattern>`
948
+ + `<pattern id="${p}w" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="4" cy="4" r="2.2" fill="#fff" opacity=".8"/></pattern></defs>`;
949
+ // rayos amarillos alternos desde detrás del ot
950
+ s += `<rect width="${W}" height="${W}" fill="#ffe14a"/>`;
951
+ let rays = '';
952
+ const cx = 150;
953
+ const cy = 150;
954
+ for (let i = 0; i < 16; i++) {
955
+ const a0 = (i / 16) * Math.PI * 2;
956
+ const a1 = a0 + Math.PI / 16;
957
+ rays += `M${cx} ${cy}L${n(cx + Math.cos(a0) * 320)} ${n(cy + Math.sin(a0) * 320)}L${n(cx + Math.cos(a1) * 320)} ${n(cy + Math.sin(a1) * 320)}Z`;
958
+ }
959
+ s += `<path d="${rays}" fill="#ffd400"/><rect width="${W}" height="${W}" fill="url(#${p}d)" opacity=".7"/>`;
960
+ // estallido con la onomatopeya
961
+ const bx = 68 + rnd() * 12;
962
+ const by = 66;
963
+ let burst = '';
964
+ for (let i = 0; i < 26; i++) {
965
+ const a = (i / 26) * Math.PI * 2;
966
+ const r = i % 2 ? 30 : 56 + rnd() * 10;
967
+ burst += `${i ? 'L' : 'M'}${n(bx + Math.cos(a) * r * 1.25)} ${n(by + Math.sin(a) * r)}`;
968
+ }
969
+ s += `<path d="${burst}Z" fill="#e4002b" stroke="#111" stroke-width="4" stroke-linejoin="miter"/>`;
970
+ s += `<path d="${burst}Z" fill="#fff36b" transform="translate(${n(bx * 0.38)} ${n(by * 0.38)}) scale(.62)"/>`;
971
+ const fs = word.length > 4 ? 22 : 28;
972
+ s += `<text x="${n(bx)}" y="${n(by + fs * 0.36)}" text-anchor="middle" transform="rotate(-10 ${n(bx)} ${n(by)})" font-family="Impact,'Arial Black','Helvetica Neue',sans-serif" font-weight="900" font-size="${fs}" fill="#0072ce" stroke="#111" stroke-width="1.6" paint-order="stroke">${word}</text>`;
973
+ // globo de diálogo arriba a la derecha
974
+ s += '<path d="M196 22H284Q292 22 292 30V58Q292 66 284 66H236L222 80L224 66H196Q188 66 188 58V30Q188 22 196 22Z" fill="#fff" stroke="#111" stroke-width="3"/>';
975
+ s += '<text x="240" y="40" text-anchor="middle" font-family="\'Comic Sans MS\',\'Comic Neue\',\'Chalkboard SE\',sans-serif" font-weight="700" font-size="12" fill="#111">I\'M AN OT,</text><text x="240" y="56" text-anchor="middle" font-family="\'Comic Sans MS\',\'Comic Neue\',\'Chalkboard SE\',sans-serif" font-weight="700" font-size="12" fill="#111">DARLING!</text>';
976
+ // suelo azul con trama blanca, y el marco de la viñeta
977
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#0072ce"/><rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="url(#${p}w)" opacity=".5"/><path d="M0 ${GROUND}H${W}" stroke="#111" stroke-width="4"/>`;
978
+ s += `<rect x="3" y="3" width="${W - 6}" height="${W - 6}" fill="none" stroke="#111" stroke-width="6"/>`;
979
+ return s;
980
+ },
981
+
982
+ watercolor(p, rnd) {
983
+ const sd = () => Math.floor(rnd() * 900);
984
+ let s = `<defs><filter id="${p}w" x="-15%" y="-15%" width="130%" height="130%"><feTurbulence type="fractalNoise" baseFrequency=".022" numOctaves="3" seed="${sd()}"/><feDisplacementMap in="SourceGraphic" scale="16" xChannelSelector="R" yChannelSelector="G" result="d"/><feGaussianBlur in="d" stdDeviation="1.1" result="b"/><feGaussianBlur in="d" stdDeviation="3.5" result="eb"/><feComposite in="d" in2="eb" operator="out" result="e"/><feColorMatrix in="e" values=".6 0 0 0 0 0 .6 0 0 0 0 0 .6 0 0 0 0 0 .5 0" result="ed"/><feMerge><feMergeNode in="b"/><feMergeNode in="ed"/></feMerge></filter>`
985
+ + `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".7" numOctaves="3" seed="${sd()}"/><feColorMatrix values="0 0 0 0 .45 0 0 0 0 .38 0 0 0 0 .3 0 0 0 1.1 -.6"/></filter>`
986
+ + `<linearGradient id="${p}sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6fa3d6"/><stop offset=".7" stop-color="#a9cbe8"/><stop offset="1" stop-color="#a9cbe8" stop-opacity="0"/></linearGradient></defs>`;
987
+ s += `<rect width="${W}" height="${W}" fill="#fbf7ee"/>`;
988
+ const F = `filter="url(#${p}w)"`;
989
+ // aguadas: cielo, nubes sin pintar, sol corrido
990
+ s += `<g ${F}><rect x="10" y="10" width="280" height="170" fill="url(#${p}sky)" opacity=".85"/></g>`;
991
+ for (let k = 0; k < 3; k++) {
992
+ const x = 30 + k * 90 + rnd() * 30;
993
+ const y = 40 + rnd() * 50;
994
+ s += `<g ${F} fill="#fbf7ee"><ellipse cx="${n(x)}" cy="${n(y)}" rx="${n(22 + rnd() * 12)}" ry="11"/><ellipse cx="${n(x + 16)}" cy="${n(y - 8)}" rx="16" ry="12"/></g>`;
995
+ }
996
+ s += `<g ${F}><circle cx="${n(220 + rnd() * 40)}" cy="${n(54 + rnd() * 16)}" r="22" fill="#f4b860" opacity=".8"/></g>`;
997
+ // colinas en capas (cada una más cálida y opaca)
998
+ const hill = (base, amp, col, op) => {
999
+ let d = `M0 ${W}V${base}`;
1000
+ for (let x = 0; x <= W; x += 30) d += `Q${x + 15} ${n(base - amp * (0.4 + rnd() * 0.6))} ${x + 30} ${n(base - rnd() * amp * 0.3)}`;
1001
+ return `<g ${F}><path d="${d}V${W}Z" fill="${col}" opacity="${op}"/></g>`;
1002
+ };
1003
+ s += hill(170, 40, '#7d8fc8', 0.7) + hill(205, 34, '#5fae7a', 0.72) + hill(GROUND, 18, '#a4c75a', 0.8);
1004
+ s += `<g ${F}><rect x="10" y="${GROUND + 4}" width="280" height="${W - GROUND - 14}" fill="#c49a66" opacity=".65"/></g>`;
1005
+ // salpicaduras
1006
+ const spl = ['#d35d6e', '#6fa3d6', '#e7a33e', '#7fb38a'];
1007
+ let sp = '';
1008
+ for (let i = 0; i < 12; i++) sp += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * W)}" r="${n(0.8 + rnd() * 2.6)}" fill="${spl[i % 4]}" opacity=".55"/>`;
1009
+ s += sp;
1010
+ // papel: grano y margen sin pintar
1011
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)" opacity=".7"/>`;
1012
+ s += `<g ${F}><path d="M-20 -20H320V320H-20ZM12 12V288H288V12Z" fill="#fbf7ee" fill-rule="evenodd"/></g>`;
1013
+ return s;
1014
+ },
1015
+
1016
+ pixel(p, rnd) {
1017
+ // sprites de texto: cada carácter es un color, '.' es transparente (se agrupan tiras horizontales)
1018
+ const sprite = (rows, x0, y0, k, cols) => {
1019
+ let o = '';
1020
+ rows.forEach((row, y) => {
1021
+ for (let x = 0; x < row.length;) {
1022
+ const ch = row[x];
1023
+ let e = x + 1;
1024
+ while (e < row.length && row[e] === ch) e++;
1025
+ if (cols[ch]) o += `<rect x="${n(x0 + x * k)}" y="${n(y0 + y * k)}" width="${(e - x) * k}" height="${k}" fill="${cols[ch]}"/>`;
1026
+ x = e;
1027
+ }
1028
+ });
1029
+ return o;
1030
+ };
1031
+ let s = `<rect width="${W}" height="${W}" fill="#5c94fc"/>`;
1032
+ const cloud = ['.....wwww.......', '...wwwwwwww.....', '..wwwwwwwwwwww..', '.wwwwwwwwwwwwww.', 'wwwwwwwwwwwwwwww', 'wwwbwwwwwwwbwwww', '.wwbbbwwwwbbbww.', '..bbbbbbbbbbbb..'];
1033
+ for (let i = 0; i < 2; i++) s += sprite(cloud, Math.round((20 + i * 150 + rnd() * 40) / 4) * 4, 28 + Math.round(rnd() * 6) * 4, 4, { w: '#fcfcfc', b: '#3cbcfc' });
1034
+ // colina escalonada con su contorno
1035
+ const hx = 40 + Math.round(rnd() * 8) * 5;
1036
+ let hill = '';
1037
+ for (let x = -60; x <= 60; x += 6) {
1038
+ const h = Math.round(Math.sqrt(Math.max(0, 1 - (x / 64) ** 2)) * 70 / 6) * 6;
1039
+ hill += `<rect x="${hx + x}" y="${GROUND - h}" width="6" height="${h}" fill="#00a800"/><rect x="${hx + x}" y="${GROUND - h}" width="6" height="3" fill="#005800"/>`;
1040
+ }
1041
+ s += hill + `<rect x="${hx - 14}" y="${GROUND - 40}" width="4" height="10" fill="#005800"/><rect x="${hx + 10}" y="${GROUND - 50}" width="4" height="10" fill="#005800"/>`;
1042
+ // ladrillos y bloque «?»
1043
+ const qx = 170 + Math.round(rnd() * 4) * 20;
1044
+ const brick = (x, y) => `<rect x="${x}" y="${y}" width="20" height="20" fill="#c84c0c"/><path d="M${x} ${y + 1}H${x + 20}M${x} ${y + 10}H${x + 20}M${x + 10} ${y + 1}V${y + 10}M${x + 4} ${y + 10}V${y + 20}M${x + 16} ${y + 10}V${y + 20}" stroke="#000" stroke-width="2"/>`;
1045
+ s += brick(qx - 20, 150) + brick(qx + 20, 150);
1046
+ const q = ['..wwww..', '.ww..ww.', '.....ww.', '....ww..', '...ww...', '...ww...', '........', '...ww...'];
1047
+ s += `<rect x="${qx}" y="150" width="20" height="20" fill="#f8b800"/><rect x="${qx}" y="150" width="20" height="20" fill="none" stroke="#000" stroke-width="2"/>${sprite(q, qx + 2, 152, 2, { w: '#a81000' })}`;
1048
+ s += `<rect x="${qx + 1}" y="151" width="2" height="2" fill="#000"/><rect x="${qx + 17}" y="151" width="2" height="2" fill="#000"/><rect x="${qx + 1}" y="167" width="2" height="2" fill="#000"/><rect x="${qx + 17}" y="167" width="2" height="2" fill="#000"/>`;
1049
+ // moneda
1050
+ const coin = ['.yy.', 'yowy', 'yowy', 'yowy', 'yowy', '.yy.'];
1051
+ s += sprite(coin, qx + 6, 126, 2, { y: '#f8b800', o: '#ac7c00', w: '#fcfcfc' });
1052
+ // tubería verde
1053
+ const px = qx > 200 ? 18 : 246;
1054
+ s += `<rect x="${px + 4}" y="${GROUND - 34}" width="28" height="34" fill="#00a800"/><rect x="${px + 8}" y="${GROUND - 34}" width="5" height="34" fill="#58d854"/><rect x="${px + 26}" y="${GROUND - 34}" width="4" height="34" fill="#005800"/>`;
1055
+ s += `<rect x="${px}" y="${GROUND - 48}" width="36" height="14" fill="#00a800" stroke="#000" stroke-width="2"/><rect x="${px + 4}" y="${GROUND - 46}" width="5" height="10" fill="#58d854"/>`;
1056
+ // suelo de bloques
1057
+ let g = '';
1058
+ for (let y = GROUND; y < W; y += 20) {
1059
+ for (let x = 0; x < W; x += 20) {
1060
+ g += `<rect x="${x}" y="${y}" width="20" height="20" fill="#c84c0c"/><path d="M${x} ${y + 19}V${y}H${x + 19}" fill="none" stroke="#fcbcb0" stroke-width="2"/><path d="M${x + 1} ${y + 19}H${x + 19}V${y + 1}" fill="none" stroke="#000" stroke-width="2"/>`;
1061
+ }
1062
+ }
1063
+ s += g;
1064
+ // marcador
1065
+ const font = 'font-family="\'Press Start 2P\',\'Courier New\',ui-monospace,monospace" font-weight="700" font-size="11" fill="#fcfcfc"';
1066
+ s += `<text x="14" y="18" ${font}>OT×07</text><text x="150" y="18" text-anchor="middle" ${font}>${String(Math.floor(rnd() * 90 + 10) * 100).padStart(6, '0')}</text><text x="286" y="18" text-anchor="end" ${font}>WORLD 1-7</text>`;
1067
+ return s;
1068
+ },
1069
+
1070
+ synthwave(p, rnd) {
1071
+ const HZ = 182;
1072
+ let s = `<defs><linearGradient id="${p}sky" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="${HZ}"><stop offset="0" stop-color="#0d0221"/><stop offset=".55" stop-color="#3a0f6e"/><stop offset="1" stop-color="#c2187a"/></linearGradient>`
1073
+ + `<linearGradient id="${p}sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffe12b"/><stop offset=".5" stop-color="#ff8a2b"/><stop offset="1" stop-color="#ff2b8a"/></linearGradient>`
1074
+ + `<linearGradient id="${p}chr" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e8f6ff"/><stop offset=".48" stop-color="#7fb8ff"/><stop offset=".52" stop-color="#2a1050"/><stop offset="1" stop-color="#ff7ae0"/></linearGradient>`
1075
+ + `<filter id="${p}gl" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
1076
+ s += `<rect width="${W}" height="${HZ}" fill="url(#${p}sky)"/>`;
1077
+ let st = '';
1078
+ for (let i = 0; i < 34; i++) st += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * HZ * 0.6)}" r="${n(0.4 + rnd() * 1.1)}"/>`;
1079
+ s += `<g fill="#fff" opacity=".85">${st}</g>`;
1080
+ // sol retro con franjas recortadas (rellenas con el mismo cielo)
1081
+ const sx = 150;
1082
+ s += `<circle cx="${sx}" cy="${HZ - 30}" r="66" fill="url(#${p}sun)"/>`;
1083
+ let bars = '';
1084
+ for (let i = 0; i < 6; i++) bars += `<rect x="${sx - 70}" y="${n(HZ - 52 + i * 9.5)}" width="140" height="${n(1.5 + i * 1.1)}"/>`;
1085
+ s += `<g fill="url(#${p}sky)">${bars}</g>`;
1086
+ // montañas de alambre a los lados
1087
+ const mtn = (x0, x1, top) => {
1088
+ let d = `M${x0} ${HZ}`;
1089
+ const k = 6;
1090
+ for (let i = 1; i < k; i++) d += `L${n(x0 + ((x1 - x0) * i) / k)} ${n(HZ - (i % 2 ? top * (0.6 + rnd() * 0.4) : top * (0.2 + rnd() * 0.3)))}`;
1091
+ return `${d}L${x1} ${HZ}Z`;
1092
+ };
1093
+ const mt = mtn(-10, 110, 60) + mtn(190, 310, 54);
1094
+ s += `<path d="${mt}" fill="#1a0838" stroke="#2bf3ff" stroke-width="1.3" stroke-linejoin="round" filter="url(#${p}gl)"/>`;
1095
+ // suelo: rejilla magenta en perspectiva
1096
+ s += `<rect y="${HZ}" width="${W}" height="${W - HZ}" fill="#12002a"/>`;
1097
+ let grid = '';
1098
+ for (let i = -10; i <= 10; i++) grid += `M${150 + i * 14} ${HZ}L${150 + i * 90} ${W}`;
1099
+ for (let j = 1; j < 9; j++) grid += `M0 ${n(HZ + (W - HZ) * (j / 9) ** 1.8)}H${W}`;
1100
+ s += `<path d="${grid}" stroke="#ff2bd6" stroke-width="1.2" fill="none" filter="url(#${p}gl)"/><path d="M0 ${HZ}H${W}" stroke="#ffb3f2" stroke-width="2" filter="url(#${p}gl)"/>`;
1101
+ // palmeras en silueta
1102
+ const palm = (x, h, flip) => {
1103
+ const top = HZ + 6 - h;
1104
+ const lean = flip ? -14 : 14;
1105
+ let o = `<path d="M${x} ${HZ + 6}Q${x + lean * 0.3} ${n(top + h * 0.5)} ${x + lean} ${top}" fill="none" stroke="#0a0014" stroke-width="5" stroke-linecap="round"/>`;
1106
+ for (let i = 0; i < 6; i++) {
1107
+ const a = -Math.PI + (i / 5) * Math.PI + (rnd() - 0.5) * 0.3;
1108
+ const ex = x + lean + Math.cos(a) * 34;
1109
+ const ey = top + Math.sin(a) * 18 + 16;
1110
+ o += `<path d="M${x + lean} ${top}Q${n((x + lean + ex) / 2)} ${n(top - 12)} ${n(ex)} ${n(ey)}" fill="none" stroke="#0a0014" stroke-width="4" stroke-linecap="round"/>`;
1111
+ }
1112
+ return o;
1113
+ };
1114
+ s += palm(26, 120, false) + palm(278, 104, true);
1115
+ // rótulo cromado
1116
+ s += `<text x="150" y="40" text-anchor="middle" font-family="'Brush Script MT','Segoe Script',cursive" font-style="italic" font-weight="700" font-size="30" fill="#ff2bd6" filter="url(#${p}gl)" transform="rotate(-6 150 36)">Seven</text>`;
1117
+ s += `<text x="150" y="68" text-anchor="middle" font-family="Impact,'Arial Black',sans-serif" font-style="italic" font-size="26" letter-spacing="4" fill="url(#${p}chr)" stroke="#fff" stroke-width=".6">OTS·1987</text>`;
1118
+ return s;
1119
+ },
1120
+
1121
+ nouveau(p, rnd) {
1122
+ let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".65" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .4 0 0 0 0 .3 0 0 0 0 .18 0 0 0 1 -.6"/></filter></defs>`;
1123
+ s += `<rect width="${W}" height="${W}" fill="#e9dbb8"/>`;
1124
+ // panel arqueado salvia
1125
+ const arch = `M30 ${GROUND}V120A120 120 0 0 1 270 120V${GROUND}Z`;
1126
+ s += `<path d="${arch}" fill="#a8b48c"/>`;
1127
+ // halo de Mucha: anillos dorados, cuentas y radios
1128
+ const hx = 150;
1129
+ const hy = 104;
1130
+ s += `<circle cx="${hx}" cy="${hy}" r="98" fill="#d9b98a"/><circle cx="${hx}" cy="${hy}" r="88" fill="none" stroke="#c9a24a" stroke-width="4"/><circle cx="${hx}" cy="${hy}" r="74" fill="#efe2c4"/>`;
1131
+ let sp = '';
1132
+ for (let i = 0; i < 36; i++) {
1133
+ const a = (i / 36) * Math.PI * 2;
1134
+ sp += `M${n(hx + Math.cos(a) * 20)} ${n(hy + Math.sin(a) * 20)}L${n(hx + Math.cos(a) * 72)} ${n(hy + Math.sin(a) * 72)}`;
1135
+ }
1136
+ s += `<path d="${sp}" stroke="#c9a24a" stroke-width="1" opacity=".45"/>`;
1137
+ let beads = '';
1138
+ for (let i = 0; i < 28; i++) {
1139
+ const a = (i / 28) * Math.PI * 2;
1140
+ beads += `<circle cx="${n(hx + Math.cos(a) * 81)}" cy="${n(hy + Math.sin(a) * 81)}" r="${i % 2 ? 2.2 : 3.6}" fill="${i % 2 ? '#6e4a5e' : '#c9a24a'}"/>`;
1141
+ }
1142
+ s += beads + `<circle cx="${hx}" cy="${hy}" r="74" fill="none" stroke="#4a3324" stroke-width="1.5"/>`;
1143
+ // tallos en latigazo a ambos lados, con hojas y flores
1144
+ const vine = (sx) => {
1145
+ const m = sx < 150 ? 1 : -1;
1146
+ const x = sx;
1147
+ let o = `<path d="M${x} ${GROUND}C${x + 30 * m} 200 ${x - 22 * m} 150 ${x + 12 * m} 110S${x + 40 * m} 50 ${x + 6 * m} 28" fill="none" stroke="#4f7a74" stroke-width="3.2" stroke-linecap="round"/>`;
1148
+ for (const [lx, ly, a] of [[x + 14 * m, 200, 40], [x - 2 * m, 160, -30], [x + 20 * m, 120, 50], [x + 16 * m, 70, -40]]) {
1149
+ o += `<ellipse cx="${n(lx)}" cy="${ly}" rx="11" ry="4.5" fill="#8a9a6b" stroke="#4a3324" stroke-width="1" transform="rotate(${a * m} ${n(lx)} ${ly})"/>`;
1150
+ }
1151
+ for (const [fx, fy] of [[x + 6 * m, 28], [x + 26 * m, 96]]) {
1152
+ let pet = '';
1153
+ for (let i = 0; i < 5; i++) {
1154
+ const a = (i / 5) * Math.PI * 2 + rnd() * 0.3;
1155
+ pet += `<circle cx="${n(fx + Math.cos(a) * 6)}" cy="${n(fy + Math.sin(a) * 6)}" r="5" fill="#c98a7a" stroke="#4a3324" stroke-width=".8"/>`;
1156
+ }
1157
+ o += `${pet}<circle cx="${n(fx)}" cy="${fy}" r="3.2" fill="#c9a24a"/>`;
1158
+ }
1159
+ return o;
1160
+ };
1161
+ s += vine(20) + vine(280);
1162
+ // marco doble de arco y friso de mosaico
1163
+ s += `<path d="${arch}" fill="none" stroke="#4a3324" stroke-width="3"/><path d="M36 ${GROUND}V121A114 114 0 0 1 264 121V${GROUND}" fill="none" stroke="#c9a24a" stroke-width="1.5"/>`;
1164
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#8a9a6b"/><rect y="${GROUND}" width="${W}" height="4" fill="#4a3324"/>`;
1165
+ let tiles = '';
1166
+ for (let x = 6; x < W; x += 24) tiles += `<rect x="${x}" y="${GROUND + 14}" width="16" height="16" rx="3" fill="none" stroke="#c9a24a" stroke-width="1.6"/><circle cx="${x + 8}" cy="${GROUND + 22}" r="3" fill="#c98a7a"/>`;
1167
+ s += tiles + `<rect y="${GROUND + 40}" width="${W}" height="2" fill="#c9a24a"/>`;
1168
+ s += `<rect x="5" y="5" width="${W - 10}" height="${W - 10}" rx="10" fill="none" stroke="#4a3324" stroke-width="3"/><rect x="10" y="10" width="${W - 20}" height="${W - 20}" rx="7" fill="none" stroke="#c9a24a" stroke-width="1.2"/>`;
1169
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
1170
+ return s;
1171
+ },
1172
+
1173
+ cubism(p, rnd) {
1174
+ let s = `<defs><linearGradient id="${p}a" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".35"/></linearGradient><linearGradient id="${p}b" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#000" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity=".2"/></linearGradient><linearGradient id="${p}c" x1="0" y1="1" x2="0" y2="0"><stop offset="0" stop-color="#000" stop-opacity=".3"/><stop offset=".6" stop-color="#000" stop-opacity="0"/></linearGradient>`
1175
+ + `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".8" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .3 0 0 0 0 .24 0 0 0 0 .18 0 0 0 1 -.6"/></filter></defs>`;
1176
+ // planos facetados de Voronoi con su propia luz
1177
+ const sites = jitterGrid(rnd, 0, 0, W, W, 5, 5, 1.2);
1178
+ const cells = voronoi(sites, 0, 0, W, W);
1179
+ const pal = ['#c8a046', '#7a5232', '#7c7a46', '#4e6a86', '#b5583a', '#e6d6b0', '#8aa0a8', '#c8a046', '#e6d6b0'];
1180
+ let lines = '';
1181
+ cells.forEach((poly) => {
1182
+ const d = polyD(poly);
1183
+ s += `<path d="${d}" fill="${pal[Math.floor(rnd() * pal.length)]}"/><path d="${d}" fill="url(#${p}${'abc'[Math.floor(rnd() * 3)]})"/>`;
1184
+ lines += d;
1185
+ });
1186
+ s += `<path d="${lines}" fill="none" stroke="#2e2a26" stroke-width="2" stroke-linejoin="miter"/>`;
1187
+ // líneas de fuga que cruzan todo
1188
+ let cut = '';
1189
+ for (let i = 0; i < 4; i++) cut += `M${n(rnd() * W)} 0L${n(rnd() * W)} ${W}`;
1190
+ s += `<path d="${cut}" stroke="#2e2a26" stroke-width="1.4" opacity=".7"/>`;
1191
+ // guitarra fragmentada (mitad desplazada) y recorte de periódico
1192
+ const gx = 52;
1193
+ const gy = 120;
1194
+ const gtr = `M${gx} ${gy - 46}C${gx + 26} ${gy - 46} ${gx + 26} ${gy - 12} ${gx + 14} ${gy}C${gx + 34} ${gy + 14} ${gx + 30} ${gy + 56} ${gx} ${gy + 56}C${gx - 30} ${gy + 56} ${gx - 34} ${gy + 14} ${gx - 14} ${gy}C${gx - 26} ${gy - 12} ${gx - 26} ${gy - 46} ${gx} ${gy - 46}Z`;
1195
+ s += `<defs><clipPath id="${p}gl"><rect x="0" y="0" width="${gx}" height="${W}"/></clipPath><clipPath id="${p}gr"><rect x="${gx}" y="0" width="${W}" height="${W}"/></clipPath></defs>`;
1196
+ s += `<g clip-path="url(#${p}gl)"><path d="${gtr}" fill="#b07a3a" stroke="#2e2a26" stroke-width="2.4"/></g>`;
1197
+ s += `<g clip-path="url(#${p}gr)" transform="translate(6 -10)"><path d="${gtr}" fill="#7a5232" stroke="#2e2a26" stroke-width="2.4"/><path d="M${gx + 4} ${gy - 80}V${gy + 40}M${gx + 8} ${gy - 80}V${gy + 40}" stroke="#e6d6b0" stroke-width="1"/></g>`;
1198
+ s += `<circle cx="${gx}" cy="${gy + 16}" r="10" fill="#2e2a26"/><rect x="${gx - 5}" y="${gy - 110}" width="10" height="66" fill="#3a2e22"/>`;
1199
+ const rot = -12 + rnd() * 24;
1200
+ s += `<g transform="rotate(${n(rot)} 238 52)"><rect x="196" y="30" width="86" height="44" fill="#e9e1cc" stroke="#2e2a26" stroke-width="1.2"/><text x="239" y="54" text-anchor="middle" font-family="'Bodoni 72',Didot,'Times New Roman',serif" font-weight="700" font-size="20" fill="#2e2a26">JOUR</text><path d="M202 62H276M202 67H264" stroke="#2e2a26" stroke-width="1.6" opacity=".6"/></g>`;
1201
+ // mesa en escorzo
1202
+ s += `<path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}L310 ${W}L-10 ${W}Z" fill="#7a5232"/><path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}L310 ${W}L-10 ${W}Z" fill="url(#${p}c)"/>`;
1203
+ s += `<path d="M-10 ${GROUND + 14}L310 ${GROUND + 30}M-10 ${GROUND + 34}L310 ${GROUND + 54}M140 ${GROUND}L120 ${W}" stroke="#2e2a26" stroke-width="1.5" fill="none"/><path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}" stroke="#2e2a26" stroke-width="3"/>`;
1204
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
1205
+ return s;
1206
+ },
1207
+
1208
+ crayon(p, rnd) {
1209
+ const sd = Math.floor(rnd() * 900);
1210
+ let s = `<defs><filter id="${p}w" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".05" numOctaves="2" seed="${sd}" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="4" xChannelSelector="R" yChannelSelector="G" result="d"/><feTurbulence type="fractalNoise" baseFrequency=".9 .25" numOctaves="2" seed="${sd + 1}" result="wx"/><feColorMatrix in="wx" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 2.3" result="wa"/><feComposite in="d" in2="wa" operator="in"/></filter></defs>`;
1211
+ s += `<rect width="${W}" height="${W}" fill="#fffdf6"/>`;
1212
+ const zig = (x0, x1, y0, y1, step) => {
1213
+ let d = '';
1214
+ let up = true;
1215
+ for (let x = x0; x <= x1; x += step) {
1216
+ d += `${d ? 'L' : 'M'}${n(x + rnd() * 3)} ${n(up ? y0 + rnd() * 6 : y1 - rnd() * 6)}`;
1217
+ up = !up;
1218
+ }
1219
+ return d;
1220
+ };
1221
+ let g = '';
1222
+ // cielo: franja de zigzag azul arriba
1223
+ g += `<path d="${zig(-4, 304, -4, 44, 7)}" stroke="#5fa8e8" stroke-width="5"/>`;
1224
+ // sol con cara en la esquina
1225
+ g += '<circle cx="40" cy="44" r="22" fill="#f6d32b"/>';
1226
+ let ray = '';
1227
+ for (let i = 0; i < 10; i++) {
1228
+ const a = (i / 10) * Math.PI * 2 + 0.2;
1229
+ ray += `M${n(40 + Math.cos(a) * 28)} ${n(44 + Math.sin(a) * 28)}L${n(40 + Math.cos(a) * (40 + rnd() * 6))} ${n(44 + Math.sin(a) * (40 + rnd() * 6))}`;
1230
+ }
1231
+ g += `<path d="${ray}" stroke="#f58a1f" stroke-width="4"/><circle cx="33" cy="40" r="2.6" fill="#222"/><circle cx="47" cy="40" r="2.6" fill="#222"/><path d="M31 50Q40 58 49 50" stroke="#222" stroke-width="2.6"/>`;
1232
+ // nubes de borde ondulado, pájaros en M
1233
+ for (let k = 0; k < 2; k++) {
1234
+ const x = 120 + k * 90 + rnd() * 20;
1235
+ const y = 70 + rnd() * 20;
1236
+ g += `<path d="M${n(x)} ${n(y)}q6 -14 16 -6q8 -12 18 -2q12 -4 12 8q-2 8 -12 6h-30q-10 -2 -4 -6Z" fill="#fff" stroke="#2f6fd6" stroke-width="3"/>`;
1237
+ }
1238
+ for (let i = 0; i < 3; i++) {
1239
+ const x = 90 + rnd() * 140;
1240
+ const y = 108 + rnd() * 30;
1241
+ g += `<path d="M${n(x)} ${n(y)}q5 -7 9 0q4 -7 9 0" stroke="#222" stroke-width="2.4"/>`;
1242
+ }
1243
+ // casita (pared roja rellena a zigzag, techo marrón, puerta y ventana)
1244
+ const hx = 222 + rnd() * 18;
1245
+ g += `<rect x="${n(hx)}" y="172" width="56" height="${GROUND - 172}" fill="#ffd7cf"/><path d="${zig(hx, hx + 56, 174, GROUND - 2, 5)}" stroke="#e8352b" stroke-width="3" opacity=".75"/><rect x="${n(hx)}" y="172" width="56" height="${GROUND - 172}" stroke="#c22a20" stroke-width="3.5"/>`;
1246
+ g += `<path d="M${n(hx - 8)} 174L${n(hx + 28)} 138L${n(hx + 64)} 174Z" fill="#8b5a2b" stroke="#5a3a1a" stroke-width="3"/><rect x="${n(hx + 8)}" y="208" width="14" height="${GROUND - 208}" fill="#2f6fd6" stroke="#222" stroke-width="2.4"/><rect x="${n(hx + 32)}" y="186" width="16" height="16" fill="#f6d32b" stroke="#222" stroke-width="2.4"/><path d="M${n(hx + 40)} 186V202M${n(hx + 32)} 194H${n(hx + 48)}" stroke="#222" stroke-width="2"/>`;
1247
+ // pasto en zigzag verde y flores
1248
+ g += `<path d="${zig(-4, 304, GROUND - 10, GROUND + 4, 5)}" stroke="#4caf3c" stroke-width="3"/>`;
1249
+ g += `<path d="${zig(-4, 304, GROUND, W, 9)}" stroke="#7cc95a" stroke-width="5" opacity=".7"/>`;
1250
+ const fc = ['#e8352b', '#8a4fc4', '#f27bb5', '#f58a1f'];
1251
+ for (let i = 0; i < 4; i++) {
1252
+ const x = 16 + i * 50 + rnd() * 20;
1253
+ const y = GROUND + 14 + rnd() * 30;
1254
+ g += `<path d="M${n(x)} ${n(y)}V${n(y + 22)}" stroke="#2e8a2e" stroke-width="3"/><circle cx="${n(x)}" cy="${n(y)}" r="7" fill="${fc[i]}"/><circle cx="${n(x)}" cy="${n(y)}" r="2.6" fill="#f6d32b"/>`;
1255
+ }
1256
+ s += `<g fill="none" stroke-linecap="round" stroke-linejoin="round" filter="url(#${p}w)">${g}</g>`;
1257
+ return s;
1258
+ },
1259
+
1260
+ blueprint(p, rnd) {
1261
+ const line = '#eaf2ff';
1262
+ const mono = 'font-family="\'Courier New\',ui-monospace,monospace"';
1263
+ let s = `<defs><pattern id="${p}m" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M10 0H0V10" fill="none" stroke="${line}" stroke-width=".4" opacity=".22"/></pattern>`
1264
+ + `<pattern id="${p}M" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M50 0H0V50" fill="none" stroke="${line}" stroke-width=".8" opacity=".3"/></pattern>`
1265
+ + `<pattern id="${p}h" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><path d="M0 0V6" stroke="${line}" stroke-width="1"/></pattern>`
1266
+ + `<marker id="${p}ar" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 1L10 5L0 9Z" fill="${line}"/></marker>`
1267
+ + `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".012" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .5 0 0 0 -.18"/></filter></defs>`;
1268
+ s += `<rect width="${W}" height="${W}" fill="#1d4e89"/><rect width="${W}" height="${W}" filter="url(#${p}g)"/><rect width="${W}" height="${W}" fill="url(#${p}m)"/><rect width="${W}" height="${W}" fill="url(#${p}M)"/>`;
1269
+ s += `<rect x="8" y="8" width="${W - 16}" height="${W - 16}" fill="none" stroke="${line}" stroke-width="1.6"/><rect x="12" y="12" width="${W - 24}" height="${W - 24}" fill="none" stroke="${line}" stroke-width=".6"/>`;
1270
+ // cotas: ancho arriba y alto a la izquierda
1271
+ const w = 150 + Math.round(rnd() * 6) * 10;
1272
+ s += `<g stroke="${line}" stroke-width="1" fill="none"><path d="M${150 - 90} 46V60M${150 + 90} 46V60M30 60H44M30 ${GROUND}H44"/><path d="M${150 - 88} 52H${150 + 88}" marker-start="url(#${p}ar)" marker-end="url(#${p}ar)"/><path d="M36 62V${GROUND - 2}" marker-start="url(#${p}ar)" marker-end="url(#${p}ar)"/></g>`;
1273
+ s += `<text x="150" y="47" text-anchor="middle" ${mono} font-size="10" fill="${line}">${w} mm</text><text x="31" y="${(60 + GROUND) / 2}" text-anchor="middle" transform="rotate(-90 31 ${(60 + GROUND) / 2})" ${mono} font-size="10" fill="${line}">${w + 20 + Math.round(rnd() * 4) * 10} mm</text>`;
1274
+ // trazos de construcción: arcos de compás y una rosa de los vientos
1275
+ s += `<g stroke="${line}" fill="none" stroke-width=".8" opacity=".6" stroke-dasharray="5 3"><circle cx="150" cy="150" r="118"/><path d="M14 150H286M150 20V${GROUND}"/></g>`;
1276
+ s += `<g stroke="${line}" fill="none" stroke-width="1"><circle cx="258" cy="44" r="16"/><circle cx="258" cy="44" r="3"/><path d="M258 22V66M236 44H280"/></g><path d="M258 26L262 44H254Z" fill="${line}"/><text x="258" y="22" text-anchor="middle" ${mono} font-size="8" fill="${line}" dy="-2">N</text>`;
1277
+ // engranaje de detalle
1278
+ let gear = '';
1279
+ for (let i = 0; i < 10; i++) {
1280
+ const a = (i / 10) * Math.PI * 2;
1281
+ gear += `M${n(262 + Math.cos(a) * 14)} ${n(150 + Math.sin(a) * 14)}L${n(262 + Math.cos(a) * 19)} ${n(150 + Math.sin(a) * 19)}`;
1282
+ }
1283
+ s += `<g stroke="${line}" fill="none" stroke-width="1.2"><circle cx="262" cy="150" r="14"/><circle cx="262" cy="150" r="5"/><path d="${gear}" stroke-width="3"/><path d="M248 136L226 116H210" stroke-width=".8"/></g><text x="208" y="112" ${mono} font-size="8" fill="${line}">DET. A</text>`;
1284
+ // suelo en sección (rayado) y cajetín
1285
+ s += `<rect y="${GROUND}" width="${W}" height="12" fill="url(#${p}h)" opacity=".7"/><path d="M12 ${GROUND}H${W - 12}" stroke="${line}" stroke-width="1.8"/>`;
1286
+ s += `<g stroke="${line}" fill="none" stroke-width="1"><rect x="176" y="256" width="112" height="32"/><path d="M176 272H288M232 256V288"/></g>`;
1287
+ s += `<g ${mono} font-size="8" fill="${line}"><text x="180" y="267">7OTS</text><text x="236" y="267">OT-${String(Math.floor(rnd() * 900) + 100)}</text><text x="180" y="283">ESC 1:1</text><text x="236" y="283">HOJA 7/7</text></g>`;
1288
+ return s;
1289
+ },
1290
+
1291
+ pointillism(p, rnd) {
1292
+ // cada color es una trama de toques: base clara + puntos del color, más claros, más oscuros y vecinos
1293
+ let defs = '';
1294
+ let k = 0;
1295
+ const dots = (base) => {
1296
+ const id = `${p}p${k++}`;
1297
+ const [h, s0, l] = toHsl(base);
1298
+ const cols = [base, base, fromHsl(h, s0, Math.min(0.92, l + 0.18)), fromHsl(h, s0, Math.max(0.12, l - 0.16)), fromHsl((h + 30) % 360, Math.min(1, s0 + 0.1), l), fromHsl((h + 330) % 360, s0, l), fromHsl((h + 180) % 360, 0.5, 0.7), '#fffaf0'];
1299
+ let c = '';
1300
+ // rejilla con jitter (sin huecos ni grumos) en una tesela grande: no se nota la repetición
1301
+ jitterGrid(rnd, 0, 0, 20, 20, 7, 7, 0.9).forEach(([x, y], i) => {
1302
+ const r = n(1.05 + rnd() * 0.6);
1303
+ const f = cols[(i * 5 + Math.floor(rnd() * 3)) % cols.length];
1304
+ // copias al otro lado de la tesela para los que asoman por el borde
1305
+ const wx = x < 2 ? 20 : x > 18 ? -20 : 0;
1306
+ const wy = y < 2 ? 20 : y > 18 ? -20 : 0;
1307
+ const at = [[0, 0], ...(wx ? [[wx, 0]] : []), ...(wy ? [[0, wy]] : []), ...(wx && wy ? [[wx, wy]] : [])];
1308
+ for (const [dx, dy] of at) c += `<circle cx="${n(x + dx)}" cy="${n(y + dy)}" r="${r}" fill="${f}"/>`;
1309
+ });
1310
+ defs += `<pattern id="${id}" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="20" height="20" fill="${mix(base, '#fffaf0', 0.5)}"/>${c}</pattern>`;
1311
+ return `url(#${id})`;
1312
+ };
1313
+ let s = '';
1314
+ // cielo, río, orilla lejana y árboles, pasto al sol y a la sombra (La Grande Jatte)
1315
+ s += `<rect width="${W}" height="140" fill="${dots('#9cc4e4')}"/>`;
1316
+ s += `<rect y="120" width="${W}" height="40" fill="${dots('#3f78b8')}"/>`;
1317
+ s += `<path d="M0 128Q60 112 120 124T240 118T300 122V134H0Z" fill="${dots('#5d8a4a')}"/>`;
1318
+ s += `<rect y="156" width="${W}" height="${W - 156}" fill="${dots('#7fb24a')}"/>`;
1319
+ s += `<path d="M0 ${W}V200Q90 ${GROUND - 20} 180 ${GROUND + 10}T${W} ${GROUND}V${W}Z" fill="${dots('#3c6e3a')}"/>`;
1320
+ const tx = rnd() < 0.5 ? 34 : 262;
1321
+ s += `<rect x="${tx - 6}" y="40" width="12" height="${GROUND - 34}" fill="${dots('#5a4030')}"/>`;
1322
+ s += `<ellipse cx="${tx}" cy="54" rx="58" ry="52" fill="${dots('#2f5a34')}"/>`;
1323
+ // velero y sombrilla en silueta
1324
+ const bx = 120 + rnd() * 60;
1325
+ s += `<path d="M${n(bx)} 132L${n(bx)} 92L${n(bx + 22)} 128Z" fill="${dots('#f3ead8')}"/><path d="M${n(bx - 10)} 132H${n(bx + 26)}L${n(bx + 20)} 138H${n(bx - 4)}Z" fill="${dots('#8a3a2a')}"/>`;
1326
+ const ux = tx < 150 ? 238 : 62;
1327
+ s += `<path d="M${ux - 26} 186Q${ux} 160 ${ux + 26} 186Z" fill="${dots('#c84a4a')}"/><path d="M${ux} 184V${GROUND - 6}" stroke="#3a2a20" stroke-width="2"/>`;
1328
+ // marco pintado de puntos (como hacía Seurat)
1329
+ s += `<path d="M0 0H${W}V${W}H0ZM10 10V${W - 10}H${W - 10}V10Z" fill-rule="evenodd" fill="${dots('#2a3f7a')}"/>`;
1330
+ return `<defs>${defs}</defs>${s}`;
1331
+ },
1332
+
1333
+ woodcut(p, rnd) {
1334
+ const ink = '#15120f';
1335
+ const pap = '#ecdfc4';
1336
+ let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".55" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .93 0 0 0 0 .87 0 0 0 0 .77 0 0 0 2.2 -1.25"/></filter>`
1337
+ + `<filter id="${p}r" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".06" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter></defs>`;
1338
+ s += `<rect width="${W}" height="${W}" fill="${pap}"/>`;
1339
+ let g = `<rect x="10" y="10" width="${W - 20}" height="${W - 20}" fill="${ink}"/>`;
1340
+ // cielo negro con tallas onduladas
1341
+ let cuts = '';
1342
+ for (let y = 22; y < 180; y += 9) {
1343
+ let x = 12 + rnd() * 20;
1344
+ while (x < W - 30) {
1345
+ const len = 20 + rnd() * 50;
1346
+ const a = 1.5 + rnd() * 2;
1347
+ cuts += `M${n(x)} ${n(y)}q${n(len / 4)} ${n(-a)} ${n(len / 2)} 0t${n(len / 2)} 0`;
1348
+ x += len + 8 + rnd() * 14;
1349
+ }
1350
+ }
1351
+ g += `<path d="${cuts}" fill="none" stroke="${pap}" stroke-width="1.6" stroke-linecap="round"/>`;
1352
+ // sol rojo con anillos tallados y rayos en cuña
1353
+ const sx = 200 + rnd() * 50;
1354
+ const sy = 64;
1355
+ let rays = '';
1356
+ for (let i = 0; i < 18; i++) {
1357
+ const a = (i / 18) * Math.PI * 2;
1358
+ const a1 = a + 0.06;
1359
+ rays += `M${n(sx + Math.cos(a) * 34)} ${n(sy + Math.sin(a) * 34)}L${n(sx + Math.cos(a - 0.06) * 58)} ${n(sy + Math.sin(a - 0.06) * 58)}L${n(sx + Math.cos(a1) * 58)} ${n(sy + Math.sin(a1) * 58)}Z`;
1360
+ }
1361
+ g += `<circle cx="${n(sx)}" cy="${sy}" r="58" fill="${ink}"/><path d="${rays}" fill="${pap}"/><circle cx="${n(sx)}" cy="${sy}" r="28" fill="#b8352a"/>`;
1362
+ g += `<circle cx="${n(sx)}" cy="${sy}" r="20" fill="none" stroke="${pap}" stroke-width="1.6"/><circle cx="${n(sx)}" cy="${sy}" r="12" fill="none" stroke="${pap}" stroke-width="1.4"/>`;
1363
+ // montañas claras con rayado negro siguiendo la ladera
1364
+ const ridge = (base, amp) => {
1365
+ const pts = [];
1366
+ for (let x = 0; x <= W; x += 30) pts.push([x, base - amp * (0.3 + rnd() * 0.7)]);
1367
+ return pts;
1368
+ };
1369
+ const pts = ridge(196, 70);
1370
+ const dOf = (dy) => `M0 ${n(pts[0][1] + dy)}${pts.map(([x, y]) => `L${x} ${n(y + dy)}`).join('')}`;
1371
+ g += `<path d="${dOf(0)}L${W} ${GROUND}L0 ${GROUND}Z" fill="${pap}"/>`;
1372
+ let hatch = '';
1373
+ for (let dy = 8; dy < 90; dy += 7) hatch += dOf(dy);
1374
+ g += `<path d="${hatch}" fill="none" stroke="${ink}" stroke-width="${n(2.2)}" stroke-linejoin="round"/>`;
1375
+ // pino tallado a un lado
1376
+ const tx = sx > 225 ? 46 : 254;
1377
+ let pine = '';
1378
+ for (let i = 0; i < 5; i++) {
1379
+ const y = 120 + i * 22;
1380
+ const w = 12 + i * 7;
1381
+ pine += `M${tx} ${y - 20}L${tx + w} ${y + 8}L${tx - w} ${y + 8}Z`;
1382
+ }
1383
+ g += `<path d="${pine}" fill="${ink}" stroke="${pap}" stroke-width="1.2"/><rect x="${tx - 4}" y="${GROUND - 14}" width="8" height="14" fill="${ink}"/>`;
1384
+ // suelo negro con golpes de gubia
1385
+ g += `<rect x="10" y="${GROUND}" width="${W - 20}" height="${W - GROUND - 10}" fill="${ink}"/>`;
1386
+ let gouge = '';
1387
+ for (let i = 0; i < 34; i++) {
1388
+ const x = 16 + rnd() * (W - 40);
1389
+ const y = GROUND + 8 + rnd() * 40;
1390
+ gouge += `M${n(x)} ${n(y)}l${n(8 + rnd() * 12)} ${n(-1 - rnd() * 2)}`;
1391
+ }
1392
+ g += `<path d="M10 ${GROUND + 2}H${W - 10}" stroke="${pap}" stroke-width="2"/><path d="${gouge}" stroke="${pap}" stroke-width="2" stroke-linecap="round"/>`;
1393
+ s += `<g filter="url(#${p}r)">${g}</g>`;
1394
+ // tinta desigual: motas de papel que asoman
1395
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
1396
+ return s;
1397
+ },
1398
+ };
1399
+
1400
+ /**
1401
+ * Contenido del fondo de un estilo en el marco 0 0 300 300 ('' sin estilo).
1402
+ * @param {string} style
1403
+ * @param {number} seed styleSeed(spec)
1404
+ * @param {string} [prefix] prefijo de ids (único si el fondo va dentro de otro SVG)
1405
+ */
1406
+ export function styleBackdrop(style, seed, prefix = 'bd') {
1407
+ const f = BACKDROPS[style];
1408
+ return f ? f(`${prefix}-`, seeded((seed ^ 0x9e3779b9) >>> 0)) : '';
1409
+ }
1410
+
1411
+ /** Fondo como documento SVG independiente (para background-image o un <img>). */
1412
+ export function backdropSVG(style, seed) {
1413
+ const inner = styleBackdrop(style, seed);
1414
+ return inner ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${W}" preserveAspectRatio="xMidYMax slice">${inner}</svg>` : '';
1415
+ }
1416
+
1417
+ /** Fondo como url("data:…") para CSS (se rasteriza una vez: no cuesta nada al animar). */
1418
+ export function backdropCSS(style, seed) {
1419
+ const svg = backdropSVG(style, seed);
1420
+ return svg ? `url("data:image/svg+xml,${encodeURIComponent(svg)}")` : '';
1421
+ }
1422
+
1423
+ /** Fondo anidado que cubre un viewBox 'x y w h' (para SVG estáticos). */
1424
+ export function backdropIn(style, seed, viewBox, prefix) {
1425
+ const inner = styleBackdrop(style, seed, prefix);
1426
+ if (!inner) return '';
1427
+ const [x, y, w, h] = String(viewBox).split(/[\s,]+/).map(Number);
1428
+ return `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="0 0 ${W} ${W}" preserveAspectRatio="xMidYMax slice" overflow="hidden">${inner}</svg>`;
1429
+ }