@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.
- package/README.md +37 -17
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/lib/appearance.js +26 -10
- package/lib/blink.js +3 -3
- package/lib/bloub-heixiu/ambient.d.ts +3 -0
- package/lib/bloub-heixiu/ambient.js +12 -0
- package/lib/bloub-heixiu/character.d.ts +33 -0
- package/lib/bloub-heixiu/character.js +124 -0
- package/lib/bloub-heixiu/effects.d.ts +4 -0
- package/lib/bloub-heixiu/effects.js +91 -0
- package/lib/bloub-heixiu/flight.d.ts +15 -0
- package/lib/bloub-heixiu/flight.js +66 -0
- package/lib/bloub-heixiu/follow.d.ts +13 -0
- package/lib/bloub-heixiu/follow.js +23 -0
- package/lib/bloub-heixiu/layout.d.ts +18 -0
- package/lib/bloub-heixiu/layout.js +12 -0
- package/lib/bloub-heixiu/renderer.d.ts +16 -0
- package/lib/bloub-heixiu/renderer.js +94 -0
- package/lib/bloub-heixiu/runtime.d.ts +6 -0
- package/lib/bloub-heixiu/runtime.js +395 -0
- package/lib/bloub-heixiu/signals.d.ts +17 -0
- package/lib/bloub-heixiu/signals.js +53 -0
- package/lib/bloub-heixiu/styles.d.ts +2 -0
- package/lib/bloub-heixiu/styles.js +98 -0
- package/lib/chrome/accessibility.d.ts +1 -1
- package/lib/chrome/accessibility.js +5 -13
- package/lib/chrome/composer-avatar.d.ts +10 -0
- package/lib/chrome/composer-avatar.js +599 -0
- package/lib/chrome/composer-send-heixiu.d.ts +3 -0
- package/lib/chrome/composer-send-heixiu.js +143 -0
- package/lib/chrome/composer.d.ts +2 -2
- package/lib/chrome/composer.js +84 -95
- package/lib/chrome/controls.d.ts +8 -0
- package/lib/chrome/controls.js +348 -0
- package/lib/chrome/conversation-messages.d.ts +1 -1
- package/lib/chrome/conversation-messages.js +207 -9
- package/lib/chrome/conversation.d.ts +1 -1
- package/lib/chrome/conversation.js +6 -0
- package/lib/chrome/frames.d.ts +1 -1
- package/lib/chrome/frames.js +14 -0
- package/lib/chrome/identity.js +4 -18
- package/lib/chrome/portal.d.ts +1 -1
- package/lib/chrome/portal.js +1 -1
- package/lib/chrome/sidebar.d.ts +1 -1
- package/lib/chrome/sidebar.js +118 -153
- package/lib/chrome/tokens.d.ts +1 -1
- package/lib/chrome/tokens.js +121 -68
- package/lib/chrome/workspace.d.ts +2 -2
- package/lib/chrome/workspace.js +35 -254
- package/lib/chrome.js +4 -0
- package/lib/client.d.ts +5 -12
- package/lib/client.js +5864 -4447
- package/lib/composer-avatar.d.ts +18 -0
- package/lib/composer-avatar.js +441 -0
- package/lib/composer-send-heixiu.d.ts +8 -0
- package/lib/composer-send-heixiu.js +133 -0
- package/lib/gaze.js +3 -3
- package/lib/generated-backgrounds.d.ts +14 -0
- package/lib/generated-backgrounds.js +14 -0
- package/lib/generated-identity.d.ts +1 -0
- package/lib/generated-identity.js +1 -0
- package/lib/generated-keyart.d.ts +18 -3
- package/lib/generated-keyart.js +18 -3
- package/lib/host-contract.d.ts +9 -0
- package/lib/host-contract.js +13 -0
- package/lib/host-dom.d.ts +11 -0
- package/lib/host-dom.js +60 -0
- package/lib/loading-heixiu.js +3 -3
- package/lib/metallic-brand-mark.d.ts +10 -0
- package/lib/metallic-brand-mark.js +309 -0
- package/lib/plugin.d.ts +1 -1
- package/lib/plugin.js +31 -16
- package/lib/portal.js +11 -10
- package/lib/reactions.js +3 -3
- package/lib/scene/avatar-model.d.ts +6 -0
- package/lib/scene/avatar-model.js +646 -0
- package/lib/scene/avatar-motion.d.ts +3 -0
- package/lib/scene/avatar-motion.js +799 -0
- package/lib/scene/character-idle.d.ts +17 -0
- package/lib/scene/character-idle.js +65 -0
- package/lib/scene/character-layout.d.ts +24 -0
- package/lib/scene/character-layout.js +51 -0
- package/lib/scene/character-poses.d.ts +26 -0
- package/lib/scene/character-poses.js +11 -0
- package/lib/scene/heixiu-feedback.d.ts +1 -1
- package/lib/scene/heixiu-feedback.js +0 -17
- package/lib/scene/pointer-bridge.d.ts +21 -0
- package/lib/scene/pointer-bridge.js +155 -0
- package/lib/scene/runtime.d.ts +12 -0
- package/lib/scene/runtime.js +87 -0
- package/lib/scene/styles.d.ts +7 -0
- package/lib/scene/styles.js +75 -0
- package/lib/scene/sylva-performance.d.ts +11 -0
- package/lib/scene/sylva-performance.js +58 -0
- package/lib/scene/wallpaper-character-styles.d.ts +3 -0
- package/lib/scene/wallpaper-character-styles.js +43 -0
- package/lib/scene/wallpaper-character.d.ts +4 -0
- package/lib/scene/wallpaper-character.js +195 -0
- package/lib/scene.d.ts +7 -13
- package/lib/scene.js +7 -1023
- package/lib/sidebar-glass.js +88 -35
- package/lib/sidebar-heixiu-roaming.js +5 -5
- package/lib/state.d.ts +14 -2
- package/lib/state.js +12 -4
- package/lib/theme.js +2 -2
- package/lib/vendor/bloub/decor.d.ts +114 -0
- package/lib/vendor/bloub/decor.js +198 -0
- package/lib/vendor/bloub/engine.d.ts +213 -0
- package/lib/vendor/bloub/engine.js +452 -0
- package/lib/vendor/bloub/expressions.d.ts +35 -0
- package/lib/vendor/bloub/expressions.js +143 -0
- package/lib/vendor/bloub/eyefit.d.ts +75 -0
- package/lib/vendor/bloub/eyefit.js +393 -0
- package/lib/vendor/bloub/face.d.ts +63 -0
- package/lib/vendor/bloub/face.js +106 -0
- package/lib/vendor/bloub/math.d.ts +22 -0
- package/lib/vendor/bloub/math.js +36 -0
- package/lib/vendor/bloub/profiles.d.ts +8 -0
- package/lib/vendor/bloub/profiles.js +20 -0
- package/lib/vendor/bloub/repere.d.ts +30 -0
- package/lib/vendor/bloub/repere.js +32 -0
- package/lib/vendor/bloub/shape.d.ts +80 -0
- package/lib/vendor/bloub/shape.js +268 -0
- package/lib/vendor/bloub/skins.d.ts +36 -0
- package/lib/vendor/bloub/skins.js +74 -0
- package/lib/vendor/bloub/states.d.ts +87 -0
- package/lib/vendor/bloub/states.js +468 -0
- package/package.json +23 -6
- package/lib/appearance.d.ts.map +0 -1
- package/lib/appearance.js.map +0 -1
- package/lib/blink.d.ts.map +0 -1
- package/lib/blink.js.map +0 -1
- package/lib/boot-appearance.d.ts.map +0 -1
- package/lib/boot-appearance.js.map +0 -1
- package/lib/boot-loader.d.ts.map +0 -1
- package/lib/boot-loader.js.map +0 -1
- package/lib/chrome/accessibility.d.ts.map +0 -1
- package/lib/chrome/accessibility.js.map +0 -1
- package/lib/chrome/blink.d.ts.map +0 -1
- package/lib/chrome/blink.js.map +0 -1
- package/lib/chrome/composer.d.ts.map +0 -1
- package/lib/chrome/composer.js.map +0 -1
- package/lib/chrome/conversation-messages.d.ts.map +0 -1
- package/lib/chrome/conversation-messages.js.map +0 -1
- package/lib/chrome/conversation.d.ts.map +0 -1
- package/lib/chrome/conversation.js.map +0 -1
- package/lib/chrome/frames.d.ts.map +0 -1
- package/lib/chrome/frames.js.map +0 -1
- package/lib/chrome/gaze.d.ts.map +0 -1
- package/lib/chrome/gaze.js.map +0 -1
- package/lib/chrome/identity.d.ts.map +0 -1
- package/lib/chrome/identity.js.map +0 -1
- package/lib/chrome/overlays.d.ts.map +0 -1
- package/lib/chrome/overlays.js.map +0 -1
- package/lib/chrome/portal.d.ts.map +0 -1
- package/lib/chrome/portal.js.map +0 -1
- package/lib/chrome/reactions.d.ts.map +0 -1
- package/lib/chrome/reactions.js.map +0 -1
- package/lib/chrome/sidebar.d.ts.map +0 -1
- package/lib/chrome/sidebar.js.map +0 -1
- package/lib/chrome/tokens.d.ts.map +0 -1
- package/lib/chrome/tokens.js.map +0 -1
- package/lib/chrome/workspace.d.ts.map +0 -1
- package/lib/chrome/workspace.js.map +0 -1
- package/lib/chrome.d.ts.map +0 -1
- package/lib/chrome.js.map +0 -1
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js.map +0 -7
- package/lib/gaze.d.ts.map +0 -1
- package/lib/gaze.js.map +0 -1
- package/lib/generated-identity.d.ts.map +0 -1
- package/lib/generated-identity.js.map +0 -1
- package/lib/generated-keyart.d.ts.map +0 -1
- package/lib/generated-keyart.js.map +0 -1
- package/lib/generated-loading-assets.d.ts.map +0 -1
- package/lib/generated-loading-assets.js.map +0 -1
- package/lib/generated-sidebar-assets.d.ts +0 -4
- package/lib/generated-sidebar-assets.d.ts.map +0 -1
- package/lib/generated-sidebar-assets.js +0 -4
- package/lib/generated-sidebar-assets.js.map +0 -1
- package/lib/heixiu-interactions.d.ts.map +0 -1
- package/lib/heixiu-interactions.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/loading-heixiu.d.ts.map +0 -1
- package/lib/loading-heixiu.js.map +0 -1
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js.map +0 -1
- package/lib/portal.d.ts.map +0 -1
- package/lib/portal.js.map +0 -1
- package/lib/reactions.d.ts.map +0 -1
- package/lib/reactions.js.map +0 -1
- package/lib/scene/heixiu-feedback.d.ts.map +0 -1
- package/lib/scene/heixiu-feedback.js.map +0 -1
- package/lib/scene/heixiu-interactions.d.ts.map +0 -1
- package/lib/scene/heixiu-interactions.js.map +0 -1
- package/lib/scene/state-transitions.d.ts.map +0 -1
- package/lib/scene/state-transitions.js.map +0 -1
- package/lib/scene.d.ts.map +0 -1
- package/lib/scene.js.map +0 -1
- package/lib/sidebar-glass.d.ts.map +0 -1
- package/lib/sidebar-glass.js.map +0 -1
- package/lib/sidebar-heixiu-escape.d.ts.map +0 -1
- package/lib/sidebar-heixiu-escape.js.map +0 -1
- package/lib/sidebar-heixiu-roaming.d.ts.map +0 -1
- package/lib/sidebar-heixiu-roaming.js.map +0 -1
- package/lib/state.d.ts.map +0 -1
- package/lib/state.js.map +0 -1
- package/lib/theme.d.ts.map +0 -1
- package/lib/theme.js.map +0 -1
|
@@ -0,0 +1,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
|