@norbert_bodziony/bloub 0.2.0

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.
@@ -0,0 +1,21 @@
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;
@@ -0,0 +1,7 @@
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;
@@ -0,0 +1,29 @@
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;
@@ -0,0 +1,79 @@
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;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Formes et couleurs proposees par le personnalisateur du bot.
3
+ *
4
+ * A la difference des silhouettes d'animation (`profiles.ts`), celles-ci ne sont
5
+ * PAS relevees sur la video : elles sont construites analytiquement d'apres la
6
+ * grille du personnalisateur d'origine. Deux sources distinctes, donc, et c'est
7
+ * volontaire — les etats animes doivent rester fideles a la video, les formes de
8
+ * base sont un choix d'utilisateur.
9
+ */
10
+ /**
11
+ * Les identifiants sont enumeres plutot que deduits du tableau : c'est ce qui
12
+ * permet a la couche i18n de verifier A LA COMPILATION que chaque forme a bien
13
+ * sa traduction dans les trois langues (`t(\`shapes.${id}\`)` ne compile que si
14
+ * la cle existe). Un `as const` sur le tableau aurait le meme effet mais
15
+ * rendrait `radii` en lecture seule, alors que le moteur le passe tel quel.
16
+ */
17
+ export type ShapeId = 'cercle' | 'galet' | 'squircle' | 'capsule' | 'triangle' | 'hexagone' | 'nuage' | 'goutte';
18
+ export interface BotShape {
19
+ id: ShapeId;
20
+ radii: number[];
21
+ }
22
+ export declare const SHAPES: BotShape[];
23
+ export declare const SHAPE_BY_ID: Map<string, BotShape>;
24
+ export declare const DEFAULT_SHAPE = "cercle";
25
+ export type ColorId = 'encre' | 'creme' | 'brun' | 'rouge' | 'orange' | 'ambre' | 'vert' | 'turquoise' | 'bleu' | 'violet' | 'rose' | 'gris';
26
+ export interface BotColor {
27
+ id: ColorId;
28
+ hex: string;
29
+ }
30
+ /** Palette du personnalisateur d'origine. */
31
+ export declare const COLORS: BotColor[];
32
+ export declare const COLOR_BY_ID: Map<string, BotColor>;
33
+ export declare const DEFAULT_COLOR = "encre";
34
+ /** Melange deux couleurs hex. Sert a la brume de profondeur des particules. */
35
+ export declare function mixHex(from: string, to: string, t: number): string;
@@ -0,0 +1,86 @@
1
+ import { type ArcSpec, type DotRender } from './decor.js';
2
+ import { type HeadGaze } from './face.js';
3
+ import { type Silhouette } from './shape.js';
4
+ export interface EyeCfg {
5
+ /** largeur locale (axe court de la gelule), en unites de rayon de boule */
6
+ w: number;
7
+ /** hauteur locale (axe long) */
8
+ h: number;
9
+ /** 1 = ouvert, 0 = ferme */
10
+ open: number;
11
+ /**
12
+ * Inclinaison propre de la gelule, en degres, positif = le haut part a
13
+ * droite. Appliquee APRES le repere tangent de la sphere. Sans elle, les deux
14
+ * yeux penchent forcement du meme cote (le roulis de tete) et la colere comme
15
+ * la tristesse, qui demandent des inclinaisons en miroir, sont hors de portee.
16
+ */
17
+ tilt?: number;
18
+ }
19
+ export interface Pose {
20
+ /** silhouette du corps, en unites de rayon de boule */
21
+ sil: Silhouette;
22
+ /** decalage global du corps ET des yeux */
23
+ offX: number;
24
+ offY: number;
25
+ gaze: HeadGaze;
26
+ /** demi-ecart des yeux sur la sphere, en degres */
27
+ split: number;
28
+ /** [oeil interieur, oeil exterieur] */
29
+ eyes: [EyeCfg, EyeCfg];
30
+ /** opacite des yeux : sert aux etats sans visage */
31
+ eyeAlpha: number;
32
+ bodyAlpha: number;
33
+ dots: DotRender[];
34
+ arcs: ArcSpec[];
35
+ notif: {
36
+ x: number;
37
+ y: number;
38
+ r: number;
39
+ notch: number;
40
+ } | null;
41
+ /** true = le decor passe derriere le corps (particules de l'eclatement) */
42
+ dotsBehind: boolean;
43
+ }
44
+ export type StateId = 'idle' | 'thinking' | 'wink' | 'wide' | 'alert' | 'notify' | 'exclaim' | 'sleep' | 'egg' | 'hexagon' | 'play' | 'orbit' | 'burst' | 'comet'
45
+ /** transition d'interface, pas une animation du catalogue : hors `SEQUENCE` */
46
+ | 'swirl';
47
+ export interface StateDef {
48
+ id: StateId;
49
+ /** duree de maintien quand la sequence complete est jouee */
50
+ duration: number;
51
+ /**
52
+ * duree en dessous de laquelle l'animation est coupee avant d'aboutir : le
53
+ * "!" ne revient pas, le corps reste eclate. Elle se lit dans les constantes
54
+ * de `pose` ci-dessous, elle ne se choisit pas. Absente = l'etat ignore le
55
+ * temps ou boucle, n'importe quelle duree lui va (voir `MIN_BLOCK`).
56
+ */
57
+ minDuration?: number;
58
+ /** duree du morph d'entree */
59
+ morph: number;
60
+ /** true = l'entree est masquee par un clignement, comme dans la video */
61
+ blinkIn: boolean;
62
+ /**
63
+ * true = le corps est la silhouette "au repos", donc remplacable par la forme
64
+ * choisie dans le personnalisateur. Les etats qui dessinent leur propre forme
65
+ * (le "!", les points, l'oeuf, le triangle...) valent false : c'est cette forme
66
+ * la qui EST l'animation.
67
+ */
68
+ baseBody: boolean;
69
+ /**
70
+ * true = l'etat porte le visage "au repos", donc remplacable par l'expression
71
+ * choisie. Seul `idle` : les autres etats a visage ont une expression relevee
72
+ * sur la video, c'est precisement ce qu'on reproduit.
73
+ */
74
+ baseFace: boolean;
75
+ pose(local: number): Pose;
76
+ }
77
+ export declare const STATES: StateDef[];
78
+ export declare const STATE_BY_ID: Map<StateId, StateDef>;
79
+ /** Ordre de lecture de la sequence complete, calque sur la video de reference. */
80
+ /**
81
+ * Date, en temps local, ou chaque etat est le plus lisible : c'est la pose que
82
+ * montrent les vignettes et la planche. Rendu deterministe, donc comparable
83
+ * d'une execution a l'autre. Le type force a couvrir tout nouvel etat.
84
+ */
85
+ export declare const POSES: Record<StateId, number>;
86
+ export declare const SEQUENCE: StateId[];
@@ -0,0 +1,33 @@
1
+ import { type JSX } from 'solid-js';
2
+ import { type GazeScript } from '../ui/gaze.js';
3
+ import { type Block } from '../bot/cycles.js';
4
+ import { type StateId } from '../bot/states.js';
5
+ export interface BloubBotRef {
6
+ readonly svg: SVGSVGElement;
7
+ seek(index: number, offset?: number): void;
8
+ renderAt(seconds: number): void;
9
+ }
10
+ export interface BloubBotProps {
11
+ size?: number;
12
+ shape?: string;
13
+ color?: string;
14
+ expression?: string;
15
+ paper?: string;
16
+ ariaLabel?: string;
17
+ frozenAt?: number;
18
+ cycle?: Block[];
19
+ follow?: boolean;
20
+ gaze?: GazeScript | null;
21
+ block?: number;
22
+ state?: StateId;
23
+ playing?: boolean;
24
+ elapsed?: number;
25
+ onBlockChange?: (value: number) => void;
26
+ onStateChange?: (value: StateId) => void;
27
+ onPlayingChange?: (value: boolean) => void;
28
+ onElapsedChange?: (value: number) => void;
29
+ ref?: (value: BloubBotRef) => void;
30
+ class?: string;
31
+ style?: JSX.CSSProperties | string;
32
+ }
33
+ export default function BloubBot(rawProps: BloubBotProps): JSX.Element;
@@ -0,0 +1,10 @@
1
+ export { default as BloubBot } from './components/BloubBot.js';
2
+ export type { BloubBotProps, BloubBotRef } from './components/BloubBot.js';
3
+ export * from './bot/engine.js';
4
+ export * from './bot/cycles.js';
5
+ export * from './bot/states.js';
6
+ export * from './bot/skins.js';
7
+ export * from './bot/expressions.js';
8
+ export { RAYON, DEMI_VIEWBOX } from './bot/repere.js';
9
+ export type { GazeScript, Aim } from './ui/gaze.js';
10
+ export { lookTarget, tourLook, TOUR_TIME, TURN_TIME } from './ui/gaze.js';
@@ -0,0 +1,130 @@
1
+ import type { Look } from '../bot/engine.js';
2
+ import type { ExpressionId } from '../bot/expressions.js';
3
+ /**
4
+ * Ou le bot regarde quand il suit le curseur. Pur, comme `src/ui/timeline.ts` :
5
+ * la position du pointeur entre en coordonnees deja normalisees, donc la regle
6
+ * se teste sans DOM — et elle a besoin de l'etre, parce que deux signes s'y
7
+ * trompent facilement.
8
+ */
9
+ /**
10
+ * Angles en degres d'orientation de tete. CHOISIS, pas releves : la video de
11
+ * reference ne montre aucun suivi de curseur. Assez amples pour se distinguer de
12
+ * la derive au repos (±7deg de lacet, ±5,5 de tangage), assez retenus pour
13
+ * qu'aucun oeil ne parte derriere le limbe de la sphere.
14
+ */
15
+ export declare const YAW_MAX = 16;
16
+ export declare const PITCH_MAX = 13;
17
+ /**
18
+ * Hauteur a laquelle le regard se tient, curseur au centre. CHOISIE : legerement
19
+ * au-dessus de l'equateur, ce qui donne un bot attentif plutot qu'absent.
20
+ *
21
+ * C'est une valeur ABSOLUE, et c'est tout le point : en relatif, la hauteur des
22
+ * yeux suivait celle de chaque expression, et comme « neutre » regarde a
23
+ * +28,6deg quand les humeurs sont entre -9 et +9, les yeux tombaient d'un coup
24
+ * au premier changement d'humeur.
25
+ */
26
+ export declare const PITCH = 10;
27
+ /**
28
+ * Direction ou la tete se pose dans la vue des reglages : le bot cesse de regarder en haut a
29
+ * droite (sa pose de repos) pour regarder a GAUCHE, du cote du panneau.
30
+ *
31
+ * Ce n'est pas un miroir de l'image : les yeux font vraiment le tour de la
32
+ * sphere, donc ils gardent leur inclinaison en `\\` et leur compression de
33
+ * profondeur. Retourner l'image les aurait couches en `//`.
34
+ */
35
+ export declare const TURN = 26;
36
+ /**
37
+ * Tour complet parcouru EN CHEMIN : les yeux ne glissent pas en travers du
38
+ * visage, ils font le tour de la boule avant d'arriver.
39
+ *
40
+ * C'est gratuit parce que les yeux vivent sur une sphere : passe 90deg de lacet
41
+ * ils franchissent le limbe, le moteur les retire de l'image, puis ils
42
+ * reapparaissent de l'autre cote. Le tourbillon n'est donc pas un effet pose
43
+ * par-dessus, c'est la meme projection orthographique poussee d'un tour.
44
+ *
45
+ * Et surtout : il ATTERRIT JUSTE par construction, `-360deg` etant le meme angle
46
+ * que `0`. C'est ce qui le distingue d'une pose de regard ecrite dans un etat,
47
+ * qui laisse les yeux la ou sa courbe se termine.
48
+ */
49
+ export declare const SPIN = 360;
50
+ /**
51
+ * Duree du tour. Un peu plus courte que le bloc d'entree (`swirl`) : les yeux
52
+ * doivent etre poses a gauche avant que les anneaux ne s'effacent.
53
+ */
54
+ export declare const TURN_TIME = 1.1;
55
+ /**
56
+ * Humeurs que le bot traverse pendant qu'il suit le curseur.
57
+ *
58
+ * Toutes ont un ROULIS NUL, et c'est le critere de selection. Le lacet et le
59
+ * tangage sont neutralises par le suivi (ils sont absolus), mais pas le roulis —
60
+ * il fait pencher la tete, donc il deplace les yeux verticalement, et une humeur
61
+ * a -15deg suivie d'une a +8 les fait sauter. Ce qui reste pour distinguer les
62
+ * humeurs, c'est la FORME des yeux : ronds, plisses, ecarquilles, aplatis. C'est
63
+ * largement suffisant, et c'est ce qui se lit.
64
+ *
65
+ * Ce n'est donc pas une liste de gouts : y ajouter « curieux » (roulis -15deg)
66
+ * ramenerait le saut.
67
+ */
68
+ export declare const HUMEURS: readonly ExpressionId[];
69
+ /**
70
+ * Un regard SCRIPTE : evalue a chaque image avec le temps ecoule depuis le debut
71
+ * de l'arrivee, en secondes. Le script porte donc sa propre horloge et peut
72
+ * enchainer plusieurs mouvements — le composant ne fait que l'evaluer et n'a
73
+ * aucune duree a connaitre.
74
+ *
75
+ * REGLE, et c'est elle qui rend un script sans entretien : il doit FINIR a
76
+ * `mix: 0`, ou la pose de l'etat commande seule. Il n'y a alors jamais rien a
77
+ * relacher, et ce relachement — qui se verrait sous la forme d'un dernier
78
+ * glissement des yeux, juste quand tout devrait etre pose — n'existe pas.
79
+ *
80
+ * Le type reste general alors qu'il n'y a qu'un script aujourd'hui : quatre ont
81
+ * ete ecrits et compares cote a cote avant d'en garder un, et c'est cette forme
82
+ * qui a permis de les essayer sans toucher au moteur.
83
+ */
84
+ export type GazeScript = (t: number) => Look;
85
+ /**
86
+ * « Le tour » : la boule a l'air de tourner sur elle-meme.
87
+ *
88
+ * `mix` reste a ZERO du debut a la fin : aucune direction n'est imposee, seul
89
+ * `spin` fond, ce qui fait passer les yeux DERRIERE la boule avant de les
90
+ * ramener exactement la ou l'expression choisie les met.
91
+ *
92
+ * Ease-in-OUT et non l'ease-out exponentiel du reste du projet : ce n'est pas une
93
+ * valeur qui se pose, c'est un objet qui tourne. En ease-out, les deux tiers du
94
+ * tour etaient avales en 0,3 s — un a-coup, pas une rotation.
95
+ *
96
+ * Il est VIF au passage du limbe — 20 px entre deux images sur une boule de 100
97
+ * de rayon — et ce n'est pas un defaut de reglage : pres du bord, un petit angle
98
+ * devient un grand deplacement a l'ecran, et l'oeil disparait puis reparait de
99
+ * l'autre cote. Ralentir n'y change rien, c'est la trajectoire qui veut ca, et
100
+ * c'est justement ce qui fait l'effet. Ne pas chercher a l'adoucir.
101
+ *
102
+ * Corollaire indispensable : ce tour ne se joue que sur un CERCLE. Les yeux sont
103
+ * recolles au contour reel (`radiusAtAngle`), donc sur une forme non circulaire
104
+ * ils suivent le profil en tournant et sautillent. Voir `forme` dans `App.vue`.
105
+ */
106
+ export declare const TOUR_TIME = 1.5;
107
+ export declare const tourLook: GazeScript;
108
+ export interface Aim {
109
+ /** ecart horizontal du pointeur au centre du bot, -1 a 1 (droite positive) */
110
+ nx: number;
111
+ /** ecart vertical, -1 a 1, dans le sens de l'ecran (bas positif) */
112
+ ny: number;
113
+ /** avancement de l'arrivee, 0 a 1 */
114
+ tour: number;
115
+ /** false = aucun pointeur connu : la tete reste tournee, mais elle revit */
116
+ pointer: boolean;
117
+ }
118
+ /**
119
+ * Cible de regard.
120
+ *
121
+ * `tour` mene tout : il fait monter l'emprise sur la pose (`mix`) et fondre le
122
+ * tour parcouru (`spin`) en meme temps. A 0 la pose de l'etat commande seule ; a
123
+ * 1 la tete est posee a gauche et suit le curseur.
124
+ *
125
+ * Rien ici ne compense l'expression affichee : c'est le moteur qui melange,
126
+ * parce que lui seul connait la pose a l'instant t. Le faire ici obligerait a
127
+ * lire le lacet d'ARRIVEE de l'expression pendant que le moteur, lui, morphe
128
+ * encore — et les yeux sautaient a chaque changement d'humeur.
129
+ */
130
+ export declare function lookTarget({ nx, ny, tour, pointer }: Aim): Look;
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@norbert_bodziony/bloub",
3
+ "private": false,
4
+ "version": "0.2.0",
5
+ "type": "module",
6
+ "description": "SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.",
7
+ "license": "MIT",
8
+ "author": "Jérémy Perret",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/NorbertBodziony/bloub.git"
12
+ },
13
+ "homepage": "https://bloub-app.pages.dev",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "svg",
19
+ "animation",
20
+ "avatar",
21
+ "solid-js",
22
+ "morphing"
23
+ ],
24
+ "packageManager": "bun@1.4.0",
25
+ "files": [
26
+ "dist/package/index.js",
27
+ "dist/package/server.js",
28
+ "dist/package/types"
29
+ ],
30
+ "types": "./dist/package/types/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/package/types/index.d.ts",
34
+ "browser": "./dist/package/index.js",
35
+ "import": "./dist/package/server.js",
36
+ "default": "./dist/package/server.js"
37
+ }
38
+ },
39
+ "sideEffects": false,
40
+ "scripts": {
41
+ "dev": "vite",
42
+ "typecheck": "tsc --noEmit",
43
+ "build": "bun run typecheck && bun run build:app && bun run build:package",
44
+ "build:app": "vite build",
45
+ "build:package": "vite build --config vite.lib.config.ts && vite build --config vite.lib.ssr.config.ts && tsc -p tsconfig.lib.json",
46
+ "preview": "vite preview",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:package": "bun run build:package && tsc -p tsconfig.package-test.json && bun tests/package-runtime.ts",
50
+ "test:tarball": "bun tools/test-package-tarball.mjs",
51
+ "pack:check": "bun run test:package && bun run test:tarball",
52
+ "prepublishOnly": "bun run test && bun run typecheck && bun run pack:check"
53
+ },
54
+ "peerDependencies": {
55
+ "solid-js": "^1.9.15"
56
+ },
57
+ "devDependencies": {
58
+ "@tailwindcss/vite": "^4.3.3",
59
+ "@types/node": "^26.0.0",
60
+ "happy-dom": "^20.11.2",
61
+ "mediabunny": "^1.54.0",
62
+ "solid-js": "^1.9.15",
63
+ "tailwindcss": "^4.3.3",
64
+ "typescript": "^5.9.3",
65
+ "vite": "^8.2.1",
66
+ "vite-plugin-solid": "^2.11.14",
67
+ "vitest": "^4.1.10"
68
+ }
69
+ }