@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,63 @@
1
+ /** Demi-ecart des yeux sur la sphere, en degres (separation totale ~31deg). */
2
+ export declare const EYE_SPLIT = 15.46;
3
+ /** Taille de l'oeil au repos, en unites de rayon de boule. */
4
+ export declare const EYE_W = 0.186;
5
+ export declare const EYE_H = 0.412;
6
+ /** Orientation de tete au repos, ajustee sur les frames de reference. */
7
+ export declare const REST_GAZE: HeadGaze;
8
+ export interface EyePose {
9
+ x: number;
10
+ y: number;
11
+ /** matrice tangente 2x2 : [a b c d] au sens SVG matrix(a,b,c,d,e,f) */
12
+ a: number;
13
+ b: number;
14
+ c: number;
15
+ d: number;
16
+ /** composante z de la normale : > 0 = face visible */
17
+ depth: number;
18
+ }
19
+ export interface HeadGaze {
20
+ /** lacet, degres, positif = regarde a droite */
21
+ yaw: number;
22
+ /** tangage, degres, positif = regarde en haut */
23
+ pitch: number;
24
+ /** roulis, degres, inclinaison de la tete */
25
+ roll: number;
26
+ }
27
+ /**
28
+ * Repere de la tete puis des deux yeux.
29
+ * Repere ecran : x a droite, y vers le bas, z vers le spectateur.
30
+ * L'indice 0 est l'oeil interieur, l'indice 1 l'oeil exterieur.
31
+ */
32
+ export declare function eyePoses(gaze: HeadGaze, scale: number, split?: number): [EyePose, EyePose];
33
+ /**
34
+ * Vie au repos : derive lente du regard, saccades, clignements.
35
+ *
36
+ * Fonction pure du temps (aucun etat interne), donc pause, reprise et saut a
37
+ * une date arbitraire donnent toujours la meme image. Les valeurs sont des
38
+ * ECARTS a ajouter a la pose de l'etat courant.
39
+ */
40
+ export interface Liveliness {
41
+ dYaw: number;
42
+ dPitch: number;
43
+ dRoll: number;
44
+ /** 1 = oeil ouvert, 0 = ferme (ecrasement vertical en repere ecran) */
45
+ lid: number;
46
+ driftX: number;
47
+ driftY: number;
48
+ breath: number;
49
+ }
50
+ export interface LivelinessOptions {
51
+ wander?: number;
52
+ blink?: boolean;
53
+ float?: boolean;
54
+ }
55
+ export declare function liveliness(t: number, opt?: LivelinessOptions): Liveliness;
56
+ /**
57
+ * Le clignement est un ecrasement VERTICAL en repere ecran autour du centre de
58
+ * l'oeil (mesure : la largeur de bbox est conservee, la hauteur tombe a ~0.35),
59
+ * pas un retrecissement le long de l'axe incline de la gelule. On le compose
60
+ * donc apres la matrice tangente, en n'affectant que les sorties en y.
61
+ */
62
+ export declare function blinkScale(lid: number): number;
63
+ //# sourceMappingURL=face.d.ts.map
@@ -0,0 +1,106 @@
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 { clamp, createRng, loopNoise } from './math.js';
4
+ /** Demi-ecart des yeux sur la sphere, en degres (separation totale ~31deg). */
5
+ export const EYE_SPLIT = 15.46;
6
+ /** Taille de l'oeil au repos, en unites de rayon de boule. */
7
+ export const EYE_W = 0.186;
8
+ export const EYE_H = 0.412;
9
+ /** Orientation de tete au repos, ajustee sur les frames de reference. */
10
+ export const REST_GAZE = { yaw: 28.49, pitch: 28.62, roll: -13 };
11
+ const deg = (d) => (d * Math.PI) / 180;
12
+ /** Fait tourner deux vecteurs d'un repere orthonorme dans leur plan commun. */
13
+ function spin(u, v, angle) {
14
+ const c = Math.cos(angle);
15
+ const s = Math.sin(angle);
16
+ return [
17
+ [u[0] * c + v[0] * s, u[1] * c + v[1] * s, u[2] * c + v[2] * s],
18
+ [v[0] * c - u[0] * s, v[1] * c - u[1] * s, v[2] * c - u[2] * s]
19
+ ];
20
+ }
21
+ /**
22
+ * Repere de la tete puis des deux yeux.
23
+ * Repere ecran : x a droite, y vers le bas, z vers le spectateur.
24
+ * L'indice 0 est l'oeil interieur, l'indice 1 l'oeil exterieur.
25
+ */
26
+ export function eyePoses(gaze, scale, split = EYE_SPLIT) {
27
+ let f = [0, 0, 1];
28
+ let right = [1, 0, 0];
29
+ let down = [0, 1, 0];
30
+ [f, right] = spin(f, right, deg(gaze.yaw));
31
+ [down, f] = spin(down, f, deg(gaze.pitch));
32
+ [right, down] = spin(right, down, deg(gaze.roll));
33
+ const build = (side) => {
34
+ const [ef, er] = spin(f, right, deg(split * side));
35
+ return {
36
+ x: ef[0] * scale,
37
+ y: ef[1] * scale,
38
+ a: er[0],
39
+ b: er[1],
40
+ c: down[0],
41
+ d: down[1],
42
+ depth: ef[2]
43
+ };
44
+ };
45
+ return [build(-1), build(1)];
46
+ }
47
+ const BLINK_RNG = createRng(0x5eed);
48
+ /** Calendrier de clignements pre-tire : deterministe et sans etat. */
49
+ const BLINKS = (() => {
50
+ const out = [];
51
+ let t = 1.4;
52
+ while (t < 900) {
53
+ out.push(t);
54
+ // 1.9 a 4.6 s entre deux clignements, plus un double clignement parfois
55
+ t += 1.9 + BLINK_RNG() * 2.7;
56
+ if (BLINK_RNG() < 0.18) {
57
+ out.push(t);
58
+ t += 0.24;
59
+ }
60
+ }
61
+ return out;
62
+ })();
63
+ /** Mesure : 1 a 2 frames a 10 fps. */
64
+ const BLINK_DUR = 0.18;
65
+ function blinkLid(t) {
66
+ // A desktop companion can remain mounted longer than the 15-minute demo.
67
+ t = ((t % 900) + 900) % 900;
68
+ for (let i = 0; i < BLINKS.length; i++) {
69
+ const start = BLINKS[i];
70
+ if (t < start)
71
+ break;
72
+ const k = (t - start) / BLINK_DUR;
73
+ if (k >= 0 && k <= 1) {
74
+ // fermeture rapide, reouverture un peu plus lente
75
+ return k < 0.45 ? 1 - k / 0.45 : (k - 0.45) / 0.55;
76
+ }
77
+ }
78
+ return 1;
79
+ }
80
+ export function liveliness(t, opt = {}) {
81
+ const { wander = 1, blink = true, float = true } = opt;
82
+ // Periodes premieres entre elles : la derive ne se repete jamais a l'oeil.
83
+ return {
84
+ dYaw: (loopNoise(t, 11.3, 0.4) * 5.5 + loopNoise(t, 3.7, 2.1) * 1.6) * wander,
85
+ dPitch: (loopNoise(t, 9.1, 1.3) * 4.2 + loopNoise(t, 4.3, 0.7) * 1.3) * wander,
86
+ dRoll: loopNoise(t, 13.7, 3.2) * 2.2 * wander,
87
+ lid: blink ? blinkLid(t) : 1,
88
+ // Au repos la video est quasiment immobile (centre stable a +-0.003, rayon
89
+ // constant) : toute la vie passe par le regard et les clignements. On garde
90
+ // juste de quoi ne pas figer completement l'image.
91
+ driftX: float ? loopNoise(t, 7.9, 1.9) * 0.006 : 0,
92
+ driftY: float ? loopNoise(t, 5.3, 0.3) * 0.007 : 0,
93
+ // La largeur est constante, seule la hauteur respire tres legerement.
94
+ breath: float ? 1 + Math.sin((t / 3.4) * Math.PI * 2) * 0.005 : 1
95
+ };
96
+ }
97
+ /**
98
+ * Le clignement est un ecrasement VERTICAL en repere ecran autour du centre de
99
+ * l'oeil (mesure : la largeur de bbox est conservee, la hauteur tombe a ~0.35),
100
+ * pas un retrecissement le long de l'axe incline de la gelule. On le compose
101
+ * donc apres la matrice tangente, en n'affectant que les sorties en y.
102
+ */
103
+ export function blinkScale(lid) {
104
+ return 0.06 + 0.94 * clamp(lid);
105
+ }
106
+ //# sourceMappingURL=face.js.map
@@ -0,0 +1,22 @@
1
+ export declare const TAU: number;
2
+ export declare const clamp: (v: number, lo?: number, hi?: number) => number;
3
+ export declare const lerp: (a: number, b: number, t: number) => number;
4
+ export type Easing = (t: number) => number;
5
+ /**
6
+ * Mesure sur la video : les transitions sont des ease-out exponentiels, sans
7
+ * depassement du corps. Les seuls effets de ressort sont locaux (le pop de la
8
+ * pastille de notification, l'ouverture des yeux) et sont ecrits directement
9
+ * dans l'etat concerne.
10
+ */
11
+ export declare const easings: {
12
+ easeOutCubic: (t: number) => number;
13
+ easeInOutCubic: (t: number) => number;
14
+ easeOutQuint: (t: number) => number;
15
+ };
16
+ /** Bruit 1D periodique : boucle sans couture sur `period`, utile pour la derive du regard. */
17
+ export declare function loopNoise(t: number, period: number, seed?: number): number;
18
+ /** PRNG deterministe (mulberry32) : meme sequence a chaque lecture. */
19
+ export declare function createRng(seed: number): () => number;
20
+ /** Arrondi court : divise par ~2 le poids des chaines de path generees a 60 fps. */
21
+ export declare const r2: (v: number) => number;
22
+ //# sourceMappingURL=math.d.ts.map
@@ -0,0 +1,36 @@
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
+ export const TAU = Math.PI * 2;
4
+ export const clamp = (v, lo = 0, hi = 1) => (v < lo ? lo : v > hi ? hi : v);
5
+ export const lerp = (a, b, t) => a + (b - a) * t;
6
+ /**
7
+ * Mesure sur la video : les transitions sont des ease-out exponentiels, sans
8
+ * depassement du corps. Les seuls effets de ressort sont locaux (le pop de la
9
+ * pastille de notification, l'ouverture des yeux) et sont ecrits directement
10
+ * dans l'etat concerne.
11
+ */
12
+ export const easings = {
13
+ easeOutCubic: (t) => 1 - (1 - t) ** 3,
14
+ easeInOutCubic: (t) => (t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2),
15
+ easeOutQuint: (t) => 1 - (1 - t) ** 5
16
+ };
17
+ /** Bruit 1D periodique : boucle sans couture sur `period`, utile pour la derive du regard. */
18
+ export function loopNoise(t, period, seed = 0) {
19
+ const p = (t / period) * TAU;
20
+ return (0.55 * Math.sin(p + seed) +
21
+ 0.3 * Math.sin(2 * p + seed * 1.7 + 1.1) +
22
+ 0.15 * Math.sin(3 * p + seed * 2.3 + 2.4));
23
+ }
24
+ /** PRNG deterministe (mulberry32) : meme sequence a chaque lecture. */
25
+ export function createRng(seed) {
26
+ let a = seed >>> 0;
27
+ return () => {
28
+ a = (a + 0x6d2b79f5) >>> 0;
29
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
30
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
31
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
32
+ };
33
+ }
34
+ /** Arrondi court : divise par ~2 le poids des chaines de path generees a 60 fps. */
35
+ export const r2 = (v) => Math.round(v * 100) / 100;
36
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1,8 @@
1
+ export declare const PROFILE_SAMPLES = 64;
2
+ export declare const PROFILES: {
3
+ readonly egg: readonly [0.8369, 0.8424, 0.8497, 0.8585, 0.8674, 0.8775, 0.8878, 0.8983, 0.9089, 0.9185, 0.9288, 0.9374, 0.9445, 0.9504, 0.9543, 0.9559, 0.9555, 0.9519, 0.9466, 0.9389, 0.9302, 0.9193, 0.9085, 0.8969, 0.8852, 0.8734, 0.8625, 0.8513, 0.8411, 0.8325, 0.8243, 0.8179, 0.8137, 0.8112, 0.8102, 0.8128, 0.8178, 0.8262, 0.8374, 0.8518, 0.8702, 0.8922, 0.9169, 0.9446, 0.9741, 1.0023, 1.0267, 1.0433, 1.0481, 1.0393, 1.0216, 0.997, 0.9697, 0.9418, 0.9169, 0.8949, 0.876, 0.8604, 0.849, 0.8394, 0.8337, 0.8314, 0.8305, 0.8326];
4
+ readonly hexagon: readonly [0.921, 0.9282, 0.9441, 0.9706, 0.9984, 1.0059, 0.9896, 0.9562, 0.929, 0.9124, 0.9047, 0.9058, 0.9157, 0.9349, 0.9642, 0.9873, 0.9882, 0.9665, 0.9336, 0.9105, 0.8968, 0.8918, 0.8955, 0.908, 0.9293, 0.9611, 0.982, 0.9812, 0.959, 0.9282, 0.9089, 0.8978, 0.8964, 0.9026, 0.9189, 0.9439, 0.9778, 0.999, 0.9964, 0.9713, 0.9439, 0.9274, 0.9196, 0.9206, 0.9308, 0.9502, 0.9799, 1.0121, 1.0226, 1.0071, 0.9752, 0.951, 0.9366, 0.9316, 0.9351, 0.9485, 0.9711, 1.0026, 1.0213, 1.0155, 0.9863, 0.9547, 0.9347, 0.9232];
5
+ readonly triangle: readonly [0.7819, 0.8211, 0.8747, 0.944, 1.0223, 1.096, 1.1401, 1.134, 1.0808, 1.0047, 0.9265, 0.8603, 0.8104, 0.773, 0.745, 0.7273, 0.7151, 0.7118, 0.7148, 0.7245, 0.7427, 0.768, 0.8037, 0.8518, 0.9148, 0.9876, 1.0583, 1.1073, 1.1109, 1.0667, 0.994, 0.9164, 0.8482, 0.7948, 0.7555, 0.7261, 0.7056, 0.6925, 0.6859, 0.6869, 0.6938, 0.7084, 0.7305, 0.7615, 0.804, 0.8595, 0.9311, 1.0092, 1.0791, 1.1171, 1.1054, 1.0501, 0.9779, 0.905, 0.845, 0.799, 0.7656, 0.7413, 0.7258, 0.716, 0.7146, 0.7204, 0.733, 0.7528];
6
+ };
7
+ export type ProfileName = keyof typeof PROFILES;
8
+ //# sourceMappingURL=profiles.d.ts.map
@@ -0,0 +1,20 @@
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
+ // Profils radiaux r(theta) releves au pixel sur la video de reference.
4
+ // theta = 0 pointe vers la droite et croit dans le sens horaire (y vers le bas).
5
+ // Unite : rayon de la boule au repos = 1.
6
+ //
7
+ // Genere par tools/extract-profiles.py — ne pas editer a la main.
8
+ export const PROFILE_SAMPLES = 64;
9
+ export const PROFILES = {
10
+ // oeuf : meme hauteur que la boule, retreci en largeur
11
+ // image 164, empreinte mesuree 1.647 x 2.000
12
+ egg: [0.8369, 0.8424, 0.8497, 0.8585, 0.8674, 0.8775, 0.8878, 0.8983, 0.9089, 0.9185, 0.9288, 0.9374, 0.9445, 0.9504, 0.9543, 0.9559, 0.9555, 0.9519, 0.9466, 0.9389, 0.9302, 0.9193, 0.9085, 0.8969, 0.8852, 0.8734, 0.8625, 0.8513, 0.8411, 0.8325, 0.8243, 0.8179, 0.8137, 0.8112, 0.8102, 0.8128, 0.8178, 0.8262, 0.8374, 0.8518, 0.8702, 0.8922, 0.9169, 0.9446, 0.9741, 1.0023, 1.0267, 1.0433, 1.0481, 1.0393, 1.0216, 0.9970, 0.9697, 0.9418, 0.9169, 0.8949, 0.8760, 0.8604, 0.8490, 0.8394, 0.8337, 0.8314, 0.8305, 0.8326],
13
+ // hexagone pointe en haut, coins tres arrondis
14
+ // image 174, empreinte mesuree 1.826 x 2.011
15
+ hexagon: [0.9210, 0.9282, 0.9441, 0.9706, 0.9984, 1.0059, 0.9896, 0.9562, 0.9290, 0.9124, 0.9047, 0.9058, 0.9157, 0.9349, 0.9642, 0.9873, 0.9882, 0.9665, 0.9336, 0.9105, 0.8968, 0.8918, 0.8955, 0.9080, 0.9293, 0.9611, 0.9820, 0.9812, 0.9590, 0.9282, 0.9089, 0.8978, 0.8964, 0.9026, 0.9189, 0.9439, 0.9778, 0.9990, 0.9964, 0.9713, 0.9439, 0.9274, 0.9196, 0.9206, 0.9308, 0.9502, 0.9799, 1.0121, 1.0226, 1.0071, 0.9752, 0.9510, 0.9366, 0.9316, 0.9351, 0.9485, 0.9711, 1.0026, 1.0213, 1.0155, 0.9863, 0.9547, 0.9347, 0.9232],
16
+ // triangle pointe en haut, coins tres arrondis
17
+ // image 190, empreinte mesuree 1.995 x 1.884
18
+ triangle: [0.7819, 0.8211, 0.8747, 0.9440, 1.0223, 1.0960, 1.1401, 1.1340, 1.0808, 1.0047, 0.9265, 0.8603, 0.8104, 0.7730, 0.7450, 0.7273, 0.7151, 0.7118, 0.7148, 0.7245, 0.7427, 0.7680, 0.8037, 0.8518, 0.9148, 0.9876, 1.0583, 1.1073, 1.1109, 1.0667, 0.9940, 0.9164, 0.8482, 0.7948, 0.7555, 0.7261, 0.7056, 0.6925, 0.6859, 0.6869, 0.6938, 0.7084, 0.7305, 0.7615, 0.8040, 0.8595, 0.9311, 1.0092, 1.0791, 1.1171, 1.1054, 1.0501, 0.9779, 0.9050, 0.8450, 0.7990, 0.7656, 0.7413, 0.7258, 0.7160, 0.7146, 0.7204, 0.7330, 0.7528],
19
+ };
20
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Le repere de tout ce que le moteur rend.
3
+ *
4
+ * `engine.sample()` sort des coordonnees en unites de viewBox, et ces deux nombres en sont
5
+ * la definition : sans eux, une sortie du moteur ne veut rien dire. Ils vivaient dans
6
+ * `BloubBot.vue`, donc hors d'atteinte — un `<script setup>` n'exporte rien — et
7
+ * `export.ts` en redisait un a la main avec le commentaire qui nommait le probleme.
8
+ *
9
+ * Ils sont ici parce que `src/bot/` est ce qui se lit et se consomme du dehors : le
10
+ * composant Vue est UN client du moteur, pas sa definition.
11
+ */
12
+ /**
13
+ * Rayon de la boule au repos, en unites de viewBox. C'est le `scale` que le composant
14
+ * passe a `BotEngine`.
15
+ *
16
+ * Choisi et non mesure : c'est l'unite de travail. Tout le reste du dossier s'exprime en
17
+ * fractions de ce rayon, ce qui rend les mesures relevees sur la video independantes de la
18
+ * taille d'affichage.
19
+ */
20
+ export declare const RAYON = 100;
21
+ /**
22
+ * Demi-cote du viewBox affiche. La marge au-dela du rayon loge les anneaux.
23
+ *
24
+ * Ce n'est pas une valeur libre : les anneaux de l'orbite et le swoosh de la comete montent
25
+ * a 1,4 fois le rayon, soit 140. Rien ne les borne au runtime — c'est le reglage a la main
26
+ * des tableaux `RINGS` et `SWOOSH` (`decor.ts`) qui les tient sous 158, et un test le
27
+ * verrouille.
28
+ */
29
+ export declare const DEMI_VIEWBOX = 158;
30
+ //# sourceMappingURL=repere.d.ts.map
@@ -0,0 +1,32 @@
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
+ * Le repere de tout ce que le moteur rend.
5
+ *
6
+ * `engine.sample()` sort des coordonnees en unites de viewBox, et ces deux nombres en sont
7
+ * la definition : sans eux, une sortie du moteur ne veut rien dire. Ils vivaient dans
8
+ * `BloubBot.vue`, donc hors d'atteinte — un `<script setup>` n'exporte rien — et
9
+ * `export.ts` en redisait un a la main avec le commentaire qui nommait le probleme.
10
+ *
11
+ * Ils sont ici parce que `src/bot/` est ce qui se lit et se consomme du dehors : le
12
+ * composant Vue est UN client du moteur, pas sa definition.
13
+ */
14
+ /**
15
+ * Rayon de la boule au repos, en unites de viewBox. C'est le `scale` que le composant
16
+ * passe a `BotEngine`.
17
+ *
18
+ * Choisi et non mesure : c'est l'unite de travail. Tout le reste du dossier s'exprime en
19
+ * fractions de ce rayon, ce qui rend les mesures relevees sur la video independantes de la
20
+ * taille d'affichage.
21
+ */
22
+ export const RAYON = 100;
23
+ /**
24
+ * Demi-cote du viewBox affiche. La marge au-dela du rayon loge les anneaux.
25
+ *
26
+ * Ce n'est pas une valeur libre : les anneaux de l'orbite et le swoosh de la comete montent
27
+ * a 1,4 fois le rayon, soit 140. Rien ne les borne au runtime — c'est le reglage a la main
28
+ * des tableaux `RINGS` et `SWOOSH` (`decor.ts`) qui les tient sous 158, et un test le
29
+ * verrouille.
30
+ */
31
+ export const DEMI_VIEWBOX = 158;
32
+ //# sourceMappingURL=repere.js.map
@@ -0,0 +1,80 @@
1
+ import { type ProfileName } from './profiles.js';
2
+ export interface Point {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ /**
7
+ * Une silhouette = un profil radial r(theta) plus une pose.
8
+ *
9
+ * Tout passe par des profils echantillonnes au MEME nombre d'angles : deux
10
+ * formes quelconques ont donc des points qui se correspondent un a un, et le
11
+ * morphing se reduit a une interpolation lineaire des rayons. C'est ce qui
12
+ * rend les transitions propres sans librairie de morphing de path.
13
+ */
14
+ export interface Silhouette {
15
+ radii: number[];
16
+ /** rotation du profil, en radians */
17
+ rot: number;
18
+ /** decalage du centre, en unites de rayon de boule */
19
+ cx: number;
20
+ cy: number;
21
+ /** squash & stretch, applique en repere ecran (apres rotation) */
22
+ sx: number;
23
+ sy: number;
24
+ }
25
+ export declare function silhouette(name: ProfileName, pose?: Partial<Silhouette>): Silhouette;
26
+ /** Cercle parfait : sert de base neutre (point, bulle, cible de fondu). */
27
+ export declare function circle(radius: number, pose?: Partial<Silhouette>): Silhouette;
28
+ /** Interpolation de deux silhouettes. `out` est reutilise pour eviter d'allouer a 60 fps. */
29
+ export declare function blend(a: Silhouette, b: Silhouette, t: number, out?: Silhouette): Silhouette;
30
+ /** Projette la silhouette en points ecran. `scale` = rayon de la boule en unites de viewBox. */
31
+ export declare function toPoints(s: Silhouette, scale: number, out?: Point[]): Point[];
32
+ /**
33
+ * Polyligne fermee -> cubiques Catmull-Rom.
34
+ *
35
+ * Avec 64 points les tangentes centrees suffisent largement : le contour est
36
+ * lisse au pixel pres meme affiche en 600 px, et la chaine reste courte.
37
+ */
38
+ export declare function closedPath(pts: Point[], tension?: number): string;
39
+ /**
40
+ * Polygone quelconque -> profil radial, par lancer de rayon depuis `center`.
41
+ *
42
+ * Sert a fabriquer les formes qui ne s'expriment pas naturellement en r(theta)
43
+ * (la barre tronconique du "!"). Calcule une seule fois au chargement, jamais
44
+ * dans la boucle de rendu.
45
+ */
46
+ export declare function profileFromPolygon(poly: Point[], cx: number, cy: number): number[];
47
+ /** Enveloppe convexe de deux cercles : la barre tronconique du "!" vertical. */
48
+ export declare function hullOfCircles(x1: number, y1: number, r1: number, x2: number, y2: number, r2v: number, steps?: number): Point[];
49
+ /**
50
+ * Rayon du profil dans une direction quelconque, par interpolation entre les
51
+ * deux echantillons voisins.
52
+ *
53
+ * Sert a recaler ce qui est pose "sur" le corps (les yeux, la pastille de
54
+ * notification) quand la silhouette n'est plus un cercle : sans ca, un oeil
55
+ * place a 0.62 rayon sort d'une forme dont le bord est a 0.55 dans cette
56
+ * direction, et le masque le rogne.
57
+ */
58
+ export declare function radiusAtAngle(radii: number[], angle: number): number;
59
+ /**
60
+ * Superellipse : |x/sx|^n + |y/sy|^n = 1.
61
+ * n = 2 donne une ellipse, n ~ 4 le squircle du personnalisateur.
62
+ */
63
+ export declare function superellipseProfile(n: number, sx?: number, sy?: number): number[];
64
+ /**
65
+ * Profil radial de l'UNION de disques : r(theta) = la plus lointaine des
66
+ * intersections rayon/cercle. Exact tant que l'origine est dans l'union — c'est
67
+ * ce qui donne les bosses du nuage sans booleen de path.
68
+ */
69
+ export declare function unionOfCirclesProfile(circles: Array<{
70
+ x: number;
71
+ y: number;
72
+ r: number;
73
+ }>): number[];
74
+ /** Polygone regulier a coins arrondis, inscrit dans `radius`. */
75
+ export declare function regularPolygonProfile(sides: number, radius: number, rc: number, rotationDeg?: number): number[];
76
+ /** Polyligne fermee exacte : garde les segments droits (contrairement a closedPath). */
77
+ export declare function polyPath(pts: Point[], scale?: number): string;
78
+ /** Capsule (stade) centree sur l'origine : la forme exacte des yeux du bot. */
79
+ export declare function capsulePath(w: number, h: number): string;
80
+ //# sourceMappingURL=shape.d.ts.map
@@ -0,0 +1,268 @@
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 { TAU, lerp, r2 } from './math.js';
4
+ import { PROFILES, PROFILE_SAMPLES } from './profiles.js';
5
+ const ANGLES = Array.from({ length: PROFILE_SAMPLES }, (_, i) => (i / PROFILE_SAMPLES) * TAU);
6
+ const COS = ANGLES.map(Math.cos);
7
+ const SIN = ANGLES.map(Math.sin);
8
+ export function silhouette(name, pose = {}) {
9
+ return {
10
+ radii: [...PROFILES[name]],
11
+ rot: 0,
12
+ cx: 0,
13
+ cy: 0,
14
+ sx: 1,
15
+ sy: 1,
16
+ ...pose
17
+ };
18
+ }
19
+ /** Cercle parfait : sert de base neutre (point, bulle, cible de fondu). */
20
+ export function circle(radius, pose = {}) {
21
+ return {
22
+ radii: new Array(PROFILE_SAMPLES).fill(radius),
23
+ rot: 0,
24
+ cx: 0,
25
+ cy: 0,
26
+ sx: 1,
27
+ sy: 1,
28
+ ...pose
29
+ };
30
+ }
31
+ /** Interpolation de deux silhouettes. `out` est reutilise pour eviter d'allouer a 60 fps. */
32
+ export function blend(a, b, t, out) {
33
+ const dst = out ?? { radii: new Array(PROFILE_SAMPLES), rot: 0, cx: 0, cy: 0, sx: 1, sy: 1 };
34
+ for (let i = 0; i < PROFILE_SAMPLES; i++) {
35
+ dst.radii[i] = lerp(a.radii[i] ?? 1, b.radii[i] ?? 1, t);
36
+ }
37
+ // Rotation par le chemin le plus court : evite de faire un tour complet
38
+ // quand on passe par exemple de +170deg a -170deg.
39
+ let dRot = b.rot - a.rot;
40
+ while (dRot > Math.PI)
41
+ dRot -= TAU;
42
+ while (dRot < -Math.PI)
43
+ dRot += TAU;
44
+ dst.rot = a.rot + dRot * t;
45
+ dst.cx = lerp(a.cx, b.cx, t);
46
+ dst.cy = lerp(a.cy, b.cy, t);
47
+ dst.sx = lerp(a.sx, b.sx, t);
48
+ dst.sy = lerp(a.sy, b.sy, t);
49
+ return dst;
50
+ }
51
+ /** Projette la silhouette en points ecran. `scale` = rayon de la boule en unites de viewBox. */
52
+ export function toPoints(s, scale, out = []) {
53
+ const cr = Math.cos(s.rot);
54
+ const sr = Math.sin(s.rot);
55
+ for (let i = 0; i < PROFILE_SAMPLES; i++) {
56
+ const r = s.radii[i] ?? 1;
57
+ const x = r * (COS[i] ?? 0);
58
+ const y = r * (SIN[i] ?? 0);
59
+ // rotation puis squash en repere ecran, puis translation
60
+ const rx = x * cr - y * sr;
61
+ const ry = x * sr + y * cr;
62
+ const p = out[i] ?? { x: 0, y: 0 };
63
+ p.x = (rx * s.sx + s.cx) * scale;
64
+ p.y = (ry * s.sy + s.cy) * scale;
65
+ out[i] = p;
66
+ }
67
+ out.length = PROFILE_SAMPLES;
68
+ return out;
69
+ }
70
+ /**
71
+ * Polyligne fermee -> cubiques Catmull-Rom.
72
+ *
73
+ * Avec 64 points les tangentes centrees suffisent largement : le contour est
74
+ * lisse au pixel pres meme affiche en 600 px, et la chaine reste courte.
75
+ */
76
+ export function closedPath(pts, tension = 1 / 6) {
77
+ const n = pts.length;
78
+ if (n < 3)
79
+ return '';
80
+ const first = pts[0];
81
+ let d = `M${r2(first.x)} ${r2(first.y)}`;
82
+ for (let i = 0; i < n; i++) {
83
+ const p0 = pts[(i - 1 + n) % n];
84
+ const p1 = pts[i];
85
+ const p2 = pts[(i + 1) % n];
86
+ const p3 = pts[(i + 2) % n];
87
+ const c1x = p1.x + (p2.x - p0.x) * tension;
88
+ const c1y = p1.y + (p2.y - p0.y) * tension;
89
+ const c2x = p2.x - (p3.x - p1.x) * tension;
90
+ const c2y = p2.y - (p3.y - p1.y) * tension;
91
+ d += `C${r2(c1x)} ${r2(c1y)} ${r2(c2x)} ${r2(c2y)} ${r2(p2.x)} ${r2(p2.y)}`;
92
+ }
93
+ return `${d}Z`;
94
+ }
95
+ /**
96
+ * Polygone quelconque -> profil radial, par lancer de rayon depuis `center`.
97
+ *
98
+ * Sert a fabriquer les formes qui ne s'expriment pas naturellement en r(theta)
99
+ * (la barre tronconique du "!"). Calcule une seule fois au chargement, jamais
100
+ * dans la boucle de rendu.
101
+ */
102
+ export function profileFromPolygon(poly, cx, cy) {
103
+ const radii = new Array(PROFILE_SAMPLES).fill(0);
104
+ const n = poly.length;
105
+ for (let k = 0; k < PROFILE_SAMPLES; k++) {
106
+ const dx = COS[k] ?? 0;
107
+ const dy = SIN[k] ?? 0;
108
+ let best = 0;
109
+ for (let i = 0; i < n; i++) {
110
+ const a = poly[i];
111
+ const b = poly[(i + 1) % n];
112
+ const ex = b.x - a.x;
113
+ const ey = b.y - a.y;
114
+ const den = dx * ey - dy * ex;
115
+ if (Math.abs(den) < 1e-9)
116
+ continue;
117
+ const px = a.x - cx;
118
+ const py = a.y - cy;
119
+ const t = (px * ey - py * ex) / den; // distance le long du rayon
120
+ const u = (px * dy - py * dx) / den; // position sur le segment
121
+ if (t > best && u >= 0 && u <= 1)
122
+ best = t;
123
+ }
124
+ radii[k] = best;
125
+ }
126
+ return radii;
127
+ }
128
+ /** Enveloppe convexe de deux cercles : la barre tronconique du "!" vertical. */
129
+ export function hullOfCircles(x1, y1, r1, x2, y2, r2v, steps = 96) {
130
+ const dx = x2 - x1;
131
+ const dy = y2 - y1;
132
+ const dist = Math.hypot(dx, dy) || 1e-6;
133
+ // angle des tangentes externes communes
134
+ const base = Math.atan2(dy, dx);
135
+ const spread = Math.acos(Math.max(-1, Math.min(1, (r1 - r2v) / dist)));
136
+ const pts = [];
137
+ // arc du grand cercle
138
+ for (let i = 0; i <= steps / 2; i++) {
139
+ const a = base + spread + ((TAU - 2 * spread) * i) / (steps / 2);
140
+ pts.push({ x: x1 + Math.cos(a) * r1, y: y1 + Math.sin(a) * r1 });
141
+ }
142
+ // arc du petit cercle
143
+ for (let i = 0; i <= steps / 2; i++) {
144
+ const a = base - spread + ((2 * spread) * i) / (steps / 2);
145
+ pts.push({ x: x2 + Math.cos(a) * r2v, y: y2 + Math.sin(a) * r2v });
146
+ }
147
+ return pts;
148
+ }
149
+ /**
150
+ * Rayon du profil dans une direction quelconque, par interpolation entre les
151
+ * deux echantillons voisins.
152
+ *
153
+ * Sert a recaler ce qui est pose "sur" le corps (les yeux, la pastille de
154
+ * notification) quand la silhouette n'est plus un cercle : sans ca, un oeil
155
+ * place a 0.62 rayon sort d'une forme dont le bord est a 0.55 dans cette
156
+ * direction, et le masque le rogne.
157
+ */
158
+ export function radiusAtAngle(radii, angle) {
159
+ const n = radii.length;
160
+ const t = ((((angle / TAU) % 1) + 1) % 1) * n;
161
+ const i = Math.floor(t);
162
+ return lerp(radii[i % n] ?? 1, radii[(i + 1) % n] ?? 1, t - i);
163
+ }
164
+ /**
165
+ * Superellipse : |x/sx|^n + |y/sy|^n = 1.
166
+ * n = 2 donne une ellipse, n ~ 4 le squircle du personnalisateur.
167
+ */
168
+ export function superellipseProfile(n, sx = 1, sy = 1) {
169
+ return ANGLES.map((_, i) => {
170
+ const c = Math.abs((COS[i] ?? 0) / sx) ** n;
171
+ const s = Math.abs((SIN[i] ?? 0) / sy) ** n;
172
+ return (c + s) ** (-1 / n);
173
+ });
174
+ }
175
+ /**
176
+ * Profil radial de l'UNION de disques : r(theta) = la plus lointaine des
177
+ * intersections rayon/cercle. Exact tant que l'origine est dans l'union — c'est
178
+ * ce qui donne les bosses du nuage sans booleen de path.
179
+ */
180
+ export function unionOfCirclesProfile(circles) {
181
+ const out = new Array(PROFILE_SAMPLES).fill(0);
182
+ for (let i = 0; i < PROFILE_SAMPLES; i++) {
183
+ const dx = COS[i] ?? 0;
184
+ const dy = SIN[i] ?? 0;
185
+ let best = 0;
186
+ for (const c of circles) {
187
+ const b = dx * c.x + dy * c.y;
188
+ const disc = b * b - (c.x * c.x + c.y * c.y - c.r * c.r);
189
+ if (disc < 0)
190
+ continue;
191
+ const t = b + Math.sqrt(disc);
192
+ if (t > best)
193
+ best = t;
194
+ }
195
+ out[i] = best;
196
+ }
197
+ return out;
198
+ }
199
+ /**
200
+ * Polygone a coins arrondis, par somme de Minkowski avec un disque : chaque
201
+ * arete est poussee de `rc` vers l'exterieur, chaque sommet devient un arc de
202
+ * rayon `rc`. Les sommets sont donc a poser au rayon voulu MOINS rc.
203
+ * Attend un polygone en sens horaire (repere ecran, y vers le bas).
204
+ */
205
+ function roundedPolygon(verts, rc, arcSteps = 10) {
206
+ const n = verts.length;
207
+ const out = [];
208
+ const normal = (a, b) => {
209
+ const dx = b.x - a.x;
210
+ const dy = b.y - a.y;
211
+ const len = Math.hypot(dx, dy) || 1;
212
+ // sens horaire + y vers le bas : la normale sortante est (dy, -dx)
213
+ return Math.atan2(-dx / len, dy / len);
214
+ };
215
+ for (let i = 0; i < n; i++) {
216
+ const prev = verts[(i - 1 + n) % n];
217
+ const cur = verts[i];
218
+ const next = verts[(i + 1) % n];
219
+ const a0 = normal(prev, cur);
220
+ const a1 = normal(cur, next);
221
+ let d = a1 - a0;
222
+ while (d > Math.PI)
223
+ d -= TAU;
224
+ while (d < -Math.PI)
225
+ d += TAU;
226
+ for (let k = 0; k <= arcSteps; k++) {
227
+ const a = a0 + (d * k) / arcSteps;
228
+ out.push({ x: cur.x + Math.cos(a) * rc, y: cur.y + Math.sin(a) * rc });
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+ /** Polygone regulier a coins arrondis, inscrit dans `radius`. */
234
+ export function regularPolygonProfile(sides, radius, rc, rotationDeg = 0) {
235
+ const rot = (rotationDeg * Math.PI) / 180;
236
+ const verts = Array.from({ length: sides }, (_, i) => {
237
+ // sens horaire a l'ecran : theta croissant avec y vers le bas
238
+ const a = rot + (i / sides) * TAU;
239
+ return { x: Math.cos(a) * (radius - rc), y: Math.sin(a) * (radius - rc) };
240
+ });
241
+ return profileFromPolygon(roundedPolygon(verts, rc), 0, 0);
242
+ }
243
+ /** Polyligne fermee exacte : garde les segments droits (contrairement a closedPath). */
244
+ export function polyPath(pts, scale = 1) {
245
+ if (pts.length < 3)
246
+ return '';
247
+ let d = '';
248
+ for (let i = 0; i < pts.length; i++) {
249
+ const p = pts[i];
250
+ d += `${i === 0 ? 'M' : 'L'}${r2(p.x * scale)} ${r2(p.y * scale)}`;
251
+ }
252
+ return `${d}Z`;
253
+ }
254
+ /** Capsule (stade) centree sur l'origine : la forme exacte des yeux du bot. */
255
+ export function capsulePath(w, h) {
256
+ const hw = Math.max(w, 0.01) / 2;
257
+ const hh = Math.max(h, 0.01) / 2;
258
+ const r = Math.min(hw, hh);
259
+ return (`M${r2(-hw)} ${r2(-hh + r)}` +
260
+ `A${r2(r)} ${r2(r)} 0 0 1 ${r2(-hw + r)} ${r2(-hh)}` +
261
+ `L${r2(hw - r)} ${r2(-hh)}` +
262
+ `A${r2(r)} ${r2(r)} 0 0 1 ${r2(hw)} ${r2(-hh + r)}` +
263
+ `L${r2(hw)} ${r2(hh - r)}` +
264
+ `A${r2(r)} ${r2(r)} 0 0 1 ${r2(hw - r)} ${r2(hh)}` +
265
+ `L${r2(-hw + r)} ${r2(hh)}` +
266
+ `A${r2(r)} ${r2(r)} 0 0 1 ${r2(-hw)} ${r2(hh - r)}Z`);
267
+ }
268
+ //# sourceMappingURL=shape.js.map