@fabi-play/avatars 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,76 @@
1
+ Fabi Play Kit License
2
+ Version 1.0 (DRAFT, not yet reviewed by a lawyer)
3
+
4
+ Copyright (c) 2026 Fabi Play. All rights reserved.
5
+
6
+ This license applies to the Fabi Play Kit: the packages @fabi-play/protocol, @fabi-play/motion,
7
+ @fabi-play/avatars, @fabi-play/sdk and @fabi-play/engine, in source and built form, as published on npm, served
8
+ from https://cdn.fabiplay.app or found in this repository, including their code, type declarations, documentation,
9
+ avatar artwork and other assets (together, the "Kit").
10
+
11
+ 1. Free of charge, for Fabi Play only
12
+
13
+ Fabi Play grants you a worldwide, non-exclusive, non-transferable, non-sublicensable, royalty-free license to
14
+ use, copy, and modify the Kit, and to distribute it as part of a Permitted Work, solely to make, test, run and
15
+ distribute Permitted Works.
16
+
17
+ A "Permitted Work" is a game, tool, or other software made to run on, be published on, or support the
18
+ development of games for the Fabi Play platform (https://fabiplay.app), including games submitted to Fabi Play
19
+ and tools used to build, test or preview them.
20
+
21
+ 2. What is not allowed
22
+
23
+ You may not, without Fabi Play's prior written permission:
24
+
25
+ a. use the Kit, or any part or modified version of it, in software that is not a Permitted Work, including in
26
+ any other game platform, product or service;
27
+ b. distribute the Kit, or a modified version of it, on its own or as a library, SDK, engine or framework for
28
+ others to build with, other than as part of a Permitted Work (installing it from npm or loading it from
29
+ https://cdn.fabiplay.app is not a distribution by you);
30
+ c. sell, rent or sublicense the Kit itself;
31
+ d. remove or alter this license, copyright notices, or the license banners in the built files;
32
+ e. use the avatars, the mascot "Fabi", or the name, logos or other trademarks of Fabi Play, except as the Kit
33
+ does when it runs inside a Permitted Work. This license grants no trademark rights;
34
+ f. use the Kit to circumvent, interfere with or misrepresent the Fabi Play platform, its safety, privacy or
35
+ parental controls, or its rules for games.
36
+
37
+ 3. Visible, not open source
38
+
39
+ The Kit's code is published so that developers can read it, debug with it and build Permitted Works. Being
40
+ visible does not make it open source; no rights are granted beyond those stated here.
41
+
42
+ 4. Your works
43
+
44
+ You keep all rights in your own code and content. This license does not give Fabi Play any rights in your
45
+ Permitted Works; publishing a game on Fabi Play is governed by Fabi Play's separate developer terms.
46
+
47
+ 5. Feedback
48
+
49
+ If you send Fabi Play suggestions or changes for the Kit, Fabi Play may use them without obligation to you.
50
+
51
+ 6. Termination
52
+
53
+ This license ends automatically if you break its terms. If you fix the breach within 30 days of becoming aware
54
+ of it, the license is reinstated, unless Fabi Play has already told you in writing that it is terminated. On
55
+ termination you must stop using and distributing the Kit. Permitted Works already distributed to end users in
56
+ compliance with this license may continue to be used by those end users.
57
+
58
+ 7. No warranty
59
+
60
+ THE KIT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
61
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
62
+
63
+ 8. Limitation of liability
64
+
65
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL FABI PLAY OR ITS CONTRIBUTORS BE LIABLE FOR ANY CLAIM,
66
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
67
+ CONNECTION WITH THE KIT OR THE USE OR OTHER DEALINGS IN THE KIT.
68
+
69
+ 9. Changes to this license
70
+
71
+ Fabi Play may publish new versions of this license. Each version of the Kit is governed by the license version
72
+ it is distributed with, unless you choose to accept a later version.
73
+
74
+ 10. Contact
75
+
76
+ For permissions beyond this license: https://fabiplay.app
package/README.md CHANGED
@@ -1,3 +1,133 @@
1
- # Temporary Holding Version
1
+ # @fabi-play/avatars
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Thirty-seven original, child-friendly avatars, drawn from a body pose: Fabi the fox (the fox of the app icon, the
4
+ mascot), offered in every age band, and 12 for each band. The TV shows them in place
5
+ of the plain silhouette when the phone's video is not on screen. Each child picks one on the phone, among the avatars
6
+ of their age band. The phone stores only the avatar's id; any valid id is accepted everywhere, whatever the band.
7
+
8
+ All characters are original. None is an existing character or a real person. The people of 9–12 have generic faces,
9
+ varied skin tones and gender-neutral looks, and are told apart by their gear.
10
+
11
+ | Band | Style | id · en · it (the first four are the slot defaults) |
12
+ | --- | --- | --- |
13
+ | all | the app icon's fox: first in every band's list, `special: true`, drawn with the 6–8 proportions; never a slot default | `mascot` Fabi · Fabi |
14
+ | 3–5 | cuddly baby animals: big round heads, chubby limbs, soft colours, a soft brown outline | `bunny` Bunny · Coniglietto, `bear` Bear · Orsetto, `panda` Panda · Panda, `penguin` Penguin · Pinguino, `koala` Koala · Koala, `frog` Frog · Ranocchio, `cat` Cat · Gattino, `owl` Owl · Gufetto, `chick` Chick · Pulcino, `puppy` Puppy · Cucciolo, `lamb` Lamb · Agnellino, `duckling` Duckling · Anatroccolo |
15
+ | 6–8 | animals and adventure | `fox` Fox · Volpe, `lion` Lion · Leoncino, `dragon` Little Dragon · Draghetto, `unicorn` Unicorn · Unicorno, `tiger` Tiger · Tigrotto, `monkey` Monkey · Scimmietta, `dinosaur` Dinosaur · Dinosauro, `octopus` Octopus · Polpetto, `ghost` Friendly Ghost · Fantasmino, `shark` Shark · Squalo, `wolf` Wolf · Lupo, `parrot` Parrot · Pappagallo |
16
+ | 9–12 | older kids: smaller heads, slimmer longer limbs, gear, cooler colours, no blush, a cool outline | `robot` Robot · Robot, `astronaut` Astronaut · Astronauta, `ninja` Ninja · Ninja, `skater` Skater · Skater, `alien` Alien · Alieno, `explorer` Explorer · Giramondo, `wizard` Wizard · Mago, `knight` Knight · Cavaliere, `racer` Racer · Pilota, `pirate` Pirate · Pirata, `diver` Diver · Sub, `hero` Hero · Supereroe |
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @fabi-play/avatars
22
+ ```
23
+
24
+ ESM only, types included; depends on `@fabi-play/protocol`. Games get the avatars from `@fabi-play/sdk`
25
+ (`play.drawAvatar`) and need not install this package.
26
+
27
+ ## API
28
+
29
+ ```ts
30
+ import { drawAvatar, drawAvatarIcon, avatarIconSvg, AVATARS, avatarById, isAvatarId } from '@fabi-play/avatars'
31
+
32
+ // On the TV or in a game, every frame (box: where player space 0–1 lands, e.g. cameraBox(...)):
33
+ drawAvatar(ctx, body, player.avatar ?? 'fox', box, { alpha: 0.9 })
34
+
35
+ // A picker on a canvas:
36
+ drawAvatarIcon(ctx, 'panda', x, y, 96)
37
+
38
+ // A picker on the phone, without a canvas:
39
+ el.innerHTML = avatarIconSvg('panda', 96) // web DOM
40
+ <SvgXml xml={avatarIconSvg('panda', 96)} /> // React Native, react-native-svg
41
+ <img src={avatarIconDataUri('panda', 96)} /> // data URI
42
+ ```
43
+
44
+ - `AVATARS: readonly AvatarDef[]` (all 37), `AVATAR_IDS`, `DEFAULT_AVATAR` (the fox), `avatarById(id)`, `isAvatarId(x)`.
45
+ - `AVATAR_BANDS: Record<AgeRange, readonly AvatarId[]>` (picker order: the mascot, `MASCOT_ID`, first, then the
46
+ band's own 12, whose first four are the slot defaults, see `defaultAvatarFor(slot, ageRange?)` in
47
+ `@fabi-play/protocol`), `avatarsFor(ageRange)` (the definitions, in that order), `bandOf(id)` (`'all'` for the
48
+ mascot, 6–8 for an unknown id). Every definition has its `band` (`AvatarBand`: an age range or `'all'`);
49
+ `special: true` marks the mascot, so a picker can mark it. `bodyBand(def)` gives the proportions it is drawn with
50
+ (6–8 for `'all'`).
51
+ - `drawAvatar(ctx, body, def | id, box, { alpha?, mirrored? })`. Unknown ids draw the default avatar. Pass
52
+ `mirrored` when the body is in camera space rather than player space. It draws nothing when the shoulders are not
53
+ seen (it returns whether it drew), and it never throws on missing or broken points.
54
+ - `drawAvatarIcon(ctx, def | id, x, y, size)`: a head-and-shoulders portrait in a neutral pose, clipped to its square.
55
+ - `avatarIconSvg(def | id, size, { background?, radius? })`: the same portrait as an SVG document string. Its
56
+ clip-path id is unique per call (avatar, size and a counter), so many portraits can sit inline in one page.
57
+ `avatarIconDataUri` returns the same as a data URI.
58
+ - `smoothBody(prev, next, amount = 0.5)`: optional per-player smoothing. Points ease towards the new frame. A point
59
+ that was seen stays visible until its visibility drops below 0.3, so limbs don't blink. Keep the result per player.
60
+ - Poses for previews and tests: `neutralPose()`, `armsUpPose()`, `starJumpPose()`, `squatPose()`, `wavePose()`,
61
+ `TEST_POSES`, `poseBody(spec)`.
62
+ - Lower level: `avatarShapes(body, def, box)` returns the shape list, `drawShapes(ctx, shapes)` draws it on a canvas,
63
+ and `shapesToSvg(shapes)` turns it into SVG. `frameOf(body, box)` gives the rig's torso frame.
64
+
65
+ ## How it is drawn
66
+
67
+ One source of truth: the rig and the portrait build a small **shape list** (`circle`, `ellipse`, `rect`, `path`,
68
+ `line`, `group` with translate/rotate/scale). Two small backends read it: a canvas backend and an SVG backend.
69
+ Because both read the same list, canvas and SVG pictures match. Paths use only absolute `M L Q C Z`, so the canvas
70
+ backend does not need `Path2D`. The rig builds them as number arrays (`PathData`: a `PATH` command code, then its
71
+ coordinates, rounded to 2 decimals as the SVG prints them), so nothing is turned into a string and parsed back every
72
+ frame; `pathToString` prints one. A head depends only on the avatar and the outline width, so it is built once and
73
+ shared across frames (treat shape lists as read-only). `test/snapshot.json` holds a hash of every avatar's pictures:
74
+ a change that alters them by accident fails the tests (`UPDATE_SNAPSHOT=1` after one meant to).
75
+
76
+ ### Rig rules (`src/rig.ts`)
77
+
78
+ - **Unit U**: the torso length (shoulder midpoint to hip midpoint). It is at least 1.15 × the shoulder width, so
79
+ bending forward does not shrink the avatar. Every size is a multiple of U, so avatars scale with distance.
80
+ - **Torso**: a smooth rounded shape along the torso axis (square to the shoulder line when shoulders to hips is under
81
+ 0.25 × the shoulder width: a child bent towards the camera). It runs from just above the shoulders to just below the
82
+ hips, and is at least 0.68 U wide at the shoulders and 0.8 U wide at the hips. A belly patch sits on top.
83
+ - **Limbs**: thick round strokes from shoulder to elbow to wrist (0.22 U) and from hip to knee to ankle (0.28 U),
84
+ each with an outline. Arms start just below the shoulder. Legs start inside the torso, so nothing looks detached.
85
+ Hands are round caps. Feet are ovals, flippers or boots.
86
+ - **Proportions by band** (`BAND_BODY`): the joints are always the child's, only sizes change. An avatar of every
87
+ band (`'all'`, the mascot) takes the 6–8 row.
88
+
89
+ | band | head radius | head centre above the shoulders | arm / leg thickness | hand radius | torso below the hips | torso min half width (shoulders / hips) |
90
+ | --- | --- | --- | --- | --- | --- | --- |
91
+ | 3–5 | 0.75 U | 0.74 head radii | 0.30 / 0.36 U | 0.19 U | 0.26 U | 0.40 / 0.50 U, rounder tummy |
92
+ | 6–8 | 0.6 U | 0.78 | 0.22 / 0.28 U | 0.15 U | 0.14 U | 0.34 / 0.40 U |
93
+ | 9–12 | 0.5 U | 0.92 | 0.17 / 0.21 U | 0.125 U | 0.10 U | 0.36 / 0.32 U, slimmer hips |
94
+
95
+ In 3–5 an arm raised well above the shoulders is drawn over the big head, which would hide it.
96
+ - **Head**: radius by band (above), centred above the shoulder midpoint along the torso axis. It follows the
97
+ nose a little (at most 0.35 radii) and tilts with the ear line, or the eye line if the ears are missing. The tilt is
98
+ clamped to ±0.5 rad from the torso, and a swapped pair is treated as a child facing away.
99
+ - **Tail, wings, backpack, tentacles**: these hang off the hips or the back, in torso coordinates, and are drawn
100
+ behind the body. The tail goes on the child's right. Portraits leave it out.
101
+ - **Draw order**: back extras, legs, torso, arms, head, then (3–5 only) raised arms.
102
+ - **Degrading**: a point counts as seen at visibility ≥ 0.5. If the elbow or knee is missing, that limb is hidden.
103
+ If the wrist or ankle is missing, the limb ends at the elbow or knee. If the hips are missing, the torso is one U
104
+ long and the legs are hidden. If either shoulder is missing, nothing is drawn.
105
+
106
+ ## Adding an avatar
107
+
108
+ 1. Add an entry to `AVATARS` in `src/defs.ts` and its id to its band in `AVATAR_BANDS` (`src/index.ts`) (an avatar
109
+ of band `'all'` goes in every band, after the mascot). Choose a new
110
+ kebab-case `id` and never rename it later, because phones store it. Fill in `name.en`, `name.it` and `band`. Pick a `palette` with `body`, `limbs`, `belly`, `accent` and
111
+ `outline`, plus optional `hands`, `feet` and `ears`. Then choose a `head` shape, `ears`, the `face` (eyes, mouth,
112
+ muzzle, nose, cheeks), a `tail`, `headExtras` and `bodyExtras`. `extraColors` covers stripes, spots, manes and
113
+ lights.
114
+ 2. Make it different from the others by its silhouette (ears, head, tail, extras) **and** its body colour. Use a
115
+ body colour that reads on both the night background (#1f1813) and a light one. Keep the dark outline.
116
+ 3. If a part does not exist yet, add a kind to the union in `src/types.ts` and draw it in `src/head.ts` (head units:
117
+ radius 1, y down) or in `src/rig.ts` (torso units of U). Don't write per-id special cases.
118
+ 4. Run `pnpm -F @fabi-play/avatars preview <dir> --details` and look at the PNGs: the portrait, every pose, and
119
+ both backgrounds. Then run `pnpm -F @fabi-play/avatars test`.
120
+
121
+ ## Scripts
122
+
123
+ - `pnpm -F @fabi-play/avatars typecheck` / `test`
124
+ - `pnpm -F @fabi-play/avatars preview [out-dir] [--details]` writes `contact-sheet.html`, grouped by age band. If
125
+ Chrome is installed, it also renders `contact-sheet.png`: every portrait (SVG and canvas side by side), and every
126
+ avatar in three poses on night and light backgrounds. `--details` adds `icons.png` and large
127
+ `detail-<band>-<n>.png` pages with six poses each, including one with missing points (`detail-all-1.png`: the
128
+ mascot, next to the 6–8 fox). The default output is `packages/avatars/preview/`, which git ignores.
129
+
130
+ ## License
131
+
132
+ Fabi Play Kit License: free to use, for making games and tools for Fabi Play only. The avatars and the mascot Fabi
133
+ are part of the Kit: see [LICENSE](./LICENSE), section 2e.
@@ -0,0 +1,425 @@
1
+ // @fabi-play/avatars: 37 child-friendly avatars: Fabi the fox in every age band and 12 per band, drawn from a body pose, on canvas or as SVG portraits for the pickers. See https://docs.fabiplay.app.
2
+ // Generated by scripts/build-lib.mjs (fabiplay-kit) from the sources: do not edit.
3
+
4
+ import { AgeRange, Body as Body$1 } from '@fabi-play/protocol';
5
+
6
+ /**
7
+ * A tiny shape list, the one source of truth for every avatar picture. The rig and the portrait build a list of
8
+ * shapes; two small backends draw it: on a 2D canvas (the TV, the games) and as an SVG string (the phones, the
9
+ * previews). Both read the same numbers, so the canvas and the SVG pictures match.
10
+ *
11
+ * Paths use a small subset of SVG path syntax, absolute commands only (M L Q C Z), so the canvas backend can trace
12
+ * them without Path2D (not available everywhere, e.g. React Native canvases or test mocks). The rig builds them as
13
+ * PathData, number arrays (no strings built and parsed again every frame); fixed paths may stay strings.
14
+ */
15
+ /** How an avatar shape is painted (CSS colours). */
16
+ export interface Paint {
17
+ /** Fill colour; none without it. */
18
+ fill?: string;
19
+ /** Outline colour; none without it. */
20
+ stroke?: string;
21
+ /** Stroke width, in the units of the shape (scaled by the groups around it). */
22
+ lw?: number;
23
+ /** 0–1, multiplied with the alpha around it. */
24
+ alpha?: number;
25
+ }
26
+ /** One shape of an avatar picture (the rig's output), in canvas units; k says which. */
27
+ export type Shape = (Paint & {
28
+ k: "circle";
29
+ x: number;
30
+ y: number;
31
+ r: number;
32
+ }) | (Paint & {
33
+ k: "ellipse";
34
+ x: number;
35
+ y: number;
36
+ rx: number;
37
+ ry: number;
38
+ rot?: number;
39
+ })
40
+ /** A rounded rectangle by its centre, rotated by rot radians around it. */
41
+ | (Paint & {
42
+ k: "rect";
43
+ x: number;
44
+ y: number;
45
+ w: number;
46
+ h: number;
47
+ r: number;
48
+ rot?: number;
49
+ }) | (Paint & {
50
+ k: "path";
51
+ d: string | PathData;
52
+ })
53
+ /** A polyline [x0, y0, x1, y1, …], stroked with round caps and joins. */
54
+ | (Paint & {
55
+ k: "line";
56
+ pts: number[];
57
+ })
58
+ /** Children drawn translated by (x, y), then rotated by rot radians, then scaled by s. */
59
+ | {
60
+ k: "group";
61
+ x: number;
62
+ y: number;
63
+ rot?: number;
64
+ s?: number;
65
+ alpha?: number;
66
+ items: Shape[];
67
+ };
68
+ /**
69
+ * A path as numbers: each command code (PATH.M, …) followed by its coordinates, absolute: M x y, L x y,
70
+ * Q cx cy x y, C c1x c1y c2x c2y x y, Z. Coordinates are rounded to 2 decimals, as the SVG prints them, so canvas and
71
+ * SVG pictures match.
72
+ */
73
+ export type PathData = readonly number[];
74
+ export declare const PATH: {
75
+ readonly M: 0;
76
+ readonly L: 1;
77
+ readonly Q: 2;
78
+ readonly C: 3;
79
+ readonly Z: 4;
80
+ };
81
+ /** The part of CanvasRenderingContext2D the backend uses (so tests can pass a mock). */
82
+ export type Ctx2D = Pick<CanvasRenderingContext2D, "save" | "restore" | "translate" | "rotate" | "scale" | "beginPath" | "closePath" | "moveTo" | "lineTo" | "quadraticCurveTo" | "bezierCurveTo" | "arc" | "ellipse" | "fill" | "stroke" | "rect" | "clip" | "globalAlpha" | "fillStyle" | "strokeStyle" | "lineWidth" | "lineCap" | "lineJoin">;
83
+ export declare function drawShapes(ctx: Ctx2D, shapes: readonly Shape[]): void;
84
+ /** PathData as SVG path syntax: "M1 2 L3 4 Q5 6 7 8 Z". */
85
+ export declare function pathToString(d: string | PathData): string;
86
+ /** The coordinates of a path, as x, y pairs in a flat list (control points included). */
87
+ export declare function pathPoints(d: string | PathData): number[];
88
+ export declare function shapesToSvg(shapes: readonly Shape[]): string;
89
+ export interface Pt {
90
+ x: number;
91
+ y: number;
92
+ }
93
+ /** A five-pointed star of radius 1. */
94
+ export declare const STAR = "M0 -1 L0.29 -0.4 L0.95 -0.31 L0.47 0.15 L0.59 0.81 L0 0.5 L-0.59 0.81 L-0.47 0.15 L-0.95 -0.31 L-0.29 -0.4 Z";
95
+ /** An avatar's colours (CSS). */
96
+ export interface Palette {
97
+ /** Head and torso. */
98
+ body: string;
99
+ /** Arms and legs. */
100
+ limbs: string;
101
+ /** Belly patch, muzzle, inner face. */
102
+ belly: string;
103
+ /** Details: inner ears, horns, beaks, tail tips, spikes. */
104
+ accent: string;
105
+ /** The outline around every part: dark, so the avatar reads on light and dark backgrounds. */
106
+ outline: string;
107
+ /** Hands; defaults to limbs. */
108
+ hands?: string;
109
+ /** Feet; defaults to limbs. */
110
+ feet?: string;
111
+ /** Solid ears of another colour (panda); by default ears are body-coloured with an accent inside. */
112
+ ears?: string;
113
+ /** The face and head when they differ from the torso (people, the lamb's face); defaults to body. */
114
+ skin?: string;
115
+ /** Hair (hair, curls, locks). */
116
+ hair?: string;
117
+ }
118
+ /** The head's outline. */
119
+ export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket";
120
+ /** The ears (or what is in their place: horns, antennae, a fin). */
121
+ export type EarKind = "none" | "pointy" | "round" | "big-round" | "long" | "side" | "tufts" | "antenna" | "antennae" | "horns" | "unicorn" | "floppy" | "droopy" | "fin" | "human";
122
+ /** How the eyes are drawn. */
123
+ export type EyeKind = "dot" | "big" | "mask" | "screen" | "almond" | "visor" | "calm" | "slit" | "goggle" | "bead";
124
+ /** How the mouth (or beak) is drawn. */
125
+ export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws";
126
+ /** The patch around the nose and mouth. */
127
+ export type MuzzleKind = "none" | "oval" | "heart" | "monkey" | "disc" | "cheeks" | "band";
128
+ /** The tail, behind the body. */
129
+ export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume";
130
+ /** Things on the head. */
131
+ export type HeadExtra = "mane" | "rainbow-mane" | "stripes" | "spikes" | "spots" | "curl" | "helmet" | "bolts" | "whiskers" | "tuft" | "brows" | "eggshell" | "wool" | "eye-spot" | "crest" | "forehead" | "gills" | "ruff" | "hair" | "curls" | "locks" | "ninja-hood" | "cap" | "safari-hat" | "wizard-hat" | "plume" | "stripe" | "bandana" | "eye-patch" | "dive-mask" | "domino";
132
+ /** Things on the body. */
133
+ export type BodyExtra = "wings" | "backpack" | "sheet" | "tentacles" | "panel" | "badge" | "joints" | "feathers" | "flippers" | "boots" | "scarf" | "spots" | "wool" | "robe" | "cape" | "fins" | "tank" | "straps" | "belt" | "stripes" | "pauldrons" | "emblem" | "pocket" | "racing-stripe" | "star";
134
+ /** An avatar's face. */
135
+ export interface FaceDef {
136
+ /** How the eyes are drawn. */
137
+ eyes: EyeKind;
138
+ /** Eye colour for big/screen/almond/visor eyes. */
139
+ eyeColor?: string;
140
+ /** Eyes higher than usual and bulging over the head (frog). */
141
+ eyesOnTop?: boolean;
142
+ /** Ring around the eyes (owl). */
143
+ eyeRing?: string;
144
+ /** How the mouth is drawn. */
145
+ mouth: MouthKind;
146
+ /** Nose colour; none when missing. */
147
+ nose?: string;
148
+ /** The patch around nose and mouth. */
149
+ muzzle: MuzzleKind;
150
+ /** Blush colour; none when missing. */
151
+ cheeks?: string;
152
+ /** Eyebrows of this colour (people): a little attitude. */
153
+ brows?: string;
154
+ }
155
+ /** The age band an avatar is for, or 'all': offered in every band (Fabi the fox, the app's mascot). */
156
+ export type AvatarBand = AgeRange | "all";
157
+ /** An avatar, as data: one renderer draws any definition. */
158
+ export interface AvatarDef {
159
+ /** Stable kebab-case id: stored on the phone and sent around, never renamed. */
160
+ id: string;
161
+ /** Its name, in English and Italian, for pickers. */
162
+ name: {
163
+ en: string;
164
+ it: string;
165
+ };
166
+ /**
167
+ * The age band the avatar is for: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in every
168
+ * band's list, drawn with the 6–8 proportions.
169
+ */
170
+ band: AvatarBand;
171
+ /** A special avatar (the mascot): first in every band's list; pickers may mark it. */
172
+ special?: boolean;
173
+ /** Its colours. */
174
+ palette: Palette;
175
+ /** The head's outline. */
176
+ head: HeadShape;
177
+ /** The ears, or what is in their place. */
178
+ ears: EarKind;
179
+ /** The face. */
180
+ face: FaceDef;
181
+ /** The tail. */
182
+ tail: TailKind;
183
+ /** Things on the head (manes, hats, masks). */
184
+ headExtras: readonly HeadExtra[];
185
+ /** Things on the body (wings, capes, belts). */
186
+ bodyExtras: readonly BodyExtra[];
187
+ /** Colours of the rainbow mane, of the stripes, of the spots: per-avatar extra colours. */
188
+ extraColors?: readonly string[];
189
+ }
190
+ /** Where player space (0–1) lands on the canvas. */
191
+ export interface Box {
192
+ /** Left edge, canvas units. */
193
+ x: number;
194
+ /** Top edge, canvas units. */
195
+ y: number;
196
+ /** Width, canvas units. */
197
+ w: number;
198
+ /** Height, canvas units. */
199
+ h: number;
200
+ }
201
+ export declare const AVATARS: readonly AvatarDef[];
202
+ export type XY = readonly [
203
+ number,
204
+ number
205
+ ];
206
+ export interface PoseSpec {
207
+ nose: XY;
208
+ leftShoulder: XY;
209
+ rightShoulder: XY;
210
+ leftElbow: XY;
211
+ rightElbow: XY;
212
+ leftWrist: XY;
213
+ rightWrist: XY;
214
+ leftHip: XY;
215
+ rightHip: XY;
216
+ leftKnee: XY;
217
+ rightKnee: XY;
218
+ leftAnkle: XY;
219
+ rightAnkle: XY;
220
+ /** Head tilt in radians (positive: clockwise on screen). */
221
+ tilt?: number;
222
+ }
223
+ /** A body from a pose spec: torso T tall (0–1 of the frame height), shoulders centred at (cx, cy). */
224
+ export declare function poseBody(spec: Partial<PoseSpec>, { cx, cy, T }?: {
225
+ cx?: number | undefined;
226
+ cy?: number | undefined;
227
+ T?: number | undefined;
228
+ }): Body$1;
229
+ /** Standing, arms relaxed: the pose of the portraits. */
230
+ export declare const neutralPose: () => Body$1;
231
+ /** Both arms straight up. */
232
+ export declare const armsUpPose: () => Body$1;
233
+ /** Mid star jump: arms and legs wide, the body up in the air. */
234
+ export declare const starJumpPose: () => Body$1;
235
+ /** A squat: hips down, knees out, arms forward (seen from the front, the hands come in front of the chest). */
236
+ export declare const squatPose: () => Body$1;
237
+ /** Waving with the right hand, head tilted. */
238
+ export declare const wavePose: () => Body$1;
239
+ export declare const TEST_POSES: {
240
+ readonly neutral: () => Body$1;
241
+ readonly armsUp: () => Body$1;
242
+ readonly starJump: () => Body$1;
243
+ readonly squat: () => Body$1;
244
+ readonly wave: () => Body$1;
245
+ };
246
+ export type TestPose = keyof typeof TEST_POSES;
247
+ export declare const VISIBLE = 0.5;
248
+ /** Face landmarks (MediaPipe pose) the head tilt is read from. */
249
+ export declare const FACE: {
250
+ readonly leftEye: 2;
251
+ readonly rightEye: 5;
252
+ readonly leftEar: 7;
253
+ readonly rightEar: 8;
254
+ };
255
+ /**
256
+ * Body proportions by age band, in torso units U (head, limbs, torso) unless said otherwise. The joints are always
257
+ * the child's: limbs run through them and the head sits on the shoulders; only sizes change.
258
+ *
259
+ * 3–5: a big round head, chubby limbs, a round tummy that reaches low (the legs look short): toddlers.
260
+ * 6–8: the original proportions.
261
+ * 9–12: a smaller head, slimmer limbs and hips, a squarer torso: older kids.
262
+ */
263
+ export interface BandBody {
264
+ /** Head radius. */
265
+ head: number;
266
+ /** How far the head centre sits above the shoulder line, in head radii. */
267
+ neck: number;
268
+ /** Limb thickness. */
269
+ arm: number;
270
+ leg: number;
271
+ /** Hand radius. */
272
+ hand: number;
273
+ /** Feet size, as a factor. */
274
+ foot: number;
275
+ /** The torso: at least this half width at the shoulders and the hips, and this much past the joints. */
276
+ shoulderMin: number;
277
+ shoulderPad: number;
278
+ hipMin: number;
279
+ hipPad: number;
280
+ /** How far the torso reaches below the hips. */
281
+ bottom: number;
282
+ /** The waist's bulge, as a share of the wider of shoulders and hips. */
283
+ bulge: number;
284
+ /** A raised arm is drawn over the head (a big head would hide it). */
285
+ armsOverHead: boolean;
286
+ }
287
+ export declare const BAND_BODY: Record<AgeRange, BandBody>;
288
+ /** The proportions an avatar is drawn with: its band's, 6–8's for an avatar of every band ('all') or anything else. */
289
+ export declare const bodyBand: (def: Pick<AvatarDef, "band">) => AgeRange;
290
+ /** Head radius in torso units (6–8; see BAND_BODY for the others). */
291
+ export declare const HEAD: number;
292
+ /** The head tilts at most this much (rad) from the shoulders' line. */
293
+ export declare const MAX_TILT = 0.3;
294
+ /**
295
+ * Tilts under this (rad) are noise, not a child tilting their head: no tilt. Past it the tilt grows from 0, so
296
+ * there is no jump at the edge.
297
+ */
298
+ export declare const TILT_DEAD_ZONE = 0.12;
299
+ export interface RigOptions {
300
+ mirrored?: boolean;
301
+ /** Portraits leave the tail out: in a head-and-shoulders crop it would only peek in as a stray shape. */
302
+ portrait?: boolean;
303
+ /** Body proportions (see BAND_BODY); 6–8 by default. avatarShapes takes the avatar's own band. */
304
+ band?: AgeRange;
305
+ }
306
+ /** The torso frame: origin between the shoulders, r to the right, d down along the torso; lengths in pixels. */
307
+ export interface Frame {
308
+ S: Pt;
309
+ r: Pt;
310
+ d: Pt;
311
+ U: number;
312
+ /** Shoulders to hips along d. */
313
+ L: number;
314
+ hipsOk: boolean;
315
+ head: Pt;
316
+ headR: number;
317
+ headRot: number;
318
+ angle: number;
319
+ }
320
+ /** The torso frame and head placement, or null when the shoulders are not seen. */
321
+ export declare function frameOf(body: Body$1, box: Box, opts?: RigOptions): Frame | null;
322
+ /** All the shapes of an avatar for this body, in canvas pixels. Empty when the shoulders are not seen. */
323
+ export declare function avatarShapes(body: Body$1, def: AvatarDef, box: Box, opts?: RigOptions): Shape[];
324
+ export interface InterpolatorOptions {
325
+ /** A player whose bodies stop for this long (ms of the sender's clock) starts afresh when they come back. */
326
+ lostMs?: number;
327
+ /** At most this far past the newest body (ms), when the next one is late. */
328
+ maxExtrapolateMs?: number;
329
+ }
330
+ export declare class BodyInterpolator {
331
+ private readonly tracks;
332
+ /** Page clock minus sender clock, at its smallest (the fastest delivery): NaN before the first body. */
333
+ private offset;
334
+ private lastArrival;
335
+ /** The time between bodies (sender's clock), smoothed. */
336
+ private interval;
337
+ private lastT;
338
+ private readonly lostMs;
339
+ private readonly maxExtrapolateMs;
340
+ constructor(o?: InterpolatorOptions);
341
+ /** The time between bodies the drawing is behind by, in ms. */
342
+ get delayMs(): number;
343
+ /**
344
+ * A new body for this player, taken at t (the sender's clock), arriving now (this page's clock, by default
345
+ * performance.now()). Bodies for several players of one camera frame share t.
346
+ */
347
+ push(player: number, t: number, body: Body$1, arrival?: number): void;
348
+ /** The player's body to draw at time now (this page's clock); null for a player never pushed or forgotten. */
349
+ at(player: number, at?: number): Body$1 | null;
350
+ /** A player gone: their next body starts afresh. */
351
+ forget(player: number): void;
352
+ /** Every player gone (a new scene). */
353
+ clear(): void;
354
+ private clock;
355
+ }
356
+ /**
357
+ * The body a share u of the way from a to b (u past 1: beyond b, along the same line). A point seen in both and not
358
+ * jumping slides; any other takes the nearer body's point, visibility and all.
359
+ */
360
+ export declare function blend(a: Body$1, b: Body$1, u: number): Body$1;
361
+ export declare const AVATAR_IDS: readonly string[];
362
+ export type AvatarId = string;
363
+ /** Fabi the fox, the app's mascot: an avatar of every band ('all'), first in each band's list. */
364
+ export declare const MASCOT_ID: AvatarId;
365
+ /**
366
+ * The avatars of each age band, in the order the pickers show them: the mascot first (special: pickers may mark
367
+ * it), then the band's own 12. The first four of the band's own are the defaults of the four player slots
368
+ * (defaultAvatarFor in @fabi-play/protocol), not the mascot. Every avatar is in exactly one band, except those of
369
+ * band 'all' (the mascot), which are in every band.
370
+ */
371
+ export declare const AVATAR_BANDS: Readonly<Record<AgeRange, readonly AvatarId[]>>;
372
+ /** The avatar of a child who has not picked one yet: the fox (6–8's first). */
373
+ export declare const DEFAULT_AVATAR: AvatarDef;
374
+ export declare const isAvatarId: (v: unknown) => v is AvatarId;
375
+ /** The avatar with this id, or undefined. */
376
+ export declare const avatarById: (id: string) => AvatarDef | undefined;
377
+ /** The avatars of an age band, in picker order (6–8 for anything that is not an age band). */
378
+ export declare function avatarsFor(ageRange: AgeRange): readonly AvatarDef[];
379
+ /** The age band of an avatar: 'all' for the mascot (in every band), 6–8 (the default avatar's) for an unknown id. */
380
+ export declare const bandOf: (id: AvatarId) => AvatarBand;
381
+ export interface DrawOptions {
382
+ /** 0–1, for the whole avatar. */
383
+ alpha?: number;
384
+ /** The body is in camera space (not mirrored like player space): flip it left to right. */
385
+ mirrored?: boolean;
386
+ }
387
+ /**
388
+ * Draws the avatar posed like the body. box is where player space (0–1) lands on the canvas, as for the silhouette
389
+ * (see cameraBox in the screen). Draws nothing when the shoulders are not seen; never throws on missing points.
390
+ * Returns whether it drew.
391
+ */
392
+ export declare function drawAvatar(ctx: CanvasRenderingContext2D | Ctx2D, body: Body$1, def: AvatarDef | string, box: Box, opts?: DrawOptions): boolean;
393
+ /** The portrait: head and shoulders of the neutral pose, framed in a size × size square at (0, 0). */
394
+ export declare function iconShapes(def: AvatarDef | string, size: number): Shape[];
395
+ /** Draws the portrait in the square (x, y, size), clipped to it. */
396
+ export declare function drawAvatarIcon(ctx: CanvasRenderingContext2D | Ctx2D, def: AvatarDef | string, x: number, y: number, size: number): void;
397
+ export interface IconSvgOptions {
398
+ /** A background colour for the square; transparent by default. */
399
+ background?: string;
400
+ /** Corner radius of the background, in pixels. */
401
+ radius?: number;
402
+ }
403
+ /** The portrait as a standalone SVG document (size × size pixels). Use it as markup, an <img> data URI, or SvgXml. */
404
+ export declare function avatarIconSvg(def: AvatarDef | string, size: number, opts?: IconSvgOptions): string;
405
+ /** avatarIconSvg as a data URI, for <img src> or React Native <Image> with SVG support. */
406
+ export declare const avatarIconDataUri: (def: AvatarDef | string, size: number, opts?: IconSvgOptions) => string;
407
+ /**
408
+ * Smooths a body against the previous frame, so the avatar does not jitter or blink: positions ease towards the new
409
+ * points (amount 0–1: how much of the new frame to take), and a point seen in the last frame stays visible until
410
+ * its visibility drops well below the threshold. Keep the result per player and pass it back as prev.
411
+ *
412
+ * The face (nose, eyes, ears, mouth: what places and tilts the head) is smoothed much more, and relative to the
413
+ * shoulders: the head follows the body at once when the child moves, but face-point noise and small nods barely
414
+ * move it.
415
+ */
416
+ export declare function smoothBody(prev: Body$1 | null | undefined, next: Body$1, amount?: number): Body$1;
417
+ export interface Bounds {
418
+ minX: number;
419
+ minY: number;
420
+ maxX: number;
421
+ maxY: number;
422
+ }
423
+ export declare function bounds(shapes: readonly Shape[]): Bounds;
424
+
425
+ export {};