@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,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
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Ou poser le visage sur une forme du personnalisateur.
3
+ *
4
+ * Les yeux vivent sur une sphere, et `radiusAtAngle` les recolle au contour reel au
5
+ * prorata du rayon local. Ce prorata place bien leur CENTRE, mais l'oeil a une taille :
6
+ * la marge qui lui reste devant le bord est multipliee par le meme facteur, donc une
7
+ * silhouette etroite dans sa direction le pousse contre le bord jusqu'a ce que le
8
+ * masque l'ouvre vers l'exterieur. La gelule apparaissait comme une encoche dans le
9
+ * corps sur `capsule`, `triangle`, `nuage` et `goutte`.
10
+ *
11
+ * Ce module resout le probleme UNE FOIS, au chargement, et rend une table de decalages.
12
+ * Ce choix est l'essentiel du correctif, bien plus que la geometrie qui suit :
13
+ *
14
+ * Resolue dans la boucle de rendu, la correction reagit a tout ce qui bouge a soixante
15
+ * images par seconde — la derive du regard, le pointeur, l'expression en cours de
16
+ * morph, le bord le plus proche qui change, l'oeil le plus contraint qui change. Sept
17
+ * variantes ont ete ecrites ainsi et toutes produisaient un artefact de mouvement
18
+ * visible : tremblement permanent, saut de direction de 26 unites quand le bord de
19
+ * reference basculait, grossissement brusque quand la taille entrait dans le calcul.
20
+ * Le defaut n'etait dans aucune de leurs geometries, il etait dans le fait de resoudre
21
+ * par image.
22
+ *
23
+ * Le reste du moteur ne travaille pas comme ca : les poses sont DECLAREES et il ne fait
24
+ * que les interpoler avec des courbes connues. Un decalage tabule rentre dans ce moule.
25
+ * Il ne bouge pas quand le regard derive ni quand le pointeur bouge, et sur un changement
26
+ * de forme ou d'expression il ne fait qu'aller d'une entree de table a l'autre, sur la
27
+ * courbe de ce morph. Le tremblement devient impossible par construction, au lieu d'etre
28
+ * repousse : interpoler entre deux constantes est monotone, alors que re-resoudre le
29
+ * probleme sur un regard en cours d'interpolation ne l'est pas.
30
+ *
31
+ * Corollaire agreable : le solveur n'a plus aucune contrainte de continuite, puisqu'il ne
32
+ * tourne pas pendant l'animation. Il peut donc sonder tout un faisceau de directions et
33
+ * couvrir le pire cas de la derive du regard, ce qu'une version par image ne pouvait pas
34
+ * se permettre.
35
+ *
36
+ * La table est une constante de module, batie a l'import a partir de donnees pures :
37
+ * meme nature que le calendrier de clignements de `face.ts`, deterministe et sans etat,
38
+ * donc sans effet sur la purete de `engine.sample(t)`.
39
+ */
40
+ import { type StateId } from './states.js';
41
+ /**
42
+ * Table des decalages, batie a l'import : une entree par (forme, etat a corps de base,
43
+ * expression). Seuls `idle` et `swirl` portent le visage de repos, donc seuls eux se
44
+ * declinent par expression — les trois autres etats a corps de base ont un visage releve
45
+ * sur la video et une seule entree.
46
+ *
47
+ * Clef par REFERENCE du tableau de rayons, ce qui est deja la convention du moteur : ses
48
+ * gardes `radii === this.shape` et `expression === this.expr` reposent sur la meme
49
+ * stabilite. Un profil inconnu, ou `null`, ne corrige rien — l'API accepte n'importe quel
50
+ * tableau et le moteur n'a pas a dependre de la prudence de ses appelants.
51
+ */
52
+ declare function batir(): Map<number[], Map<string, {
53
+ x: number;
54
+ y: number;
55
+ }>>;
56
+ /**
57
+ * Decalage a appliquer aux deux yeux pour cette forme sur cet etat, en unites de rayon
58
+ * de boule — le moteur le remet a son echelle.
59
+ *
60
+ * Vaut zero des que la forme n'est pas au catalogue, ce qui couvre `null` et le cercle :
61
+ * sur le cercle les deux profils sont le meme, donc la marge est deja celle exigee et la
62
+ * descente sort au premier tour. La forme relevee sur la video ne bouge donc pas, sans
63
+ * cas particulier.
64
+ */
65
+ export declare function decalageDesYeux(radii: number[] | null, state: StateId, expr: string | null): {
66
+ x: number;
67
+ y: number;
68
+ };
69
+ /** Pour les tests : de quoi verifier la table sans refaire la geometrie. */
70
+ /** Pour les tests : de quoi chronometrer la construction de la table. */
71
+ export declare const POUR_TESTS: {
72
+ batir: typeof batir;
73
+ };
74
+ export {};
75
+ //# sourceMappingURL=eyefit.d.ts.map
@@ -0,0 +1,393 @@
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
+ /**
4
+ * Ou poser le visage sur une forme du personnalisateur.
5
+ *
6
+ * Les yeux vivent sur une sphere, et `radiusAtAngle` les recolle au contour reel au
7
+ * prorata du rayon local. Ce prorata place bien leur CENTRE, mais l'oeil a une taille :
8
+ * la marge qui lui reste devant le bord est multipliee par le meme facteur, donc une
9
+ * silhouette etroite dans sa direction le pousse contre le bord jusqu'a ce que le
10
+ * masque l'ouvre vers l'exterieur. La gelule apparaissait comme une encoche dans le
11
+ * corps sur `capsule`, `triangle`, `nuage` et `goutte`.
12
+ *
13
+ * Ce module resout le probleme UNE FOIS, au chargement, et rend une table de decalages.
14
+ * Ce choix est l'essentiel du correctif, bien plus que la geometrie qui suit :
15
+ *
16
+ * Resolue dans la boucle de rendu, la correction reagit a tout ce qui bouge a soixante
17
+ * images par seconde — la derive du regard, le pointeur, l'expression en cours de
18
+ * morph, le bord le plus proche qui change, l'oeil le plus contraint qui change. Sept
19
+ * variantes ont ete ecrites ainsi et toutes produisaient un artefact de mouvement
20
+ * visible : tremblement permanent, saut de direction de 26 unites quand le bord de
21
+ * reference basculait, grossissement brusque quand la taille entrait dans le calcul.
22
+ * Le defaut n'etait dans aucune de leurs geometries, il etait dans le fait de resoudre
23
+ * par image.
24
+ *
25
+ * Le reste du moteur ne travaille pas comme ca : les poses sont DECLAREES et il ne fait
26
+ * que les interpoler avec des courbes connues. Un decalage tabule rentre dans ce moule.
27
+ * Il ne bouge pas quand le regard derive ni quand le pointeur bouge, et sur un changement
28
+ * de forme ou d'expression il ne fait qu'aller d'une entree de table a l'autre, sur la
29
+ * courbe de ce morph. Le tremblement devient impossible par construction, au lieu d'etre
30
+ * repousse : interpoler entre deux constantes est monotone, alors que re-resoudre le
31
+ * probleme sur un regard en cours d'interpolation ne l'est pas.
32
+ *
33
+ * Corollaire agreable : le solveur n'a plus aucune contrainte de continuite, puisqu'il ne
34
+ * tourne pas pendant l'animation. Il peut donc sonder tout un faisceau de directions et
35
+ * couvrir le pire cas de la derive du regard, ce qu'une version par image ne pouvait pas
36
+ * se permettre.
37
+ *
38
+ * La table est une constante de module, batie a l'import a partir de donnees pures :
39
+ * meme nature que le calendrier de clignements de `face.ts`, deterministe et sans etat,
40
+ * donc sans effet sur la purete de `engine.sample(t)`.
41
+ */
42
+ import { EXPRESSIONS } from './expressions.js';
43
+ import { eyePoses } from './face.js';
44
+ import { radiusAtAngle, toPoints } from './shape.js';
45
+ import { SHAPES } from './skins.js';
46
+ import { STATES } from './states.js';
47
+ /** Rayon de reference du solveur. Le decalage rendu est en unites de ce rayon. */
48
+ const R = 100;
49
+ /**
50
+ * Amplitudes maximales de la vie au repos, lues sur `liveliness` : `loopNoise` est
51
+ * borne a 1 en valeur absolue, donc ces sommes sont des bornes exactes et non des
52
+ * estimations.
53
+ *
54
+ * Il faut les couvrir, sinon la correction est juste sur la pose nominale et fausse une
55
+ * seconde plus tard : 7 degres de lacet deplacent l'oeil d'une douzaine d'unites sur une
56
+ * boule de rayon 100. C'est precisement ce qui faisait deborder `capsule` + `effraye`
57
+ * alors qu'une mesure a un seul instant le declarait bon.
58
+ */
59
+ const DERIVE_YAW = 5.5 + 1.6;
60
+ const DERIVE_PITCH = 4.2 + 1.3;
61
+ /** Flottement du centre, en unites de rayon de boule. */
62
+ const DERIVE_X = 0.006;
63
+ const DERIVE_Y = 0.007;
64
+ /**
65
+ * Empreintes des deux yeux d'un visage, posees sur un profil.
66
+ *
67
+ * Une gelule est exactement un segment epaissi d'un disque de rayon `r`. Son image par
68
+ * la matrice tangente est donc un segment epaissi d'une ELLIPSE, et un disque du rayon
69
+ * de son grand axe la couvre : d'ou la plus grande valeur singuliere. La mesure reste
70
+ * ainsi conservatrice au sens strict, une marge positive garantissant que la gelule est
71
+ * dedans.
72
+ *
73
+ * Le clignement n'y est pas : un oeil ferme n'a pas besoin qu'on lui fasse de la place.
74
+ */
75
+ function empreintes(visage, sil, radii) {
76
+ const out = [];
77
+ const poses = eyePoses(visage.gaze, R, visage.split);
78
+ for (let i = 0; i < 2; i++) {
79
+ const e = poses[i];
80
+ if (e.depth <= 0.02)
81
+ continue;
82
+ const cfg = visage.eyes[i];
83
+ const phi = ((cfg.tilt ?? 0) * Math.PI) / 180;
84
+ const cp = Math.cos(phi);
85
+ const sp = Math.sin(phi);
86
+ const ax = e.a * cp + e.c * sp;
87
+ const ay = e.b * cp + e.d * sp;
88
+ const cx = -e.a * sp + e.c * cp;
89
+ const cy = -e.b * sp + e.d * cp;
90
+ const hw = Math.max(cfg.w * R, 0.01) / 2;
91
+ const hh = Math.max(cfg.h * R, 0.01) / 2;
92
+ const r = Math.min(hw, hh);
93
+ // l'axe est celui de la plus grande dimension
94
+ const long = hh > hw;
95
+ const demi = long ? hh - r : hw - r;
96
+ // le prorata du rayon local, exactement comme le fait le moteur
97
+ const fit = radiusAtAngle(radii, Math.atan2(e.y, e.x) - sil.rot);
98
+ out.push({
99
+ x: e.x * fit,
100
+ y: e.y * fit,
101
+ ax: (long ? cx : ax) * demi,
102
+ ay: (long ? cy : ay) * demi,
103
+ r,
104
+ m: [ax, ay, cx, cy]
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+ /**
110
+ * Approche la plus courte entre un contour et un segment : la distance, et le vecteur
111
+ * qui va du contour vers le segment — le sens qui degage.
112
+ *
113
+ * Les deux sortent de la MEME passe. Les calculer separement doublait le seul vrai cout
114
+ * de ce module, qui est ce balayage.
115
+ */
116
+ function approche(pts, x0, y0, x1, y1) {
117
+ const sx = x1 - x0;
118
+ const sy = y1 - y0;
119
+ const len2 = sx * sx + sy * sy;
120
+ let best = Infinity;
121
+ let vx = 0;
122
+ let vy = 0;
123
+ for (let i = 0; i < pts.length; i++) {
124
+ const p = pts[i];
125
+ let t = len2 > 0 ? ((p.x - x0) * sx + (p.y - y0) * sy) / len2 : 0;
126
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
127
+ const ex = x0 + t * sx - p.x;
128
+ const ey = y0 + t * sy - p.y;
129
+ const d2 = ex * ex + ey * ey;
130
+ if (d2 < best) {
131
+ best = d2;
132
+ vx = ex;
133
+ vy = ey;
134
+ }
135
+ }
136
+ const d = Math.sqrt(best);
137
+ return { d, ux: d > 1e-9 ? vx / d : 0, uy: d > 1e-9 ? vy / d : 0 };
138
+ }
139
+ /**
140
+ * Flottement du centre au repos, en unites de viewBox. Il est ajoute au rayon de la
141
+ * gelule : moins d'une unite, donc l'absorber ainsi coute moins cher que de multiplier les
142
+ * epreuves par ses quatre coins.
143
+ */
144
+ const FLOTTEMENT = Math.hypot(DERIVE_X, DERIVE_Y) * R;
145
+ /** Marge de la gelule la plus serree, et le sens qui la degage. */
146
+ function pire(pts, emps, tx, ty) {
147
+ let marge = Infinity;
148
+ let ux = 0;
149
+ let uy = 0;
150
+ for (const e of emps) {
151
+ const x = e.x + tx;
152
+ const y = e.y + ty;
153
+ const a = approche(pts, x - e.ax, y - e.ay, x + e.ax, y + e.ay);
154
+ // fonction d'appui de l'ellipse dans la direction de l'approche
155
+ const [m0, m1, m2, m3] = e.m;
156
+ const rayon = e.r * Math.hypot(m0 * a.ux + m1 * a.uy, m2 * a.ux + m3 * a.uy) + FLOTTEMENT;
157
+ if (a.d - rayon < marge) {
158
+ marge = a.d - rayon;
159
+ ux = a.ux;
160
+ uy = a.uy;
161
+ }
162
+ }
163
+ return { marge, ux, uy };
164
+ }
165
+ /**
166
+ * Directions sondees et pas de la dichotomie. Le produit des deux est le cout de
167
+ * construction de la table, seul chiffre a surveiller ici.
168
+ */
169
+ const DIRECTIONS = 12;
170
+ const DICHOTOMIE = 8;
171
+ /**
172
+ * Le decalage a poser sur les deux yeux pour cette forme, cet etat et cette expression.
173
+ *
174
+ * Une TRANSLATION commune aux deux yeux, donc une isometrie : ecart entre les yeux,
175
+ * tailles et inclinaisons sont conserves au pixel. Le visage est seulement pose un peu
176
+ * plus bas sur un corps qui n'a pas de place en haut, ce qui est le geste qu'on ferait a
177
+ * la main. Les variantes qui bornaient chaque oeil separement ecartaient la paire, et
178
+ * celles qui mettaient le visage a l'echelle rapetissaient les yeux — visiblement.
179
+ *
180
+ * La marge visee est celle du profil D'ORIGINE, pas un degagement strict : sur le cercle
181
+ * l'oeil exterieur frole deja le bord, 17,3 unites pour une boule de rayon 100, et c'est
182
+ * voulu, c'est ce qui donne le volume. Elle est plafonnee par ce que la forme offre en son
183
+ * centre, sinon la demande est intenable sur un corps plat.
184
+ *
185
+ * RECHERCHE DIRECTIONNELLE et non descente. On cherche la translation de plus petite
186
+ * norme qui tient, donc on sonde une couronne de directions et on dichotomie la distance
187
+ * le long de chacune. Une descente de gradient a ete ecrite d'abord et elle ne converge
188
+ * pas : degager la paire d'un bord la rapproche de l'autre, si bien qu'elle tatonne et ne
189
+ * fait que garder son meilleur essai — passer ses tours de 40 a 18 suffisait a faire
190
+ * reapparaitre 34 debordements. Ici le resultat ne depend pas d'une convergence : chaque
191
+ * direction est resolue exactement, au pas de dichotomie pres.
192
+ */
193
+ function resous(epreuves) {
194
+ if (!epreuves.length)
195
+ return { x: 0, y: 0 };
196
+ /** La marge la plus serree sur toutes les epreuves, pour une translation donnee. */
197
+ const marge = (tx, ty) => {
198
+ let m = Infinity;
199
+ for (const ep of epreuves)
200
+ m = Math.min(m, pire(ep.contour, ep.empreintes, tx, ty).marge);
201
+ return m;
202
+ };
203
+ // Marge exigee : la plus serree que le profil d'origine tolere, sur toutes les
204
+ // epreuves. Puis plafonnee par le plus degage que la forme puisse offrir a la paire,
205
+ // son centre.
206
+ let requis = Infinity;
207
+ for (const ep of epreuves) {
208
+ requis = Math.min(requis, pire(ep.calContour, ep.reference, 0, 0).marge);
209
+ }
210
+ /*
211
+ * La course doit pouvoir atteindre le centre du corps : `wide` a des gelules de 87
212
+ * unites de long, et sur un triangle elles ne tiennent que vers le milieu, a une
213
+ * cinquantaine d'unites de leur place nominale. Une course fixe les laissait dehors.
214
+ */
215
+ let mx = 0;
216
+ let my = 0;
217
+ const emps = epreuves[0].empreintes;
218
+ for (const e of emps) {
219
+ mx -= e.x / emps.length;
220
+ my -= e.y / emps.length;
221
+ }
222
+ const course = Math.max(0.35 * R, Math.hypot(mx, my) * 1.25);
223
+ // Plafond de la demande : ce que la forme offre en son centre, toujours atteignable.
224
+ requis = Math.min(requis, marge(mx, my));
225
+ /*
226
+ * Deja bon : le cas du cercle, et de toute forme assez large. La gelule doit RENTRER
227
+ * en plus de n'etre pas plus serree que sur le profil d'origine — sans cette seconde
228
+ * condition, une forme ou rien ne rentre satisfait la premiere de facon degeneree et
229
+ * on abandonnait. `wide` a des gelules de 87 unites de long, `notify` de 50 de
230
+ * diametre : sur un triangle ou une goutte elles debordent quoi qu'on fasse, et il
231
+ * faut alors viser le moins pire, pas renoncer.
232
+ */
233
+ const depart = marge(0, 0);
234
+ if (depart >= requis && depart >= 0)
235
+ return { x: 0, y: 0 };
236
+ const cible = Math.max(requis, 0);
237
+ let meilleurX = 0;
238
+ let meilleurY = 0;
239
+ let meilleureNorme = Infinity;
240
+ // repli quand rien ne rentre : la translation qui degage le plus, sondee au passage
241
+ let secoursX = 0;
242
+ let secoursY = 0;
243
+ let secours = depart;
244
+ for (let d = 0; d < DIRECTIONS; d++) {
245
+ const a = (d / DIRECTIONS) * Math.PI * 2;
246
+ const ux = Math.cos(a);
247
+ const uy = Math.sin(a);
248
+ if (marge(ux * course, uy * course) < cible) {
249
+ // cette direction ne mene nulle part ; on garde quand meme le meilleur degagement
250
+ // pas de solution par la, mais peut-etre un meilleur degagement
251
+ for (const k of [0.3, 0.6, 1]) {
252
+ const m = marge(ux * course * k, uy * course * k);
253
+ if (m > secours) {
254
+ secours = m;
255
+ secoursX = ux * course * k;
256
+ secoursY = uy * course * k;
257
+ }
258
+ }
259
+ continue;
260
+ }
261
+ // la plus courte distance qui tient, le long de cette direction
262
+ let bas = 0;
263
+ let haut = course;
264
+ for (let i = 0; i < DICHOTOMIE; i++) {
265
+ const mid = (bas + haut) / 2;
266
+ if (marge(ux * mid, uy * mid) >= cible)
267
+ haut = mid;
268
+ else
269
+ bas = mid;
270
+ }
271
+ if (haut < meilleureNorme) {
272
+ meilleureNorme = haut;
273
+ meilleurX = ux * haut;
274
+ meilleurY = uy * haut;
275
+ }
276
+ }
277
+ const x = meilleureNorme === Infinity ? secoursX : meilleurX;
278
+ const y = meilleureNorme === Infinity ? secoursY : meilleurY;
279
+ // rendu en unites de RAYON DE BOULE : le moteur le remet a son echelle
280
+ return { x: +(x / R).toFixed(6), y: +(y / R).toFixed(6) };
281
+ }
282
+ /**
283
+ * Le visage a couvrir : celui de l'expression si l'etat l'accepte, le sien sinon.
284
+ *
285
+ * UNE entree de table par expression, et non un pire cas commun a toutes. Un pire cas
286
+ * paraissait plus sur — un decalage constant ne peut pas bouger quand l'expression change
287
+ * — mais il est intenable : sur une capsule, `neutre` a les yeux hauts et demande a
288
+ * descendre quand `effraye` les a bas et demande a monter. Aucune translation unique ne
289
+ * satisfait les deux, et la mesure le confirme (4 debordements de 4,8 unites).
290
+ *
291
+ * Une entree par expression n'est pas moins fluide pour autant : le moteur interpole
292
+ * entre DEUX CONSTANTES, ce qui est monotone par construction. Ce qui tremblait, c'etait
293
+ * de re-resoudre le probleme sur un regard en cours d'interpolation.
294
+ */
295
+ function visageDe(def, pose, expr) {
296
+ if (def.baseFace && expr)
297
+ return { gaze: expr.gaze, split: expr.split, eyes: expr.eyes };
298
+ return { gaze: pose.gaze, split: pose.split, eyes: pose.eyes };
299
+ }
300
+ /** Les dates a echantillonner dans un etat : une seule si sa pose ne bouge pas. */
301
+ function dates(def) {
302
+ /** Tout ce dont le solveur se sert : si rien ne bouge, une date suffit. */
303
+ const signature = (p) => JSON.stringify([p.gaze, p.split, p.eyes, p.sil.rot, p.sil.cx, p.sil.cy, p.sil.sx, p.sil.sy]);
304
+ if (signature(def.pose(0)) === signature(def.pose(def.duration)))
305
+ return [0];
306
+ const n = 3;
307
+ return Array.from({ length: n }, (_, i) => (i / (n - 1)) * def.duration);
308
+ }
309
+ /** Le decalage d'une forme sur un etat et une expression, derive comprise. */
310
+ function decalagePour(def, radii, expr) {
311
+ const epreuves = [];
312
+ for (const t of dates(def)) {
313
+ const pose = def.pose(t);
314
+ const contour = toPoints({ ...pose.sil, radii }, R);
315
+ const calContour = toPoints(pose.sil, R);
316
+ const v = visageDe(def, pose, expr);
317
+ // Les quatre coins de la derive bornent la pose nominale, qui est leur centre : la
318
+ // tester en plus ne changerait aucune marge et coute une epreuve sur cinq.
319
+ const coins = [];
320
+ for (const dy of [-DERIVE_YAW, DERIVE_YAW]) {
321
+ for (const dp of [-DERIVE_PITCH, DERIVE_PITCH]) {
322
+ coins.push({
323
+ ...v,
324
+ gaze: { yaw: v.gaze.yaw + dy, pitch: v.gaze.pitch + dp, roll: v.gaze.roll }
325
+ });
326
+ }
327
+ }
328
+ for (const c of coins) {
329
+ epreuves.push({
330
+ empreintes: empreintes(c, pose.sil, radii),
331
+ reference: empreintes(c, pose.sil, pose.sil.radii),
332
+ contour,
333
+ calContour
334
+ });
335
+ }
336
+ }
337
+ return resous(epreuves);
338
+ }
339
+ /** Zero, la valeur commune a tout ce qui n'a rien a corriger. */
340
+ const NUL = { x: 0, y: 0 };
341
+ /** Clef d'une entree : l'etat, et l'expression quand l'etat l'accepte. */
342
+ const clef = (state, expr) => `${state}|${expr ?? ''}`;
343
+ /**
344
+ * Table des decalages, batie a l'import : une entree par (forme, etat a corps de base,
345
+ * expression). Seuls `idle` et `swirl` portent le visage de repos, donc seuls eux se
346
+ * declinent par expression — les trois autres etats a corps de base ont un visage releve
347
+ * sur la video et une seule entree.
348
+ *
349
+ * Clef par REFERENCE du tableau de rayons, ce qui est deja la convention du moteur : ses
350
+ * gardes `radii === this.shape` et `expression === this.expr` reposent sur la meme
351
+ * stabilite. Un profil inconnu, ou `null`, ne corrige rien — l'API accepte n'importe quel
352
+ * tableau et le moteur n'a pas a dependre de la prudence de ses appelants.
353
+ */
354
+ function batir() {
355
+ return new Map(SHAPES.map((forme) => {
356
+ const par = new Map();
357
+ for (const def of STATES) {
358
+ if (!def.baseBody)
359
+ continue;
360
+ const expressions = def.baseFace ? [null, ...EXPRESSIONS] : [null];
361
+ for (const expr of expressions) {
362
+ par.set(clef(def.id, expr?.id ?? null), decalagePour(def, forme.radii, expr));
363
+ }
364
+ }
365
+ return [forme.radii, par];
366
+ }));
367
+ }
368
+ // The theme uses the authored body, not the customiser. Avoid building its
369
+ // fitting catalogue at startup; preserve the solver for a future custom shape.
370
+ let DECALAGES;
371
+ const CATALOG_PROFILES = new Set(SHAPES.map(shape => shape.radii));
372
+ /**
373
+ * Decalage a appliquer aux deux yeux pour cette forme sur cet etat, en unites de rayon
374
+ * de boule — le moteur le remet a son echelle.
375
+ *
376
+ * Vaut zero des que la forme n'est pas au catalogue, ce qui couvre `null` et le cercle :
377
+ * sur le cercle les deux profils sont le meme, donc la marge est deja celle exigee et la
378
+ * descente sort au premier tour. La forme relevee sur la video ne bouge donc pas, sans
379
+ * cas particulier.
380
+ */
381
+ export function decalageDesYeux(radii, state, expr) {
382
+ if (!radii || !CATALOG_PROFILES.has(radii))
383
+ return NUL;
384
+ const par = (DECALAGES ??= batir()).get(radii);
385
+ if (!par)
386
+ return NUL;
387
+ // un etat sans visage de repos n'a qu'une entree, quelle que soit l'expression
388
+ return par.get(clef(state, expr)) ?? par.get(clef(state, null)) ?? NUL;
389
+ }
390
+ /** Pour les tests : de quoi verifier la table sans refaire la geometrie. */
391
+ /** Pour les tests : de quoi chronometrer la construction de la table. */
392
+ export const POUR_TESTS = { batir };
393
+ //# sourceMappingURL=eyefit.js.map