@fabi-play/sdk 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 +76 -0
- package/README.md +86 -2
- package/dist/avatars.d.ts +256 -0
- package/dist/avatars.js +18 -0
- package/dist/chunks/chunk-57WXWAXG.js +2112 -0
- package/dist/fabi-play.d.ts +714 -0
- package/dist/fabi-play.js +1 -0
- package/dist/fabi-play.schema.json +176 -0
- package/dist/fabi-play.umd.js +2612 -0
- package/dist/fabi-play.umd.min.js +1 -0
- package/dist/index.d.ts +801 -0
- package/dist/index.js +490 -0
- package/dist/integrity.json +12 -0
- package/package.json +68 -3
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// fabi-play.js: the Fabi Play SDK as a script tag. It sets window.FabiPlay; these are its types.
|
|
2
|
+
// Generated by scripts/types.mjs from the sources: do not edit.
|
|
3
|
+
//
|
|
4
|
+
// Use it next to fabi-play.js, in a script (not a module) or a // @ts-check JavaScript file:
|
|
5
|
+
// /// <reference path="fabi-play.d.ts" />
|
|
6
|
+
// or list it in tsconfig.json / jsconfig.json "include" (or "files"). The types are FabiPlay.Point, FabiPlay.PlayerFrame, …
|
|
7
|
+
|
|
8
|
+
/** The Fabi Play SDK's types, for script-tag games: FabiPlay.PlayerFrame, FabiPlay.Point, FabiPlay.Gesture, … */
|
|
9
|
+
declare namespace FabiPlay {
|
|
10
|
+
/** Anything with Play's on(): a Play, or a stand-in in tests. */
|
|
11
|
+
export type On = Pick<Play, "on">;
|
|
12
|
+
/** What the child does, as the keyboard adapter names it. */
|
|
13
|
+
export type Action = "jump" | "squat" | "left" | "right" | "clap" | "arms-up";
|
|
14
|
+
/** Action → key code (KeyboardEvent.code: Space, Enter, ArrowUp, KeyW, Digit1…). */
|
|
15
|
+
export type KeyMap = Partial<Record<Action, string>>;
|
|
16
|
+
/** The fields of a KeyboardEvent for one key. */
|
|
17
|
+
export interface KeyInfo {
|
|
18
|
+
/** KeyboardEvent.key: ' ', 'Enter', 'ArrowLeft', 'a'… */
|
|
19
|
+
key: string;
|
|
20
|
+
/** KeyboardEvent.code: 'Space', 'Enter', 'ArrowLeft', 'KeyA'… */
|
|
21
|
+
code: string;
|
|
22
|
+
/** Legacy, still read by many games. */
|
|
23
|
+
keyCode: number;
|
|
24
|
+
}
|
|
25
|
+
/** Where the keyboard adapter sends its key events. */
|
|
26
|
+
export interface KeyTarget {
|
|
27
|
+
/** Sends a keydown or keyup. */
|
|
28
|
+
dispatch(type: "keydown" | "keyup", key: KeyInfo): void;
|
|
29
|
+
}
|
|
30
|
+
/** Options of keyboard(). */
|
|
31
|
+
export interface KeyboardOptions {
|
|
32
|
+
/** Whose movements (default 1); another player can get other keys (e.g. WASD). */
|
|
33
|
+
player?: number;
|
|
34
|
+
/** Where the keys go: by default the page (domKeys()). */
|
|
35
|
+
target?: KeyTarget;
|
|
36
|
+
/** The timer for taps and releases (tests). */
|
|
37
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
38
|
+
}
|
|
39
|
+
/** Where the pointer adapter sends its moves and clicks. */
|
|
40
|
+
export interface PointerTarget {
|
|
41
|
+
/** Screen size in px. */
|
|
42
|
+
size(): {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
/** The hand moved to x, y (px). */
|
|
47
|
+
move(x: number, y: number): void;
|
|
48
|
+
/** The hand stayed still long enough at x, y (px): a click. */
|
|
49
|
+
click(x: number, y: number): void;
|
|
50
|
+
/** Draws the hand: progress 0–1 while it stays still, to show the click coming. */
|
|
51
|
+
cursor(x: number, y: number, progress: number): void;
|
|
52
|
+
}
|
|
53
|
+
/** Options of pointer(). */
|
|
54
|
+
export interface PointerOptions {
|
|
55
|
+
/** Whose hand (default 1). */
|
|
56
|
+
player?: number;
|
|
57
|
+
/** Which hand drives the pointer; "any" picks the higher one (the one the child is raising). */
|
|
58
|
+
hand?: "left" | "right" | "any";
|
|
59
|
+
/** How long the hand must stay still to click. */
|
|
60
|
+
dwellMs?: number;
|
|
61
|
+
/** How still: a share of the screen's diagonal. */
|
|
62
|
+
radius?: number;
|
|
63
|
+
/** Where the events go: by default the page, with a round cursor (domPointer()). */
|
|
64
|
+
target?: PointerTarget;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One body: for each landmark x, y (0–1, y downwards) and visibility (0–1), so 33 × 3 numbers.
|
|
68
|
+
* In "player space": mirrored like a mirror, whatever camera took it, so a child moving to their left moves left.
|
|
69
|
+
*/
|
|
70
|
+
export type Body = number[];
|
|
71
|
+
/** The languages Fabi Play speaks: Italian and English. */
|
|
72
|
+
export type Locale = "it" | "en";
|
|
73
|
+
/**
|
|
74
|
+
* Age ranges: each has its own games. Only the range is kept, never a birth date: it is all the catalogue and the
|
|
75
|
+
* games need (simpler rules and no losing for the little ones, scores and challenges for the older ones).
|
|
76
|
+
*/
|
|
77
|
+
export type AgeRange = "3-5" | "6-8" | "9-12";
|
|
78
|
+
/** A message as it travels: the object plus the ns marker (a literal type, so declaration files need no constant). */
|
|
79
|
+
export type Wire<T> = T & {
|
|
80
|
+
ns: "fabi-play";
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Why the game stops, when it asks for a break:
|
|
84
|
+
* - level: a level is over (the best moment);
|
|
85
|
+
* - gameover: the game is over and a new one may start.
|
|
86
|
+
* Never at the game's own pause menu: a child who stops for a moment must not get an ad for it.
|
|
87
|
+
*/
|
|
88
|
+
export type BreakReason = "level" | "gameover";
|
|
89
|
+
/**
|
|
90
|
+
* The movements the host recognises, per player:
|
|
91
|
+
* - `jump`, then `land` when the feet are back on the ground;
|
|
92
|
+
* - `squat`, then `stand`;
|
|
93
|
+
* - `clap`: the hands meet;
|
|
94
|
+
* - `freeze` (the child stands still, a statue), then `move`;
|
|
95
|
+
* - `lane-left`, `lane-center`, `lane-right`: a sideways step into a lane (the child's own left, as in a mirror).
|
|
96
|
+
*/
|
|
97
|
+
export type Gesture = "jump" | "land" | "squat" | "stand" | "clap" | "freeze" | "move" | "lane-left" | "lane-center" | "lane-right";
|
|
98
|
+
/** A pose the whole body holds: arms up, arms wide, arms in a V, a star (arms and legs apart), arms down. */
|
|
99
|
+
export type Shape = "arms-up" | "arms-wide" | "arms-v" | "star" | "arms-down";
|
|
100
|
+
/** A point: in player space (0–1 of the camera image, y downwards) or, after toScreen, in fractions of the screen. */
|
|
101
|
+
export interface Point {
|
|
102
|
+
/** 0 left, 1 right. */
|
|
103
|
+
x: number;
|
|
104
|
+
/** 0 top, 1 bottom. */
|
|
105
|
+
y: number;
|
|
106
|
+
}
|
|
107
|
+
/** One player in a frame the game gets: the body, its hands as pointers, its shape, its avatar. */
|
|
108
|
+
export interface PlayerFrame {
|
|
109
|
+
/** The player, from 1. */
|
|
110
|
+
id: number;
|
|
111
|
+
/** The 33 landmarks, in player space (see Body; P names the indices). */
|
|
112
|
+
body: Body;
|
|
113
|
+
/**
|
|
114
|
+
* The left hand as a pointer, in fractions of the screen (0–1): the wrist, with the space the arms reach around the
|
|
115
|
+
* shoulders stretched to the whole screen and clamped to it, so a child reaches every corner without walking. Not
|
|
116
|
+
* where the wrist is on screen: draw on the body with toScreen(wrist). null when the camera doesn't see the wrist (or the child holds Fabi Play's crossed arms).
|
|
117
|
+
*/
|
|
118
|
+
leftHand: Point | null;
|
|
119
|
+
/** The right hand as a pointer, as leftHand. */
|
|
120
|
+
rightHand: Point | null;
|
|
121
|
+
/** The pose held right now, if any. */
|
|
122
|
+
shape: Shape | null;
|
|
123
|
+
/** Which lane the child stands in: -1 left, 0 centre, 1 right. */
|
|
124
|
+
lane: -1 | 0 | 1;
|
|
125
|
+
/**
|
|
126
|
+
* The avatar of player id (slot id − 1): the one the child picked on the phone, or the slot's default
|
|
127
|
+
* (defaultAvatarFor). It rides on every frame, so a game never misses a change and needs no extra message. Hosts
|
|
128
|
+
* before avatars don't send it; the SDK's avatarOf falls back to the slot's default then, and for ids it does not
|
|
129
|
+
* know.
|
|
130
|
+
*/
|
|
131
|
+
avatar?: string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What is behind the game's frame: 'video', the phone's camera video (mirror mode; the game paints no opaque
|
|
135
|
+
* background), or 'none', the screen's plain background (the game paints its own).
|
|
136
|
+
*/
|
|
137
|
+
export type Backdrop = "video" | "none";
|
|
138
|
+
/**
|
|
139
|
+
* Where the whole camera image is, in fractions of the game's viewport (0–1; it may reach past the edges when the
|
|
140
|
+
* image is cropped to fill the screen), with any backdrop, so avatars and hands are where the children stand (a
|
|
141
|
+
* "virtual mirror"). The screen decides it: with 'video', where the video is drawn (cover or contain); with 'none',
|
|
142
|
+
* the whole image contained at 92% of the viewport's height, bottom-aligned. Games don't compute it: they just use
|
|
143
|
+
* it. Body points are in player space (0–1 of the camera image, already mirrored like the video), so a point p is on
|
|
144
|
+
* screen at (x + p.x·w, y + p.y·h): see toScreen.
|
|
145
|
+
*/
|
|
146
|
+
export interface BackdropView {
|
|
147
|
+
/** Left edge, a fraction of the viewport's width. */
|
|
148
|
+
x: number;
|
|
149
|
+
/** Top edge, a fraction of the viewport's height. */
|
|
150
|
+
y: number;
|
|
151
|
+
/** Width, a fraction of the viewport's width. */
|
|
152
|
+
w: number;
|
|
153
|
+
/** Height, a fraction of the viewport's height. */
|
|
154
|
+
h: number;
|
|
155
|
+
}
|
|
156
|
+
/** Quality tiers for drawing, best first: what @fabi-play/engine calls engine.tier.name. */
|
|
157
|
+
export type DeviceTier = "high" | "medium" | "low";
|
|
158
|
+
/**
|
|
159
|
+
* What the screen knows about the device it runs on (init.device), so a game need not measure it again: each game
|
|
160
|
+
* has its own origin, so what one game measured and cached is not seen by the next. A hint, not an order: a game
|
|
161
|
+
* still lowers its tier during play when its frames are slow. Every field is optional; older hosts send none.
|
|
162
|
+
*/
|
|
163
|
+
export interface DeviceHint {
|
|
164
|
+
/** A starting quality tier: the TV platform's default, or lower when the screen's own drawing fell back to Canvas 2D. */
|
|
165
|
+
tier?: DeviceTier;
|
|
166
|
+
/** The display's refresh rate, Hz (50, 60, …), measured by the screen on idle frames. */
|
|
167
|
+
hz?: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What a game may add to its stats message (debug, only when init said stats): how it draws. renderer is a short
|
|
171
|
+
* lowercase name ('webgl2', 'webgl1', 'canvas2d', or another engine's own), up to 16 characters.
|
|
172
|
+
*/
|
|
173
|
+
export interface GameStats {
|
|
174
|
+
/** Frames the game drew in the last second (all of its drawing, not only the avatars). */
|
|
175
|
+
fps?: number;
|
|
176
|
+
/** What draws: 'webgl2', 'webgl1', 'canvas2d', … */
|
|
177
|
+
renderer?: string;
|
|
178
|
+
/** The quality tier it draws at now. */
|
|
179
|
+
tier?: DeviceTier;
|
|
180
|
+
}
|
|
181
|
+
/** What the screen (the host) sends to the game. The SDK checks it and hands it out as events. */
|
|
182
|
+
export type HostMessage =
|
|
183
|
+
/** view: where the camera image is, with any backdrop (see BackdropView); absent if the screen doesn't know. */
|
|
184
|
+
/** stats: a debug host (the simulator) asks the game to say how often it draws (stats messages); absent on the TV. */
|
|
185
|
+
/** device: what the screen knows about the device (a starting tier, the display's rate); absent if it knows nothing. */
|
|
186
|
+
{
|
|
187
|
+
type: "init";
|
|
188
|
+
v: number;
|
|
189
|
+
locale: Locale;
|
|
190
|
+
mode: "tv" | "solo";
|
|
191
|
+
players: number[];
|
|
192
|
+
muted: boolean;
|
|
193
|
+
ageRange: AgeRange;
|
|
194
|
+
backdrop: Backdrop;
|
|
195
|
+
view?: BackdropView;
|
|
196
|
+
stats?: boolean;
|
|
197
|
+
device?: DeviceHint;
|
|
198
|
+
}
|
|
199
|
+
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit changed). */
|
|
200
|
+
| {
|
|
201
|
+
type: "backdrop";
|
|
202
|
+
backdrop: Backdrop;
|
|
203
|
+
view?: BackdropView;
|
|
204
|
+
}
|
|
205
|
+
/** Every camera frame (about 30 a second): t, when it was taken (ms), and each player seen. */
|
|
206
|
+
| {
|
|
207
|
+
type: "frame";
|
|
208
|
+
t: number;
|
|
209
|
+
players: PlayerFrame[];
|
|
210
|
+
}
|
|
211
|
+
/** A player made a movement (see Gesture). */
|
|
212
|
+
| {
|
|
213
|
+
type: "gesture";
|
|
214
|
+
t: number;
|
|
215
|
+
player: number;
|
|
216
|
+
gesture: Gesture;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The game is paused: stop the clock until resume. reason: 'user', a grown-up (phone, remote) or a child's
|
|
220
|
+
* crossed arms; 'distance', a child stood too close to the screen for 2 s (it resumes by itself once they are back
|
|
221
|
+
* in the green zone). Absent from hosts before it: take it as 'user'. Either way Fabi Play draws the pause
|
|
222
|
+
* itself.
|
|
223
|
+
*/
|
|
224
|
+
| {
|
|
225
|
+
type: "pause";
|
|
226
|
+
reason?: PauseReason;
|
|
227
|
+
} | {
|
|
228
|
+
type: "resume";
|
|
229
|
+
}
|
|
230
|
+
/** A break starts: mute, stop timers, wait for break-done. */
|
|
231
|
+
| {
|
|
232
|
+
type: "break-start";
|
|
233
|
+
}
|
|
234
|
+
/** The answer to break: true if a break was shown; either way the game goes on now. */
|
|
235
|
+
| {
|
|
236
|
+
type: "break-done";
|
|
237
|
+
shown: boolean;
|
|
238
|
+
};
|
|
239
|
+
/** Why a game is paused (see HostMessage pause). */
|
|
240
|
+
export type PauseReason = "user" | "distance";
|
|
241
|
+
/** What the game may send to the screen; anything else is dropped. */
|
|
242
|
+
export type GameMessage =
|
|
243
|
+
/** Assets loaded: the screen hides its loader. */
|
|
244
|
+
{
|
|
245
|
+
type: "ready";
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The game has stopped by itself (see BreakReason): Fabi Play may show an ad there, if its rules allow. Never asked
|
|
249
|
+
* in the middle of play; the answer is always a break-done. 'unknown': a reason we don't take (e.g. 'pause'),
|
|
250
|
+
* answered at once with no break.
|
|
251
|
+
*/
|
|
252
|
+
| {
|
|
253
|
+
type: "break";
|
|
254
|
+
reason: BreakReason | "unknown";
|
|
255
|
+
}
|
|
256
|
+
/** A player's score so far, shown by the screen. */
|
|
257
|
+
| {
|
|
258
|
+
type: "score";
|
|
259
|
+
player: number;
|
|
260
|
+
score: number;
|
|
261
|
+
}
|
|
262
|
+
/** The game is over: each player's score and stars (0–3). */
|
|
263
|
+
| {
|
|
264
|
+
type: "end";
|
|
265
|
+
scores: {
|
|
266
|
+
player: number;
|
|
267
|
+
score: number;
|
|
268
|
+
stars: 0 | 1 | 2 | 3;
|
|
269
|
+
}[];
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Debug, only when init said stats: how many times a second the game drew its avatars (the SDK counts the
|
|
273
|
+
* animation frames drawAvatar or smoothedBody ran in), about once a second; from a game on the engine, also its
|
|
274
|
+
* frame rate, renderer and tier (GameStats). Hosts may ignore it.
|
|
275
|
+
*/
|
|
276
|
+
| ({
|
|
277
|
+
type: "stats";
|
|
278
|
+
renderFps: number;
|
|
279
|
+
} & GameStats);
|
|
280
|
+
/** The movements a game is played with, as its manifest lists them. */
|
|
281
|
+
export type Movement = "hands" | "jump" | "squat" | "clap" | "freeze" | "lanes" | "shapes";
|
|
282
|
+
/**
|
|
283
|
+
* What a game declares about itself (fabi-play.json at its address): the catalogue filters on it. Checked by
|
|
284
|
+
* parseManifest; fabi-play.schema.json (in @fabi-play/sdk) describes the same rules for editors.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* {
|
|
288
|
+
* "$schema": "./node_modules/@fabi-play/sdk/dist/fabi-play.schema.json",
|
|
289
|
+
* "id": "star-catcher", "version": "1.0",
|
|
290
|
+
* "title": { "it": "Acchiappastelle", "en": "Star catcher" },
|
|
291
|
+
* "description": { "it": "Prendi le stelle con le mani.", "en": "Catch the stars with your hands." },
|
|
292
|
+
* "ageRanges": ["3-5", "6-8"], "movements": ["hands"], "players": { "min": 1, "max": 2 },
|
|
293
|
+
* "minutes": 3, "entry": "index.html", "sdk": 1,
|
|
294
|
+
* "intro": { "steps": [
|
|
295
|
+
* { "say": { "it": "Prendi le stelle!", "en": "Catch the stars!" }, "image": "intro/stars.jpg" },
|
|
296
|
+
* { "say": { "it": "Alza le braccia!", "en": "Arms up!" }, "move": "arms-up", "practice": true }
|
|
297
|
+
* ] }
|
|
298
|
+
* }
|
|
299
|
+
*/
|
|
300
|
+
export interface GameManifest {
|
|
301
|
+
/** For editors only (the JSON Schema to check the file against); ignored by Fabi Play. */
|
|
302
|
+
$schema?: string;
|
|
303
|
+
/** Lowercase letters, digits and single hyphens, up to 60 characters: the game's id in the catalogue. */
|
|
304
|
+
id: string;
|
|
305
|
+
/** The game's version: 1.0 or 1.0.2. */
|
|
306
|
+
version: string;
|
|
307
|
+
/** The name, in Italian and English, up to 60 characters each. */
|
|
308
|
+
title: Record<Locale, string>;
|
|
309
|
+
/** One or two sentences, in Italian and English, up to 300 characters each. */
|
|
310
|
+
description: Record<Locale, string>;
|
|
311
|
+
/** The ranges it is made for; the catalogue shows it only to players in one of them. */
|
|
312
|
+
ageRanges: AgeRange[];
|
|
313
|
+
/** The movements it is played with (at least one). */
|
|
314
|
+
movements: Movement[];
|
|
315
|
+
/** How many children can play together: 1 to 4, min ≤ max. */
|
|
316
|
+
players: {
|
|
317
|
+
min: number;
|
|
318
|
+
max: number;
|
|
319
|
+
};
|
|
320
|
+
/** Minutes of a typical game, 1 to 20. */
|
|
321
|
+
minutes: number;
|
|
322
|
+
/**
|
|
323
|
+
* Page that starts the game: a path relative to the game's address (no scheme, no leading /, no "." or ".."
|
|
324
|
+
* segments, up to 200 characters), of letters, digits, _ and -, ending in lowercase .html: index.html, play/index.html.
|
|
325
|
+
*/
|
|
326
|
+
entry: string;
|
|
327
|
+
/** Protocol version of the SDK it was built with: 1 (PROTOCOL_VERSION). */
|
|
328
|
+
sdk: number;
|
|
329
|
+
/** Optional: the backdrops it can draw over besides its own; ['video'] opts in to mirror mode. */
|
|
330
|
+
backdrop?: GameBackdrop[];
|
|
331
|
+
/** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
|
|
332
|
+
intro?: GameIntro;
|
|
333
|
+
}
|
|
334
|
+
/** The backdrops a game may declare in its manifest (its own background is always supported). */
|
|
335
|
+
export type GameBackdrop = Exclude<Backdrop, "none">;
|
|
336
|
+
/** The moves an intro step can show; the platform draws them with each child's avatar. Same names the SDK detects. */
|
|
337
|
+
export type IntroMove = "jump" | "squat" | "clap" | "lane-left" | "lane-right" | "arms-up" | "arms-wide" | "arms-v" | "star";
|
|
338
|
+
/** One sentence in every language, or one per age range (each with every language). */
|
|
339
|
+
export type IntroText = Record<Locale, string> | Partial<Record<AgeRange, Record<Locale, string>>>;
|
|
340
|
+
/** One step of the intro: Fabi says one sentence, over an image of the game or a move shown with the avatars. */
|
|
341
|
+
export interface IntroStep {
|
|
342
|
+
say: IntroText;
|
|
343
|
+
/**
|
|
344
|
+
* An image of the game, 16:9: a path relative to the game's address with the same rules as entry (no scheme, no
|
|
345
|
+
* leading /, no "." or ".." segments), ending in lowercase .jpg or .png (no WebP: old TV browsers may not decode it): intro/track.jpg.
|
|
346
|
+
*/
|
|
347
|
+
image?: string;
|
|
348
|
+
/** Or a move, shown with the avatars. Exactly one of image and move. */
|
|
349
|
+
move?: IntroMove;
|
|
350
|
+
/** Only with move: the children try it and each gets a tick; the step moves on when all did, or after 8 s. Default false: Fabi only shows it. */
|
|
351
|
+
practice?: boolean;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Fabi explains the game on the TV, before calibration, in 1 to 4 steps (the platform adds its own final "ready"
|
|
355
|
+
* step). Only data: Fabi Play draws it, and speaks each sentence with a voice clip generated at publication from the
|
|
356
|
+
* text (voiceKey); a text without its clip yet is shown in the bubble only.
|
|
357
|
+
*/
|
|
358
|
+
export interface GameIntro {
|
|
359
|
+
steps: IntroStep[];
|
|
360
|
+
}
|
|
361
|
+
/** Where player space (0–1) lands on the canvas. */
|
|
362
|
+
export interface Box {
|
|
363
|
+
/** Left edge, canvas units. */
|
|
364
|
+
x: number;
|
|
365
|
+
/** Top edge, canvas units. */
|
|
366
|
+
y: number;
|
|
367
|
+
/** Width, canvas units. */
|
|
368
|
+
w: number;
|
|
369
|
+
/** Height, canvas units. */
|
|
370
|
+
h: number;
|
|
371
|
+
}
|
|
372
|
+
/** Where player space (0–1, the body points) lands on the canvas, in the canvas's drawing units. */
|
|
373
|
+
export type AvatarBox = Box;
|
|
374
|
+
/** Options of drawAvatar (and, smooth and interpolate, of smoothedBody). */
|
|
375
|
+
export interface DrawAvatarOptions {
|
|
376
|
+
/** 0–1, for the whole avatar. */
|
|
377
|
+
alpha?: number;
|
|
378
|
+
/**
|
|
379
|
+
* Smoothing, per player (needs a player id: a frame, or a body with a player number): true (default, 0.5), a
|
|
380
|
+
* number 0–1 (how much of each new frame to take; 1 is none), or false. The avatar then doesn't jitter, and limbs
|
|
381
|
+
* the camera loses for a moment don't blink. It advances once per new body, however often you draw.
|
|
382
|
+
*/
|
|
383
|
+
smooth?: boolean | number;
|
|
384
|
+
/**
|
|
385
|
+
* true (default): the body for this animation frame, interpolated between the camera's last two (needs a frame,
|
|
386
|
+
* or a player id), so the avatar moves at the display's rate. false: the newest body as it came (smoothed).
|
|
387
|
+
*/
|
|
388
|
+
interpolate?: boolean;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Draws an avatar; returns the body it was posed with (smoothed unless smooth: false, interpolated for this
|
|
392
|
+
* animation frame unless interpolate: false), or null if it drew nothing.
|
|
393
|
+
*/
|
|
394
|
+
export type DrawAvatar = (ctx: CanvasRenderingContext2D, who: PlayerFrame | Body, player?: number | string, box?: AvatarBox | null, options?: DrawAvatarOptions) => Body | null;
|
|
395
|
+
/** A frame's body smoothed (and interpolated) for its player, or a player id's last one; null if none. */
|
|
396
|
+
export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatarOptions, "smooth" | "interpolate">) => Body | null;
|
|
397
|
+
/** What the 'start' event says: who plays and how. */
|
|
398
|
+
export interface StartInfo {
|
|
399
|
+
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
400
|
+
ageRange: AgeRange;
|
|
401
|
+
/** The ids of the players (1, 2, …). */
|
|
402
|
+
players: number[];
|
|
403
|
+
/** The language to show text in. */
|
|
404
|
+
locale: Locale;
|
|
405
|
+
/** 'tv': on a TV, children standing back; 'solo': a tablet in front of one child. */
|
|
406
|
+
mode: "tv" | "solo";
|
|
407
|
+
/** 'video': mirror mode, paint no opaque background; 'none': paint your own. See the backdrop event. */
|
|
408
|
+
backdrop: Backdrop;
|
|
409
|
+
/** Where the camera image is (with 'video', the video; with 'none', where the avatars stand), see toScreen; null if unknown. */
|
|
410
|
+
view: BackdropView | null;
|
|
411
|
+
/** What the screen knows about the device (a starting tier, the display's rate), or null: see Play.device. */
|
|
412
|
+
device: DeviceHint | null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
|
|
416
|
+
* freeze, move, lane-left, lane-center, lane-right) is an event of its own, with the player who made it.
|
|
417
|
+
*/
|
|
418
|
+
export type EasyEvents = {
|
|
419
|
+
[G in Gesture]: {
|
|
420
|
+
player: number;
|
|
421
|
+
};
|
|
422
|
+
} & {
|
|
423
|
+
/** The screen started the game (late listeners still get it). */
|
|
424
|
+
start: StartInfo;
|
|
425
|
+
/** Every frame, the hands of each player, as screen shares (0–1). */
|
|
426
|
+
hands: {
|
|
427
|
+
player: number;
|
|
428
|
+
left: Point | null;
|
|
429
|
+
right: Point | null;
|
|
430
|
+
};
|
|
431
|
+
/** When a player's shape changes (arms up, wide, …). */
|
|
432
|
+
shape: {
|
|
433
|
+
player: number;
|
|
434
|
+
shape: Shape | null;
|
|
435
|
+
};
|
|
436
|
+
/** Every frame, every player: bodies for drawAvatar and your own hit-tests. */
|
|
437
|
+
players: {
|
|
438
|
+
players: PlayerFrame[];
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* The game is paused: stop the clock until resume. reason 'user': a grown-up (or a child's crossed arms);
|
|
442
|
+
* 'distance': a child is too close to the screen (it resumes by itself once they step back). Fabi Play draws the
|
|
443
|
+
* pause either way.
|
|
444
|
+
*/
|
|
445
|
+
pause: {
|
|
446
|
+
reason: PauseReason;
|
|
447
|
+
};
|
|
448
|
+
/** Go on after a pause. */
|
|
449
|
+
resume: Record<string, never>;
|
|
450
|
+
/** Fabi Play shows an ad: mute and stop the clock (the script tag mutes audio and video for you). */
|
|
451
|
+
"break-start": Record<string, never>;
|
|
452
|
+
/** Go on; shown says whether an ad was shown. */
|
|
453
|
+
"break-end": {
|
|
454
|
+
shown: boolean;
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* The backdrop changed mid-game, 'video' (no opaque background) or 'none' (your own background again), or the
|
|
458
|
+
* camera image moved on the screen (view).
|
|
459
|
+
*/
|
|
460
|
+
backdrop: {
|
|
461
|
+
backdrop: Backdrop;
|
|
462
|
+
view: BackdropView | null;
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
/** An event name of FabiPlay.on. */
|
|
466
|
+
export type EasyEvent = keyof EasyEvents;
|
|
467
|
+
/**
|
|
468
|
+
* The simple API: window.FabiPlay from the script tag (fabi-play.js), or easy(connect()) with the npm package.
|
|
469
|
+
* Everything can be called before the screen starts the game: it waits for it.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* FabiPlay.on('jump', ({ player }) => hop(player))
|
|
473
|
+
* FabiPlay.on('hands', ({ player, right }) => right && aim(player, FabiPlay.toScreen(right)))
|
|
474
|
+
* await FabiPlay.break('level')
|
|
475
|
+
* FabiPlay.end(120)
|
|
476
|
+
*/
|
|
477
|
+
export interface Easy {
|
|
478
|
+
/**
|
|
479
|
+
* Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
|
|
480
|
+
* 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop' (see EasyEvents). Returns a function that
|
|
481
|
+
* stops listening.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* const off = FabiPlay.on('clap', ({ player }) => fire(player))
|
|
485
|
+
*/
|
|
486
|
+
on<K extends EasyEvent>(event: K, listener: (data: EasyEvents[K]) => void): () => void;
|
|
487
|
+
/** Say the game is loaded; done for you on page load unless the script tag says data-ready="manual". */
|
|
488
|
+
ready(): void;
|
|
489
|
+
/** The score so far, for the screen to show; player 1 unless said. */
|
|
490
|
+
score(score: number, player?: number): void;
|
|
491
|
+
/** The game stopped by itself ('level' or 'gameover', never its pause menu): Fabi Play may show an ad. Resolves when it can go on. */
|
|
492
|
+
break(reason?: BreakReason): Promise<boolean>;
|
|
493
|
+
/** The end: a score, or one per player. Stars from 0 to 3, 2 if not given. */
|
|
494
|
+
end(score: number | {
|
|
495
|
+
player: number;
|
|
496
|
+
score: number;
|
|
497
|
+
stars?: 0 | 1 | 2 | 3;
|
|
498
|
+
}[]): void;
|
|
499
|
+
/**
|
|
500
|
+
* Presses keys for movements, for a game played with the keyboard (see keyboard()): by default Space while in the
|
|
501
|
+
* air, ArrowDown while crouched, arrows for side steps, Enter on a clap. The script tag's data-keys does the same.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* FabiPlay.keys({ jump: 'Space', left: 'ArrowLeft', right: 'ArrowRight' })
|
|
505
|
+
*/
|
|
506
|
+
keys(map?: KeyMap, options?: Omit<KeyboardOptions, "target">): void;
|
|
507
|
+
/**
|
|
508
|
+
* The child's hand as the mouse, for a game played with the pointer (see pointer()): it moves the pointer and
|
|
509
|
+
* clicks by staying still on a spot. The script tag's data-pointer does the same.
|
|
510
|
+
*/
|
|
511
|
+
pointer(options?: Omit<PointerOptions, "target">): void;
|
|
512
|
+
/** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
|
|
513
|
+
readonly view: BackdropView | null;
|
|
514
|
+
/** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
|
|
515
|
+
readonly device: DeviceHint | null;
|
|
516
|
+
/** For engines, debug only: how the game draws now (see Play.reportStats). Dropped before start. */
|
|
517
|
+
reportStats(stats: GameStats): void;
|
|
518
|
+
/**
|
|
519
|
+
* A body point (player space, 0–1, as in players[i].body) on the screen, in fractions: where the child is, in the
|
|
520
|
+
* video or on their avatar. Draw hands and effects at toScreen(wrist), in every mode. Unchanged without a view.
|
|
521
|
+
*/
|
|
522
|
+
toScreen(p: Point): Point;
|
|
523
|
+
/**
|
|
524
|
+
* The avatar id a player picked on the phone, by player id or from a frame (players[i]); the slot's default
|
|
525
|
+
* (fox, lion, dragon, unicorn: the 6–8 band's, as no age is known here) until the first frame says otherwise.
|
|
526
|
+
*/
|
|
527
|
+
avatarOf(player: number | PlayerFrame): string;
|
|
528
|
+
/**
|
|
529
|
+
* Draws a player's avatar posed like them, smoothed per player: drawAvatar(ctx, players[i]). Optional: player (a
|
|
530
|
+
* player id, or an avatar id), box (where player space 0–1 lands on the canvas; default the view, where the child
|
|
531
|
+
* stands, else the whole canvas), { alpha, smooth }. Draw avatars only when backdrop is not 'video'. Returns the
|
|
532
|
+
* smoothed body it drew (or null): put the hands on it, so they stay in the avatar's hands.
|
|
533
|
+
*/
|
|
534
|
+
drawAvatar: DrawAvatar;
|
|
535
|
+
/**
|
|
536
|
+
* A player's body smoothed as drawAvatar draws it (from a frame: smooths it, shared with drawAvatar; from a player
|
|
537
|
+
* id: the last one, or null). Use it for hit-tests and glows in avatar mode, so they follow the avatar's hands.
|
|
538
|
+
*/
|
|
539
|
+
smoothedBody: SmoothedBody;
|
|
540
|
+
}
|
|
541
|
+
/** The events of Play.on, by name: each listener gets the host's message of that type. */
|
|
542
|
+
export type Events = {
|
|
543
|
+
/** Every camera frame (about 30 a second): each player's body, hands, shape, lane and avatar. */
|
|
544
|
+
frame: Extract<HostMessage, {
|
|
545
|
+
type: "frame";
|
|
546
|
+
}>;
|
|
547
|
+
/** A player made a movement: jump, land, squat, stand, clap, freeze, move, lane-left/center/right. */
|
|
548
|
+
gesture: Extract<HostMessage, {
|
|
549
|
+
type: "gesture";
|
|
550
|
+
}>;
|
|
551
|
+
/** The game is paused (reason: 'user', or 'distance' when a child is too close): stop the clock until resume. */
|
|
552
|
+
pause: Extract<HostMessage, {
|
|
553
|
+
type: "pause";
|
|
554
|
+
}>;
|
|
555
|
+
/** Go on after a pause. */
|
|
556
|
+
resume: Extract<HostMessage, {
|
|
557
|
+
type: "resume";
|
|
558
|
+
}>;
|
|
559
|
+
/** Fabi Play is showing a break: mute, stop the clock, until break-done. */
|
|
560
|
+
"break-start": Extract<HostMessage, {
|
|
561
|
+
type: "break-start";
|
|
562
|
+
}>;
|
|
563
|
+
/** The break is over (shown: whether an ad was shown): unmute and go on. */
|
|
564
|
+
"break-done": Extract<HostMessage, {
|
|
565
|
+
type: "break-done";
|
|
566
|
+
}>;
|
|
567
|
+
/** What is behind the game changed, or where its video is: see Play.backdrop and Play.view. */
|
|
568
|
+
backdrop: Extract<HostMessage, {
|
|
569
|
+
type: "backdrop";
|
|
570
|
+
}>;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
|
|
574
|
+
* the scores and the end.
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* const play = await connect()
|
|
578
|
+
* play.on('gesture', (g) => g.gesture === 'jump' && hop(g.player))
|
|
579
|
+
* play.ready()
|
|
580
|
+
*/
|
|
581
|
+
export interface Play {
|
|
582
|
+
/** The language to show text in. */
|
|
583
|
+
readonly locale: Locale;
|
|
584
|
+
/** 'tv': on a TV or Chromecast, children standing back; 'solo': a tablet in front of one child. */
|
|
585
|
+
readonly mode: "tv" | "solo";
|
|
586
|
+
/** The ids of the players in this game (1, 2, …), as frames and gestures name them. */
|
|
587
|
+
readonly players: number[];
|
|
588
|
+
/** The screen's sound is off: start the game muted. */
|
|
589
|
+
readonly muted: boolean;
|
|
590
|
+
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
591
|
+
readonly ageRange: AgeRange;
|
|
592
|
+
/**
|
|
593
|
+
* What is behind the game, now: 'video' (mirror mode: the players' camera video fills the screen under the
|
|
594
|
+
* game; paint no opaque background, only what is on top) or 'none' (paint your own background). Only games whose
|
|
595
|
+
* manifest says "backdrop": ["video"] ever get 'video'; it can change mid-game.
|
|
596
|
+
*/
|
|
597
|
+
readonly backdrop: Backdrop;
|
|
598
|
+
/**
|
|
599
|
+
* Where the whole camera image is, in fractions of the game's viewport, with any backdrop. The screen decides it:
|
|
600
|
+
* with 'video' where the video is drawn, with 'none' where the avatars stand. null if the host didn't say (older
|
|
601
|
+
* hosts). Use toScreen rather than reading it, and don't compute your own.
|
|
602
|
+
*/
|
|
603
|
+
readonly view: BackdropView | null;
|
|
604
|
+
/**
|
|
605
|
+
* A body point (player space, 0–1, as in frame.players[i].body) on the game's viewport, in fractions: where the
|
|
606
|
+
* child is, in the video or on the avatar. Draw hands and effects on the children at toScreen(wrist), in every
|
|
607
|
+
* mode. Unchanged without a view.
|
|
608
|
+
*/
|
|
609
|
+
toScreen(p: Point): Point;
|
|
610
|
+
/**
|
|
611
|
+
* The avatar id a player picked on the phone (fox, lion, robot, …), by player id or from a frame. Each
|
|
612
|
+
* frame carries it; before the first frame, from an older host, or for an id this SDK doesn't know: the slot's
|
|
613
|
+
* default (fox, lion, dragon, unicorn for players 1–4: the 6–8 band's).
|
|
614
|
+
*/
|
|
615
|
+
avatarOf(player: number | PlayerFrame): string;
|
|
616
|
+
/**
|
|
617
|
+
* Draws a player's avatar posed like their body, smoothed per player. who: a frame (its body and avatar) or a
|
|
618
|
+
* body; player: a player id (whose avatar, and whose smoothing) or an avatar id. box: where player space (0–1)
|
|
619
|
+
* lands on the canvas, in its drawing units; by default the view (where the child stands), else the whole canvas.
|
|
620
|
+
* Draw avatars only when backdrop is not 'video': in mirror mode the children are on screen themselves. Draws
|
|
621
|
+
* nothing when the shoulders aren't seen. Returns the body it posed the avatar with (smoothed), or null if it drew nothing: put hands
|
|
622
|
+
* and effects on that body's points, so they stay in the avatar's hands. Call it every animation frame: the body
|
|
623
|
+
* is interpolated for that frame between the camera's last two (about one camera interval, 33 ms, behind the
|
|
624
|
+
* newest), so the avatar moves at the display's rate, not the camera's ({ interpolate: false } for the newest).
|
|
625
|
+
*/
|
|
626
|
+
drawAvatar: DrawAvatar;
|
|
627
|
+
/**
|
|
628
|
+
* A player's body smoothed as drawAvatar draws it, for hit-tests and effects in avatar mode: from a frame, it
|
|
629
|
+
* smooths that frame's body (once per new body, shared with drawAvatar); from a player id, the last smoothed body,
|
|
630
|
+
* or null. Options: { smooth, interpolate } as in drawAvatar: by default the same interpolated body drawAvatar
|
|
631
|
+
* draws in this animation frame, so hit-tests catch where the child sees the avatar's hand.
|
|
632
|
+
*/
|
|
633
|
+
smoothedBody: SmoothedBody;
|
|
634
|
+
/**
|
|
635
|
+
* What the screen knows about the device (DeviceHint): a starting quality tier and the display's rate, Hz. null
|
|
636
|
+
* if it said nothing (older hosts, a plain browser). A hint for engines: start at this tier instead of measuring
|
|
637
|
+
* the device again (@fabi-play/engine does it by itself), and still lower it if frames are slow.
|
|
638
|
+
*/
|
|
639
|
+
readonly device: DeviceHint | null;
|
|
640
|
+
/**
|
|
641
|
+
* For engines, debug only: how the game draws now (fps, renderer, tier). The SDK adds the last one to its stats
|
|
642
|
+
* messages when a debug host (the simulator) asked for them, and drops it otherwise. @fabi-play/engine calls it
|
|
643
|
+
* by itself about once a second.
|
|
644
|
+
*/
|
|
645
|
+
reportStats(stats: GameStats): void;
|
|
646
|
+
/** Called when backdrop or view changes; returns a function that stops listening. */
|
|
647
|
+
onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
|
|
648
|
+
/**
|
|
649
|
+
* Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop' (see
|
|
650
|
+
* Events). Returns a function that stops listening.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* const off = play.on('frame', (f) => (players = f.players))
|
|
654
|
+
*/
|
|
655
|
+
on<K extends keyof Events>(event: K, listener: (m: Events[K]) => void): () => void;
|
|
656
|
+
/** Assets loaded, the game can start: the screen hides its loader. Call it within 10 seconds of the page loading. */
|
|
657
|
+
ready(): void;
|
|
658
|
+
/** A player's score so far, for the screen to show. */
|
|
659
|
+
score(player: number, score: number): void;
|
|
660
|
+
/**
|
|
661
|
+
* The game has stopped by itself: a level is over or the game is over (never its pause menu). Fabi Play may show
|
|
662
|
+
* an ad there, if its rules allow (break-start, then break-done); resolves when the game can go on, true if an
|
|
663
|
+
* ad was shown. Never call it in the middle of play.
|
|
664
|
+
*/
|
|
665
|
+
break(reason?: BreakReason): Promise<boolean>;
|
|
666
|
+
/**
|
|
667
|
+
* The game is over: each player's score and stars (0–3). The screen shows the results and takes over.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* play.end([{ player: 1, score: 120, stars: 3 }])
|
|
671
|
+
*/
|
|
672
|
+
end(scores: {
|
|
673
|
+
player: number;
|
|
674
|
+
score: number;
|
|
675
|
+
stars: 0 | 1 | 2 | 3;
|
|
676
|
+
}[]): void;
|
|
677
|
+
}
|
|
678
|
+
/** How the SDK reaches the host; in a page, the parent window. Tests pass their own. */
|
|
679
|
+
export interface Channel {
|
|
680
|
+
/** Sends a message to the host. */
|
|
681
|
+
post(message: Wire<GameMessage>): void;
|
|
682
|
+
/** Receives everything that arrives; the SDK picks out the host's messages. */
|
|
683
|
+
listen(listener: (data: unknown) => void): void;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* window.FabiPlay, as fabi-play.js sets it: the simple API (Easy), plus the avatars' rig it carries for the engine.
|
|
687
|
+
* For a script-tag game written in TypeScript: `declare const FabiPlay: FabiPlayGlobal` (or reference
|
|
688
|
+
* dist/fabi-play.d.ts, which declares it).
|
|
689
|
+
*/
|
|
690
|
+
export interface FabiPlayGlobal extends Easy {
|
|
691
|
+
/**
|
|
692
|
+
* The avatars' rig and data fabi-play.js carries (for drawAvatar), handed over to fabi-play-engine.lite.js or a
|
|
693
|
+
* bundler alias so the engine doesn't ship them twice. Not a game API: draw avatars with drawAvatar. Typed in
|
|
694
|
+
* `@fabi-play/sdk/avatars`.
|
|
695
|
+
*/
|
|
696
|
+
readonly avatars: Readonly<Record<string, unknown>>;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* The Fabi Play SDK, set by fabi-play.js: listen to movements by name, draw avatars, say when the game is ready,
|
|
702
|
+
* the scores and the end. Everything can be called before the screen starts the game.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* FabiPlay.on('jump', ({ player }) => hop(player))
|
|
706
|
+
* FabiPlay.on('players', ({ players }) => (frames = players))
|
|
707
|
+
* FabiPlay.end(120)
|
|
708
|
+
*/
|
|
709
|
+
declare const FabiPlay: FabiPlay.FabiPlayGlobal
|
|
710
|
+
|
|
711
|
+
interface Window {
|
|
712
|
+
/** The Fabi Play SDK, set by fabi-play.js (see the global FabiPlay). */
|
|
713
|
+
readonly FabiPlay: FabiPlay.FabiPlayGlobal
|
|
714
|
+
}
|