@lemoncat7/dsh-theme-xiaohei 0.3.0-alpha.23 → 0.3.2
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/README.md +39 -17
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/lib/appearance.js +26 -10
- package/lib/blink.js +3 -3
- package/lib/bloub-heixiu/ambient.d.ts +3 -0
- package/lib/bloub-heixiu/ambient.js +12 -0
- package/lib/bloub-heixiu/character.d.ts +33 -0
- package/lib/bloub-heixiu/character.js +124 -0
- package/lib/bloub-heixiu/effects.d.ts +4 -0
- package/lib/bloub-heixiu/effects.js +91 -0
- package/lib/bloub-heixiu/flight.d.ts +15 -0
- package/lib/bloub-heixiu/flight.js +66 -0
- package/lib/bloub-heixiu/follow.d.ts +13 -0
- package/lib/bloub-heixiu/follow.js +23 -0
- package/lib/bloub-heixiu/layout.d.ts +18 -0
- package/lib/bloub-heixiu/layout.js +12 -0
- package/lib/bloub-heixiu/renderer.d.ts +16 -0
- package/lib/bloub-heixiu/renderer.js +94 -0
- package/lib/bloub-heixiu/runtime.d.ts +6 -0
- package/lib/bloub-heixiu/runtime.js +395 -0
- package/lib/bloub-heixiu/signals.d.ts +17 -0
- package/lib/bloub-heixiu/signals.js +53 -0
- package/lib/bloub-heixiu/styles.d.ts +2 -0
- package/lib/bloub-heixiu/styles.js +98 -0
- package/lib/chrome/accessibility.d.ts +1 -1
- package/lib/chrome/accessibility.js +5 -13
- package/lib/chrome/composer-avatar.d.ts +10 -0
- package/lib/chrome/composer-avatar.js +599 -0
- package/lib/chrome/composer-send-heixiu.d.ts +3 -0
- package/lib/chrome/composer-send-heixiu.js +143 -0
- package/lib/chrome/composer.d.ts +2 -2
- package/lib/chrome/composer.js +84 -95
- package/lib/chrome/controls.d.ts +8 -0
- package/lib/chrome/controls.js +348 -0
- package/lib/chrome/conversation-messages.d.ts +1 -1
- package/lib/chrome/conversation-messages.js +207 -9
- package/lib/chrome/conversation.d.ts +1 -1
- package/lib/chrome/conversation.js +6 -0
- package/lib/chrome/identity.js +4 -18
- package/lib/chrome/portal.d.ts +1 -1
- package/lib/chrome/portal.js +1 -1
- package/lib/chrome/sidebar.d.ts +1 -1
- package/lib/chrome/sidebar.js +134 -153
- package/lib/chrome/tokens.d.ts +1 -1
- package/lib/chrome/tokens.js +121 -68
- package/lib/chrome/workspace.d.ts +2 -2
- package/lib/chrome/workspace.js +35 -254
- package/lib/chrome.js +4 -0
- package/lib/client.d.ts +5 -12
- package/lib/client.js +6262 -4455
- package/lib/composer-avatar.d.ts +18 -0
- package/lib/composer-avatar.js +441 -0
- package/lib/composer-send-heixiu.d.ts +8 -0
- package/lib/composer-send-heixiu.js +133 -0
- package/lib/gaze.js +3 -3
- package/lib/generated-backgrounds.d.ts +14 -0
- package/lib/generated-backgrounds.js +14 -0
- package/lib/generated-character-motion.d.ts +4 -0
- package/lib/generated-character-motion.js +2 -0
- package/lib/generated-identity.d.ts +1 -0
- package/lib/generated-identity.js +1 -0
- package/lib/generated-keyart.d.ts +18 -3
- package/lib/generated-keyart.js +18 -3
- package/lib/host-contract.d.ts +9 -0
- package/lib/host-contract.js +13 -0
- package/lib/host-dom.d.ts +11 -0
- package/lib/host-dom.js +60 -0
- package/lib/loading-heixiu.js +3 -3
- package/lib/metallic-brand-mark.d.ts +10 -0
- package/lib/metallic-brand-mark.js +323 -0
- package/lib/plugin.d.ts +1 -1
- package/lib/plugin.js +31 -16
- package/lib/portal.js +11 -10
- package/lib/reactions.js +3 -3
- package/lib/scene/avatar-model.d.ts +6 -0
- package/lib/scene/avatar-model.js +646 -0
- package/lib/scene/avatar-motion.d.ts +3 -0
- package/lib/scene/avatar-motion.js +799 -0
- package/lib/scene/character-idle.d.ts +17 -0
- package/lib/scene/character-idle.js +65 -0
- package/lib/scene/character-layout.d.ts +24 -0
- package/lib/scene/character-layout.js +51 -0
- package/lib/scene/character-motion-clock.d.ts +18 -0
- package/lib/scene/character-motion-clock.js +57 -0
- package/lib/scene/character-motion-types.d.ts +12 -0
- package/lib/scene/character-motion-types.js +2 -0
- package/lib/scene/character-motion.d.ts +6 -0
- package/lib/scene/character-motion.js +126 -0
- package/lib/scene/character-poses.d.ts +26 -0
- package/lib/scene/character-poses.js +11 -0
- package/lib/scene/character-rig-renderer.d.ts +4 -0
- package/lib/scene/character-rig-renderer.js +85 -0
- package/lib/scene/character-rig.d.ts +20 -0
- package/lib/scene/character-rig.js +53 -0
- package/lib/scene/heixiu-feedback.d.ts +1 -1
- package/lib/scene/heixiu-feedback.js +0 -17
- package/lib/scene/pointer-bridge.d.ts +21 -0
- package/lib/scene/pointer-bridge.js +155 -0
- package/lib/scene/runtime.d.ts +12 -0
- package/lib/scene/runtime.js +87 -0
- package/lib/scene/styles.d.ts +7 -0
- package/lib/scene/styles.js +75 -0
- package/lib/scene/sylva-performance.d.ts +11 -0
- package/lib/scene/sylva-performance.js +58 -0
- package/lib/scene/wallpaper-character-styles.d.ts +3 -0
- package/lib/scene/wallpaper-character-styles.js +53 -0
- package/lib/scene/wallpaper-character.d.ts +4 -0
- package/lib/scene/wallpaper-character.js +213 -0
- package/lib/scene.d.ts +7 -13
- package/lib/scene.js +7 -1023
- package/lib/sidebar-glass.js +99 -36
- package/lib/sidebar-heixiu-roaming.js +5 -5
- package/lib/sidebar-reveal.d.ts +8 -0
- package/lib/sidebar-reveal.js +51 -0
- package/lib/state.d.ts +14 -2
- package/lib/state.js +12 -4
- package/lib/theme.js +2 -2
- package/lib/vendor/bloub/decor.d.ts +114 -0
- package/lib/vendor/bloub/decor.js +198 -0
- package/lib/vendor/bloub/engine.d.ts +213 -0
- package/lib/vendor/bloub/engine.js +452 -0
- package/lib/vendor/bloub/expressions.d.ts +35 -0
- package/lib/vendor/bloub/expressions.js +143 -0
- package/lib/vendor/bloub/eyefit.d.ts +75 -0
- package/lib/vendor/bloub/eyefit.js +393 -0
- package/lib/vendor/bloub/face.d.ts +63 -0
- package/lib/vendor/bloub/face.js +106 -0
- package/lib/vendor/bloub/math.d.ts +22 -0
- package/lib/vendor/bloub/math.js +36 -0
- package/lib/vendor/bloub/profiles.d.ts +8 -0
- package/lib/vendor/bloub/profiles.js +20 -0
- package/lib/vendor/bloub/repere.d.ts +30 -0
- package/lib/vendor/bloub/repere.js +32 -0
- package/lib/vendor/bloub/shape.d.ts +80 -0
- package/lib/vendor/bloub/shape.js +268 -0
- package/lib/vendor/bloub/skins.d.ts +36 -0
- package/lib/vendor/bloub/skins.js +74 -0
- package/lib/vendor/bloub/states.d.ts +87 -0
- package/lib/vendor/bloub/states.js +468 -0
- package/package.json +23 -6
- package/lib/appearance.d.ts.map +0 -1
- package/lib/appearance.js.map +0 -1
- package/lib/blink.d.ts.map +0 -1
- package/lib/blink.js.map +0 -1
- package/lib/boot-appearance.d.ts.map +0 -1
- package/lib/boot-appearance.js.map +0 -1
- package/lib/boot-loader.d.ts.map +0 -1
- package/lib/boot-loader.js.map +0 -1
- package/lib/chrome/accessibility.d.ts.map +0 -1
- package/lib/chrome/accessibility.js.map +0 -1
- package/lib/chrome/blink.d.ts.map +0 -1
- package/lib/chrome/blink.js.map +0 -1
- package/lib/chrome/composer.d.ts.map +0 -1
- package/lib/chrome/composer.js.map +0 -1
- package/lib/chrome/conversation-messages.d.ts.map +0 -1
- package/lib/chrome/conversation-messages.js.map +0 -1
- package/lib/chrome/conversation.d.ts.map +0 -1
- package/lib/chrome/conversation.js.map +0 -1
- package/lib/chrome/frames.d.ts.map +0 -1
- package/lib/chrome/frames.js.map +0 -1
- package/lib/chrome/gaze.d.ts.map +0 -1
- package/lib/chrome/gaze.js.map +0 -1
- package/lib/chrome/identity.d.ts.map +0 -1
- package/lib/chrome/identity.js.map +0 -1
- package/lib/chrome/overlays.d.ts.map +0 -1
- package/lib/chrome/overlays.js.map +0 -1
- package/lib/chrome/portal.d.ts.map +0 -1
- package/lib/chrome/portal.js.map +0 -1
- package/lib/chrome/reactions.d.ts.map +0 -1
- package/lib/chrome/reactions.js.map +0 -1
- package/lib/chrome/sidebar.d.ts.map +0 -1
- package/lib/chrome/sidebar.js.map +0 -1
- package/lib/chrome/tokens.d.ts.map +0 -1
- package/lib/chrome/tokens.js.map +0 -1
- package/lib/chrome/workspace.d.ts.map +0 -1
- package/lib/chrome/workspace.js.map +0 -1
- package/lib/chrome.d.ts.map +0 -1
- package/lib/chrome.js.map +0 -1
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js.map +0 -7
- package/lib/gaze.d.ts.map +0 -1
- package/lib/gaze.js.map +0 -1
- package/lib/generated-identity.d.ts.map +0 -1
- package/lib/generated-identity.js.map +0 -1
- package/lib/generated-keyart.d.ts.map +0 -1
- package/lib/generated-keyart.js.map +0 -1
- package/lib/generated-loading-assets.d.ts.map +0 -1
- package/lib/generated-loading-assets.js.map +0 -1
- package/lib/generated-sidebar-assets.d.ts +0 -4
- package/lib/generated-sidebar-assets.d.ts.map +0 -1
- package/lib/generated-sidebar-assets.js +0 -4
- package/lib/generated-sidebar-assets.js.map +0 -1
- package/lib/heixiu-interactions.d.ts.map +0 -1
- package/lib/heixiu-interactions.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/loading-heixiu.d.ts.map +0 -1
- package/lib/loading-heixiu.js.map +0 -1
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js.map +0 -1
- package/lib/portal.d.ts.map +0 -1
- package/lib/portal.js.map +0 -1
- package/lib/reactions.d.ts.map +0 -1
- package/lib/reactions.js.map +0 -1
- package/lib/scene/heixiu-feedback.d.ts.map +0 -1
- package/lib/scene/heixiu-feedback.js.map +0 -1
- package/lib/scene/heixiu-interactions.d.ts.map +0 -1
- package/lib/scene/heixiu-interactions.js.map +0 -1
- package/lib/scene/state-transitions.d.ts.map +0 -1
- package/lib/scene/state-transitions.js.map +0 -1
- package/lib/scene.d.ts.map +0 -1
- package/lib/scene.js.map +0 -1
- package/lib/sidebar-glass.d.ts.map +0 -1
- package/lib/sidebar-glass.js.map +0 -1
- package/lib/sidebar-heixiu-escape.d.ts.map +0 -1
- package/lib/sidebar-heixiu-escape.js.map +0 -1
- package/lib/sidebar-heixiu-roaming.d.ts.map +0 -1
- package/lib/sidebar-heixiu-roaming.js.map +0 -1
- package/lib/state.d.ts.map +0 -1
- package/lib/state.js.map +0 -1
- package/lib/theme.d.ts.map +0 -1
- package/lib/theme.js.map +0 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// Adapted from bloub, Copyright (c) 2026 Jérémy Perret (MIT).
|
|
2
|
+
// See THIRD_PARTY_NOTICES.md for source revision and local changes.
|
|
3
|
+
import { arcRender } from './decor.js';
|
|
4
|
+
import { blendExpression } from './expressions.js';
|
|
5
|
+
import { decalageDesYeux } from './eyefit.js';
|
|
6
|
+
import { blinkScale, eyePoses, liveliness } from './face.js';
|
|
7
|
+
import { clamp, easings, lerp, r2 } from './math.js';
|
|
8
|
+
import { blend, capsulePath, closedPath, radiusAtAngle, toPoints } from './shape.js';
|
|
9
|
+
import { STATE_BY_ID } from './states.js';
|
|
10
|
+
const NO_LOOK = { yaw: 0, pitch: 0, mix: 0, spin: 0, wander: 1 };
|
|
11
|
+
const lerpLook = (a, b, t) => ({
|
|
12
|
+
yaw: lerp(a.yaw, b.yaw, t),
|
|
13
|
+
pitch: lerp(a.pitch, b.pitch, t),
|
|
14
|
+
mix: lerp(a.mix, b.mix, t),
|
|
15
|
+
spin: lerp(a.spin, b.spin, t),
|
|
16
|
+
wander: lerp(a.wander, b.wander, t)
|
|
17
|
+
});
|
|
18
|
+
const lerpEye = (a, b, t) => ({
|
|
19
|
+
w: lerp(a.w, b.w, t),
|
|
20
|
+
h: lerp(a.h, b.h, t),
|
|
21
|
+
open: lerp(a.open, b.open, t),
|
|
22
|
+
tilt: lerp(a.tilt ?? 0, b.tilt ?? 0, t)
|
|
23
|
+
});
|
|
24
|
+
/** Interpolation de deux poses. Le decor se croise en opacite, pas en geometrie. */
|
|
25
|
+
function blendPose(a, b, t) {
|
|
26
|
+
const out = 1 - t;
|
|
27
|
+
return {
|
|
28
|
+
sil: blend(a.sil, b.sil, t),
|
|
29
|
+
offX: lerp(a.offX, b.offX, t),
|
|
30
|
+
offY: lerp(a.offY, b.offY, t),
|
|
31
|
+
gaze: {
|
|
32
|
+
yaw: lerp(a.gaze.yaw, b.gaze.yaw, t),
|
|
33
|
+
pitch: lerp(a.gaze.pitch, b.gaze.pitch, t),
|
|
34
|
+
roll: lerp(a.gaze.roll, b.gaze.roll, t)
|
|
35
|
+
},
|
|
36
|
+
split: lerp(a.split, b.split, t),
|
|
37
|
+
eyes: [lerpEye(a.eyes[0], b.eyes[0], t), lerpEye(a.eyes[1], b.eyes[1], t)],
|
|
38
|
+
eyeAlpha: lerp(a.eyeAlpha, b.eyeAlpha, t),
|
|
39
|
+
bodyAlpha: lerp(a.bodyAlpha, b.bodyAlpha, t),
|
|
40
|
+
dots: [
|
|
41
|
+
...a.dots.map((d) => ({ ...d, opacity: d.opacity * out })),
|
|
42
|
+
...b.dots.map((d) => ({ ...d, opacity: d.opacity * t }))
|
|
43
|
+
],
|
|
44
|
+
arcs: [
|
|
45
|
+
...a.arcs.map((r) => ({ ...r, id: `a${r.id}`, opacity: r.opacity * out })),
|
|
46
|
+
...b.arcs.map((r) => ({ ...r, id: `b${r.id}`, opacity: r.opacity * t }))
|
|
47
|
+
],
|
|
48
|
+
// Theme adaptation: grow/shrink badge and transparent notch together,
|
|
49
|
+
// including interrupted morphs, instead of flashing them at the midpoint.
|
|
50
|
+
notif: a.notif && b.notif ? {
|
|
51
|
+
x: lerp(a.notif.x, b.notif.x, t), y: lerp(a.notif.y, b.notif.y, t),
|
|
52
|
+
r: lerp(a.notif.r, b.notif.r, t), notch: lerp(a.notif.notch, b.notif.notch, t),
|
|
53
|
+
} : b.notif ? { ...b.notif, r: b.notif.r * t, notch: b.notif.notch * t }
|
|
54
|
+
: a.notif ? { ...a.notif, r: a.notif.r * out, notch: a.notif.notch * out } : null,
|
|
55
|
+
dotsBehind: t < 0.5 ? a.dotsBehind : b.dotsBehind
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Moteur sans horloge : `sample(t)` est une fonction pure du temps.
|
|
60
|
+
*
|
|
61
|
+
* Consequence pratique : pause, reprise, ralenti et saut a une date arbitraire
|
|
62
|
+
* donnent exactement la meme image, et le rendu est testable sans DOM.
|
|
63
|
+
*/
|
|
64
|
+
export class BotEngine {
|
|
65
|
+
/** rayon de la boule au repos, en unites de viewBox */
|
|
66
|
+
scale;
|
|
67
|
+
cur;
|
|
68
|
+
prev = null;
|
|
69
|
+
/**
|
|
70
|
+
* Pose de depart FIGEE, posee seulement quand un changement d'etat arrive alors qu'un
|
|
71
|
+
* fondu est deja en cours. Cf. `setState`.
|
|
72
|
+
*/
|
|
73
|
+
departFige = null;
|
|
74
|
+
tCur = 0;
|
|
75
|
+
tPrev = 0;
|
|
76
|
+
blinkAt = -10;
|
|
77
|
+
pts = [];
|
|
78
|
+
shape = null;
|
|
79
|
+
shapePrev = null;
|
|
80
|
+
shapeAt = -10;
|
|
81
|
+
expr = null;
|
|
82
|
+
exprPrev = null;
|
|
83
|
+
exprAt = -10;
|
|
84
|
+
look = NO_LOOK;
|
|
85
|
+
lookPrev = NO_LOOK;
|
|
86
|
+
lookAt = -10;
|
|
87
|
+
/** duree de rattrapage en cours ; voir `LOOK_MORPH`, sa valeur par defaut */
|
|
88
|
+
lookMorph = 0.24;
|
|
89
|
+
/** duree du morph quand on change la forme du corps */
|
|
90
|
+
static SHAPE_MORPH = 0.45;
|
|
91
|
+
/**
|
|
92
|
+
* Duree de rattrapage du regard vers la cible. Plus court que `SHAPE_MORPH` :
|
|
93
|
+
* un regard qui suit doit paraitre attentif, pas visqueux. Comme la cible est
|
|
94
|
+
* reposee a chaque mouvement de souris, c'est cette duree qui donne au suivi
|
|
95
|
+
* son inertie — le regard n'atteint jamais tout a fait un curseur qui bouge.
|
|
96
|
+
*/
|
|
97
|
+
static LOOK_MORPH = 0.24;
|
|
98
|
+
constructor(scale = 100, initial = 'idle', shape = null, expression = null) {
|
|
99
|
+
this.scale = scale;
|
|
100
|
+
this.cur = initial;
|
|
101
|
+
this.shape = shape;
|
|
102
|
+
this.expr = expression;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Expression de repos choisie dans le personnalisateur. Comme la forme, elle
|
|
106
|
+
* glisse vers la nouvelle valeur au lieu de sauter.
|
|
107
|
+
*/
|
|
108
|
+
setExpression(expression, now = 0) {
|
|
109
|
+
if (expression === this.expr)
|
|
110
|
+
return;
|
|
111
|
+
this.exprPrev = this.expr;
|
|
112
|
+
this.expr = expression;
|
|
113
|
+
this.exprAt = now;
|
|
114
|
+
}
|
|
115
|
+
/** Expression effective a l'instant `now`, morph en cours compris. */
|
|
116
|
+
exprAtTime(now) {
|
|
117
|
+
const to = this.expr;
|
|
118
|
+
const from = this.exprPrev;
|
|
119
|
+
if (!to || !from)
|
|
120
|
+
return to;
|
|
121
|
+
const k = (now - this.exprAt) / BotEngine.SHAPE_MORPH;
|
|
122
|
+
if (k >= 1)
|
|
123
|
+
return to;
|
|
124
|
+
return blendExpression(from, to, easings.easeOutQuint(clamp(k)));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Forme choisie dans le personnalisateur. Elle ne remplace le corps que sur
|
|
128
|
+
* les etats au repos (`baseBody`) : sur les autres, la silhouette EST
|
|
129
|
+
* l'animation et ne doit pas etre ecrasee.
|
|
130
|
+
*
|
|
131
|
+
* Le changement se fait en morph, pas d'un coup : comme toutes les formes sont
|
|
132
|
+
* echantillonnees aux memes angles, il suffit d'interpoler les rayons.
|
|
133
|
+
*/
|
|
134
|
+
setShape(radii, now = 0) {
|
|
135
|
+
if (radii === this.shape)
|
|
136
|
+
return;
|
|
137
|
+
this.shapePrev = this.shape;
|
|
138
|
+
this.shape = radii;
|
|
139
|
+
this.shapeAt = now;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Forme effective a l'instant `now`, morph en cours compris.
|
|
143
|
+
*
|
|
144
|
+
* Ne remet PAS `shapePrev` a null en fin de morph : `sample` doit rester une
|
|
145
|
+
* fonction pure du temps, donc relire une date passee doit redonner l'image
|
|
146
|
+
* intermediaire. On garde juste une reference de plus.
|
|
147
|
+
*/
|
|
148
|
+
shapeAtTime(now) {
|
|
149
|
+
const to = this.shape;
|
|
150
|
+
const from = this.shapePrev;
|
|
151
|
+
if (!to || !from)
|
|
152
|
+
return to;
|
|
153
|
+
const k = (now - this.shapeAt) / BotEngine.SHAPE_MORPH;
|
|
154
|
+
if (k >= 1)
|
|
155
|
+
return to;
|
|
156
|
+
const t = easings.easeOutQuint(clamp(k));
|
|
157
|
+
// alloue seulement pendant le morph ; hors morph on rend le tableau tel quel
|
|
158
|
+
return to.map((r, i) => lerp(from[i] ?? r, r, t));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Nouvelle cible de regard, `null` pour revenir a celui de l'etat.
|
|
162
|
+
*
|
|
163
|
+
* Elle repart de la valeur COURANTE, et non de la cible precedente comme
|
|
164
|
+
* `setShape` : cette methode est appelee a chaque mouvement de pointeur, et
|
|
165
|
+
* repartir de l'ancienne cible ferait reculer le regard d'un cran avant
|
|
166
|
+
* chaque rattrapage — le suivi tremblerait au lieu de glisser.
|
|
167
|
+
*
|
|
168
|
+
* Meme contrat que `setShape` par ailleurs : l'etat externe entre par un
|
|
169
|
+
* setter horodate, jamais par une variable lue pendant `sample`, sinon le
|
|
170
|
+
* moteur cesse d'etre une fonction pure du temps.
|
|
171
|
+
*/
|
|
172
|
+
setLook(look, now, morph = BotEngine.LOOK_MORPH) {
|
|
173
|
+
/*
|
|
174
|
+
* Une cible non finie est refusee. Le moteur GARDE la derniere : un `NaN`
|
|
175
|
+
* pose une seule fois se propagerait a chaque image et le bot ne se
|
|
176
|
+
* reposerait plus jamais. C'est arrive pour de vrai — un
|
|
177
|
+
* `getBoundingClientRect` sur une boite de taille nulle donne `0 / 0` chez
|
|
178
|
+
* l'appelant. Celui-la est corrige, mais le moteur n'a pas a dependre de la
|
|
179
|
+
* prudence de ses appelants pour rester rejouable.
|
|
180
|
+
*/
|
|
181
|
+
if (look && !Number.isFinite(look.yaw + look.pitch + look.mix + look.spin + look.wander)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.lookPrev = this.lookAtTime(now);
|
|
185
|
+
this.look = look ?? NO_LOOK;
|
|
186
|
+
this.lookAt = now;
|
|
187
|
+
this.lookMorph = morph;
|
|
188
|
+
}
|
|
189
|
+
/** Regard effectif a l'instant `now`, rattrapage en cours compris. */
|
|
190
|
+
lookAtTime(now) {
|
|
191
|
+
const k = (now - this.lookAt) / this.lookMorph;
|
|
192
|
+
if (k >= 1)
|
|
193
|
+
return this.look;
|
|
194
|
+
return lerpLook(this.lookPrev, this.look, easings.easeOutQuint(clamp(k)));
|
|
195
|
+
}
|
|
196
|
+
posed(def, t, shape, expr) {
|
|
197
|
+
let pose = def.pose(t);
|
|
198
|
+
if (def.baseBody && shape) {
|
|
199
|
+
// on garde la pose (rotation, decalage, squash) et on n'echange que le profil
|
|
200
|
+
pose = { ...pose, sil: { ...pose.sil, radii: shape } };
|
|
201
|
+
}
|
|
202
|
+
if (def.baseFace && expr) {
|
|
203
|
+
pose = { ...pose, gaze: expr.gaze, split: expr.split, eyes: expr.eyes };
|
|
204
|
+
}
|
|
205
|
+
return pose;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Decalage des yeux a l'instant `now` pour un etat donne, en unites de rayon de boule.
|
|
209
|
+
*
|
|
210
|
+
* Il est LU dans une table et interpole, jamais recalcule : `eyefit.ts` explique
|
|
211
|
+
* pourquoi cette distinction est tout le correctif. Ici il ne reste qu'a l'interpoler
|
|
212
|
+
* sur l'axe de la forme, avec exactement la courbe et la duree du morph de silhouette
|
|
213
|
+
* — c'est la meme cause, donc ce doit etre le meme mouvement.
|
|
214
|
+
*
|
|
215
|
+
* On interroge la table sur les BORNES du morph (`shapePrev` et `shape`) et non sur le
|
|
216
|
+
* profil que rend `shapeAtTime` : celui-la est un tableau neuf alloue a chaque image,
|
|
217
|
+
* donc sans identite, et il n'existe dans aucune table.
|
|
218
|
+
*/
|
|
219
|
+
decalageAtTime(now, state) {
|
|
220
|
+
/**
|
|
221
|
+
* Un axe de morph : on lit la table sur ses deux BORNES et on interpole avec sa
|
|
222
|
+
* courbe. Jamais sur la valeur interpolee — celle-la n'a pas d'identite et n'existe
|
|
223
|
+
* dans aucune table, et c'est en la lui donnant a manger que les versions
|
|
224
|
+
* precedentes tremblaient.
|
|
225
|
+
*/
|
|
226
|
+
const surAxe = (debut, duree, a, b) => {
|
|
227
|
+
if (a === b)
|
|
228
|
+
return b;
|
|
229
|
+
const k = (now - debut) / duree;
|
|
230
|
+
if (k >= 1)
|
|
231
|
+
return b;
|
|
232
|
+
const t = easings.easeOutQuint(clamp(k));
|
|
233
|
+
return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
|
|
234
|
+
};
|
|
235
|
+
// axe de l'expression, pour chacune des deux formes en presence
|
|
236
|
+
const parForme = (radii) => surAxe(this.exprAt, BotEngine.SHAPE_MORPH, decalageDesYeux(radii, state, this.exprPrev?.id ?? null), decalageDesYeux(radii, state, this.expr?.id ?? null));
|
|
237
|
+
// puis axe de la forme
|
|
238
|
+
return surAxe(this.shapeAt, BotEngine.SHAPE_MORPH, parForme(this.shapePrev), parForme(this.shape));
|
|
239
|
+
}
|
|
240
|
+
get state() {
|
|
241
|
+
return this.cur;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Repart sur `id` SANS etat precedent, comme un moteur neuf pose sur cet etat.
|
|
245
|
+
*
|
|
246
|
+
* C'est ce que veut dire « rembobiner » pour ce moteur. `setState` seul ne peut pas le
|
|
247
|
+
* faire : il garde l'etat quitte pour le fondre, ce qui est exactement son role en
|
|
248
|
+
* lecture, et exactement ce qu'il ne faut pas quand on revient au debut d'une sequence.
|
|
249
|
+
* Rejouer l'image 0 apres une passe complete melangeait le premier etat avec le DERNIER,
|
|
250
|
+
* et l'export GIF s'ouvrait sur une boule sans yeux — la comete a un `eyeAlpha` nul.
|
|
251
|
+
*
|
|
252
|
+
* `sample` reste une fonction pure du temps : comme `setState`, ceci est un setter DATE,
|
|
253
|
+
* appele par le pilote de la sequence, jamais pendant un echantillonnage.
|
|
254
|
+
*/
|
|
255
|
+
reset(id, now) {
|
|
256
|
+
this.cur = id;
|
|
257
|
+
this.prev = null;
|
|
258
|
+
this.departFige = null;
|
|
259
|
+
this.tCur = now;
|
|
260
|
+
this.tPrev = now;
|
|
261
|
+
this.blinkAt = -10;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Origine du fondu en cours : la pose figee s'il y en a une, sinon l'etat quitte evalue
|
|
265
|
+
* a son propre temps ecoule — donc encore en train de s'animer, ce qui est voulu.
|
|
266
|
+
*/
|
|
267
|
+
origine(now, shape, expr) {
|
|
268
|
+
if (this.departFige)
|
|
269
|
+
return this.departFige;
|
|
270
|
+
if (!this.prev)
|
|
271
|
+
return null;
|
|
272
|
+
const prevDef = STATE_BY_ID.get(this.prev);
|
|
273
|
+
return this.posed(prevDef, Math.max(0, now - this.tPrev), shape, expr);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pose composite a l'instant `now`, fondu en cours compris : exactement ce que `sample`
|
|
277
|
+
* melange, avant la couche de vie au repos et de regard. Extraite pour que `setState`
|
|
278
|
+
* puisse la figer.
|
|
279
|
+
*/
|
|
280
|
+
poseComposee(now) {
|
|
281
|
+
const def = STATE_BY_ID.get(this.cur);
|
|
282
|
+
const shape = this.shapeAtTime(now);
|
|
283
|
+
const expr = this.exprAtTime(now);
|
|
284
|
+
const pose = this.posed(def, Math.max(0, now - this.tCur), shape, expr);
|
|
285
|
+
const since = now - this.tCur;
|
|
286
|
+
if (since >= def.morph)
|
|
287
|
+
return pose;
|
|
288
|
+
const origine = this.origine(now, shape, expr);
|
|
289
|
+
if (!origine)
|
|
290
|
+
return pose;
|
|
291
|
+
return blendPose(origine, pose, easings.easeOutQuint(clamp(since / def.morph)));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Changement d'etat, date.
|
|
295
|
+
*
|
|
296
|
+
* Le moteur ne garde qu'UNE case d'historique, donc un changement qui arrive pendant un
|
|
297
|
+
* fondu remplacait l'origine du melange par la pose PLEINE de l'etat qu'on quittait, au
|
|
298
|
+
* lieu de l'image partiellement melangee qui etait a l'ecran. Mesure sur
|
|
299
|
+
* `idle -> wide -> idle` a 100 ms : 35,9 px de saut contre 8,0 px de mouvement normal.
|
|
300
|
+
*
|
|
301
|
+
* On fige donc la pose composite courante et on melange depuis elle. Continu par
|
|
302
|
+
* construction, quel que soit le nombre de changements enchaines.
|
|
303
|
+
*
|
|
304
|
+
* Et SEULEMENT dans ce cas. Figer a chaque changement arreterait net l'animation de
|
|
305
|
+
* l'etat qu'on quitte pendant tout le fondu — le « ! » d'`alert` se figerait en pleine
|
|
306
|
+
* course — alors qu'il n'y a rien a corriger hors morph : l'etat quitte y est deja
|
|
307
|
+
* exactement l'image affichee. La lecture d'un montage, dont les blocs durent au moins
|
|
308
|
+
* le plus long fondu (`MIN_BLOCK`), ne fige donc jamais rien et rend au bit ce qu'elle
|
|
309
|
+
* rendait.
|
|
310
|
+
*/
|
|
311
|
+
setState(id, now) {
|
|
312
|
+
if (id === this.cur)
|
|
313
|
+
return;
|
|
314
|
+
const morph = STATE_BY_ID.get(this.cur).morph;
|
|
315
|
+
const enPleinFondu = this.prev !== null && now - this.tCur < morph;
|
|
316
|
+
this.departFige = enPleinFondu ? this.poseComposee(now) : null;
|
|
317
|
+
this.prev = this.cur;
|
|
318
|
+
this.tPrev = this.tCur;
|
|
319
|
+
this.cur = id;
|
|
320
|
+
this.tCur = now;
|
|
321
|
+
// Dans la video, chaque changement de forme est masque par un clignement.
|
|
322
|
+
if (STATE_BY_ID.get(id)?.blinkIn)
|
|
323
|
+
this.blinkAt = now;
|
|
324
|
+
}
|
|
325
|
+
sample(now) {
|
|
326
|
+
const R = this.scale;
|
|
327
|
+
const def = STATE_BY_ID.get(this.cur);
|
|
328
|
+
const shape = this.shapeAtTime(now);
|
|
329
|
+
const expr = this.exprAtTime(now);
|
|
330
|
+
let pose = this.posed(def, Math.max(0, now - this.tCur), shape, expr);
|
|
331
|
+
let decalage = this.decalageAtTime(now, this.cur);
|
|
332
|
+
// --- transition -------------------------------------------------------
|
|
333
|
+
const since = now - this.tCur;
|
|
334
|
+
// L'etat precedent n'est jamais purge : `since < def.morph` suffit a
|
|
335
|
+
// l'ignorer une fois le fondu passe, et l'oublier rendrait le moteur non
|
|
336
|
+
// rejouable — relire une date d'avant la fin du fondu ne le retrouverait
|
|
337
|
+
// plus. C'est l'optimisation qui parait innocente et qui casse tout.
|
|
338
|
+
const origine = since < def.morph ? this.origine(now, shape, expr) : null;
|
|
339
|
+
if (origine) {
|
|
340
|
+
// Ease-out exponentiel : c'est la courbe mesuree sur la video. Le corps
|
|
341
|
+
// n'a pas d'overshoot (seuls la pastille et l'ouverture des yeux en ont).
|
|
342
|
+
// Le ratio est borne : relire une date ANTERIEURE au changement d'etat
|
|
343
|
+
// donnerait un ratio negatif, que l'ease-out extrapole — la silhouette
|
|
344
|
+
// part alors trente fois trop loin.
|
|
345
|
+
const ratio = easings.easeOutQuint(clamp(since / def.morph));
|
|
346
|
+
pose = blendPose(origine, pose, ratio);
|
|
347
|
+
// Le decalage des yeux suit la MEME courbe que la silhouette qui le motive. Il vient
|
|
348
|
+
// de l'etat quitte, que `setState` renseigne toujours en meme temps que l'origine —
|
|
349
|
+
// le test est la pour le typage, pas pour un cas reel.
|
|
350
|
+
const quitte = this.prev;
|
|
351
|
+
if (quitte) {
|
|
352
|
+
const avant = this.decalageAtTime(now, quitte);
|
|
353
|
+
decalage = {
|
|
354
|
+
x: lerp(avant.x, decalage.x, ratio),
|
|
355
|
+
y: lerp(avant.y, decalage.y, ratio)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// --- vie au repos -----------------------------------------------------
|
|
360
|
+
const alive = pose.eyeAlpha > 0.01;
|
|
361
|
+
const look = this.lookAtTime(now);
|
|
362
|
+
const life = liveliness(now, { wander: alive ? look.wander : 0, blink: alive });
|
|
363
|
+
const gaze = {
|
|
364
|
+
// Les deux visees REMPLACENT celles de la pose au lieu de s'y ajouter (voir
|
|
365
|
+
// `Look`), et le tour se retranche en chemin. La derive s'ajoute APRES le
|
|
366
|
+
// melange, sinon la cible l'annulerait en meme temps que la pose — or elle
|
|
367
|
+
// doit survivre a une tete tournee sans pointeur.
|
|
368
|
+
yaw: lerp(pose.gaze.yaw, look.yaw, look.mix) + life.dYaw - look.spin,
|
|
369
|
+
pitch: lerp(pose.gaze.pitch, look.pitch, look.mix) + life.dPitch,
|
|
370
|
+
// le roulis, lui, ne suit rien : la tete du bot est penchee de -13deg dans
|
|
371
|
+
// la video, et la faire rouler avec le curseur casse cette signature
|
|
372
|
+
roll: pose.gaze.roll + life.dRoll
|
|
373
|
+
};
|
|
374
|
+
// clignement declenche par le changement d'etat, en plus du calendrier
|
|
375
|
+
const forced = clamp((now - this.blinkAt) / 0.2);
|
|
376
|
+
const forcedLid = forced < 1 ? Math.abs(forced * 2 - 1) : 1;
|
|
377
|
+
const lid = Math.min(life.lid, forcedLid);
|
|
378
|
+
const offX = pose.offX + life.driftX;
|
|
379
|
+
const offY = pose.offY + life.driftY;
|
|
380
|
+
// --- corps ------------------------------------------------------------
|
|
381
|
+
const sil = {
|
|
382
|
+
...pose.sil,
|
|
383
|
+
cx: pose.sil.cx + offX,
|
|
384
|
+
cy: pose.sil.cy + offY,
|
|
385
|
+
sy: pose.sil.sy * life.breath
|
|
386
|
+
};
|
|
387
|
+
const bodyPath = closedPath(toPoints(sil, R, this.pts));
|
|
388
|
+
// --- yeux -------------------------------------------------------------
|
|
389
|
+
// Les yeux vivent sur une sphere de rayon 1 ; des que la silhouette n'est
|
|
390
|
+
// plus un cercle, on les ramene au prorata du rayon reel dans leur
|
|
391
|
+
// direction, sinon ils debordent et le masque les coupe.
|
|
392
|
+
const bodyRadius = (x, y) => radiusAtAngle(pose.sil.radii, Math.atan2(y, x) - pose.sil.rot);
|
|
393
|
+
const eyes = [];
|
|
394
|
+
if (pose.eyeAlpha > 0.01) {
|
|
395
|
+
const poses = eyePoses(gaze, R, pose.split);
|
|
396
|
+
for (let i = 0; i < 2; i++) {
|
|
397
|
+
const e = poses[i];
|
|
398
|
+
if (e.depth <= 0.02)
|
|
399
|
+
continue;
|
|
400
|
+
const cfg = pose.eyes[i];
|
|
401
|
+
const fit = bodyRadius(e.x, e.y);
|
|
402
|
+
// Inclinaison propre de l'oeil : on compose le repere tangent avec une
|
|
403
|
+
// rotation dans le plan de l'oeil (Basis x Rot). C'est ce qui permet des
|
|
404
|
+
// inclinaisons en miroir entre les deux yeux.
|
|
405
|
+
const phi = ((cfg.tilt ?? 0) * Math.PI) / 180;
|
|
406
|
+
const cp = Math.cos(phi);
|
|
407
|
+
const sp = Math.sin(phi);
|
|
408
|
+
const ax = e.a * cp + e.c * sp;
|
|
409
|
+
const ay = e.b * cp + e.d * sp;
|
|
410
|
+
const cx2 = -e.a * sp + e.c * cp;
|
|
411
|
+
const cy2 = -e.b * sp + e.d * cp;
|
|
412
|
+
// Le clignement s'applique APRES tout ca : c'est un ecrasement vertical
|
|
413
|
+
// a l'ecran, pas le long de l'axe de la gelule.
|
|
414
|
+
const k = blinkScale(Math.min(lid, cfg.open));
|
|
415
|
+
eyes.push({
|
|
416
|
+
width: cfg.w * R,
|
|
417
|
+
height: cfg.h * R,
|
|
418
|
+
openMatrix: `matrix(${r2(ax)},${r2(ay)},${r2(cx2)},${r2(cy2)},${r2(e.x * fit + (offX + decalage.x) * R)},${r2(e.y * fit + (offY + decalage.y) * R)})`,
|
|
419
|
+
lid: Math.min(lid, cfg.open),
|
|
420
|
+
d: capsulePath(cfg.w * R, cfg.h * R),
|
|
421
|
+
matrix: `matrix(${r2(ax)},${r2(ay * k)},${r2(cx2)},${r2(cy2 * k)},${r2(e.x * fit + (offX + decalage.x) * R)},${r2(e.y * fit + (offY + decalage.y) * R)})`,
|
|
422
|
+
alpha: pose.eyeAlpha * clamp(e.depth / 0.12)
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// --- decor ------------------------------------------------------------
|
|
427
|
+
const dots = pose.dots
|
|
428
|
+
.filter((p) => p.opacity > 0.01 && p.r > 0.0005)
|
|
429
|
+
.map((p) => ({ ...p, x: (p.x + offX) * R, y: (p.y + offY) * R, r: p.r * R }));
|
|
430
|
+
// la pastille est posee sur le contour : elle suit donc la forme aussi
|
|
431
|
+
const nFit = pose.notif ? bodyRadius(pose.notif.x, pose.notif.y) : 1;
|
|
432
|
+
const nx = pose.notif ? (pose.notif.x * nFit + offX) * R : 0;
|
|
433
|
+
const ny = pose.notif ? (pose.notif.y * nFit + offY) * R : 0;
|
|
434
|
+
const notif = pose.notif ? { x: nx, y: ny, r: pose.notif.r * R } : null;
|
|
435
|
+
const notch = pose.notif ? { x: nx, y: ny, r: pose.notif.notch * R } : null;
|
|
436
|
+
return {
|
|
437
|
+
bodyPath,
|
|
438
|
+
bodyAlpha: pose.bodyAlpha,
|
|
439
|
+
eyes,
|
|
440
|
+
dots,
|
|
441
|
+
dotsBehind: pose.dotsBehind,
|
|
442
|
+
// Les etats declarent des arcs en unites de rayon de boule ; le moteur
|
|
443
|
+
// est le seul a connaitre l'echelle du viewBox, donc c'est lui qui trace.
|
|
444
|
+
arcs: pose.arcs
|
|
445
|
+
.filter((a) => a.opacity > 0.01)
|
|
446
|
+
.map((a) => arcRender(a.seed, a.t, R, a.id, a.opacity)),
|
|
447
|
+
notif,
|
|
448
|
+
notch
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type HeadGaze } from './face.js';
|
|
2
|
+
import type { EyeCfg } from './states.js';
|
|
3
|
+
/**
|
|
4
|
+
* Expression de repos du bot.
|
|
5
|
+
*
|
|
6
|
+
* Le visage ne tient qu'à deux gélules, donc tout se joue sur quatre leviers :
|
|
7
|
+
* l'orientation de la tête, l'écart des yeux, leurs proportions, et
|
|
8
|
+
* l'inclinaison propre de chaque œil. C'est ce dernier qui permet la colère et
|
|
9
|
+
* la tristesse : elles demandent des inclinaisons EN MIROIR (les hauts qui
|
|
10
|
+
* convergent ou divergent), impossible avec le seul roulis de tête qui incline
|
|
11
|
+
* les deux yeux du même côté.
|
|
12
|
+
*
|
|
13
|
+
* Seul l'état de repos porte cette expression. Les états expressifs de la vidéo
|
|
14
|
+
* (clin d'œil, yeux écarquillés, notification) gardent la leur : c'est elle
|
|
15
|
+
* qu'on est venu reproduire.
|
|
16
|
+
*
|
|
17
|
+
* Les amplitudes s'appuient sur bible-strong-avatar-lab, qui expose le même
|
|
18
|
+
* modèle (tête X/Y/Z, largeur et hauteur par œil, écart, angle par œil) : chez
|
|
19
|
+
* eux la largeur va de 0,8 à 2,7 fois le neutre, la hauteur de 0,3 à 1,5, et
|
|
20
|
+
* les angles jusqu'à ±80°. On reste dans cette enveloppe.
|
|
21
|
+
*/
|
|
22
|
+
/** Enumeres pour que la couche i18n verifie leurs traductions a la compilation. */
|
|
23
|
+
export type ExpressionId = 'neutre' | 'attentif' | 'surpris' | 'excite' | 'heureux' | 'hilare' | 'colere' | 'triste' | 'effraye' | 'mefiant' | 'confus' | 'curieux' | 'fier' | 'timide' | 'blase' | 'somnolent';
|
|
24
|
+
export interface BotExpression {
|
|
25
|
+
id: ExpressionId;
|
|
26
|
+
gaze: HeadGaze;
|
|
27
|
+
split: number;
|
|
28
|
+
eyes: [EyeCfg, EyeCfg];
|
|
29
|
+
}
|
|
30
|
+
export declare const EXPRESSIONS: BotExpression[];
|
|
31
|
+
export declare const EXPRESSION_BY_ID: Map<string, BotExpression>;
|
|
32
|
+
export declare const DEFAULT_EXPRESSION = "neutre";
|
|
33
|
+
/** Interpolation de deux expressions : le changement se fait en glissant. */
|
|
34
|
+
export declare function blendExpression(a: BotExpression, b: BotExpression, t: number): BotExpression;
|
|
35
|
+
//# sourceMappingURL=expressions.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Adapted from bloub, Copyright (c) 2026 Jérémy Perret (MIT).
|
|
2
|
+
// See THIRD_PARTY_NOTICES.md for source revision and local changes.
|
|
3
|
+
import { EYE_H, EYE_SPLIT, EYE_W, REST_GAZE } from './face.js';
|
|
4
|
+
import { lerp } from './math.js';
|
|
5
|
+
/** `tilt` en degrés, positif = le haut de la gélule part vers la droite. */
|
|
6
|
+
const eye = (w, h, tilt = 0, open = 1) => ({ w, h, tilt, open });
|
|
7
|
+
/** Les deux yeux identiques, inclinaisons en miroir si `tilt` est fourni. */
|
|
8
|
+
const pair = (w, h, tilt = 0, open = 1) => [
|
|
9
|
+
eye(w, h, tilt, open),
|
|
10
|
+
eye(w, h, -tilt, open)
|
|
11
|
+
];
|
|
12
|
+
export const EXPRESSIONS = [
|
|
13
|
+
{
|
|
14
|
+
// la pose relevée image par image sur la vidéo de référence
|
|
15
|
+
id: 'neutre',
|
|
16
|
+
gaze: { ...REST_GAZE },
|
|
17
|
+
split: EYE_SPLIT,
|
|
18
|
+
eyes: [eye(EYE_W, EYE_H), eye(EYE_W, EYE_H)]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'attentif',
|
|
22
|
+
gaze: { yaw: 4, pitch: 5, roll: -4 },
|
|
23
|
+
split: 16,
|
|
24
|
+
eyes: pair(0.21, 0.44)
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'surpris',
|
|
28
|
+
gaze: { yaw: 3, pitch: -3, roll: 0 },
|
|
29
|
+
split: 19,
|
|
30
|
+
eyes: pair(0.45, 0.47)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'excite',
|
|
34
|
+
gaze: { yaw: 6, pitch: -14, roll: 0 },
|
|
35
|
+
split: 19.5,
|
|
36
|
+
eyes: pair(0.4, 0.56, -10)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// yeux plissés en arc : les hauts convergent légèrement
|
|
40
|
+
id: 'heureux',
|
|
41
|
+
gaze: { yaw: 5, pitch: 9, roll: 0 },
|
|
42
|
+
split: 17,
|
|
43
|
+
eyes: pair(0.27, 0.17, 14)
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'hilare',
|
|
47
|
+
gaze: { yaw: 4, pitch: 14, roll: 0 },
|
|
48
|
+
split: 18,
|
|
49
|
+
eyes: pair(0.34, 0.13, 20)
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
// hauts des yeux qui convergent fort vers le centre + yeux étrécis
|
|
53
|
+
id: 'colere',
|
|
54
|
+
gaze: { yaw: 3, pitch: 7, roll: 0 },
|
|
55
|
+
split: 17,
|
|
56
|
+
eyes: pair(0.34, 0.15, 30)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
// l'inverse : les hauts divergent, et le regard tombe
|
|
60
|
+
id: 'triste',
|
|
61
|
+
gaze: { yaw: 3, pitch: -13, roll: 0 },
|
|
62
|
+
split: 16,
|
|
63
|
+
eyes: pair(0.22, 0.4, -28)
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'effraye',
|
|
67
|
+
gaze: { yaw: 2, pitch: -20, roll: 0 },
|
|
68
|
+
split: 20.5,
|
|
69
|
+
eyes: pair(0.4, 0.6)
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
// un œil franchement plus fermé que l'autre
|
|
73
|
+
id: 'mefiant',
|
|
74
|
+
gaze: { yaw: 12, pitch: 6, roll: -6 },
|
|
75
|
+
split: 16,
|
|
76
|
+
eyes: [eye(0.21, 0.4), eye(0.22, 0.15)]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
// asymétrique sur les deux axes : tailles ET inclinaisons dépareillées.
|
|
80
|
+
// L'œil plissé est volontairement plat (rapport 1,6) : à un rapport proche
|
|
81
|
+
// de 1 il serait rond, et son inclinaison ne se verrait pas.
|
|
82
|
+
id: 'confus',
|
|
83
|
+
gaze: { yaw: -14, pitch: 3, roll: 8 },
|
|
84
|
+
split: 16.5,
|
|
85
|
+
eyes: [eye(0.2, 0.44, -18), eye(0.28, 0.17, 14)]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// la tête penche : c'est le roulis qui porte la curiosité
|
|
89
|
+
id: 'curieux',
|
|
90
|
+
gaze: { yaw: 16, pitch: -9, roll: -15 },
|
|
91
|
+
split: 16.5,
|
|
92
|
+
eyes: [eye(0.24, 0.46, -8), eye(0.2, 0.38, -8)]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'fier',
|
|
96
|
+
gaze: { yaw: 5, pitch: 17, roll: 0 },
|
|
97
|
+
split: 17,
|
|
98
|
+
eyes: pair(0.3, 0.15, 18)
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'timide',
|
|
102
|
+
gaze: { yaw: -19, pitch: -14, roll: -7 },
|
|
103
|
+
split: 14,
|
|
104
|
+
eyes: pair(0.17, 0.3)
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// fentes horizontales et regard qui part sur le côté
|
|
108
|
+
id: 'blase',
|
|
109
|
+
gaze: { yaw: -22, pitch: 2, roll: 0 },
|
|
110
|
+
split: 16,
|
|
111
|
+
eyes: pair(0.3, 0.12)
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
// paupières à moitié tombées : on passe par `open`, donc l'écrasement
|
|
115
|
+
// vertical à l'écran, le même mécanisme que le clignement
|
|
116
|
+
id: 'somnolent',
|
|
117
|
+
gaze: { yaw: 6, pitch: -9, roll: -3 },
|
|
118
|
+
split: 16,
|
|
119
|
+
eyes: pair(0.2, 0.42, 0, 0.42)
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
export const EXPRESSION_BY_ID = new Map(EXPRESSIONS.map((e) => [e.id, e]));
|
|
123
|
+
export const DEFAULT_EXPRESSION = 'neutre';
|
|
124
|
+
const lerpEyeCfg = (a, b, t) => ({
|
|
125
|
+
w: lerp(a.w, b.w, t),
|
|
126
|
+
h: lerp(a.h, b.h, t),
|
|
127
|
+
tilt: lerp(a.tilt ?? 0, b.tilt ?? 0, t),
|
|
128
|
+
open: lerp(a.open, b.open, t)
|
|
129
|
+
});
|
|
130
|
+
/** Interpolation de deux expressions : le changement se fait en glissant. */
|
|
131
|
+
export function blendExpression(a, b, t) {
|
|
132
|
+
return {
|
|
133
|
+
id: b.id,
|
|
134
|
+
gaze: {
|
|
135
|
+
yaw: lerp(a.gaze.yaw, b.gaze.yaw, t),
|
|
136
|
+
pitch: lerp(a.gaze.pitch, b.gaze.pitch, t),
|
|
137
|
+
roll: lerp(a.gaze.roll, b.gaze.roll, t)
|
|
138
|
+
},
|
|
139
|
+
split: lerp(a.split, b.split, t),
|
|
140
|
+
eyes: [lerpEyeCfg(a.eyes[0], b.eyes[0], t), lerpEyeCfg(a.eyes[1], b.eyes[1], t)]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=expressions.js.map
|