@7ots/cli 0.1.6 → 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 (61) hide show
  1. package/README.md +3 -3
  2. package/cli/7ots.mjs +13 -1
  3. package/cli/lib/account.mjs +152 -2
  4. package/cli/lib/assistant.mjs +107 -38
  5. package/cli/lib/brain.mjs +13 -2
  6. package/cli/lib/config.mjs +5 -1
  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 +352 -11
  11. package/cli/lib/prompts.mjs +31 -17
  12. package/cli/lib/prowl.mjs +110 -0
  13. package/cli/pet/electron/director.cjs +37 -5
  14. package/cli/pet/electron/main.cjs +92 -10
  15. package/cli/pet/pet.html +149 -10
  16. package/cli/pet/settings.html +549 -43
  17. package/dist/7ots.esm.js +216 -137
  18. package/dist/7ots.esm.js.map +4 -4
  19. package/dist/7ots.iife.js +216 -137
  20. package/dist/7ots.iife.js.map +4 -4
  21. package/llms.txt +1 -1
  22. package/package.json +17 -14
  23. package/server/channels/agent.mjs +7 -1
  24. package/server/channels/index.mjs +3 -2
  25. package/server/platform/connect.mjs +4 -1
  26. package/server/platform/db.mjs +14 -1
  27. package/server/platform/device.mjs +101 -4
  28. package/server/platform/mods.mjs +222 -0
  29. package/server/platform/orquesta.mjs +78 -14
  30. package/server/platform/profile.mjs +161 -0
  31. package/server/platform/routes.mjs +39 -33
  32. package/server/platform/runtime.mjs +5 -3
  33. package/server/platform/store.mjs +1 -1
  34. package/server/platform/vm.mjs +688 -17
  35. package/server/server.mjs +12 -2
  36. package/server/tts.mjs +17 -4
  37. package/src/avatar/three.js +3 -2
  38. package/src/character/AvatarEditor.js +95 -7
  39. package/src/character/Character.js +116 -17
  40. package/src/character/TradingCard.js +226 -0
  41. package/src/character/mods/index.js +4 -0
  42. package/src/character/mods/orc.js +64 -0
  43. package/src/character/mods.js +251 -0
  44. package/src/character/more/acc-head.js +213 -0
  45. package/src/character/more/acc-rest.js +342 -0
  46. package/src/character/more/brows.js +39 -0
  47. package/src/character/more/cheeks.js +78 -0
  48. package/src/character/more/eyes.js +288 -0
  49. package/src/character/more/mouths.js +259 -0
  50. package/src/character/more/patterns.js +120 -0
  51. package/src/character/parts.js +100 -0
  52. package/src/character/skin.js +166 -1
  53. package/src/character/styles.js +1285 -9
  54. package/src/character3d/Character3D.js +253 -37
  55. package/src/character3d/index.js +7 -1
  56. package/src/character3d/model.js +367 -0
  57. package/src/i18n/messages/character.js +37 -22
  58. package/src/i18n/messages/platform.js +33 -0
  59. package/src/identity/random.js +17 -3
  60. package/src/identity/schema.js +10 -0
  61. package/src/index.js +1 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Catálogo 2 (2026-10): piezas nuevas, añadidas al final de su lista en parts.js.
3
+ * Cada pieza: { id, label (es), en, pt, as3d (id existente más parecido para el visor 3D), … }.
4
+ */
5
+
6
+ /** @param {object} h ayudantes de dibujo de parts.js */
7
+ export default (h) => {
8
+ const { n, shade, star, heartPath } = h;
9
+ // p = { box {x,y,w,h}, color, eyeY, r } — se recortan con la silueta del cuerpo
10
+ const X = (p, a) => p.box.x + a * p.box.w;
11
+ const Y = (p, b) => p.box.y + b * p.box.h;
12
+ /** Banda horizontal con borde ondulado (de y hacia abajo si down, hacia arriba si no). */
13
+ const wavyBand = (p, y, down, amp, waves) => {
14
+ const { x, w, h } = p.box;
15
+ const step = (w + 20) / (waves * 2);
16
+ let d = `M${n(x - 10)} ${n(y)}`;
17
+ for (let i = 0; i < waves * 2; i++) d += `q${n(step / 2)} ${n(i % 2 ? amp : -amp)} ${n(step)} 0`;
18
+ return `${d}V${n(down ? y + h : y - h)}H${n(x - 10)}Z`;
19
+ };
20
+
21
+ return [
22
+ {
23
+ id: 'polka', label: 'Lunares', en: 'Polka dots', pt: 'Bolinhas', as3d: 'spots',
24
+ draw: (p) => {
25
+ const s = p.box.w / 5;
26
+ const out = [];
27
+ for (let j = 0; j * s < p.box.h + s; j++) for (let i = -1; i * s < p.box.w + s; i++) out.push(`<circle cx="${n(p.box.x + i * s + (j % 2 ? s / 2 : 0))}" cy="${n(p.box.y + j * s + s * 0.3)}" r="${n(s * 0.2)}" fill="${p.color}"/>`);
28
+ return out.join('');
29
+ },
30
+ },
31
+ {
32
+ id: 'zigzag', label: 'Zigzag', en: 'Zigzag', pt: 'Zigue-zague', as3d: 'stripes',
33
+ draw: (p) => [0.3, 0.55, 0.8].map((b) => {
34
+ const k = p.box.w / 8;
35
+ let d = `M${n(p.box.x - k)} ${n(Y(p, b))}`;
36
+ for (let i = 0; i < 10; i++) d += `l${n(k)} ${n(i % 2 ? k * 0.7 : -k * 0.7)}`;
37
+ return `<path d="${d}" stroke="${p.color}" stroke-width="${n(p.box.h / 16)}" fill="none" stroke-linejoin="round"/>`;
38
+ }).join(''),
39
+ },
40
+ {
41
+ id: 'checker', label: 'Cuadros', en: 'Checkers', pt: 'Xadrez', as3d: 'stripes',
42
+ draw: (p) => {
43
+ const s = p.box.w / 6;
44
+ const out = [];
45
+ for (let j = 0; j * s < p.box.h; j++) for (let i = 0; i * s < p.box.w; i++) if ((i + j) % 2) out.push(`<rect x="${n(p.box.x + i * s)}" y="${n(p.box.y + j * s)}" width="${n(s)}" height="${n(s)}" fill="${p.color}"/>`);
46
+ return out.join('');
47
+ },
48
+ },
49
+ {
50
+ id: 'argyle', label: 'Rombos', en: 'Argyle', pt: 'Losangos', as3d: 'spots',
51
+ draw: (p) => {
52
+ const s = p.box.w / 4;
53
+ const hh = s * 1.3;
54
+ const out = [];
55
+ for (let j = 0; j * hh < p.box.h + hh; j++) for (let i = 0; i * s <= p.box.w + s; i++) if ((i + j) % 2 === 0) out.push(`<path d="M${n(p.box.x + i * s)} ${n(p.box.y + j * hh - hh / 2)}l${n(s / 2)} ${n(hh / 2)}l${n(-s / 2)} ${n(hh / 2)}l${n(-s / 2)} ${n(-hh / 2)}Z" fill="${p.color}"/>`);
56
+ return `${out.join('')}<g stroke="${shade(p.color, -0.2)}" stroke-width="1.2" opacity=".55">${Array.from({ length: 8 }, (_, i) => `<path d="M${n(p.box.x + (i - 3) * s)} ${n(p.box.y)}l${n(p.box.h / 1.3)} ${n(p.box.h)}M${n(p.box.x + (i + 1) * s)} ${n(p.box.y)}l${n(-p.box.h / 1.3)} ${n(p.box.h)}"/>`).join('')}</g>`;
57
+ },
58
+ },
59
+ {
60
+ id: 'waves', label: 'Olas', en: 'Waves', pt: 'Ondas', as3d: 'stripes',
61
+ draw: (p) => [0.28, 0.5, 0.72, 0.94].map((b) => {
62
+ const k = p.box.w / 6;
63
+ let d = `M${n(p.box.x - 10)} ${n(Y(p, b))}`;
64
+ for (let i = 0; i < 8; i++) d += `q${n(k / 2)} ${n(i % 2 ? k * 0.35 : -k * 0.35)} ${n(k)} 0`;
65
+ return `<path d="${d}" stroke="${p.color}" stroke-width="${n(p.box.h / 18)}" fill="none" stroke-linecap="round"/>`;
66
+ }).join(''),
67
+ },
68
+ {
69
+ id: 'cow', label: 'Vaca', en: 'Cow', pt: 'Vaquinha', as3d: 'spots',
70
+ draw: (p) => [[0.12, 0.35, 0.16, 0.12, 20], [0.85, 0.22, 0.14, 0.1, -15], [0.72, 0.72, 0.18, 0.12, 30], [0.2, 0.85, 0.13, 0.1, -25], [0.5, 0.05, 0.12, 0.08, 10]].map(([a, b, rx, ry, rot]) => {
71
+ const cx = X(p, a);
72
+ const cy = Y(p, b);
73
+ const RX = rx * p.box.w;
74
+ const RY = ry * p.box.h;
75
+ return `<path d="M${n(cx - RX)} ${n(cy)}C${n(cx - RX)} ${n(cy - RY * 1.2)} ${n(cx - RX * 0.1)} ${n(cy - RY * 0.8)} ${n(cx + RX * 0.2)} ${n(cy - RY)}C${n(cx + RX * 1.1)} ${n(cy - RY * 1.1)} ${n(cx + RX * 1.1)} ${n(cy + RY * 0.4)} ${n(cx + RX * 0.5)} ${n(cy + RY * 0.8)}C${n(cx)} ${n(cy + RY * 1.3)} ${n(cx - RX * 1.1)} ${n(cy + RY)} ${n(cx - RX)} ${n(cy)}Z" transform="rotate(${rot} ${n(cx)} ${n(cy)})" fill="${p.color}"/>`;
76
+ }).join(''),
77
+ },
78
+ {
79
+ id: 'leopard', label: 'Leopardo', en: 'Leopard', pt: 'Onça', as3d: 'spots',
80
+ draw: (p) => [[0.15, 0.3], [0.45, 0.2], [0.8, 0.32], [0.28, 0.55], [0.62, 0.5], [0.9, 0.65], [0.12, 0.78], [0.45, 0.8], [0.75, 0.9]].map(([a, b], i) => {
81
+ const r = p.box.w * 0.065;
82
+ const cx = X(p, a);
83
+ const cy = Y(p, b);
84
+ const rot = i * 47;
85
+ return `<g transform="rotate(${rot} ${n(cx)} ${n(cy)})"><ellipse cx="${n(cx)}" cy="${n(cy)}" rx="${n(r * 0.9)}" ry="${n(r * 0.7)}" fill="${shade(p.color, 0.25)}"/><path d="M${n(cx - r)} ${n(cy + r * 0.3)}A${n(r)} ${n(r * 0.8)} 0 0 1 ${n(cx + r * 0.6)} ${n(cy - r * 0.75)}M${n(cx + r)} ${n(cy - r * 0.1)}A${n(r)} ${n(r * 0.8)} 0 0 1 ${n(cx - r * 0.2)} ${n(cy + r * 0.85)}" stroke="${shade(p.color, -0.45)}" stroke-width="${n(r * 0.4)}" fill="none" stroke-linecap="round"/></g>`;
86
+ }).join(''),
87
+ },
88
+ {
89
+ id: 'socks', label: 'Calcetines', en: 'Socks', pt: 'Meias', as3d: 'twotone',
90
+ draw: (p) => `<path d="${wavyBand(p, Y(p, 0.8), true, p.box.h * 0.035, 5)}" fill="${p.color}"/>`,
91
+ },
92
+ {
93
+ id: 'cap', label: 'Gorrito', en: 'Cap', pt: 'Touquinha', as3d: 'twotone',
94
+ draw: (p) => `<path d="${wavyBand(p, Math.min(Y(p, 0.28), p.eyeY - p.r * 1.9), false, p.box.h * 0.035, 4)}" fill="${p.color}"/>`,
95
+ },
96
+ {
97
+ id: 'heartbelly', label: 'Barriga corazón', en: 'Heart belly', pt: 'Barriga coração', as3d: 'belly',
98
+ draw: (p) => `<path d="${heartPath(p.box.w * 0.24)}" transform="translate(${n(X(p, 0.5))} ${n(Y(p, 0.74))})" fill="${p.color}"/>`,
99
+ },
100
+ {
101
+ id: 'starbelly', label: 'Barriga estrella', en: 'Star belly', pt: 'Barriga estrela', as3d: 'belly',
102
+ draw: (p) => `<path d="${star(p.box.w * 0.24, 0.5)}" transform="translate(${n(X(p, 0.5))} ${n(Y(p, 0.76))})" fill="${p.color}" stroke="${p.color}" stroke-width="${n(p.box.w * 0.05)}" stroke-linejoin="round"/>`,
103
+ },
104
+ {
105
+ id: 'galaxy', label: 'Galaxia', en: 'Galaxy', pt: 'Galáxia', as3d: 'stars',
106
+ draw: (p) => {
107
+ let out = '';
108
+ let s = 7;
109
+ const rnd = () => ((s = (s * 16807) % 2147483647) / 2147483647);
110
+ for (let i = 0; i < 26; i++) {
111
+ const x = X(p, rnd());
112
+ const y = Y(p, rnd());
113
+ const k = rnd();
114
+ out += k > 0.82 ? `<path d="${star(p.box.w * 0.055, 0.3, 4)}" transform="translate(${n(x)} ${n(y)})" fill="${p.color}"/>` : `<circle cx="${n(x)}" cy="${n(y)}" r="${n(p.box.w * (0.01 + k * 0.016))}" fill="${p.color}" opacity="${n(0.7 + k * 0.3)}"/>`;
115
+ }
116
+ return out;
117
+ },
118
+ },
119
+ ];
120
+ };
@@ -17,6 +17,13 @@
17
17
 
18
18
  import '../i18n/messages/character.js';
19
19
  import { t as globalT, addCatalog } from '../i18n/index.js';
20
+ import moreEyes from './more/eyes.js';
21
+ import moreBrows from './more/brows.js';
22
+ import moreMouths from './more/mouths.js';
23
+ import moreCheeks from './more/cheeks.js';
24
+ import morePatterns from './more/patterns.js';
25
+ import moreAccHead from './more/acc-head.js';
26
+ import moreAccRest from './more/acc-rest.js';
20
27
 
21
28
  const n = (v) => Math.round(v * 100) / 100;
22
29
 
@@ -125,6 +132,41 @@ export const SHAPES = [
125
132
  }),
126
133
  },
127
134
  { id: 'mushroom', label: 'Seta', points: superShape({ rxT: 78, rxB: 46, ryT: 62, ryB: 64, nT: 2.2, nB: 3.2 }) },
135
+ // ── catálogo 2 (2026-10): añadidas al final; las semillas antiguas no las sacan (ver CATALOG_V1) ──
136
+ {
137
+ id: 'peanut',
138
+ label: 'Cacahuete', en: 'Peanut', pt: 'Amendoim',
139
+ points: superShape({ rxT: 56, rxB: 64, ryT: 72, ryB: 60, nT: 2.2, nB: 2.4, tweak: (i, x, y) => (i === 4 || i === 12 ? [100 + (x - 100) * 0.84, y + 4] : i === 3 || i === 13 ? [100 + (x - 100) * 1.04, y] : [x, y]) }),
140
+ },
141
+ { id: 'onigiri', label: 'Onigiri', en: 'Onigiri', pt: 'Onigiri', points: superShape({ rxT: 40, rxB: 72, ryT: 74, ryB: 54, nT: 1.5, nB: 3.4 }) },
142
+ { id: 'bell', label: 'Campana', en: 'Bell', pt: 'Sino', points: superShape({ rxT: 48, rxB: 74, ryT: 72, ryB: 54, nT: 2.2, nB: 5.5 }) },
143
+ { id: 'bun', label: 'Bollito', en: 'Bun', pt: 'Pãozinho', points: superShape({ rxT: 70, rxB: 74, ryT: 70, ryB: 40, nT: 2.1, nB: 5 }) },
144
+ {
145
+ id: 'bear',
146
+ label: 'Osito', en: 'Teddy', pt: 'Ursinho',
147
+ points: superShape({
148
+ rxT: 60, rxB: 64, ryT: 62, ryB: 60, nT: 2.3, nB: 2.4,
149
+ tweak: (i, x, y) => (i === 2 || i === 14 ? [x + (i === 2 ? 14 : -14), y - 16] : i === 1 || i === 15 ? [x, y + 6] : [x, y]),
150
+ }),
151
+ },
152
+ { id: 'potato', label: 'Patata', en: 'Potato', pt: 'Batata', points: superShape({ rxT: 62, rxB: 66, ryT: 62, ryB: 58, nT: 2.3, nB: 2.5, tweak: (i, x, y) => [x + Math.sin(i * 2.9 + 1) * 6, y + Math.cos(i * 1.3 + 2) * 6] }) },
153
+ { id: 'gem', label: 'Gema', en: 'Gem', pt: 'Gema', points: superShape({ rxT: 66, rxB: 66, ryT: 66, ryB: 70, nT: 1.35, nB: 1.35 }) },
154
+ { id: 'chubby', label: 'Rechoncho', en: 'Chubby', pt: 'Fofinho', points: superShape({ rxT: 58, rxB: 82, ryT: 60, ryB: 46, nT: 2.2, nB: 3.2 }) },
155
+ { id: 'capsule', label: 'Cápsula', en: 'Capsule', pt: 'Cápsula', points: superShape({ rxT: 48, rxB: 48, ryT: 78, ryB: 72, nT: 2.6, nB: 2.6 }) },
156
+ { id: 'barrel', label: 'Tonel', en: 'Barrel', pt: 'Barril', points: superShape({ rxT: 64, rxB: 64, ryT: 64, ryB: 62, nT: 3, nB: 3, tweak: (i, x, y) => (i === 4 || i === 12 ? [100 + (x - 100) * 1.06, y] : [x, y]) }) },
157
+ { id: 'flame', label: 'Llama', en: 'Flame', pt: 'Chama', points: superShape({ rxT: 40, rxB: 64, ryT: 80, ryB: 54, nT: 1.6, nB: 2.4, tweak: (i, x, y) => (i === 0 ? [x + 16, y + 4] : i === 1 ? [x + 6, y] : i === 15 ? [x + 8, y] : [x, y]) }) },
158
+ {
159
+ id: 'owl',
160
+ label: 'Búho', en: 'Owl', pt: 'Coruja',
161
+ points: superShape({
162
+ rxT: 60, rxB: 64, ryT: 62, ryB: 60, nT: 3, nB: 2.4,
163
+ tweak: (i, x, y) => (i === 2 || i === 14 ? [x + (i === 2 ? 4 : -4), y - 20] : i === 1 || i === 15 ? [x, y + 10] : i === 0 ? [x, y + 10] : [x, y]),
164
+ }),
165
+ },
166
+ { id: 'slime', label: 'Slime', en: 'Slime', pt: 'Slime', points: superShape({ rxT: 56, rxB: 80, ryT: 78, ryB: 30, nT: 2, nB: 6, tweak: (i, x, y) => (i >= 6 && i <= 10 ? [x, y + (i % 2 ? 4 : -1)] : [x, y]) }) },
167
+ { id: 'apple', label: 'Manzana', en: 'Apple', pt: 'Maçã', points: superShape({ rxT: 64, rxB: 60, ryT: 62, ryB: 62, nT: 2.4, nB: 2.2, tweak: (i, x, y) => (i === 0 ? [x, y + 12] : i === 1 || i === 15 ? [x, y + 2] : [x, y]) }) },
168
+ { id: 'lemon', label: 'Limón', en: 'Lemon', pt: 'Limão', points: superShape({ rxT: 70, rxB: 70, ryT: 56, ryB: 56, nT: 2, nB: 2, tweak: (i, x, y) => (i === 4 || i === 12 ? [x + (i === 4 ? 12 : -12), y] : [x, y]) }) },
169
+ { id: 'alien', label: 'Alien', en: 'Alien', pt: 'Alienígena', points: superShape({ rxT: 72, rxB: 44, ryT: 64, ryB: 66, nT: 2.3, nB: 1.7 }) },
128
170
  ];
129
171
 
130
172
  // ───────────────────────────── ojos ─────────────────────────────
@@ -236,6 +278,7 @@ export const EYES = [
236
278
  { id: 'teary', label: 'Llorosos', draw: (p) => { const [x, y] = look(p, 0.1); return `<circle cx="${x}" cy="${y}" r="${n(p.r * 0.95)}" fill="${p.ink}"/>${shine(x - p.r * 0.32, y - p.r * 0.34, p.r * 0.32)}${shine(x + p.r * 0.3, y + p.r * 0.34, p.r * 0.14)}<path d="M${n(p.r * 0.7 * p.side)} ${n(p.r * 0.9)}q${n(-p.r * 0.35)} ${n(p.r * 0.6)} 0 ${n(p.r * 0.8)}q${n(p.r * 0.35)} ${n(-p.r * 0.2)} 0 ${n(-p.r * 0.8)}Z" fill="#60a5fa"/>`; } },
237
279
  { id: 'hypno', label: 'Hipnosis', draw: (p) => `<circle r="${p.r}" fill="#fff"/>${[0.8, 0.55, 0.3].map((k, i) => `<circle r="${n(p.r * k)}" fill="none" stroke="${i % 2 ? p.iris : p.ink}" stroke-width="${n(p.r * 0.14)}"/>`).join('')}` },
238
280
  { id: 'focused', label: 'Decididos', draw: (p) => `<ellipse rx="${n(p.r)}" ry="${n(p.r * 0.7)}" fill="${p.white}"/>${pupil(p, p.r * 0.5, 0.3)}<path d="M${n(-p.r * 1.1)} ${n(-p.r * 0.55)}H${n(p.r * 1.1)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.24)}" stroke-linecap="round"/>` },
281
+ ...moreEyes({ n, shade, shine, star, heartPath, look, pupil }),
239
282
  ];
240
283
 
241
284
  // ───────────────────────────── cejas ─────────────────────────────
@@ -252,6 +295,7 @@ export const BROWS = [
252
295
  { id: 'dots', label: 'Maro', draw: (p) => `<ellipse rx="${n(p.r * 0.38)}" ry="${n(p.r * 0.24)}" fill="${p.color}"/>` },
253
296
  { id: 'unibrow', label: 'Uniceja', uni: true, draw: (p) => brow(p, `M${n(-p.sp - p.r * 0.9)} ${n(p.r * 0.1)}Q${n(-p.sp / 2)} ${n(-p.r * 0.45)} 0 ${n(p.r * 0.05)}Q${n(p.sp / 2)} ${n(-p.r * 0.45)} ${n(p.sp + p.r * 0.9)} ${n(p.r * 0.1)}`, 0.34) },
254
297
  { id: 'angled', label: 'Picudas', draw: (p) => brow(p, `M${n(-p.r * 0.9 * p.side)} ${n(p.r * 0.15)}L${n(p.r * 0.35 * p.side)} ${n(-p.r * 0.35)}L${n(p.r * 0.9 * p.side)} ${n(-p.r * 0.05)}`, 0.2) },
298
+ ...moreBrows({ n, shade, brow, star, heartPath }),
255
299
  ];
256
300
 
257
301
  // ───────────────────────────── bocas ─────────────────────────────
@@ -334,6 +378,7 @@ export const MOUTHS = [
334
378
  { id: 'drool', label: 'Babeando', draw: (p) => `${openMouth({ ...p, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.2 })}<path d="M${n(p.w * 0.6)} ${n(p.w * 0.1)}q${n(-p.w * 0.12)} ${n(p.w * 0.5)} 0 ${n(p.w * 0.65)}q${n(p.w * 0.12)} ${n(-p.w * 0.15)} 0 ${n(-p.w * 0.65)}Z" fill="#93c5fd"/>` },
335
379
  { id: 'square', label: 'Cuadrada', draw: (p) => { const h = p.w * (0.3 + p.open * 0.7); return `<rect x="${n(-p.w * 0.7)}" y="${n(-h / 2)}" width="${n(p.w * 1.4)}" height="${n(h)}" rx="2" fill="${p.ink}" stroke="${p.lip}" stroke-width="2"/>`; } },
336
380
  { id: 'mustachio', label: 'Sonrisa ladeada', draw: (p) => openMouth({ ...p, curve: p.curve + 0.3 }, { min: 0.1, max: 0.8, teeth: true }) },
381
+ ...moreMouths({ n, shade, mouthShape, openMouth, line, closedOr, star, heartPath }),
337
382
  ];
338
383
 
339
384
  // ───────────────────────────── mejillas y marcas ─────────────────────────────
@@ -348,6 +393,7 @@ export const CHEEKS = [
348
393
  { id: 'whiskers', label: 'Bigotes de gato', draw: (p) => [-1, 1].map((s) => [-0.25, 0, 0.25].map((k) => `<path d="M${n(s * (p.sp * 0.7))} ${n(p.r * 2.4 + k * p.r * 2)}L${n(s * (p.sp + p.r * 2.6))} ${n(p.r * 2.2 + k * p.r * 3.2)}" stroke="#1f2937" stroke-width="${n(p.r * 0.1)}" stroke-linecap="round" opacity=".7"/>`).join('')).join('') },
349
394
  { id: 'stars', label: 'Estrellitas', draw: (p) => [-1, 1].map((s) => `<path d="${star(p.r * 0.4)}" transform="translate(${n(s * (p.sp + p.r * 0.6))} ${n(p.r * 1.9)})" fill="#facc15"/>`).join('') },
350
395
  { id: 'paint', label: 'Pintura de guerra', draw: (p) => [-1, 1].map((s) => `<path d="M${n(s * (p.sp - p.r * 0.2))} ${n(p.r * 1.4)}h${n(s * p.r * 1.6)}M${n(s * (p.sp - p.r * 0.2))} ${n(p.r * 1.9)}h${n(s * p.r * 1.6)}" stroke="${p.color}" stroke-width="${n(p.r * 0.25)}" stroke-linecap="round"/>`).join('') },
396
+ ...moreCheeks({ n, shade, star, heartPath }),
351
397
  ];
352
398
 
353
399
  // ───────────────────────────── patrones del cuerpo ─────────────────────────────
@@ -364,6 +410,7 @@ export const PATTERNS = [
364
410
  { id: 'hearts', label: 'Corazones', draw: (p) => [[0.2, 0.35], [0.8, 0.3], [0.7, 0.75], [0.25, 0.8], [0.5, 0.95]].map(([a, b]) => `<path d="${heartPath(p.box.w * 0.06)}" transform="translate(${n(p.box.x + a * p.box.w)} ${n(p.box.y + b * p.box.h)})" fill="${p.color}"/>`).join('') },
365
411
  { id: 'stars', label: 'Estrellas', draw: (p) => [[0.18, 0.3], [0.82, 0.28], [0.75, 0.72], [0.22, 0.78], [0.5, 0.93]].map(([a, b]) => `<path d="${star(p.box.w * 0.06)}" transform="translate(${n(p.box.x + a * p.box.w)} ${n(p.box.y + b * p.box.h)})" fill="${p.color}"/>`).join('') },
366
412
  { id: 'circuit', label: 'Circuito', draw: (p) => [0.25, 0.5, 0.75].map((b, i) => `<path d="M${n(p.box.x)} ${n(p.box.y + b * p.box.h)}h${n(p.box.w * 0.18)}l${n(p.box.w * 0.06)} ${n(i % 2 ? 8 : -8)}h${n(p.box.w * 0.1)}M${n(p.box.x + p.box.w)} ${n(p.box.y + b * p.box.h + 6)}h${n(-p.box.w * 0.2)}" stroke="${p.color}" stroke-width="2" fill="none"/><circle cx="${n(p.box.x + p.box.w * 0.34)}" cy="${n(p.box.y + b * p.box.h + (i % 2 ? 8 : -8))}" r="2.5" fill="${p.color}"/>`).join('') },
413
+ ...morePatterns({ n, shade, star, heartPath }),
367
414
  ];
368
415
 
369
416
  // ───────────────────────────── acabados ─────────────────────────────
@@ -376,6 +423,14 @@ export const FINISHES = [
376
423
  { id: 'toon', label: 'Cómic' },
377
424
  { id: 'neon', label: 'Neón' },
378
425
  { id: 'sketch', label: 'Boceto' },
426
+ // catálogo 2 (2026-10): se dibujan en Character.js (FINISH2)
427
+ { id: 'pastel', label: 'Pastel', en: 'Pastel', pt: 'Pastel', as3d: 'soft3d' },
428
+ { id: 'metal', label: 'Metálico', en: 'Metallic', pt: 'Metálico', as3d: 'glossy' },
429
+ { id: 'pearl', label: 'Nácar', en: 'Pearl', pt: 'Madrepérola', as3d: 'glossy' },
430
+ { id: 'glass', label: 'Cristal', en: 'Glass', pt: 'Vidro', as3d: 'glossy' },
431
+ { id: 'velvet', label: 'Terciopelo', en: 'Velvet', pt: 'Veludo', as3d: 'clay' },
432
+ { id: 'holo', label: 'Holográfico', en: 'Holographic', pt: 'Holográfico', as3d: 'glossy' },
433
+ { id: 'vinyl', label: 'Vinilo', en: 'Vinyl toy', pt: 'Vinil', as3d: 'toon' },
379
434
  ];
380
435
 
381
436
  // ───────────────────────────── texturas ─────────────────────────────
@@ -392,6 +447,16 @@ export const TEXTURES = [
392
447
  { id: 'ridges', label: 'Anillos' },
393
448
  { id: 'gloss', label: 'Gelatina' },
394
449
  { id: 'panels', label: 'Paneles' },
450
+ // catálogo 2 (2026-10): solo se eligen a mano ('auto' nunca las saca)
451
+ { id: 'knit', label: 'Punto', en: 'Knit', pt: 'Tricô', as3d: 'fuzz' },
452
+ { id: 'polka', label: 'Lunares', en: 'Polka dots', pt: 'Bolinhas', as3d: 'speckles' },
453
+ { id: 'glitter', label: 'Purpurina', en: 'Glitter', pt: 'Purpurina', as3d: 'speckles' },
454
+ { id: 'stone', label: 'Piedra', en: 'Stone', pt: 'Pedra', as3d: 'speckles' },
455
+ { id: 'wood', label: 'Madera', en: 'Wood', pt: 'Madeira', as3d: 'ridges' },
456
+ { id: 'bubbles', label: 'Burbujas', en: 'Bubbles', pt: 'Bolhas', as3d: 'gloss' },
457
+ { id: 'stitches', label: 'Costuras', en: 'Stitches', pt: 'Costuras', as3d: 'fuzz' },
458
+ { id: 'honeycomb', label: 'Panal', en: 'Honeycomb', pt: 'Favo de mel', as3d: 'scales' },
459
+ { id: 'quilt', label: 'Acolchado', en: 'Quilted', pt: 'Acolchoado', as3d: 'fuzz' },
395
460
  ];
396
461
 
397
462
  addCatalog({
@@ -499,6 +564,16 @@ export const ACCESSORIES = [
499
564
  { id: 'headphones', label: 'Auriculares', group: 'head', anchor: 'eyes', color: '#111827', color2: '#ef4444', draw: (c) => `<path d="M${n(-c.w * 0.52)} 0Q${n(-c.w * 0.6)} ${n(-c.eyeDy - 16)} 0 ${n(-c.eyeDy - 16)}Q${n(c.w * 0.6)} ${n(-c.eyeDy - 16)} ${n(c.w * 0.52)} 0" stroke="${c.color}" stroke-width="8" fill="none" stroke-linecap="round"/>${[-1, 1].map((s) => `<rect x="${n(s * c.w * 0.52 - 11)}" y="-16" width="22" height="34" rx="10" fill="${c.color2}" ${S(3)}/>`).join('')}` },
500
565
  { id: 'hair', label: 'Tupé', group: 'head', anchor: 'top', color: '#78350f', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-30 14Q-40 -20 -6 -26Q-10 -40 14 -36Q40 -30 34 14Q24 -8 0 -4Q-18 -2 -30 14Z" fill="${c.color}" ${S(3)}/></g>` },
501
566
  { id: 'sprout', label: 'Brote', group: 'head', anchor: 'top', color: '#22c55e', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M0 6V-20" stroke="#15803d" stroke-width="4" stroke-linecap="round"/><path d="M0 -18Q-24 -34 -28 -14Q-10 -8 0 -18Z" fill="${c.color}" ${S(2.5)}/><path d="M0 -20Q20 -38 28 -20Q12 -10 0 -20Z" fill="${c.color}" ${S(2.5)}/></g>` },
567
+ // Sombreros añadidos después: `random: false` los deja fuera de randomCharacter (si entraran en
568
+ // la lista del grupo, cada semilla ya creada sacaría otro ot). Se eligen a mano en el editor.
569
+ { id: 'fedora', label: 'Fedora', group: 'head', anchor: 'top', random: false, color: '#57534e', color2: '#1c1917', draw: (c) => `<g transform="scale(${n(c.k)}) rotate(-6)"><path d="M-64 8Q-58 -2 -40 2Q0 10 40 2Q58 -2 64 8Q42 20 0 20Q-42 20 -64 8Z" fill="${c.color}" ${S(3)}/><path d="M-34 6Q-38 -30 -20 -38Q-6 -30 0 -34Q6 -30 20 -38Q38 -30 34 6Q0 14 -34 6Z" fill="${c.color}" ${S(3)}/><path d="M-35 -4Q0 4 35 -4L35 5Q0 13 -35 5Z" fill="${c.color2}"/><path d="M-24 -30Q-28 -14 -26 -6" stroke="#fff" stroke-width="3" fill="none" opacity=".18" stroke-linecap="round"/></g>` },
570
+ { id: 'bowler', label: 'Bombín', group: 'head', anchor: 'top', random: false, color: '#1f2937', color2: '#78350f', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-50 10Q-52 0 -42 3Q0 12 42 3Q52 0 50 10Q30 20 0 20Q-30 20 -50 10Z" fill="${c.color}" ${S(3)}/><path d="M-34 8Q-38 -44 0 -44Q38 -44 34 8Q0 15 -34 8Z" fill="${c.color}" ${S(3)}/><path d="M-35 -2Q0 6 35 -2L34 6Q0 13 -34 6Z" fill="${c.color2}"/><path d="M-20 -34Q-28 -20 -26 -8" stroke="#fff" stroke-width="3.5" fill="none" opacity=".2" stroke-linecap="round"/></g>` },
571
+ { id: 'propeller', label: 'Gorra de hélice', group: 'head', anchor: 'top', random: false, color: '#3b82f6', color2: '#facc15', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-44 14Q-46 -32 0 -34Q46 -32 44 14Z" fill="${c.color}" ${S(3)}/><path d="M-15 13Q-17 -22 0 -34Q17 -22 15 13Z" fill="${c.color2}"/><path d="M-44 14Q-46 -32 0 -34Q46 -32 44 14Z" fill="none" ${S(3)}/><path d="M-48 12Q0 22 48 12" stroke="${shade(c.color, -0.25)}" stroke-width="5" fill="none" stroke-linecap="round"/><path d="M0 -34V-46" stroke="#1f2937" stroke-width="3.5" stroke-linecap="round"/><ellipse cx="-17" cy="-48" rx="17" ry="5.5" fill="#ef4444" ${S(2)}/><ellipse cx="17" cy="-48" rx="17" ry="5.5" fill="#22c55e" ${S(2)}/><circle cy="-48" r="4.5" fill="${c.color2}" ${S(2)}/></g>` },
572
+ { id: 'witch', label: 'Sombrero de bruja', group: 'head', anchor: 'top', random: false, color: '#312e81', color2: '#a855f7', draw: (c) => `<g transform="scale(${n(c.k)})"><ellipse cy="10" rx="62" ry="12" fill="${c.color}" ${S(3)}/><path d="M-34 8Q-22 -38 4 -66Q22 -86 46 -76Q22 -66 18 -40Q24 -12 34 8Q0 16 -34 8Z" fill="${c.color}" ${S(3)}/><path d="M-32 -2Q0 6 32 -2L34 8Q0 16 -34 8Z" fill="${c.color2}"/><rect x="-8" y="-2" width="16" height="12" rx="2" fill="none" stroke="#facc15" stroke-width="3"/></g>` },
573
+ { id: 'fez', label: 'Fez', group: 'head', anchor: 'top', random: false, color: '#b91c1c', color2: '#111827', draw: (c) => `<g transform="scale(${n(c.k)}) rotate(6)"><path d="M-26 12L-20 -34Q0 -38 20 -34L26 12Q0 18 -26 12Z" fill="${c.color}" ${S(3)}/><ellipse cy="-34" rx="20" ry="5" fill="${shade(c.color, -0.18)}" ${S(2.5)}/><path d="M0 -36Q22 -38 26 -8" stroke="${c.color2}" stroke-width="3" fill="none" stroke-linecap="round"/><path d="M21 -10h10l3 16h-16Z" fill="${c.color2}"/><path d="M-14 -26V6" stroke="#fff" stroke-width="3" opacity=".15" stroke-linecap="round"/></g>` },
574
+ { id: 'sombrero', label: 'Sombrero mexicano', group: 'head', anchor: 'top', random: false, color: '#eab308', color2: '#dc2626', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-84 10Q-80 -6 -56 2Q0 16 56 2Q80 -6 84 10Q60 30 0 30Q-60 30 -84 10Z" fill="${c.color}" ${S(3)}/><path d="M-26 8Q-30 -40 0 -44Q30 -40 26 8Q0 16 -26 8Z" fill="${c.color}" ${S(3)}/><path d="M-27 -4L-18 3L-9 -4L0 3L9 -4L18 3L27 -4" stroke="${c.color2}" stroke-width="4" fill="none" stroke-linejoin="round"/>${[-70, -48, -24, 0, 24, 48, 70].map((x) => `<circle cx="${x}" cy="${n(22 - Math.abs(x) * 0.12)}" r="3.5" fill="${c.color2}"/>`).join('')}</g>` },
575
+ { id: 'bucket', label: 'Gorro pescador', group: 'head', anchor: 'top', random: false, color: '#a3a35a', color2: '#65662f', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-46 4Q0 14 46 4L60 20Q0 34 -60 20Z" fill="${shade(c.color, -0.08)}" ${S(3)}/><path d="M-36 -26Q0 -38 36 -26L44 6Q0 16 -44 6Z" fill="${c.color}" ${S(3)}/><path d="M-50 12Q0 24 50 12M-40 -1Q0 9 40 -1" stroke="${c.color2}" stroke-width="1.8" stroke-dasharray="4 3" fill="none"/></g>` },
576
+ { id: 'jester', label: 'Bufón', group: 'head', anchor: 'top', random: false, color: '#7c3aed', color2: '#16a34a', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-40 6Q-48 -28 -76 -26Q-56 -42 -26 -34Q-14 -20 -10 6Z" fill="${c.color}" ${S(3)}/><path d="M40 6Q48 -28 76 -26Q56 -42 26 -34Q14 -20 10 6Z" fill="${c.color}" ${S(3)}/><path d="M-16 6Q-18 -42 0 -64Q18 -42 16 6Z" fill="${c.color2}" ${S(3)}/>${[[-76, -26], [0, -64], [76, -26]].map(([x, y]) => `<circle cx="${x}" cy="${y}" r="6.5" fill="#facc15" ${S(2)}/>`).join('')}<rect x="-46" y="0" width="92" height="16" rx="8" fill="${shade(c.color, -0.2)}" ${S(3)}/></g>` },
502
577
  // ── ojos ──
503
578
  { id: 'glasses', label: 'Gafas redondas', group: 'eyes', anchor: 'eyes', color: '#1f2937', draw: (c) => `${[-1, 1].map((s) => `<circle cx="${n(s * c.sp)}" r="${n(c.r * 1.55)}" fill="#fff" fill-opacity=".12" stroke="${c.color}" stroke-width="3.5"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.55)} -2Q0 -8 ${n(c.sp - c.r * 1.55)} -2" stroke="${c.color}" stroke-width="3" fill="none"/>` },
504
579
  { id: 'square', label: 'Gafas cuadradas', group: 'eyes', anchor: 'eyes', color: '#1f2937', draw: (c) => `${[-1, 1].map((s) => `<rect x="${n(s * c.sp - c.r * 1.7)}" y="${n(-c.r * 1.3)}" width="${n(c.r * 3.4)}" height="${n(c.r * 2.6)}" rx="4" fill="#fff" fill-opacity=".12" stroke="${c.color}" stroke-width="4"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.7)} -3H${n(c.sp - c.r * 1.7)}" stroke="${c.color}" stroke-width="4"/>` },
@@ -551,6 +626,8 @@ export const ACCESSORIES = [
551
626
  { id: 'hearts', label: 'Corazones', group: 'extra', anchor: 'top', color: '#f43f5e', draw: () => [[-40, -10, 7], [36, -18, 9], [8, -34, 6]].map(([x, y, r]) => `<path d="${heartPath(r)}" transform="translate(${x} ${y})" fill="#f43f5e"/>`).join('') },
552
627
  { id: 'zzz', label: 'Zzz', group: 'extra', anchor: 'top', color: '#60a5fa', draw: (c) => `<text x="30" y="-10" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="18" fill="${c.color}">Z</text><text x="46" y="-26" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="13" fill="${c.color}">z</text><text x="58" y="-38" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="10" fill="${c.color}">z</text>` },
553
628
  { id: 'sweat', label: 'Gota de sudor', group: 'extra', anchor: 'eyes', color: '#60a5fa', draw: (c) => `<path d="M${n(c.sp + c.r * 2.4)} ${n(-c.r * 2.4)}q-7 10 0 14q7 -4 0 -14Z" fill="${c.color}" stroke="#1e40af" stroke-width="1.5"/>` },
629
+ ...moreAccHead({ n, shade, S, star, heartPath, ellD, hornD, catEarD, tailD, ANGEL_WING, BAT_WING }),
630
+ ...moreAccRest({ n, shade, S, star, heartPath, ellD, hornD, catEarD, tailD, ANGEL_WING, BAT_WING }),
554
631
  ];
555
632
 
556
633
  // ───────────────────────────── estilos (plantillas completas) ─────────────────────────────
@@ -586,5 +663,28 @@ export const PRESETS = [
586
663
  { id: 'sprout', label: 'Brotecito', spec: { body: { shape: 'pear', color: '#86efac' }, finish: 'glossy', eyes: { type: 'kawaii' }, mouth: { type: 'tiny' }, cheeks: { type: 'blush' }, accessories: [acc('sprout')] } },
587
664
  ];
588
665
 
666
+ /**
667
+ * Tamaño de cada lista en el catálogo 1 (antes de 2026-10). randomCharacter(seed, { legacy: true })
668
+ * sortea solo entre estas primeras piezas para que las semillas antiguas sigan dando el mismo ot.
669
+ * Las piezas nuevas van SIEMPRE al final de su lista.
670
+ */
671
+ export const CATALOG_V1 = { shapes: 16, eyes: 33, brows: 9, mouths: 28, cheeks: 8, patterns: 10, finishes: 7, textures: 10, accessories: 80 };
672
+
589
673
  /** Listas por tipo (para partLabel). Moods y gestos viven en Character.js y solo tienen catálogo. */
590
674
  const CATALOG = { shapes: SHAPES, eyes: EYES, brows: BROWS, mouths: MOUTHS, cheeks: CHEEKS, patterns: PATTERNS, finishes: FINISHES, textures: TEXTURES, accessories: ACCESSORIES, groups: ACCESSORY_GROUPS, presets: PRESETS };
675
+
676
+ // Textos en/pt de las piezas del catálogo 2 (llevan `en` y `pt` junto al `label` en español).
677
+ {
678
+ const by = { es: {}, en: {}, pt: {} };
679
+ for (const [kind, list] of Object.entries(CATALOG)) {
680
+ for (const x of list) {
681
+ if (Array.isArray(x) || !x.en) continue;
682
+ for (const l of ['es', 'en', 'pt']) (by[l][kind] ||= {})[x.id] = l === 'es' ? x.label : x[l];
683
+ }
684
+ }
685
+ addCatalog({ es: { character: by.es }, en: { character: by.en }, pt: { character: by.pt } });
686
+ }
687
+
688
+ const ALIAS3D = Object.fromEntries(['eyes', 'brows', 'mouths', 'cheeks', 'patterns', 'accessories', 'finishes', 'textures'].map((k) => [k, Object.fromEntries(CATALOG[k].filter((x) => x.as3d).map((x) => [x.id, x.as3d]))]));
689
+ /** Id equivalente para el visor 3D (las piezas nuevas sin modelo propio usan la más parecida). */
690
+ export const as3d = (kind, id) => ALIAS3D[kind]?.[id] || id;
@@ -55,6 +55,23 @@ const AUTO = {
55
55
  wide: ['scales', 'ridges', 'fuzz', 'speckles'],
56
56
  tall: ['ridges', 'scales', 'fur', 'speckles'],
57
57
  squircle: ['scales', 'fuzz', 'speckles', 'panels'],
58
+ // catálogo 2 (formas nuevas; las antiguas no cambian)
59
+ bear: ['fur', 'fur', 'fuzz'],
60
+ owl: ['feathers', 'feathers', 'fuzz'],
61
+ slime: ['gloss', 'gloss', 'speckles'],
62
+ gem: ['gloss', 'panels'],
63
+ barrel: ['ridges', 'panels'],
64
+ capsule: ['gloss', 'panels', 'none'],
65
+ potato: ['speckles', 'speckles', 'fuzz'],
66
+ apple: ['gloss', 'speckles'],
67
+ lemon: ['speckles', 'gloss'],
68
+ peanut: ['speckles', 'ridges'],
69
+ bun: ['fuzz', 'speckles', 'none'],
70
+ onigiri: ['none', 'speckles'],
71
+ bell: ['gloss', 'none'],
72
+ flame: ['gloss', 'fuzz'],
73
+ alien: ['gloss', 'scales', 'speckles'],
74
+ chubby: ['fur', 'fuzz', 'speckles'],
58
75
  };
59
76
  const AUTO_ANY = ['fur', 'fuzz', 'scales', 'feathers', 'speckles', 'ridges', 'gloss', 'fuzz', 'fur', 'none'];
60
77
 
@@ -65,6 +82,7 @@ export function textureOf(spec) {
65
82
  const f = spec.finish;
66
83
  const sh = spec.body.shape;
67
84
  if (f === 'neon' || f === 'sketch') return 'none';
85
+ if (f === 'metal' || f === 'glass' || f === 'holo' || f === 'pearl') return 'none'; // acabados lisos del catálogo 2
68
86
  if (sh === 'robot' || spec.body.pattern === 'circuit') return 'panels';
69
87
  if (sh === 'cat' || spec.body.pattern === 'tiger' || spec.accessories.some((a) => a.id === 'catears' || a.id === 'bunny')) return 'fur';
70
88
  const h = skinHash(spec);
@@ -208,7 +226,154 @@ function speckTile(u, r) {
208
226
  return { W, H: W, body: blot(dk, '#000', 0.16) };
209
227
  }
210
228
 
211
- const TILES = { fur: furTile, fuzz: fuzzTile, scales: scalesTile, feathers: feathersTile, speckles: speckTile };
229
+ /** Punto de media: filas de uves. */
230
+ function knitTile(u) {
231
+ const W = 7 * u;
232
+ const H = 6 * u;
233
+ let dk = '';
234
+ let lt = '';
235
+ for (const y of [-H, 0, H]) {
236
+ dk += `M0 ${n(y)}L${n(W / 2)} ${n(y + H * 0.9)}L${n(W)} ${n(y)}`;
237
+ lt += `M${n(0.9 * u)} ${n(y + 0.6 * u)}L${n(W / 2)} ${n(y + H * 0.6)}L${n(W - 0.9 * u)} ${n(y + 0.6 * u)}`;
238
+ }
239
+ return { W, H, body: ink(dk, '#000', 0.16, 1.1 * u) + ink(lt, '#fff', 0.22, 0.8 * u) };
240
+ }
241
+
242
+ /** Lunares regulares. */
243
+ function polkaTile(u) {
244
+ const W = 14 * u;
245
+ const rr = 2.3 * u;
246
+ let lt = '';
247
+ for (const [x, y] of [[W / 4, W / 4], [(3 * W) / 4, (3 * W) / 4]]) lt += dot(x, y, rr);
248
+ return { W, H: W, body: blot(lt, '#fff', 0.5) };
249
+ }
250
+
251
+ /** Purpurina: destellos de cuatro puntas y motas. */
252
+ function glitterTile(u, r) {
253
+ const W = 12 * u;
254
+ const star = (x, y, s) =>
255
+ `M${n(x)} ${n(y - s)}L${n(x + s * 0.22)} ${n(y - s * 0.22)}L${n(x + s)} ${n(y)}L${n(x + s * 0.22)} ${n(y + s * 0.22)}L${n(x)} ${n(y + s)}L${n(x - s * 0.22)} ${n(y + s * 0.22)}L${n(x - s)} ${n(y)}L${n(x - s * 0.22)} ${n(y - s * 0.22)}Z`;
256
+ let lt = '';
257
+ let dk = '';
258
+ for (let i = 0; i < 9; i++) {
259
+ const x = r() * W;
260
+ const y = r() * W;
261
+ const s = (i < 2 ? 1.8 : 0.4 + r() * 0.5) * u;
262
+ for (const [ox, oy] of wrap([x - s, y - s, x + s, y + s], W, W)) {
263
+ if (i < 2) lt += star(x + ox, y + oy, s);
264
+ else if (i % 3) lt += dot(x + ox, y + oy, s);
265
+ else dk += dot(x + ox, y + oy, s);
266
+ }
267
+ }
268
+ return { W, H: W, body: blot(dk, '#000', 0.18) + blot(lt, '#fff', 0.75) };
269
+ }
270
+
271
+ /** Piedra: grietas finas y granitos. */
272
+ function stoneTile(u, r) {
273
+ const W = 26 * u;
274
+ const cl = (v) => Math.min(W, Math.max(0, v));
275
+ let ck = '';
276
+ let dk = '';
277
+ let lt = '';
278
+ for (let i = 0; i < 3; i++) {
279
+ let x = r() * W;
280
+ let y = r() * W;
281
+ ck += `M${n(x)} ${n(y)}`;
282
+ const a = r() * Math.PI * 2;
283
+ for (let k = 0; k < 3; k++) {
284
+ const b = a + (r() - 0.5) * 1.4;
285
+ x = cl(x + Math.cos(b) * 4.2 * u);
286
+ y = cl(y + Math.sin(b) * 4.2 * u);
287
+ ck += `L${n(x)} ${n(y)}`;
288
+ }
289
+ }
290
+ for (let i = 0; i < 10; i++) {
291
+ const x = r() * W;
292
+ const y = r() * W;
293
+ const rr = (0.3 + r() * 0.5) * u;
294
+ for (const [ox, oy] of wrap([x - rr, y - rr, x + rr, y + rr], W, W)) {
295
+ if (i % 2) dk += dot(x + ox, y + oy, rr);
296
+ else lt += dot(x + ox, y + oy, rr);
297
+ }
298
+ }
299
+ const lip = `<path d="${ck}" fill="none" stroke="#fff" stroke-opacity=".22" stroke-width="${n(0.6 * u)}" stroke-linecap="round" stroke-linejoin="round" transform="translate(${n(0.6 * u)} ${n(0.6 * u)})"/>`;
300
+ return { W, H: W, body: lip + ink(ck, '#000', 0.3, 0.8 * u) + blot(dk, '#000', 0.18) + blot(lt, '#fff', 0.3) };
301
+ }
302
+
303
+ /** Veta de madera: líneas onduladas verticales. */
304
+ function woodTile(u) {
305
+ const W = 10 * u;
306
+ const H = 40 * u;
307
+ const vein = (x, a) => `M${n(x)} 0Q${n(x + a)} ${n(H / 4)} ${n(x)} ${n(H / 2)}Q${n(x - a)} ${n((3 * H) / 4)} ${n(x)} ${n(H)}`;
308
+ return { W, H, body: ink(vein(W * 0.25, 1.8 * u) + vein(W * 0.7, -1.2 * u), '#000', 0.15, 0.9 * u) + ink(vein(W * 0.45, 1.4 * u), '#fff', 0.16, 0.7 * u) };
309
+ }
310
+
311
+ /** Burbujas: aros claros con un brillito. */
312
+ function bubblesTile(u, r) {
313
+ const W = 26 * u;
314
+ let ring = '';
315
+ let shine = '';
316
+ // una por cuadrante, desplazada al azar: repartidas sin amontonarse
317
+ for (let i = 0; i < 4; i++) {
318
+ const rr = (1.3 + r() * 2.2) * u;
319
+ const x = ((i % 2) + 0.2 + r() * 0.6) * (W / 2);
320
+ const y = ((i >> 1) + 0.2 + r() * 0.6) * (W / 2);
321
+ for (const [ox, oy] of wrap([x - rr, y - rr, x + rr, y + rr], W, W)) {
322
+ ring += dot(x + ox, y + oy, rr);
323
+ shine += dot(x + ox - rr * 0.38, y + oy - rr * 0.38, rr * 0.28);
324
+ }
325
+ }
326
+ return { W, H: W, body: `<path d="${ring}" fill="#fff" fill-opacity=".12" stroke="#fff" stroke-opacity=".5" stroke-width="${n(0.6 * u)}"/>${blot(shine, '#fff', 0.7)}` };
327
+ }
328
+
329
+ /** Costuras: retales unidos con pespunte. */
330
+ function stitchTile(u) {
331
+ const W = 22 * u;
332
+ const seam = `M0 ${n(W / 2)}H${n(W)}M${n(W / 2)} 0V${n(W)}`;
333
+ const st = `M0 ${n(W / 2 + 1.5 * u)}H${n(W)}M${n(W / 2 + 1.5 * u)} 0V${n(W)}`;
334
+ const dash = `<path d="${st}" fill="none" stroke="#fff" stroke-opacity=".6" stroke-width="${n(0.7 * u)}" stroke-dasharray="${n(1.6 * u)} ${n(1.4 * u)}"/>`;
335
+ return { W, H: W, body: ink(seam, '#000', 0.2, 0.9 * u) + dash };
336
+ }
337
+
338
+ /** Panal: rejilla hexagonal. */
339
+ function honeycombTile(u) {
340
+ const s = 4.6 * u;
341
+ const W = Math.sqrt(3) * s;
342
+ const H = 3 * s;
343
+ const hex = (x, y) => `M${n(x)} ${n(y - s)}L${n(x + W / 2)} ${n(y - s / 2)}L${n(x + W / 2)} ${n(y + s / 2)}L${n(x)} ${n(y + s)}L${n(x - W / 2)} ${n(y + s / 2)}L${n(x - W / 2)} ${n(y - s / 2)}Z`;
344
+ let dk = '';
345
+ for (const [x, y] of [[0, 0], [W, 0], [W / 2, H / 2], [0, H], [W, H]]) dk += hex(x, y);
346
+ const edge = (c, op, w, t) => `<path d="${dk}" fill="none" stroke="${c}" stroke-opacity="${op}" stroke-width="${n(w)}" stroke-linejoin="round"${t ? ` transform="translate(0 ${n(t)})"` : ''}/>`;
347
+ return { W, H, body: edge('#fff', 0.2, 0.7 * u, 0.8 * u) + edge('#000', 0.2, 0.9 * u, 0) };
348
+ }
349
+
350
+ /** Acolchado: rombos con botones. */
351
+ function quiltTile(u) {
352
+ const W = 16 * u;
353
+ const o = 1.1 * u;
354
+ const dk = `M0 0L${n(W)} ${n(W)}M${n(W)} 0L0 ${n(W)}`;
355
+ const lt = `M0 ${n(o)}L${n(W - o)} ${n(W)}M${n(W)} ${n(o)}L${n(o)} ${n(W)}`;
356
+ let bt = '';
357
+ for (const [x, y] of [[0, 0], [W, 0], [0, W], [W, W], [W / 2, W / 2]]) bt += dot(x, y, 1 * u);
358
+ return { W, H: W, body: ink(lt, '#fff', 0.2, 1 * u) + ink(dk, '#000', 0.15, 1.1 * u) + blot(bt, '#000', 0.22) };
359
+ }
360
+
361
+ const TILES = {
362
+ fur: furTile,
363
+ fuzz: fuzzTile,
364
+ scales: scalesTile,
365
+ feathers: feathersTile,
366
+ speckles: speckTile,
367
+ knit: knitTile,
368
+ polka: polkaTile,
369
+ glitter: glitterTile,
370
+ stone: stoneTile,
371
+ wood: woodTile,
372
+ bubbles: bubblesTile,
373
+ stitches: stitchTile,
374
+ honeycomb: honeycombTile,
375
+ quilt: quiltTile,
376
+ };
212
377
 
213
378
  // ───────────────────────────── piel del cuerpo ─────────────────────────────
214
379