@7ots/cli 0.1.7 → 0.1.8

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 (57) hide show
  1. package/README.md +1 -1
  2. package/cli/7ots.mjs +13 -1
  3. package/cli/lib/account.mjs +152 -2
  4. package/cli/lib/assistant.mjs +106 -41
  5. package/cli/lib/brain.mjs +13 -2
  6. package/cli/lib/config.mjs +3 -0
  7. package/cli/lib/inferences.mjs +30 -6
  8. package/cli/lib/integrations.mjs +69 -5
  9. package/cli/lib/orquesta.mjs +44 -1
  10. package/cli/lib/pet-server.mjs +344 -11
  11. package/cli/pet/electron/director.cjs +37 -5
  12. package/cli/pet/electron/main.cjs +92 -10
  13. package/cli/pet/pet.html +149 -10
  14. package/cli/pet/settings.html +545 -42
  15. package/dist/7ots.esm.js +216 -137
  16. package/dist/7ots.esm.js.map +4 -4
  17. package/dist/7ots.iife.js +216 -137
  18. package/dist/7ots.iife.js.map +4 -4
  19. package/package.json +1 -1
  20. package/server/channels/agent.mjs +7 -1
  21. package/server/channels/index.mjs +3 -2
  22. package/server/platform/connect.mjs +4 -1
  23. package/server/platform/db.mjs +9 -1
  24. package/server/platform/device.mjs +101 -4
  25. package/server/platform/mods.mjs +222 -0
  26. package/server/platform/orquesta.mjs +78 -14
  27. package/server/platform/profile.mjs +161 -0
  28. package/server/platform/routes.mjs +38 -33
  29. package/server/platform/runtime.mjs +3 -1
  30. package/server/platform/store.mjs +1 -1
  31. package/server/platform/vm.mjs +370 -91
  32. package/server/server.mjs +12 -2
  33. package/src/avatar/three.js +3 -2
  34. package/src/character/AvatarEditor.js +95 -7
  35. package/src/character/Character.js +116 -17
  36. package/src/character/TradingCard.js +226 -0
  37. package/src/character/mods/index.js +4 -0
  38. package/src/character/mods/orc.js +64 -0
  39. package/src/character/mods.js +251 -0
  40. package/src/character/more/acc-head.js +213 -0
  41. package/src/character/more/acc-rest.js +342 -0
  42. package/src/character/more/brows.js +39 -0
  43. package/src/character/more/cheeks.js +78 -0
  44. package/src/character/more/eyes.js +288 -0
  45. package/src/character/more/mouths.js +259 -0
  46. package/src/character/more/patterns.js +120 -0
  47. package/src/character/parts.js +90 -0
  48. package/src/character/skin.js +166 -1
  49. package/src/character/styles.js +1285 -9
  50. package/src/character3d/Character3D.js +148 -35
  51. package/src/character3d/index.js +7 -1
  52. package/src/character3d/model.js +367 -0
  53. package/src/i18n/messages/character.js +34 -22
  54. package/src/i18n/messages/platform.js +15 -0
  55. package/src/identity/random.js +17 -3
  56. package/src/identity/schema.js +10 -0
  57. package/src/index.js +1 -0
@@ -45,6 +45,23 @@ export const STYLES = [
45
45
  { id: 'blueprint', label: { es: 'Plano técnico', en: 'Blueprint', pt: 'Planta técnica' } },
46
46
  { id: 'pointillism', label: { es: 'Puntillismo', en: 'Pointillism', pt: 'Pontilhismo' } },
47
47
  { id: 'woodcut', label: { es: 'Xilografía', en: 'Woodcut', pt: 'Xilogravura' } },
48
+ { id: 'riso', label: { es: 'Risografía', en: 'Riso print', pt: 'Risografia' } },
49
+ { id: 'dotmatrix', label: { es: 'Matriz verde', en: 'Dot matrix', pt: 'Matriz verde' } },
50
+ { id: 'chalk', label: { es: 'Tiza', en: 'Chalk', pt: 'Giz' } },
51
+ { id: 'sticker', label: { es: 'Calcomanía', en: 'Sticker', pt: 'Adesivo' } },
52
+ { id: 'felt', label: { es: 'Fieltro', en: 'Felt', pt: 'Feltro' } },
53
+ { id: 'papercut', label: { es: 'Papel recortado', en: 'Papercut', pt: 'Papel recortado' } },
54
+ { id: 'lowpoly', label: { es: 'Low poly', en: 'Low poly', pt: 'Low poly' } },
55
+ { id: 'marble', label: { es: 'Mármol', en: 'Marble', pt: 'Mármore' } },
56
+ { id: 'candy', label: { es: 'Gomita', en: 'Gummy', pt: 'Bala de goma' } },
57
+ { id: 'holo', label: { es: 'Holográfico', en: 'Holographic', pt: 'Holográfico' } },
58
+ { id: 'sumie', label: { es: 'Sumi-e', en: 'Sumi-e', pt: 'Sumi-e' } },
59
+ { id: 'tarot', label: { es: 'Tarot', en: 'Tarot', pt: 'Tarô' } },
60
+ { id: 'retro70', label: { es: 'Años 70', en: 'Seventies', pt: 'Anos 70' } },
61
+ { id: 'mosaic', label: { es: 'Mosaico', en: 'Mosaic', pt: 'Mosaico' } },
62
+ { id: 'neonsign', label: { es: 'Letrero de neón', en: 'Neon sign', pt: 'Letreiro neon' } },
63
+ { id: 'graffiti', label: { es: 'Grafiti', en: 'Graffiti', pt: 'Grafite' } },
64
+ { id: 'thermal', label: { es: 'Cámara térmica', en: 'Thermal', pt: 'Câmera térmica' } },
48
65
  ];
49
66
  export const STYLE_IDS = STYLES.map((s) => s.id);
50
67
 
@@ -113,6 +130,12 @@ const PAL = {
113
130
  nouveau: ['#8a9a6b', '#c98a7a', '#c9a24a', '#4f7a74', '#efe2c4', '#b5654a', '#6e4a5e', '#a8b48c', '#d9b98a'],
114
131
  cubism: ['#c8a046', '#7a5232', '#7c7a46', '#4e6a86', '#b5583a', '#e6d6b0', '#2e2a26', '#8aa0a8'],
115
132
  crayon: ['#e8352b', '#f58a1f', '#f6d32b', '#4caf3c', '#2f6fd6', '#8a4fc4', '#f27bb5', '#8b5a2b', '#5fc8e8', '#222222'],
133
+ riso: ['#ff48b0', '#0078bf', '#ffe800', '#00a95c', '#ff6c2f', '#765ba7'],
134
+ dotmatrix: ['#081820', '#346856', '#88c070', '#e0f8d0'],
135
+ tarot: ['#1c2a5a', '#c9a23a', '#efe3c4', '#a3262e', '#2f7f7a', '#6b3a7a', '#e07a3a'],
136
+ retro70: ['#e8762c', '#f2b33d', '#8a4b2a', '#c94f2c', '#d9c58a', '#6b7a3a', '#4a2c1e', '#3f7f7a'],
137
+ mosaic: ['#b5452e', '#d98a3a', '#e6c26a', '#3f7a8a', '#2c4a6e', '#6a8a4a', '#efe4cc', '#7a3a4a', '#3a3530'],
138
+ graffiti: ['#ff2e63', '#08d9d6', '#f9ed32', '#7cfc00', '#ff8c00', '#9b5de5', '#00bbf9', '#ffffff', '#111111'],
116
139
  };
117
140
 
118
141
  /** Mezcla lineal de dos colores (t=0 → a). */
@@ -127,6 +150,22 @@ const clampHsl = (c, s0, s1, l0, l1) => {
127
150
  return fromHsl(h, Math.min(s1, Math.max(s0, s)), Math.min(l1, Math.max(l0, l)));
128
151
  };
129
152
 
153
+ /** Tubo de neón del color: mismo tono a tope (los grises van a rosa). */
154
+ const neonTube = (c) => {
155
+ const [h, s] = toHsl(c);
156
+ return fromHsl(s < 0.15 ? 322 : h, 1, 0.62);
157
+ };
158
+ /** Trazado (M…Z) de una estrella de cinco puntas de radio `r`. */
159
+ function star5(x, y, r) {
160
+ let d = '';
161
+ for (let i = 0; i < 10; i++) {
162
+ const a = -Math.PI / 2 + (i * Math.PI) / 5;
163
+ const k = i % 2 ? r * 0.42 : r;
164
+ d += `${i ? 'L' : 'M'}${n(x + Math.cos(a) * k)} ${n(y + Math.sin(a) * k)}`;
165
+ }
166
+ return d + 'Z';
167
+ }
168
+
130
169
  /** Primario Bauhaus de un color: por tono (los grises van a negro o crema). */
131
170
  function bauhausOf(hex) {
132
171
  const [h, s, l] = toHsl(hex);
@@ -262,6 +301,139 @@ const LOOKS = {
262
301
  return luminance(c) > 0.5 ? '#ecdfc4' : '#15120f';
263
302
  },
264
303
  },
304
+ riso: {
305
+ finish: 'flat', texture: 'none', ink: '#0078bf', lw: 2.6,
306
+ eyes: { white: '#f7f1e3', ink: '#1d2a6b' },
307
+ map: (c, role, spec) => {
308
+ if (role === 'cheek' || role === 'lip') return '#ff48b0';
309
+ if (role === 'brow') return '#0078bf';
310
+ if (role === 'pattern') return nearest(c, PAL.riso, [nearest(spec.body.color, PAL.riso, ['#0078bf'])]);
311
+ return nearest(c, PAL.riso, role === 'body' ? ['#0078bf'] : []);
312
+ },
313
+ },
314
+ dotmatrix: {
315
+ finish: 'flat', texture: 'none', ink: '#081820', lw: 4,
316
+ eyes: { white: '#e0f8d0', ink: '#081820', iris: '#081820' },
317
+ map: (c, role) => {
318
+ if (role === 'body') return '#88c070';
319
+ if (role === 'pattern' || role === 'cheek' || role === 'lip') return '#346856';
320
+ if (role === 'brow' || role === 'iris') return '#081820';
321
+ return PAL.dotmatrix[Math.min(3, Math.floor(luminance(c) * 4))];
322
+ },
323
+ },
324
+ chalk: {
325
+ finish: 'flat', texture: 'none', ink: '#f4f4ee', lw: 3,
326
+ eyes: { white: '#f4f4ee', ink: '#22352b' },
327
+ map: (c, role) => {
328
+ if (role === 'brow' || role === 'lip') return '#f4f4ee';
329
+ if (role === 'iris') return clampHsl(c, 0.3, 0.55, 0.35, 0.5);
330
+ return clampHsl(c, 0.35, 0.65, 0.68, 0.8);
331
+ },
332
+ },
333
+ sticker: {
334
+ finish: 'flat', texture: 'none', ink: '#26222b', lw: 2.4,
335
+ eyes: { white: '#ffffff', ink: '#1a1720' },
336
+ map: (c, role) => (role === 'brow' ? '#1a1720' : role === 'cheek' ? '#ff6f9c' : clampHsl(c, 0.7, 0.95, 0.46, 0.62)),
337
+ },
338
+ felt: {
339
+ finish: 'flat', texture: 'none', ink: '', lw: 0,
340
+ eyes: { white: '#f6f1e6', ink: '#2a2420' },
341
+ map: (c, role) => (role === 'brow' ? '#3a302a' : role === 'iris' ? clampHsl(c, 0.25, 0.5, 0.25, 0.42) : clampHsl(c, 0.28, 0.55, 0.42, 0.64)),
342
+ },
343
+ papercut: {
344
+ finish: 'flat', texture: 'none', ink: '', lw: 0,
345
+ eyes: { white: '#fffdf8', ink: '#3a3340' },
346
+ map: (c, role) => (role === 'brow' ? '#3a3340' : role === 'iris' ? clampHsl(c, 0.3, 0.55, 0.3, 0.45) : clampHsl(c, 0.35, 0.62, 0.6, 0.78)),
347
+ },
348
+ lowpoly: {
349
+ finish: 'flat', texture: 'none', ink: '', lw: 0,
350
+ eyes: { white: '#f5f7fa', ink: '#1d2330' },
351
+ map: (c, role) => (role === 'brow' ? '#1d2330' : clampHsl(c, 0.35, 0.75, 0.36, 0.6)),
352
+ },
353
+ marble: {
354
+ finish: 'flat', texture: 'none', ink: '#8d877c', lw: 1.8,
355
+ eyes: { white: '#f7f4ee', ink: '#4f4a43' },
356
+ map: (c, role) => {
357
+ if (role === 'body') return mix(grey(c, 0.86, 0.95), c, 0.08);
358
+ if (role === 'pattern') return mix(grey(c, 0.7, 0.85), c, 0.1);
359
+ if (role === 'iris') return grey(c, 0.35, 0.6);
360
+ if (role === 'cheek') return '#e2c4b8';
361
+ if (role === 'lip') return '#b4a69a';
362
+ if (role === 'brow') return '#7a746a';
363
+ return mix(c, '#c9a45a', 0.7); // lo que lleva puesto, dorado a la hoja
364
+ },
365
+ },
366
+ candy: {
367
+ finish: 'flat', texture: 'none', ink: (s) => mix(s.body.color, '#3a0a26', 0.45), lw: 2.4,
368
+ eyes: { white: '#ffffff', ink: '#3a1530' },
369
+ map: (c, role) => (role === 'brow' ? '#3a1530' : role === 'cheek' ? '#ff7fb0' : clampHsl(c, 0.72, 1, 0.56, 0.7)),
370
+ },
371
+ holo: {
372
+ finish: 'flat', texture: 'none', ink: '#6f74a8', lw: 2,
373
+ eyes: { white: '#fbfbff', ink: '#272a48' },
374
+ map: (c, role) => (role === 'brow' ? '#272a48' : role === 'iris' ? clampHsl(c, 0.5, 0.8, 0.4, 0.55) : clampHsl(c, 0.25, 0.5, 0.74, 0.86)),
375
+ },
376
+ sumie: {
377
+ finish: 'flat', texture: 'none', ink: '#1d1b19', lw: 3.6,
378
+ eyes: { white: '#f3eee2', ink: '#1d1b19', iris: '#1d1b19' },
379
+ map: (c, role) => {
380
+ if (role === 'body') return grey(c, 0.62, 0.9);
381
+ if (role === 'pattern') return grey(c, 0.3, 0.58);
382
+ if (role === 'cheek') return '#c96b5c';
383
+ if (role === 'lip' || role === 'brow') return '#1d1b19';
384
+ return grey(c, 0.15, 0.85);
385
+ },
386
+ },
387
+ tarot: {
388
+ finish: 'flat', texture: 'none', ink: '#1a1430', lw: 3,
389
+ eyes: { white: '#efe3c4', ink: '#1a1430' },
390
+ map: (c, role) => {
391
+ if (role === 'cheek') return '#d9877a';
392
+ if (role === 'lip') return '#a3262e';
393
+ if (role === 'brow') return '#1a1430';
394
+ return nearest(c, PAL.tarot, role === 'body' ? ['#1c2a5a', '#efe3c4'] : []);
395
+ },
396
+ },
397
+ retro70: {
398
+ finish: 'flat', texture: 'none', ink: '#4a2c1e', lw: 3,
399
+ eyes: { white: '#fbf1dc', ink: '#3a2216' },
400
+ map: (c, role) => (role === 'cheek' ? '#e8762c' : role === 'brow' ? '#3a2216' : nearest(c, PAL.retro70, role === 'body' ? ['#4a2c1e', '#d9c58a'] : [])),
401
+ },
402
+ mosaic: {
403
+ finish: 'flat', texture: 'none', ink: '#3a3530', lw: 4,
404
+ eyes: { white: '#efe4cc', ink: '#2a2622' },
405
+ map: (c, role) => (role === 'brow' ? '#2a2622' : nearest(c, PAL.mosaic, role === 'body' ? ['#efe4cc', '#3a3530'] : [])),
406
+ },
407
+ neonsign: {
408
+ // cuerpo casi negro; el contorno es el tubo, del color del cuerpo original
409
+ finish: 'flat', texture: 'none', ink: (s) => neonTube(s.body.color), lw: 3.4,
410
+ eyes: { white: '#f8f0ff', ink: '#120f18' },
411
+ map: (c, role, spec) => {
412
+ if (role === 'body') return mix(c, '#0b0b0b', 0.82);
413
+ if (role === 'pattern') return mix(c, '#0b0b0b', 0.62);
414
+ if (role === 'brow' || role === 'lip') return neonTube(spec.body.color);
415
+ if (role === 'cheek') return fromHsl((toHsl(neonTube(spec.body.color))[0] + 40) % 360, 1, 0.66);
416
+ return neonTube(c);
417
+ },
418
+ },
419
+ graffiti: {
420
+ finish: 'flat', texture: 'none', ink: '#111111', lw: 5.5,
421
+ eyes: { white: '#ffffff', ink: '#111111' },
422
+ map: (c, role) => (role === 'brow' ? '#111111' : role === 'cheek' ? '#ff2e63' : nearest(c, PAL.graffiti, role === 'body' ? ['#ffffff', '#111111'] : [])),
423
+ },
424
+ thermal: {
425
+ // el filtro pasa la luminancia a la paleta «ironbow»: claro = caliente, oscuro = frío
426
+ finish: 'flat', texture: 'none', ink: '', lw: 0,
427
+ eyes: { white: '#fffbe0', ink: '#0a0520', iris: '#8a1a8a' },
428
+ map: (c, role) => {
429
+ if (role === 'body') return '#ff7a1a';
430
+ if (role === 'pattern' || role === 'lip') return '#ffb000';
431
+ if (role === 'cheek') return '#ffe46a';
432
+ if (role === 'brow') return '#2a0a6a';
433
+ if (role === 'iris') return '#8a1a8a';
434
+ return mix('#120838', '#4a2aa8', luminance(c)); // lo que lleva puesto está frío
435
+ },
436
+ },
265
437
  };
266
438
 
267
439
  /** Semilla del estilo para un ot: la misma que su piel (no cambia al arrastrar puntos). */
@@ -279,22 +451,24 @@ export function stylizeSpec(spec) {
279
451
  const look = LOOKS[id];
280
452
  if (!look) return spec;
281
453
  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');
454
+ const keep = spec._modColors || {};
455
+ const m = (c, role, path) => (c && !keep[path] ? look.map(c, role, spec) : c);
456
+ s.body.color = m(spec.body.color, 'body', 'body.color');
457
+ s.body.patternColor = m(spec.body.patternColor, 'pattern', 'body.patternColor');
458
+ s.eyes.iris = m(spec.eyes.iris, 'iris', 'eyes.iris');
286
459
  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');
460
+ s.cheeks.color = m(spec.cheeks.color, 'cheek', 'cheeks.color');
461
+ s.mouth.lipColor = m(spec.mouth.lipColor, 'lip', 'mouth.lipColor');
462
+ if (spec.mouth.color) s.mouth.color = m(spec.mouth.color, 'brow', 'mouth.color');
463
+ if (spec.brows.color) s.brows.color = m(spec.brows.color, 'brow', 'brows.color');
291
464
  for (const a of s.accessories) {
465
+ if (a.id.startsWith('mod:')) continue; // las piezas de un mod traen sus colores
292
466
  a.color = m(a.color, 'acc');
293
467
  a.color2 = m(a.color2, 'acc');
294
468
  }
295
469
  s.finish = look.finish;
296
470
  s.texture = look.texture;
297
- s._style = { id, seed: styleSeed(spec), ink: look.ink, lw: look.lw };
471
+ s._style = { id, seed: styleSeed(spec), ink: typeof look.ink === 'function' ? look.ink(s) : look.ink, lw: look.lw };
298
472
  return s;
299
473
  }
300
474
 
@@ -464,6 +638,133 @@ function filterBody(id, seed, q, animate) {
464
638
  + '<feMerge><feMergeNode in="un"/><feMergeNode in="dj"/></feMerge>';
465
639
  case 'woodcut':
466
640
  return full ? wobble(seed, 'SourceGraphic', 2, 0.05) + grain(seed, '#efe4cc', 1.4, 0.7) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
641
+ case 'riso': {
642
+ // dos tambores: la tinta rosa sale corrida bajo el azul; papel sin entintar en motas
643
+ const mis = '<feFlood flood-color="#ff48b0" flood-opacity=".8"/><feComposite in2="SourceAlpha" operator="in"/><feOffset dx="3.5" dy="-2.5" result="mis"/>';
644
+ return full
645
+ ? mis + grain(seed, '#f7f1e3', 1.5, 0.85) + '<feMerge><feMergeNode in="mis"/><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>'
646
+ : mis + '<feMerge><feMergeNode in="mis"/><feMergeNode in="SourceGraphic"/></feMerge>';
647
+ }
648
+ case 'dotmatrix':
649
+ // pantalla de 4 verdes: bloques de 6 y cada bloque al verde más cercano por luminancia
650
+ return '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="6" height="6"/><feTile result="tile"/>'
651
+ + '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feMorphology operator="dilate" radius="2"/>'
652
+ + '<feComponentTransfer><feFuncA type="discrete" tableValues="0 1 1"/></feComponentTransfer>'
653
+ + '<feColorMatrix values=".2126 .7152 .0722 0 0 .2126 .7152 .0722 0 0 .2126 .7152 .0722 0 0 0 0 0 1 0"/>'
654
+ + '<feComponentTransfer><feFuncR type="discrete" tableValues=".03 .2 .53 .88"/><feFuncG type="discrete" tableValues=".09 .41 .75 .97"/><feFuncB type="discrete" tableValues=".13 .34 .44 .82"/></feComponentTransfer>';
655
+ case 'chalk':
656
+ if (!full) return '';
657
+ // tiza: trazo tembloroso, el pizarrón asomando entre los granos y polvo blanco alrededor
658
+ return wobble(seed, 'SourceGraphic', 2.4, 0.05)
659
+ + `<feTurbulence type="fractalNoise" baseFrequency=".75" numOctaves="2" seed="${(seed + 7) % 997}" result="cn"/>`
660
+ + '<feColorMatrix in="cn" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -5 0 0 0 3.6" result="ca"/><feComposite in="wb" in2="ca" operator="in" result="ch"/>'
661
+ + '<feGaussianBlur in="SourceAlpha" stdDeviation="3.5" result="db"/><feFlood flood-color="#f4f4ee" flood-opacity=".2"/><feComposite in2="db" operator="in" result="dust"/>'
662
+ + '<feMerge><feMergeNode in="dust"/><feMergeNode in="ch"/></feMerge>';
663
+ case 'sticker': {
664
+ // troquel: borde blanco redondeado (alfa difuminado y cortado) y, en full, la sombra de la pegatina
665
+ const cut = '<feGaussianBlur in="SourceAlpha" stdDeviation="3.5"/><feComponentTransfer result="cut"><feFuncA type="linear" slope="14" intercept="-.7"/></feComponentTransfer>'
666
+ + '<feFlood flood-color="#ffffff"/><feComposite in2="cut" operator="in" result="wh"/>';
667
+ return full
668
+ ? cut + '<feGaussianBlur in="cut" stdDeviation="2.4"/><feOffset dx="1.5" dy="3.5"/><feColorMatrix values="0 0 0 0 .1 0 0 0 0 .08 0 0 0 0 .14 0 0 0 .38 0" result="sh"/>'
669
+ + '<feMerge><feMergeNode in="sh"/><feMergeNode in="wh"/><feMergeNode in="SourceGraphic"/></feMerge>'
670
+ : cut + '<feMerge><feMergeNode in="wh"/><feMergeNode in="SourceGraphic"/></feMerge>';
671
+ }
672
+ case 'felt':
673
+ if (!full) return '';
674
+ // fieltro: borde de fibras sueltas y pelusa clara y oscura
675
+ return wobble(seed, 'SourceGraphic', 3, 0.45)
676
+ + grain(seed, '#ffffff', 1.5, 1.3, 'gl') + grain(seed + 3, '#000000', 1.3, 0.9, 'gd')
677
+ + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gl"/><feMergeNode in="gd"/></feMerge>';
678
+ case 'papercut': {
679
+ // cartulina recortada que flota sobre el fondo: sombra suave (barata, también en lite)
680
+ const sh = '<feGaussianBlur in="SourceAlpha" stdDeviation="2.6"/><feOffset dx="2" dy="4"/><feColorMatrix values="0 0 0 0 .12 0 0 0 0 .08 0 0 0 0 .18 0 0 0 .4 0" result="sh"/>';
681
+ return full
682
+ ? sh + grain(seed, '#ffffff', 1.1, 1.4) + '<feMerge><feMergeNode in="sh"/><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>'
683
+ : sh + '<feMerge><feMergeNode in="sh"/><feMergeNode in="SourceGraphic"/></feMerge>';
684
+ }
685
+ case 'marble': {
686
+ if (!full) return '';
687
+ // vetas finas (pliegues de turbulencia estirada) y luz difusa de escultura
688
+ const light = '<feDistantLight azimuth="225" elevation="58"/>';
689
+ return `<feTurbulence type="turbulence" baseFrequency=".012 .03" numOctaves="4" seed="${seed % 997}" result="vn"/>`
690
+ + '<feColorMatrix in="vn" values="0 0 0 0 .38 0 0 0 0 .36 0 0 0 0 .34 -5 0 0 0 .62" result="va"/><feComposite in="va" in2="SourceAlpha" operator="in" result="vein"/>'
691
+ + '<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="b"/>'
692
+ + `<feDiffuseLighting in="b" surfaceScale="5" diffuseConstant="1.1" lighting-color="#fff" result="dl">${light}</feDiffuseLighting>`
693
+ + '<feComposite in="SourceGraphic" in2="dl" operator="arithmetic" k1=".7" k2=".34" result="sh"/>'
694
+ + '<feMerge><feMergeNode in="sh"/><feMergeNode in="vein"/></feMerge>';
695
+ }
696
+ case 'candy': {
697
+ if (!full) return '';
698
+ // gomita: brillo especular de gelatina sobre la silueta redondeada y algo de transparencia
699
+ const light = '<feDistantLight azimuth="235" elevation="50"/>';
700
+ return '<feGaussianBlur in="SourceAlpha" stdDeviation="7" result="b"/>'
701
+ + `<feSpecularLighting in="b" surfaceScale="7" specularConstant=".9" specularExponent="26" lighting-color="#fff" result="sp">${light}</feSpecularLighting>`
702
+ + '<feComposite in="sp" in2="SourceAlpha" operator="in" result="sp2"/>'
703
+ + '<feColorMatrix in="SourceGraphic" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 .94 0" result="tr"/>'
704
+ + '<feComposite in="tr" in2="sp2" operator="arithmetic" k2="1" k3=".85"/>';
705
+ }
706
+ case 'holo': {
707
+ if (!full) return '';
708
+ // tornasol: el tono gira solo (al animar) y destellos de purpurina
709
+ const anim = animate ? '<animate attributeName="values" values="0;360" dur="7s" repeatCount="indefinite"/>' : '';
710
+ return `<feColorMatrix in="SourceGraphic" type="hueRotate" values="0" result="hr">${anim}</feColorMatrix>`
711
+ + `<feTurbulence type="fractalNoise" baseFrequency="1.6" numOctaves="1" seed="${seed % 997}" result="gn"/>`
712
+ + '<feColorMatrix in="gn" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 9 0 0 0 -5.5" result="ga"/><feComposite in="ga" in2="SourceAlpha" operator="in" result="gl"/>'
713
+ + '<feMerge><feMergeNode in="hr"/><feMergeNode in="gl"/></feMerge>';
714
+ }
715
+ case 'sumie':
716
+ // tinta china: casi sin color; en full el trazo tiembla y se corre en el papel de arroz
717
+ return '<feColorMatrix in="SourceGraphic" type="saturate" values=".15" result="bw"/>'
718
+ + (full
719
+ ? wobble(seed, 'bw', 2.6, 0.04) + '<feGaussianBlur in="wb" stdDeviation="1.8"/><feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 .5 0" result="bl"/>'
720
+ + grain(seed, '#f3eee2', 1.3, 0.8) + '<feMerge><feMergeNode in="bl"/><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>'
721
+ : '');
722
+ case 'tarot':
723
+ return full ? wobble(seed, 'SourceGraphic', 1.2, 0.05) + grain(seed, '#5a4020', 1.2) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
724
+ case 'retro70':
725
+ // foto desteñida: negros levantados, tono cálido y grano
726
+ return full
727
+ ? '<feColorMatrix in="SourceGraphic" values=".9 .06 0 0 .07 0 .86 .02 0 .05 0 .04 .76 0 .04 0 0 0 1 0" result="fd"/>' + grain(seed, '#5a3a1e', 1.2) + '<feMerge><feMergeNode in="fd"/><feMergeNode in="gr"/></feMerge>'
728
+ : '';
729
+ case 'mosaic': {
730
+ // teselas de 6 (5 + junta de mortero); en full cada tesela cambia de tono y queda puesta a mano
731
+ let f = '<feFlood flood-color="#9a9282"/><feComposite in2="SourceAlpha" operator="in" result="gt"/>';
732
+ let src = 'SourceGraphic';
733
+ if (full) {
734
+ f += `<feTurbulence type="fractalNoise" baseFrequency=".21" numOctaves="1" seed="${seed % 997}"/><feColorMatrix values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1" result="tg"/>`
735
+ + '<feComposite in="SourceGraphic" in2="tg" operator="arithmetic" k1=".6" k2=".72" result="tv"/>';
736
+ src = 'tv';
737
+ }
738
+ f += '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="6" height="6"/><feTile result="tile"/>'
739
+ + `<feComposite in="${src}" in2="tile" operator="in"/><feMorphology operator="dilate" radius="1.6"/>`
740
+ + '<feComponentTransfer result="ts"><feFuncA type="discrete" tableValues="0 1 1"/></feComponentTransfer>';
741
+ return f + (full ? wobble(seed, 'ts', 1.8, 0.11) + '<feMerge><feMergeNode in="gt"/><feMergeNode in="wb"/></feMerge>' : '<feMerge><feMergeNode in="gt"/><feMergeNode in="ts"/></feMerge>');
742
+ }
743
+ case 'neonsign': {
744
+ if (!full) return '';
745
+ // resplandor solo de lo luminoso (el tubo); al animar, el halo parpadea como un letrero viejo
746
+ const flick = animate ? '<animate attributeName="slope" values="1.5;1.5;1.5;.3;1.5;1.5;1.5;1.5;.6;1.5;.2;1.5" dur="5s" repeatCount="indefinite" calcMode="discrete"/>' : '';
747
+ return '<feColorMatrix in="SourceGraphic" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 .7 .7 .7 0 -.55" result="br"/>'
748
+ + '<feGaussianBlur in="br" stdDeviation="3" result="g1"/><feGaussianBlur in="br" stdDeviation="9" result="g2"/>'
749
+ + `<feComponentTransfer in="g2" result="g3"><feFuncA type="linear" slope="1.5">${flick}</feFuncA></feComponentTransfer>`
750
+ + '<feMerge><feMergeNode in="g3"/><feMergeNode in="g1"/><feMergeNode in="SourceGraphic"/></feMerge>';
751
+ }
752
+ case 'graffiti':
753
+ if (!full) return '';
754
+ // espray: niebla de gotitas alrededor del trazo
755
+ return '<feGaussianBlur in="SourceGraphic" stdDeviation="3.5"/><feComponentTransfer result="bl"><feFuncA type="linear" slope="2.2"/></feComponentTransfer>'
756
+ + `<feTurbulence type="fractalNoise" baseFrequency="1.1" numOctaves="1" seed="${seed % 997}" result="sn"/>`
757
+ + '<feColorMatrix in="sn" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 12 0 0 0 -6.9" result="sm"/>'
758
+ + '<feComposite in="bl" in2="sm" operator="in" result="spk"/>'
759
+ + '<feMerge><feMergeNode in="spk"/><feMergeNode in="SourceGraphic"/></feMerge>';
760
+ case 'thermal': {
761
+ // cámara térmica: luminancia → paleta «ironbow» (barato: vale en lite); en full, desenfoque y ruido del sensor
762
+ const ramp = '<feColorMatrix values=".2126 .7152 .0722 0 0 .2126 .7152 .0722 0 0 .2126 .7152 .0722 0 0 0 0 0 1 0"/>'
763
+ + '<feComponentTransfer result="rm"><feFuncR type="table" tableValues=".04 .16 .54 .91 1 1"/><feFuncG type="table" tableValues=".02 .04 .1 .25 .69 .98"/><feFuncB type="table" tableValues=".13 .42 .54 .11 0 .88"/></feComponentTransfer>';
764
+ return full
765
+ ? '<feGaussianBlur in="SourceGraphic" stdDeviation="1.2"/>' + ramp + grain(seed, '#ffffff', 0.8, 1.4) + '<feMerge><feMergeNode in="rm"/><feMergeNode in="gr"/></feMerge>'
766
+ : ramp;
767
+ }
467
768
  default:
468
769
  return '';
469
770
  }
@@ -660,6 +961,242 @@ function innerLayer(s, L, uid, d) {
660
961
  svg += `<path d="${d}" fill="url(#${uid}-wh)" transform="translate(${n(b.w * 0.22)} ${n(b.h * 0.2)})"/>`;
661
962
  break;
662
963
  }
964
+ case 'riso': {
965
+ // trama de medios tonos en la sombra (más gruesa cuanto más adentro) y papel sin tinta como brillo
966
+ defs += `<pattern id="${uid}-h1" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(22)"><circle cx="2.5" cy="2.5" r="1" fill="#0078bf" opacity=".6"/></pattern>`;
967
+ defs += `<pattern id="${uid}-h2" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(22)"><circle cx="2.5" cy="2.5" r="1.9" fill="#0078bf" opacity=".6"/></pattern>`;
968
+ svg += `<path d="${d}" fill="url(#${uid}-h1)" transform="translate(${n(b.w * 0.12)} ${n(b.h * 0.1)})"/>`;
969
+ svg += `<path d="${d}" fill="url(#${uid}-h2)" transform="translate(${n(b.w * 0.3)} ${n(b.h * 0.26)})"/>`;
970
+ svg += `<ellipse cx="${n(b.x + b.w * 0.3)}" cy="${n(b.y + b.h * 0.2)}" rx="${n(b.w * 0.13)}" ry="${n(b.h * 0.07)}" fill="#f7f1e3" opacity=".75" transform="rotate(-25 ${n(b.x + b.w * 0.3)} ${n(b.y + b.h * 0.2)})"/>`;
971
+ break;
972
+ }
973
+ case 'dotmatrix': {
974
+ // sombra tramada en damero y sombra llena, alineadas a los bloques de 6 del filtro
975
+ const q6 = (v) => Math.round(v / 6) * 6;
976
+ defs += `<pattern id="${uid}-dt" width="12" height="12" patternUnits="userSpaceOnUse"><rect width="6" height="6" fill="#346856"/><rect x="6" y="6" width="6" height="6" fill="#346856"/></pattern>`;
977
+ svg += `<path d="${d}" fill="url(#${uid}-dt)" transform="translate(${q6(b.w * 0.16)} ${q6(b.h * 0.14)})"/>`;
978
+ svg += `<path d="${d}" fill="#346856" transform="translate(${q6(b.w * 0.34)} ${q6(b.h * 0.3)})"/>`;
979
+ svg += `<rect x="${q6(b.x + b.w * 0.2)}" y="${q6(b.y + b.h * 0.12)}" width="12" height="6" fill="#e0f8d0"/><rect x="${q6(b.x + b.w * 0.2)}" y="${q6(b.y + b.h * 0.12) + 6}" width="6" height="6" fill="#e0f8d0"/>`;
980
+ break;
981
+ }
982
+ case 'chalk': {
983
+ // rayado de tiza: trazos claros en la luz y el pizarrón asomando en rayas en la sombra
984
+ let lt = '';
985
+ for (let i = 0; i < 12; i++) {
986
+ const x = b.x + b.w * (0.08 + rnd() * 0.5);
987
+ const y = b.y + b.h * (0.08 + rnd() * 0.45);
988
+ const l = b.w * (0.08 + rnd() * 0.12);
989
+ lt += `M${n(x)} ${n(y)}l${n(l)} ${n(-l * 0.5)}`;
990
+ }
991
+ svg += `<path d="${lt}" stroke="#fff" stroke-width="2.4" stroke-linecap="round" opacity=".35"/>`;
992
+ defs += `<pattern id="${uid}-ch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(-30)"><rect width="6" height="2.2" fill="#22352b"/></pattern>`;
993
+ svg += `<path d="${d}" fill="url(#${uid}-ch)" opacity=".55" transform="translate(${n(b.w * 0.2)} ${n(b.h * 0.18)})"/>`;
994
+ break;
995
+ }
996
+ case 'sticker': {
997
+ // vinilo brillante: dos franjas de reflejo en diagonal y un destello
998
+ const x0 = b.x - 10;
999
+ const y0 = b.y - 10;
1000
+ svg += `<path d="M${n(x0)} ${n(b.y + b.h * 0.42)}L${n(b.x + b.w * 0.5)} ${n(y0)}L${n(b.x + b.w * 0.72)} ${n(y0)}L${n(x0)} ${n(b.y + b.h * 0.6)}Z" fill="#fff" opacity=".3"/>`;
1001
+ svg += `<path d="M${n(x0)} ${n(b.y + b.h * 0.68)}L${n(b.x + b.w * 0.84)} ${n(y0)}L${n(b.x + b.w * 0.9)} ${n(y0)}L${n(x0)} ${n(b.y + b.h * 0.74)}Z" fill="#fff" opacity=".22"/>`;
1002
+ svg += `<path d="${d}" fill="#000" opacity=".08" transform="translate(${n(b.w * 0.1)} ${n(b.h * 0.12)})"/>`;
1003
+ break;
1004
+ }
1005
+ case 'felt': {
1006
+ // puntada corrida por dentro del borde en hilo de otro tono, relleno abullonado
1007
+ const k = Math.max(0.7, 1 - 18 / b.w);
1008
+ const cx = L.cx;
1009
+ const cy = b.y + b.h * 0.55;
1010
+ const thread = luminance(s.body.color) > 0.45 ? mix(s.body.color, '#2a2420', 0.55) : mix(s.body.color, '#fff8ec', 0.7);
1011
+ defs += `<radialGradient id="${uid}-fp" cx=".35" cy=".3" r=".75"><stop offset="0" stop-color="#fff" stop-opacity=".22"/><stop offset=".6" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".18"/></radialGradient>`;
1012
+ 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}-fp)"/>`;
1013
+ svg += `<path d="${d}" fill="none" stroke="${thread}" stroke-width="${n(2.4 / k)}" stroke-dasharray="${n(5.5 / k)} ${n(4 / k)}" stroke-linecap="round" transform="translate(${n(cx * (1 - k))} ${n(cy * (1 - k))}) scale(${n(k)})"/>`;
1014
+ break;
1015
+ }
1016
+ case 'papercut': {
1017
+ // capas de cartulina: el contorno repetido más chico, cada capa más clara y con su sombra
1018
+ defs += `<filter id="${uid}-ps" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="1.2" dy="2.4" stdDeviation="1.6" flood-color="#2a1f3a" flood-opacity=".35"/></filter>`;
1019
+ const cx = L.cx;
1020
+ const cy = b.y + b.h * 0.78;
1021
+ [0.8, 0.6, 0.4].forEach((k, i) => {
1022
+ svg += `<path d="${d}" fill="${mix(s.body.color, '#ffffff', 0.16 * (i + 1))}" filter="url(#${uid}-ps)" transform="translate(${n(cx * (1 - k))} ${n(cy * (1 - k))}) scale(${k})"/>`;
1023
+ });
1024
+ break;
1025
+ }
1026
+ case 'lowpoly': {
1027
+ // facetas: rejilla con jitter partida en triángulos; cada uno con la luz de su normal sobre una cúpula
1028
+ const x0 = b.x - 8;
1029
+ const y0 = b.y - 8;
1030
+ const w = b.w + 16;
1031
+ const h = b.h + 16;
1032
+ const cols = 5;
1033
+ const rows = 6;
1034
+ const cy = b.y + b.h * 0.5;
1035
+ const P = [];
1036
+ for (let r = 0; r <= rows; r++) {
1037
+ const row = [];
1038
+ for (let c = 0; c <= cols; c++) {
1039
+ const edge = r === 0 || c === 0 || r === rows || c === cols;
1040
+ const x = x0 + (w * (c + (edge ? 0 : (rnd() - 0.5) * 0.7))) / cols;
1041
+ const y = y0 + (h * (r + (edge ? 0 : (rnd() - 0.5) * 0.7))) / rows;
1042
+ const u = (x - L.cx) / (w / 2);
1043
+ const v = (y - cy) / (h / 2);
1044
+ row.push([x, y, Math.sqrt(Math.max(0, 1 - u * u - v * v)) * Math.min(w, h) * 0.6 + rnd() * 5]);
1045
+ }
1046
+ P.push(row);
1047
+ }
1048
+ const Lv = [-0.48, -0.62, 0.62];
1049
+ const tri = (a, e, c) => {
1050
+ const ux = e[0] - a[0], uy = e[1] - a[1], uz = e[2] - a[2];
1051
+ const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
1052
+ let nx = uy * vz - uz * vy;
1053
+ let ny = uz * vx - ux * vz;
1054
+ let nz = ux * vy - uy * vx;
1055
+ if (nz < 0) [nx, ny, nz] = [-nx, -ny, -nz];
1056
+ const sh = (nx * Lv[0] + ny * Lv[1] + nz * Lv[2]) / (Math.hypot(nx, ny, nz) || 1);
1057
+ const lit = sh > 0.62;
1058
+ const col = lit ? '#fff' : '#000';
1059
+ const op = Math.min(0.5, lit ? (sh - 0.62) * 1.2 : (0.62 - sh) * 0.6);
1060
+ return `<path d="M${n(a[0])} ${n(a[1])}L${n(e[0])} ${n(e[1])}L${n(c[0])} ${n(c[1])}Z" fill="${col}" stroke="${col}" stroke-width=".5" stroke-linejoin="round" opacity="${n(op)}"/>`;
1061
+ };
1062
+ for (let r = 0; r < rows; r++) {
1063
+ for (let c = 0; c < cols; c++) {
1064
+ const a = P[r][c], e = P[r][c + 1], f = P[r + 1][c + 1], g = P[r + 1][c];
1065
+ svg += (r + c) % 2 ? tri(a, e, f) + tri(a, f, g) : tri(a, e, g) + tri(e, f, g);
1066
+ }
1067
+ }
1068
+ break;
1069
+ }
1070
+ case 'marble': {
1071
+ // modelado: luz cenital arriba a la izquierda, sombra tallada y alguna veta (también en lite)
1072
+ defs += `<radialGradient id="${uid}-ml" cx=".34" cy=".26" r=".8"><stop offset="0" stop-color="#fff" stop-opacity=".55"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#4a443c" stop-opacity=".22"/></radialGradient>`;
1073
+ 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}-ml)"/>`;
1074
+ let v = '';
1075
+ for (let i = 0; i < 3; i++) {
1076
+ const y = b.y + b.h * (0.2 + i * 0.28 + rnd() * 0.1);
1077
+ v += `M${n(b.x - 6)} ${n(y)}C${n(b.x + b.w * 0.3)} ${n(y - 14 + rnd() * 10)} ${n(b.x + b.w * 0.55)} ${n(y + 18 * rnd())} ${n(b.x + b.w + 6)} ${n(y - 20 + rnd() * 12)}`;
1078
+ }
1079
+ svg += `<path d="${v}" fill="none" stroke="#7d776d" stroke-width=".9" opacity=".35"/>`;
1080
+ break;
1081
+ }
1082
+ case 'candy': {
1083
+ // gomita azucarada: borde interior más intenso, cristalitos de azúcar y un brillo
1084
+ svg += `<path d="${d}" fill="none" stroke="${mix(s.body.color, '#000', 0.25)}" stroke-width="12" opacity=".3"/>`;
1085
+ let sug = '';
1086
+ for (let i = 0; i < 46; i++) {
1087
+ const x = b.x + rnd() * b.w;
1088
+ const y = b.y + rnd() * b.h;
1089
+ const z = 1.3 + rnd() * 1.6;
1090
+ sug += `<rect x="${n(x)}" y="${n(y)}" width="${n(z)}" height="${n(z)}" transform="rotate(${Math.floor(rnd() * 90)} ${n(x)} ${n(y)})" opacity="${n(0.45 + rnd() * 0.45)}"/>`;
1091
+ }
1092
+ svg += `<g fill="#fff">${sug}</g>`;
1093
+ svg += `<ellipse cx="${n(b.x + b.w * 0.3)}" cy="${n(b.y + b.h * 0.2)}" rx="${n(b.w * 0.12)}" ry="${n(b.h * 0.06)}" fill="#fff" opacity=".6" transform="rotate(-30 ${n(b.x + b.w * 0.3)} ${n(b.y + b.h * 0.2)})"/>`;
1094
+ break;
1095
+ }
1096
+ case 'holo': {
1097
+ // arcoíris tornasolado en diagonal, rayas de brillo y destellos
1098
+ defs += `<linearGradient id="${uid}-hg" gradientUnits="userSpaceOnUse" x1="${n(b.x)}" y1="${n(b.y)}" x2="${n(b.x + b.w * 0.55)}" y2="${n(b.y + b.h * 0.55)}" spreadMethod="reflect">`
1099
+ + '<stop offset="0" stop-color="#ff8fd0"/><stop offset=".22" stop-color="#ffe08a"/><stop offset=".42" stop-color="#8affc0"/><stop offset=".62" stop-color="#8ad0ff"/><stop offset=".82" stop-color="#b98aff"/><stop offset="1" stop-color="#ff8fd0"/></linearGradient>';
1100
+ 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}-hg)" opacity=".62"/>`;
1101
+ let sh = '';
1102
+ for (let i = 0; i < 5; i++) {
1103
+ const o = b.w * (0.1 + i * 0.22 + rnd() * 0.05);
1104
+ sh += `M${n(b.x + o)} ${n(b.y - 6)}l${n(-b.h * 0.6)} ${n(b.h * 1.2)}`;
1105
+ }
1106
+ svg += `<path d="${sh}" stroke="#fff" stroke-width="1.4" opacity=".35"/>`;
1107
+ let sp = '';
1108
+ for (let i = 0; i < 3; i++) {
1109
+ const x = b.x + b.w * (0.15 + rnd() * 0.7);
1110
+ const y = b.y + b.h * (0.15 + rnd() * 0.7);
1111
+ const r = 3 + rnd() * 3;
1112
+ sp += `M${n(x)} ${n(y - r)}Q${n(x)} ${n(y)} ${n(x + r)} ${n(y)}Q${n(x)} ${n(y)} ${n(x)} ${n(y + r)}Q${n(x)} ${n(y)} ${n(x - r)} ${n(y)}Q${n(x)} ${n(y)} ${n(x)} ${n(y - r)}Z`;
1113
+ }
1114
+ svg += `<path d="${sp}" fill="#fff" opacity=".9"/>`;
1115
+ break;
1116
+ }
1117
+ case 'sumie': {
1118
+ // aguada: la tinta se junta abajo y una pincelada seca cruza el cuerpo
1119
+ defs += `<linearGradient id="${uid}-sw" x1="0" y1="0" x2="0" y2="1"><stop offset=".35" stop-color="#1d1b19" stop-opacity="0"/><stop offset="1" stop-color="#1d1b19" stop-opacity=".4"/></linearGradient>`;
1120
+ 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)"/>`;
1121
+ const x0 = b.x + b.w * 0.12;
1122
+ const y0 = b.y + b.h * 0.95;
1123
+ let br = '';
1124
+ for (let i = 0; i < 5; i++) {
1125
+ const o = (i - 2) * b.w * 0.028;
1126
+ br += `<path d="M${n(x0 + o)} ${n(y0 + o)}C${n(b.x + b.w * 0.05 + o)} ${n(b.y + b.h * 0.5)} ${n(b.x + b.w * 0.3 + o)} ${n(b.y + b.h * 0.15)} ${n(b.x + b.w * 0.62 + o)} ${n(b.y + b.h * 0.08 + o)}" stroke-width="${n(2 + rnd() * 3)}" stroke-dasharray="${Math.floor(20 + rnd() * 40)} ${Math.floor(2 + rnd() * 6)} ${Math.floor(10 + rnd() * 30)} ${Math.floor(3 + rnd() * 5)}"/>`;
1127
+ }
1128
+ svg += `<g fill="none" stroke="#1d1b19" stroke-linecap="round" opacity=".2">${br}</g>`;
1129
+ break;
1130
+ }
1131
+ case 'tarot': {
1132
+ // cielo de carta: filete de puntos dorados por dentro del borde, estrellas y una luna
1133
+ const gold = s.body.color === '#c9a23a' ? '#efe3c4' : '#c9a23a';
1134
+ const k = Math.max(0.72, 1 - 16 / b.w);
1135
+ const cy = b.y + b.h * 0.55;
1136
+ svg += `<path d="${d}" fill="none" stroke="${gold}" stroke-width="${n(2.2 / k)}" stroke-dasharray="0 ${n(6 / k)}" stroke-linecap="round" transform="translate(${n(L.cx * (1 - k))} ${n(cy * (1 - k))}) scale(${n(k)})"/>`;
1137
+ let st = '';
1138
+ for (let i = 0; i < 5; i++) {
1139
+ const x = b.x + b.w * (0.15 + rnd() * 0.7);
1140
+ const y = b.y + b.h * (0.45 + rnd() * 0.45);
1141
+ const r = 2.2 + rnd() * 2.6;
1142
+ st += star5(x, y, r);
1143
+ }
1144
+ svg += `<path d="${st}" fill="${gold}"/>`;
1145
+ const mx = b.x + b.w * 0.78;
1146
+ const my = b.y + b.h * 0.3;
1147
+ svg += `<path d="M${n(mx)} ${n(my - 7)}A7 7 0 1 0 ${n(mx)} ${n(my + 7)}A5.4 5.4 0 1 1 ${n(mx)} ${n(my - 7)}Z" fill="${gold}"/>`;
1148
+ break;
1149
+ }
1150
+ case 'retro70': {
1151
+ // franjas setenteras que cruzan el cuerpo en curva
1152
+ const cols = ['#4a2c1e', '#c94f2c', '#e8762c', '#f2b33d', '#d9c58a'].filter((c) => c !== s.body.color).slice(1, 4);
1153
+ const x0 = b.x - 12;
1154
+ const x1 = b.x + b.w + 12;
1155
+ cols.forEach((c, i) => {
1156
+ const y = b.y + b.h * (0.7 + i * 0.085);
1157
+ svg += `<path d="M${n(x0)} ${n(y + 14)}C${n(b.x + b.w * 0.3)} ${n(y + 16)} ${n(b.x + b.w * 0.6)} ${n(y - 22)} ${n(x1)} ${n(y - 30)}" fill="none" stroke="${c}" stroke-width="${n(b.h * 0.075)}"/>`;
1158
+ });
1159
+ svg += `<path d="${d}" fill="#fff" opacity=".12" transform="translate(${n(-b.w * 0.08)} ${n(-b.h * 0.1)})"/>`;
1160
+ break;
1161
+ }
1162
+ case 'mosaic': {
1163
+ // opus vermiculatum: una fila de teselas de otro color siguiendo el borde, sombra y luz
1164
+ const k = Math.max(0.7, 1 - 20 / b.w);
1165
+ const cy = b.y + b.h * 0.55;
1166
+ svg += `<path d="${d}" fill="#000" opacity=".2" transform="translate(${n(b.w * 0.2)} ${n(b.h * 0.18)})"/>`;
1167
+ svg += `<path d="${d}" fill="none" stroke="${s.body.patternColor && s.body.patternColor !== s.body.color ? s.body.patternColor : '#efe4cc'}" stroke-width="${n(6 / k)}" opacity=".9" transform="translate(${n(L.cx * (1 - k))} ${n(cy * (1 - k))}) scale(${n(k)})"/>`;
1168
+ svg += `<ellipse cx="${n(b.x + b.w * 0.3)}" cy="${n(b.y + b.h * 0.22)}" rx="${n(b.w * 0.14)}" ry="${n(b.h * 0.08)}" fill="#fff" opacity=".35"/>`;
1169
+ break;
1170
+ }
1171
+ case 'neonsign': {
1172
+ // segundo tubo por dentro (con sus cortes) y el reflejo del vidrio
1173
+ const tube = neonTube(s.body.color);
1174
+ const k = Math.max(0.6, 1 - 26 / b.w);
1175
+ const cy = b.y + b.h * 0.55;
1176
+ svg += `<path d="${d}" fill="none" stroke="${tube}" stroke-width="${n(2.2 / k)}" stroke-dasharray="${n(70 / k)} ${n(9 / k)}" stroke-linecap="round" opacity=".75" transform="translate(${n(L.cx * (1 - k))} ${n(cy * (1 - k))}) scale(${n(k)})"/>`;
1177
+ defs += `<linearGradient id="${uid}-ng" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${tube}" stop-opacity=".22"/><stop offset=".5" stop-color="${tube}" stop-opacity="0"/></linearGradient>`;
1178
+ 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}-ng)"/>`;
1179
+ break;
1180
+ }
1181
+ case 'graffiti': {
1182
+ // relleno de espray: se funde a otro color abajo, burbujas de brillo y reflejo blanco
1183
+ const c2 = PAL.graffiti.filter((c) => c !== s.body.color && c !== '#ffffff' && c !== '#111111')[Math.floor(rnd() * 6)];
1184
+ defs += `<linearGradient id="${uid}-gf" x1="0" y1="0" x2="0" y2="1"><stop offset=".4" stop-color="${c2}" stop-opacity="0"/><stop offset=".85" stop-color="${c2}" stop-opacity=".95"/></linearGradient>`;
1185
+ 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}-gf)"/>`;
1186
+ let bub = '';
1187
+ for (let i = 0; i < 4; i++) bub += `<circle cx="${n(b.x + b.w * (0.15 + rnd() * 0.7))}" cy="${n(b.y + b.h * (0.35 + rnd() * 0.5))}" r="${n(2.5 + rnd() * 4)}" fill="none" stroke="#fff" stroke-width="1.6" opacity=".55"/>`;
1188
+ svg += bub;
1189
+ const gx = b.x + b.w * 0.2;
1190
+ const gy = b.y + b.h * 0.3;
1191
+ svg += `<path d="M${n(gx)} ${n(gy + 16)}Q${n(gx)} ${n(gy)} ${n(gx + 14)} ${n(gy - 6)}" fill="none" stroke="#fff" stroke-width="4.5" stroke-linecap="round"/><circle cx="${n(gx + 22)}" cy="${n(gy - 8)}" r="2.4" fill="#fff"/>`;
1192
+ break;
1193
+ }
1194
+ case 'thermal': {
1195
+ // calor: el centro del cuerpo más caliente que los bordes
1196
+ defs += `<radialGradient id="${uid}-th" cx=".5" cy=".48" r=".62"><stop offset="0" stop-color="#fff" stop-opacity=".6"/><stop offset=".45" stop-color="#fff" stop-opacity=".12"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></radialGradient>`;
1197
+ 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}-th)"/>`;
1198
+ break;
1199
+ }
663
1200
  }
664
1201
  return { defs, svg };
665
1202
  }
@@ -691,6 +1228,53 @@ export function styleLayers(s, L, ctx) {
691
1228
  const W = 300;
692
1229
  const GROUND = 240;
693
1230
 
1231
+ /** Filtro de grano para un fondo: motas de un color (rgb 0..1) con la densidad de `a`/`b`. */
1232
+ const bdGrain = (id, rnd, rgb, a = 1.4, b = -0.72, freq = 0.75) =>
1233
+ `<filter id="${id}" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 ${rgb[0]} 0 0 0 0 ${rgb[1]} 0 0 0 0 ${rgb[2]} 0 0 0 ${a} ${b}"/></filter>`;
1234
+
1235
+ /** Rejilla con jitter partida en triángulos que cubre el rectángulo; `color(x, y)` en el centroide. */
1236
+ function facets(rnd, x0, y0, w, h, cols, rows, color) {
1237
+ const P = [];
1238
+ for (let r = 0; r <= rows; r++) {
1239
+ const row = [];
1240
+ for (let c = 0; c <= cols; c++) {
1241
+ const ex = c === 0 || c === cols;
1242
+ const ey = r === 0 || r === rows;
1243
+ row.push([x0 + (w * (c + (ex ? 0 : (rnd() - 0.5) * 0.8))) / cols, y0 + (h * (r + (ey ? 0 : (rnd() - 0.5) * 0.8))) / rows]);
1244
+ }
1245
+ P.push(row);
1246
+ }
1247
+ let s = '';
1248
+ const t = (a, b, c) => {
1249
+ const col = color((a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3);
1250
+ s += `<path d="M${n(a[0])} ${n(a[1])}L${n(b[0])} ${n(b[1])}L${n(c[0])} ${n(c[1])}Z" fill="${col}" stroke="${col}" stroke-width=".7" stroke-linejoin="round"/>`;
1251
+ };
1252
+ for (let r = 0; r < rows; r++) {
1253
+ for (let c = 0; c < cols; c++) {
1254
+ const a = P[r][c], b = P[r][c + 1], e = P[r + 1][c + 1], d = P[r + 1][c];
1255
+ if ((r + c) % 2) {
1256
+ t(a, b, e);
1257
+ t(a, e, d);
1258
+ } else {
1259
+ t(a, b, d);
1260
+ t(b, e, d);
1261
+ }
1262
+ }
1263
+ }
1264
+ return s;
1265
+ }
1266
+
1267
+ /** Celdas de una rejilla de `size` cuyo centro cae en el círculo (para dibujar en bloques). */
1268
+ function blockDisc(cx, cy, r, size) {
1269
+ let d = '';
1270
+ for (let y = Math.floor((cy - r) / size) * size; y < cy + r; y += size) {
1271
+ for (let x = Math.floor((cx - r) / size) * size; x < cx + r; x += size) {
1272
+ if ((x + size / 2 - cx) ** 2 + (y + size / 2 - cy) ** 2 <= r * r) d += `M${x} ${y}h${size}v${size}h${-size}Z`;
1273
+ }
1274
+ }
1275
+ return d;
1276
+ }
1277
+
694
1278
  const BACKDROPS = {
695
1279
  ukiyoe(p, rnd) {
696
1280
  const sunX = 190 + rnd() * 50;
@@ -1395,6 +1979,698 @@ const BACKDROPS = {
1395
1979
  s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
1396
1980
  return s;
1397
1981
  },
1982
+ riso(p, rnd) {
1983
+ const pk = '#ff48b0';
1984
+ const bl = '#0078bf';
1985
+ const yl = '#ffe800';
1986
+ const M = 'style="mix-blend-mode:multiply"';
1987
+ let s = `<defs>${bdGrain(`${p}g`, rnd, [0.97, 0.95, 0.9], 1.7, -0.95)}`
1988
+ + `<pattern id="${p}hb" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(18)"><circle cx="3" cy="3" r="1.7" fill="${bl}"/></pattern>`
1989
+ + `<pattern id="${p}hp" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(-24)"><circle cx="2.5" cy="2.5" r="1.4" fill="${pk}"/></pattern></defs>`;
1990
+ s += `<rect width="${W}" height="${W}" fill="#f7f1e3"/>`;
1991
+ // sol amarillo con su doble rosa en trama, corrido
1992
+ const sx = 60 + rnd() * 180;
1993
+ s += `<circle cx="${n(sx)}" cy="80" r="46" fill="${yl}" ${M}/><circle cx="${n(sx + 10)}" cy="73" r="46" fill="url(#${p}hp)" ${M}/>`;
1994
+ // nubes de una sola tinta
1995
+ for (let i = 0; i < 2; i++) {
1996
+ const cx = 40 + rnd() * 220;
1997
+ const cy = 40 + rnd() * 50;
1998
+ s += `<rect x="${n(cx - 30)}" y="${n(cy)}" width="60" height="16" rx="8" fill="${bl}" opacity=".55" ${M}/>`;
1999
+ }
2000
+ // cerros: uno azul plano, otro rosa en trama, superpuestos (la mezcla hace el violeta)
2001
+ const hill = (y, a) => `M-10 ${GROUND}V${n(y)}Q${n(50 + rnd() * 40)} ${n(y - a)} ${n(120 + rnd() * 30)} ${n(y)}T${n(230 + rnd() * 20)} ${n(y + rnd() * 10)}T${W + 10} ${n(y - 10)}V${GROUND}Z`;
2002
+ s += `<path d="${hill(170, 50)}" fill="${bl}" opacity=".9" ${M}/>`;
2003
+ s += `<path d="${hill(196, 40)}" fill="url(#${p}hp)" ${M}/><path d="${hill(210, 30)}" fill="${pk}" opacity=".75" ${M}/>`;
2004
+ // suelo: amarillo con trama azul corrida
2005
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="${yl}" ${M}/><rect x="3" y="${GROUND + 4}" width="${W}" height="${W - GROUND}" fill="url(#${p}hb)" opacity=".7" ${M}/>`;
2006
+ // flores de dos tintas
2007
+ for (let i = 0; i < 6; i++) {
2008
+ const x = rnd() < 0.5 ? 14 + rnd() * 70 : 216 + rnd() * 70;
2009
+ const y = GROUND + 14 + rnd() * 40;
2010
+ s += `<circle cx="${n(x)}" cy="${n(y)}" r="5" fill="${pk}" ${M}/><circle cx="${n(x + 2)}" cy="${n(y - 1.5)}" r="2.2" fill="${bl}" ${M}/>`;
2011
+ }
2012
+ // marcas de registro en las esquinas
2013
+ let reg = '';
2014
+ for (const [x, y] of [[14, 14], [W - 14, 14], [14, W - 14], [W - 14, W - 14]]) reg += `<circle cx="${x}" cy="${y}" r="4.5"/><path d="M${x - 8} ${y}H${x + 8}M${x} ${y - 8}V${y + 8}"/>`;
2015
+ s += `<g fill="none" stroke="${bl}" stroke-width=".8">${reg}</g><g fill="none" stroke="${pk}" stroke-width=".8" transform="translate(1.5 -1)">${reg}</g>`;
2016
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2017
+ return s;
2018
+ },
2019
+
2020
+ dotmatrix(p, rnd) {
2021
+ const [c0, c1, c2, c3] = PAL.dotmatrix;
2022
+ const B = 6;
2023
+ let s = `<defs><pattern id="${p}dt" width="12" height="12" patternUnits="userSpaceOnUse"><rect width="6" height="6" fill="${c2}"/><rect x="6" y="6" width="6" height="6" fill="${c2}"/></pattern>`
2024
+ + `<pattern id="${p}px" width="3" height="3" patternUnits="userSpaceOnUse"><path d="M0 0H3M0 0V3" stroke="${c0}" stroke-width=".5" opacity=".12"/></pattern></defs>`;
2025
+ s += `<g shape-rendering="crispEdges"><rect width="${W}" height="${W}" fill="${c3}"/>`;
2026
+ // horizonte tramado
2027
+ s += `<rect y="132" width="${W}" height="36" fill="url(#${p}dt)"/><rect y="168" width="${W}" height="${GROUND - 168}" fill="${c2}"/>`;
2028
+ // sol y nubes en bloques
2029
+ const sx = 6 * Math.floor(30 + rnd() * 12);
2030
+ s += `<path d="${blockDisc(sx, 66, 28, B)}" fill="${c2}"/><path d="${blockDisc(sx, 66, 17, B)}" fill="${c3}"/>`;
2031
+ for (let i = 0; i < 3; i++) {
2032
+ const cx = 6 * Math.floor(4 + rnd() * 40);
2033
+ const cy = 6 * Math.floor(4 + rnd() * 10);
2034
+ s += `<path d="${blockDisc(cx, cy, 11, B)}${blockDisc(cx + 14, cy + 3, 9, B)}${blockDisc(cx - 13, cy + 4, 8, B)}" fill="${c2}"/>`;
2035
+ }
2036
+ // colinas escalonadas
2037
+ let hills = '';
2038
+ const ph = rnd() * 6;
2039
+ for (let x = 0; x < W; x += B) {
2040
+ const h = Math.round((Math.sin(x / 34 + ph) * 18 + Math.sin(x / 13) * 6 + 40) / B) * B;
2041
+ hills += `M${x} ${GROUND - h}h${B}V${GROUND}h${-B}Z`;
2042
+ }
2043
+ s += `<path d="${hills}" fill="${c1}"/>`;
2044
+ // árbol de bloques a un lado y tubería al otro
2045
+ const tx = rnd() < 0.5 ? 36 : 252;
2046
+ s += `<rect x="${tx - 3}" y="${GROUND - 42}" width="6" height="42" fill="${c0}"/><path d="${blockDisc(tx, GROUND - 54, 20, B)}" fill="${c0}"/><path d="${blockDisc(tx - 4, GROUND - 58, 11, B)}" fill="${c1}"/>`;
2047
+ const px = tx < 150 ? 246 : 30;
2048
+ s += `<rect x="${px - 15}" y="${GROUND - 36}" width="30" height="36" fill="${c1}"/><rect x="${px - 18}" y="${GROUND - 48}" width="36" height="12" fill="${c1}"/><path d="M${px - 18} ${GROUND - 48}h36v12h-36ZM${px - 15} ${GROUND - 36}V${GROUND}M${px + 15} ${GROUND - 36}V${GROUND}" fill="none" stroke="${c0}" stroke-width="3"/><rect x="${px - 12}" y="${GROUND - 45}" width="6" height="6" fill="${c3}"/>`;
2049
+ // monedas flotando
2050
+ for (let i = 0; i < 3; i++) {
2051
+ const cx = 6 * Math.floor(7 + i * 16 + rnd() * 4);
2052
+ s += `<path d="${blockDisc(cx, 114, 7, 3)}" fill="${c1}"/><rect x="${cx - 1.5}" y="108" width="3" height="12" fill="${c3}"/>`;
2053
+ }
2054
+ // suelo de ladrillos
2055
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="${c1}"/>`;
2056
+ let br = '';
2057
+ for (let r = 0; r * 12 < W - GROUND; r++) {
2058
+ const y = GROUND + r * 12;
2059
+ br += `M0 ${y}H${W}`;
2060
+ for (let x = r % 2 ? 12 : 0; x < W; x += 24) br += `M${x} ${y}v12`;
2061
+ }
2062
+ s += `<path d="${br}" stroke="${c0}" stroke-width="2" fill="none"/><rect y="${GROUND}" width="${W}" height="3" fill="${c2}"/>`;
2063
+ // marcador
2064
+ s += `<text x="12" y="22" font-family="monospace" font-weight="bold" font-size="12" fill="${c0}">SCORE ${String(1000 + Math.floor(rnd() * 9000)).padStart(6, '0')}</text>`;
2065
+ let hearts = '';
2066
+ for (let i = 0; i < 3; i++) {
2067
+ const x = 246 + i * 15;
2068
+ hearts += `M${x} 12h3v3h-3ZM${x + 6} 12h3v3h-3ZM${x - 3} 15h15v3h-15ZM${x} 18h9v3h-9ZM${x + 3} 21h3v3h-3Z`;
2069
+ }
2070
+ s += `<path d="${hearts}" fill="${c0}"/></g>`;
2071
+ // rejilla de la pantalla LCD
2072
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}px)"/>`;
2073
+ return s;
2074
+ },
2075
+
2076
+ chalk(p, rnd) {
2077
+ const ch = '#f4f4ee';
2078
+ let s = `<defs><radialGradient id="${p}bd" cx=".5" cy=".45" r=".75"><stop offset="0" stop-color="#3a5a4c"/><stop offset="1" stop-color="#1f342b"/></radialGradient>`
2079
+ + `<filter id="${p}c" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".9" numOctaves="2" seed="${Math.floor(rnd() * 900)}" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="2.5" xChannelSelector="R" yChannelSelector="G" result="d"/><feColorMatrix in="n" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3.6 0 0 0 2.5" result="a"/><feComposite in="d" in2="a" operator="in"/></filter>`
2080
+ + `<filter id="${p}sm" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="9"/></filter>`
2081
+ + `${bdGrain(`${p}g`, rnd, [0.95, 0.96, 0.93], 1.2, -0.66, 0.9)}</defs>`;
2082
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}bd)"/>`;
2083
+ // borrones de mota
2084
+ let sm = '';
2085
+ for (let i = 0; i < 5; i++) sm += `<ellipse cx="${n(30 + rnd() * 240)}" cy="${n(30 + rnd() * 200)}" rx="${n(30 + rnd() * 40)}" ry="${n(10 + rnd() * 14)}" transform="rotate(${Math.floor(rnd() * 40 - 20)})"/>`;
2086
+ s += `<g fill="${ch}" opacity=".07" filter="url(#${p}sm)">${sm}</g>`;
2087
+ // dibujos de tiza
2088
+ let g = '';
2089
+ const sx = rnd() < 0.5 ? 58 : 242;
2090
+ let rays = '';
2091
+ for (let i = 0; i < 10; i++) {
2092
+ const a = (i / 10) * Math.PI * 2;
2093
+ rays += `M${n(sx + Math.cos(a) * 24)} ${n(62 + Math.sin(a) * 24)}L${n(sx + Math.cos(a) * 34)} ${n(62 + Math.sin(a) * 34)}`;
2094
+ }
2095
+ g += `<circle cx="${sx}" cy="62" r="17" fill="none" stroke="#f6e27a" stroke-width="3"/><path d="${rays}" stroke="#f6e27a" stroke-width="3" stroke-linecap="round"/>`;
2096
+ const ox = sx < 150 ? 236 : 62;
2097
+ g += `<text x="${ox}" y="56" text-anchor="middle" font-family="'Comic Sans MS','Chalkboard SE',cursive" font-size="17" fill="${ch}">a²+b²=c²</text>`;
2098
+ g += `<text x="${ox}" y="84" text-anchor="middle" font-family="'Comic Sans MS','Chalkboard SE',cursive" font-size="15" fill="#f6a5c0">7 + 3 = 10</text>`;
2099
+ let stars = '';
2100
+ for (let i = 0; i < 5; i++) stars += star5(20 + rnd() * 260, 110 + rnd() * 60, 4 + rnd() * 3);
2101
+ g += `<path d="${stars}" fill="none" stroke="${ch}" stroke-width="1.8" stroke-linejoin="round"/>`;
2102
+ g += `<path d="M${ox - 38} 104q16 -14 30 0t30 0" fill="none" stroke="#9fd8f0" stroke-width="2.6" stroke-linecap="round"/>`;
2103
+ // suelo: raya de tiza y pasto en trazos
2104
+ let grass = `M8 ${GROUND + 2}Q80 ${GROUND - 3} 150 ${GROUND + 1}T${W - 8} ${GROUND}`;
2105
+ for (let x = 14; x < W - 10; x += 9 + rnd() * 8) grass += `M${n(x)} ${GROUND + 1}l${n(-2 + rnd() * 4)} ${n(-6 - rnd() * 6)}`;
2106
+ g += `<path d="${grass}" fill="none" stroke="#b6e3a0" stroke-width="2.6" stroke-linecap="round"/>`;
2107
+ s += `<g filter="url(#${p}c)">${g}</g>`;
2108
+ // marco de madera y repisa con tizas y borrador
2109
+ s += `<path d="M0 0H${W}V${W}H0ZM9 9V${W - 9}H${W - 9}V9Z" fill-rule="evenodd" fill="#8a5a34"/><path d="M9 9H${W - 9}V${W - 9}H9Z" fill="none" stroke="#5e3b20" stroke-width="2"/>`;
2110
+ s += `<rect x="0" y="${W - 22}" width="${W}" height="22" fill="#9a6a40"/><rect x="0" y="${W - 22}" width="${W}" height="4" fill="#b8844f"/>`;
2111
+ s += `<rect x="40" y="${W - 28}" width="22" height="6" rx="3" fill="${ch}"/><rect x="70" y="${W - 28}" width="16" height="6" rx="3" fill="#f6a5c0"/>`;
2112
+ s += `<rect x="210" y="${W - 34}" width="46" height="12" rx="2" fill="#d8c7a8"/><rect x="210" y="${W - 26}" width="46" height="4" fill="#4a4a52"/>`;
2113
+ s += `<rect width="${W}" height="${W - 22}" filter="url(#${p}g)" opacity=".5"/>`;
2114
+ return s;
2115
+ },
2116
+
2117
+ sticker(p, rnd) {
2118
+ let s = `<defs><filter id="${p}sh" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="1.5" dy="3" stdDeviation="2" flood-color="#0b2a1e" flood-opacity=".45"/></filter></defs>`;
2119
+ // tapete de corte con su cuadrícula, guías a 45° y regla
2120
+ s += `<rect width="${W}" height="${W}" fill="#2f7d62"/>`;
2121
+ let minor = '';
2122
+ let major = '';
2123
+ for (let v = 10; v < W; v += 10) (v % 50 ? (minor += `M${v} 0V${W}M0 ${v}H${W}`) : (major += `M${v} 0V${W}M0 ${v}H${W}`));
2124
+ s += `<path d="${minor}" stroke="#fff" stroke-width=".5" opacity=".14"/><path d="${major}" stroke="#fff" stroke-width="1" opacity=".32"/>`;
2125
+ s += `<path d="M0 ${W}L${W} 0M0 150L150 0M150 ${W}L${W} 150" stroke="#f2d24a" stroke-width=".9" opacity=".45"/>`;
2126
+ let nums = '';
2127
+ for (let v = 50; v < W; v += 50) nums += `<text x="${v + 2}" y="9">${v / 10}</text><text x="2" y="${v - 2}">${v / 10}</text>`;
2128
+ s += `<g font-family="sans-serif" font-size="7" fill="#fff" opacity=".6">${nums}</g>`;
2129
+ // pegatinas sueltas: troquel blanco, color y brillo
2130
+ const shapes = [
2131
+ (c) => `<path d="${star5(0, 0, 18)}" fill="${c}"/>`,
2132
+ (c) => `<path d="M0 16C-24 0 -16 -18 0 -8C16 -18 24 0 0 16Z" fill="${c}"/>`,
2133
+ (c) => `<path d="M4 -20L-12 4H0L-4 20L12 -4H0Z" fill="${c}"/>`,
2134
+ (c) => `<circle r="16" fill="${c}"/><circle cx="-5.5" cy="-3" r="2.2" fill="#1a1720"/><circle cx="5.5" cy="-3" r="2.2" fill="#1a1720"/><path d="M-7 5Q0 11 7 5" fill="none" stroke="#1a1720" stroke-width="2" stroke-linecap="round"/>`,
2135
+ (c) => `<path d="M-22 -12H22V8H-4L-12 16V8H-22Z" fill="${c}"/><text y="3" text-anchor="middle" font-family="sans-serif" font-weight="900" font-size="12" fill="#1a1720">WOW</text>`,
2136
+ (c) => `<path d="M-20 8A20 20 0 0 1 20 8" fill="none" stroke="${c}" stroke-width="6"/><path d="M-14 8A14 14 0 0 1 14 8" fill="none" stroke="#ffd23f" stroke-width="6"/><path d="M-8 8A8 8 0 0 1 8 8" fill="none" stroke="#3ec1d3" stroke-width="6"/>`,
2137
+ ];
2138
+ const cols = ['#ff5d8f', '#ffd23f', '#3ec1d3', '#ff8c42', '#a77dff', '#7ed957'];
2139
+ const spots = [[44, 46], [256, 50], [36, 150], [264, 156], [46, 262], [254, 262]];
2140
+ const order = shapes.map((f, i) => [rnd(), i]).sort((a, b) => a[0] - b[0]).map((x) => x[1]);
2141
+ spots.forEach(([x, y], i) => {
2142
+ const art = shapes[order[i]](cols[(i + Math.floor(rnd() * 6)) % 6]);
2143
+ s += `<g transform="translate(${x} ${y}) rotate(${Math.floor(rnd() * 40 - 20)})" filter="url(#${p}sh)"><g stroke="#fff" stroke-width="9" stroke-linejoin="round" stroke-linecap="round">${art}</g>${art}<path d="M-14 -10L-6 -16" stroke="#fff" stroke-width="2.5" stroke-linecap="round" opacity=".7"/></g>`;
2144
+ });
2145
+ return s;
2146
+ },
2147
+
2148
+ felt(p, rnd) {
2149
+ let s = `<defs><pattern id="${p}ln" width="4" height="4" patternUnits="userSpaceOnUse"><path d="M0 1H4M1 0V4" stroke="#8a7a60" stroke-width=".7" opacity=".22"/></pattern>`
2150
+ + `<radialGradient id="${p}vg" cx=".5" cy=".5" r=".7"><stop offset=".6" stop-color="#000" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".25"/></radialGradient></defs>`;
2151
+ // lino de fondo y la tela tensada dentro del bastidor
2152
+ s += `<rect width="${W}" height="${W}" fill="#cbbfa8"/><rect width="${W}" height="${W}" fill="url(#${p}ln)"/><rect width="${W}" height="${W}" fill="url(#${p}vg)"/>`;
2153
+ const R = 138;
2154
+ s += `<circle cx="150" cy="150" r="${R}" fill="#f3ecdc"/><circle cx="150" cy="150" r="${R}" fill="url(#${p}ln)"/>`;
2155
+ // bordado: sol de puntadas radiales, nubes en pespunte, flores margarita y pasto
2156
+ let g = '';
2157
+ const sx = rnd() < 0.5 ? 92 : 208;
2158
+ let sun = '';
2159
+ for (let i = 0; i < 28; i++) {
2160
+ const a = (i / 28) * Math.PI * 2;
2161
+ sun += `M${n(sx + Math.cos(a) * 3)} ${n(62 + Math.sin(a) * 3)}L${n(sx + Math.cos(a) * 15)} ${n(62 + Math.sin(a) * 15)}`;
2162
+ }
2163
+ g += `<path d="${sun}" stroke="#f2b33d" stroke-width="2.2" stroke-linecap="round"/>`;
2164
+ const cx = sx < 150 ? 206 : 94;
2165
+ g += `<path d="M${cx - 24} 74a10 10 0 0 1 8 -14a13 13 0 0 1 24 -2a10 10 0 0 1 16 16Z" fill="none" stroke="#7aa6c8" stroke-width="2" stroke-dasharray="4 2.5" stroke-linecap="round"/>`;
2166
+ const flower = (x, y, c) => {
2167
+ let pe = '';
2168
+ for (let i = 0; i < 6; i++) pe += `<ellipse cx="${x}" cy="${n(y - 6)}" rx="2.6" ry="5.5" transform="rotate(${i * 60} ${x} ${y})"/>`;
2169
+ return `<path d="M${x} ${y + 6}Q${x - 3} ${y + 20} ${x + 1} ${GROUND + 6}" fill="none" stroke="#5f8a4a" stroke-width="2"/><g fill="none" stroke="${c}" stroke-width="1.8">${pe}</g><circle cx="${x}" cy="${y}" r="2.6" fill="#f2b33d"/>`;
2170
+ };
2171
+ const fc = ['#d8708a', '#9a7ac8', '#e89a5a', '#6aa0c8'];
2172
+ for (let i = 0; i < 4; i++) {
2173
+ const x = i < 2 ? 40 + i * 28 + rnd() * 10 : 206 + (i - 2) * 30 + rnd() * 10;
2174
+ g += flower(n(x), n(GROUND - 30 - rnd() * 34), fc[Math.floor(rnd() * 4)]);
2175
+ }
2176
+ let grass = '';
2177
+ for (let x = 22; x < 280; x += 5 + rnd() * 4) grass += `M${n(x)} ${GROUND + 6}l${n(-2 + rnd() * 4)} ${n(-6 - rnd() * 7)}`;
2178
+ g += `<path d="${grass}" stroke="#6f9a52" stroke-width="1.8" stroke-linecap="round"/>`;
2179
+ g += `<path d="M14 ${GROUND + 6}Q150 ${GROUND - 2} 286 ${GROUND + 6}" fill="none" stroke="#4f7a3a" stroke-width="2.4" stroke-dasharray="6 4" stroke-linecap="round"/>`;
2180
+ // tierra en puntadas cruzadas
2181
+ let xs = '';
2182
+ for (let y = GROUND + 18; y < W - 20; y += 10) {
2183
+ for (let x = 30 + (y % 20 ? 5 : 0); x < 270; x += 10) {
2184
+ if ((x - 150) ** 2 + (y - 150) ** 2 < (R - 10) ** 2) xs += `M${x - 3} ${y - 3}l6 6M${x + 3} ${y - 3}l-6 6`;
2185
+ }
2186
+ }
2187
+ g += `<path d="${xs}" stroke="#b08a62" stroke-width="1.4" stroke-linecap="round" opacity=".8"/>`;
2188
+ s += g;
2189
+ // bastidor de madera: aro doble y tornillo arriba
2190
+ s += `<circle cx="150" cy="150" r="${R + 2}" fill="none" stroke="#000" stroke-width="16" opacity=".12" transform="translate(2 4)"/>`;
2191
+ s += `<circle cx="150" cy="150" r="${R + 2}" fill="none" stroke="#c8995e" stroke-width="13"/><circle cx="150" cy="150" r="${R + 2}" fill="none" stroke="#a87a44" stroke-width="1.4"/><circle cx="150" cy="150" r="${R + 7}" fill="none" stroke="#e2bb84" stroke-width="1.2"/>`;
2192
+ s += `<rect x="138" y="0" width="24" height="12" rx="2" fill="#b88a50"/><rect x="132" y="2" width="36" height="5" rx="2" fill="#9a9aa2"/><circle cx="168" cy="4.5" r="4" fill="#bdbdc4"/>`;
2193
+ return s;
2194
+ },
2195
+
2196
+ papercut(p, rnd) {
2197
+ const sets = [
2198
+ ['#fde8d7', '#f7b267', '#f4845f', '#f27059', '#c46a8a', '#8a5a8a', '#5a4a7a'],
2199
+ ['#e4f3ef', '#ffd6a5', '#b8e0d2', '#95c8b8', '#6aa89a', '#4f8a7e', '#3a6a62'],
2200
+ ];
2201
+ const pal = sets[Math.floor(rnd() * sets.length)];
2202
+ let s = `<defs><filter id="${p}s" x="-10%" y="-20%" width="120%" height="150%"><feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#2a1f3a" flood-opacity=".35"/></filter>${bdGrain(`${p}g`, rnd, [1, 1, 1], 1.1, -0.62, 1.1)}</defs>`;
2203
+ s += `<rect width="${W}" height="${W}" fill="${pal[0]}"/>`;
2204
+ const sx = 60 + rnd() * 180;
2205
+ s += `<circle cx="${n(sx)}" cy="70" r="30" fill="${pal[1]}" filter="url(#${p}s)"/><circle cx="${n(sx)}" cy="70" r="20" fill="#fff" opacity=".35"/>`;
2206
+ for (let i = 0; i < 2; i++) {
2207
+ const x = 40 + rnd() * 220;
2208
+ const y = 30 + rnd() * 40;
2209
+ s += `<path d="M${n(x - 26)} ${n(y + 8)}a10 10 0 0 1 12 -12a14 14 0 0 1 26 -2a10 10 0 0 1 14 14Z" fill="#fff" filter="url(#${p}s)"/>`;
2210
+ }
2211
+ // capas de cerros de cartulina, del fondo al frente
2212
+ for (let i = 0; i < 5; i++) {
2213
+ const y = 112 + i * 30;
2214
+ const a = 26 - i * 3;
2215
+ let d = `M-10 ${W}V${n(y)}`;
2216
+ const k = 4;
2217
+ for (let j = 0; j < k; j++) {
2218
+ const x1 = -10 + ((W + 20) * (j + 0.5)) / k;
2219
+ const x2 = -10 + ((W + 20) * (j + 1)) / k;
2220
+ d += `Q${n(x1)} ${n(y + (j % 2 ? a : -a) * (0.6 + rnd() * 0.6))} ${n(x2)} ${n(y + (rnd() - 0.5) * 10)}`;
2221
+ }
2222
+ if (i === 4) d = `M-10 ${W}V${GROUND + 4}Q80 ${GROUND - 8} 150 ${GROUND}T${W + 10} ${GROUND - 2}V${W}Z`;
2223
+ else d += `V${W}Z`;
2224
+ s += `<path d="${d}" fill="${pal[i + 2]}" filter="url(#${p}s)"/>`;
2225
+ if (i === 2 || i === 3) {
2226
+ // pinos de papel en la capa
2227
+ for (let t = 0; t < 2; t++) {
2228
+ const tx = rnd() < 0.5 ? 20 + rnd() * 70 : 210 + rnd() * 70;
2229
+ const ty = y + 8;
2230
+ s += `<path d="M${n(tx)} ${n(ty - 34)}L${n(tx + 12)} ${n(ty)}H${n(tx - 12)}Z" fill="${pal[i + 3] || pal[6]}" filter="url(#${p}s)"/>`;
2231
+ }
2232
+ }
2233
+ }
2234
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2235
+ return s;
2236
+ },
2237
+
2238
+ lowpoly(p, rnd) {
2239
+ const sky0 = '#3d5a80';
2240
+ const sky1 = '#f6bd60';
2241
+ const jit = (c) => mix(c, rnd() < 0.5 ? '#ffffff' : '#000000', rnd() * 0.07);
2242
+ let s = facets(rnd, 0, 0, W, 190, 6, 4, (x, y) => jit(mix(sky0, sky1, Math.min(1, y / 180))));
2243
+ // sol octogonal
2244
+ const sx = 60 + rnd() * 180;
2245
+ let sun = '';
2246
+ for (let i = 0; i < 8; i++) {
2247
+ const a0 = (i / 8) * Math.PI * 2;
2248
+ const a1 = ((i + 1) / 8) * Math.PI * 2;
2249
+ sun += `<path d="M${n(sx)} 64L${n(sx + Math.cos(a0) * 24)} ${n(64 + Math.sin(a0) * 24)}L${n(sx + Math.cos(a1) * 24)} ${n(64 + Math.sin(a1) * 24)}Z" fill="${i % 2 ? '#fff1c1' : '#ffe39a'}" stroke="${i % 2 ? '#fff1c1' : '#ffe39a'}" stroke-width=".5"/>`;
2250
+ }
2251
+ s += sun;
2252
+ // montañas facetadas: lado iluminado y lado en sombra, con nieve
2253
+ const mount = (px, py, half, base, lit, dark) => {
2254
+ const P = [px, py];
2255
+ const BL = [px - half, base];
2256
+ const BR = [px + half * (0.9 + rnd() * 0.3), base];
2257
+ const L1 = [px - half * 0.5 + (rnd() - 0.5) * 10, (py + base) / 2 + (rnd() - 0.5) * 10];
2258
+ const R1 = [px + half * 0.5 + (rnd() - 0.5) * 10, (py + base) / 2 + (rnd() - 0.5) * 10];
2259
+ const C = [px + (rnd() - 0.5) * half * 0.3, base - (base - py) * 0.3];
2260
+ const T = (a, b, c, col) => `<path d="M${n(a[0])} ${n(a[1])}L${n(b[0])} ${n(b[1])}L${n(c[0])} ${n(c[1])}Z" fill="${col}" stroke="${col}" stroke-width=".6" stroke-linejoin="round"/>`;
2261
+ const at = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
2262
+ return T(P, L1, C, lit) + T(L1, BL, C, mix(lit, dark, 0.3)) + T(P, C, R1, mix(lit, dark, 0.6)) + T(R1, C, BR, dark) + T(BL, BR, C, mix(lit, dark, 0.45))
2263
+ + T(P, at(P, L1, 0.38), at(P, C, 0.3), '#f4f6fb') + T(P, at(P, C, 0.3), at(P, R1, 0.38), '#c9d2e3');
2264
+ };
2265
+ s += mount(70 + rnd() * 30, 92, 90, 214, '#7d8fb3', '#46557a');
2266
+ s += mount(215 + rnd() * 30, 80, 100, 214, '#8a9cc0', '#4b5b82');
2267
+ s += mount(150, 126, 70, 214, '#6b7fa6', '#3a4870');
2268
+ // suelo de facetas verdes
2269
+ s += facets(rnd, 0, 206, W, W - 206, 6, 3, (x, y) => jit(mix('#8cc084', '#4f7d4a', (y - 206) / 94)));
2270
+ // pinos de dos caras
2271
+ for (let i = 0; i < 4; i++) {
2272
+ const tx = i < 2 ? 18 + i * 40 + rnd() * 14 : 214 + (i - 2) * 40 + rnd() * 14;
2273
+ const ty = GROUND - 8 + rnd() * 10;
2274
+ const h = 44 + rnd() * 20;
2275
+ s += `<path d="M${n(tx)} ${n(ty - h)}L${n(tx)} ${n(ty)}L${n(tx - 14)} ${n(ty)}Z" fill="#3f7a4f"/><path d="M${n(tx)} ${n(ty - h)}L${n(tx + 14)} ${n(ty)}L${n(tx)} ${n(ty)}Z" fill="#2a5a3a"/><path d="M${n(tx - 2)} ${n(ty)}h4v8h-4Z" fill="#5a3a2a"/>`;
2276
+ }
2277
+ return s;
2278
+ },
2279
+
2280
+ marble(p, rnd) {
2281
+ let s = `<defs><linearGradient id="${p}wl" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#cfc6b6"/><stop offset="1" stop-color="#e4ddd0"/></linearGradient>`
2282
+ + `<radialGradient id="${p}sp" cx=".5" cy="0" r="1"><stop offset="0" stop-color="#fffaf0" stop-opacity=".75"/><stop offset=".6" stop-color="#fffaf0" stop-opacity="0"/></radialGradient>`
2283
+ + `<linearGradient id="${p}cl" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#d6d0c4"/><stop offset=".35" stop-color="#f6f2ea"/><stop offset="1" stop-color="#bfb7a8"/></linearGradient>`
2284
+ + `<filter id="${p}v" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency=".012 .04" numOctaves="4" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .42 0 0 0 0 .4 0 0 0 0 .38 -5 0 0 0 .55"/><feComposite in2="SourceGraphic" operator="in"/></filter></defs>`;
2285
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}wl)"/>`;
2286
+ // hornacina en arco detrás del ot
2287
+ s += `<path d="M76 ${GROUND}V96A74 74 0 0 1 224 96V${GROUND}Z" fill="#bdb3a2"/><path d="M84 ${GROUND}V98A66 66 0 0 1 216 98V${GROUND}Z" fill="#c9c0b0"/>`;
2288
+ s += `<path d="M76 96A74 74 0 0 1 224 96" fill="none" stroke="#ece6da" stroke-width="3"/>`;
2289
+ // columnas estriadas con capitel jónico y basa
2290
+ const column = (x) => {
2291
+ let f = '';
2292
+ for (let i = 1; i < 5; i++) f += `M${x + i * 7} 46V${GROUND + 14}`;
2293
+ return `<rect x="${x}" y="46" width="35" height="${GROUND - 32}" fill="url(#${p}cl)"/><path d="${f}" stroke="#a89f90" stroke-width="1.2"/>`
2294
+ + `<rect x="${x - 6}" y="34" width="47" height="12" fill="#ece6da"/><circle cx="${x - 3}" cy="44" r="7" fill="#ece6da" stroke="#a89f90"/><circle cx="${x + 38}" cy="44" r="7" fill="#ece6da" stroke="#a89f90"/><circle cx="${x - 3}" cy="44" r="2.5" fill="#a89f90"/><circle cx="${x + 38}" cy="44" r="2.5" fill="#a89f90"/><rect x="${x - 8}" y="26" width="51" height="8" fill="#f2ede4"/>`
2295
+ + `<rect x="${x - 6}" y="${GROUND + 14}" width="47" height="8" fill="#ece6da"/><rect x="${x - 9}" y="${GROUND + 22}" width="53" height="6" fill="#d6cfc2"/>`;
2296
+ };
2297
+ s += column(16) + column(249);
2298
+ // foco cenital
2299
+ s += `<path d="M110 0H190L250 ${GROUND}H50Z" fill="url(#${p}sp)"/>`;
2300
+ // suelo de damero en perspectiva
2301
+ s += `<rect y="${GROUND + 28}" width="${W}" height="${W - GROUND - 28}" fill="#e8e2d6"/>`;
2302
+ let ck = '';
2303
+ for (let i = -6; i < 6; i++) ck += `M${150 + i * 30} ${GROUND + 28}L${150 + i * 46} ${W}`;
2304
+ s += `<path d="${ck}M0 ${GROUND + 40}H${W}" stroke="#8a8274" stroke-width="1" opacity=".5"/>`;
2305
+ // pedestal con placa
2306
+ s += `<rect x="72" y="${GROUND}" width="156" height="9" fill="#f4f0e8"/><rect x="82" y="${GROUND + 9}" width="136" height="${W - GROUND - 9}" fill="#e6e0d4"/><rect x="82" y="${GROUND + 9}" width="136" height="4" fill="#cfc7b8"/>`;
2307
+ s += `<rect x="72" y="${GROUND}" width="156" height="${W - GROUND}" filter="url(#${p}v)" opacity=".8"/>`;
2308
+ s += `<rect x="122" y="${GROUND + 24}" width="56" height="20" rx="1.5" fill="#b8923e"/><rect x="124" y="${GROUND + 26}" width="52" height="16" fill="none" stroke="#7a5e22" stroke-width=".8"/>`;
2309
+ s += `<text x="150" y="${GROUND + 37}" text-anchor="middle" font-family="Georgia,serif" font-size="8" letter-spacing="1" fill="#4a3810">MMXXVI</text>`;
2310
+ s += `<rect x="16" y="46" width="35" height="${GROUND - 32}" filter="url(#${p}v)" opacity=".6"/><rect x="249" y="46" width="35" height="${GROUND - 32}" filter="url(#${p}v)" opacity=".6"/>`;
2311
+ return s;
2312
+ },
2313
+
2314
+ candy(p, rnd) {
2315
+ const sprinkle = ['#ff4f8b', '#ffd23f', '#3ec1d3', '#7ed957', '#a77dff', '#ffffff', '#ff8c42'];
2316
+ let s = `<defs><pattern id="${p}st" width="22" height="22" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="22" height="22" fill="#ffd6e8"/><rect width="9" height="22" fill="#ffc2dc"/></pattern>`
2317
+ + `<radialGradient id="${p}gl" cx=".35" cy=".3" r=".7"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>`;
2318
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}st)"/>`;
2319
+ // chupetines espirales
2320
+ const lolly = (x, y, r, c) => {
2321
+ let sp = `M${x} ${y}`;
2322
+ for (let a = 0; a < Math.PI * 6; a += 0.3) sp += `L${n(x + Math.cos(a) * (a / (Math.PI * 6)) * r)} ${n(y + Math.sin(a) * (a / (Math.PI * 6)) * r)}`;
2323
+ return `<rect x="${x - 3}" y="${y}" width="6" height="${GROUND - y}" rx="3" fill="#fff" stroke="#e8b8cc" stroke-width="1"/><circle cx="${x}" cy="${y}" r="${r}" fill="${c}"/>`
2324
+ + `<path d="${sp}" fill="none" stroke="#fff" stroke-width="${n(r * 0.16)}" stroke-linecap="round"/><circle cx="${x}" cy="${y}" r="${r}" fill="url(#${p}gl)"/>`;
2325
+ };
2326
+ const left = rnd() < 0.5;
2327
+ s += lolly(left ? 48 : 252, 92 + rnd() * 20, 32, '#ff6fa8');
2328
+ s += lolly(left ? 256 : 44, 130 + rnd() * 20, 24, '#7fd3e8');
2329
+ // caramelos envueltos flotando
2330
+ for (let i = 0; i < 2; i++) {
2331
+ const x = 90 + i * 120 + rnd() * 20;
2332
+ const y = 30 + rnd() * 30;
2333
+ const c = sprinkle[Math.floor(rnd() * 5)];
2334
+ s += `<g transform="rotate(${Math.floor(rnd() * 60 - 30)} ${n(x)} ${n(y)})"><path d="M${n(x - 12)} ${n(y)}l-10 -8v16Z M${n(x + 12)} ${n(y)}l10 -8v16Z" fill="${c}" opacity=".8"/><ellipse cx="${n(x)}" cy="${n(y)}" rx="13" ry="9" fill="${c}"/><ellipse cx="${n(x - 4)}" cy="${n(y - 3)}" rx="5" ry="2.5" fill="#fff" opacity=".6"/></g>`;
2335
+ }
2336
+ // pastel: bizcocho de chocolate con glaseado que chorrea
2337
+ s += `<rect y="${GROUND + 10}" width="${W}" height="${W - GROUND - 10}" fill="#8b5a3c"/><rect y="${GROUND + 40}" width="${W}" height="6" fill="#ffe0ec"/>`;
2338
+ let ic = `M0 ${GROUND - 4}H${W}V${GROUND + 12}`;
2339
+ for (let x = W; x > 0; x -= 20 + rnd() * 16) {
2340
+ const h = 6 + rnd() * 22;
2341
+ ic += `L${n(x - 4)} ${GROUND + 12}V${n(GROUND + 12 + h)}a5 5 0 0 1 -10 0V${GROUND + 12}`;
2342
+ }
2343
+ s += `<path d="${ic}L0 ${GROUND + 12}Z" fill="#ff9ec7"/><path d="M0 ${GROUND - 2}H${W}" stroke="#ffd0e4" stroke-width="3"/>`;
2344
+ // grageas en el aire y sobre el glaseado
2345
+ let sp = '';
2346
+ for (let i = 0; i < 70; i++) {
2347
+ const x = rnd() * W;
2348
+ const y = i < 40 ? rnd() * (GROUND - 10) : GROUND - 4 + rnd() * 14;
2349
+ sp += `<rect x="${n(x)}" y="${n(y)}" width="7" height="2.6" rx="1.3" fill="${sprinkle[Math.floor(rnd() * sprinkle.length)]}" transform="rotate(${Math.floor(rnd() * 180)} ${n(x)} ${n(y)})"/>`;
2350
+ }
2351
+ s += sp;
2352
+ return s;
2353
+ },
2354
+
2355
+ holo(p, rnd) {
2356
+ const rb = ['#ff8fd0', '#ffe08a', '#8affc0', '#8ad0ff', '#b98aff'];
2357
+ let s = `<defs><linearGradient id="${p}bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#140a2e"/><stop offset=".55" stop-color="#2a1458"/><stop offset="1" stop-color="#0d1a3a"/></linearGradient>`
2358
+ + `<linearGradient id="${p}rb" x1="0" y1="0" x2="1" y2="0">${rb.map((c, i) => `<stop offset="${n(i / (rb.length - 1))}" stop-color="${c}"/>`).join('')}</linearGradient>`
2359
+ + `<linearGradient id="${p}fl" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#0d0820" stop-opacity=".85"/></linearGradient>`
2360
+ + `<filter id="${p}gw" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="1.6" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
2361
+ s += `<rect width="${W}" height="${W}" fill="url(#${p}bg)"/>`;
2362
+ // haces tornasolados en diagonal
2363
+ for (let i = 0; i < 3; i++) {
2364
+ const x = -60 + i * 120 + rnd() * 40;
2365
+ s += `<path d="M${n(x)} 0h${n(30 + rnd() * 30)}l120 ${GROUND}h${n(-40 - rnd() * 30)}Z" fill="url(#${p}rb)" opacity=".13"/>`;
2366
+ }
2367
+ // prismas flotando
2368
+ for (let i = 0; i < 3; i++) {
2369
+ const x = i === 1 ? 150 + (rnd() - 0.5) * 60 : i ? 250 + rnd() * 20 : 30 + rnd() * 20;
2370
+ const y = i === 1 ? 30 + rnd() * 10 : 90 + rnd() * 70;
2371
+ const r = 10 + rnd() * 8;
2372
+ s += `<path d="M${n(x)} ${n(y - r)}L${n(x + r * 0.7)} ${n(y)}L${n(x)} ${n(y + r * 1.2)}L${n(x - r * 0.7)} ${n(y)}Z" fill="url(#${p}rb)" opacity=".85"/><path d="M${n(x)} ${n(y - r)}L${n(x + r * 0.7)} ${n(y)}L${n(x)} ${n(y + r * 1.2)}Z" fill="#fff" opacity=".25"/>`;
2373
+ }
2374
+ // piso tornasol con rejilla en perspectiva
2375
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="url(#${p}rb)" opacity=".75"/><rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="url(#${p}fl)"/>`;
2376
+ let grid = '';
2377
+ for (let i = -8; i <= 8; i++) grid += `M${150 + i * 18} ${GROUND}L${150 + i * 60} ${W}`;
2378
+ for (let j = 1; j < 6; j++) grid += `M0 ${n(GROUND + (W - GROUND) * (j / 6) ** 1.6)}H${W}`;
2379
+ s += `<path d="${grid}" stroke="#fff" stroke-width=".7" opacity=".35"/><path d="M0 ${GROUND}H${W}" stroke="#fff" stroke-width="1.5" opacity=".8"/>`;
2380
+ // purpurina y destellos
2381
+ let gl = '';
2382
+ for (let i = 0; i < 120; i++) gl += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * GROUND)}" r="${n(0.4 + rnd() * 1.1)}" fill="${rb[Math.floor(rnd() * rb.length)]}" opacity="${n(0.4 + rnd() * 0.6)}"/>`;
2383
+ s += gl;
2384
+ let sp = '';
2385
+ for (let i = 0; i < 9; i++) {
2386
+ const x = rnd() * W;
2387
+ const y = rnd() * (GROUND - 20);
2388
+ const r = 3 + rnd() * 6;
2389
+ sp += `M${n(x)} ${n(y - r)}Q${n(x)} ${n(y)} ${n(x + r)} ${n(y)}Q${n(x)} ${n(y)} ${n(x)} ${n(y + r)}Q${n(x)} ${n(y)} ${n(x - r)} ${n(y)}Q${n(x)} ${n(y)} ${n(x)} ${n(y - r)}Z`;
2390
+ }
2391
+ s += `<path d="${sp}" fill="#fff" filter="url(#${p}gw)"/>`;
2392
+ return s;
2393
+ },
2394
+
2395
+ sumie(p, rnd) {
2396
+ const ink = '#1d1b19';
2397
+ let s = `<defs>${bdGrain(`${p}g`, rnd, [0.55, 0.5, 0.42], 1.3, -0.74, 0.6)}<filter id="${p}b" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.5"/></filter>`
2398
+ + `<filter id="${p}r" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency=".05" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="5" xChannelSelector="R" yChannelSelector="G"/></filter>`
2399
+ + `<linearGradient id="${p}m" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${ink}" stop-opacity=".45"/><stop offset="1" stop-color="${ink}" stop-opacity="0"/></linearGradient>`
2400
+ + `<linearGradient id="${p}m2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${ink}" stop-opacity=".2"/><stop offset="1" stop-color="${ink}" stop-opacity="0"/></linearGradient></defs>`;
2401
+ s += `<rect width="${W}" height="${W}" fill="#f3eee2"/>`;
2402
+ // montañas en aguada que se pierden en la niebla
2403
+ const range = (base, amp, fill) => {
2404
+ let d = `M-10 ${base + 40}`;
2405
+ for (let x = -10; x <= W + 10; x += 25) d += `L${x} ${n(base - amp * (0.3 + rnd() * 0.7) * (x % 50 ? 0.6 : 1))}`;
2406
+ return `<path d="${d}L${W + 10} ${base + 40}Z" fill="url(#${fill})" filter="url(#${p}b)"/>`;
2407
+ };
2408
+ s += range(120, 50, `${p}m2`) + range(170, 60, `${p}m`);
2409
+ // ensō detrás del ot
2410
+ const a0 = rnd() * Math.PI * 2;
2411
+ const R = 98;
2412
+ const end = a0 + Math.PI * 1.82;
2413
+ const pt = (a) => `${n(150 + Math.cos(a) * R)} ${n(140 + Math.sin(a) * R)}`;
2414
+ s += `<g filter="url(#${p}r)"><path d="M${pt(a0)}A${R} ${R} 0 1 1 ${pt(end)}" fill="none" stroke="${ink}" stroke-width="11" stroke-linecap="round" opacity=".82"/>`
2415
+ + `<path d="M${pt(a0 + 0.3)}A${R + 4} ${R + 4} 0 0 1 ${pt(a0 + 1.6)}" fill="none" stroke="#f3eee2" stroke-width="1.6" stroke-dasharray="14 6 30 4" opacity=".8"/></g>`;
2416
+ // bambú a un lado
2417
+ const bx = rnd() < 0.5 ? 30 : 262;
2418
+ let bam = '';
2419
+ for (let k = 0; k < 2; k++) {
2420
+ const x = bx + k * 14;
2421
+ for (let y = 10 + k * 30; y < GROUND; y += 34) bam += `<rect x="${x - 4}" y="${y}" width="8" height="31" rx="2"/>`;
2422
+ }
2423
+ let lv = '';
2424
+ for (let i = 0; i < 7; i++) {
2425
+ const x = bx + (rnd() - 0.3) * 20;
2426
+ const y = 30 + rnd() * 140;
2427
+ const dir = rnd() < 0.5 ? -1 : 1;
2428
+ lv += `<path d="M${n(x)} ${n(y)}q${n(dir * 14)} ${n(-4)} ${n(dir * 30)} ${n(6 + rnd() * 6)}q${n(-dir * 14)} ${n(2)} ${n(-dir * 30)} ${n(-6 - rnd() * 6)}Z"/>`;
2429
+ }
2430
+ s += `<g fill="${ink}" opacity=".78" filter="url(#${p}r)">${bam}${lv}</g>`;
2431
+ // pincelada del suelo y caligrafía vertical
2432
+ s += `<path d="M20 ${GROUND + 6}C80 ${GROUND - 4} 200 ${GROUND - 2} 286 ${GROUND + 4}L282 ${GROUND + 10}C200 ${GROUND + 6} 100 ${GROUND + 12} 24 ${GROUND + 12}Z" fill="${ink}" opacity=".7" filter="url(#${p}r)"/>`;
2433
+ const cx = bx < 150 ? 270 : 26;
2434
+ let cal = '';
2435
+ for (let i = 0; i < 4; i++) {
2436
+ const y = 26 + i * 22;
2437
+ cal += `M${cx - 6} ${y}h12M${cx} ${y - 5}v13M${cx - 5} ${y + 6}l${n(4 + rnd() * 4)} ${n(3 + rnd() * 3)}`;
2438
+ }
2439
+ s += `<path d="${cal}" stroke="${ink}" stroke-width="2.4" stroke-linecap="round" fill="none" opacity=".75"/>`;
2440
+ // sello rojo
2441
+ s += `<g transform="rotate(-4 ${cx} 128)"><rect x="${cx - 10}" y="118" width="20" height="22" rx="2" fill="#b8352a"/><path d="M${cx - 6} 123h12M${cx} 123v13M${cx - 6} 129h12M${cx - 5} 136h10" stroke="#f3eee2" stroke-width="1.6" fill="none"/></g>`;
2442
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2443
+ return s;
2444
+ },
2445
+
2446
+ tarot(p, rnd) {
2447
+ const navy = '#1c2a5a';
2448
+ const gold = '#c9a23a';
2449
+ const cream = '#efe3c4';
2450
+ const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI'];
2451
+ let s = `<defs>${bdGrain(`${p}g`, rnd, [0.35, 0.25, 0.1], 1.2, -0.66)}</defs>`;
2452
+ s += `<rect width="${W}" height="${W}" fill="${cream}"/><rect x="12" y="12" width="${W - 24}" height="${W - 24}" rx="6" fill="${navy}"/>`;
2453
+ // estrellas
2454
+ let st = '';
2455
+ for (let i = 0; i < 40; i++) st += `<circle cx="${n(18 + rnd() * (W - 36))}" cy="${n(46 + rnd() * 180)}" r="${n(0.5 + rnd() * 0.9)}"/>`;
2456
+ s += `<g fill="${cream}" opacity=".8">${st}</g>`;
2457
+ let big = '';
2458
+ for (let i = 0; i < 7; i++) big += star5(24 + rnd() * (W - 48), 52 + rnd() * 150, 3 + rnd() * 3);
2459
+ s += `<path d="${big}" fill="${gold}"/>`;
2460
+ // anillos radiantes detrás del ot
2461
+ s += `<g fill="none" stroke="${gold}" opacity=".35"><circle cx="150" cy="150" r="74" stroke-width="1.2"/><circle cx="150" cy="150" r="86" stroke-width=".8" stroke-dasharray="2 5"/><circle cx="150" cy="150" r="100" stroke-width=".8"/></g>`;
2462
+ // luna creciente y sol con cara
2463
+ const left = rnd() < 0.5;
2464
+ const mx = left ? 58 : 242;
2465
+ const sx = left ? 242 : 58;
2466
+ s += `<path d="M${mx} 52A24 24 0 1 0 ${mx} 100A19 19 0 1 1 ${mx} 52Z" fill="${gold}" transform="rotate(${left ? -20 : 20} ${mx} 76)"/>`;
2467
+ let rays = '';
2468
+ for (let i = 0; i < 16; i++) {
2469
+ const a = (i / 16) * Math.PI * 2;
2470
+ const r1 = i % 2 ? 30 : 36;
2471
+ rays += `M${n(sx + Math.cos(a - 0.12) * 19)} ${n(76 + Math.sin(a - 0.12) * 19)}L${n(sx + Math.cos(a) * r1)} ${n(76 + Math.sin(a) * r1)}L${n(sx + Math.cos(a + 0.12) * 19)} ${n(76 + Math.sin(a + 0.12) * 19)}Z`;
2472
+ }
2473
+ s += `<path d="${rays}" fill="${gold}"/><circle cx="${sx}" cy="76" r="18" fill="${gold}" stroke="${navy}" stroke-width="1"/>`;
2474
+ s += `<circle cx="${sx - 6}" cy="73" r="1.8" fill="${navy}"/><circle cx="${sx + 6}" cy="73" r="1.8" fill="${navy}"/><path d="M${sx - 6} 82Q${sx} 87 ${sx + 6} 82" fill="none" stroke="${navy}" stroke-width="1.4" stroke-linecap="round"/>`;
2475
+ // tierra y agua estilizadas
2476
+ s += `<path d="M12 ${GROUND}Q80 ${GROUND - 16} 150 ${GROUND}T${W - 12} ${GROUND - 4}V${W - 40}H12Z" fill="#2f7f7a"/><path d="M12 ${GROUND}Q80 ${GROUND - 16} 150 ${GROUND}T${W - 12} ${GROUND - 4}" fill="none" stroke="${gold}" stroke-width="1.6"/>`;
2477
+ let wv = '';
2478
+ for (let y = GROUND + 12; y < W - 42; y += 8) wv += `M20 ${y}q10 -4 20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0t20 0`;
2479
+ s += `<path d="${wv}" fill="none" stroke="${cream}" stroke-width="1" opacity=".35"/>`;
2480
+ // filete dorado doble, cartelas con número y ornamento
2481
+ s += `<rect x="18" y="18" width="${W - 36}" height="${W - 36}" rx="4" fill="none" stroke="${gold}" stroke-width="2.4"/><rect x="23" y="23" width="${W - 46}" height="${W - 46}" rx="3" fill="none" stroke="${gold}" stroke-width=".8"/>`;
2482
+ s += `<rect x="110" y="16" width="80" height="22" rx="2" fill="${cream}" stroke="${gold}" stroke-width="1.6"/>`;
2483
+ s += `<text x="150" y="33" text-anchor="middle" font-family="Georgia,'Times New Roman',serif" font-weight="bold" font-size="15" letter-spacing="2" fill="${navy}">${ROMAN[Math.floor(rnd() * ROMAN.length)]}</text>`;
2484
+ s += `<rect x="70" y="${W - 40}" width="160" height="22" rx="2" fill="${cream}" stroke="${gold}" stroke-width="1.6"/><path d="${star5(150, W - 29, 5)}${star5(120, W - 29, 3)}${star5(180, W - 29, 3)}" fill="${navy}"/><path d="M84 ${W - 29}H108M192 ${W - 29}H216" stroke="${navy}" stroke-width="1"/>`;
2485
+ for (const [x, y] of [[18, 18], [W - 18, 18], [18, W - 18], [W - 18, W - 18]]) s += `<path d="M${x} ${y - 6}L${x + 6} ${y}L${x} ${y + 6}L${x - 6} ${y}Z" fill="${gold}"/>`;
2486
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2487
+ return s;
2488
+ },
2489
+
2490
+ retro70(p, rnd) {
2491
+ const cols = ['#4a2c1e', '#8a4b2a', '#c94f2c', '#e8762c', '#f2b33d'];
2492
+ let s = `<defs>${bdGrain(`${p}g`, rnd, [0.35, 0.22, 0.1], 1.3, -0.7)}</defs>`;
2493
+ s += `<rect width="${W}" height="${W}" fill="#f6e8c8"/>`;
2494
+ // rayos de sol desde el horizonte
2495
+ let rays = '';
2496
+ const ox = 150;
2497
+ for (let i = 0; i < 24; i += 2) {
2498
+ const a0 = Math.PI + (i / 24) * Math.PI;
2499
+ const a1 = Math.PI + ((i + 1) / 24) * Math.PI;
2500
+ rays += `M${ox} ${GROUND}L${n(ox + Math.cos(a0) * 400)} ${n(GROUND + Math.sin(a0) * 400)}L${n(ox + Math.cos(a1) * 400)} ${n(GROUND + Math.sin(a1) * 400)}Z`;
2501
+ }
2502
+ s += `<path d="${rays}" fill="#f2c46d" opacity=".55"/>`;
2503
+ // arcoíris setentero
2504
+ cols.forEach((c, i) => {
2505
+ const r = 132 - i * 17;
2506
+ s += `<path d="M${ox - r} ${GROUND}A${r} ${r} 0 0 1 ${ox + r} ${GROUND}" fill="none" stroke="${c}" stroke-width="14"/>`;
2507
+ });
2508
+ s += `<path d="M${ox - 48} ${GROUND}A48 48 0 0 1 ${ox + 48} ${GROUND}Z" fill="#f6e8c8"/>`;
2509
+ // nubes redondas en dos tonos
2510
+ for (let i = 0; i < 2; i++) {
2511
+ const x = i ? 238 + rnd() * 30 : 34 + rnd() * 30;
2512
+ const y = 40 + rnd() * 40;
2513
+ s += `<path d="M${n(x - 26)} ${n(y + 8)}a9 9 0 0 1 10 -12a13 13 0 0 1 24 -2a9 9 0 0 1 18 14Z" fill="#fff8e8" stroke="#8a4b2a" stroke-width="2"/>`;
2514
+ }
2515
+ // suelo con ondas
2516
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#8a4b2a"/>`;
2517
+ ['#c94f2c', '#e8762c', '#f2b33d'].forEach((c, i) => {
2518
+ const y = GROUND + 12 + i * 12;
2519
+ s += `<path d="M-10 ${y}q20 -8 40 0t40 0t40 0t40 0t40 0t40 0t40 0t40 0" fill="none" stroke="${c}" stroke-width="5"/>`;
2520
+ });
2521
+ // margaritas
2522
+ const daisy = (x, y, r) => {
2523
+ let pe = '';
2524
+ for (let i = 0; i < 8; i++) pe += `<ellipse cx="${n(x)}" cy="${n(y - r)}" rx="${n(r * 0.42)}" ry="${n(r * 0.75)}" transform="rotate(${i * 45} ${n(x)} ${n(y)})"/>`;
2525
+ return `<path d="M${n(x)} ${n(y)}V${GROUND + 4}" stroke="#6b7a3a" stroke-width="3"/><g fill="#fffaf0" stroke="#4a2c1e" stroke-width="1.2">${pe}</g><circle cx="${n(x)}" cy="${n(y)}" r="${n(r * 0.45)}" fill="#f2b33d" stroke="#4a2c1e" stroke-width="1.2"/>`;
2526
+ };
2527
+ s += daisy(28 + rnd() * 12, GROUND - 28, 11) + daisy(64 + rnd() * 10, GROUND - 12, 8) + daisy(262 + rnd() * 12, GROUND - 32, 12) + daisy(228 + rnd() * 8, GROUND - 10, 7);
2528
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2529
+ return s;
2530
+ },
2531
+
2532
+ mosaic(p, rnd) {
2533
+ const T = 12;
2534
+ const BAND = 18;
2535
+ const sx = 60 + rnd() * 180;
2536
+ const sy = 64;
2537
+ const col = (x, y) => {
2538
+ if ((x - sx) ** 2 + (y - sy) ** 2 < 26 ** 2) return (x - sx) ** 2 + (y - sy) ** 2 < 14 ** 2 ? '#e6c26a' : '#d98a3a';
2539
+ const hill = GROUND - 30 + Math.sin(x / 40 + sx) * 14;
2540
+ if (y > GROUND) return rnd() < 0.5 ? '#b5452e' : '#a03c2a';
2541
+ if (y > hill) return rnd() < 0.5 ? '#6a8a4a' : '#5a7a3e';
2542
+ return mix('#2c4a6e', '#8ab0c0', Math.min(1, y / 200));
2543
+ };
2544
+ let s = `<rect width="${W}" height="${W}" fill="#b8ad98"/>`;
2545
+ let tiles = '';
2546
+ for (let y = BAND; y < W - BAND; y += T) {
2547
+ for (let x = BAND; x < W - BAND; x += T) {
2548
+ const c = mix(col(x + T / 2, y + T / 2), rnd() < 0.5 ? '#ffffff' : '#000000', rnd() * 0.12);
2549
+ const j = () => (rnd() - 0.5) * 1.6;
2550
+ tiles += `<rect x="${n(x + 1 + j())}" y="${n(y + 1 + j())}" width="${n(T - 2 + j() * 0.5)}" height="${n(T - 2 + j() * 0.5)}" fill="${c}" transform="rotate(${n((rnd() - 0.5) * 6)} ${x + T / 2} ${y + T / 2})"/>`;
2551
+ }
2552
+ }
2553
+ s += tiles;
2554
+ // greca en la cenefa
2555
+ s += `<path d="M0 0H${W}V${W}H0ZM${BAND} ${BAND}V${W - BAND}H${W - BAND}V${BAND}Z" fill-rule="evenodd" fill="#efe4cc"/>`;
2556
+ const key = (x) => `M${x} 14V4H${x + 12}V10H${x + 6}V8`;
2557
+ let k = '';
2558
+ for (let x = 2; x < W - 10; x += 16) k += key(x);
2559
+ const band = `<path d="${k}" fill="none" stroke="#3a3530" stroke-width="2.2"/>`;
2560
+ s += band + `<g transform="translate(0 ${W - 18})">${band}</g><g transform="translate(18 0) rotate(90)">${band}</g><g transform="translate(${W} 0) rotate(90)">${band}</g>`;
2561
+ s += `<path d="M${BAND} ${BAND}H${W - BAND}V${W - BAND}H${BAND}Z" fill="none" stroke="#3a3530" stroke-width="2"/>`;
2562
+ return s;
2563
+ },
2564
+
2565
+ neonsign(p, rnd) {
2566
+ const tubes = ['#ff4fd8', '#3ef0ff', '#ffe14f', '#7dff6a', '#ff6a3d'];
2567
+ const t1 = tubes[Math.floor(rnd() * tubes.length)];
2568
+ const t2 = tubes.filter((c) => c !== t1)[Math.floor(rnd() * (tubes.length - 1))];
2569
+ let s = `<defs><pattern id="${p}br" width="32" height="18" patternUnits="userSpaceOnUse"><rect width="32" height="18" fill="#140c10"/><rect x="1" y="1" width="30" height="7.5" rx="1" fill="#2e1c22"/><rect x="-15" y="10" width="30" height="7.5" rx="1" fill="#2a191f"/><rect x="17" y="10" width="30" height="7.5" rx="1" fill="#33202a"/></pattern>`
2570
+ + `<radialGradient id="${p}vg" cx=".5" cy=".35" r=".75"><stop offset="0" stop-color="${t1}" stop-opacity=".22"/><stop offset=".55" stop-color="#000" stop-opacity=".2"/><stop offset="1" stop-color="#000" stop-opacity=".7"/></radialGradient>`
2571
+ + `<filter id="${p}n" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="2.5" result="a"/><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="a"/><feMergeNode in="SourceGraphic"/></feMerge></filter>`
2572
+ + `<filter id="${p}rf" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="5 2"/></filter></defs>`;
2573
+ s += `<rect width="${W}" height="${GROUND}" fill="url(#${p}br)"/><rect width="${W}" height="${GROUND}" fill="url(#${p}vg)"/>`;
2574
+ // letrero principal con su cable
2575
+ s += `<path d="M110 0V22M190 0V22" stroke="#0a0608" stroke-width="1.5"/>`;
2576
+ const font = 'font-family="\'Brush Script MT\',\'Segoe Script\',\'URW Chancery L\',cursive" font-style="italic" font-weight="bold" font-size="46" text-anchor="middle"';
2577
+ s += `<g filter="url(#${p}n)"><text x="150" y="66" ${font} fill="none" stroke="${t1}" stroke-width="3.2" stroke-linejoin="round">7ots</text></g>`;
2578
+ s += `<text x="150" y="66" ${font} fill="none" stroke="#fff" stroke-width="1" opacity=".75">7ots</text>`;
2579
+ // corazón y flecha de neón a los lados
2580
+ const left = rnd() < 0.5;
2581
+ const hx = left ? 46 : 254;
2582
+ const ax = left ? 254 : 46;
2583
+ s += `<g filter="url(#${p}n)" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M${hx} 138C${hx - 26} 120 ${hx - 16} 98 ${hx} 110C${hx + 16} 98 ${hx + 26} 120 ${hx} 138Z" stroke="${t2}" stroke-width="3"/>`
2584
+ + `<path d="M${ax + (left ? 22 : -22)} 150H${ax - (left ? 18 : -18)}M${ax - (left ? 6 : -6)} 140L${ax - (left ? 18 : -18)} 150L${ax - (left ? 6 : -6)} 160" stroke="${t1}" stroke-width="3"/></g>`;
2585
+ s += `<path d="M${hx} 138C${hx - 26} 120 ${hx - 16} 98 ${hx} 110C${hx + 16} 98 ${hx + 26} 120 ${hx} 138Z" fill="none" stroke="#fff" stroke-width=".9" opacity=".7"/>`;
2586
+ // vereda mojada con reflejos
2587
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#0c0a10"/><path d="M0 ${GROUND}H${W}" stroke="#2a2030" stroke-width="2"/>`;
2588
+ s += `<g filter="url(#${p}rf)" opacity=".5"><rect x="110" y="${GROUND + 8}" width="80" height="34" fill="${t1}"/><rect x="${hx - 12}" y="${GROUND + 10}" width="24" height="26" fill="${t2}"/><rect x="${ax - 14}" y="${GROUND + 14}" width="28" height="18" fill="${t1}"/></g>`;
2589
+ let wet = '';
2590
+ for (let i = 0; i < 10; i++) wet += `M${n(rnd() * W)} ${n(GROUND + 8 + rnd() * 50)}h${n(10 + rnd() * 30)}`;
2591
+ s += `<path d="${wet}" stroke="#fff" stroke-width="1" opacity=".15"/>`;
2592
+ return s;
2593
+ },
2594
+
2595
+ graffiti(p, rnd) {
2596
+ const cols = ['#ff2e63', '#08d9d6', '#f9ed32', '#7cfc00', '#ff8c00', '#9b5de5', '#00bbf9'];
2597
+ const pick = () => cols[Math.floor(rnd() * cols.length)];
2598
+ let s = `<defs>${bdGrain(`${p}g`, rnd, [0.3, 0.3, 0.3], 1.4, -0.72, 0.8)}<filter id="${p}sp" x="-20%" y="-20%" width="140%" height="140%"><feTurbulence type="fractalNoise" baseFrequency="1.2" numOctaves="1" seed="${Math.floor(rnd() * 900)}" result="n"/><feGaussianBlur in="SourceGraphic" stdDeviation="2.4" result="b"/><feColorMatrix in="n" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 0 0 0 -5.6" result="m"/><feComposite in="b" in2="m" operator="in" result="d"/><feMerge><feMergeNode in="d"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
2599
+ // muro de hormigón en paneles
2600
+ s += `<rect width="${W}" height="${GROUND}" fill="#b9b5ad"/><path d="M100 0V${GROUND}M200 0V${GROUND}M0 120H${W}" stroke="#8f8b84" stroke-width="2"/>`;
2601
+ // pieza de fondo: burbujas superpuestas con contorno negro, brillo y chorreones
2602
+ const c1 = pick();
2603
+ const c2 = cols.filter((c) => c !== c1)[Math.floor(rnd() * (cols.length - 1))];
2604
+ let blobs = '';
2605
+ const B = [[96, 112, 46], [150, 92, 54], [206, 116, 44]];
2606
+ for (const [x, y, r] of B) blobs += `<circle cx="${x}" cy="${y}" r="${r}"/>`;
2607
+ let drips = '';
2608
+ for (let i = 0; i < 7; i++) {
2609
+ const x = 70 + rnd() * 160;
2610
+ const h = 20 + rnd() * 60;
2611
+ drips += `M${n(x)} 140V${n(140 + h)}`;
2612
+ }
2613
+ s += `<g filter="url(#${p}sp)"><g fill="#111" transform="translate(5 5)">${blobs}</g><g fill="${c1}" stroke="#111" stroke-width="5">${blobs}</g><g fill="${c1}">${blobs}</g>`
2614
+ + `<path d="${drips}" stroke="${c1}" stroke-width="5" stroke-linecap="round"/>`
2615
+ + `<circle cx="150" cy="92" r="30" fill="${c2}" opacity=".85"/><path d="M80 96Q84 78 100 74M136 62Q144 50 160 50" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round"/></g>`;
2616
+ // tags y una corona
2617
+ const tag = (x, y, c) => {
2618
+ let d = `M${n(x)} ${n(y)}`;
2619
+ for (let i = 0; i < 6; i++) d += `q${n(4 + rnd() * 8)} ${n(-12 - rnd() * 10)} ${n(10 + rnd() * 6)} ${n(-2 + rnd() * 4)}t${n(6 + rnd() * 6)} ${n(10 + rnd() * 6)}`;
2620
+ return `<path d="${d}" fill="none" stroke="${c}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>`;
2621
+ };
2622
+ s += tag(14, 40, '#111') + tag(196, 210, '#6a2c8c') + tag(16, 222, '#111');
2623
+ const kx = 238 + rnd() * 20;
2624
+ s += `<path d="M${n(kx - 20)} 50L${n(kx - 22)} 26L${n(kx - 10)} 38L${n(kx)} 20L${n(kx + 10)} 38L${n(kx + 22)} 26L${n(kx + 20)} 50Z" fill="none" stroke="${pick()}" stroke-width="3" stroke-linejoin="round"/>`;
2625
+ // salpicaduras
2626
+ let sp = '';
2627
+ for (let i = 0; i < 40; i++) sp += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * GROUND)}" r="${n(0.6 + rnd() * 1.8)}" fill="${pick()}" opacity=".8"/>`;
2628
+ s += sp;
2629
+ // asfalto con manchas de pintura y un aerosol tirado
2630
+ s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#4a4a4e"/><path d="M0 ${GROUND}H${W}" stroke="#2e2e32" stroke-width="3"/>`;
2631
+ for (let i = 0; i < 3; i++) s += `<ellipse cx="${n(30 + rnd() * 240)}" cy="${n(GROUND + 20 + rnd() * 30)}" rx="${n(8 + rnd() * 14)}" ry="${n(3 + rnd() * 4)}" fill="${pick()}" opacity=".85"/>`;
2632
+ const cx = rnd() < 0.5 ? 40 : 250;
2633
+ const cc = pick();
2634
+ s += `<g transform="rotate(-12 ${cx} ${GROUND + 20})"><rect x="${cx - 9}" y="${GROUND - 14}" width="18" height="36" rx="4" fill="${cc}" stroke="#111" stroke-width="2"/><rect x="${cx - 9}" y="${GROUND - 2}" width="18" height="8" fill="#fff"/><rect x="${cx - 6}" y="${GROUND - 22}" width="12" height="9" rx="3" fill="#ddd" stroke="#111" stroke-width="2"/><rect x="${cx - 2}" y="${GROUND - 26}" width="4" height="5" fill="#111"/></g>`;
2635
+ s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
2636
+ return s;
2637
+ },
2638
+
2639
+ thermal(p, rnd) {
2640
+ const ramp = ['#0a0520', '#2a0a6a', '#8a1a8a', '#e8401c', '#ffb000', '#fffbe0'];
2641
+ let s = `<defs><linearGradient id="${p}sk" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#07031a"/><stop offset="1" stop-color="#26105e"/></linearGradient>`
2642
+ + `<linearGradient id="${p}sc" x1="0" y1="1" x2="0" y2="0">${ramp.map((c, i) => `<stop offset="${n(i / (ramp.length - 1))}" stop-color="${c}"/>`).join('')}</linearGradient>`
2643
+ + `<radialGradient id="${p}hot"><stop offset="0" stop-color="#fffbe0"/><stop offset=".25" stop-color="#ffb000"/><stop offset=".55" stop-color="#e8401c" stop-opacity=".8"/><stop offset=".8" stop-color="#8a1a8a" stop-opacity=".4"/><stop offset="1" stop-color="#2a0a6a" stop-opacity="0"/></radialGradient>`
2644
+ + `<filter id="${p}bl" x="-5%" y="-5%" width="110%" height="110%"><feGaussianBlur stdDeviation="1.4"/></filter>${bdGrain(`${p}g`, rnd, [1, 1, 1], 0.9, -0.52, 1.2)}</defs>`;
2645
+ let g = `<rect width="${W}" height="${W}" fill="url(#${p}sk)"/>`;
2646
+ // edificios fríos con alguna ventana tibia
2647
+ let bld = '';
2648
+ let win = '';
2649
+ for (let x = -10; x < W; x += 34 + rnd() * 20) {
2650
+ const w = 30 + rnd() * 26;
2651
+ const h = 60 + rnd() * 90;
2652
+ bld += `<rect x="${n(x)}" y="${n(GROUND - h)}" width="${n(w)}" height="${n(h)}"/>`;
2653
+ for (let wy = GROUND - h + 10; wy < GROUND - 14; wy += 16) for (let wx = x + 6; wx < x + w - 10; wx += 12) if (rnd() < 0.3) win += `<rect x="${n(wx)}" y="${n(wy)}" width="6" height="8" fill="${rnd() < 0.3 ? '#e8401c' : '#8a1a8a'}"/>`;
2654
+ }
2655
+ g += `<g fill="#1a0848">${bld}</g>${win}`;
2656
+ // farol caliente (a la izquierda: la escala va a la derecha)
2657
+ const lx = 44 + rnd() * 10;
2658
+ g += `<rect x="${n(lx - 2)}" y="116" width="4" height="${GROUND - 116}" fill="#2a0a6a"/><circle cx="${n(lx)}" cy="112" r="30" fill="url(#${p}hot)"/>`;
2659
+ // suelo frío
2660
+ g += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#140838"/><path d="M0 ${GROUND}H${W}" stroke="#2a0a6a" stroke-width="3"/>`;
2661
+ g += `<ellipse cx="150" cy="${GROUND + 6}" rx="56" ry="7" fill="#8a1a8a" opacity=".55"/>`;
2662
+ let s2 = `${s}<g filter="url(#${p}bl)">${g}</g><rect width="${W}" height="${W}" filter="url(#${p}g)" opacity=".5"/>`;
2663
+ // HUD: esquinas, mira, escala de color y lecturas
2664
+ const hud = '#f4f0ff';
2665
+ s2 += `<g fill="none" stroke="${hud}" stroke-width="2" opacity=".85"><path d="M26 44V26H44M${W - 44} 26H${W - 26}V44M26 ${W - 44}V${W - 26}H44M${W - 44} ${W - 26}H${W - 26}V${W - 44}"/><path d="M150 132V142M150 158V168M132 150H142M158 150H168" stroke-width="1.4"/></g>`;
2666
+ s2 += `<rect x="${W - 46}" y="66" width="9" height="140" fill="url(#${p}sc)" stroke="${hud}" stroke-width=".8"/>`;
2667
+ const tmax = (36 + rnd() * 2).toFixed(1);
2668
+ const tmin = (8 + rnd() * 6).toFixed(1);
2669
+ s2 += `<g font-family="monospace" fill="${hud}"><text x="${W - 42}" y="61" font-size="8" text-anchor="middle">${tmax}°</text><text x="${W - 42}" y="216" font-size="8" text-anchor="middle">${tmin}°</text>`
2670
+ + `<text x="34" y="58" font-size="14" font-weight="bold">${(36.2 + rnd() * 0.8).toFixed(1)}°C</text><text x="34" y="70" font-size="7" opacity=".8">ε 0.95 ⌀ ${Math.floor(2 + rnd() * 3)}.${Math.floor(rnd() * 9)}m</text>`
2671
+ + `<text x="34" y="${W - 34}" font-size="8"><tspan fill="#ff4a3a">●</tspan> REC</text></g>`;
2672
+ return s2;
2673
+ },
1398
2674
  };
1399
2675
 
1400
2676
  /**