@7ots/cli 0.1.0

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 (97) hide show
  1. package/.env.example +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +655 -0
  4. package/brand/apple-touch-icon.png +0 -0
  5. package/brand/favicon.svg +4 -0
  6. package/brand/icon-512.png +0 -0
  7. package/brand/mark-mono.svg +3 -0
  8. package/brand/mark.svg +4 -0
  9. package/brand/og-es.png +0 -0
  10. package/brand/og-pt.png +0 -0
  11. package/brand/og.png +0 -0
  12. package/brand/ots.json +11 -0
  13. package/brand/tokens.css +59 -0
  14. package/brand/wordmark.svg +5 -0
  15. package/cli/7ots.mjs +400 -0
  16. package/cli/lib/brain.mjs +143 -0
  17. package/cli/lib/config.mjs +70 -0
  18. package/cli/lib/hooks.mjs +151 -0
  19. package/cli/lib/i18n.mjs +480 -0
  20. package/cli/lib/identity.mjs +51 -0
  21. package/cli/lib/install.mjs +79 -0
  22. package/cli/lib/lines.mjs +73 -0
  23. package/cli/lib/meet.mjs +166 -0
  24. package/cli/lib/open.mjs +19 -0
  25. package/cli/lib/orquesta.mjs +84 -0
  26. package/cli/lib/paths.mjs +49 -0
  27. package/cli/lib/pet-core.mjs +184 -0
  28. package/cli/lib/pet-server.mjs +296 -0
  29. package/cli/lib/prompts.mjs +75 -0
  30. package/cli/lib/terminal.mjs +145 -0
  31. package/cli/lib/ui.mjs +234 -0
  32. package/cli/lib/wizard.mjs +279 -0
  33. package/cli/pet/electron/main.cjs +86 -0
  34. package/cli/pet/pet.html +404 -0
  35. package/dist/7ots.esm.js +627 -0
  36. package/dist/7ots.esm.js.map +7 -0
  37. package/dist/7ots.iife.js +627 -0
  38. package/dist/7ots.iife.js.map +7 -0
  39. package/llms.txt +57 -0
  40. package/package.json +73 -0
  41. package/server/admin.mjs +213 -0
  42. package/server/apuchat-relay.mjs +170 -0
  43. package/server/channels/agent.mjs +136 -0
  44. package/server/channels/apuchat.mjs +364 -0
  45. package/server/channels/apumail.mjs +211 -0
  46. package/server/channels/index.mjs +69 -0
  47. package/server/contact.mjs +151 -0
  48. package/server/demo.mjs +154 -0
  49. package/server/i18n.mjs +54 -0
  50. package/server/identity.mjs +108 -0
  51. package/server/llm.mjs +413 -0
  52. package/server/platform/auth.mjs +290 -0
  53. package/server/platform/crypto.mjs +80 -0
  54. package/server/platform/db.mjs +130 -0
  55. package/server/platform/routes.mjs +388 -0
  56. package/server/platform/runtime.mjs +180 -0
  57. package/server/platform/store.mjs +331 -0
  58. package/server/sdk.mjs +125 -0
  59. package/server/server.mjs +456 -0
  60. package/server/settings.mjs +177 -0
  61. package/server/tts.mjs +186 -0
  62. package/skills/7ots/SKILL.md +59 -0
  63. package/src/actions/ActionRegistry.js +254 -0
  64. package/src/actions/PageTools.js +270 -0
  65. package/src/actions/builtins.js +565 -0
  66. package/src/auth/AuthManager.js +128 -0
  67. package/src/avatar/AvatarStage.js +473 -0
  68. package/src/character/AvatarEditor.js +829 -0
  69. package/src/character/Character.js +1191 -0
  70. package/src/character/motion.js +794 -0
  71. package/src/character/parts.js +559 -0
  72. package/src/context/ContextManager.js +472 -0
  73. package/src/core/AgentBrain.js +341 -0
  74. package/src/core/AgentWidget.js +1059 -0
  75. package/src/core/EventBus.js +61 -0
  76. package/src/core/Proactivity.js +363 -0
  77. package/src/core/storage.js +35 -0
  78. package/src/i18n/index.js +193 -0
  79. package/src/i18n/messages/character.js +543 -0
  80. package/src/i18n/messages/identity.js +141 -0
  81. package/src/i18n/messages/platform.js +114 -0
  82. package/src/i18n/messages/server.js +630 -0
  83. package/src/i18n/messages/widget.js +362 -0
  84. package/src/identity/ContactCard.js +351 -0
  85. package/src/identity/Face.js +148 -0
  86. package/src/identity/random.js +112 -0
  87. package/src/identity/schema.js +262 -0
  88. package/src/index.js +117 -0
  89. package/src/integrations/apuchat.js +212 -0
  90. package/src/integrations/apumail.js +88 -0
  91. package/src/llm/ProxyLLM.js +87 -0
  92. package/src/mcp/McpClient.js +153 -0
  93. package/src/ui/Companion.js +498 -0
  94. package/src/ui/UIManager.js +425 -0
  95. package/src/ui/VirtualPointer.js +746 -0
  96. package/src/ui/markdown.js +94 -0
  97. package/src/voice/VoiceEngine.js +227 -0
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Movimiento del personaje 2D de 7ots: gestos, metamorfosis del cuerpo, efectos y visemas.
3
+ *
4
+ * - GESTURES: gestos con anticipación, squash & stretch y follow-through. Cada uno es una
5
+ * función del tiempo normalizado t (0..1) y la edad en ms → pose { x, y, r, sx, sy } del
6
+ * cuerpo (x/y en fracciones del cuerpo, r en grados, origen abajo al centro) más retoques
7
+ * de cara { lx, ly, blink, eye, lift, open, curve, msx }. Pueden sacar manos, alas y efectos.
8
+ * - MORPHS: transformaciones temporales del cuerpo (pinchos, alas, manos, cuernos, inflarse,
9
+ * aplastarse, estirarse, derretirse, gelatina y cambio de forma «shape:<id>»).
10
+ * - EFFECTS: brillos, sudor, rubor, corazones, zzz, «!», «?», enfado, vapor, confeti,
11
+ * lágrimas, bocadillo de pensar, notas, estornudo, burbuja y mareo.
12
+ * - VISEMES / textToVisemes: formas de boca para el lip-sync (abierta, ancha, redonda…).
13
+ *
14
+ * Todo son funciones puras: Character.js las evalúa en su bucle de animación y pinta el SVG.
15
+ * Aquí no se toca el DOM y todo lo que se pinta sale de números y colores ya saneados.
16
+ */
17
+
18
+ import { BODY_POINTS, SHAPES, shade } from './parts.js';
19
+
20
+ const R = (v) => Math.round(v * 100) / 100;
21
+ const TAU = Math.PI * 2;
22
+
23
+ export const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
24
+ export const lerp = (a, b, k) => a + (b - a) * k;
25
+
26
+ export const EASE = {
27
+ linear: (t) => t,
28
+ in: (t) => t * t * t,
29
+ out: (t) => 1 - (1 - t) ** 3,
30
+ inOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
31
+ backOut: (t) => {
32
+ const c = 1.70158;
33
+ return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2;
34
+ },
35
+ elastic: (t) => (t <= 0 ? 0 : t >= 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * (TAU / 3)) + 1),
36
+ };
37
+
38
+ /** Acerca `cur` a `target` con constante de tiempo `tau` (ms), igual a cualquier framerate. */
39
+ export const approach = (cur, target, dt, tau) => target + (cur - target) * Math.exp(-dt / Math.max(1, tau));
40
+
41
+ /** Aleatorio determinista (mulberry32): las partículas de un efecto no «saltan» entre frames. */
42
+ export function seeded(seed) {
43
+ let s = seed >>> 0 || 1;
44
+ return () => {
45
+ s = (s + 0x6d2b79f5) >>> 0;
46
+ let t = s;
47
+ t = Math.imul(t ^ (t >>> 15), t | 1);
48
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
49
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
50
+ };
51
+ }
52
+
53
+ /** Onda senoidal sobre la edad en ms. */
54
+ const S = (age, hz, ph = 0) => Math.sin((age / 1000) * TAU * hz + ph);
55
+ /** Envolvente: sube en la fracción `a` inicial y baja en la `b` final. */
56
+ const env = (t, a = 0.15, b = 0.15) => Math.min(EASE.inOut(clamp(t / a)), EASE.inOut(clamp((1 - t) / b)));
57
+
58
+ // ───────────────────────────── gestos ─────────────────────────────
59
+
60
+ /** Pose neutra: cuerpo sin transformar y cara sin retoques. */
61
+ export const POSE0 = Object.freeze({ x: 0, y: 0, r: 0, sx: 1, sy: 1, lx: 0, ly: 0, blink: 0, eye: 1, lift: 0, open: 0, curve: 0, msx: 1 });
62
+
63
+ /** Fotogramas clave [[t, {campos}, easing?], …] → función t → pose (los campos que faltan vuelven a POSE0). */
64
+ function keys(list) {
65
+ return (t) => {
66
+ let i = 1;
67
+ while (i < list.length - 1 && t > list[i][0]) i++;
68
+ const [t0, a] = list[i - 1];
69
+ const [t1, b, e = 'inOut'] = list[i];
70
+ const k = EASE[e](clamp((t - t0) / (t1 - t0 || 1)));
71
+ const out = {};
72
+ for (const f in a) out[f] = lerp(a[f], f in b ? b[f] : POSE0[f], k);
73
+ for (const f in b) if (!(f in a)) out[f] = lerp(POSE0[f], b[f], k);
74
+ return out;
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Gestos. { ms (duración por defecto), pose(t, ageMs) → pose, hands?: postura de manos,
80
+ * wings?: 'slow'|'fast', fx?: [[t, efecto|metamorfosis, opts?]], hold?: se puede alargar con {ms} }.
81
+ * Se diseñan mirando a la derecha; con { dir: -1 } se reflejan x, r, lx y la mano activa.
82
+ */
83
+ export const GESTURES = {
84
+ jump: {
85
+ ms: 800,
86
+ pose: keys([[0, {}], [0.2, { sy: 0.8, sx: 1.16, lift: -1 }], [0.42, { y: -0.24, sy: 1.14, sx: 0.9, lift: 3, eye: 1.08 }, 'out'], [0.56, { y: -0.27, lift: 3 }], [0.74, { y: 0, sy: 1.1, sx: 0.93 }, 'in'], [0.84, { sy: 0.86, sx: 1.12 }], [1, {}]]),
87
+ },
88
+ bounce: {
89
+ ms: 900,
90
+ pose: keys([[0, {}], [0.12, { sy: 0.88, sx: 1.08 }], [0.3, { y: -0.12, sy: 1.06, sx: 0.95 }, 'out'], [0.46, { y: 0, sy: 0.9, sx: 1.08 }, 'in'], [0.6, { y: -0.05, sy: 1.03 }, 'out'], [0.74, { y: 0, sy: 0.95, sx: 1.04 }, 'in'], [1, {}, 'out']]),
91
+ },
92
+ wiggle: {
93
+ ms: 900,
94
+ pose: (t) => {
95
+ const e = (1 - t) ** 1.5 * EASE.out(clamp(t / 0.1));
96
+ const w = Math.sin(t * TAU * 3);
97
+ return { r: w * 10 * e, x: Math.sin(t * TAU * 3 - 0.6) * 0.02 * e, sx: 1 + 0.03 * e * Math.abs(w), lx: -Math.sin(t * TAU * 3 - 1) * 0.4 * e };
98
+ },
99
+ },
100
+ nod: {
101
+ ms: 800,
102
+ pose: keys([[0, {}], [0.18, { sy: 1.03, ly: -0.2, lift: 2 }], [0.36, { sy: 0.92, sx: 1.04, y: 0.01, ly: 0.6, lift: -1, curve: 0.2 }, 'in'], [0.54, { sy: 1.02, ly: -0.1, curve: 0.2 }], [0.72, { sy: 0.95, sx: 1.02, ly: 0.4, curve: 0.2 }, 'in'], [1, {}, 'out']]),
103
+ },
104
+ shake: {
105
+ ms: 900,
106
+ pose: (t) => {
107
+ const e = Math.sin(Math.PI * clamp(t * 1.05));
108
+ const w = Math.sin(t * TAU * 3);
109
+ return { x: w * 0.05 * e, r: -w * 3 * e, lx: -Math.sin(t * TAU * 3 - 0.8) * 0.7 * e, curve: -0.25 * e, sx: 1 - 0.02 * e };
110
+ },
111
+ },
112
+ spin: {
113
+ ms: 1000,
114
+ pose: (t) => {
115
+ const a = t < 0.12 ? Math.sin((Math.PI * t) / 0.12) : 0;
116
+ const k = EASE.inOut(clamp((t - 0.12) / 0.76));
117
+ return { sx: Math.cos(k * TAU) * (1 + 0.1 * a), sy: 1 - 0.12 * a + 0.05 * Math.sin(Math.PI * k), y: -0.12 * Math.sin(Math.PI * k) };
118
+ },
119
+ },
120
+ wave: {
121
+ ms: 1600,
122
+ hold: true,
123
+ hands: 'wave',
124
+ pose: (t, age) => {
125
+ const e = env(t, 0.15, 0.2);
126
+ return { r: (2.5 + S(age, 1.5) * 1.2) * e, curve: 0.35 * e, lift: 2 * e, lx: 0.15 * e };
127
+ },
128
+ },
129
+ think: {
130
+ ms: 2200,
131
+ hold: true,
132
+ hands: 'chin',
133
+ fx: [[0.15, 'thought']],
134
+ pose: (t, age) => {
135
+ const e = env(t, 0.12, 0.15);
136
+ return { r: -3 * e, lx: (0.55 + S(age, 0.4) * 0.1) * e, ly: -0.55 * e, lift: 3 * e, curve: -0.25 * e, msx: 1 - 0.25 * e };
137
+ },
138
+ },
139
+ celebrate: {
140
+ ms: 1700,
141
+ hands: 'up',
142
+ fx: [[0.12, 'confetti'], [0.3, 'sparkles']],
143
+ pose: (t, age) => {
144
+ const e = env(t, 0.08, 0.15);
145
+ const hop = Math.abs(Math.sin(t * TAU * 1.5));
146
+ return { y: -0.16 * hop * e, sy: 1 + (0.08 * hop - 0.04) * e, sx: 1 - (0.05 * hop - 0.02) * e, r: S(age, 1.5) * 4 * e, curve: 0.7 * e, open: 0.45 * e, eye: 1 + 0.08 * e };
147
+ },
148
+ },
149
+ surprise: {
150
+ ms: 1200,
151
+ fx: [[0.04, 'exclaim']],
152
+ pose: keys([[0, {}], [0.1, { sy: 1.16, sx: 0.88, y: -0.05, eye: 1.3, open: 0.55, lift: 7, msx: 0.7 }, 'out'], [0.3, { sy: 0.97, sx: 1.03, eye: 1.25, open: 0.5, lift: 6, msx: 0.7 }], [0.45, { sy: 1.02, eye: 1.22, open: 0.45, lift: 5, msx: 0.72 }], [0.8, { eye: 1.15, open: 0.3, lift: 4, msx: 0.8 }], [1, {}]]),
153
+ },
154
+ shrug: {
155
+ ms: 1500,
156
+ hold: true,
157
+ hands: 'shrug',
158
+ fx: [[0.2, 'question']],
159
+ pose: keys([[0, {}], [0.2, { sy: 1.05, sx: 0.97, y: -0.02, r: -3, lift: 5, curve: -0.15, lx: -0.2, ly: -0.15 }, 'out'], [0.75, { sy: 1.04, sx: 0.97, y: -0.02, r: -3, lift: 5, curve: -0.15, lx: -0.2, ly: -0.15 }], [0.88, { sy: 0.97, sx: 1.02 }], [1, {}]]),
160
+ },
161
+ sneeze: {
162
+ ms: 1700,
163
+ fx: [[0.58, 'sneeze']],
164
+ pose: keys([[0, {}], [0.15, { r: -3, sy: 1.03, lift: 3, blink: 0.3, open: 0.25, msx: 0.9 }], [0.3, { r: -2, sy: 1.01, lift: 2, blink: 0.1, open: 0.15 }], [0.52, { r: -7, sy: 1.1, sx: 0.95, y: -0.02, lift: 6, blink: 0.65, open: 0.4, msx: 0.8 }, 'in'], [0.6, { r: 9, sy: 0.8, sx: 1.15, x: 0.05, blink: 1, open: 0.95, lift: -3, msx: 1.15 }, 'out'], [0.72, { r: 5, sy: 0.9, sx: 1.06, x: 0.03, blink: 1, open: 0.2 }], [1, {}, 'out']]),
165
+ },
166
+ hiccup: {
167
+ ms: 800,
168
+ fx: [[0.1, 'bubble']],
169
+ pose: keys([[0, {}], [0.08, { y: -0.07, sy: 1.12, sx: 0.92, eye: 1.18, open: 0.3, lift: 4, msx: 0.7 }, 'out'], [0.22, { y: 0, sy: 0.94, sx: 1.04, eye: 1.08, lift: 2 }, 'in'], [0.36, { sy: 1.02 }], [1, {}, 'out']]),
170
+ },
171
+ laugh: {
172
+ ms: 1500,
173
+ hold: true,
174
+ fx: [[0.1, 'sparkles']],
175
+ pose: (t, age) => {
176
+ const e = env(t, 0.1, 0.2);
177
+ const b = Math.abs(S(age, 4.5));
178
+ return { y: -0.035 * b * e, sy: 1 + (0.05 * b - 0.02) * e, sx: 1 - 0.03 * b * e, r: S(age, 1.3) * 3 * e, open: (0.35 + 0.45 * b) * e, curve: 0.9 * e, blink: 0.55 * e, lift: 2 * e };
179
+ },
180
+ },
181
+ dance: {
182
+ ms: 2600,
183
+ hold: true,
184
+ hands: 'dance',
185
+ fx: [[0.05, 'notes']],
186
+ pose: (t, age) => {
187
+ const e = env(t, 0.08, 0.12);
188
+ const beat = S(age, 1);
189
+ const hop = Math.abs(S(age, 2));
190
+ return { r: beat * 9 * e, x: beat * 0.04 * e, y: -0.06 * hop * e, sy: 1 + (0.06 * hop - 0.03) * e, sx: 1 - (0.04 * hop - 0.02) * e, curve: 0.6 * e, lx: beat * 0.3 * e };
191
+ },
192
+ },
193
+ yawn: {
194
+ ms: 2000,
195
+ pose: keys([[0, {}], [0.25, { sy: 1.1, sx: 0.95, y: -0.01, open: 0.5, blink: 0.6, lift: 4, msx: 0.8 }], [0.55, { sy: 1.12, sx: 0.94, y: -0.015, open: 1, blink: 0.85, lift: 5, msx: 0.75 }], [0.75, { sy: 0.95, sx: 1.03, open: 0.1, blink: 0.7 }], [1, {}]]),
196
+ },
197
+ shiver: {
198
+ ms: 1300,
199
+ hold: true,
200
+ pose: (t, age) => {
201
+ const e = env(t, 0.1, 0.15);
202
+ return { x: S(age, 14) * 0.012 * e, r: S(age, 11, 1) * 1.2 * e, sx: 1 - 0.03 * e, sy: 1 - 0.04 * e, curve: -0.3 * e, eye: 1 + 0.08 * e, lift: 3 * e, open: (0.08 + 0.04 * S(age, 16)) * e, msx: 1 + 0.15 * e };
203
+ },
204
+ },
205
+ clap: {
206
+ ms: 1500,
207
+ hold: true,
208
+ hands: 'clap',
209
+ fx: [[0.2, 'sparkles']],
210
+ pose: (t, age) => {
211
+ const e = env(t, 0.1, 0.15);
212
+ const b = Math.max(0, S(age, 3.2));
213
+ return { sy: 1 + 0.025 * b * e, y: -0.015 * b * e, curve: 0.6 * e, open: 0.2 * e };
214
+ },
215
+ },
216
+ thumbsup: {
217
+ ms: 1400,
218
+ hands: 'thumb',
219
+ fx: [[0.25, 'sparkles']],
220
+ pose: keys([[0, {}], [0.15, { sy: 0.92, sx: 1.05 }], [0.3, { y: -0.05, sy: 1.05, sx: 0.97, curve: 0.7, lift: 2, r: 3 }, 'out'], [0.8, { curve: 0.7, lift: 2, r: 3, sy: 1.01 }], [1, {}]]),
221
+ },
222
+ thumbsdown: {
223
+ ms: 1400,
224
+ hands: 'thumbdown',
225
+ pose: keys([[0, {}], [0.25, { sy: 0.93, sx: 1.04, curve: -0.6, lift: -1, r: -2 }], [0.8, { sy: 0.94, sx: 1.03, curve: -0.6, lift: -1, r: -2 }], [1, {}]]),
226
+ },
227
+ point: {
228
+ ms: 1500,
229
+ hold: true,
230
+ hands: 'point',
231
+ pose: keys([[0, {}], [0.15, { r: -3, sy: 0.97 }], [0.32, { r: 5, x: 0.02, sy: 1.02, lx: 0.85, ly: -0.05, lift: 2 }, 'out'], [0.8, { r: 4, x: 0.02, lx: 0.85, lift: 2 }], [1, {}]]),
232
+ },
233
+ bow: {
234
+ ms: 1600,
235
+ hands: 'namaste',
236
+ pose: keys([[0, {}], [0.3, { sy: 0.84, sx: 1.06, y: 0.02, ly: 0.7, blink: 0.5, curve: 0.3 }], [0.65, { sy: 0.84, sx: 1.06, y: 0.02, ly: 0.7, blink: 0.6, curve: 0.3 }], [0.85, { sy: 1.03, sx: 0.98 }, 'out'], [1, {}]]),
237
+ },
238
+ fly: {
239
+ ms: 2600,
240
+ hold: true,
241
+ wings: 'fast',
242
+ pose: (t, age) => {
243
+ const e = env(t, 0.18, 0.22);
244
+ const f = S(age, 3.2);
245
+ return { y: (-0.22 + f * 0.03) * e, sy: 1 + f * 0.03 * e, sx: 1 - f * 0.02 * e, r: S(age, 0.6) * 4 * e, curve: 0.4 * e, ly: 0.2 * e };
246
+ },
247
+ },
248
+ stomp: {
249
+ ms: 1000,
250
+ hands: 'hips',
251
+ fx: [[0.38, 'anger'], [0.38, 'spikes', { ms: 600 }]],
252
+ pose: keys([[0, {}], [0.25, { y: -0.08, sy: 1.1, sx: 0.93, r: -3, curve: -0.5, lift: -3 }, 'out'], [0.38, { y: 0, sy: 0.78, sx: 1.2, curve: -0.5, lift: -3 }, 'in'], [0.5, { sy: 1.04, sx: 0.97, x: 0.01, curve: -0.5, lift: -3 }], [0.6, { x: -0.01, sy: 0.98, curve: -0.5, lift: -3 }], [1, {}, 'out']]),
253
+ },
254
+ dizzy: {
255
+ ms: 2200,
256
+ hold: true,
257
+ fx: [[0.05, 'dizzy']],
258
+ pose: (t, age) => {
259
+ const e = env(t, 0.12, 0.15);
260
+ return { r: S(age, 0.9) * 7 * e, x: S(age, 0.9, 1.2) * 0.03 * e, lx: S(age, 1.4) * 0.8 * e, ly: Math.cos((age / 1000) * TAU * 1.4) * 0.6 * e, curve: -0.1 * e, open: 0.15 * e, msx: 1 - 0.2 * e };
261
+ },
262
+ },
263
+ };
264
+
265
+ /** Nombres alternativos (incluidos los gestos del avatar 3D de TalkingHead). */
266
+ export const GESTURE_ALIAS = {
267
+ handup: 'wave', hi: 'wave', hello: 'wave', index: 'point', ok: 'nod', yes: 'nod', no: 'shake',
268
+ thumbup: 'thumbsup', thumbdown: 'thumbsdown', side: 'wiggle', namaste: 'bow', hop: 'jump', cheer: 'celebrate',
269
+ };
270
+
271
+ // ───────────────────────────── manos ─────────────────────────────
272
+
273
+ /**
274
+ * Posturas de manos. (side, ageMs, dir, c) → { u, v } en semiejes del cuerpo desde su centro
275
+ * (o { x, y } absolutos), `ang` hacia dónde apuntan los dedos (0 arriba, 90 hacia fuera)
276
+ * y `type`: 'open' | 'fist' | 'thumb' | 'thumbdown' | 'point'. Diseñadas para la mano derecha.
277
+ */
278
+ const REST = { u: 1.08, v: 0.55, ang: 160, type: 'open' };
279
+ export const HAND_POSES = {
280
+ rest: () => REST,
281
+ wave: (s, a, dir) => (s === dir ? { u: 1.28, v: -0.6, ang: 18 + S(a, 2.4) * 26, type: 'open' } : REST),
282
+ up: (s, a) => ({ u: 1.08, v: -1.02, ang: 12 + S(a, 3, s) * 10, type: 'open' }),
283
+ shrug: () => ({ u: 1.42, v: -0.12, ang: 75, type: 'open' }),
284
+ clap: (s, a) => ({ u: 0.2 + 0.32 * (1 - Math.max(0, S(a, 3.2))), v: 0.35, ang: 8, type: 'open' }),
285
+ thumb: (s, a, dir) => (s === dir ? { u: 1.22, v: -0.2, ang: 0, type: 'thumb' } : REST),
286
+ thumbdown: (s, a, dir) => (s === dir ? { u: 1.22, v: 0.12, ang: 0, type: 'thumbdown' } : REST),
287
+ point: (s, a, dir) => (s === dir ? { u: 1.5, v: -0.08, ang: 90, type: 'point' } : REST),
288
+ chin: (s, a, dir, c) => (s === dir ? { x: c.cx + s * c.W * 0.17, y: c.mouthY + c.mw * 0.95, ang: 5, type: 'fist' } : REST),
289
+ namaste: (s, a, dir, c) => ({ x: c.cx + s * c.W * 0.06, y: c.mouthY + c.H * 0.2, ang: 0, type: 'open' }),
290
+ dance: (s, a) => ({ u: 1.15, v: -0.25 + s * S(a, 1) * 0.6, ang: 30 + s * S(a, 1) * 25, type: 'open' }),
291
+ hips: () => ({ u: 0.98, v: 0.28, ang: 200, type: 'fist' }),
292
+ cover: (s, a, dir, c) => ({ x: c.cx + s * c.sp, y: c.eyeY, ang: -8, type: 'open', s: 1.3 }),
293
+ };
294
+
295
+ function handPos(pose, s, age, dir, c) {
296
+ const p = (HAND_POSES[pose] || HAND_POSES.rest)(s, age, dir, c);
297
+ const x = p.x ?? c.cx + s * p.u * c.W * 0.5;
298
+ const y = p.y ?? c.mid + p.v * c.H * 0.5;
299
+ return { x, y, ang: p.ang || 0, type: p.type || 'open', s: p.s || 1 };
300
+ }
301
+
302
+ /** Mezcla dos posturas (el tipo de mano cambia a mitad). */
303
+ function mixHand(a, b, k) {
304
+ return { x: lerp(a.x, b.x, k), y: lerp(a.y, b.y, k), ang: lerp(a.ang, b.ang, k), s: lerp(a.s, b.s, k), type: k < 0.5 ? a.type : b.type };
305
+ }
306
+
307
+ function shoulder(c, s) {
308
+ const p = c.pts;
309
+ const [a, b] = s > 0 ? [p[4], p[5]] : [p[12], p[11]];
310
+ const x = lerp(a[0], b[0], 0.3);
311
+ const y = lerp(a[1], b[1], 0.3);
312
+ return { x: lerp(x, c.cx, 0.14), y };
313
+ }
314
+
315
+ function handSVG(h, s, c) {
316
+ const hr = c.hr * h.s;
317
+ const st = `fill="${c.fill}" stroke="${c.line}" stroke-width="${c.lw}" stroke-linejoin="round"`;
318
+ const groove = (d) => `<path d="${d}" stroke="${c.line}" stroke-width="${R(c.lw * 0.8)}" fill="none" stroke-linecap="round" opacity=".55"/>`;
319
+ let g = '';
320
+ let rot = h.ang;
321
+ if (h.type === 'open') {
322
+ g = `<ellipse cx="${R(-0.78 * hr)}" cy="${R(0.15 * hr)}" rx="${R(0.3 * hr)}" ry="${R(0.52 * hr)}" transform="rotate(-38 ${R(-0.78 * hr)} ${R(0.15 * hr)})" ${st}/>`;
323
+ g += `<ellipse rx="${R(0.88 * hr)}" ry="${R(1.05 * hr)}" ${st}/>`;
324
+ g += groove(`M${R(-0.28 * hr)} ${R(-0.98 * hr)}V${R(-0.45 * hr)}M${R(0.28 * hr)} ${R(-0.98 * hr)}V${R(-0.45 * hr)}`);
325
+ } else {
326
+ const fist = `<circle r="${R(0.82 * hr)}" ${st}/>${groove(`M${R(-0.5 * hr)} ${R(-0.2 * hr)}Q0 ${R(0.15 * hr)} ${R(0.5 * hr)} ${R(-0.2 * hr)}`)}`;
327
+ if (h.type === 'point') g = `<rect x="${R(-0.24 * hr)}" y="${R(-1.95 * hr)}" width="${R(0.48 * hr)}" height="${R(1.5 * hr)}" rx="${R(0.24 * hr)}" ${st}/>${fist}`;
328
+ else if (h.type === 'thumb' || h.type === 'thumbdown') {
329
+ rot = 0;
330
+ const up = h.type === 'thumb' ? -1 : 1;
331
+ g = `<rect x="${R(-0.22 * hr)}" y="${R(up < 0 ? -1.75 * hr : 0.25 * hr)}" width="${R(0.44 * hr)}" height="${R(1.5 * hr)}" rx="${R(0.22 * hr)}" ${st}/>${fist}`;
332
+ } else g = fist;
333
+ }
334
+ return `<g transform="translate(${R(h.x)} ${R(h.y)}) scale(${s} 1) rotate(${R(rot)})">${g}</g>`;
335
+ }
336
+
337
+ function armSVG(sh, h, s, c) {
338
+ const dx = h.x - sh.x;
339
+ const dy = h.y - sh.y;
340
+ const d = Math.hypot(dx, dy) || 1;
341
+ // codo hacia fuera y abajo (brazo de goma)
342
+ const ex = (sh.x + h.x) / 2 + s * d * 0.16;
343
+ const ey = (sh.y + h.y) / 2 + d * 0.12;
344
+ const path = `M${R(sh.x)} ${R(sh.y)}Q${R(ex)} ${R(ey)} ${R(h.x)} ${R(h.y)}`;
345
+ return `<path d="${path}" stroke="${c.line}" stroke-width="${R(c.aw + c.lw * 2)}" fill="none" stroke-linecap="round"/><path d="${path}" stroke="${c.fill}" stroke-width="${R(c.aw)}" fill="none" stroke-linecap="round"/>`;
346
+ }
347
+
348
+ function drawHands(c, k, age, o) {
349
+ const dir = o.dir || 1;
350
+ const kk = clamp(k, 0, 1.15);
351
+ let back = '';
352
+ let front = '';
353
+ for (const s of [-1, 1]) {
354
+ const sh = shoulder(c, s);
355
+ let h = handPos(o.pose || 'rest', s, age, dir, c);
356
+ if (o.from && o.blend < 1) h = mixHand(handPos(o.from, s, age, dir, c), h, EASE.inOut(o.blend));
357
+ // al aparecer, la mano sale del hombro
358
+ const hh = { ...h, x: lerp(sh.x, h.x, Math.min(1, kk)), y: lerp(sh.y, h.y, Math.min(1, kk)), s: h.s * Math.max(0.01, kk) };
359
+ back += armSVG(sh, hh, s, { ...c, aw: c.aw * Math.min(1, kk) });
360
+ front += handSVG(hh, s, c);
361
+ }
362
+ return [back, front];
363
+ }
364
+
365
+ // ───────────────────────────── alas, pinchos, cuernos ─────────────────────────────
366
+
367
+ const WING = 'M0 0C18 -36 64 -56 96 -44C88 -32 82 -28 72 -25C74 -14 66 -8 54 -8C54 3 42 8 30 6C20 11 7 9 0 0Z';
368
+ const WING_LINES = 'M8 -4C30 -20 52 -30 76 -36M10 0C28 -10 42 -16 58 -20';
369
+
370
+ function drawWings(c, k, age, o) {
371
+ const fast = o.flap === 'fast';
372
+ const amp = o.still ? 0 : fast ? 32 : 11;
373
+ const hz = fast ? 3.2 : 0.9;
374
+ const sc = (c.W / 120) * 0.95 * Math.max(0.01, k);
375
+ const fill = c.pal.dark ? shade(c.pal.body, 0.35) : shade(c.pal.body, 0.55);
376
+ let out = '';
377
+ for (const s of [-1, 1]) {
378
+ const p = c.pts;
379
+ const [a, b] = s > 0 ? [p[3], p[4]] : [p[13], p[12]];
380
+ const x = lerp(lerp(a[0], b[0], 0.5), c.cx, 0.16);
381
+ const y = lerp(a[1], b[1], 0.5);
382
+ const flap = -12 + S(age, hz) * amp;
383
+ out += `<g transform="translate(${R(x)} ${R(y)}) scale(${s} 1) rotate(${R(flap)}) scale(${R(sc)})"><path d="${WING}" fill="${fill}" stroke="${c.line}" stroke-width="${R(c.lw / sc)}" stroke-linejoin="round"/><path d="${WING_LINES}" stroke="${c.line}" stroke-width="${R((c.lw * 0.8) / sc)}" fill="none" stroke-linecap="round" opacity=".45"/></g>`;
384
+ }
385
+ return [out, ''];
386
+ }
387
+
388
+ /** Normal exterior del punto i del contorno (los puntos van en sentido horario desde arriba). */
389
+ function normalAt(pts, i) {
390
+ const N = pts.length;
391
+ const a = pts[(i - 1 + N) % N];
392
+ const b = pts[(i + 1) % N];
393
+ const tx = b[0] - a[0];
394
+ const ty = b[1] - a[1];
395
+ const l = Math.hypot(tx, ty) || 1;
396
+ return [ty / l, -tx / l, tx / l, ty / l];
397
+ }
398
+
399
+ function drawSpikes(c, k, age, o) {
400
+ const pts = c.pts;
401
+ const fill = shade(c.pal.body, c.pal.dark ? 0.2 : -0.18);
402
+ let out = '';
403
+ for (let i = 0; i < BODY_POINTS; i++) {
404
+ if (i >= 6 && i <= 10) continue; // abajo no: apoya en el suelo
405
+ const order = Math.min(i, BODY_POINTS - i);
406
+ const ki = EASE.backOut(clamp(k * 1.5 - order * 0.07));
407
+ if (ki <= 0.01) continue;
408
+ const [nx, ny, tx, ty] = normalAt(pts, i);
409
+ const [x, y] = pts[i];
410
+ const len = c.W * (0.17 + (order % 2) * 0.05) * ki * (1 + (o.still ? 0 : S(age, 2.5, i) * 0.06));
411
+ const hb = c.W * 0.075;
412
+ const bx = x - nx * 4;
413
+ const by = y - ny * 4;
414
+ out += `<path d="M${R(bx - tx * hb)} ${R(by - ty * hb)}L${R(x + nx * len)} ${R(y + ny * len)}L${R(bx + tx * hb)} ${R(by + ty * hb)}Z" fill="${fill}" stroke="${c.line}" stroke-width="${c.lw}" stroke-linejoin="round"/>`;
415
+ }
416
+ return [out, ''];
417
+ }
418
+
419
+ function drawHorns(c, k, age, o) {
420
+ const pts = c.pts;
421
+ const h = c.H * 0.24 * EASE.backOut(clamp(k));
422
+ const w = c.W * 0.075;
423
+ const fill = o.color || '#f3e9d2';
424
+ let out = '';
425
+ for (const s of [-1, 1]) {
426
+ const [x, y] = pts[s > 0 ? 1 : 15];
427
+ const xx = lerp(x, c.cx, 0.1);
428
+ out += `<path d="M${R(xx - w)} ${R(y + 6)}Q${R(xx - w * 0.4)} ${R(y - h * 0.55)} ${R(xx + s * w * 1.1)} ${R(y - h)}Q${R(xx + w * 0.3)} ${R(y - h * 0.4)} ${R(xx + w)} ${R(y + 6)}Z" fill="${fill}" stroke="${c.line}" stroke-width="${c.lw}" stroke-linejoin="round"/>`;
429
+ }
430
+ return [out, ''];
431
+ }
432
+
433
+ // ───────────────────────────── deformaciones del contorno ─────────────────────────────
434
+
435
+ function scaleAbout(pts, ox, oy, sx, sy) {
436
+ return pts.map(([x, y]) => [ox + (x - ox) * sx, oy + (y - oy) * sy]);
437
+ }
438
+
439
+ /** Métricas de un contorno (para deformar sin depender de layout). */
440
+ export function ptsBox(pts) {
441
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
442
+ for (const [x, y] of pts) {
443
+ if (x < x0) x0 = x;
444
+ if (x > x1) x1 = x;
445
+ if (y < y0) y0 = y;
446
+ if (y > y1) y1 = y;
447
+ }
448
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0, cx: (x0 + x1) / 2, bottom: y1 };
449
+ }
450
+
451
+ /**
452
+ * Metamorfosis. Las de dibujo devuelven [detrás, delante]; las de deformación transforman
453
+ * los 16 puntos del contorno (y con él cara y accesorios). `room`: cuánto alejar el encuadre.
454
+ */
455
+ export const MORPHS = {
456
+ spikes: { room: 1.32, draw: drawSpikes },
457
+ wings: { room: 1.6, draw: drawWings },
458
+ hands: { room: 1.35, draw: drawHands },
459
+ horns: { room: 1.2, draw: drawHorns },
460
+ puff: { room: 1.2, deform: (pts, k, age, b) => scaleAbout(pts, b.cx, b.bottom, 1 + 0.2 * k, 1 + 0.14 * k) },
461
+ squish: { room: 1.15, deform: (pts, k, age, b) => scaleAbout(pts, b.cx, b.bottom, 1 + 0.22 * k, 1 - 0.22 * k) },
462
+ stretch: { room: 1.25, deform: (pts, k, age, b) => scaleAbout(pts, b.cx, b.bottom, 1 - 0.12 * k, 1 + 0.26 * k) },
463
+ melt: {
464
+ room: 1.3,
465
+ deform: (pts, k, age, b, still) =>
466
+ pts.map(([x, y], i) => {
467
+ const ny = clamp((y - b.y) / (b.h || 1));
468
+ const drip = ny > 0.6 && !still ? Math.sin(i * 2.1 + age / 300) * 3 * k : 0;
469
+ return [b.cx + (x - b.cx) * (1 + 0.45 * k * ny * ny), b.bottom - (b.bottom - y) * (1 - 0.38 * k) + drip];
470
+ }),
471
+ },
472
+ jelly: {
473
+ room: 1.12,
474
+ deform: (pts, k, age, b, still) => {
475
+ if (still) return pts;
476
+ return pts.map((p, i) => {
477
+ const [nx, ny] = normalAt(pts, i);
478
+ const a = b.w * 0.05 * k * Math.sin((age / 1000) * TAU * 2.6 + i * 1.3);
479
+ return [p[0] + nx * a, p[1] + ny * a];
480
+ });
481
+ },
482
+ },
483
+ };
484
+
485
+ const SHAPE_PTS = Object.fromEntries(SHAPES.map((s) => [s.id, s.points]));
486
+ /** Puntos de una forma del catálogo (para «shape:<id>»). */
487
+ export const shapePoints = (id) => SHAPE_PTS[id] || null;
488
+
489
+ // ───────────────────────────── efectos ─────────────────────────────
490
+
491
+ const star4 = (r) => `M0 ${R(-r)}Q0 0 ${R(r)} 0Q0 0 0 ${R(r)}Q0 0 ${R(-r)} 0Q0 0 0 ${R(-r)}Z`;
492
+ const heart = (r) => `M0 ${R(r * 0.9)}C${R(-r * 1.4)} ${R(-r * 0.1)} ${R(-r * 0.7)} ${R(-r * 1.05)} 0 ${R(-r * 0.35)}C${R(r * 0.7)} ${R(-r * 1.05)} ${R(r * 1.4)} ${R(-r * 0.1)} 0 ${R(r * 0.9)}Z`;
493
+ const drop = (r) => `M0 ${R(-1.4 * r)}C${R(0.9 * r)} ${R(-0.2 * r)} ${R(r)} ${R(0.5 * r)} 0 ${R(r)}C${R(-r)} ${R(0.5 * r)} ${R(-0.9 * r)} ${R(-0.2 * r)} 0 ${R(-1.4 * r)}Z`;
494
+ const pop = (q, a = 0.2) => EASE.backOut(clamp(q / a));
495
+ const fade = (q, b = 0.25) => clamp((1 - q) / b);
496
+ const at = (x, y, inner, extra = '') => `<g transform="translate(${R(x)} ${R(y)})${extra}">${inner}</g>`;
497
+
498
+ /** Efectos: { ms, loop?, layer: 'face'|'front', draw(c, p 0..1, ageMs, o, rnd) }. */
499
+ export const EFFECTS = {
500
+ sparkles: {
501
+ ms: 1300,
502
+ draw: (c, p, age, o, rnd) => {
503
+ let out = '';
504
+ for (let i = 0; i < 6; i++) {
505
+ const a = rnd() * TAU;
506
+ const d = 0.55 + rnd() * 0.25;
507
+ const q = clamp((p - i * 0.08) / 0.5);
508
+ const sz = c.W * 0.07 * (0.6 + rnd() * 0.6) * Math.sin(Math.PI * q);
509
+ if (sz <= 0.2) continue;
510
+ out += at(c.cx + Math.cos(a) * c.W * d, c.top + c.H * 0.45 + Math.sin(a) * c.H * d * 0.85, `<path d="${star4(sz)}" fill="#ffd84d" stroke="#f59e0b" stroke-width=".8"/><circle r="${R(sz * 0.22)}" fill="#fff"/>`, ` rotate(${R(age * 0.08)})`);
511
+ }
512
+ return out;
513
+ },
514
+ },
515
+ sweat: {
516
+ ms: 1800,
517
+ draw: (c, p) => {
518
+ const s = (c.W / 120) * 6 * pop(p, 0.15);
519
+ const g = at(c.cx + c.dir * c.W * 0.4, c.top + c.H * (0.2 + 0.08 * EASE.inOut(p)), `<path d="${drop(s)}" fill="#8fd3ff" stroke="#3b9ae0" stroke-width="1.2"/><ellipse cx="${R(-s * 0.3)}" cy="${R(-s * 0.1)}" rx="${R(s * 0.18)}" ry="${R(s * 0.32)}" fill="#fff" opacity=".8"/>`, ` rotate(${R(c.dir * 12)})`);
520
+ return `<g opacity="${R(fade(p, 0.25))}">${g}</g>`;
521
+ },
522
+ },
523
+ blush: {
524
+ ms: 2000,
525
+ layer: 'face',
526
+ draw: (c, p) => {
527
+ const e = Math.min(clamp(p / 0.15), fade(p, 0.25));
528
+ return [-1, 1]
529
+ .map((s) => {
530
+ const x = c.cx + s * (c.sp + c.r * 0.35);
531
+ const y = c.eyeY + c.r * 1.8;
532
+ const lines = [0, 1, 2].map((i) => `<path d="M${R(x + (i - 1) * c.r * 0.4)} ${R(y + c.r * 0.25)}l${R(c.r * 0.22)} ${R(-c.r * 0.45)}" stroke="#ff3b6e" stroke-width="${R(c.r * 0.1)}" stroke-linecap="round"/>`).join('');
533
+ return `<g opacity="${R(e)}"><ellipse cx="${R(x)}" cy="${R(y)}" rx="${R(c.r * 0.95)}" ry="${R(c.r * 0.5)}" fill="#ff5d8f" opacity=".55"/>${lines}</g>`;
534
+ })
535
+ .join('');
536
+ },
537
+ },
538
+ hearts: {
539
+ ms: 1900,
540
+ draw: (c, p, age, o, rnd) => {
541
+ let out = '';
542
+ for (let i = 0; i < 3; i++) {
543
+ const q = clamp((p - i * 0.15) / 0.7);
544
+ if (q <= 0) continue;
545
+ const sz = c.W * (0.07 + rnd() * 0.03) * pop(q, 0.2);
546
+ const x = c.cx + (i - 1) * c.W * 0.28 + Math.sin(q * TAU + i) * c.W * 0.05;
547
+ const y = c.top + c.H * 0.12 - c.H * 0.5 * EASE.out(q);
548
+ out += `<g opacity="${R(fade(q, 0.3))}">${at(x, y, `<path d="${heart(sz)}" fill="#ff4d7a" stroke="#c81e4f" stroke-width="1"/>`)}</g>`;
549
+ }
550
+ return out;
551
+ },
552
+ },
553
+ zzz: {
554
+ ms: 2600,
555
+ loop: true,
556
+ draw: (c, p) => {
557
+ let out = '';
558
+ for (let i = 0; i < 3; i++) {
559
+ const q = (p + i / 3) % 1;
560
+ const s = (c.W / 120) * (4 + q * 6);
561
+ const x = c.cx + c.dir * (c.W * 0.32 + c.W * 0.25 * q);
562
+ const y = c.top + c.H * 0.12 - c.H * 0.45 * q;
563
+ const d = `M${R(-s)} ${R(-s)}H${R(s)}L${R(-s)} ${R(s)}H${R(s)}`;
564
+ out += `<g opacity="${R(Math.sin(Math.PI * q))}">${at(x, y, `<path d="${d}" stroke="#fff" stroke-width="${R(s * 0.75)}" fill="none" stroke-linejoin="round" stroke-linecap="round" opacity=".7"/><path d="${d}" stroke="#5b6ee1" stroke-width="${R(s * 0.38)}" fill="none" stroke-linejoin="round" stroke-linecap="round"/>`)}</g>`;
565
+ }
566
+ return out;
567
+ },
568
+ },
569
+ exclaim: {
570
+ ms: 1100,
571
+ draw: (c, p, age) => {
572
+ const k = (c.W / 120) * pop(p, 0.18);
573
+ const w = Math.sin(age / 60) * 6 * fade(p, 0.6);
574
+ const g = `<path d="M-4 -24h8l-1.6 18h-4.8z" fill="#ff3b30" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><circle cy="3" r="3.6" fill="#ff3b30" stroke="#fff" stroke-width="2"/><path d="M-12 -22l-5 -5M12 -22l5 -5M-15 -8h-7M15 -8h7" stroke="#ff9f0a" stroke-width="2.4" stroke-linecap="round"/>`;
575
+ return `<g opacity="${R(fade(p, 0.2))}">${at(c.cx + c.dir * c.W * 0.36, c.top - c.H * 0.02, g, ` rotate(${R(w)}) scale(${R(k)})`)}</g>`;
576
+ },
577
+ },
578
+ question: {
579
+ ms: 1500,
580
+ draw: (c, p, age) => {
581
+ const k = (c.W / 120) * pop(p, 0.2);
582
+ const g = `<path d="M-6 -16Q-6 -24 1 -24Q8 -24 8 -17Q8 -12 2 -9Q0 -8 0 -3" stroke="#fff" stroke-width="7" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M-6 -16Q-6 -24 1 -24Q8 -24 8 -17Q8 -12 2 -9Q0 -8 0 -3" stroke="#4f7cff" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cy="4" r="3.4" fill="#4f7cff" stroke="#fff" stroke-width="1.6"/>`;
583
+ return `<g opacity="${R(fade(p, 0.2))}">${at(c.cx + c.dir * c.W * 0.4, c.top + Math.sin(age / 200) * 2, g, ` rotate(${R(c.dir * 12)}) scale(${R(k)})`)}</g>`;
584
+ },
585
+ },
586
+ anger: {
587
+ ms: 1300,
588
+ draw: (c, p, age) => {
589
+ const k = (c.W / 120) * pop(p, 0.15) * (1 + 0.12 * Math.sin(age / 70));
590
+ let d = '';
591
+ for (let q = 0; q < 4; q++) {
592
+ const [sx, sy] = [[1, -1], [1, 1], [-1, 1], [-1, -1]][q];
593
+ d += `M${2.5 * sx} ${9 * sy}Q${2.5 * sx} ${2.5 * sy} ${9 * sx} ${2.5 * sy}`;
594
+ }
595
+ return `<g opacity="${R(fade(p, 0.25))}">${at(c.cx + c.dir * c.W * 0.33, c.top + c.H * 0.14, `<path d="${d}" stroke="#fff" stroke-width="5.5" fill="none" stroke-linecap="round"/><path d="${d}" stroke="#ff2d2d" stroke-width="3" fill="none" stroke-linecap="round"/>`, ` scale(${R(k)})`)}</g>`;
596
+ },
597
+ },
598
+ steam: {
599
+ ms: 1500,
600
+ draw: (c, p) => {
601
+ let out = '';
602
+ for (const s of [-1, 1]) {
603
+ for (let i = 0; i < 3; i++) {
604
+ const q = clamp((p - i * 0.15) / 0.65);
605
+ if (q <= 0 || q >= 1) continue;
606
+ const r = c.W * (0.04 + 0.07 * q);
607
+ out += `<circle cx="${R(c.cx + s * (c.W * 0.28 + c.W * 0.18 * q))}" cy="${R(c.top + c.H * 0.12 - c.H * 0.38 * q)}" r="${R(r)}" fill="#eef0f3" stroke="#9ca3af" stroke-width="1" opacity="${R((1 - q) * 0.85)}"/>`;
608
+ }
609
+ }
610
+ return out;
611
+ },
612
+ },
613
+ confetti: {
614
+ ms: 1900,
615
+ draw: (c, p, age, o, rnd) => {
616
+ const cols = ['#ff4d7a', '#ffd84d', '#4dd2ff', '#7cff6b', '#b18cff', '#ff9f43'];
617
+ const x0 = c.cx;
618
+ const y0 = c.top + c.H * 0.05;
619
+ const tt = p * 1.9;
620
+ let out = '';
621
+ for (let i = 0; i < 22; i++) {
622
+ const a = -Math.PI / 2 + (rnd() - 0.5) * 2.2;
623
+ const v = c.H * (0.9 + rnd() * 0.8);
624
+ const x = x0 + Math.cos(a) * v * tt * 0.7;
625
+ const y = y0 + Math.sin(a) * v * tt + c.H * 1.1 * tt * tt;
626
+ const w = c.W * 0.035;
627
+ out += `<rect x="${R(x - w / 2)}" y="${R(y - w / 4)}" width="${R(w)}" height="${R(w / 2)}" fill="${cols[i % cols.length]}" transform="rotate(${R(age * (0.3 + rnd() * 0.5) + i * 40)} ${R(x)} ${R(y)})"/>`;
628
+ }
629
+ return `<g opacity="${R(fade(p, 0.3))}">${out}</g>`;
630
+ },
631
+ },
632
+ tears: {
633
+ ms: 2200,
634
+ layer: 'face',
635
+ draw: (c, p) => {
636
+ let out = '';
637
+ for (const s of [-1, 1]) {
638
+ for (let i = 0; i < 2; i++) {
639
+ const q = ((p * 2 + i * 0.5) % 1) * (p < 0.9 ? 1 : 0);
640
+ if (q <= 0) continue;
641
+ const sz = c.r * 0.28;
642
+ out += `<g opacity="${R(1 - q)}">${at(c.cx + s * (c.sp + c.r * 0.55), c.eyeY + c.r * 0.7 + c.H * 0.3 * q * q, `<path d="${drop(sz)}" fill="#8fd3ff" stroke="#3b9ae0" stroke-width=".8"/>`)}</g>`;
643
+ }
644
+ }
645
+ return out;
646
+ },
647
+ },
648
+ thought: {
649
+ ms: 2200,
650
+ draw: (c, p, age) => {
651
+ const e = fade(p, 0.15);
652
+ const k = c.W / 120;
653
+ const bx = c.cx + c.dir * c.W * 0.55;
654
+ const by = c.top - c.H * 0.2;
655
+ const pp = (q) => pop(clamp((p - q) / 0.4), 0.5);
656
+ const st = 'fill="#fff" stroke="#cbd5e1" stroke-width="1.5"';
657
+ let out = `<circle cx="${R(c.cx + c.dir * c.W * 0.3)}" cy="${R(c.top + c.H * 0.03)}" r="${R(3 * k * pp(0))}" ${st}/>`;
658
+ out += `<circle cx="${R(c.cx + c.dir * c.W * 0.4)}" cy="${R(c.top - c.H * 0.07)}" r="${R(5 * k * pp(0.08))}" ${st}/>`;
659
+ const b = pp(0.16);
660
+ out += at(bx, by, `<path d="M-20 4C-28 4 -28 -8 -19 -9C-19 -18 -6 -20 -2 -13C2 -21 16 -19 16 -10C26 -11 28 3 18 4C16 11 -16 11 -20 4Z" ${st}/>${[-8, 0, 8].map((x, i) => `<circle cx="${x}" cy="-3" r="2.4" fill="#64748b" opacity="${R(0.35 + 0.65 * Math.max(0, Math.sin(age / 180 - i * 0.9)))}"/>`).join('')}`, ` scale(${R(k * b)})`);
661
+ return `<g opacity="${R(e)}">${out}</g>`;
662
+ },
663
+ },
664
+ notes: {
665
+ ms: 2200,
666
+ draw: (c, p) => {
667
+ let out = '';
668
+ const k = c.W / 120;
669
+ for (let i = 0; i < 3; i++) {
670
+ const q = clamp((p - i * 0.18) / 0.65);
671
+ if (q <= 0 || q >= 1) continue;
672
+ const s = i % 2 ? -1 : 1;
673
+ const x = c.cx + s * c.W * (0.42 + 0.1 * q) + Math.sin(q * TAU) * 4;
674
+ const y = c.top + c.H * 0.25 - c.H * 0.5 * q;
675
+ out += `<g opacity="${R(Math.sin(Math.PI * q))}">${at(x, y, '<ellipse cx="-3" cy="6" rx="4.2" ry="3.2" transform="rotate(-20 -3 6)" fill="#8b5cf6"/><path d="M0.8 5V-12Q5 -9 8 -6" stroke="#8b5cf6" stroke-width="2" fill="none" stroke-linecap="round"/>', ` scale(${R(k)}) rotate(${R(s * 10)})`)}</g>`;
676
+ }
677
+ return out;
678
+ },
679
+ },
680
+ sneeze: {
681
+ ms: 700,
682
+ draw: (c, p, age, o, rnd) => {
683
+ let out = '';
684
+ const q = EASE.out(p);
685
+ for (let i = 0; i < 10; i++) {
686
+ const a = (rnd() - 0.5) * 1.1;
687
+ const d = c.W * (0.15 + rnd() * 0.45) * q;
688
+ const x = c.cx + c.dir * (c.mw * 0.8 + Math.cos(a) * d);
689
+ const y = c.mouthY + Math.sin(a) * d + c.H * 0.15 * q * q;
690
+ out += `<circle cx="${R(x)}" cy="${R(y)}" r="${R(c.W * (0.012 + rnd() * 0.015))}" fill="#9fd8ff" stroke="#3b9ae0" stroke-width=".6"/>`;
691
+ }
692
+ return `<g opacity="${R(1 - p)}">${out}</g>`;
693
+ },
694
+ },
695
+ bubble: {
696
+ ms: 1000,
697
+ draw: (c, p) => {
698
+ const q = EASE.out(clamp(p / 0.85));
699
+ const burst = clamp((p - 0.85) / 0.15);
700
+ const r = c.W * (0.03 + 0.05 * q) * (1 + burst * 0.4);
701
+ const x = c.cx + c.dir * c.mw * 0.5 + Math.sin(p * 9) * 3;
702
+ const y = c.mouthY - c.H * 0.3 * q;
703
+ const lines = burst > 0 ? [0, 1, 2, 3, 4, 5].map((i) => { const a = (i / 6) * TAU; return `<path d="M${R(Math.cos(a) * r * 1.1)} ${R(Math.sin(a) * r * 1.1)}L${R(Math.cos(a) * r * 1.5)} ${R(Math.sin(a) * r * 1.5)}" stroke="#7cc4ff" stroke-width="1.4" stroke-linecap="round"/>`; }).join('') : '';
704
+ return `<g opacity="${R(1 - burst)}">${at(x, y, `<circle r="${R(r)}" fill="#bfe9ff" fill-opacity=".45" stroke="#7cc4ff" stroke-width="1.4"/><ellipse cx="${R(-r * 0.35)}" cy="${R(-r * 0.35)}" rx="${R(r * 0.22)}" ry="${R(r * 0.14)}" transform="rotate(-40 ${R(-r * 0.35)} ${R(-r * 0.35)})" fill="#fff"/>${lines}`)}</g>`;
705
+ },
706
+ },
707
+ dizzy: {
708
+ ms: 1600,
709
+ loop: true,
710
+ draw: (c, p) => {
711
+ let out = '';
712
+ for (let i = 0; i < 3; i++) {
713
+ const a = p * TAU + (i * TAU) / 3;
714
+ const z = Math.sin(a);
715
+ const s = (c.W / 120) * (5 + z * 1.5);
716
+ out += `<g opacity="${R(0.65 + z * 0.35)}">${at(c.cx + Math.cos(a) * c.W * 0.4, c.top + c.H * 0.02 + z * c.H * 0.07, `<path d="${star4(s)}" fill="#ffd84d" stroke="#f59e0b" stroke-width="1"/>`)}</g>`;
717
+ }
718
+ return out;
719
+ },
720
+ },
721
+ };
722
+
723
+ // ───────────────────────────── visemas (lip-sync) ─────────────────────────────
724
+
725
+ /**
726
+ * Formas de boca: open = apertura 0..1, w = anchura relativa (≈1 normal, >1 estirada, <1 redonda).
727
+ * X silencio · A abierta · E/I estiradas · O/U redondas · M labios cerrados (m, b, p)
728
+ * · F labio-dental (f, v) · S sibilantes · C resto de consonantes.
729
+ */
730
+ export const VISEMES = Object.freeze({
731
+ X: { open: 0, w: 1 },
732
+ A: { open: 0.85, w: 1.05 },
733
+ E: { open: 0.5, w: 1.18 },
734
+ I: { open: 0.3, w: 1.25 },
735
+ O: { open: 0.7, w: 0.72 },
736
+ U: { open: 0.4, w: 0.6 },
737
+ M: { open: 0, w: 0.92 },
738
+ F: { open: 0.15, w: 1.05 },
739
+ S: { open: 0.2, w: 1.15 },
740
+ C: { open: 0.3, w: 1 },
741
+ });
742
+
743
+ const LETTER = { a: 'A', e: 'E', i: 'I', y: 'I', o: 'O', u: 'U', w: 'U', m: 'M', b: 'M', p: 'M', f: 'F', v: 'F', s: 'S', z: 'S', x: 'S', c: 'S', j: 'S', h: '' };
744
+ const WEIGHT = { A: 1, E: 1, I: 0.9, O: 1, U: 0.9, M: 0.7, F: 0.6, S: 0.6, C: 0.55 };
745
+
746
+ /**
747
+ * Texto → secuencia de visemas [{ v, w (peso ≈ duración relativa), c (índice del carácter) }].
748
+ * Funciona con español, inglés y portugués de forma aproximada (letra a letra, sin fonética).
749
+ */
750
+ export function textToVisemes(text) {
751
+ const out = [];
752
+ const s = String(text ?? '');
753
+ const push = (v, w, c) => {
754
+ const last = out[out.length - 1];
755
+ if (last && last.v === v && (v === 'X' || v === 'C' || v === 'S')) last.w = Math.min(last.w + w * 0.6, 3);
756
+ else out.push({ v, w, c });
757
+ };
758
+ for (let i = 0; i < s.length; i++) {
759
+ const ch = s[i].normalize('NFD')[0].toLowerCase();
760
+ if (/\p{L}/u.test(ch)) {
761
+ // «qu»/«gu» + e/i: la u no suena
762
+ if (ch === 'u' && /[qg]/i.test(s[i - 1] || '') && /[eiéí]/i.test(s[i + 1] || '')) continue;
763
+ let v = LETTER[ch];
764
+ if (v === '') continue; // h muda
765
+ if (v === undefined) v = 'C';
766
+ push(v, WEIGHT[v], i);
767
+ } else if (/\d/.test(ch)) {
768
+ push('C', 0.6, i);
769
+ push('A', 0.6, i);
770
+ }
771
+ else if (/[.!?…;:]/.test(ch)) push('X', /[;:]/.test(ch) ? 1.6 : 2.6, i);
772
+ else if (/[,—–-]/.test(ch)) push('X', 1.6, i);
773
+ else if (/\s/.test(ch)) push('X', 0.35, i);
774
+ }
775
+ while (out.length && out[out.length - 1].v === 'X') out.pop();
776
+ while (out.length && out[0].v === 'X') out.shift();
777
+ return out;
778
+ }
779
+
780
+ /** Secuencia genérica de sílabas para hablar «sin texto» durante `ms` (determinista con `seed`). */
781
+ export function babbleVisemes(ms, seed = 7) {
782
+ const rnd = seeded(seed);
783
+ const vowels = ['A', 'E', 'O', 'A', 'I', 'U', 'E', 'A'];
784
+ const cons = ['M', 'C', 'S', 'C', 'F', 'C'];
785
+ const out = [];
786
+ let t = 0;
787
+ while (t < ms) {
788
+ out.push({ v: cons[Math.floor(rnd() * cons.length)], w: 0.55, c: -1 });
789
+ out.push({ v: vowels[Math.floor(rnd() * vowels.length)], w: 0.8 + rnd() * 0.6, c: -1 });
790
+ if (rnd() < 0.12) out.push({ v: 'X', w: 0.8, c: -1 });
791
+ t += 150;
792
+ }
793
+ return out;
794
+ }