@fabi-play/avatars 0.3.0 → 0.5.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/README.md +53 -21
- package/dist/index.d.ts +65 -25
- package/dist/index.js +267 -28
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @fabi-play/avatars
|
|
2
2
|
|
|
3
|
-
Thirty-
|
|
4
|
-
mascot)
|
|
3
|
+
Thirty-nine original, child-friendly avatars, drawn from a body pose: 12 for each age band, the T-rex and the fairy,
|
|
4
|
+
and Fabi the fox (the fox of the app icon, the mascot). Fabi is the host, not a player: no band offers it to the
|
|
5
|
+
children, but the TV and games draw it like any other avatar. The lion, the T-rex and the fairy
|
|
6
|
+
are offered to both 3–5 and 6–8. The TV shows them in place
|
|
5
7
|
of the plain silhouette when the phone's video is not on screen. Each child picks one on the phone, among the avatars
|
|
6
8
|
of their age band. The phone stores only the avatar's id; any valid id is accepted everywhere, whatever the band.
|
|
7
9
|
|
|
@@ -10,9 +12,9 @@ varied skin tones and gender-neutral looks, and are told apart by their gear.
|
|
|
10
12
|
|
|
11
13
|
| Band | Style | id · en · it (the first four are the slot defaults) |
|
|
12
14
|
| --- | --- | --- |
|
|
13
|
-
| all | the app icon's fox:
|
|
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 |
|
|
15
|
+
| all | the app icon's fox, the host: in no band's list (pickers don't offer it), `special: true`, drawn with the 6–8 proportions; never a slot default; a child saved with it keeps it | `mascot` Fabi · Fabi |
|
|
16
|
+
| 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; then, also offered here (home 6–8): `lion`, `trex`, `fairy` |
|
|
17
|
+
| 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, `trex` T-Rex · T-Rex, `octopus` Octopus · Polpetto, `ghost` Friendly Ghost · Fantasmino, `shark` Shark · Squalo, `wolf` Wolf · Lupo, `parrot` Parrot · Pappagallo, `fairy` Fairy · Fatina |
|
|
16
18
|
| 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
19
|
|
|
18
20
|
## Install
|
|
@@ -41,27 +43,50 @@ el.innerHTML = avatarIconSvg('panda', 96) // web DOM
|
|
|
41
43
|
<img src={avatarIconDataUri('panda', 96)} /> // data URI
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
- `AVATARS: readonly AvatarDef[]` (all
|
|
45
|
-
- `AVATAR_BANDS: Record<AgeRange, readonly AvatarId[]>` (picker order: the
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
(
|
|
51
|
-
|
|
46
|
+
- `AVATARS: readonly AvatarDef[]` (all 39), `AVATAR_IDS`, `DEFAULT_AVATAR` (the fox), `avatarById(id)`, `isAvatarId(x)`.
|
|
47
|
+
- `AVATAR_BANDS: Record<AgeRange, readonly AvatarId[]>` (picker order: the band's own 12, whose first four are the
|
|
48
|
+
slot defaults, see `defaultAvatarFor(slot, ageRange?)` in `@fabi-play/protocol`, then the avatars also offered
|
|
49
|
+
there; the lists are not all the same length: 15, 14, 12). The mascot (`MASCOT_ID`) is in none: Fabi is the host,
|
|
50
|
+
not a player. It stays in `AVATARS`, `isAvatarId('mascot')` is true and `drawAvatar` draws it,
|
|
51
|
+
`avatarsFor(ageRange)` (the definitions, in that order), `bandOf(id)` (the home band: `'all'` for the mascot, 6–8
|
|
52
|
+
for an unknown id), `bandsOf(id)` (every band whose list offers it: none for the mascot). Every definition has its `band`
|
|
53
|
+
(`AvatarBand`: an age range or `'all'`) and, if it is offered elsewhere too, `alsoIn` (see "Avatars of two bands"
|
|
54
|
+
below); `special: true` marks the mascot. `bodyBand(def, shown?)` gives the proportions
|
|
55
|
+
it is drawn with (6–8 for `'all'`).
|
|
56
|
+
- `drawAvatar(ctx, body, def | id, box, { alpha?, mirrored?, band? })`. Unknown ids draw the default avatar. `band`:
|
|
57
|
+
the child's age band, when the caller knows it (see "Avatars of two bands"). Pass
|
|
52
58
|
`mirrored` when the body is in camera space rather than player space. It draws nothing when the shoulders are not
|
|
53
59
|
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
|
|
55
|
-
|
|
60
|
+
- `drawAvatarIcon(ctx, def | id, x, y, size, { band? })`: a head-and-shoulders portrait in a neutral pose, clipped to
|
|
61
|
+
its square. A picker passes the band it shows.
|
|
62
|
+
- `avatarIconSvg(def | id, size, { background?, radius?, band? })`: the same portrait as an SVG document string. Its
|
|
56
63
|
clip-path id is unique per call (avatar, size and a counter), so many portraits can sit inline in one page.
|
|
57
64
|
`avatarIconDataUri` returns the same as a data URI.
|
|
58
65
|
- `smoothBody(prev, next, amount = 0.5)`: optional per-player smoothing. Points ease towards the new frame. A point
|
|
59
66
|
that was seen stays visible until its visibility drops below 0.3, so limbs don't blink. Keep the result per player.
|
|
60
67
|
- Poses for previews and tests: `neutralPose()`, `armsUpPose()`, `starJumpPose()`, `squatPose()`, `wavePose()`,
|
|
61
68
|
`TEST_POSES`, `poseBody(spec)`.
|
|
62
|
-
- Lower level: `avatarShapes(body, def, box
|
|
69
|
+
- Lower level: `avatarShapes(body, def, box, { mirrored?, portrait?, band? })` returns the shape list,
|
|
70
|
+
`iconShapes(def, size, band?)` the portrait's, `drawShapes(ctx, shapes)` draws it on a canvas,
|
|
63
71
|
and `shapesToSvg(shapes)` turns it into SVG. `frameOf(body, box)` gives the rig's torso frame.
|
|
64
72
|
|
|
73
|
+
### Avatars of two bands
|
|
74
|
+
|
|
75
|
+
An avatar can be offered in more than one band: its `band` is its home, and `alsoIn` lists the others (today the
|
|
76
|
+
lion, the T-rex and the fairy: home 6–8, also in 3–5). It is listed in each of those bands' `AVATAR_BANDS`, after
|
|
77
|
+
the band's own, so the slot defaults never change.
|
|
78
|
+
|
|
79
|
+
- **Proportions follow the band shown.** Pass the band (`drawAvatar`/`avatarShapes` option `band`, the third argument
|
|
80
|
+
of `iconShapes`, the `band` option of `drawAvatarIcon`/`avatarIconSvg`): an avatar offered in that band takes its
|
|
81
|
+
proportions (`bodyBand(def, band)`), so the lion in a 3–5 picker or game has the toddler build. The band is the
|
|
82
|
+
child's (`Player.ageRange` on the phone and the TV) or the picker's.
|
|
83
|
+
- **Without a band, or with a band it is not offered in, it keeps its home band's** proportions: exactly what it drew
|
|
84
|
+
before. Avatars of one band, and the mascot (always 6–8), ignore `band` entirely, so passing it is always safe.
|
|
85
|
+
- **Its style stays its home band's** (outline colour, palette): one look, whatever the band. `bandOf(id)` still
|
|
86
|
+
returns the home band (one value, as before); `bandsOf(id)` returns them all.
|
|
87
|
+
- The SDK's and the engine's `drawAvatar` don't pass a band yet: in games these avatars are drawn with their home
|
|
88
|
+
band's proportions, as are their name tags.
|
|
89
|
+
|
|
65
90
|
## How it is drawn
|
|
66
91
|
|
|
67
92
|
One source of truth: the rig and the portrait build a small **shape list** (`circle`, `ellipse`, `rect`, `path`,
|
|
@@ -83,8 +108,8 @@ a change that alters them by accident fails the tests (`UPDATE_SNAPSHOT=1` after
|
|
|
83
108
|
- **Limbs**: thick round strokes from shoulder to elbow to wrist (0.22 U) and from hip to knee to ankle (0.28 U),
|
|
84
109
|
each with an outline. Arms start just below the shoulder. Legs start inside the torso, so nothing looks detached.
|
|
85
110
|
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
|
|
87
|
-
band (`'all'`, the mascot) takes the 6–8 row.
|
|
111
|
+
- **Proportions by band** (`BAND_BODY`): the joints are always the child's, only sizes change. An avatar of no
|
|
112
|
+
band (`'all'`, the mascot) takes the 6–8 row. An avatar of two bands takes the row of the band shown (above).
|
|
88
113
|
|
|
89
114
|
| band | head radius | head centre above the shoulders | arm / leg thickness | hand radius | torso below the hips | torso min half width (shoulders / hips) |
|
|
90
115
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -95,9 +120,14 @@ a change that alters them by accident fails the tests (`UPDATE_SNAPSHOT=1` after
|
|
|
95
120
|
In 3–5 an arm raised well above the shoulders is drawn over the big head, which would hide it.
|
|
96
121
|
- **Head**: radius by band (above), centred above the shoulder midpoint along the torso axis. It follows the
|
|
97
122
|
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.
|
|
123
|
+
clamped to ±0.5 rad from the torso, and a swapped pair is treated as a child facing away. The `snout-long` head
|
|
124
|
+
(the T-rex) is turned three-quarters, its snout on the left, away from the tail; its portrait is centred on the
|
|
125
|
+
head's width, snout included.
|
|
99
126
|
- **Tail, wings, backpack, tentacles**: these hang off the hips or the back, in torso coordinates, and are drawn
|
|
100
127
|
behind the body. The tail goes on the child's right. Portraits leave it out.
|
|
128
|
+
- **Held things**: the fairy's `wand` is in the hand on the right, following it (up and out from the body, leaning
|
|
129
|
+
with the forearm), under the hand; no wrist, no wand. Portraits leave it out. `tiny-arms` only makes the arms
|
|
130
|
+
slimmer, with small clawed hands: they still reach the child's hands.
|
|
101
131
|
- **Draw order**: back extras, legs, torso, arms, head, then (3–5 only) raised arms.
|
|
102
132
|
- **Degrading**: a point counts as seen at visibility ≥ 0.5. If the elbow or knee is missing, that limb is hidden.
|
|
103
133
|
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
|
|
@@ -106,7 +136,8 @@ a change that alters them by accident fails the tests (`UPDATE_SNAPSHOT=1` after
|
|
|
106
136
|
## Adding an avatar
|
|
107
137
|
|
|
108
138
|
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
|
|
139
|
+
of band `'all'` goes in no band (only the mascot, the host, is one); one offered in another band too lists it in `alsoIn` and goes
|
|
140
|
+
in that band's list as well, after the band's own). Choose a new
|
|
110
141
|
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
142
|
`outline`, plus optional `hands`, `feet` and `ears`. Then choose a `head` shape, `ears`, the `face` (eyes, mouth,
|
|
112
143
|
muzzle, nose, cheeks), a `tail`, `headExtras` and `bodyExtras`. `extraColors` covers stripes, spots, manes and
|
|
@@ -115,7 +146,8 @@ a change that alters them by accident fails the tests (`UPDATE_SNAPSHOT=1` after
|
|
|
115
146
|
body colour that reads on both the night background (#1f1813) and a light one. Keep the dark outline.
|
|
116
147
|
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
148
|
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
|
|
149
|
+
4. Run `pnpm -F @fabi-play/avatars preview <dir> --details` and look at the PNGs (an avatar of two bands is shown
|
|
150
|
+
in each, with that band's proportions): the portrait, every pose, and
|
|
119
151
|
both backgrounds. Then run `pnpm -F @fabi-play/avatars test`.
|
|
120
152
|
|
|
121
153
|
## Scripts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @fabi-play/avatars:
|
|
1
|
+
// @fabi-play/avatars: 39 child-friendly avatars: Fabi the fox in every age band, 12 per band, a T-rex and a fairy, drawn from a body pose, on canvas or as SVG portraits for the pickers. See https://docs.fabiplay.app.
|
|
2
2
|
// Generated by scripts/build-lib.mjs (fabiplay-kit) from the sources: do not edit.
|
|
3
3
|
|
|
4
4
|
import { AgeRange, Body as Body$1 } from '@fabi-play/protocol';
|
|
@@ -116,21 +116,21 @@ export interface Palette {
|
|
|
116
116
|
hair?: string;
|
|
117
117
|
}
|
|
118
118
|
/** The head's outline. */
|
|
119
|
-
export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket";
|
|
119
|
+
export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket" | "snout-long";
|
|
120
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";
|
|
121
|
+
export type EarKind = "none" | "pointy" | "round" | "big-round" | "long" | "side" | "tufts" | "antenna" | "antennae" | "horns" | "unicorn" | "floppy" | "droopy" | "fin" | "human" | "pixie";
|
|
122
122
|
/** How the eyes are drawn. */
|
|
123
123
|
export type EyeKind = "dot" | "big" | "mask" | "screen" | "almond" | "visor" | "calm" | "slit" | "goggle" | "bead";
|
|
124
124
|
/** How the mouth (or beak) is drawn. */
|
|
125
|
-
export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws";
|
|
125
|
+
export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws" | "toothy";
|
|
126
126
|
/** The patch around the nose and mouth. */
|
|
127
127
|
export type MuzzleKind = "none" | "oval" | "heart" | "monkey" | "disc" | "cheeks" | "band";
|
|
128
128
|
/** The tail, behind the body. */
|
|
129
|
-
export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume";
|
|
129
|
+
export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume" | "thick";
|
|
130
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";
|
|
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" | "bun" | "flower-crown";
|
|
132
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";
|
|
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" | "tiny-arms" | "strong-legs" | "wings-fairy" | "wand" | "petals";
|
|
134
134
|
/** An avatar's face. */
|
|
135
135
|
export interface FaceDef {
|
|
136
136
|
/** How the eyes are drawn. */
|
|
@@ -152,7 +152,7 @@ export interface FaceDef {
|
|
|
152
152
|
/** Eyebrows of this colour (people): a little attitude. */
|
|
153
153
|
brows?: string;
|
|
154
154
|
}
|
|
155
|
-
/** The age band an avatar is for, or 'all':
|
|
155
|
+
/** The age band an avatar is for, or 'all': of no band (Fabi the fox, the app's mascot: the host, in no picker). */
|
|
156
156
|
export type AvatarBand = AgeRange | "all";
|
|
157
157
|
/** An avatar, as data: one renderer draws any definition. */
|
|
158
158
|
export interface AvatarDef {
|
|
@@ -164,11 +164,17 @@ export interface AvatarDef {
|
|
|
164
164
|
it: string;
|
|
165
165
|
};
|
|
166
166
|
/**
|
|
167
|
-
* The age band the avatar is for: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in
|
|
168
|
-
* band's list, drawn with the 6–8 proportions.
|
|
167
|
+
* The age band the avatar is for, its home: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in
|
|
168
|
+
* no band's list (the mascot), drawn with the 6–8 proportions. See alsoIn for an avatar offered in other bands too.
|
|
169
169
|
*/
|
|
170
170
|
band: AvatarBand;
|
|
171
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Other age bands the avatar is also offered in (listed in their AVATAR_BANDS too), besides its own band. Shown in
|
|
173
|
+
* one of them, it is drawn with that band's proportions (bodyBand(def, band)); its band stays its home: its style
|
|
174
|
+
* (outline), bandOf, and its proportions when the band shown is not known. Not for 'all' (the mascot).
|
|
175
|
+
*/
|
|
176
|
+
alsoIn?: readonly AgeRange[];
|
|
177
|
+
/** A special avatar (the mascot): the host, not a player; in no band's list (AVATAR_BANDS). */
|
|
172
178
|
special?: boolean;
|
|
173
179
|
/** Its colours. */
|
|
174
180
|
palette: Palette;
|
|
@@ -285,8 +291,14 @@ export interface BandBody {
|
|
|
285
291
|
armsOverHead: boolean;
|
|
286
292
|
}
|
|
287
293
|
export declare const BAND_BODY: Record<AgeRange, BandBody>;
|
|
288
|
-
/**
|
|
289
|
-
|
|
294
|
+
/**
|
|
295
|
+
* The proportions an avatar is drawn with: its band's, 6–8's for the mascot ('all') or anything else.
|
|
296
|
+
* shown: the band it is shown in (the child's, the picker's), when known; an avatar offered in more than one band
|
|
297
|
+
* (alsoIn) takes the proportions of the one shown, any other keeps its own (the mascot always 6–8's).
|
|
298
|
+
*/
|
|
299
|
+
export declare const bodyBand: (def: Pick<AvatarDef, "band"> & {
|
|
300
|
+
alsoIn?: readonly AgeRange[];
|
|
301
|
+
}, shown?: AgeRange) => AgeRange;
|
|
290
302
|
/** Head radius in torso units (6–8; see BAND_BODY for the others). */
|
|
291
303
|
export declare const HEAD: number;
|
|
292
304
|
/** The head tilts at most this much (rad) from the shoulders' line. */
|
|
@@ -298,9 +310,12 @@ export declare const MAX_TILT = 0.3;
|
|
|
298
310
|
export declare const TILT_DEAD_ZONE = 0.12;
|
|
299
311
|
export interface RigOptions {
|
|
300
312
|
mirrored?: boolean;
|
|
301
|
-
/** Portraits leave the tail out: in a head-and-shoulders crop
|
|
313
|
+
/** Portraits leave the tail and the wand out: in a head-and-shoulders crop they would only peek in as stray shapes. */
|
|
302
314
|
portrait?: boolean;
|
|
303
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Body proportions (see BAND_BODY); 6–8 by default. For avatarShapes, the band the avatar is shown in: it follows
|
|
317
|
+
* it only for an avatar offered there too (alsoIn, see bodyBand); otherwise the avatar's own band.
|
|
318
|
+
*/
|
|
304
319
|
band?: AgeRange;
|
|
305
320
|
}
|
|
306
321
|
/** The torso frame: origin between the shoulders, r to the right, d down along the torso; lengths in pixels. */
|
|
@@ -360,13 +375,17 @@ export declare class BodyInterpolator {
|
|
|
360
375
|
export declare function blend(a: Body$1, b: Body$1, u: number): Body$1;
|
|
361
376
|
export declare const AVATAR_IDS: readonly string[];
|
|
362
377
|
export type AvatarId = string;
|
|
363
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* Fabi the fox, the app's mascot ('all': no band of its own). Fabi is the host, not a player: in no band's list
|
|
380
|
+
* (AVATAR_BANDS), so pickers never offer it, but it is a valid avatar (isAvatarId, avatarById, drawAvatar) for the TV
|
|
381
|
+
* and for games, and a child saved with it before keeps it.
|
|
382
|
+
*/
|
|
364
383
|
export declare const MASCOT_ID: AvatarId;
|
|
365
384
|
/**
|
|
366
|
-
* The avatars of each age band, in the order the pickers show them: the
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
385
|
+
* The avatars of each age band, in the order the pickers show them: the band's own 12, then (3–5) the ones also
|
|
386
|
+
* offered there. The first four are the defaults of the four player slots (defaultAvatarFor in @fabi-play/protocol).
|
|
387
|
+
* Every avatar is in its own band and in those of its alsoIn; those of band 'all' (the mascot, MASCOT_ID) are in
|
|
388
|
+
* none: Fabi hosts, the children play. The lists are not all the same length.
|
|
370
389
|
*/
|
|
371
390
|
export declare const AVATAR_BANDS: Readonly<Record<AgeRange, readonly AvatarId[]>>;
|
|
372
391
|
/** The avatar of a child who has not picked one yet: the fox (6–8's first). */
|
|
@@ -376,13 +395,27 @@ export declare const isAvatarId: (v: unknown) => v is AvatarId;
|
|
|
376
395
|
export declare const avatarById: (id: string) => AvatarDef | undefined;
|
|
377
396
|
/** The avatars of an age band, in picker order (6–8 for anything that is not an age band). */
|
|
378
397
|
export declare function avatarsFor(ageRange: AgeRange): readonly AvatarDef[];
|
|
379
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* The age band of an avatar, its home: 'all' for the mascot (in no band's list), 6–8 (the default avatar's) for an
|
|
400
|
+
* unknown id. An avatar offered in more than one band has one home band (its style, its proportions when the band
|
|
401
|
+
* shown is not known); bandsOf gives every band it is offered in.
|
|
402
|
+
*/
|
|
380
403
|
export declare const bandOf: (id: AvatarId) => AvatarBand;
|
|
404
|
+
/**
|
|
405
|
+
* Every age band whose list (AVATAR_BANDS) offers the avatar, youngest first; the default avatar's for an unknown id;
|
|
406
|
+
* none for the mascot (offered in no band).
|
|
407
|
+
*/
|
|
408
|
+
export declare const bandsOf: (id: AvatarId) => readonly AgeRange[];
|
|
381
409
|
export interface DrawOptions {
|
|
382
410
|
/** 0–1, for the whole avatar. */
|
|
383
411
|
alpha?: number;
|
|
384
412
|
/** The body is in camera space (not mirrored like player space): flip it left to right. */
|
|
385
413
|
mirrored?: boolean;
|
|
414
|
+
/**
|
|
415
|
+
* The age band the avatar is shown in (the child's), when known: an avatar offered in that band too (alsoIn) takes
|
|
416
|
+
* its proportions; any other avatar keeps its own band's (see bodyBand).
|
|
417
|
+
*/
|
|
418
|
+
band?: AgeRange;
|
|
386
419
|
}
|
|
387
420
|
/**
|
|
388
421
|
* Draws the avatar posed like the body. box is where player space (0–1) lands on the canvas, as for the silhouette
|
|
@@ -390,11 +423,18 @@ export interface DrawOptions {
|
|
|
390
423
|
* Returns whether it drew.
|
|
391
424
|
*/
|
|
392
425
|
export declare function drawAvatar(ctx: CanvasRenderingContext2D | Ctx2D, body: Body$1, def: AvatarDef | string, box: Box, opts?: DrawOptions): boolean;
|
|
393
|
-
/**
|
|
394
|
-
|
|
426
|
+
/**
|
|
427
|
+
* The portrait: head and shoulders of the neutral pose, framed in a size × size square at (0, 0). band: the band the
|
|
428
|
+
* picker shows (see DrawOptions.band).
|
|
429
|
+
*/
|
|
430
|
+
export declare function iconShapes(def: AvatarDef | string, size: number, band?: AgeRange): Shape[];
|
|
431
|
+
export interface IconOptions {
|
|
432
|
+
/** The band the picker shows (see DrawOptions.band). */
|
|
433
|
+
band?: AgeRange;
|
|
434
|
+
}
|
|
395
435
|
/** 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 {
|
|
436
|
+
export declare function drawAvatarIcon(ctx: CanvasRenderingContext2D | Ctx2D, def: AvatarDef | string, x: number, y: number, size: number, opts?: IconOptions): void;
|
|
437
|
+
export interface IconSvgOptions extends IconOptions {
|
|
398
438
|
/** A background colour for the square; transparent by default. */
|
|
399
439
|
background?: string;
|
|
400
440
|
/** Corner radius of the background, in pixels. */
|
package/dist/index.js
CHANGED
|
@@ -120,6 +120,7 @@ var AVATARS = [
|
|
|
120
120
|
id: "lion",
|
|
121
121
|
name: { en: "Lion", it: "Leoncino" },
|
|
122
122
|
band: "6-8",
|
|
123
|
+
alsoIn: ["3-5"],
|
|
123
124
|
palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
|
|
124
125
|
head: "round",
|
|
125
126
|
ears: "round",
|
|
@@ -486,6 +487,39 @@ var AVATARS = [
|
|
|
486
487
|
headExtras: ["hair", "domino"],
|
|
487
488
|
bodyExtras: ["cape", "emblem", "boots"],
|
|
488
489
|
extraColors: ["#ffd84a"]
|
|
490
|
+
},
|
|
491
|
+
// ── In both 3–5 and 6–8 (home 6–8, alsoIn 3–5: drawn with the proportions of the band shown) ──
|
|
492
|
+
{
|
|
493
|
+
// Next to the round 'dinosaur': a T-rex at a glance, a big head turned three-quarters with a long snout and a
|
|
494
|
+
// toothy grin, tiny arms, strong legs, a thick striped tail.
|
|
495
|
+
id: "trex",
|
|
496
|
+
name: { en: "T-Rex", it: "T-Rex" },
|
|
497
|
+
band: "6-8",
|
|
498
|
+
alsoIn: ["3-5"],
|
|
499
|
+
palette: { body: "#6cb84e", limbs: "#6cb84e", belly: "#f4e8a8", accent: "#f4e8a8", outline: INK },
|
|
500
|
+
head: "snout-long",
|
|
501
|
+
ears: "none",
|
|
502
|
+
face: { eyes: "dot", mouth: "toothy", muzzle: "none", cheeks: "#f59b8f" },
|
|
503
|
+
tail: "thick",
|
|
504
|
+
headExtras: ["stripes"],
|
|
505
|
+
bodyExtras: ["tiny-arms", "strong-legs"],
|
|
506
|
+
extraColors: ["#4a9235", "#58a23f"]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
// An original child fairy: a bun with a flower crown, little pointed ears, a petal tunic, see-through wings and
|
|
510
|
+
// a star wand in one hand.
|
|
511
|
+
id: "fairy",
|
|
512
|
+
name: { en: "Fairy", it: "Fatina" },
|
|
513
|
+
band: "6-8",
|
|
514
|
+
alsoIn: ["3-5"],
|
|
515
|
+
palette: { body: "#b99af0", limbs: "#a8693f", belly: "#b99af0", accent: "#ffd84a", outline: INK, skin: "#a8693f", hair: "#2e1d17", hands: "#a8693f", feet: "#f58cbc" },
|
|
516
|
+
head: "round",
|
|
517
|
+
ears: "pixie",
|
|
518
|
+
face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f0857a" },
|
|
519
|
+
tail: "none",
|
|
520
|
+
headExtras: ["bun", "flower-crown"],
|
|
521
|
+
bodyExtras: ["wings-fairy", "petals", "wand"],
|
|
522
|
+
extraColors: ["#c4ecff", "#f7a8cf", "#fff3a0", "#ffffff"]
|
|
489
523
|
}
|
|
490
524
|
];
|
|
491
525
|
|
|
@@ -780,6 +814,7 @@ function headShapes(def, ow) {
|
|
|
780
814
|
if (has("locks")) {
|
|
781
815
|
out.push({ k: "path", d: smoothClosed(pts(-1.14, 0.58, -1.24, -0.2, -0.86, -0.98, 0, -1.2, 0.86, -0.98, 1.24, -0.2, 1.14, 0.58, 0.8, 0.66, 0, 0.2, -0.8, 0.66)), fill: p.hair ?? p.outline, ...ol });
|
|
782
816
|
}
|
|
817
|
+
if (has("bun")) out.push({ k: "circle", x: 0.04, y: -1.14, r: 0.4, fill: p.hair ?? p.outline, ...ol });
|
|
783
818
|
if (has("ninja-hood")) {
|
|
784
819
|
for (const [rot, len2] of [
|
|
785
820
|
[0.35, 0.5],
|
|
@@ -817,7 +852,15 @@ function headShapes(def, ow) {
|
|
|
817
852
|
if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
|
|
818
853
|
if (muzzle === "cheeks") for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.4, y: 0.44, rx: 0.44, ry: 0.32, fill: mc });
|
|
819
854
|
if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
|
|
820
|
-
if (has("stripes")) {
|
|
855
|
+
if (has("stripes") && def.head === "snout-long") {
|
|
856
|
+
const c = extra(0, p.outline);
|
|
857
|
+
for (const [x0, y0, x1, y1] of [
|
|
858
|
+
[0.46, -0.94, 0.36, -0.66],
|
|
859
|
+
[0.8, -0.72, 0.62, -0.5],
|
|
860
|
+
[1, -0.36, 0.78, -0.24]
|
|
861
|
+
])
|
|
862
|
+
out.push({ k: "line", pts: [x0, y0, x1, y1], stroke: c, lw: 0.11 });
|
|
863
|
+
} else if (has("stripes")) {
|
|
821
864
|
const c = extra(0, p.outline);
|
|
822
865
|
out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
|
|
823
866
|
for (const s of SIDES) {
|
|
@@ -832,6 +875,13 @@ function headShapes(def, ow) {
|
|
|
832
875
|
out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
|
|
833
876
|
out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
|
|
834
877
|
}
|
|
878
|
+
if (def.head === "snout-long") {
|
|
879
|
+
for (const [x, y, r] of [
|
|
880
|
+
[-1.38, -0.14, 0.06],
|
|
881
|
+
[-1.14, -0.22, 0.055]
|
|
882
|
+
])
|
|
883
|
+
out.push({ k: "ellipse", x, y, rx: r, ry: r * 0.8, rot: -0.3, fill: p.outline, alpha: 0.85 });
|
|
884
|
+
}
|
|
835
885
|
if (has("forehead")) out.push({ k: "path", d: smoothClosed(pts(-0.62, -0.66, 0, -0.86, 0.62, -0.66, 0.26, -0.36, 0, -0.1, -0.26, -0.36), 0.9), fill: extra(0, p.accent) });
|
|
836
886
|
if (has("eye-spot")) out.push({ k: "ellipse", x: 0.42, y: -0.06, rx: 0.3, ry: 0.33, rot: 0.3, fill: p.ears ?? p.accent });
|
|
837
887
|
if (has("gills")) {
|
|
@@ -840,6 +890,7 @@ function headShapes(def, ow) {
|
|
|
840
890
|
const hair = p.hair ?? p.outline;
|
|
841
891
|
if (has("hair")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.06, -1.02, -0.58, -0.62, -1.04, 0.08, -1.14, 0.76, -0.92, 1.05, -0.42, 1, -0.06, 0.84, -0.4, 0.42, -0.5, 0.06, -0.4, -0.34, -0.56, -0.74, -0.38)), fill: hair, ...ol });
|
|
842
892
|
if (has("locks")) out.push({ k: "path", d: smoothClosed(pts(-1, -0.08, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1, -0.08, 0.8, -0.38, 0.36, -0.44, 0, -0.4, -0.36, -0.44, -0.8, -0.38)), fill: hair, ...ol });
|
|
893
|
+
if (has("bun")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.1, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1.02, -0.1, 0.84, -0.36, 0.5, -0.56, 0.12, -0.48, -0.22, -0.6, -0.6, -0.5, -0.86, -0.36)), fill: hair, ...ol });
|
|
843
894
|
if (has("curls")) {
|
|
844
895
|
for (let i = 0; i < 7; i++) {
|
|
845
896
|
const c = polar(-1.3 + i / 6 * 2.6, 0.86);
|
|
@@ -861,7 +912,7 @@ function headShapes(def, ow) {
|
|
|
861
912
|
}
|
|
862
913
|
if (def.face.brows) for (const s of SIDES) out.push({ k: "line", pts: [s * 0.2, -0.2, s * 0.54, -0.28], stroke: def.face.brows, lw: 0.09 });
|
|
863
914
|
out.push(...mouth(def, ow));
|
|
864
|
-
if (def.face.cheeks) for (const
|
|
915
|
+
if (def.face.cheeks) for (const c of cheekSpots(def)) out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
|
|
865
916
|
if (has("whiskers")) {
|
|
866
917
|
for (const s of SIDES)
|
|
867
918
|
for (const dy of [-0.1, 0.06]) out.push({ k: "line", pts: [s * 0.62, 0.38 + dy, s * 1.06, 0.3 + dy * 2.2], stroke: p.outline, lw: ow * 0.7, alpha: 0.75 });
|
|
@@ -883,6 +934,24 @@ function headShapes(def, ow) {
|
|
|
883
934
|
out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
|
|
884
935
|
}
|
|
885
936
|
}
|
|
937
|
+
if (has("flower-crown")) {
|
|
938
|
+
const cols = [extra(1, "#f7a8cf"), extra(2, "#fff3a0"), extra(3, WHITE)];
|
|
939
|
+
for (const a of [-0.84, -0.28, 0.28, 0.84]) {
|
|
940
|
+
const c = polar(a, 0.9);
|
|
941
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.14, ry: 0.07, rot: a + 0.5, fill: "#7cc76a", stroke: p.outline, lw: ow * 0.7 });
|
|
942
|
+
}
|
|
943
|
+
;
|
|
944
|
+
[-1.12, -0.56, 0, 0.56, 1.12].forEach((a, i) => {
|
|
945
|
+
const c = polar(a, 0.9);
|
|
946
|
+
const r = i === 2 ? 0.13 : 0.11;
|
|
947
|
+
for (let k = 0; k < 5; k++) {
|
|
948
|
+
const t = a + k / 5 * Math.PI * 2;
|
|
949
|
+
out.push({ k: "circle", x: c.x + Math.sin(t) * r, y: c.y - Math.cos(t) * r, r: r * 0.82, fill: cols[i % cols.length], stroke: p.outline, lw: ow * 0.7 });
|
|
950
|
+
}
|
|
951
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.62, fill: cols[i % cols.length] });
|
|
952
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.42, fill: p.accent });
|
|
953
|
+
});
|
|
954
|
+
}
|
|
886
955
|
if (has("helmet")) {
|
|
887
956
|
for (const s of SIDES) out.push({ k: "rect", x: s * 1, y: 0.05, w: 0.26, h: 0.56, r: 0.12, fill: p.accent, ...ol });
|
|
888
957
|
out.push({ k: "path", d: "M-0.62 -0.62 Q-0.3 -0.92 0.12 -0.9", stroke: WHITE, lw: 0.1, alpha: 0.9 });
|
|
@@ -947,6 +1016,8 @@ function headShape(def, ol) {
|
|
|
947
1016
|
return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
|
|
948
1017
|
case "bucket":
|
|
949
1018
|
return { k: "path", d: roundedPolygon(pts(-0.94, 0.96, -0.98, -0.36, -0.62, -1.04, 0.62, -1.04, 0.98, -0.36, 0.94, 0.96), 0.42), fill, ...ol };
|
|
1019
|
+
case "snout-long":
|
|
1020
|
+
return { k: "path", d: smoothClosed(SNOUT_HEAD, 0.95), fill, ...ol };
|
|
950
1021
|
case "bulb":
|
|
951
1022
|
return {
|
|
952
1023
|
k: "path",
|
|
@@ -965,6 +1036,19 @@ function headShape(def, ol) {
|
|
|
965
1036
|
};
|
|
966
1037
|
}
|
|
967
1038
|
}
|
|
1039
|
+
var SNOUT_HEAD = pts(0.7, -0.88, 0.12, -1.04, -0.3, -0.98, -0.72, -0.74, -1.2, -0.56, -1.5, -0.32, -1.6, 0.04, -1.44, 0.42, -0.9, 0.72, -0.2, 0.92, 0.5, 0.88, 0.96, 0.56, 1.08, 0, 0.98, -0.5);
|
|
1040
|
+
function eyeSpots(def, ex, ey) {
|
|
1041
|
+
if (def.head === "snout-long")
|
|
1042
|
+
return [
|
|
1043
|
+
{ x: -0.36, y: -0.5, k: 0.95 },
|
|
1044
|
+
{ x: 0.26, y: -0.48, k: 1.1 }
|
|
1045
|
+
];
|
|
1046
|
+
return SIDES.map((s) => ({ x: s * ex, y: ey, k: 1 }));
|
|
1047
|
+
}
|
|
1048
|
+
function cheekSpots(def) {
|
|
1049
|
+
if (def.head === "snout-long") return pts(-0.66, -0.14, 0.66, 0.1);
|
|
1050
|
+
return SIDES.map((s) => ({ x: s * 0.64, y: 0.3 }));
|
|
1051
|
+
}
|
|
968
1052
|
function headEllipse(def) {
|
|
969
1053
|
if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
|
|
970
1054
|
if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
|
|
@@ -1045,6 +1129,10 @@ function ears(def, ow) {
|
|
|
1045
1129
|
out.push({ k: "ellipse", x: s * 1.04, y: -0.22, rx: 0.4, ry: 0.18, rot: s * 0.32, fill: p.skin ?? p.body, ...ol });
|
|
1046
1130
|
out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
|
|
1047
1131
|
break;
|
|
1132
|
+
case "pixie":
|
|
1133
|
+
out.push({ k: "path", d: roundedPolygon(pts(s * 0.86, -0.22, s * 1.36, -0.5, s * 1.02, 0.26), 0.28), fill: p.skin ?? p.body, ...ol });
|
|
1134
|
+
out.push({ k: "line", pts: [s * 0.98, -0.12, s * 1.16, -0.3], stroke: p.accent, lw: ow * 0.8, alpha: 0.5 });
|
|
1135
|
+
break;
|
|
1048
1136
|
case "human":
|
|
1049
1137
|
out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
|
|
1050
1138
|
break;
|
|
@@ -1077,15 +1165,15 @@ function eyes(def, ow) {
|
|
|
1077
1165
|
const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
|
|
1078
1166
|
switch (f2.eyes) {
|
|
1079
1167
|
case "dot":
|
|
1080
|
-
for (const
|
|
1081
|
-
out.push({ k: "ellipse", x:
|
|
1082
|
-
out.push(shine(
|
|
1168
|
+
for (const e of eyeSpots(def, ex, ey)) {
|
|
1169
|
+
out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.12 * e.k, ry: 0.155 * e.k, fill: ink });
|
|
1170
|
+
out.push(shine(e.x + 0.04 * e.k, e.y - 0.06 * e.k, 0.045 * e.k));
|
|
1083
1171
|
}
|
|
1084
1172
|
break;
|
|
1085
1173
|
case "bead":
|
|
1086
|
-
for (const
|
|
1087
|
-
out.push({ k: "ellipse", x:
|
|
1088
|
-
out.push(shine(
|
|
1174
|
+
for (const e of eyeSpots(def, ex, ey)) {
|
|
1175
|
+
out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.16 * e.k, ry: 0.22 * e.k, fill: ink });
|
|
1176
|
+
out.push(shine(e.x + 0.05 * e.k, e.y - 0.08 * e.k, 0.065 * e.k));
|
|
1089
1177
|
}
|
|
1090
1178
|
break;
|
|
1091
1179
|
case "big":
|
|
@@ -1194,6 +1282,21 @@ function mouth(def, ow) {
|
|
|
1194
1282
|
out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
|
|
1195
1283
|
out.push({ k: "path", d: "M-0.3 0.12 Q0 -0.02 0.3 0.12 Q0.36 0.52 0.04 0.76 Q0.08 0.52 -0.08 0.46 Q-0.28 0.38 -0.3 0.12 Z", fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
|
|
1196
1284
|
break;
|
|
1285
|
+
case "toothy": {
|
|
1286
|
+
out.push({ k: "path", d: "M-1.4 0.1 Q-0.5 0.36 0.5 0.2 Q0.64 0.16 0.68 0.02 Q0.78 0.28 0.52 0.38 Q-0.4 0.78 -1.3 0.26 Z", fill: dark, stroke: dark, lw: ow * 0.6 });
|
|
1287
|
+
out.push({ k: "ellipse", x: -0.38, y: 0.5, rx: 0.24, ry: 0.08, fill: TONGUE });
|
|
1288
|
+
for (const t of [0.12, 0.27, 0.42, 0.57, 0.72, 0.86]) {
|
|
1289
|
+
const x = (1 - t) * (1 - t) * -1.4 + 2 * (1 - t) * t * -0.5 + t * t * 0.5;
|
|
1290
|
+
const y = (1 - t) * (1 - t) * 0.1 + 2 * (1 - t) * t * 0.36 + t * t * 0.2 - 0.01;
|
|
1291
|
+
out.push({ k: "path", d: roundedPolygon(pts(x - 0.08, y, x + 0.08, y, x, y + 0.14), 0.3), fill: WHITE });
|
|
1292
|
+
}
|
|
1293
|
+
for (const t of [0.3, 0.7]) {
|
|
1294
|
+
const x = (1 - t) * (1 - t) * 0.52 + 2 * (1 - t) * t * -0.4 + t * t * -1.3;
|
|
1295
|
+
const y = (1 - t) * (1 - t) * 0.38 + 2 * (1 - t) * t * 0.78 + t * t * 0.26 + 0.01;
|
|
1296
|
+
out.push({ k: "path", d: roundedPolygon(pts(x - 0.07, y, x + 0.07, y, x, y - 0.12), 0.3), fill: WHITE });
|
|
1297
|
+
}
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1197
1300
|
case "jaws": {
|
|
1198
1301
|
out.push({ k: "path", d: "M-0.5 0.3 Q0 0.5 0.5 0.3 Q0.38 0.72 0 0.72 Q-0.38 0.72 -0.5 0.3 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
|
|
1199
1302
|
for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
|
|
@@ -1220,7 +1323,10 @@ var BAND_BODY = {
|
|
|
1220
1323
|
"9-12": { head: 0.5, neck: 0.92, arm: 0.17, leg: 0.21, hand: 0.125, foot: 0.9, shoulderMin: 0.36, shoulderPad: 0.07, hipMin: 0.32, hipPad: 0.08, bottom: 0.1, bulge: 1, armsOverHead: false }
|
|
1221
1324
|
};
|
|
1222
1325
|
var DEFAULT_BAND = "6-8";
|
|
1223
|
-
var bodyBand = (def) =>
|
|
1326
|
+
var bodyBand = (def, shown) => {
|
|
1327
|
+
if (shown !== void 0 && def.band !== "all" && def.alsoIn?.includes(shown) && shown in BAND_BODY) return shown;
|
|
1328
|
+
return def.band in BAND_BODY ? def.band : DEFAULT_BAND;
|
|
1329
|
+
};
|
|
1224
1330
|
var HEAD = BAND_BODY[DEFAULT_BAND].head;
|
|
1225
1331
|
var MAX_TILT = 0.3;
|
|
1226
1332
|
var TILT_DEAD_ZONE = 0.12;
|
|
@@ -1314,7 +1420,7 @@ function headTilt(body, at, angle, U) {
|
|
|
1314
1420
|
return 0;
|
|
1315
1421
|
}
|
|
1316
1422
|
function avatarShapes(body, def, box, opts = {}) {
|
|
1317
|
-
const band = bodyBand(def);
|
|
1423
|
+
const band = bodyBand(def, opts.band);
|
|
1318
1424
|
const bb = BAND_BODY[band];
|
|
1319
1425
|
const F = frameOf(body, box, { ...opts, band });
|
|
1320
1426
|
if (!F) return [];
|
|
@@ -1346,8 +1452,8 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1346
1452
|
out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
|
|
1347
1453
|
out.push({ k: "line", pts: flat, stroke: color, lw: w });
|
|
1348
1454
|
};
|
|
1349
|
-
const legW = bb.leg * U;
|
|
1350
|
-
const armW = bb.arm * U;
|
|
1455
|
+
const legW = bb.leg * U * (has("strong-legs") ? 1.3 : 1);
|
|
1456
|
+
const armW = bb.arm * U * (has("tiny-arms") ? 0.6 : 1);
|
|
1351
1457
|
const pad = Math.max(bb.arm / 0.22, 0.9);
|
|
1352
1458
|
if (F.hipsOk) {
|
|
1353
1459
|
for (const [hip, kneeI, ankleI, side] of [
|
|
@@ -1361,7 +1467,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1361
1467
|
const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
|
|
1362
1468
|
limb(legs, pts2, legW, p.limbs);
|
|
1363
1469
|
if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
|
|
1364
|
-
if (ankle.ok) legs.push(
|
|
1470
|
+
if (ankle.ok) legs.push(...feet(def, ankle, F, side, ow, bb.foot));
|
|
1365
1471
|
}
|
|
1366
1472
|
}
|
|
1367
1473
|
for (const [sh, elbowI, wristI, side] of [
|
|
@@ -1394,7 +1500,20 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1394
1500
|
out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.2 * U, ry: 0.09 * U, rot: Math.atan2(fd.y, fd.x), fill: p.hands ?? p.accent, ...ol });
|
|
1395
1501
|
}
|
|
1396
1502
|
}
|
|
1397
|
-
|
|
1503
|
+
if (has("wand") && side === 1 && !opts.portrait) out.push(...wand(def, hand, norm(add(mul(dir, 0.4), sub(mul(r, 0.6), d))), U, ow));
|
|
1504
|
+
const handR = bb.hand * U * (has("tiny-arms") ? 0.72 : 1);
|
|
1505
|
+
if (has("tiny-arms")) {
|
|
1506
|
+
for (const a of [-0.45, 0.45]) {
|
|
1507
|
+
const c = Math.cos(a);
|
|
1508
|
+
const s = Math.sin(a);
|
|
1509
|
+
const cd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
|
|
1510
|
+
const base = add(hand, mul(cd, handR * 0.75));
|
|
1511
|
+
const tip = add(hand, mul(cd, handR * 1.6));
|
|
1512
|
+
const nrm = { x: -cd.y * handR * 0.32, y: cd.x * handR * 0.32 };
|
|
1513
|
+
out.push({ k: "path", d: roundedPolygon([add(base, nrm), tip, sub(base, nrm)], 0.2), fill: p.belly, ...ol });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
out.push({ k: "circle", x: hand.x, y: hand.y, r: handR, fill: p.hands ?? p.limbs, ...ol });
|
|
1398
1517
|
}
|
|
1399
1518
|
if (has("wings")) {
|
|
1400
1519
|
for (const s of [-1, 1]) {
|
|
@@ -1409,6 +1528,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1409
1528
|
back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
|
|
1410
1529
|
}
|
|
1411
1530
|
}
|
|
1531
|
+
if (has("wings-fairy")) back.push(...fairyWings(def, T, ht, U, ow));
|
|
1412
1532
|
if (has("cape")) {
|
|
1413
1533
|
const yb = L + 0.8 * U;
|
|
1414
1534
|
const wb = hb + 0.36 * U;
|
|
@@ -1626,6 +1746,7 @@ function avatarShapes(body, def, box, opts = {}) {
|
|
|
1626
1746
|
const k = 0.15 * U;
|
|
1627
1747
|
torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle - 0.2, s: k, items: [{ k: "path", d: STAR, fill: def.extraColors?.[0] ?? "#ffd84a", stroke: p.outline, lw: ow * 0.8 / k }] });
|
|
1628
1748
|
}
|
|
1749
|
+
if (has("petals")) torso.push(...petals(def, T, ht, hb, bulge, L, y1, U, ow, bb));
|
|
1629
1750
|
if (has("scarf")) {
|
|
1630
1751
|
const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
|
|
1631
1752
|
const c = T(0, y);
|
|
@@ -1650,6 +1771,18 @@ function cachedHead(def, ow) {
|
|
|
1650
1771
|
}
|
|
1651
1772
|
return shapes;
|
|
1652
1773
|
}
|
|
1774
|
+
function feet(def, ankle, F, side, ow, k) {
|
|
1775
|
+
const shoe = foot(def, ankle, F, side, ow, k);
|
|
1776
|
+
if (!def.bodyExtras.includes("strong-legs")) return [shoe];
|
|
1777
|
+
const U = F.U * k;
|
|
1778
|
+
const c = add(add(ankle, mul(F.d, 0.07 * U)), mul(F.r, side * 0.06 * U));
|
|
1779
|
+
const out = [shoe];
|
|
1780
|
+
for (const t of [-1, 0, 1]) {
|
|
1781
|
+
const q = add(add(c, mul(F.r, t * 0.12 * U)), mul(F.d, 0.09 * U));
|
|
1782
|
+
out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.05 * U, ry: 0.045 * U, rot: F.angle, fill: def.palette.belly, stroke: def.palette.outline, lw: ow * 0.8 });
|
|
1783
|
+
}
|
|
1784
|
+
return out;
|
|
1785
|
+
}
|
|
1653
1786
|
function foot(def, ankle, F, side, ow, k) {
|
|
1654
1787
|
const p = def.palette;
|
|
1655
1788
|
const { d, r } = F;
|
|
@@ -1803,6 +1936,30 @@ function tail(def, F, hb, ow) {
|
|
|
1803
1936
|
});
|
|
1804
1937
|
break;
|
|
1805
1938
|
}
|
|
1939
|
+
case "thick": {
|
|
1940
|
+
const pts2 = TP([
|
|
1941
|
+
[hb * 0.3, L * 0.42],
|
|
1942
|
+
[hb + 0.42 * U, L * 0.62],
|
|
1943
|
+
[hb + 0.92 * U, L * 0.96],
|
|
1944
|
+
[hb + 1.32 * U, L * 1.08],
|
|
1945
|
+
[hb + 1.44 * U, L * 0.98],
|
|
1946
|
+
[hb + 0.98 * U, L * 1.2],
|
|
1947
|
+
[hb + 0.4 * U, L * 1.22],
|
|
1948
|
+
[hb * 0.3, L * 1.12]
|
|
1949
|
+
]);
|
|
1950
|
+
out.push({ k: "path", d: smoothClosed(pts2, 0.8), fill: p.body, ...ol });
|
|
1951
|
+
const c = def.extraColors?.[0] ?? p.accent;
|
|
1952
|
+
for (const [x, y, l] of [
|
|
1953
|
+
[0.28, 0.58, 0.26],
|
|
1954
|
+
[0.62, 0.8, 0.22],
|
|
1955
|
+
[0.96, 1, 0.16]
|
|
1956
|
+
]) {
|
|
1957
|
+
const a = T(hb + x * U, L * y + 0.03 * U);
|
|
1958
|
+
const b = T(hb + (x - 0.1) * U, L * y + l * U);
|
|
1959
|
+
out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.09 * U });
|
|
1960
|
+
}
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1806
1963
|
case "spiky":
|
|
1807
1964
|
case "arrow": {
|
|
1808
1965
|
const pts2 = TP([
|
|
@@ -1835,6 +1992,81 @@ function tail(def, F, hb, ow) {
|
|
|
1835
1992
|
}
|
|
1836
1993
|
return out;
|
|
1837
1994
|
}
|
|
1995
|
+
function wand(def, hand, wd, U, ow) {
|
|
1996
|
+
const p = def.palette;
|
|
1997
|
+
const from = add(hand, mul(wd, -0.08 * U));
|
|
1998
|
+
const tip = add(hand, mul(wd, 0.52 * U));
|
|
1999
|
+
const k = 0.2 * U;
|
|
2000
|
+
return [
|
|
2001
|
+
{ k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: p.outline, lw: 0.06 * U + ow * 2 },
|
|
2002
|
+
{ k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: "#fff3d6", lw: 0.06 * U },
|
|
2003
|
+
{ k: "circle", x: tip.x, y: tip.y, r: 0.24 * U, fill: p.accent, alpha: 0.28 },
|
|
2004
|
+
{ k: "group", x: tip.x, y: tip.y, rot: Math.atan2(wd.y, wd.x) + Math.PI / 2, s: k, items: [{ k: "path", d: STAR, fill: p.accent, stroke: p.outline, lw: ow * 0.9 / k }] }
|
|
2005
|
+
];
|
|
2006
|
+
}
|
|
2007
|
+
function fairyWings(def, T, ht, U, ow) {
|
|
2008
|
+
const p = def.palette;
|
|
2009
|
+
const fill = def.extraColors?.[0] ?? "#cdeeff";
|
|
2010
|
+
const out = [];
|
|
2011
|
+
for (const s of [-1, 1]) {
|
|
2012
|
+
const lobes = [
|
|
2013
|
+
// Upper: big, up and out.
|
|
2014
|
+
[
|
|
2015
|
+
[s * ht * 0.3, 0.12 * U],
|
|
2016
|
+
[s * (ht + 0.42 * U), -0.66 * U],
|
|
2017
|
+
[s * (ht + 1 * U), -0.74 * U],
|
|
2018
|
+
[s * (ht + 1.02 * U), -0.18 * U],
|
|
2019
|
+
[s * (ht + 0.5 * U), 0.26 * U]
|
|
2020
|
+
],
|
|
2021
|
+
// Lower: smaller, down and out.
|
|
2022
|
+
[
|
|
2023
|
+
[s * ht * 0.4, 0.3 * U],
|
|
2024
|
+
[s * (ht + 0.6 * U), 0.36 * U],
|
|
2025
|
+
[s * (ht + 0.74 * U), 0.78 * U],
|
|
2026
|
+
[s * (ht + 0.3 * U), 0.9 * U]
|
|
2027
|
+
]
|
|
2028
|
+
];
|
|
2029
|
+
for (const lobe of lobes) {
|
|
2030
|
+
const d = smoothClosed(lobe.map(([x, y]) => T(x, y)), 0.9);
|
|
2031
|
+
out.push({ k: "path", d, fill, alpha: 0.6 });
|
|
2032
|
+
out.push({ k: "path", d, stroke: p.outline, lw: ow * 0.9 });
|
|
2033
|
+
const inner = lobe[Math.floor(lobe.length / 2)];
|
|
2034
|
+
const a = T(lobe[0][0], lobe[0][1]);
|
|
2035
|
+
const b = T(lobe[0][0] + (inner[0] - lobe[0][0]) * 0.75, lobe[0][1] + (inner[1] - lobe[0][1]) * 0.75);
|
|
2036
|
+
out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: "#ffffff", lw: ow * 0.9, alpha: 0.85 });
|
|
2037
|
+
}
|
|
2038
|
+
for (const [x, y, r] of [
|
|
2039
|
+
[ht + 0.72 * U, -0.42 * U, 0.075 * U],
|
|
2040
|
+
[ht + 0.44 * U, -0.16 * U, 0.05 * U],
|
|
2041
|
+
[ht + 0.5 * U, 0.6 * U, 0.055 * U]
|
|
2042
|
+
]) {
|
|
2043
|
+
const q = T(s * x, y);
|
|
2044
|
+
out.push({ k: "group", x: q.x, y: q.y, s: r, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return out;
|
|
2048
|
+
}
|
|
2049
|
+
function petals(def, T, ht, hb, bulge, L, y1, U, ow, bb) {
|
|
2050
|
+
const p = def.palette;
|
|
2051
|
+
const second = def.extraColors?.[1] ?? p.belly;
|
|
2052
|
+
const out = [];
|
|
2053
|
+
const ang = Math.atan2(T(1, 0).y - T(0, 0).y, T(1, 0).x - T(0, 0).x);
|
|
2054
|
+
const n = 5;
|
|
2055
|
+
const w = Math.max(hb, bulge * 0.92) * 1.08;
|
|
2056
|
+
const y = (L * 0.8 + y1) / 2;
|
|
2057
|
+
const order = [0, 4, 1, 3, 2];
|
|
2058
|
+
for (const i of order) {
|
|
2059
|
+
const t = i / (n - 1) - 0.5;
|
|
2060
|
+
const c = T(t * 2 * w, y + (0.5 - Math.abs(t)) * 0.1 * U);
|
|
2061
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: w * 0.3, ry: 0.34 * U, rot: ang - t * 0.6, fill: i % 2 ? second : p.body, stroke: p.outline, lw: ow });
|
|
2062
|
+
}
|
|
2063
|
+
const cy = Math.max(0.04, bb.head * (1 - bb.neck) - 0.04) * U;
|
|
2064
|
+
for (const t of [-1, 1, 0]) {
|
|
2065
|
+
const c = T(t * ht * 0.32, cy + (t ? 0 : 0.05 * U));
|
|
2066
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: ht * 0.26, ry: 0.12 * U, rot: ang + t * 0.5, fill: t ? second : p.body, stroke: p.outline, lw: ow * 0.9 });
|
|
2067
|
+
}
|
|
2068
|
+
return out;
|
|
2069
|
+
}
|
|
1838
2070
|
|
|
1839
2071
|
// src/poses.ts
|
|
1840
2072
|
var STAND = {
|
|
@@ -2016,9 +2248,9 @@ function blend(a, b, u) {
|
|
|
2016
2248
|
var AVATAR_IDS = AVATARS.map((a) => a.id);
|
|
2017
2249
|
var MASCOT_ID = "mascot";
|
|
2018
2250
|
var AVATAR_BANDS = {
|
|
2019
|
-
"3-5": [
|
|
2020
|
-
"6-8": [
|
|
2021
|
-
"9-12": [
|
|
2251
|
+
"3-5": ["bunny", "bear", "panda", "penguin", "koala", "frog", "cat", "owl", "chick", "puppy", "lamb", "duckling", "lion", "trex", "fairy"],
|
|
2252
|
+
"6-8": ["fox", "lion", "dragon", "unicorn", "tiger", "monkey", "dinosaur", "trex", "octopus", "ghost", "shark", "wolf", "parrot", "fairy"],
|
|
2253
|
+
"9-12": ["robot", "astronaut", "ninja", "skater", "alien", "explorer", "wizard", "knight", "racer", "pirate", "diver", "hero"]
|
|
2022
2254
|
};
|
|
2023
2255
|
var BY_ID = new Map(AVATARS.map((a) => [a.id, a]));
|
|
2024
2256
|
var DEFAULT_AVATAR = BY_ID.get("fox");
|
|
@@ -2029,9 +2261,13 @@ function avatarsFor(ageRange) {
|
|
|
2029
2261
|
return ids.map((id) => BY_ID.get(id));
|
|
2030
2262
|
}
|
|
2031
2263
|
var bandOf = (id) => BY_ID.get(id)?.band ?? DEFAULT_AVATAR.band;
|
|
2264
|
+
var bandsOf = (id) => {
|
|
2265
|
+
const known = BY_ID.has(id) ? id : DEFAULT_AVATAR.id;
|
|
2266
|
+
return AGE_RANGES.filter((r) => AVATAR_BANDS[r].includes(known));
|
|
2267
|
+
};
|
|
2032
2268
|
var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
|
|
2033
2269
|
function drawAvatar(ctx, body, def, box, opts = {}) {
|
|
2034
|
-
const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
|
|
2270
|
+
const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored, band: opts.band });
|
|
2035
2271
|
if (!shapes.length) return false;
|
|
2036
2272
|
ctx.save();
|
|
2037
2273
|
if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
|
|
@@ -2039,25 +2275,27 @@ function drawAvatar(ctx, body, def, box, opts = {}) {
|
|
|
2039
2275
|
ctx.restore();
|
|
2040
2276
|
return true;
|
|
2041
2277
|
}
|
|
2042
|
-
function iconShapes(def, size) {
|
|
2278
|
+
function iconShapes(def, size, band) {
|
|
2043
2279
|
const d = resolve(def);
|
|
2044
2280
|
const body = neutralPose();
|
|
2045
|
-
const
|
|
2046
|
-
const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
|
|
2047
|
-
const
|
|
2281
|
+
const shown = bodyBand(d, band);
|
|
2282
|
+
const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band: shown });
|
|
2283
|
+
const hb = bounds(headShapes(d, 0.05));
|
|
2284
|
+
const top = -hb.minY;
|
|
2048
2285
|
const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
|
|
2049
|
-
const
|
|
2286
|
+
const dx = d.head === "snout-long" ? (hb.minX + hb.maxX) / 2 * headR : 0;
|
|
2287
|
+
const k = headR / BAND_BODY[shown].head / unit.U;
|
|
2050
2288
|
const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
|
|
2051
|
-
const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
|
|
2052
|
-
return avatarShapes(body, d, box, { portrait: true });
|
|
2289
|
+
const box = { x: size / 2 - dx - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
|
|
2290
|
+
return avatarShapes(body, d, box, { portrait: true, band: shown });
|
|
2053
2291
|
}
|
|
2054
|
-
function drawAvatarIcon(ctx, def, x, y, size) {
|
|
2292
|
+
function drawAvatarIcon(ctx, def, x, y, size, opts = {}) {
|
|
2055
2293
|
ctx.save();
|
|
2056
2294
|
ctx.beginPath();
|
|
2057
2295
|
ctx.rect(x, y, size, size);
|
|
2058
2296
|
ctx.clip();
|
|
2059
2297
|
ctx.translate(x, y);
|
|
2060
|
-
drawShapes(ctx, iconShapes(def, size));
|
|
2298
|
+
drawShapes(ctx, iconShapes(def, size, opts.band));
|
|
2061
2299
|
ctx.restore();
|
|
2062
2300
|
}
|
|
2063
2301
|
var clipCount = 0;
|
|
@@ -2066,7 +2304,7 @@ function avatarIconSvg(def, size, opts = {}) {
|
|
|
2066
2304
|
const s = Math.round(size * 100) / 100;
|
|
2067
2305
|
const bg = opts.background ? `<rect width="${s}" height="${s}" rx="${opts.radius ?? 0}" fill="${opts.background}"/>` : "";
|
|
2068
2306
|
const clip = `av-${d.id}-${String(s).replace(".", "_")}-${++clipCount}`;
|
|
2069
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}"><title>${d.name.en}</title><defs><clipPath id="${clip}"><rect width="${s}" height="${s}" rx="${opts.radius ?? 0}"/></clipPath></defs>` + bg + `<g clip-path="url(#${clip})">${shapesToSvg(iconShapes(d, size))}</g></svg>`;
|
|
2307
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}"><title>${d.name.en}</title><defs><clipPath id="${clip}"><rect width="${s}" height="${s}" rx="${opts.radius ?? 0}"/></clipPath></defs>` + bg + `<g clip-path="url(#${clip})">${shapesToSvg(iconShapes(d, size, opts.band))}</g></svg>`;
|
|
2070
2308
|
}
|
|
2071
2309
|
var avatarIconDataUri = (def, size, opts) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(avatarIconSvg(def, size, opts))}`;
|
|
2072
2310
|
function smoothBody(prev, next, amount = 0.5) {
|
|
@@ -2188,6 +2426,7 @@ export {
|
|
|
2188
2426
|
avatarShapes,
|
|
2189
2427
|
avatarsFor,
|
|
2190
2428
|
bandOf,
|
|
2429
|
+
bandsOf,
|
|
2191
2430
|
blend,
|
|
2192
2431
|
bodyBand,
|
|
2193
2432
|
bounds,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabi-play/avatars",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "39 child-friendly avatars: Fabi the fox in every age band, 12 per band, a T-rex and a fairy, drawn from a body pose, on canvas or as SVG portraits for the pickers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabi-play",
|
|
7
7
|
"avatars",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@fabi-play/protocol": "^0.
|
|
35
|
+
"@fabi-play/protocol": "^0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "24.10.1",
|