@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
@@ -0,0 +1,288 @@
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
+ * Ojos: p = { r, lx, ly (-1..1 mirada), side (-1 izq, 1 der), iris, ink, white, lid, mood }, centrados en (0,0).
6
+ * El lado interior (hacia la nariz) está en x = -side.
7
+ */
8
+
9
+ /** @param {object} h ayudantes de dibujo de parts.js */
10
+ export default ({ n, shade, shine, star, heartPath, look, pupil }) => {
11
+ const stroke = (c, w) => `stroke="${c}" stroke-width="${n(w)}" stroke-linecap="round" stroke-linejoin="round"`;
12
+ /** Forma blanca recortada: lo de dentro (pupila…) no se sale de `shape`. */
13
+ const clipped = (shape, fill, inner) => {
14
+ const id = `ec${Math.round(Math.random() * 1e9)}`;
15
+ return `<clipPath id="${id}"><path d="${shape}"/></clipPath><path d="${shape}" fill="${fill}"/><g clip-path="url(#${id})">${inner}</g>`;
16
+ };
17
+ const circ = (r, cx = 0, cy = 0) => `M${n(cx - r)} ${n(cy)}a${n(r)} ${n(r)} 0 1 0 ${n(r * 2)} 0a${n(r)} ${n(r)} 0 1 0 ${n(-r * 2)} 0Z`;
18
+ const poly = (pts) => `M${pts.map(([x, y]) => `${n(x)} ${n(y)}`).join('L')}Z`;
19
+
20
+ return [
21
+ {
22
+ id: 'puppy', label: 'Perrito', en: 'Puppy', pt: 'Cachorrinho', as3d: 'kawaii',
23
+ draw: (p) => {
24
+ const r = p.r;
25
+ const [x, y] = look(p, 0.12);
26
+ return `<circle cx="${x}" cy="${y}" r="${n(r * 1.05)}" fill="${p.ink}"/><circle cx="${x}" cy="${n(+y + r * 0.38)}" r="${n(r * 0.55)}" fill="${p.iris}" opacity=".55"/>${shine(x - r * 0.34, y - r * 0.36, r * 0.36)}${shine(x + r * 0.4, y - r * 0.02, r * 0.15)}${shine(x - r * 0.02, y + r * 0.5, r * 0.1, 0.85)}`;
27
+ },
28
+ },
29
+ {
30
+ id: 'droopy', label: 'Caídos', en: 'Droopy', pt: 'Caídos', as3d: 'sad',
31
+ draw: (p) => {
32
+ const r = p.r;
33
+ const s = p.side;
34
+ return `${clipped(circ(r), p.white, `${pupil(p, r * 0.52, 0.3)}<path d="M${n(-s * (r + 1))} ${n(-r - 1)}H${n(s * (r + 1))}V${n(r * 0.1)}Q${n(-s * r * 0.1)} ${n(-r * 0.25)} ${n(-s * (r + 1))} ${n(-r * 0.55)}Z" fill="${p.lid}"/>`)}<path d="M${n(s * r)} ${n(r * 0.08)}Q${n(-s * r * 0.1)} ${n(-r * 0.25)} ${n(-s * r)} ${n(-r * 0.52)}" fill="none" ${stroke(p.ink, r * 0.16)}/>`;
35
+ },
36
+ },
37
+ { id: 'moon', label: 'Lunitas', en: 'Crescents', pt: 'Luazinhas', as3d: 'happy', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.95)} ${n(p.r * 0.4)}Q0 ${n(-p.r * 1.3)} ${n(p.r * 0.95)} ${n(p.r * 0.4)}Q0 ${n(-p.r * 0.35)} ${n(-p.r * 0.95)} ${n(p.r * 0.4)}Z" fill="${p.ink}" ${stroke(p.ink, p.r * 0.1)}/>` },
38
+ { id: 'squint', label: 'Apretados', en: 'Squeezed', pt: 'Apertados', as3d: 'closed', noBlink: true, draw: (p) => { const d = -p.side; return `<path d="M${n(-d * p.r * 0.55)} ${n(-p.r * 0.7)}L${n(d * p.r * 0.6)} 0L${n(-d * p.r * 0.55)} ${n(p.r * 0.7)}" fill="none" ${stroke(p.ink, p.r * 0.3)}/>`; } },
39
+ { id: 'uu', label: 'U u', en: 'U u', pt: 'U u', as3d: 'closed', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.75)} ${n(-p.r * 0.45)}C${n(-p.r * 0.75)} ${n(p.r * 0.85)} ${n(p.r * 0.75)} ${n(p.r * 0.85)} ${n(p.r * 0.75)} ${n(-p.r * 0.45)}" fill="none" ${stroke(p.ink, p.r * 0.26)}/>` },
40
+ {
41
+ id: 'flower', label: 'Florecitas', en: 'Flowers', pt: 'Florzinhas', as3d: 'star',
42
+ draw: (p) => {
43
+ const r = p.r;
44
+ const [x, y] = look(p, 0.12);
45
+ const petals = [0, 72, 144, 216, 288].map((a) => `<ellipse cy="${n(-r * 0.58)}" rx="${n(r * 0.4)}" ry="${n(r * 0.52)}" transform="rotate(${a})" fill="${p.iris}" stroke="${shade(p.iris, -0.35)}" stroke-width="${n(r * 0.08)}"/>`).join('');
46
+ return `<g transform="translate(${x} ${y})">${petals}<circle r="${n(r * 0.42)}" fill="#facc15" stroke="${shade('#facc15', -0.35)}" stroke-width="${n(r * 0.08)}"/><circle r="${n(r * 0.24)}" fill="${p.ink}"/>${shine(-r * 0.1, -r * 0.12, r * 0.09)}</g>`;
47
+ },
48
+ },
49
+ {
50
+ id: 'diamond', label: 'Rombos', en: 'Diamonds', pt: 'Losangos', as3d: 'star',
51
+ draw: (p) => {
52
+ const r = p.r;
53
+ const [x, y] = look(p, 0.1);
54
+ return `<g transform="translate(${x} ${y})"><path d="M0 ${n(-r * 1.15)}L${n(r * 0.85)} 0L0 ${n(r * 1.15)}L${n(-r * 0.85)} 0Z" fill="${p.iris}" ${stroke(shade(p.iris, -0.4), r * 0.12)}/><path d="M0 ${n(-r * 1.05)}L0 0L${n(-r * 0.76)} 0Z" fill="#fff" opacity=".3"/><path d="M0 ${n(r * 1.05)}L0 0L${n(r * 0.76)} 0Z" fill="#000" opacity=".15"/><path d="M0 ${n(-r * 0.45)}L${n(r * 0.32)} 0L0 ${n(r * 0.45)}L${n(-r * 0.32)} 0Z" fill="${p.ink}"/>${shine(-r * 0.22, -r * 0.35, r * 0.14)}</g>`;
55
+ },
56
+ },
57
+ {
58
+ id: 'galaxy', label: 'Galaxia', en: 'Galaxy', pt: 'Galáxia', as3d: 'kawaii',
59
+ draw: (p) => {
60
+ const r = p.r;
61
+ const [x, y] = look(p, 0.1);
62
+ const tw = [[0.35, -0.2, 0.2], [-0.25, 0.4, 0.14], [0.45, 0.45, 0.1], [-0.5, -0.05, 0.08]].map(([a, b, k]) => `<path d="${star(r * k, 0.35, 4)}" transform="translate(${n(+x + a * r)} ${n(+y + b * r)})" fill="#fff"/>`).join('');
63
+ return `<circle cx="${x}" cy="${y}" r="${r}" fill="#1e1b4b"/><circle cx="${n(+x + r * 0.15)}" cy="${n(+y + r * 0.2)}" r="${n(r * 0.62)}" fill="${p.iris}" opacity=".55"/><circle cx="${n(+x - r * 0.2)}" cy="${n(+y + r * 0.35)}" r="${n(r * 0.36)}" fill="#f472b6" opacity=".45"/>${tw}${shine(x - r * 0.38, y - r * 0.4, r * 0.24)}`;
64
+ },
65
+ },
66
+ {
67
+ id: 'fierce', label: 'Feroces', en: 'Fierce', pt: 'Ferozes', as3d: 'angry',
68
+ draw: (p) => {
69
+ const r = p.r;
70
+ const s = p.side;
71
+ const shape = `M${n(s * r * 1.2)} ${n(-r * 0.75)}L${n(-s * r * 1.1)} ${n(-r * 0.15)}Q${n(-s * r * 0.6)} ${n(r * 1.05)} ${n(s * r * 0.5)} ${n(r * 0.75)}Q${n(s * r * 1.3)} ${n(r * 0.4)} ${n(s * r * 1.2)} ${n(-r * 0.75)}Z`;
72
+ const [x, y] = look(p, 0.2);
73
+ return `${clipped(shape, p.white, `<circle cx="${n(+x + s * r * 0.05)}" cy="${n(+y + r * 0.2)}" r="${n(r * 0.55)}" fill="${p.iris}"/><circle cx="${n(+x + s * r * 0.05)}" cy="${n(+y + r * 0.2)}" r="${n(r * 0.3)}" fill="${p.ink}"/>${shine(+x + s * r * 0.05 - r * 0.15, +y + r * 0.04, r * 0.11)}`)}<path d="M${n(s * r * 1.3)} ${n(-r * 0.82)}L${n(-s * r * 1.15)} ${n(-r * 0.15)}" fill="none" ${stroke(p.ink, r * 0.26)}/>`;
74
+ },
75
+ },
76
+ { id: 'stitched', label: 'Cosidos', en: 'Stitched', pt: 'Costurados', as3d: 'closed', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.85)} 0H${n(p.r * 0.85)}" fill="none" ${stroke(p.ink, p.r * 0.18)}/><path d="${[-0.5, 0, 0.5].map((k) => `M${n(k * p.r - p.r * 0.08)} ${n(-p.r * 0.38)}L${n(k * p.r + p.r * 0.08)} ${n(p.r * 0.38)}`).join('')}" fill="none" ${stroke(p.ink, p.r * 0.14)}/>` },
77
+ {
78
+ id: 'shocked', label: 'En shock', en: 'Shocked', pt: 'Em choque', as3d: 'wide',
79
+ draw: (p) => {
80
+ const r = p.r;
81
+ const [x, y] = look(p, 0.45);
82
+ return `<ellipse rx="${n(r * 0.95)}" ry="${n(r * 1.3)}" fill="${p.white}" ${stroke(p.ink, r * 0.1)}/><circle cx="${x}" cy="${y}" r="${n(r * 0.17)}" fill="${p.ink}"/>`;
83
+ },
84
+ },
85
+ {
86
+ id: 'lovestruck', label: 'Enamorados', en: 'Lovestruck', pt: 'Apaixonados', as3d: 'heart',
87
+ draw: (p) => {
88
+ const r = p.r;
89
+ const [x, y] = look(p, 0.1);
90
+ return `<circle cx="${x}" cy="${y}" r="${n(r * 0.98)}" fill="${p.ink}"/><path d="${heartPath(r * 0.42)}" transform="translate(${n(x - r * 0.3)} ${n(y - r * 0.28)})" fill="#fb7185"/><path d="${heartPath(r * 0.2)}" transform="translate(${n(+x + r * 0.4)} ${n(+y + r * 0.38)})" fill="#fda4af"/>${shine(x - r * 0.42, y - r * 0.44, r * 0.1)}`;
91
+ },
92
+ },
93
+ {
94
+ id: 'ringed', label: 'Iris grande', en: 'Big iris', pt: 'Íris grande', as3d: 'round',
95
+ draw: (p) => {
96
+ const r = p.r;
97
+ const [x, y] = look(p, 0.3);
98
+ return `<circle r="${r}" fill="${p.white}"/><circle cx="${x}" cy="${y}" r="${n(r * 0.66)}" fill="${p.iris}" ${stroke(shade(p.iris, -0.45), r * 0.1)}/><circle cx="${x}" cy="${y}" r="${n(r * 0.42)}" fill="${shade(p.iris, 0.3)}" opacity=".5"/><circle cx="${x}" cy="${y}" r="${n(r * 0.3)}" fill="${p.ink}"/>${shine(x - r * 0.24, y - r * 0.28, r * 0.17)}`;
99
+ },
100
+ },
101
+ {
102
+ id: 'smug', label: 'Satisfechos', en: 'Smug', pt: 'Convencidos', as3d: 'tired',
103
+ draw: (p) => {
104
+ const r = p.r;
105
+ return `${clipped(circ(r), p.white, `${pupil(p, r * 0.5, 0.25)}<path d="M${-r - 1} ${n(-r - 1)}H${r + 1}V${n(-r * 0.3)}H${-r - 1}Z" fill="${p.lid}"/><path d="M${-r - 1} ${r + 1}H${r + 1}V${n(r * 0.35)}Q0 ${n(-r * 0.05)} ${-r - 1} ${n(r * 0.35)}Z" fill="${p.lid}"/>`)}<path d="M${n(-r)} ${n(-r * 0.3)}H${n(r)}" fill="none" ${stroke(p.ink, r * 0.18)}/><path d="M${n(-r * 0.9)} ${n(r * 0.33)}Q0 ${n(-r * 0.02)} ${n(r * 0.9)} ${n(r * 0.33)}" fill="none" ${stroke(shade(p.lid, -0.3), r * 0.1)}/>`;
106
+ },
107
+ },
108
+ {
109
+ id: 'snake', label: 'Serpiente', en: 'Snake', pt: 'Cobra', as3d: 'cat',
110
+ draw: (p) => {
111
+ const r = p.r;
112
+ const [x, y] = look(p, 0.3);
113
+ return `<ellipse rx="${n(r)}" ry="${n(r * 0.92)}" fill="${p.iris}" ${stroke(shade(p.iris, -0.45), r * 0.08)}/><ellipse cy="${n(r * 0.2)}" rx="${n(r * 0.66)}" ry="${n(r * 0.55)}" fill="${shade(p.iris, 0.35)}" opacity=".55"/><path d="M${x} ${n(+y - r * 0.85)}Q${n(+x + r * 0.26)} ${y} ${x} ${n(+y + r * 0.85)}Q${n(x - r * 0.26)} ${y} ${x} ${n(+y - r * 0.85)}Z" fill="${p.ink}"/>${shine(x - r * 0.42, y - r * 0.38, r * 0.14)}`;
114
+ },
115
+ },
116
+ {
117
+ id: 'frog', label: 'Rana', en: 'Frog', pt: 'Sapo', as3d: 'round',
118
+ draw: (p) => {
119
+ const r = p.r;
120
+ const [x, y] = look(p, 0.28);
121
+ return `<circle cy="${n(-r * 0.1)}" r="${n(r * 1.18)}" fill="${p.lid}" ${stroke(shade(p.lid, -0.25), r * 0.08)}/><circle r="${n(r * 0.86)}" fill="${p.white}"/><ellipse cx="${x}" cy="${y}" rx="${n(r * 0.55)}" ry="${n(r * 0.28)}" fill="${p.ink}"/>${shine(x - r * 0.3, y - r * 0.32, r * 0.14)}`;
122
+ },
123
+ },
124
+ {
125
+ id: 'coin', label: 'Monedas', en: 'Coins', pt: 'Moedas', as3d: 'button',
126
+ draw: (p) => {
127
+ const r = p.r;
128
+ return `<circle r="${r}" fill="#facc15" ${stroke('#b45309', r * 0.12)}/><circle r="${n(r * 0.68)}" fill="none" stroke="#d97706" stroke-width="${n(r * 0.09)}"/><path d="${star(r * 0.42)}" fill="#f59e0b"/><path d="M${n(-r * 0.62)} ${n(-r * 0.3)}A${n(r * 0.7)} ${n(r * 0.7)} 0 0 1 ${n(-r * 0.1)} ${n(-r * 0.72)}" fill="none" ${stroke('#fff', r * 0.14)} opacity=".7"/>`;
129
+ },
130
+ },
131
+ {
132
+ id: 'pie', label: 'Dibujo animado', en: 'Cartoon', pt: 'Desenho animado', as3d: 'bean',
133
+ draw: (p) => {
134
+ const r = p.r;
135
+ const [x, y] = look(p, 0.22);
136
+ const shape = `M0 ${n(-r * 1.1)}C${n(r * 0.95)} ${n(-r * 1.1)} ${n(r * 0.95)} ${n(r * 1.1)} 0 ${n(r * 1.1)}C${n(-r * 0.95)} ${n(r * 1.1)} ${n(-r * 0.95)} ${n(-r * 1.1)} 0 ${n(-r * 1.1)}Z`;
137
+ return `${clipped(shape, p.white, `<ellipse cx="${x}" cy="${n(+y + r * 0.2)}" rx="${n(r * 0.4)}" ry="${n(r * 0.62)}" fill="${p.ink}"/><path d="M${x} ${n(+y + r * 0.05)}L${n(+x + r * 0.5)} ${n(+y - r * 0.3)}L${n(+x + r * 0.25)} ${n(+y - r * 0.65)}Z" fill="${p.white}"/>`)}<path d="${shape}" fill="none" ${stroke(p.ink, r * 0.1)}/>`;
138
+ },
139
+ },
140
+ {
141
+ id: 'sideeye', label: 'De reojo', en: 'Side-eye', pt: 'De rabo de olho', as3d: 'tired',
142
+ draw: (p) => {
143
+ const r = p.r;
144
+ const x = n(r * 0.42 + p.lx * r * 0.1);
145
+ return `${clipped(circ(r), p.white, `<circle cx="${x}" cy="${n(r * 0.1)}" r="${n(r * 0.5)}" fill="${p.ink}"/>${shine(x - r * 0.18, r * -0.08, r * 0.15)}<path d="M${-r - 1} ${n(-r - 1)}H${r + 1}V${n(-r * 0.25)}H${-r - 1}Z" fill="${p.lid}"/>`)}<path d="M${n(-r)} ${n(-r * 0.25)}H${n(r)}" fill="none" ${stroke(p.ink, r * 0.16)}/>`;
146
+ },
147
+ },
148
+ {
149
+ id: 'mischief', label: 'Traviesos', en: 'Mischievous', pt: 'Travessos', as3d: 'happy',
150
+ draw: (p) => {
151
+ const r = p.r;
152
+ const shape = `M${n(-r * 1.05)} ${n(r * 0.35)}Q0 ${n(-r * 1.55)} ${n(r * 1.05)} ${n(r * 0.35)}Q0 ${n(r * 0.05)} ${n(-r * 1.05)} ${n(r * 0.35)}Z`;
153
+ const [x, y] = look(p, 0.25);
154
+ return `${clipped(shape, p.white, `<circle cx="${x}" cy="${n(+y - r * 0.12)}" r="${n(r * 0.48)}" fill="${p.ink}"/>${shine(x - r * 0.16, y - r * 0.3, r * 0.14)}`)}<path d="M${n(-r * 1.05)} ${n(r * 0.35)}Q0 ${n(-r * 1.55)} ${n(r * 1.05)} ${n(r * 0.35)}" fill="none" ${stroke(p.ink, r * 0.2)}/><path d="M${n(-r * 0.9)} ${n(r * 0.3)}Q0 ${n(r * 0.05)} ${n(r * 0.9)} ${n(r * 0.3)}" fill="none" ${stroke(p.ink, r * 0.1)}/>`;
155
+ },
156
+ },
157
+ {
158
+ id: 'gem', label: 'Joyas', en: 'Gems', pt: 'Joias', as3d: 'star',
159
+ draw: (p) => {
160
+ const r = p.r;
161
+ const [x, y] = look(p, 0.1);
162
+ const hex = (k) => Array.from({ length: 6 }, (_, i) => [Math.cos(Math.PI / 6 + (i * Math.PI) / 3) * r * k, Math.sin(Math.PI / 6 + (i * Math.PI) / 3) * r * k]);
163
+ const o = hex(1.05);
164
+ const m = hex(0.5);
165
+ const facets = o.map((a, i) => { const b = o[(i + 1) % 6]; return `<path d="${poly([a, b, m[(i + 1) % 6], m[i]])}" fill="${i >= 3 ? shade(p.iris, 0.35 - (i - 3) * 0.12) : shade(p.iris, -0.1 - i * 0.08)}"/>`; }).join('');
166
+ return `<g transform="translate(${x} ${y})">${facets}<path d="${poly(m)}" fill="${shade(p.iris, 0.15)}"/><path d="${poly(o)}" fill="none" ${stroke(shade(p.iris, -0.5), r * 0.1)}/>${shine(-r * 0.15, -r * 0.2, r * 0.13)}</g>`;
167
+ },
168
+ },
169
+ {
170
+ id: 'rainbow', label: 'Arcoíris', en: 'Rainbow', pt: 'Arco-íris', as3d: 'hypno',
171
+ draw: (p) => {
172
+ const r = p.r;
173
+ const [x, y] = look(p, 0.25);
174
+ const rings = [['#ef4444', 0.72], ['#f59e0b', 0.6], ['#22c55e', 0.48], ['#3b82f6', 0.36]].map(([c, k]) => `<circle cx="${x}" cy="${y}" r="${n(r * k)}" fill="${c}"/>`).join('');
175
+ return `<circle r="${r}" fill="${p.white}"/>${rings}<circle cx="${x}" cy="${y}" r="${n(r * 0.22)}" fill="${p.ink}"/>${shine(x - r * 0.3, y - r * 0.32, r * 0.14)}`;
176
+ },
177
+ },
178
+ {
179
+ id: 'owl', label: 'Búho', en: 'Owl', pt: 'Coruja', as3d: 'wide',
180
+ draw: (p) => {
181
+ const r = p.r;
182
+ const [x, y] = look(p, 0.25);
183
+ const ray = Array.from({ length: 12 }, (_, i) => { const a = (i * Math.PI) / 6; return `M${n(Math.cos(a) * r * 0.98)} ${n(Math.sin(a) * r * 0.98)}L${n(Math.cos(a) * r * 1.18)} ${n(Math.sin(a) * r * 1.18)}`; }).join('');
184
+ return `<circle r="${n(r * 1.2)}" fill="${shade(p.lid, 0.35)}" ${stroke(shade(p.lid, -0.25), r * 0.07)}/><path d="${ray}" fill="none" ${stroke(shade(p.lid, -0.25), r * 0.08)}/><circle r="${n(r * 0.92)}" fill="${p.white}"/><circle cx="${x}" cy="${y}" r="${n(r * 0.68)}" fill="${p.iris}"/><circle cx="${x}" cy="${y}" r="${n(r * 0.4)}" fill="${p.ink}"/>${shine(x - r * 0.26, y - r * 0.28, r * 0.16)}`;
185
+ },
186
+ },
187
+ {
188
+ id: 'wobbly', label: 'Gelatina', en: 'Wobbly', pt: 'Gelatina', as3d: 'googly',
189
+ draw: (p) => {
190
+ const r = p.r;
191
+ const pts = Array.from({ length: 28 }, (_, i) => { const a = (i / 28) * Math.PI * 2; const k = 1 + 0.08 * Math.sin(a * 3 + p.side) + 0.05 * Math.cos(a * 5); return [Math.cos(a) * r * k, Math.sin(a) * r * k]; });
192
+ return `<path d="${poly(pts)}" fill="${p.white}" ${stroke(p.ink, r * 0.1)}/>${pupil(p, r * 0.48, 0.38)}`;
193
+ },
194
+ },
195
+ {
196
+ id: 'bubble', label: 'Burbuja', en: 'Bubble', pt: 'Bolha', as3d: 'glow',
197
+ draw: (p) => {
198
+ const r = p.r;
199
+ const [x, y] = look(p, 0.25);
200
+ return `<circle r="${n(r * 1.02)}" fill="${p.iris}" fill-opacity=".3" ${stroke(p.iris, r * 0.1)}/><circle cx="${x}" cy="${y}" r="${n(r * 0.42)}" fill="${shade(p.iris, -0.35)}"/><path d="M${n(-r * 0.72)} ${n(r * 0.05)}Q${n(-r * 0.72)} ${n(-r * 0.68)} ${n(-r * 0.02)} ${n(-r * 0.76)}" fill="none" ${stroke('#fff', r * 0.17)} opacity=".9"/>${shine(r * 0.45, r * 0.45, r * 0.1, 0.8)}`;
201
+ },
202
+ },
203
+ { id: 'dash', label: 'Rayitas', en: 'Dashes', pt: 'Risquinhos', as3d: 'closed', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.72)} 0H${n(p.r * 0.72)}" fill="none" ${stroke(p.ink, p.r * 0.32)}/>` },
204
+ { id: 'plus', label: 'Cruces', en: 'Plus', pt: 'Cruzes', as3d: 'cross', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.62)} 0H${n(p.r * 0.62)}M0 ${n(-p.r * 0.62)}V${n(p.r * 0.62)}" fill="none" ${stroke(p.ink, p.r * 0.3)}/>` },
205
+ {
206
+ id: 'confused', label: 'Confundidos', en: 'Confused', pt: 'Confusos', as3d: 'round',
207
+ draw: (p) => {
208
+ const k = p.side < 0 ? 1.15 : 0.72;
209
+ const r = p.r * k;
210
+ const [x, y] = look({ ...p, r }, 0.35);
211
+ return `<circle cy="${n(p.side < 0 ? 0 : p.r * 0.15)}" r="${n(r)}" fill="${p.white}" ${stroke(p.ink, p.r * 0.08)}/><g transform="translate(0 ${n(p.side < 0 ? 0 : p.r * 0.15)})"><circle cx="${x}" cy="${y}" r="${n(r * 0.42)}" fill="${p.ink}"/>${shine(x - r * 0.15, y - r * 0.17, r * 0.13)}</g>`;
212
+ },
213
+ },
214
+ {
215
+ id: 'starry', label: 'Pupila estrella', en: 'Star pupils', pt: 'Pupila estrela', as3d: 'star',
216
+ draw: (p) => {
217
+ const r = p.r;
218
+ const [x, y] = look(p, 0.3);
219
+ return `<circle r="${r}" fill="${p.white}"/><path d="${star(r * 0.62, 0.48)}" transform="translate(${x} ${y})" fill="${p.iris}" ${stroke(shade(p.iris, -0.45), r * 0.08)}/>${shine(x - r * 0.12, y - r * 0.16, r * 0.1)}`;
220
+ },
221
+ },
222
+ {
223
+ id: 'heartpupil', label: 'Pupila corazón', en: 'Heart pupils', pt: 'Pupila coração', as3d: 'heart',
224
+ draw: (p) => {
225
+ const r = p.r;
226
+ const [x, y] = look(p, 0.3);
227
+ return `<circle r="${r}" fill="${p.white}"/><path d="${heartPath(r * 0.55)}" transform="translate(${x} ${n(+y + r * 0.05)})" fill="#ef3b5d"/>${shine(x - r * 0.2, y - r * 0.12, r * 0.1)}`;
228
+ },
229
+ },
230
+ {
231
+ id: 'welling', label: 'A punto de llorar', en: 'Welling up', pt: 'Quase chorando', as3d: 'teary',
232
+ draw: (p) => {
233
+ const r = p.r;
234
+ const [x, y] = look(p, 0.1);
235
+ const shape = circ(r * 0.98, +x, +y);
236
+ return `${clipped(shape, p.ink, `<path d="M${n(x - r)} ${n(+y + r * 0.3)}Q${n(x - r * 0.5)} ${n(+y + r * 0.15)} ${x} ${n(+y + r * 0.3)}T${n(+x + r)} ${n(+y + r * 0.3)}V${n(+y + r)}H${n(x - r)}Z" fill="#7dd3fc" opacity=".6"/>`)}${shine(x - r * 0.32, y - r * 0.36, r * 0.33)}${shine(+x + r * 0.32, y - r * 0.05, r * 0.14)}${shine(x - r * 0.3, +y + r * 0.55, r * 0.09, 0.9)}${shine(+x + r * 0.2, +y + r * 0.6, r * 0.07, 0.9)}`;
237
+ },
238
+ },
239
+ {
240
+ id: 'visor', label: 'Visor', en: 'Visor', pt: 'Viseira', as3d: 'led', single: true,
241
+ draw: (p) => {
242
+ const r = p.r;
243
+ const x = n(p.lx * r * 1.3);
244
+ return `<rect x="${n(-r * 2.7)}" y="${n(-r * 0.62)}" width="${n(r * 5.4)}" height="${n(r * 1.24)}" rx="${n(r * 0.62)}" fill="#0b1220" ${stroke('#475569', r * 0.1)}/><rect x="${n(x - r * 1.1)}" y="${n(-r * 0.36)}" width="${n(r * 2.2)}" height="${n(r * 0.72)}" rx="${n(r * 0.36)}" fill="${p.iris}" opacity=".35"/><rect x="${n(x - r * 0.8)}" y="${n(-r * 0.24)}" width="${n(r * 1.6)}" height="${n(r * 0.48)}" rx="${n(r * 0.24)}" fill="${p.iris}"/><rect x="${n(x - r * 0.6)}" y="${n(-r * 0.16)}" width="${n(r * 0.6)}" height="${n(r * 0.1)}" rx="1" fill="#fff" opacity=".7"/>`;
245
+ },
246
+ },
247
+ {
248
+ id: 'scanner', label: 'Escáner', en: 'Scanner', pt: 'Scanner', as3d: 'robot',
249
+ draw: (p) => {
250
+ const r = p.r;
251
+ const [x, y] = look(p, 0.2);
252
+ return `<circle r="${r}" fill="#0b1220" ${stroke('#64748b', r * 0.1)}/><circle r="${n(r * 0.62)}" fill="none" stroke="${p.iris}" stroke-width="${n(r * 0.2)}" opacity=".9"/><circle r="${n(r * 0.62)}" fill="none" stroke="${p.iris}" stroke-width="${n(r * 0.45)}" opacity=".2"/><circle cx="${x}" cy="${y}" r="${n(r * 0.22)}" fill="${p.iris}"/><circle cx="${n(x - r * 0.06)}" cy="${n(y - r * 0.06)}" r="${n(r * 0.08)}" fill="#fff"/>`;
253
+ },
254
+ },
255
+ {
256
+ id: 'triclops', label: 'Tres ojos', en: 'Three eyes', pt: 'Três olhos', as3d: 'cyclops', single: true,
257
+ draw: (p) => {
258
+ const r = p.r;
259
+ const one = (cx, cy, k) => { const rr = r * k; const [x, y] = look({ ...p, r: rr }, 0.3); return `<g transform="translate(${n(cx)} ${n(cy)})"><circle r="${n(rr)}" fill="${p.white}" ${stroke(shade(p.lid, -0.2), r * 0.08)}/><circle cx="${x}" cy="${y}" r="${n(rr * 0.58)}" fill="${p.iris}"/><circle cx="${x}" cy="${y}" r="${n(rr * 0.3)}" fill="${p.ink}"/>${shine(x - rr * 0.22, y - rr * 0.26, rr * 0.16)}</g>`; };
260
+ return one(-r * 1.75, r * 0.4, 0.88) + one(r * 1.75, r * 0.4, 0.88) + one(0, -r * 0.45, 1.15);
261
+ },
262
+ },
263
+ {
264
+ id: 'sewn', label: 'Botones cosidos', en: 'Sewn buttons', pt: 'Botões costurados', as3d: 'button', noBlink: true,
265
+ draw: (p) => {
266
+ const r = p.r;
267
+ const c = p.iris;
268
+ return `<circle r="${r}" fill="${c}" ${stroke(shade(c, -0.4), r * 0.1)}/><circle r="${n(r * 0.72)}" fill="none" stroke="${shade(c, -0.2)}" stroke-width="${n(r * 0.08)}"/>${[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => `<circle cx="${n(a * r * 0.3)}" cy="${n(b * r * 0.3)}" r="${n(r * 0.12)}" fill="${shade(c, -0.5)}"/>`).join('')}<path d="M${n(-r * 0.3)} ${n(-r * 0.3)}L${n(r * 0.3)} ${n(r * 0.3)}M${n(r * 0.3)} ${n(-r * 0.3)}L${n(-r * 0.3)} ${n(r * 0.3)}" fill="none" ${stroke('#f5f5f4', r * 0.12)}/>${shine(-r * 0.5, -r * 0.5, r * 0.12, 0.7)}`;
269
+ },
270
+ },
271
+ {
272
+ id: 'bored', label: 'Aburridos', en: 'Bored', pt: 'Entediados', as3d: 'tired',
273
+ draw: (p) => {
274
+ const r = p.r;
275
+ const x = n(p.lx * r * 0.35);
276
+ return `<path d="M${n(-r)} ${n(-r * 0.1)}H${n(r)}A${n(r)} ${n(r)} 0 0 1 ${n(-r)} ${n(-r * 0.1)}Z" fill="${p.white}"/><circle cx="${x}" cy="${n(r * 0.38)}" r="${n(r * 0.4)}" fill="${p.ink}"/>${shine(x - r * 0.14, r * 0.24, r * 0.12)}<path d="M${n(-r * 1.08)} ${n(-r * 0.1)}H${n(r * 1.08)}" fill="none" ${stroke(p.ink, r * 0.2)}/>`;
277
+ },
278
+ },
279
+ {
280
+ id: 'shoujo', label: 'Shoujo', en: 'Shoujo', pt: 'Shoujo', as3d: 'anime',
281
+ draw: (p) => {
282
+ const r = p.r;
283
+ const [x, y] = look(p, 0.12);
284
+ return `<ellipse cx="${x}" cy="${y}" rx="${n(r * 0.85)}" ry="${n(r * 1.15)}" fill="${p.ink}"/><ellipse cx="${x}" cy="${n(+y + r * 0.38)}" rx="${n(r * 0.62)}" ry="${n(r * 0.6)}" fill="${p.iris}" opacity=".85"/><ellipse cx="${n(x - r * 0.28)}" cy="${n(y - r * 0.45)}" rx="${n(r * 0.3)}" ry="${n(r * 0.38)}" fill="#fff"/><path d="${star(r * 0.24, 0.3, 4)}" transform="translate(${n(+x + r * 0.32)} ${n(+y + r * 0.12)})" fill="#fff"/>${shine(x - r * 0.25, +y + r * 0.62, r * 0.08, 0.9)}`;
285
+ },
286
+ },
287
+ ];
288
+ };
@@ -0,0 +1,259 @@
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, mouthShape, openMouth, line, closedOr, heartPath } = h;
9
+ // p = { w (media anchura), open 0..1, curve -1..1, ink, lip, tongue, teeth, lipColor }
10
+
11
+ /** Forma rellena de boca con su interior recortado y el borde encima. */
12
+ const clipped = (p, shape, inner, sw = Math.max(1.4, p.w * 0.1)) => {
13
+ const id = `m${Math.round(Math.random() * 1e9)}`;
14
+ return `<clipPath id="${id}"><path d="${shape}"/></clipPath><path d="${shape}" fill="${p.ink}"/><g clip-path="url(#${id})">${inner}</g><path d="${shape}" fill="none" stroke="${p.lip}" stroke-width="${n(sw)}" stroke-linejoin="round"/>`;
15
+ };
16
+ /** y de la línea de la sonrisa básica ('smile') en x. */
17
+ const smileY = (p, x) => {
18
+ const y0 = -p.w * 0.1 - p.curve * 3;
19
+ const yc = p.w * 0.75 + p.curve * p.w * 0.4;
20
+ const t = (x / p.w + 1) / 2;
21
+ return y0 * (1 - 2 * t * (1 - t)) + 2 * t * (1 - t) * yc;
22
+ };
23
+ const smileD = (p, k = 1) => `M${n(-p.w * k)} ${n(smileY(p, -p.w * k))}Q0 ${n(p.w * 0.75 + p.curve * p.w * 0.4)} ${n(p.w * k)} ${n(smileY(p, p.w * k))}`;
24
+ const smile = (p) => line(p, `M${n(-p.w)} ${n(-p.w * 0.1 - p.curve * 3)}Q0 ${n(p.w * 0.75 + p.curve * p.w * 0.4)} ${n(p.w)} ${n(-p.w * 0.1 - p.curve * 3)}`);
25
+ const fangAt = (p, x, y, s = 1) => `<path d="M${n(x - p.w * 0.11 * s)} ${n(y - 0.5)}L${n(x)} ${n(y + p.w * 0.3 * s)}L${n(x + p.w * 0.11 * s)} ${n(y - 0.5)}Z" fill="${p.teeth}" stroke="${p.lip}" stroke-width="1" stroke-linejoin="round"/>`;
26
+ const tongueTip = (p, x, y, s = 1) => `<path d="M${n(x - p.w * 0.17 * s)} ${n(y)}v${n(p.w * 0.14 * s)}a${n(p.w * 0.17 * s)} ${n(p.w * 0.17 * s)} 0 0 0 ${n(p.w * 0.34 * s)} 0v${n(-p.w * 0.14 * s)}Z" fill="${p.tongue}" stroke="${shade(p.tongue, -0.3)}" stroke-width="1.2"/><path d="M${n(x)} ${n(y + p.w * 0.04)}v${n(p.w * 0.14 * s)}" stroke="${shade(p.tongue, -0.3)}" stroke-width="1"/>`;
27
+
28
+ return [
29
+ {
30
+ id: 'dgrin', label: 'Sonrisa D', en: 'Big D grin', pt: 'Sorrisão D', as3d: 'laugh',
31
+ draw: (p) => {
32
+ const w = p.w;
33
+ const hh = w * (0.7 + p.open * 0.45);
34
+ const shape = `M${n(-w)} ${n(-w * 0.05)}Q0 ${n(-w * 0.18)} ${n(w)} ${n(-w * 0.05)}C${n(w)} ${n(hh * 0.8)} ${n(w * 0.5)} ${n(hh)} 0 ${n(hh)}C${n(-w * 0.5)} ${n(hh)} ${n(-w)} ${n(hh * 0.8)} ${n(-w)} ${n(-w * 0.05)}Z`;
35
+ return clipped(p, shape, `<rect x="${n(-w)}" y="${n(-w * 0.3)}" width="${n(w * 2)}" height="${n(w * 0.5)}" fill="${p.teeth}"/><ellipse cy="${n(hh)}" rx="${n(w * 0.55)}" ry="${n(hh * 0.42)}" fill="${p.tongue}"/>`);
36
+ },
37
+ },
38
+ {
39
+ id: 'shark', label: 'Tiburón', en: 'Shark', pt: 'Tubarão', as3d: 'monster',
40
+ draw: (p) => {
41
+ const w = p.w * 1.1;
42
+ const hh = p.w * (0.5 + p.open * 0.55);
43
+ const cv = Math.max(0.2, p.curve + 0.4);
44
+ const c = cv * w * 0.35;
45
+ const shape = mouthShape(w, hh, cv);
46
+ const vT = -0.2 * c - 0.075 * hh + p.w * 0.08;
47
+ const vB = hh + c * 0.4 - p.w * 0.1;
48
+ let top = `M${n(-w - 2)} ${n(-w)}H${n(w + 2)}V${n(vT)}`;
49
+ for (let i = 6; i >= 0; i--) top += `L${n(-w + (i * 2 * w) / 6)} ${n(vT)}${i ? `L${n(-w + ((i - 0.5) * 2 * w) / 6)} ${n(vT + p.w * 0.32)}` : ''}`;
50
+ let bot = `M${n(-w - 2)} ${n(hh + w)}H${n(w + 2)}V${n(vB)}`;
51
+ for (let i = 5; i >= 0; i--) bot += `L${n(-w * 0.85 + (i * 1.7 * w) / 5)} ${n(vB)}${i ? `L${n(-w * 0.85 + ((i - 0.5) * 1.7 * w) / 5)} ${n(vB - p.w * 0.24)}` : ''}`;
52
+ return clipped(p, shape, `<path d="${top}Z" fill="${p.teeth}"/><path d="${bot}Z" fill="${p.teeth}"/>`);
53
+ },
54
+ },
55
+ {
56
+ id: 'fang', label: 'Un colmillo', en: 'Single fang', pt: 'Um caninho', as3d: 'vampire',
57
+ draw: (p) => {
58
+ if (p.open > 0.12) {
59
+ const w = p.w * 0.85;
60
+ return `${openMouth(p, { w, max: 0.85 })}${fangAt(p, w * 0.45, -p.curve * w * 0.3, 0.9)}`;
61
+ }
62
+ return `${fangAt(p, p.w * 0.4, smileY(p, p.w * 0.4), 1.35)}${smile(p)}`;
63
+ },
64
+ },
65
+ {
66
+ id: 'fangs', label: 'Colmillitos', en: 'Tiny fangs', pt: 'Caninhos', as3d: 'vampire',
67
+ draw: (p) => {
68
+ if (p.open > 0.12) return openMouth({ ...p, w: p.w * 0.9 }, { w: p.w * 0.9, max: 0.85, fangs: true });
69
+ return `${fangAt(p, -p.w * 0.38, smileY(p, -p.w * 0.38), 1.1)}${fangAt(p, p.w * 0.38, smileY(p, p.w * 0.38), 1.1)}${smile(p)}`;
70
+ },
71
+ },
72
+ {
73
+ id: 'bunny', label: 'Conejito', en: 'Bunny', pt: 'Coelhinho', as3d: 'buck',
74
+ draw: (p) => {
75
+ const w = p.w;
76
+ const teeth = (y) => `<path d="M${n(-w * 0.24)} ${n(y)}h${n(w * 0.48)}v${n(w * 0.3)}q0 ${n(w * 0.1)} ${n(-w * 0.1)} ${n(w * 0.1)}h${n(-w * 0.28)}q${n(-w * 0.1)} 0 ${n(-w * 0.1)} ${n(-w * 0.1)}Z" fill="${p.teeth}" stroke="${p.lip}" stroke-width="1.2" stroke-linejoin="round"/><path d="M0 ${n(y)}v${n(w * 0.34)}" stroke="${p.lip}" stroke-width="1"/>`;
77
+ if (p.open > 0.12) return `${openMouth({ ...p, curve: Math.max(0.3, p.curve + 0.5) }, { w: w * 0.75, min: 0.1, max: 0.75, tongue: false })}${teeth(-w * 0.08)}`;
78
+ return `${teeth(w * 0.02)}${line(p, `M${n(-w * 0.6)} ${n(-w * 0.08)}Q${n(-w * 0.3)} ${n(w * 0.32)} 0 ${n(w * 0.02)}Q${n(w * 0.3)} ${n(w * 0.32)} ${n(w * 0.6)} ${n(-w * 0.08)}`, 0.14)}`;
79
+ },
80
+ },
81
+ {
82
+ id: 'zigzag', label: 'Zigzag', en: 'Zigzag', pt: 'Zigue-zague', as3d: 'wavy',
83
+ draw: closedOr((p) => line(p, `M${n(-p.w * 0.85)} ${n(-p.w * 0.08)}${Array.from({ length: 6 }, (_, i) => `L${n(-p.w * 0.85 + ((i + 1) * p.w * 1.7) / 6)} ${n(i % 2 ? -p.w * 0.08 : p.w * 0.16)}`).join('')}`, 0.14)),
84
+ },
85
+ {
86
+ id: 'uwu', label: 'UwU', en: 'UwU', pt: 'UwU', as3d: 'cat',
87
+ draw: closedOr((p) => line(p, `M${n(-p.w * 0.5)} ${n(-p.w * 0.08)}Q${n(-p.w * 0.25)} ${n(p.w * 0.36)} 0 ${n(p.w * 0.02)}Q${n(p.w * 0.25)} ${n(p.w * 0.36)} ${n(p.w * 0.5)} ${n(-p.w * 0.08)}`, 0.13)),
88
+ },
89
+ {
90
+ id: 'yawn', label: 'Bostezo', en: 'Yawn', pt: 'Bocejo', as3d: 'open',
91
+ draw: (p) => {
92
+ const rx = p.w * 0.5;
93
+ const ry = p.w * (0.5 + p.open * 0.35);
94
+ const cy = p.w * 0.25;
95
+ const shape = `M${n(-rx)} ${n(cy)}a${n(rx)} ${n(ry)} 0 1 0 ${n(rx * 2)} 0a${n(rx)} ${n(ry)} 0 1 0 ${n(-rx * 2)} 0Z`;
96
+ return clipped(p, shape, `<ellipse cy="${n(cy + ry * 0.95)}" rx="${n(rx * 0.75)}" ry="${n(ry * 0.45)}" fill="${p.tongue}"/><ellipse cy="${n(cy - ry * 0.7)}" rx="${n(rx * 0.12)}" ry="${n(ry * 0.22)}" fill="${shade(p.tongue, -0.15)}"/>`, 2);
97
+ },
98
+ },
99
+ {
100
+ id: 'vee', label: 'Uve', en: 'Little v', pt: 'Vezinho', as3d: 'tiny',
101
+ draw: closedOr((p) => line(p, `M${n(-p.w * 0.36)} ${n(-p.w * 0.1)}L0 ${n(p.w * 0.22)}L${n(p.w * 0.36)} ${n(-p.w * 0.1)}`, 0.15)),
102
+ },
103
+ {
104
+ id: 'dimples', label: 'Hoyuelos', en: 'Dimples', pt: 'Covinhas', as3d: 'smile',
105
+ draw: closedOr((p) => {
106
+ const k = 0.8;
107
+ const y = smileY(p, -p.w * k);
108
+ return `${line(p, smileD(p, k))}${[-1, 1].map((s) => `<path d="M${n(s * p.w * 0.9)} ${n(y - p.w * 0.12)}q${n(s * p.w * 0.12)} ${n(p.w * 0.1)} ${n(s * p.w * 0.02)} ${n(p.w * 0.26)}" stroke="${p.lip}" stroke-width="${n(Math.max(1.3, p.w * 0.11))}" fill="none" stroke-linecap="round"/>`).join('')}`;
109
+ }),
110
+ },
111
+ {
112
+ id: 'bubblegum', label: 'Chicle', en: 'Bubblegum', pt: 'Chiclete', as3d: 'kiss',
113
+ draw: (p) => {
114
+ if (p.open > 0.12) return openMouth(p, { w: p.w * 0.6, max: 0.8 });
115
+ const r = p.w * 0.72;
116
+ return `<ellipse rx="${n(p.w * 0.22)}" ry="${n(p.w * 0.16)}" fill="#f472b6"/><circle cy="${n(r * 0.55)}" r="${n(r)}" fill="#f9a8d4" stroke="${shade('#f9a8d4', -0.3)}" stroke-width="1.5" opacity=".95"/><ellipse cx="${n(-r * 0.38)}" cy="${n(r * 0.2)}" rx="${n(r * 0.22)}" ry="${n(r * 0.14)}" transform="rotate(-35 ${n(-r * 0.38)} ${n(r * 0.2)})" fill="#fff" opacity=".75"/><circle cx="${n(r * 0.35)}" cy="${n(r * 0.95)}" r="${n(r * 0.07)}" fill="#fff" opacity=".6"/>`;
117
+ },
118
+ },
119
+ {
120
+ id: 'tongueside', label: 'Lengua de lado', en: 'Side tongue', pt: 'Língua de lado', as3d: 'tongue',
121
+ draw: closedOr((p) => {
122
+ const x = p.w * 0.42;
123
+ const y = smileY(p, x);
124
+ return `<g transform="rotate(-14 ${n(x)} ${n(y)})">${tongueTip(p, x, y - p.w * 0.04, 1.3)}</g>${smile(p)}`;
125
+ }),
126
+ },
127
+ {
128
+ id: 'lipbite', label: 'Mordiéndose el labio', en: 'Lip bite', pt: 'Mordendo o lábio', as3d: 'buck',
129
+ draw: closedOr((p) => {
130
+ const w = p.w;
131
+ return `<path d="M${n(-w * 0.32)} ${n(w * 0.32)}Q0 ${n(w * 0.48)} ${n(w * 0.32)} ${n(w * 0.32)}" stroke="${p.lip}" stroke-width="${n(Math.max(1.3, w * 0.1))}" fill="none" stroke-linecap="round" opacity=".45"/>${line(p, `M${n(-w * 0.75)} ${n(-w * 0.05)}Q0 ${n(w * 0.3)} ${n(w * 0.75)} ${n(-w * 0.05)}`, 0.15)}<path d="M${n(-w * 0.22)} ${n(w * 0.06)}h${n(w * 0.44)}v${n(w * 0.16)}q0 ${n(w * 0.08)} ${n(-w * 0.08)} ${n(w * 0.08)}h${n(-w * 0.28)}q${n(-w * 0.08)} 0 ${n(-w * 0.08)} ${n(-w * 0.08)}Z" fill="${p.teeth}" stroke="${p.lip}" stroke-width="1.1" stroke-linejoin="round"/><path d="M0 ${n(w * 0.06)}v${n(w * 0.24)}" stroke="${p.lip}" stroke-width="0.9"/>`;
132
+ }),
133
+ },
134
+ {
135
+ id: 'heart', label: 'Corazón', en: 'Heart', pt: 'Coração', as3d: 'oh',
136
+ draw: (p) => {
137
+ const r = p.w * (0.45 + p.open * 0.3);
138
+ const id = `mh${Math.round(Math.random() * 1e9)}`;
139
+ const tf = `translate(0 ${n(r * 0.35)})`;
140
+ return `<clipPath id="${id}"><path d="${heartPath(r)}" transform="${tf}"/></clipPath><path d="${heartPath(r)}" transform="${tf}" fill="${p.ink}"/><g clip-path="url(#${id})"><ellipse cy="${n(r * 1.05)}" rx="${n(r * 0.55)}" ry="${n(r * 0.4)}" fill="${p.tongue}"/></g><path d="${heartPath(r)}" transform="${tf}" fill="none" stroke="${p.lip}" stroke-width="${n(Math.max(1.4, p.w * 0.1))}" stroke-linejoin="round"/>`;
141
+ },
142
+ },
143
+ {
144
+ id: 'stitch', label: 'Cosida', en: 'Stitched', pt: 'Costurada', as3d: 'flat',
145
+ draw: closedOr((p) => {
146
+ const w = p.w;
147
+ const y = (x) => w * 0.1 * (1 - (x / (w * 0.8)) ** 2);
148
+ return `${line(p, `M${n(-w * 0.8)} 0Q0 ${n(w * 0.2)} ${n(w * 0.8)} 0`, 0.13)}${[-0.55, -0.2, 0.2, 0.55].map((k) => `<path d="M${n(k * w - w * 0.05)} ${n(y(k * w) - w * 0.2)}L${n(k * w + w * 0.05)} ${n(y(k * w) + w * 0.2)}" stroke="${p.lip}" stroke-width="${n(Math.max(1.4, w * 0.1))}" stroke-linecap="round"/>`).join('')}`;
149
+ }),
150
+ },
151
+ {
152
+ id: 'zipper', label: 'Cremallera', en: 'Zipper', pt: 'Zíper', as3d: 'flat',
153
+ draw: (p) => {
154
+ const w = p.w;
155
+ const tab = (x, y) => `<rect x="${n(x)}" y="${n(y - w * 0.06)}" width="${n(w * 0.2)}" height="${n(w * 0.4)}" rx="${n(w * 0.07)}" fill="#e2e8f0" stroke="${p.lip}" stroke-width="1.2"/><circle cx="${n(x + w * 0.1)}" cy="${n(y + w * 0.22)}" r="${n(w * 0.045)}" fill="${p.lip}"/>`;
156
+ if (p.open > 0.12) return `${openMouth(p, { w: w * 0.8, max: 0.8 })}${tab(w * 0.8, -w * 0.05)}`;
157
+ const ticks = Array.from({ length: 9 }, (_, i) => `<rect x="${n(-w * 0.78 + i * w * 0.19)}" y="${n(i % 2 ? 0 : -w * 0.13)}" width="${n(w * 0.1)}" height="${n(w * 0.13)}" rx="1" fill="#cbd5e1" stroke="${p.lip}" stroke-width=".8"/>`).join('');
158
+ return `<path d="M${n(-w * 0.82)} 0H${n(w * 0.82)}" stroke="${p.lip}" stroke-width="${n(Math.max(1.4, w * 0.08))}" stroke-linecap="round"/>${ticks}${tab(w * 0.82, -w * 0.04)}`;
159
+ },
160
+ },
161
+ {
162
+ id: 'equalizer', label: 'Ecualizador', en: 'Equalizer', pt: 'Equalizador', as3d: 'robot',
163
+ draw: (p) => {
164
+ const w = p.w;
165
+ const hh = w * 0.62;
166
+ const k = [0.35, 0.6, 0.85, 1, 0.85, 0.6, 0.35];
167
+ const bw = (w * 1.6) / 7;
168
+ return `<rect x="${n(-w)}" y="${n(-hh / 2)}" width="${n(w * 2)}" height="${n(hh)}" rx="${n(hh * 0.3)}" fill="#0b1220" stroke="#94a3b8" stroke-width="2"/>${k.map((v, i) => {
169
+ const bh = Math.max(1.5, (hh - 5) * (0.15 + v * (0.12 + p.open * 0.73)));
170
+ return `<rect x="${n(-w * 0.8 + i * bw + bw * 0.18)}" y="${n(-bh / 2)}" width="${n(bw * 0.64)}" height="${n(bh)}" rx="1" fill="${v > 0.9 && p.open > 0.5 ? '#facc15' : '#4ade80'}"/>`;
171
+ }).join('')}`;
172
+ },
173
+ },
174
+ {
175
+ id: 'frog', label: 'Rana', en: 'Froggy', pt: 'Sapinho', as3d: 'smile',
176
+ draw: (p) => {
177
+ const w = p.w * 1.35;
178
+ if (p.open > 0.12) return openMouth(p, { w, max: 0.6 });
179
+ return `${line(p, `M${n(-w)} ${n(-p.w * 0.15)}Q0 ${n(p.w * 0.5 + p.curve * p.w * 0.3)} ${n(w)} ${n(-p.w * 0.15)}`, 0.14)}${[-1, 1].map((s) => `<path d="M${n(s * (w - p.w * 0.1))} ${n(-p.w * 0.32)}l${n(s * p.w * 0.12)} ${n(p.w * 0.24)}" stroke="${p.lip}" stroke-width="${n(Math.max(1.4, p.w * 0.1))}" stroke-linecap="round"/>`).join('')}`;
180
+ },
181
+ },
182
+ {
183
+ id: 'meh', label: 'Desganada', en: 'Meh', pt: 'Desanimada', as3d: 'frown',
184
+ draw: closedOr((p) => line(p, `M${n(-p.w * 0.7)} ${n(p.w * 0.02)}Q${n(p.w * 0.1)} ${n(-p.w * 0.14)} ${n(p.w * 0.7)} ${n(p.w * 0.3)}`, 0.16)),
185
+ },
186
+ {
187
+ id: 'wail', label: 'Llorando', en: 'Wail', pt: 'Berreiro', as3d: 'frown',
188
+ draw: (p) => openMouth({ ...p, curve: Math.min(-0.5, p.curve - 0.8) }, { min: 0.5, max: 1 }),
189
+ },
190
+ {
191
+ id: 'gasp', label: 'Sorpresa', en: 'Gasp', pt: 'Susto', as3d: 'oh',
192
+ draw: (p) => {
193
+ const w = p.w * 0.55;
194
+ const t = p.w * (0.32 + p.open * 0.4);
195
+ const b = p.w * 0.3;
196
+ const shape = `M${n(-w)} ${n(b - p.w * 0.06)}Q${n(-w)} ${n(-t)} 0 ${n(-t)}Q${n(w)} ${n(-t)} ${n(w)} ${n(b - p.w * 0.06)}Q${n(w)} ${n(b)} ${n(w - p.w * 0.08)} ${n(b)}H${n(-w + p.w * 0.08)}Q${n(-w)} ${n(b)} ${n(-w)} ${n(b - p.w * 0.06)}Z`;
197
+ return clipped(p, shape, `<ellipse cy="${n(b)}" rx="${n(w * 0.75)}" ry="${n(p.w * 0.2)}" fill="${p.tongue}"/>`, 2);
198
+ },
199
+ },
200
+ {
201
+ id: 'yum', label: 'Relamiéndose', en: 'Yum', pt: 'Lambendo', as3d: 'tongue',
202
+ draw: closedOr((p) => {
203
+ const x = -p.w * 0.62;
204
+ const y = smileY(p, x);
205
+ return `${smile(p)}<g transform="rotate(-40 ${n(x)} ${n(y)})">${tongueTip(p, x, y - p.w * 0.18, 1.1)}</g>`;
206
+ }),
207
+ },
208
+ {
209
+ id: 'triangle', label: 'Triangular', en: 'Triangle', pt: 'Triangular', as3d: 'open',
210
+ draw: (p) => {
211
+ const w = p.w * 0.6;
212
+ const hh = p.w * (0.5 + p.open * 0.55);
213
+ const top = -p.w * 0.15;
214
+ const shape = `M${n(-w)} ${n(top)}H${n(w)}L0 ${n(top + hh)}Z`;
215
+ return clipped(p, shape, `<ellipse cy="${n(top + hh * 0.85)}" rx="${n(w * 0.5)}" ry="${n(hh * 0.3)}" fill="${p.tongue}"/>`, Math.max(2, p.w * 0.14));
216
+ },
217
+ },
218
+ {
219
+ id: 'smallgrin', label: 'Sonrisita', en: 'Little grin', pt: 'Sorrisinho', as3d: 'grin',
220
+ draw: (p) => openMouth({ ...p, curve: Math.max(0.45, p.curve + 0.6) }, { w: p.w * 0.6, min: 0.45, teeth: true }),
221
+ },
222
+ {
223
+ id: 'squiggle', label: 'Ondulada', en: 'Squiggle', pt: 'Ondulada', as3d: 'wavy',
224
+ draw: closedOr((p) => {
225
+ let d = '';
226
+ for (let i = 0; i <= 24; i++) {
227
+ const u = -1 + i / 12;
228
+ const x = u * p.w * 0.9;
229
+ const y = p.w * (0.28 + p.curve * 0.1) * (1 - u * u) - p.w * 0.08 + Math.sin(u * Math.PI * 3) * p.w * 0.07;
230
+ d += `${i ? 'L' : 'M'}${n(x)} ${n(y)}`;
231
+ }
232
+ return line(p, d, 0.14);
233
+ }),
234
+ },
235
+ {
236
+ id: 'duckbill', label: 'Pico de pato', en: 'Duck bill', pt: 'Bico de pato', as3d: 'beak',
237
+ draw: (p) => {
238
+ const w = p.w;
239
+ const o = p.open * w * 0.5;
240
+ const st = `stroke="${shade('#f97316', -0.35)}" stroke-width="1.2"`;
241
+ return `${o > 1 ? `<ellipse cy="${n(w * 0.08)}" rx="${n(w * 0.7)}" ry="${n(o * 0.6 + 1)}" fill="${p.ink}"/>` : ''}<path d="M${n(-w * 0.78)} ${n(w * 0.12 + o / 2)}Q${n(-w * 0.85)} ${n(w * 0.4 + o / 2)} 0 ${n(w * 0.42 + o / 2)}Q${n(w * 0.85)} ${n(w * 0.4 + o / 2)} ${n(w * 0.78)} ${n(w * 0.12 + o / 2)}Z" fill="#ea580c" ${st}/><path d="M${n(-w * 0.9)} ${n(w * 0.1 - o / 2)}Q${n(-w * 0.95)} ${n(-w * 0.32 - o / 2)} 0 ${n(-w * 0.34 - o / 2)}Q${n(w * 0.95)} ${n(-w * 0.32 - o / 2)} ${n(w * 0.9)} ${n(w * 0.1 - o / 2)}Q0 ${n(w * 0.26 - o / 2)} ${n(-w * 0.9)} ${n(w * 0.1 - o / 2)}Z" fill="#fb923c" ${st}/><ellipse cx="${n(-w * 0.2)}" cy="${n(-w * 0.16 - o / 2)}" rx="${n(w * 0.06)}" ry="${n(w * 0.035)}" fill="${shade('#f97316', -0.45)}"/><ellipse cx="${n(w * 0.2)}" cy="${n(-w * 0.16 - o / 2)}" rx="${n(w * 0.06)}" ry="${n(w * 0.035)}" fill="${shade('#f97316', -0.45)}"/><ellipse cx="${n(-w * 0.45)}" cy="${n(-w * 0.2 - o / 2)}" rx="${n(w * 0.16)}" ry="${n(w * 0.05)}" fill="#fff" opacity=".45"/>`;
242
+ },
243
+ },
244
+ {
245
+ id: 'blep', label: 'Blep', en: 'Blep', pt: 'Blep', as3d: 'tongue',
246
+ draw: closedOr((p) => `${tongueTip(p, 0, p.w * 0.02, 0.95)}${line(p, `M${n(-p.w * 0.38)} ${n(-p.w * 0.02)}Q0 ${n(p.w * 0.12)} ${n(p.w * 0.38)} ${n(-p.w * 0.02)}`, 0.14)}`),
247
+ },
248
+ {
249
+ id: 'scream', label: 'Grito', en: 'Scream', pt: 'Grito', as3d: 'open',
250
+ draw: (p) => {
251
+ const rx = p.w * 0.32;
252
+ const ry = p.w * (0.62 + p.open * 0.3);
253
+ const cy = p.w * 0.3;
254
+ const shape = `M0 ${n(cy - ry)}C${n(rx * 1.4)} ${n(cy - ry)} ${n(rx * 0.9)} ${n(cy + ry)} 0 ${n(cy + ry)}C${n(-rx * 0.9)} ${n(cy + ry)} ${n(-rx * 1.4)} ${n(cy - ry)} 0 ${n(cy - ry)}Z`;
255
+ return clipped(p, shape, `<ellipse cy="${n(cy + ry * 0.9)}" rx="${n(rx * 0.7)}" ry="${n(ry * 0.3)}" fill="${p.tongue}"/>`, 2);
256
+ },
257
+ },
258
+ ];
259
+ };