@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 +84 -0
- package/dist/avatars.d.ts +26 -14
- package/dist/avatars.js +1 -1
- package/dist/chunks/{chunk-GFNOIOSX.js → chunk-H3BKYVPL.js} +301 -22
- package/dist/fabi-play.d.ts +193 -10
- package/dist/fabi-play.js +1 -1
- package/dist/fabi-play.schema.json +32 -3
- package/dist/fabi-play.umd.js +453 -26
- package/dist/fabi-play.umd.min.js +1 -1
- package/dist/index.d.ts +212 -11
- package/dist/index.js +166 -4
- package/dist/integrity.json +6 -6
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -84,6 +84,38 @@ export interface PointerOptions {
|
|
|
84
84
|
}
|
|
85
85
|
/** The child's hand as the mouse: it moves the pointer and clicks by staying still on a spot. */
|
|
86
86
|
export declare function pointer(play: On, options?: PointerOptions): () => void;
|
|
87
|
+
/** The events the remote's cursor makes in the game's page, as a Magic Remote's (a mouse). */
|
|
88
|
+
export type CursorEventType = "pointermove" | "mousemove" | "pointerdown" | "mousedown" | "pointerup" | "mouseup" | "click" | "pointercancel";
|
|
89
|
+
/** Where the cursor's events go (connect's options.cursor): by default the page (domCursor()). */
|
|
90
|
+
export interface CursorTarget {
|
|
91
|
+
/** The game's viewport in px. */
|
|
92
|
+
size(): {
|
|
93
|
+
width: number;
|
|
94
|
+
height: number;
|
|
95
|
+
};
|
|
96
|
+
/** One event at x, y (px); buttons: 1 while OK is held, else 0. */
|
|
97
|
+
dispatch(type: CursorEventType, x: number, y: number, buttons: number): void;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Mouse and pointer events (pointerType 'mouse', pointerId 1) on what is under the cursor, as a Magic Remote's:
|
|
101
|
+
* pointerover/mouseover and pointerout/mouseout when it moves onto another element, a click only where the press
|
|
102
|
+
* started. Browsers without PointerEvent (old TVs) get the mouse events only. The screen draws the cursor itself.
|
|
103
|
+
*/
|
|
104
|
+
export declare function domCursor(doc?: Document): CursorTarget;
|
|
105
|
+
/**
|
|
106
|
+
* Turns the screen's cursor messages into events on a target: moves, presses (OK down) and releases (OK up, then a
|
|
107
|
+
* click). cancel(): the cursor stops (a pause, a break, a turn) while OK is held: pointercancel and mouseup, no click.
|
|
108
|
+
*/
|
|
109
|
+
export declare function cursorDriver(target: CursorTarget): {
|
|
110
|
+
update(c: {
|
|
111
|
+
x: number;
|
|
112
|
+
y: number;
|
|
113
|
+
down: boolean;
|
|
114
|
+
}): void;
|
|
115
|
+
cancel(): void;
|
|
116
|
+
};
|
|
117
|
+
/** What became of a picture the game asked to save (HostMessage picture-result, Play.savePicture). */
|
|
118
|
+
export type PictureResult = "sent" | "unavailable" | "too-big" | "error";
|
|
87
119
|
/**
|
|
88
120
|
* One body: for each landmark x, y (0–1, y downwards) and visibility (0–1), so 33 × 3 numbers.
|
|
89
121
|
* In "player space": mirrored like a mirror, whatever camera took it, so a child moving to their left moves left.
|
|
@@ -225,7 +257,18 @@ export type HostMessage =
|
|
|
225
257
|
*/
|
|
226
258
|
/**
|
|
227
259
|
* pointer: only with input 'remote', true when the TV's remote also points (LG's Magic Remote): it reaches the game
|
|
228
|
-
* as ordinary pointer and mouse events (hover, click with OK)
|
|
260
|
+
* as ordinary pointer and mouse events (hover, click with OK); or, for a game whose manifest says "remoteCursor":
|
|
261
|
+
* true, when the screen moves a cursor with the arrows (cursor messages, which the SDK turns into the same pointer
|
|
262
|
+
* and mouse events). Absent = false.
|
|
263
|
+
*/
|
|
264
|
+
/**
|
|
265
|
+
* pictures: a phone that can save pictures is linked now (its hello said pictures), so the game may send picture
|
|
266
|
+
* messages; it changes with a capabilities message. Absent = false.
|
|
267
|
+
*/
|
|
268
|
+
/**
|
|
269
|
+
* orientation: the screen's orientation when the game starts (width ≥ height: 'landscape', see orientationOf);
|
|
270
|
+
* only a game whose manifest lists 'portrait' gets 'portrait'. Informational, to choose a layout: lay out from the
|
|
271
|
+
* real viewport size all the same. Absent (older hosts) = 'landscape'.
|
|
229
272
|
*/
|
|
230
273
|
{
|
|
231
274
|
type: "init";
|
|
@@ -242,6 +285,8 @@ export type HostMessage =
|
|
|
242
285
|
input?: GameInput;
|
|
243
286
|
turns?: number[];
|
|
244
287
|
pointer?: boolean;
|
|
288
|
+
pictures?: boolean;
|
|
289
|
+
orientation?: Orientation;
|
|
245
290
|
}
|
|
246
291
|
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
|
|
247
292
|
| {
|
|
@@ -295,6 +340,36 @@ export type HostMessage =
|
|
|
295
340
|
| {
|
|
296
341
|
type: "turn";
|
|
297
342
|
players: number[];
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* What the screen can do for the game changed mid-game: pictures, whether a phone that can save pictures is linked
|
|
346
|
+
* now (one joined, or the last one left). init carries the first value.
|
|
347
|
+
*/
|
|
348
|
+
| {
|
|
349
|
+
type: "capabilities";
|
|
350
|
+
pictures: boolean;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The answer to a picture message, with its id (absent if the picture had none): 'sent', the phone has it (saved,
|
|
354
|
+
* or saved once the parent allows it); 'unavailable', no phone that saves pictures is linked; 'too-big', over
|
|
355
|
+
* MAX_PICTURE_BYTES; 'error', anything else (the link dropped, the phone failed, no answer in time).
|
|
356
|
+
*/
|
|
357
|
+
| {
|
|
358
|
+
type: "picture-result";
|
|
359
|
+
id?: number;
|
|
360
|
+
result: PictureResult;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Remote mode with "remoteCursor": true: the cursor the arrows move (see KeyCursor), x and y in fractions of the
|
|
364
|
+
* game's frame (0–1), down while OK is held. Sent when it moves and when OK goes down or up; the SDK turns it into
|
|
365
|
+
* pointer and mouse events in the game's page (pointermove, pointerdown, pointerup, click, …), as a Magic Remote's.
|
|
366
|
+
* The screen draws the cursor itself, above the game.
|
|
367
|
+
*/
|
|
368
|
+
| {
|
|
369
|
+
type: "cursor";
|
|
370
|
+
x: number;
|
|
371
|
+
y: number;
|
|
372
|
+
down: boolean;
|
|
298
373
|
};
|
|
299
374
|
/** Why a game is paused (see HostMessage pause). */
|
|
300
375
|
export type PauseReason = "user" | "distance";
|
|
@@ -336,7 +411,20 @@ export type GameMessage =
|
|
|
336
411
|
| ({
|
|
337
412
|
type: "stats";
|
|
338
413
|
renderFps: number;
|
|
339
|
-
} & GameStats)
|
|
414
|
+
} & GameStats)
|
|
415
|
+
/**
|
|
416
|
+
* Save a picture on the family's phone (a drawing, the photo of a wall). data: a base64 data URL, data:image/jpeg
|
|
417
|
+
* or data:image/png, at most MAX_PICTURE_BYTES, whose bytes really are that type (checkPicture); title: a name for
|
|
418
|
+
* it (MAX_PICTURE_TITLE characters); id: the game's own, echoed in picture-result. Anything else is dropped: a game
|
|
419
|
+
* can send only an image this way. The screen sends it to the phone over their direct link only, never to a
|
|
420
|
+
* server, and answers picture-result.
|
|
421
|
+
*/
|
|
422
|
+
| {
|
|
423
|
+
type: "picture";
|
|
424
|
+
data: string;
|
|
425
|
+
title?: string;
|
|
426
|
+
id?: number;
|
|
427
|
+
};
|
|
340
428
|
/** The movements a game is played with, as its manifest lists them. */
|
|
341
429
|
export type Movement = "hands" | "jump" | "squat" | "clap" | "freeze" | "lanes" | "shapes";
|
|
342
430
|
/**
|
|
@@ -390,6 +478,14 @@ export interface GameManifest {
|
|
|
390
478
|
backdrop?: GameBackdrop[];
|
|
391
479
|
/** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
|
|
392
480
|
intro?: GameIntro;
|
|
481
|
+
/**
|
|
482
|
+
* Optional: the game's cover, the card in the catalogue (TV carousel, phone, admin): a path relative to the game's
|
|
483
|
+
* address (no scheme, no leading /, no "." or ".." segments, no backslash, up to COVER_MAX_PATH characters, see
|
|
484
|
+
* isCoverPath), ending in lowercase .jpg, .jpeg, .png or .webp: cover.jpg. A 16:9 image (COVER_ASPECT, within
|
|
485
|
+
* COVER_ASPECT_TOLERANCE), recommended 1280×720 and at most COVER_MAX_BYTES; JPEG is the safest for old TV
|
|
486
|
+
* browsers. The review checks the file; absent or broken, the catalogue falls back to another image.
|
|
487
|
+
*/
|
|
488
|
+
cover?: string;
|
|
393
489
|
/** Optional: how it can be played; absent = ['body']. Must include 'body' (Fabi Play is a movement platform); add 'remote' to be offered without a phone. */
|
|
394
490
|
inputs?: GameInput[];
|
|
395
491
|
/**
|
|
@@ -406,7 +502,27 @@ export interface GameManifest {
|
|
|
406
502
|
* a game drawn on exact spots of the live video).
|
|
407
503
|
*/
|
|
408
504
|
framing?: GameFraming;
|
|
505
|
+
/**
|
|
506
|
+
* Optional, only with 'remote' in inputs: true and, in remote mode, the arrows move a free cursor instead of making
|
|
507
|
+
* gestures (they speed up when held, see KeyCursor) and OK presses and releases it: the game gets it as pointer and
|
|
508
|
+
* mouse events, the same as a Magic Remote's (init.pointer true), so a game written for the pointer works with
|
|
509
|
+
* both. Back is still the platform's pause. For games picked or drawn with a pointer (a painting game). Absent or
|
|
510
|
+
* false: arrows and OK make gestures.
|
|
511
|
+
*/
|
|
512
|
+
remoteCursor?: boolean;
|
|
513
|
+
/**
|
|
514
|
+
* Optional: the screen orientations the game can be played in; absent = ['landscape'] (see gameOrientations).
|
|
515
|
+
* Non-empty, without repeats, from ORIENTATIONS. A phone alone («Gioca qui / Play here») is held upright, so it
|
|
516
|
+
* offers only games that list 'portrait'; the TV and the tablet are landscape. A game that lists both still lays
|
|
517
|
+
* out from its real viewport size; init.orientation says which one it started in.
|
|
518
|
+
*/
|
|
519
|
+
orientations?: Orientation[];
|
|
409
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* A screen's orientation: 'landscape' when its width is at least its height, else 'portrait' (see orientationOf).
|
|
523
|
+
* GameManifest.orientations lists those a game supports; HostMessage init.orientation says the one it starts in.
|
|
524
|
+
*/
|
|
525
|
+
export type Orientation = "landscape" | "portrait";
|
|
410
526
|
/** How the screen frames the camera image for a game: see GameManifest.framing. */
|
|
411
527
|
export type GameFraming = "auto" | "fixed";
|
|
412
528
|
/**
|
|
@@ -550,6 +666,8 @@ export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatar
|
|
|
550
666
|
export declare function avatarDrawer(book: AvatarBook, scene: () => {
|
|
551
667
|
view: BackdropView | null;
|
|
552
668
|
}): DrawAvatar;
|
|
669
|
+
/** What savePicture takes: a canvas (any size) or a data URL (data:image/jpeg or data:image/png, base64). */
|
|
670
|
+
export type PictureSource = HTMLCanvasElement | string;
|
|
553
671
|
/** What the 'start' event says: who plays and how, as it was at the start (late listeners get the same; see Easy for now). */
|
|
554
672
|
export interface StartInfo {
|
|
555
673
|
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
@@ -558,8 +676,10 @@ export interface StartInfo {
|
|
|
558
676
|
players: readonly number[];
|
|
559
677
|
/** The language to show text in. */
|
|
560
678
|
locale: Locale;
|
|
561
|
-
/** 'tv': on a TV, children standing back; 'solo': a tablet in front of
|
|
679
|
+
/** 'tv': on a TV, children standing back; 'solo': a tablet or a phone in front of the children. */
|
|
562
680
|
mode: "tv" | "solo";
|
|
681
|
+
/** 'landscape' or 'portrait' (a phone held upright): the screen's orientation at the start, see Play.orientation. */
|
|
682
|
+
orientation: Orientation;
|
|
563
683
|
/** 'video': mirror mode, paint no opaque background; 'none': paint your own. See the backdrop event. */
|
|
564
684
|
backdrop: Backdrop;
|
|
565
685
|
/** Where the camera image is (with 'video', the video; with 'none', where the avatars stand), see toScreen; null if unknown. */
|
|
@@ -570,8 +690,12 @@ export interface StartInfo {
|
|
|
570
690
|
turns: readonly number[];
|
|
571
691
|
/** 'body' (the default) or 'remote': played with the TV remote, keys turned into gestures (see Play.input). */
|
|
572
692
|
input: GameInput;
|
|
573
|
-
/** With input 'remote', whether the remote also points (LG's Magic Remote; see Play.pointer). */
|
|
693
|
+
/** With input 'remote', whether the remote also points (LG's Magic Remote, or the arrows as a cursor; see Play.pointer). */
|
|
574
694
|
pointer: boolean;
|
|
695
|
+
/** Whether savePicture can reach a phone at the start (see Play.canSavePicture; the capabilities event says when it changes). */
|
|
696
|
+
canSavePicture: boolean;
|
|
697
|
+
/** The screen's sound is off: play no sound, and make every cue that sound would give visible instead. */
|
|
698
|
+
muted: boolean;
|
|
575
699
|
}
|
|
576
700
|
/**
|
|
577
701
|
* The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
|
|
@@ -630,6 +754,10 @@ export type EasyEvents = {
|
|
|
630
754
|
turn: {
|
|
631
755
|
players: number[];
|
|
632
756
|
};
|
|
757
|
+
/** What the screen can do changed: pictures, whether savePicture can reach a phone now (see canSavePicture). */
|
|
758
|
+
capabilities: {
|
|
759
|
+
pictures: boolean;
|
|
760
|
+
};
|
|
633
761
|
};
|
|
634
762
|
/** An event name of FabiPlay.on. */
|
|
635
763
|
export type EasyEvent = keyof EasyEvents;
|
|
@@ -646,7 +774,7 @@ export type EasyEvent = keyof EasyEvents;
|
|
|
646
774
|
export interface Easy {
|
|
647
775
|
/**
|
|
648
776
|
* Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
|
|
649
|
-
* 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop', 'turn' (see EasyEvents). Returns a function that
|
|
777
|
+
* 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop', 'turn', 'capabilities' (see EasyEvents). Returns a function that
|
|
650
778
|
* stops listening.
|
|
651
779
|
*
|
|
652
780
|
* @example
|
|
@@ -687,8 +815,26 @@ export interface Easy {
|
|
|
687
815
|
readonly turns: readonly number[];
|
|
688
816
|
/** How this session is played (see Play.input); 'body' before start. */
|
|
689
817
|
readonly input: GameInput;
|
|
690
|
-
/**
|
|
818
|
+
/**
|
|
819
|
+
* With input 'remote', whether the remote also points (Play.pointer: a Magic Remote, or the arrows as a cursor with
|
|
820
|
+
* "remoteCursor": true; either way plain pointer and mouse events). pointer() is the hand adapter. False before start.
|
|
821
|
+
*/
|
|
691
822
|
readonly hasPointer: boolean;
|
|
823
|
+
/** Whether savePicture can reach a phone now (see Play.canSavePicture); false before start. */
|
|
824
|
+
readonly canSavePicture: boolean;
|
|
825
|
+
/**
|
|
826
|
+
* Saves a picture on the family's phone: a canvas (scaled to 1920 px at most, JPEG) or a data URL (JPEG or PNG),
|
|
827
|
+
* with an optional title. Resolves 'sent', 'unavailable', 'too-big' or 'error' (see Play.savePicture); called before
|
|
828
|
+
* start, it waits for it. Only TV → phone over their direct link, never to a server.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* if (FabiPlay.canSavePicture) FabiPlay.savePicture(canvas, { title: 'Il nostro muro' })
|
|
832
|
+
*/
|
|
833
|
+
savePicture(source: PictureSource, opts?: {
|
|
834
|
+
title?: string;
|
|
835
|
+
}): Promise<PictureResult>;
|
|
836
|
+
/** The screen's sound is off (see StartInfo.muted); false before start. */
|
|
837
|
+
readonly muted: boolean;
|
|
692
838
|
/** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
|
|
693
839
|
readonly view: BackdropView | null;
|
|
694
840
|
/** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
|
|
@@ -764,6 +910,20 @@ export type Events = {
|
|
|
764
910
|
turn: Extract<HostMessage, {
|
|
765
911
|
type: "turn";
|
|
766
912
|
}>;
|
|
913
|
+
/**
|
|
914
|
+
* What the screen can do for the game changed: pictures, whether savePicture can reach a phone now (see
|
|
915
|
+
* Play.canSavePicture). Only when it changes.
|
|
916
|
+
*/
|
|
917
|
+
capabilities: Extract<HostMessage, {
|
|
918
|
+
type: "capabilities";
|
|
919
|
+
}>;
|
|
920
|
+
/**
|
|
921
|
+
* Remote mode with "remoteCursor": true: the cursor the arrows move (x, y in fractions of the page, down while OK
|
|
922
|
+
* is held). Nothing to do with it: the SDK already makes the pointer and mouse events a Magic Remote would.
|
|
923
|
+
*/
|
|
924
|
+
cursor: Extract<HostMessage, {
|
|
925
|
+
type: "cursor";
|
|
926
|
+
}>;
|
|
767
927
|
};
|
|
768
928
|
/**
|
|
769
929
|
* The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
|
|
@@ -808,10 +968,43 @@ export interface Play {
|
|
|
808
968
|
/**
|
|
809
969
|
* Only with input 'remote': the remote also points (LG's Magic Remote), reaching the game as ordinary pointer and
|
|
810
970
|
* mouse events (hover, click with OK). Then let what the game has to pick be clicked too (large hit areas, a hover
|
|
811
|
-
* highlight).
|
|
812
|
-
* the mouse
|
|
971
|
+
* highlight). Also true for a game whose manifest says "remoteCursor": true, when the arrows move a cursor: the SDK
|
|
972
|
+
* makes the same pointer and mouse events from it (OK held: pointerdown … pointerup, then click), so a game written
|
|
973
|
+
* for the pointer works with both. false otherwise, and from hosts that don't say. Not the pointer() adapter, which
|
|
974
|
+
* makes a child's hand the mouse.
|
|
813
975
|
*/
|
|
814
976
|
readonly pointer: boolean;
|
|
977
|
+
/**
|
|
978
|
+
* Whether savePicture can reach a phone now: true when the screen says a phone that saves pictures is linked (the
|
|
979
|
+
* app; the web phone can't). It can change mid-game: listen to 'capabilities'. Hide or grey a "save" button while
|
|
980
|
+
* it is false, and never make the children wait on it.
|
|
981
|
+
*/
|
|
982
|
+
readonly canSavePicture: boolean;
|
|
983
|
+
/**
|
|
984
|
+
* Saves a picture on the family's phone, in its gallery (a drawing, the photo of a wall). source: a canvas (scaled
|
|
985
|
+
* down to MAX_PICTURE_SIDE, 1920 px on its longer side, drawn on white and encoded as JPEG at quality 0.85, lower
|
|
986
|
+
* if it would be over 3 MiB) or a data URL (data:image/jpeg or data:image/png, base64, sent as it is). title: a
|
|
987
|
+
* name for it (up to 60 characters). Resolves with what became of it: 'sent' (the phone has it), 'unavailable' (no
|
|
988
|
+
* phone that saves pictures: canSavePicture is false), 'too-big' (over 3 MiB), 'error' (a canvas that can't be read,
|
|
989
|
+
* not an image, the link dropped, no answer in 2 minutes). Never rejects.
|
|
990
|
+
*
|
|
991
|
+
* Privacy: the picture goes only from the TV to the phone over their direct link (WebRTC, encrypted), never to a
|
|
992
|
+
* server, and nothing else can be sent this way. If the phone can't save it (permission denied), Fabi Play tells the
|
|
993
|
+
* parent on the phone: don't tell the children.
|
|
994
|
+
*
|
|
995
|
+
* @example
|
|
996
|
+
* if (play.canSavePicture) await play.savePicture(canvas, { title: 'Our wall' })
|
|
997
|
+
*/
|
|
998
|
+
savePicture(source: PictureSource, opts?: {
|
|
999
|
+
title?: string;
|
|
1000
|
+
}): Promise<PictureResult>;
|
|
1001
|
+
/**
|
|
1002
|
+
* The screen's orientation when the game started: 'landscape' (width ≥ height; the TV, the tablet, and what older
|
|
1003
|
+
* hosts mean) or 'portrait' (a phone held upright, only for games whose manifest lists it in orientations). Use it
|
|
1004
|
+
* to choose a layout, but size everything from the real viewport (innerWidth × innerHeight): it is not updated if
|
|
1005
|
+
* the screen turns.
|
|
1006
|
+
*/
|
|
1007
|
+
readonly orientation: Orientation;
|
|
815
1008
|
/** The screen's sound is off: start the game muted. */
|
|
816
1009
|
readonly muted: boolean;
|
|
817
1010
|
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
@@ -873,8 +1066,8 @@ export interface Play {
|
|
|
873
1066
|
/** Called when backdrop or view changes; returns a function that stops listening. */
|
|
874
1067
|
onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
|
|
875
1068
|
/**
|
|
876
|
-
* Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop', 'turn'
|
|
877
|
-
* (see Events). Returns a function that stops listening.
|
|
1069
|
+
* Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop', 'turn',
|
|
1070
|
+
* 'capabilities', 'cursor' (see Events). Returns a function that stops listening.
|
|
878
1071
|
*
|
|
879
1072
|
* @example
|
|
880
1073
|
* const off = play.on('frame', (f) => (players = f.players))
|
|
@@ -923,6 +1116,14 @@ export interface FabiPlayGlobal extends Easy {
|
|
|
923
1116
|
*/
|
|
924
1117
|
readonly avatars: Readonly<Record<string, unknown>>;
|
|
925
1118
|
}
|
|
1119
|
+
/** connect()'s options. */
|
|
1120
|
+
export interface ConnectOptions {
|
|
1121
|
+
/**
|
|
1122
|
+
* Where the remote's cursor events go ("remoteCursor": true): by default the page (domCursor()), null for nowhere
|
|
1123
|
+
* (the game reads the 'cursor' event itself). Tests pass their own.
|
|
1124
|
+
*/
|
|
1125
|
+
cursor?: CursorTarget | null;
|
|
1126
|
+
}
|
|
926
1127
|
/** The channel to the screen that embeds the game: the parent window, listening only to messages from it. */
|
|
927
1128
|
export declare const windowChannel: (win?: Window) => Channel;
|
|
928
1129
|
/**
|
|
@@ -937,7 +1138,7 @@ export declare const windowChannel: (win?: Window) => Channel;
|
|
|
937
1138
|
* play.on('gesture', ({ player, gesture }) => gesture === 'jump' && jump(player))
|
|
938
1139
|
* play.ready()
|
|
939
1140
|
*/
|
|
940
|
-
export declare function connect(channel?: Channel): Promise<Play>;
|
|
1141
|
+
export declare function connect(channel?: Channel, options?: ConnectOptions): Promise<Play>;
|
|
941
1142
|
|
|
942
1143
|
|
|
943
1144
|
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BodyInterpolator,
|
|
3
|
+
MAX_PICTURE_SIDE,
|
|
3
4
|
NS,
|
|
5
|
+
PICTURE_JPEG_QUALITY,
|
|
4
6
|
PROTOCOL_VERSION,
|
|
5
7
|
__publicField,
|
|
8
|
+
checkPicture,
|
|
6
9
|
defaultAvatarFor,
|
|
7
10
|
drawAvatar,
|
|
8
11
|
isAvatarId,
|
|
12
|
+
parseCursor,
|
|
9
13
|
parseDeviceHint,
|
|
10
14
|
parseGameStats,
|
|
15
|
+
parsePictureResult,
|
|
16
|
+
parsePictureTitle,
|
|
11
17
|
parseTurnOrder,
|
|
12
18
|
parseTurnPlayers,
|
|
13
19
|
parseView,
|
|
14
20
|
smoothBody,
|
|
15
21
|
toScreen,
|
|
16
22
|
wire
|
|
17
|
-
} from "./chunks/chunk-
|
|
23
|
+
} from "./chunks/chunk-H3BKYVPL.js";
|
|
18
24
|
|
|
19
25
|
// src/adapters.ts
|
|
20
26
|
function muteDuringBreaks(play, win = window) {
|
|
@@ -210,6 +216,59 @@ function pointer(play, options = {}) {
|
|
|
210
216
|
target.cursor(x, y, clicked ? 0 : progress);
|
|
211
217
|
});
|
|
212
218
|
}
|
|
219
|
+
function domCursor(doc = document) {
|
|
220
|
+
const win = doc.defaultView;
|
|
221
|
+
const Pointer = win?.PointerEvent;
|
|
222
|
+
const Mouse = win?.MouseEvent ?? MouseEvent;
|
|
223
|
+
let over = null;
|
|
224
|
+
let pressed = null;
|
|
225
|
+
const fire = (el, type, x, y, buttons, related = null) => {
|
|
226
|
+
const init = { bubbles: true, cancelable: true, composed: true, view: win, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, buttons, relatedTarget: related, pointerId: 1, pointerType: "mouse", isPrimary: true };
|
|
227
|
+
if (type.startsWith("pointer")) {
|
|
228
|
+
if (Pointer) el.dispatchEvent(new Pointer(type, init));
|
|
229
|
+
} else el.dispatchEvent(new Mouse(type, init));
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
// The viewport (the game's frame), not the document, which may be taller.
|
|
233
|
+
size: () => ({ width: win?.innerWidth ?? doc.documentElement.clientWidth, height: win?.innerHeight ?? doc.documentElement.clientHeight }),
|
|
234
|
+
dispatch(type, x, y, buttons) {
|
|
235
|
+
const el = doc.elementFromPoint(x, y) ?? doc.body ?? doc.documentElement;
|
|
236
|
+
if (type === "pointermove" && el !== over) {
|
|
237
|
+
if (over?.isConnected) for (const t of ["pointerout", "mouseout"]) fire(over, t, x, y, buttons, el);
|
|
238
|
+
for (const t of ["pointerover", "mouseover"]) fire(el, t, x, y, buttons, over);
|
|
239
|
+
over = el;
|
|
240
|
+
}
|
|
241
|
+
if (type === "pointerdown") pressed = el;
|
|
242
|
+
if (type === "click") {
|
|
243
|
+
const same = pressed === el;
|
|
244
|
+
pressed = null;
|
|
245
|
+
if (!same) return;
|
|
246
|
+
}
|
|
247
|
+
fire(el, type, x, y, buttons);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function cursorDriver(target) {
|
|
252
|
+
let last = null;
|
|
253
|
+
const send = (types, x, y, buttons) => {
|
|
254
|
+
const { width, height } = target.size();
|
|
255
|
+
for (const t of types) target.dispatch(t, x * width, y * height, buttons);
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
update(c) {
|
|
259
|
+
const was = last?.down ?? false;
|
|
260
|
+
if (!last || last.x !== c.x || last.y !== c.y) send(["pointermove", "mousemove"], c.x, c.y, was ? 1 : 0);
|
|
261
|
+
if (c.down && !was) send(["pointerdown", "mousedown"], c.x, c.y, 1);
|
|
262
|
+
if (!c.down && was) send(["pointerup", "mouseup", "click"], c.x, c.y, 0);
|
|
263
|
+
last = { x: c.x, y: c.y, down: c.down };
|
|
264
|
+
},
|
|
265
|
+
cancel() {
|
|
266
|
+
if (!last?.down) return;
|
|
267
|
+
send(["pointercancel", "mouseup"], last.x, last.y, 0);
|
|
268
|
+
last = { ...last, down: false };
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
213
272
|
|
|
214
273
|
// src/avatars.ts
|
|
215
274
|
var frameTime = () => {
|
|
@@ -342,6 +401,7 @@ function easy(connecting) {
|
|
|
342
401
|
p.on("break-done", (m) => emit("break-end", { shown: m.shown }));
|
|
343
402
|
p.onBackdrop((backdrop, view) => emit("backdrop", { backdrop, view }));
|
|
344
403
|
p.on("turn", (m) => emit("turn", { players: [...m.players] }));
|
|
404
|
+
p.on("capabilities", (m) => emit("capabilities", { pictures: m.pictures }));
|
|
345
405
|
p.on("frame", (f) => {
|
|
346
406
|
emit("players", { players: f.players });
|
|
347
407
|
for (const pl of f.players) {
|
|
@@ -388,6 +448,13 @@ function easy(connecting) {
|
|
|
388
448
|
get hasPointer() {
|
|
389
449
|
return play?.pointer ?? false;
|
|
390
450
|
},
|
|
451
|
+
get canSavePicture() {
|
|
452
|
+
return play?.canSavePicture ?? false;
|
|
453
|
+
},
|
|
454
|
+
savePicture: (source, opts) => new Promise((resolve) => withPlay((p) => void p.savePicture(source, opts).then(resolve))),
|
|
455
|
+
get muted() {
|
|
456
|
+
return play?.muted ?? false;
|
|
457
|
+
},
|
|
391
458
|
get view() {
|
|
392
459
|
return play?.view ?? null;
|
|
393
460
|
},
|
|
@@ -402,7 +469,7 @@ function easy(connecting) {
|
|
|
402
469
|
};
|
|
403
470
|
}
|
|
404
471
|
var none = Object.freeze([]);
|
|
405
|
-
var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer });
|
|
472
|
+
var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, orientation: p.orientation, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer, canSavePicture: p.canSavePicture, muted: p.muted });
|
|
406
473
|
var playingNow = (p) => p.input === "remote" ? p.players[0] ?? 1 : 1;
|
|
407
474
|
function endOf(p, score, last) {
|
|
408
475
|
const now = playingNow(p);
|
|
@@ -411,6 +478,46 @@ function endOf(p, score, last) {
|
|
|
411
478
|
return [...played.map((player) => ({ player, score: last.get(player) ?? 0, stars: 2 })), { player: now, score, stars: 2 }];
|
|
412
479
|
}
|
|
413
480
|
|
|
481
|
+
// src/picture.ts
|
|
482
|
+
var QUALITIES = [PICTURE_JPEG_QUALITY, 0.7, 0.55];
|
|
483
|
+
var SHRINK = 0.75;
|
|
484
|
+
var MIN_SIDE = 320;
|
|
485
|
+
function encodePicture(source) {
|
|
486
|
+
if (typeof source === "string") {
|
|
487
|
+
const checked = checkPicture(source);
|
|
488
|
+
return "ok" in checked ? { data: source } : { error: checked.error === "too-big" ? "too-big" : "error" };
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
if (!source || typeof source.toDataURL !== "function") return { error: "error" };
|
|
492
|
+
const { width, height } = source;
|
|
493
|
+
if (!(width > 0 && height > 0)) return { error: "error" };
|
|
494
|
+
const doc = source.ownerDocument ?? document;
|
|
495
|
+
let scale = Math.min(1, MAX_PICTURE_SIDE / Math.max(width, height));
|
|
496
|
+
for (; ; ) {
|
|
497
|
+
const w = Math.max(1, Math.round(width * scale));
|
|
498
|
+
const h = Math.max(1, Math.round(height * scale));
|
|
499
|
+
const flat = doc.createElement("canvas");
|
|
500
|
+
flat.width = w;
|
|
501
|
+
flat.height = h;
|
|
502
|
+
const ctx = flat.getContext("2d");
|
|
503
|
+
if (!ctx) return { error: "error" };
|
|
504
|
+
ctx.fillStyle = "#fff";
|
|
505
|
+
ctx.fillRect(0, 0, w, h);
|
|
506
|
+
ctx.drawImage(source, 0, 0, w, h);
|
|
507
|
+
for (const q of QUALITIES) {
|
|
508
|
+
const data = flat.toDataURL("image/jpeg", q);
|
|
509
|
+
const checked = checkPicture(data);
|
|
510
|
+
if ("ok" in checked) return { data };
|
|
511
|
+
if (checked.error !== "too-big") return { error: "error" };
|
|
512
|
+
}
|
|
513
|
+
if (Math.max(w, h) * SHRINK < MIN_SIDE) return { error: "too-big" };
|
|
514
|
+
scale *= SHRINK;
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
return { error: "error" };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
414
521
|
// src/index.ts
|
|
415
522
|
var windowChannel = (win = window) => ({
|
|
416
523
|
post: (m) => win.parent.postMessage(m, "*"),
|
|
@@ -419,7 +526,8 @@ var windowChannel = (win = window) => ({
|
|
|
419
526
|
});
|
|
420
527
|
var INIT_TIMEOUT_MS = 1e4;
|
|
421
528
|
var STATS_MS = 1e3;
|
|
422
|
-
|
|
529
|
+
var PICTURE_TIMEOUT_MS = 12e4;
|
|
530
|
+
function connect(channel = windowChannel(), options = {}) {
|
|
423
531
|
return new Promise((resolve, reject) => {
|
|
424
532
|
const listeners = /* @__PURE__ */ new Map();
|
|
425
533
|
const breaks = [];
|
|
@@ -431,6 +539,17 @@ function connect(channel = windowChannel()) {
|
|
|
431
539
|
let input = "body";
|
|
432
540
|
let players = Object.freeze([]);
|
|
433
541
|
let turns = players;
|
|
542
|
+
let pictures = false;
|
|
543
|
+
let pictureId = 0;
|
|
544
|
+
const pictureWaits = /* @__PURE__ */ new Map();
|
|
545
|
+
let cursor;
|
|
546
|
+
const cursorTarget = () => {
|
|
547
|
+
if (cursor === void 0) {
|
|
548
|
+
const target = options.cursor !== void 0 ? options.cursor : typeof document !== "undefined" ? domCursor() : null;
|
|
549
|
+
cursor = target ? cursorDriver(target) : null;
|
|
550
|
+
}
|
|
551
|
+
return cursor;
|
|
552
|
+
};
|
|
434
553
|
const backdropListeners = /* @__PURE__ */ new Set();
|
|
435
554
|
const avatars = new AvatarBook();
|
|
436
555
|
const drawAvatar2 = avatarDrawer(avatars, () => ({ view }));
|
|
@@ -451,6 +570,7 @@ function connect(channel = windowChannel()) {
|
|
|
451
570
|
const device = parseDeviceHint(m.device) ?? null;
|
|
452
571
|
input = m.input === "remote" ? "remote" : "body";
|
|
453
572
|
const pointer2 = input === "remote" && m.pointer === true;
|
|
573
|
+
pictures = m.pictures === true;
|
|
454
574
|
players = Object.freeze(Array.isArray(m.players) ? [...m.players] : []);
|
|
455
575
|
const order = input === "remote" && players.length === 1 ? parseTurnOrder(m.turns) : void 0;
|
|
456
576
|
turns = Object.freeze(order && order[0] === players[0] ? order : [...players]);
|
|
@@ -474,6 +594,8 @@ function connect(channel = windowChannel()) {
|
|
|
474
594
|
input,
|
|
475
595
|
pointer: pointer2,
|
|
476
596
|
muted: m.muted,
|
|
597
|
+
// Hosts before orientations don't send it: landscape.
|
|
598
|
+
orientation: m.orientation === "portrait" ? "portrait" : "landscape",
|
|
477
599
|
ageRange: m.ageRange ?? "6-8",
|
|
478
600
|
get backdrop() {
|
|
479
601
|
return backdrop;
|
|
@@ -505,7 +627,28 @@ function connect(channel = windowChannel()) {
|
|
|
505
627
|
breaks.push({ reason, done });
|
|
506
628
|
channel.post(wire({ type: "break", reason }));
|
|
507
629
|
}),
|
|
508
|
-
end: (scores) => channel.post(wire({ type: "end", scores }))
|
|
630
|
+
end: (scores) => channel.post(wire({ type: "end", scores })),
|
|
631
|
+
get canSavePicture() {
|
|
632
|
+
return pictures;
|
|
633
|
+
},
|
|
634
|
+
savePicture: async (source, opts) => {
|
|
635
|
+
if (!pictures) return "unavailable";
|
|
636
|
+
const encoded = encodePicture(source);
|
|
637
|
+
if ("error" in encoded) return encoded.error;
|
|
638
|
+
const id = ++pictureId;
|
|
639
|
+
const title = parsePictureTitle(opts?.title);
|
|
640
|
+
return new Promise((done) => {
|
|
641
|
+
const timer2 = setTimeout(() => finish("error"), PICTURE_TIMEOUT_MS);
|
|
642
|
+
timer2.unref?.();
|
|
643
|
+
const finish = (r) => {
|
|
644
|
+
clearTimeout(timer2);
|
|
645
|
+
pictureWaits.delete(id);
|
|
646
|
+
done(r);
|
|
647
|
+
};
|
|
648
|
+
pictureWaits.set(id, finish);
|
|
649
|
+
channel.post(wire({ type: "picture", data: encoded.data, id, ...title ? { title } : {} }));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
509
652
|
});
|
|
510
653
|
} else if (!started) return;
|
|
511
654
|
else {
|
|
@@ -528,6 +671,23 @@ function connect(channel = windowChannel()) {
|
|
|
528
671
|
players = Object.freeze(next);
|
|
529
672
|
message = { type: "turn", players: [...next] };
|
|
530
673
|
}
|
|
674
|
+
if (m.type === "picture-result") {
|
|
675
|
+
const r = parsePictureResult(m);
|
|
676
|
+
if (r?.id !== void 0) pictureWaits.get(r.id)?.(r.result);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (m.type === "capabilities") {
|
|
680
|
+
if (typeof m.pictures !== "boolean" || m.pictures === pictures) return;
|
|
681
|
+
pictures = m.pictures;
|
|
682
|
+
message = { type: "capabilities", pictures };
|
|
683
|
+
}
|
|
684
|
+
if (m.type === "cursor") {
|
|
685
|
+
const c = input === "remote" ? parseCursor(m) : void 0;
|
|
686
|
+
if (!c) return;
|
|
687
|
+
message = c;
|
|
688
|
+
cursorTarget()?.update(c);
|
|
689
|
+
}
|
|
690
|
+
if (m.type === "pause" || m.type === "break-start" || m.type === "turn") cursor?.cancel();
|
|
531
691
|
if (m.type === "break-start") inBreak = breaks.length > 0;
|
|
532
692
|
for (const l of listeners.get(m.type) ?? []) l(message);
|
|
533
693
|
if (m.type === "break-done") {
|
|
@@ -544,7 +704,9 @@ export {
|
|
|
544
704
|
DEFAULT_KEYS,
|
|
545
705
|
avatarDrawer,
|
|
546
706
|
connect,
|
|
707
|
+
cursorDriver,
|
|
547
708
|
defaultBox,
|
|
709
|
+
domCursor,
|
|
548
710
|
domKeys,
|
|
549
711
|
domPointer,
|
|
550
712
|
easy,
|
package/dist/integrity.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@fabi-play/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"algorithm": "sha384",
|
|
5
5
|
"files": {
|
|
6
|
-
"fabi-play.js": "sha384-
|
|
7
|
-
"fabi-play.umd.js": "sha384-
|
|
8
|
-
"fabi-play.umd.min.js": "sha384-
|
|
9
|
-
"fabi-play.d.ts": "sha384-
|
|
10
|
-
"fabi-play.schema.json": "sha384-
|
|
6
|
+
"fabi-play.js": "sha384-JCeReswB7As8HjTU/NrUm65v4hdP6h+MeBvkXSOhbepLjp0ZRDwO971vvtz5c7nE",
|
|
7
|
+
"fabi-play.umd.js": "sha384-Z0PlTJwDDUykoewaUvtN9zMAajhdAGUChI0QHPAKEr9OY2skgcSpKdDHyFAAzWbO",
|
|
8
|
+
"fabi-play.umd.min.js": "sha384-CwK92hvLuW7J5QrbkM8YTsovrVOSfIeNXWgrnASrqAv0BvvfeWEvUCbsyLiuw5gk",
|
|
9
|
+
"fabi-play.d.ts": "sha384-r4+XUf2qkT+nZ5RSkQooR7IZUr/Y4F9iZGwx9lB1rHehmqytu7zPMEsQWW6rIHta",
|
|
10
|
+
"fabi-play.schema.json": "sha384-MzT9DYjL6d80Ff+4iZQFbrQVpg7q02ok5Bzkb2UKshUStaFZocty+JxuzLHXXheR"
|
|
11
11
|
}
|
|
12
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabi-play/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "What a game imports: players, movements and the end of the game, from inside its sandboxed frame.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabi-play",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@fabi-play/avatars": "^0.
|
|
40
|
-
"@fabi-play/protocol": "^0.
|
|
39
|
+
"@fabi-play/avatars": "^0.5.0",
|
|
40
|
+
"@fabi-play/protocol": "^0.5.0",
|
|
41
41
|
"dts-bundle-generator": "9.5.1",
|
|
42
42
|
"esbuild": "0.28.2",
|
|
43
43
|
"tsx": "4.23.15",
|