@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.
- package/.env.example +140 -0
- package/LICENSE +21 -0
- package/README.md +655 -0
- package/brand/apple-touch-icon.png +0 -0
- package/brand/favicon.svg +4 -0
- package/brand/icon-512.png +0 -0
- package/brand/mark-mono.svg +3 -0
- package/brand/mark.svg +4 -0
- package/brand/og-es.png +0 -0
- package/brand/og-pt.png +0 -0
- package/brand/og.png +0 -0
- package/brand/ots.json +11 -0
- package/brand/tokens.css +59 -0
- package/brand/wordmark.svg +5 -0
- package/cli/7ots.mjs +400 -0
- package/cli/lib/brain.mjs +143 -0
- package/cli/lib/config.mjs +70 -0
- package/cli/lib/hooks.mjs +151 -0
- package/cli/lib/i18n.mjs +480 -0
- package/cli/lib/identity.mjs +51 -0
- package/cli/lib/install.mjs +79 -0
- package/cli/lib/lines.mjs +73 -0
- package/cli/lib/meet.mjs +166 -0
- package/cli/lib/open.mjs +19 -0
- package/cli/lib/orquesta.mjs +84 -0
- package/cli/lib/paths.mjs +49 -0
- package/cli/lib/pet-core.mjs +184 -0
- package/cli/lib/pet-server.mjs +296 -0
- package/cli/lib/prompts.mjs +75 -0
- package/cli/lib/terminal.mjs +145 -0
- package/cli/lib/ui.mjs +234 -0
- package/cli/lib/wizard.mjs +279 -0
- package/cli/pet/electron/main.cjs +86 -0
- package/cli/pet/pet.html +404 -0
- package/dist/7ots.esm.js +627 -0
- package/dist/7ots.esm.js.map +7 -0
- package/dist/7ots.iife.js +627 -0
- package/dist/7ots.iife.js.map +7 -0
- package/llms.txt +57 -0
- package/package.json +73 -0
- package/server/admin.mjs +213 -0
- package/server/apuchat-relay.mjs +170 -0
- package/server/channels/agent.mjs +136 -0
- package/server/channels/apuchat.mjs +364 -0
- package/server/channels/apumail.mjs +211 -0
- package/server/channels/index.mjs +69 -0
- package/server/contact.mjs +151 -0
- package/server/demo.mjs +154 -0
- package/server/i18n.mjs +54 -0
- package/server/identity.mjs +108 -0
- package/server/llm.mjs +413 -0
- package/server/platform/auth.mjs +290 -0
- package/server/platform/crypto.mjs +80 -0
- package/server/platform/db.mjs +130 -0
- package/server/platform/routes.mjs +388 -0
- package/server/platform/runtime.mjs +180 -0
- package/server/platform/store.mjs +331 -0
- package/server/sdk.mjs +125 -0
- package/server/server.mjs +456 -0
- package/server/settings.mjs +177 -0
- package/server/tts.mjs +186 -0
- package/skills/7ots/SKILL.md +59 -0
- package/src/actions/ActionRegistry.js +254 -0
- package/src/actions/PageTools.js +270 -0
- package/src/actions/builtins.js +565 -0
- package/src/auth/AuthManager.js +128 -0
- package/src/avatar/AvatarStage.js +473 -0
- package/src/character/AvatarEditor.js +829 -0
- package/src/character/Character.js +1191 -0
- package/src/character/motion.js +794 -0
- package/src/character/parts.js +559 -0
- package/src/context/ContextManager.js +472 -0
- package/src/core/AgentBrain.js +341 -0
- package/src/core/AgentWidget.js +1059 -0
- package/src/core/EventBus.js +61 -0
- package/src/core/Proactivity.js +363 -0
- package/src/core/storage.js +35 -0
- package/src/i18n/index.js +193 -0
- package/src/i18n/messages/character.js +543 -0
- package/src/i18n/messages/identity.js +141 -0
- package/src/i18n/messages/platform.js +114 -0
- package/src/i18n/messages/server.js +630 -0
- package/src/i18n/messages/widget.js +362 -0
- package/src/identity/ContactCard.js +351 -0
- package/src/identity/Face.js +148 -0
- package/src/identity/random.js +112 -0
- package/src/identity/schema.js +262 -0
- package/src/index.js +117 -0
- package/src/integrations/apuchat.js +212 -0
- package/src/integrations/apumail.js +88 -0
- package/src/llm/ProxyLLM.js +87 -0
- package/src/mcp/McpClient.js +153 -0
- package/src/ui/Companion.js +498 -0
- package/src/ui/UIManager.js +425 -0
- package/src/ui/VirtualPointer.js +746 -0
- package/src/ui/markdown.js +94 -0
- 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
|
+
}
|