@lemoncat7/dsh-theme-xiaohei 0.3.0-alpha.22 → 0.3.1

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 (210) hide show
  1. package/README.md +37 -17
  2. package/THIRD_PARTY_NOTICES.md +41 -0
  3. package/lib/appearance.js +26 -10
  4. package/lib/blink.js +3 -3
  5. package/lib/bloub-heixiu/ambient.d.ts +3 -0
  6. package/lib/bloub-heixiu/ambient.js +12 -0
  7. package/lib/bloub-heixiu/character.d.ts +33 -0
  8. package/lib/bloub-heixiu/character.js +124 -0
  9. package/lib/bloub-heixiu/effects.d.ts +4 -0
  10. package/lib/bloub-heixiu/effects.js +91 -0
  11. package/lib/bloub-heixiu/flight.d.ts +15 -0
  12. package/lib/bloub-heixiu/flight.js +66 -0
  13. package/lib/bloub-heixiu/follow.d.ts +13 -0
  14. package/lib/bloub-heixiu/follow.js +23 -0
  15. package/lib/bloub-heixiu/layout.d.ts +18 -0
  16. package/lib/bloub-heixiu/layout.js +12 -0
  17. package/lib/bloub-heixiu/renderer.d.ts +16 -0
  18. package/lib/bloub-heixiu/renderer.js +94 -0
  19. package/lib/bloub-heixiu/runtime.d.ts +6 -0
  20. package/lib/bloub-heixiu/runtime.js +395 -0
  21. package/lib/bloub-heixiu/signals.d.ts +17 -0
  22. package/lib/bloub-heixiu/signals.js +53 -0
  23. package/lib/bloub-heixiu/styles.d.ts +2 -0
  24. package/lib/bloub-heixiu/styles.js +98 -0
  25. package/lib/chrome/accessibility.d.ts +1 -1
  26. package/lib/chrome/accessibility.js +5 -13
  27. package/lib/chrome/composer-avatar.d.ts +10 -0
  28. package/lib/chrome/composer-avatar.js +599 -0
  29. package/lib/chrome/composer-send-heixiu.d.ts +3 -0
  30. package/lib/chrome/composer-send-heixiu.js +143 -0
  31. package/lib/chrome/composer.d.ts +2 -2
  32. package/lib/chrome/composer.js +84 -95
  33. package/lib/chrome/controls.d.ts +8 -0
  34. package/lib/chrome/controls.js +348 -0
  35. package/lib/chrome/conversation-messages.d.ts +1 -1
  36. package/lib/chrome/conversation-messages.js +207 -9
  37. package/lib/chrome/conversation.d.ts +1 -1
  38. package/lib/chrome/conversation.js +6 -0
  39. package/lib/chrome/frames.d.ts +1 -1
  40. package/lib/chrome/frames.js +14 -0
  41. package/lib/chrome/identity.js +4 -18
  42. package/lib/chrome/portal.d.ts +1 -1
  43. package/lib/chrome/portal.js +1 -1
  44. package/lib/chrome/sidebar.d.ts +1 -1
  45. package/lib/chrome/sidebar.js +118 -153
  46. package/lib/chrome/tokens.d.ts +1 -1
  47. package/lib/chrome/tokens.js +121 -68
  48. package/lib/chrome/workspace.d.ts +2 -2
  49. package/lib/chrome/workspace.js +35 -254
  50. package/lib/chrome.js +4 -0
  51. package/lib/client.d.ts +5 -12
  52. package/lib/client.js +5864 -4447
  53. package/lib/composer-avatar.d.ts +18 -0
  54. package/lib/composer-avatar.js +441 -0
  55. package/lib/composer-send-heixiu.d.ts +8 -0
  56. package/lib/composer-send-heixiu.js +133 -0
  57. package/lib/gaze.js +3 -3
  58. package/lib/generated-backgrounds.d.ts +14 -0
  59. package/lib/generated-backgrounds.js +14 -0
  60. package/lib/generated-identity.d.ts +1 -0
  61. package/lib/generated-identity.js +1 -0
  62. package/lib/generated-keyart.d.ts +18 -3
  63. package/lib/generated-keyart.js +18 -3
  64. package/lib/host-contract.d.ts +9 -0
  65. package/lib/host-contract.js +13 -0
  66. package/lib/host-dom.d.ts +11 -0
  67. package/lib/host-dom.js +60 -0
  68. package/lib/loading-heixiu.js +3 -3
  69. package/lib/metallic-brand-mark.d.ts +10 -0
  70. package/lib/metallic-brand-mark.js +309 -0
  71. package/lib/plugin.d.ts +1 -1
  72. package/lib/plugin.js +31 -16
  73. package/lib/portal.js +11 -10
  74. package/lib/reactions.js +3 -3
  75. package/lib/scene/avatar-model.d.ts +6 -0
  76. package/lib/scene/avatar-model.js +646 -0
  77. package/lib/scene/avatar-motion.d.ts +3 -0
  78. package/lib/scene/avatar-motion.js +799 -0
  79. package/lib/scene/character-idle.d.ts +17 -0
  80. package/lib/scene/character-idle.js +65 -0
  81. package/lib/scene/character-layout.d.ts +24 -0
  82. package/lib/scene/character-layout.js +51 -0
  83. package/lib/scene/character-poses.d.ts +26 -0
  84. package/lib/scene/character-poses.js +11 -0
  85. package/lib/scene/heixiu-feedback.d.ts +1 -1
  86. package/lib/scene/heixiu-feedback.js +0 -17
  87. package/lib/scene/pointer-bridge.d.ts +21 -0
  88. package/lib/scene/pointer-bridge.js +155 -0
  89. package/lib/scene/runtime.d.ts +12 -0
  90. package/lib/scene/runtime.js +87 -0
  91. package/lib/scene/styles.d.ts +7 -0
  92. package/lib/scene/styles.js +75 -0
  93. package/lib/scene/sylva-performance.d.ts +11 -0
  94. package/lib/scene/sylva-performance.js +58 -0
  95. package/lib/scene/wallpaper-character-styles.d.ts +3 -0
  96. package/lib/scene/wallpaper-character-styles.js +43 -0
  97. package/lib/scene/wallpaper-character.d.ts +4 -0
  98. package/lib/scene/wallpaper-character.js +195 -0
  99. package/lib/scene.d.ts +7 -13
  100. package/lib/scene.js +7 -1023
  101. package/lib/sidebar-glass.js +88 -35
  102. package/lib/sidebar-heixiu-roaming.js +5 -5
  103. package/lib/state.d.ts +14 -2
  104. package/lib/state.js +12 -4
  105. package/lib/theme.js +2 -2
  106. package/lib/vendor/bloub/decor.d.ts +114 -0
  107. package/lib/vendor/bloub/decor.js +198 -0
  108. package/lib/vendor/bloub/engine.d.ts +213 -0
  109. package/lib/vendor/bloub/engine.js +452 -0
  110. package/lib/vendor/bloub/expressions.d.ts +35 -0
  111. package/lib/vendor/bloub/expressions.js +143 -0
  112. package/lib/vendor/bloub/eyefit.d.ts +75 -0
  113. package/lib/vendor/bloub/eyefit.js +393 -0
  114. package/lib/vendor/bloub/face.d.ts +63 -0
  115. package/lib/vendor/bloub/face.js +106 -0
  116. package/lib/vendor/bloub/math.d.ts +22 -0
  117. package/lib/vendor/bloub/math.js +36 -0
  118. package/lib/vendor/bloub/profiles.d.ts +8 -0
  119. package/lib/vendor/bloub/profiles.js +20 -0
  120. package/lib/vendor/bloub/repere.d.ts +30 -0
  121. package/lib/vendor/bloub/repere.js +32 -0
  122. package/lib/vendor/bloub/shape.d.ts +80 -0
  123. package/lib/vendor/bloub/shape.js +268 -0
  124. package/lib/vendor/bloub/skins.d.ts +36 -0
  125. package/lib/vendor/bloub/skins.js +74 -0
  126. package/lib/vendor/bloub/states.d.ts +87 -0
  127. package/lib/vendor/bloub/states.js +468 -0
  128. package/package.json +23 -6
  129. package/lib/appearance.d.ts.map +0 -1
  130. package/lib/appearance.js.map +0 -1
  131. package/lib/blink.d.ts.map +0 -1
  132. package/lib/blink.js.map +0 -1
  133. package/lib/boot-appearance.d.ts.map +0 -1
  134. package/lib/boot-appearance.js.map +0 -1
  135. package/lib/boot-loader.d.ts.map +0 -1
  136. package/lib/boot-loader.js.map +0 -1
  137. package/lib/chrome/accessibility.d.ts.map +0 -1
  138. package/lib/chrome/accessibility.js.map +0 -1
  139. package/lib/chrome/blink.d.ts.map +0 -1
  140. package/lib/chrome/blink.js.map +0 -1
  141. package/lib/chrome/composer.d.ts.map +0 -1
  142. package/lib/chrome/composer.js.map +0 -1
  143. package/lib/chrome/conversation-messages.d.ts.map +0 -1
  144. package/lib/chrome/conversation-messages.js.map +0 -1
  145. package/lib/chrome/conversation.d.ts.map +0 -1
  146. package/lib/chrome/conversation.js.map +0 -1
  147. package/lib/chrome/frames.d.ts.map +0 -1
  148. package/lib/chrome/frames.js.map +0 -1
  149. package/lib/chrome/gaze.d.ts.map +0 -1
  150. package/lib/chrome/gaze.js.map +0 -1
  151. package/lib/chrome/identity.d.ts.map +0 -1
  152. package/lib/chrome/identity.js.map +0 -1
  153. package/lib/chrome/overlays.d.ts.map +0 -1
  154. package/lib/chrome/overlays.js.map +0 -1
  155. package/lib/chrome/portal.d.ts.map +0 -1
  156. package/lib/chrome/portal.js.map +0 -1
  157. package/lib/chrome/reactions.d.ts.map +0 -1
  158. package/lib/chrome/reactions.js.map +0 -1
  159. package/lib/chrome/sidebar.d.ts.map +0 -1
  160. package/lib/chrome/sidebar.js.map +0 -1
  161. package/lib/chrome/tokens.d.ts.map +0 -1
  162. package/lib/chrome/tokens.js.map +0 -1
  163. package/lib/chrome/workspace.d.ts.map +0 -1
  164. package/lib/chrome/workspace.js.map +0 -1
  165. package/lib/chrome.d.ts.map +0 -1
  166. package/lib/chrome.js.map +0 -1
  167. package/lib/client.d.ts.map +0 -1
  168. package/lib/client.js.map +0 -7
  169. package/lib/gaze.d.ts.map +0 -1
  170. package/lib/gaze.js.map +0 -1
  171. package/lib/generated-identity.d.ts.map +0 -1
  172. package/lib/generated-identity.js.map +0 -1
  173. package/lib/generated-keyart.d.ts.map +0 -1
  174. package/lib/generated-keyart.js.map +0 -1
  175. package/lib/generated-loading-assets.d.ts.map +0 -1
  176. package/lib/generated-loading-assets.js.map +0 -1
  177. package/lib/generated-sidebar-assets.d.ts +0 -4
  178. package/lib/generated-sidebar-assets.d.ts.map +0 -1
  179. package/lib/generated-sidebar-assets.js +0 -4
  180. package/lib/generated-sidebar-assets.js.map +0 -1
  181. package/lib/heixiu-interactions.d.ts.map +0 -1
  182. package/lib/heixiu-interactions.js.map +0 -1
  183. package/lib/index.d.ts.map +0 -1
  184. package/lib/index.js.map +0 -1
  185. package/lib/loading-heixiu.d.ts.map +0 -1
  186. package/lib/loading-heixiu.js.map +0 -1
  187. package/lib/plugin.d.ts.map +0 -1
  188. package/lib/plugin.js.map +0 -1
  189. package/lib/portal.d.ts.map +0 -1
  190. package/lib/portal.js.map +0 -1
  191. package/lib/reactions.d.ts.map +0 -1
  192. package/lib/reactions.js.map +0 -1
  193. package/lib/scene/heixiu-feedback.d.ts.map +0 -1
  194. package/lib/scene/heixiu-feedback.js.map +0 -1
  195. package/lib/scene/heixiu-interactions.d.ts.map +0 -1
  196. package/lib/scene/heixiu-interactions.js.map +0 -1
  197. package/lib/scene/state-transitions.d.ts.map +0 -1
  198. package/lib/scene/state-transitions.js.map +0 -1
  199. package/lib/scene.d.ts.map +0 -1
  200. package/lib/scene.js.map +0 -1
  201. package/lib/sidebar-glass.d.ts.map +0 -1
  202. package/lib/sidebar-glass.js.map +0 -1
  203. package/lib/sidebar-heixiu-escape.d.ts.map +0 -1
  204. package/lib/sidebar-heixiu-escape.js.map +0 -1
  205. package/lib/sidebar-heixiu-roaming.d.ts.map +0 -1
  206. package/lib/sidebar-heixiu-roaming.js.map +0 -1
  207. package/lib/state.d.ts.map +0 -1
  208. package/lib/state.js.map +0 -1
  209. package/lib/theme.d.ts.map +0 -1
  210. package/lib/theme.js.map +0 -1
@@ -0,0 +1,213 @@
1
+ import { type ArcRender, type DotRender } from './decor.js';
2
+ import { type BotExpression } from './expressions.js';
3
+ import { type StateId } from './states.js';
4
+ export interface RenderedEye {
5
+ d: string;
6
+ matrix: string;
7
+ alpha: number;
8
+ /** Original local bounds, exposed for the theme's hollow ellipse renderer. */
9
+ width: number;
10
+ height: number;
11
+ /** Uncompressed tangent and lid allow a stroked eye to close without vanishing. */
12
+ openMatrix: string;
13
+ lid: number;
14
+ }
15
+ export interface BotFrame {
16
+ bodyPath: string;
17
+ bodyAlpha: number;
18
+ eyes: RenderedEye[];
19
+ dots: DotRender[];
20
+ /** true = les points passent derriere le corps (particules de l'eclatement) */
21
+ dotsBehind: boolean;
22
+ arcs: ArcRender[];
23
+ notif: {
24
+ x: number;
25
+ y: number;
26
+ r: number;
27
+ } | null;
28
+ notch: {
29
+ x: number;
30
+ y: number;
31
+ r: number;
32
+ } | null;
33
+ }
34
+ /**
35
+ * Ou le bot porte son regard quand quelque chose d'exterieur le pilote — le
36
+ * pointeur de la souris, aujourd'hui.
37
+ *
38
+ * `yaw` et `pitch` sont des directions ABSOLUES, qui remplacent celles de la pose
39
+ * a mesure que `mix` monte. Deux raisons, chacune un piege deja tombe :
40
+ *
41
+ * - c'est le MOTEUR qui doit faire ce melange, pas l'appelant, parce que lui seul
42
+ * connait la pose A CET INSTANT. Un appelant qui compenserait l'orientation de
43
+ * l'expression lirait sa valeur d'arrivee pendant que le morph est encore en
44
+ * cours, et les yeux sautaient a chaque changement d'humeur ;
45
+ * - et il faut que ce soit absolu sur les DEUX axes. En relatif, la hauteur des
46
+ * yeux suivait celle de chaque expression — « neutre » regarde a +28,6deg quand
47
+ * les autres sont entre -9 et +9 — donc les yeux tombaient d'un coup au premier
48
+ * changement d'humeur. Ce qui fait le caractere d'une expression pendant le
49
+ * suivi, c'est la FORME de ses yeux (plisses, ronds, dissymetriques), pas
50
+ * l'endroit ou elle regarde : celui-la, c'est le curseur qui le decide.
51
+ *
52
+ * `mix` dit a quel point l'exterieur commande la DIRECTION (0 = pas du tout).
53
+ *
54
+ * `wander` dit, separement, ce qui reste de derive automatique. Les deux ne se
55
+ * confondent pas : quand le pointeur bouge, la derive doit s'eteindre — cumulees,
56
+ * le bot aurait l'air de chercher le curseur sans jamais le tenir. Mais quand il
57
+ * n'y a PAS de pointeur (arrivee au clavier, au tactile, ou souris sortie de la
58
+ * fenetre), la tete doit rester tournee ET continuer de vivre. Les confondre
59
+ * figeait le regard des que la vue s'ouvrait.
60
+ *
61
+ * `spin` est un tour a parcourir EN CHEMIN, en degres, qu'on fait fondre vers 0
62
+ * avec l'arrivee. Comme les yeux vivent sur une sphere, un tour les fait passer
63
+ * derriere la boule et revenir de l'autre cote — et `-360deg` etant le meme
64
+ * angle que `0`, il ne change rien a l'endroit ou ils se posent.
65
+ */
66
+ export interface Look {
67
+ yaw: number;
68
+ pitch: number;
69
+ mix: number;
70
+ spin: number;
71
+ wander: number;
72
+ }
73
+ /**
74
+ * Moteur sans horloge : `sample(t)` est une fonction pure du temps.
75
+ *
76
+ * Consequence pratique : pause, reprise, ralenti et saut a une date arbitraire
77
+ * donnent exactement la meme image, et le rendu est testable sans DOM.
78
+ */
79
+ export declare class BotEngine {
80
+ /** rayon de la boule au repos, en unites de viewBox */
81
+ readonly scale: number;
82
+ private cur;
83
+ private prev;
84
+ /**
85
+ * Pose de depart FIGEE, posee seulement quand un changement d'etat arrive alors qu'un
86
+ * fondu est deja en cours. Cf. `setState`.
87
+ */
88
+ private departFige;
89
+ private tCur;
90
+ private tPrev;
91
+ private blinkAt;
92
+ private pts;
93
+ private shape;
94
+ private shapePrev;
95
+ private shapeAt;
96
+ private expr;
97
+ private exprPrev;
98
+ private exprAt;
99
+ private look;
100
+ private lookPrev;
101
+ private lookAt;
102
+ /** duree de rattrapage en cours ; voir `LOOK_MORPH`, sa valeur par defaut */
103
+ private lookMorph;
104
+ /** duree du morph quand on change la forme du corps */
105
+ static readonly SHAPE_MORPH = 0.45;
106
+ /**
107
+ * Duree de rattrapage du regard vers la cible. Plus court que `SHAPE_MORPH` :
108
+ * un regard qui suit doit paraitre attentif, pas visqueux. Comme la cible est
109
+ * reposee a chaque mouvement de souris, c'est cette duree qui donne au suivi
110
+ * son inertie — le regard n'atteint jamais tout a fait un curseur qui bouge.
111
+ */
112
+ static readonly LOOK_MORPH = 0.24;
113
+ constructor(scale?: number, initial?: StateId, shape?: number[] | null, expression?: BotExpression | null);
114
+ /**
115
+ * Expression de repos choisie dans le personnalisateur. Comme la forme, elle
116
+ * glisse vers la nouvelle valeur au lieu de sauter.
117
+ */
118
+ setExpression(expression: BotExpression | null, now?: number): void;
119
+ /** Expression effective a l'instant `now`, morph en cours compris. */
120
+ private exprAtTime;
121
+ /**
122
+ * Forme choisie dans le personnalisateur. Elle ne remplace le corps que sur
123
+ * les etats au repos (`baseBody`) : sur les autres, la silhouette EST
124
+ * l'animation et ne doit pas etre ecrasee.
125
+ *
126
+ * Le changement se fait en morph, pas d'un coup : comme toutes les formes sont
127
+ * echantillonnees aux memes angles, il suffit d'interpoler les rayons.
128
+ */
129
+ setShape(radii: number[] | null, now?: number): void;
130
+ /**
131
+ * Forme effective a l'instant `now`, morph en cours compris.
132
+ *
133
+ * Ne remet PAS `shapePrev` a null en fin de morph : `sample` doit rester une
134
+ * fonction pure du temps, donc relire une date passee doit redonner l'image
135
+ * intermediaire. On garde juste une reference de plus.
136
+ */
137
+ private shapeAtTime;
138
+ /**
139
+ * Nouvelle cible de regard, `null` pour revenir a celui de l'etat.
140
+ *
141
+ * Elle repart de la valeur COURANTE, et non de la cible precedente comme
142
+ * `setShape` : cette methode est appelee a chaque mouvement de pointeur, et
143
+ * repartir de l'ancienne cible ferait reculer le regard d'un cran avant
144
+ * chaque rattrapage — le suivi tremblerait au lieu de glisser.
145
+ *
146
+ * Meme contrat que `setShape` par ailleurs : l'etat externe entre par un
147
+ * setter horodate, jamais par une variable lue pendant `sample`, sinon le
148
+ * moteur cesse d'etre une fonction pure du temps.
149
+ */
150
+ setLook(look: Look | null, now: number, morph?: number): void;
151
+ /** Regard effectif a l'instant `now`, rattrapage en cours compris. */
152
+ private lookAtTime;
153
+ private posed;
154
+ /**
155
+ * Decalage des yeux a l'instant `now` pour un etat donne, en unites de rayon de boule.
156
+ *
157
+ * Il est LU dans une table et interpole, jamais recalcule : `eyefit.ts` explique
158
+ * pourquoi cette distinction est tout le correctif. Ici il ne reste qu'a l'interpoler
159
+ * sur l'axe de la forme, avec exactement la courbe et la duree du morph de silhouette
160
+ * — c'est la meme cause, donc ce doit etre le meme mouvement.
161
+ *
162
+ * On interroge la table sur les BORNES du morph (`shapePrev` et `shape`) et non sur le
163
+ * profil que rend `shapeAtTime` : celui-la est un tableau neuf alloue a chaque image,
164
+ * donc sans identite, et il n'existe dans aucune table.
165
+ */
166
+ private decalageAtTime;
167
+ get state(): StateId;
168
+ /**
169
+ * Repart sur `id` SANS etat precedent, comme un moteur neuf pose sur cet etat.
170
+ *
171
+ * C'est ce que veut dire « rembobiner » pour ce moteur. `setState` seul ne peut pas le
172
+ * faire : il garde l'etat quitte pour le fondre, ce qui est exactement son role en
173
+ * lecture, et exactement ce qu'il ne faut pas quand on revient au debut d'une sequence.
174
+ * Rejouer l'image 0 apres une passe complete melangeait le premier etat avec le DERNIER,
175
+ * et l'export GIF s'ouvrait sur une boule sans yeux — la comete a un `eyeAlpha` nul.
176
+ *
177
+ * `sample` reste une fonction pure du temps : comme `setState`, ceci est un setter DATE,
178
+ * appele par le pilote de la sequence, jamais pendant un echantillonnage.
179
+ */
180
+ reset(id: StateId, now: number): void;
181
+ /**
182
+ * Origine du fondu en cours : la pose figee s'il y en a une, sinon l'etat quitte evalue
183
+ * a son propre temps ecoule — donc encore en train de s'animer, ce qui est voulu.
184
+ */
185
+ private origine;
186
+ /**
187
+ * Pose composite a l'instant `now`, fondu en cours compris : exactement ce que `sample`
188
+ * melange, avant la couche de vie au repos et de regard. Extraite pour que `setState`
189
+ * puisse la figer.
190
+ */
191
+ private poseComposee;
192
+ /**
193
+ * Changement d'etat, date.
194
+ *
195
+ * Le moteur ne garde qu'UNE case d'historique, donc un changement qui arrive pendant un
196
+ * fondu remplacait l'origine du melange par la pose PLEINE de l'etat qu'on quittait, au
197
+ * lieu de l'image partiellement melangee qui etait a l'ecran. Mesure sur
198
+ * `idle -> wide -> idle` a 100 ms : 35,9 px de saut contre 8,0 px de mouvement normal.
199
+ *
200
+ * On fige donc la pose composite courante et on melange depuis elle. Continu par
201
+ * construction, quel que soit le nombre de changements enchaines.
202
+ *
203
+ * Et SEULEMENT dans ce cas. Figer a chaque changement arreterait net l'animation de
204
+ * l'etat qu'on quitte pendant tout le fondu — le « ! » d'`alert` se figerait en pleine
205
+ * course — alors qu'il n'y a rien a corriger hors morph : l'etat quitte y est deja
206
+ * exactement l'image affichee. La lecture d'un montage, dont les blocs durent au moins
207
+ * le plus long fondu (`MIN_BLOCK`), ne fige donc jamais rien et rend au bit ce qu'elle
208
+ * rendait.
209
+ */
210
+ setState(id: StateId, now: number): void;
211
+ sample(now: number): BotFrame;
212
+ }
213
+ //# sourceMappingURL=engine.d.ts.map
@@ -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