@fabi-play/sdk 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 CHANGED
@@ -45,6 +45,38 @@ From the CDN, pinned to an exact version with its Subresource Integrity hash (li
45
45
  | `fabi-play.d.ts` | types for script-tag games (`FabiPlay.PlayerFrame`, …) |
46
46
  | `fabi-play.schema.json` | the JSON Schema of `fabi-play.json` |
47
47
 
48
+ ## The cover
49
+
50
+ The game's card in the catalogue (the TV's carousel, the phone, the admin) is its cover, an image you draw. Put it
51
+ with the game's files and name it in `fabi-play.json`:
52
+
53
+ ```json
54
+ "cover": "cover.jpg"
55
+ ```
56
+
57
+ - **16:9**, 1280×720 recommended, at most 150 KB (`COVER_MAX_BYTES`). `.jpg`, `.jpeg`, `.png` or `.webp`; JPG
58
+ (quality about 80) is the safest on old TV browsers.
59
+ - **A path relative to the game's address**, like `entry`: no scheme, no leading `/`, no `\`, no `.` or `..`
60
+ segments, extension in lowercase, up to 128 characters.
61
+ - **Optional.** Fabi Play's review checks the file (it exists, it is an image, its size, its shape: 16:9 within 2%,
62
+ `COVER_ASPECT`, `COVER_ASPECT_TOLERANCE`) and warns about a problem; without a working cover the catalogue shows
63
+ another picture of the game.
64
+
65
+ ## Portrait (a phone held upright)
66
+
67
+ A phone alone («Gioca qui / Play here») is held upright, close to the child (about one step away, upper body in
68
+ view), and offers only games that say they can be played in portrait:
69
+
70
+ ```json
71
+ "orientations": ["landscape", "portrait"]
72
+ ```
73
+
74
+ - **Optional**, `["landscape"]` by default; non-empty, no repeats, from `"landscape"` and `"portrait"`.
75
+ - `play.orientation` (and the `start` event's `orientation`) says the one the game started in: `'portrait'` or
76
+ `'landscape'` (also what older hosts mean). Use it to choose a layout, but size everything from the real viewport
77
+ (`innerWidth` × `innerHeight`).
78
+ - On a phone the child is close and often seen from the head to the knees: don't rely on the feet being visible.
79
+
48
80
  ## Fabi explains your game (intro)
49
81
 
50
82
  Before calibration, Fabi (the mascot) can explain your game on the TV in a few steps: a speech bubble over an image
@@ -127,6 +159,25 @@ opts in with `inputs` in `fabi-play.json`:
127
159
  `"remote"` must work with the arrow keys alone and, where it has things to pick (bubbles, answers, buttons), let a
128
160
  pointer click them as well: pointer events, large hit areas, a hover highlight. Fabi Play's own menus, pickers
129
161
  and turn screen are clickable too.
162
+ - **The arrows as a free cursor (`"remoteCursor": true`).** A game picked or drawn with a pointer (a painting
163
+ game) can ask for the arrows to move a cursor instead of making gestures, with `"remoteCursor": true` in
164
+ `fabi-play.json` (only next to `"remote"` in `inputs`):
165
+
166
+ ```json
167
+ "inputs": ["body", "remote"], "remoteCursor": true
168
+ ```
169
+
170
+ Then, in remote mode, the screen draws a cursor above the game and moves it with the arrows: slowly at first, for
171
+ precision, faster the longer an arrow is held, stopping at the edges. OK presses it: held, it stays pressed until
172
+ released (OK held = paint); a quick OK is a click. Back is still Fabi Play's pause. The game gets it exactly like
173
+ a Magic Remote: `play.pointer` / `FabiPlay.hasPointer` is `true`, and the SDK makes ordinary pointer and mouse
174
+ events in the page at the cursor (`pointermove`/`mousemove`, `pointerover`/`pointerout`, `pointerdown`/`mousedown`
175
+ while OK goes down, `pointerup`/`mouseup` then `click` when it comes up; `pointerType` `'mouse'`; at a pause, a
176
+ break or a turn a held OK is let go with `pointercancel` and `mouseup`, no click). So a game written for the
177
+ Magic Remote's pointer works with the arrows unchanged, and the other way round. The arrows make no gestures
178
+ then: no lanes, no jump, no squat. `play.on('cursor', ({ x, y, down }) => …)` gives the raw position (fractions of
179
+ the page) if you want it, but the events are enough. Old TV browsers without `PointerEvent` get the mouse events
180
+ only: listen to both, or to mouse events.
130
181
  - **One remote is one child; several children take turns, a round each, if the game follows the contract.**
131
182
  Turns are not free: the game must follow the steps below, and say so in `fabi-play.json` with
132
183
  `"turns": true` (only next to `"remote"` in `inputs`): `"inputs": ["body", "remote"], "turns": true`. A game
@@ -159,6 +210,39 @@ opts in with `inputs` in `fabi-play.json`:
159
210
  - **Games aimed with the hands** (`movements: ["hands"]`, the `pointer()` adapter) should not list `"remote"`
160
211
  unless they offer a remote-friendly selection: a child with a remote has no hand cursor.
161
212
 
213
+ ## Saving a picture on the phone
214
+
215
+ A game can save a picture on the family's phone, in its gallery: the drawing at the end, the photo of a wall before
216
+ it is cleared.
217
+
218
+ ```js
219
+ if (play.canSavePicture) {
220
+ const result = await play.savePicture(canvas, { title: 'Il nostro muro' })
221
+ // 'sent' | 'unavailable' | 'too-big' | 'error'
222
+ }
223
+ play.on('capabilities', ({ pictures }) => showSaveButton(pictures))
224
+ ```
225
+
226
+ With the script tag: `FabiPlay.savePicture(canvas, { title })`, `FabiPlay.canSavePicture`,
227
+ `FabiPlay.on('capabilities', ({ pictures }) => …)`, and `canSavePicture` in the `start` event.
228
+
229
+ - **`canSavePicture`** is `true` when the screen says a phone that can save pictures is linked (the Fabi Play app;
230
+ the web phone can't, and nor can TV-only remote sessions). It can change mid-game (a phone joins, the last one
231
+ leaves): the `capabilities` event says so. `false` before the start and from older screens.
232
+ - **`savePicture(source, { title })`** takes a canvas or a data URL. A canvas is scaled down to 1920 px on its
233
+ longer side, drawn on white (JPEG has no transparency) and encoded as JPEG at quality 0.85 (lower, then smaller,
234
+ if it would be over 3 MiB). A data URL must be `data:image/jpeg;base64,…` or `data:image/png;base64,…`, at most
235
+ 3 MiB, and is sent as it is. `title` (optional, up to 60 characters) names the file. A canvas tainted by another
236
+ site's image can't be read: draw only your own images on it.
237
+ - **It resolves, never rejects**, with `'sent'` (the phone has it), `'unavailable'` (`canSavePicture` is false),
238
+ `'too-big'` (over 3 MiB) or `'error'` (not an image, a canvas that can't be read, the link dropped, no answer in
239
+ 2 minutes). Never make the children wait on it: show the picture "flying into the phone" and go on.
240
+ - **If the phone can't save it** (the parent denied the permission), Fabi Play tells the parent on the phone; the
241
+ game is told `'sent'` or `'error'`, and should not tell the children.
242
+ - **Privacy.** The picture goes only from the TV to the phone, over the direct link they already share (WebRTC,
243
+ encrypted), never through or to a server, and no copy is kept anywhere else. Only a JPEG or PNG image can travel
244
+ this way (the screen checks the data URL and its bytes): a game can send nothing else to the phone.
245
+
162
246
  Docs: https://docs.fabiplay.app.
163
247
 
164
248
  ## License
package/dist/avatars.d.ts CHANGED
@@ -106,21 +106,21 @@ export interface Palette {
106
106
  hair?: string;
107
107
  }
108
108
  /** The head's outline. */
109
- export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket";
109
+ export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket" | "snout-long";
110
110
  /** The ears (or what is in their place: horns, antennae, a fin). */
111
- export type EarKind = "none" | "pointy" | "round" | "big-round" | "long" | "side" | "tufts" | "antenna" | "antennae" | "horns" | "unicorn" | "floppy" | "droopy" | "fin" | "human";
111
+ export type EarKind = "none" | "pointy" | "round" | "big-round" | "long" | "side" | "tufts" | "antenna" | "antennae" | "horns" | "unicorn" | "floppy" | "droopy" | "fin" | "human" | "pixie";
112
112
  /** How the eyes are drawn. */
113
113
  export type EyeKind = "dot" | "big" | "mask" | "screen" | "almond" | "visor" | "calm" | "slit" | "goggle" | "bead";
114
114
  /** How the mouth (or beak) is drawn. */
115
- export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws";
115
+ export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws" | "toothy";
116
116
  /** The patch around the nose and mouth. */
117
117
  export type MuzzleKind = "none" | "oval" | "heart" | "monkey" | "disc" | "cheeks" | "band";
118
118
  /** The tail, behind the body. */
119
- export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume";
119
+ export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume" | "thick";
120
120
  /** Things on the head. */
121
- 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";
121
+ 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";
122
122
  /** Things on the body. */
123
- 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";
123
+ 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";
124
124
  /** An avatar's face. */
125
125
  export interface FaceDef {
126
126
  /** How the eyes are drawn. */
@@ -142,7 +142,7 @@ export interface FaceDef {
142
142
  /** Eyebrows of this colour (people): a little attitude. */
143
143
  brows?: string;
144
144
  }
145
- /** The age band an avatar is for, or 'all': offered in every band (Fabi the fox, the app's mascot). */
145
+ /** The age band an avatar is for, or 'all': of no band (Fabi the fox, the app's mascot: the host, in no picker). */
146
146
  export type AvatarBand = AgeRange | "all";
147
147
  /** An avatar, as data: one renderer draws any definition. */
148
148
  export interface AvatarDef {
@@ -154,11 +154,17 @@ export interface AvatarDef {
154
154
  it: string;
155
155
  };
156
156
  /**
157
- * The age band the avatar is for: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in every
158
- * band's list, drawn with the 6–8 proportions.
157
+ * The age band the avatar is for, its home: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in
158
+ * no band's list (the mascot), drawn with the 6–8 proportions. See alsoIn for an avatar offered in other bands too.
159
159
  */
160
160
  band: AvatarBand;
161
- /** A special avatar (the mascot): first in every band's list; pickers may mark it. */
161
+ /**
162
+ * Other age bands the avatar is also offered in (listed in their AVATAR_BANDS too), besides its own band. Shown in
163
+ * one of them, it is drawn with that band's proportions (bodyBand(def, band)); its band stays its home: its style
164
+ * (outline), bandOf, and its proportions when the band shown is not known. Not for 'all' (the mascot).
165
+ */
166
+ alsoIn?: readonly AgeRange[];
167
+ /** A special avatar (the mascot): the host, not a player; in no band's list (AVATAR_BANDS). */
162
168
  special?: boolean;
163
169
  /** Its colours. */
164
170
  palette: Palette;
@@ -223,9 +229,12 @@ export interface BandBody {
223
229
  export declare const BAND_BODY: Record<AgeRange, BandBody>;
224
230
  export interface RigOptions {
225
231
  mirrored?: boolean;
226
- /** Portraits leave the tail out: in a head-and-shoulders crop it would only peek in as a stray shape. */
232
+ /** Portraits leave the tail and the wand out: in a head-and-shoulders crop they would only peek in as stray shapes. */
227
233
  portrait?: boolean;
228
- /** Body proportions (see BAND_BODY); 6–8 by default. avatarShapes takes the avatar's own band. */
234
+ /**
235
+ * Body proportions (see BAND_BODY); 6–8 by default. For avatarShapes, the band the avatar is shown in: it follows
236
+ * it only for an avatar offered there too (alsoIn, see bodyBand); otherwise the avatar's own band.
237
+ */
229
238
  band?: AgeRange;
230
239
  }
231
240
  /** The torso frame: origin between the shoulders, r to the right, d down along the torso; lengths in pixels. */
@@ -250,7 +259,10 @@ export declare function avatarShapes(body: Body, def: AvatarDef, box: Box, opts?
250
259
  export declare const DEFAULT_AVATAR: AvatarDef;
251
260
  /** The avatar with this id, or undefined. */
252
261
  export declare const avatarById: (id: string) => AvatarDef | undefined;
253
- /** The portrait: head and shoulders of the neutral pose, framed in a size × size square at (0, 0). */
254
- export declare function iconShapes(def: AvatarDef | string, size: number): Shape[];
262
+ /**
263
+ * The portrait: head and shoulders of the neutral pose, framed in a size × size square at (0, 0). band: the band the
264
+ * picker shows (see DrawOptions.band).
265
+ */
266
+ export declare function iconShapes(def: AvatarDef | string, size: number, band?: AgeRange): Shape[];
255
267
 
256
268
  export {};
package/dist/avatars.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  drawShapes,
7
7
  frameOf,
8
8
  iconShapes
9
- } from "./chunks/chunk-GFNOIOSX.js";
9
+ } from "./chunks/chunk-H3BKYVPL.js";
10
10
  export {
11
11
  BAND_BODY,
12
12
  DEFAULT_AVATAR,