@fabi-play/sdk 0.5.0 → 0.7.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 +27 -1
- package/dist/avatars.js +1 -1
- package/dist/chunks/{chunk-H3BKYVPL.js → chunk-WS2JRQ53.js} +72 -0
- package/dist/fabi-play.d.ts +64 -2
- package/dist/fabi-play.js +1 -1
- package/dist/fabi-play.umd.js +98 -6
- package/dist/fabi-play.umd.min.js +1 -1
- package/dist/index.d.ts +64 -2
- package/dist/index.js +34 -6
- package/dist/integrity.json +5 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -76,6 +76,31 @@ view), and offers only games that say they can be played in portrait:
|
|
|
76
76
|
`'landscape'` (also what older hosts mean). Use it to choose a layout, but size everything from the real viewport
|
|
77
77
|
(`innerWidth` × `innerHeight`).
|
|
78
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
|
+
- **Aim with `play.aim`, not with `toScreen(wrist)`.** On a phone the camera image (`play.view`) is drawn small at the
|
|
80
|
+
bottom of the screen, and raised hands even leave it, so a wrist through `toScreen` never reaches the top.
|
|
81
|
+
`play.aim(player, side)` (`side` `'left'` or `'right'`; a player id or a frame) is where a hand aims, in fractions
|
|
82
|
+
of the page: in portrait the arms' reach around the shoulders stretched to the whole screen (arms straight up near
|
|
83
|
+
the top, y ≈ 0.15; arms out to the sides near the edges; the wrist on the shoulder line at y ≈ 0.53; hands by the
|
|
84
|
+
hips near the bottom, y ≈ 0.87), held for 300 ms when the camera loses the wrist; in landscape exactly
|
|
85
|
+
`toScreen(wrist)` on the avatar's hand (or the real one over the video), as before. So one line works on every
|
|
86
|
+
screen: **use `aim` for what the hand touches, catches or presses; `toScreen(wrist)` only to draw on the body**.
|
|
87
|
+
The script tag has `FabiPlay.aim(player, side)` and the `hands` event's `aim: { left, right }`; with
|
|
88
|
+
`@fabi-play/engine`, `engine.hands()`, `engine.touched()` and `engine.handMarkers()` already aim, nothing to change.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const p = play.aim(1, 'right')
|
|
92
|
+
if (p && near(star, p.x * innerWidth, p.y * innerHeight)) pop(star)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Safe area.** A phone's notch, rounded corners and home bar cover parts of the page, and the game's frame can't
|
|
96
|
+
read `env(safe-area-inset-*)`. `play.safeArea` (`FabiPlay.safeArea`, the `start` event's `safeArea`) says how much,
|
|
97
|
+
in fractions of the page: `{ top, bottom, left, right }`, top and bottom of its height, left and right of its
|
|
98
|
+
width, each from 0 to 0.25; zeros on a TV, a tablet and from older hosts. Keep buttons, scores and targets out of
|
|
99
|
+
it, and still paint your background under it:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
hud.style.top = `${play.safeArea.top * innerHeight}px`
|
|
103
|
+
```
|
|
79
104
|
|
|
80
105
|
## Fabi explains your game (intro)
|
|
81
106
|
|
|
@@ -116,7 +141,8 @@ of the game, or a move shown with the children's own avatars, which they can als
|
|
|
116
141
|
|
|
117
142
|
`play.view` is where the camera image lies on your page, so each child's avatar and hands are where the child stands.
|
|
118
143
|
Draw anything that belongs to a child at `play.toScreen(point)` (a wrist, the head, the avatar's box), never at your
|
|
119
|
-
own mapping of player space.
|
|
144
|
+
own mapping of player space. For what a hand touches, use `play.aim(player, side)`: the same as `toScreen(wrist)` on
|
|
145
|
+
a TV or a tablet, and on a phone held upright the hand stretched to the whole screen (see Portrait).
|
|
120
146
|
|
|
121
147
|
The view is not fixed. By default the screen frames the children: when they stand far from the camera it zooms in on
|
|
122
148
|
them, up to 1.8×, so every child's avatar is about the same size and reaches the same things, near or far. It decides
|
package/dist/avatars.js
CHANGED
|
@@ -70,6 +70,73 @@ function parseView(v) {
|
|
|
70
70
|
return isNumber(x) && isNumber(y) && isNumber(w) && isNumber(h) && w > 0 && h > 0 ? { x, y, w, h } : void 0;
|
|
71
71
|
}
|
|
72
72
|
var toScreen = (view, p) => view ? { x: view.x + p.x * view.w, y: view.y + p.y * view.h } : { x: p.x, y: p.y };
|
|
73
|
+
var AIM_VISIBLE = 0.5;
|
|
74
|
+
var HAND_SPACE = { left: 1.6, top: 1.6, w: 3.2, h: 3 };
|
|
75
|
+
var TORSO_PER_SHOULDER_WIDTH = 1.2;
|
|
76
|
+
function aimPoint(who, side, opts = {}) {
|
|
77
|
+
const body = Array.isArray(who) ? who : who.body;
|
|
78
|
+
if (!Array.isArray(body)) return null;
|
|
79
|
+
const w = side === "left" ? P.leftWrist : P.rightWrist;
|
|
80
|
+
const at = (i) => isNumber(body[i * 3]) && isNumber(body[i * 3 + 1]) && (body[i * 3 + 2] ?? 0) >= AIM_VISIBLE ? i : -1;
|
|
81
|
+
if (at(w) < 0) return null;
|
|
82
|
+
const wx = body[w * 3];
|
|
83
|
+
const wy = body[w * 3 + 1];
|
|
84
|
+
if (opts.orientation !== "portrait") return toScreen(opts.view, { x: wx, y: wy });
|
|
85
|
+
if (at(P.leftShoulder) < 0 || at(P.rightShoulder) < 0) return null;
|
|
86
|
+
const ls = P.leftShoulder * 3;
|
|
87
|
+
const rs = P.rightShoulder * 3;
|
|
88
|
+
const sx = (body[ls] + body[rs]) / 2;
|
|
89
|
+
const sy = (body[ls + 1] + body[rs + 1]) / 2;
|
|
90
|
+
const lh = P.leftHip * 3;
|
|
91
|
+
const rh = P.rightHip * 3;
|
|
92
|
+
const hx = (body[lh] + body[rh]) / 2;
|
|
93
|
+
const hy = (body[lh + 1] + body[rh + 1]) / 2;
|
|
94
|
+
let torso = isNumber(hx) && isNumber(hy) ? Math.hypot(hx - sx, hy - sy) : 0;
|
|
95
|
+
if (at(P.leftHip) < 0 || at(P.rightHip) < 0) torso = Math.max(torso, TORSO_PER_SHOULDER_WIDTH * Math.hypot(body[ls] - body[rs], body[ls + 1] - body[rs + 1]));
|
|
96
|
+
torso = Math.max(torso, 0.01);
|
|
97
|
+
return {
|
|
98
|
+
x: clamp01((wx - sx + HAND_SPACE.left * torso) / (HAND_SPACE.w * torso)),
|
|
99
|
+
y: clamp01((wy - sy + HAND_SPACE.top * torso) / (HAND_SPACE.h * torso))
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var AIM_HOLD_MS = 300;
|
|
103
|
+
var AimHold = class {
|
|
104
|
+
constructor(holdMs = AIM_HOLD_MS) {
|
|
105
|
+
__publicField(this, "holdMs", holdMs);
|
|
106
|
+
__publicField(this, "last", /* @__PURE__ */ new Map());
|
|
107
|
+
}
|
|
108
|
+
/** A number for a player's hand. */
|
|
109
|
+
static key(player, side) {
|
|
110
|
+
return player * 2 + (side === "right" ? 1 : 0);
|
|
111
|
+
}
|
|
112
|
+
/** The aim to use at time t (ms) for the hand key, given what aimPoint said now (p, or null if lost). */
|
|
113
|
+
at(key, p, t) {
|
|
114
|
+
const l = this.last.get(key);
|
|
115
|
+
if (p) {
|
|
116
|
+
if (l) {
|
|
117
|
+
l.x = p.x;
|
|
118
|
+
l.y = p.y;
|
|
119
|
+
l.t = t;
|
|
120
|
+
} else this.last.set(key, { x: p.x, y: p.y, t });
|
|
121
|
+
return p;
|
|
122
|
+
}
|
|
123
|
+
if (!l) return null;
|
|
124
|
+
if (t - l.t <= this.holdMs) return { x: l.x, y: l.y };
|
|
125
|
+
this.last.delete(key);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/** Forgets every hand. */
|
|
129
|
+
clear() {
|
|
130
|
+
this.last.clear();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var MAX_SAFE_INSET = 0.25;
|
|
134
|
+
var NO_SAFE_AREA = Object.freeze({ top: 0, bottom: 0, left: 0, right: 0 });
|
|
135
|
+
function parseSafeArea(v) {
|
|
136
|
+
if (!isObject(v)) return void 0;
|
|
137
|
+
const f2 = (n) => isNumber(n) ? Math.min(Math.max(n, 0), MAX_SAFE_INSET) : 0;
|
|
138
|
+
return { top: f2(v.top), bottom: f2(v.bottom), left: f2(v.left), right: f2(v.right) };
|
|
139
|
+
}
|
|
73
140
|
var DEVICE_TIERS = ["high", "medium", "low"];
|
|
74
141
|
function parseDeviceHint(v) {
|
|
75
142
|
if (!isObject(v)) return void 0;
|
|
@@ -106,6 +173,7 @@ function parseTurnOrder(v) {
|
|
|
106
173
|
var COVER_ASPECT = 16 / 9;
|
|
107
174
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
108
175
|
var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
176
|
+
var clamp01 = (n) => Math.min(Math.max(n, 0), 1);
|
|
109
177
|
var POSE_HEADER = 12;
|
|
110
178
|
var POSE_BODY_BYTES = LANDMARKS * 4;
|
|
111
179
|
var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
|
|
@@ -2385,6 +2453,10 @@ export {
|
|
|
2385
2453
|
wire,
|
|
2386
2454
|
parseView,
|
|
2387
2455
|
toScreen,
|
|
2456
|
+
aimPoint,
|
|
2457
|
+
AimHold,
|
|
2458
|
+
NO_SAFE_AREA,
|
|
2459
|
+
parseSafeArea,
|
|
2388
2460
|
parseDeviceHint,
|
|
2389
2461
|
parseGameStats,
|
|
2390
2462
|
parseCursor,
|
package/dist/fabi-play.d.ts
CHANGED
|
@@ -178,6 +178,20 @@ declare namespace FabiPlay {
|
|
|
178
178
|
/** Height, a fraction of the viewport's height. */
|
|
179
179
|
h: number;
|
|
180
180
|
}
|
|
181
|
+
/** A hand: the child's own left or right. */
|
|
182
|
+
export type HandSide = "left" | "right";
|
|
183
|
+
/**
|
|
184
|
+
* The parts of the game's viewport a phone's notch, rounded corners or home bar may cover, in fractions of the
|
|
185
|
+
* viewport (top and bottom of its height, left and right of its width; each 0–0.25). The game's frame cannot read
|
|
186
|
+
* env(safe-area-inset-*), so the screen sends it (init.safeArea): keep buttons, scores and targets out of it, and
|
|
187
|
+
* still paint the background under it. Zeros where nothing is covered (a TV, a tablet, older hosts).
|
|
188
|
+
*/
|
|
189
|
+
export interface SafeArea {
|
|
190
|
+
top: number;
|
|
191
|
+
bottom: number;
|
|
192
|
+
left: number;
|
|
193
|
+
right: number;
|
|
194
|
+
}
|
|
181
195
|
/** Quality tiers for drawing, best first: what @fabi-play/engine calls engine.tier.name. */
|
|
182
196
|
export type DeviceTier = "high" | "medium" | "low";
|
|
183
197
|
/**
|
|
@@ -235,6 +249,10 @@ declare namespace FabiPlay {
|
|
|
235
249
|
* only a game whose manifest lists 'portrait' gets 'portrait'. Informational, to choose a layout: lay out from the
|
|
236
250
|
* real viewport size all the same. Absent (older hosts) = 'landscape'.
|
|
237
251
|
*/
|
|
252
|
+
/**
|
|
253
|
+
* safeArea: what a phone's notch, rounded corners and home bar cover, in fractions of the viewport (see SafeArea;
|
|
254
|
+
* parseSafeArea clamps each side to 0–0.25). Absent (a TV, a tablet, older hosts) = nothing covered.
|
|
255
|
+
*/
|
|
238
256
|
{
|
|
239
257
|
type: "init";
|
|
240
258
|
v: number;
|
|
@@ -252,6 +270,7 @@ declare namespace FabiPlay {
|
|
|
252
270
|
pointer?: boolean;
|
|
253
271
|
pictures?: boolean;
|
|
254
272
|
orientation?: Orientation;
|
|
273
|
+
safeArea?: SafeArea;
|
|
255
274
|
}
|
|
256
275
|
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
|
|
257
276
|
| {
|
|
@@ -581,6 +600,8 @@ declare namespace FabiPlay {
|
|
|
581
600
|
mode: "tv" | "solo";
|
|
582
601
|
/** 'landscape' or 'portrait' (a phone held upright): the screen's orientation at the start, see Play.orientation. */
|
|
583
602
|
orientation: Orientation;
|
|
603
|
+
/** What a phone's notch and home bar cover, fractions of the viewport (zeros if nothing): see Play.safeArea. */
|
|
604
|
+
safeArea: Readonly<SafeArea>;
|
|
584
605
|
/** 'video': mirror mode, paint no opaque background; 'none': paint your own. See the backdrop event. */
|
|
585
606
|
backdrop: Backdrop;
|
|
586
607
|
/** Where the camera image is (with 'video', the video; with 'none', where the avatars stand), see toScreen; null if unknown. */
|
|
@@ -609,11 +630,20 @@ declare namespace FabiPlay {
|
|
|
609
630
|
} & {
|
|
610
631
|
/** The screen started the game (late listeners still get it). */
|
|
611
632
|
start: StartInfo;
|
|
612
|
-
/**
|
|
633
|
+
/**
|
|
634
|
+
* Every frame, the hands of each player, in fractions of the screen (0–1). aim: where each hand aims, for what it
|
|
635
|
+
* touches (see Easy.aim: the hand on the body in landscape, stretched to the whole screen in portrait). left and
|
|
636
|
+
* right: the hands as pointers, the arms' reach stretched to the whole screen in every orientation
|
|
637
|
+
* (PlayerFrame.leftHand and rightHand). null: not seen.
|
|
638
|
+
*/
|
|
613
639
|
hands: {
|
|
614
640
|
player: number;
|
|
615
641
|
left: Point | null;
|
|
616
642
|
right: Point | null;
|
|
643
|
+
aim: {
|
|
644
|
+
left: Point | null;
|
|
645
|
+
right: Point | null;
|
|
646
|
+
};
|
|
617
647
|
};
|
|
618
648
|
/** When a player's shape changes (arms up, wide, …). */
|
|
619
649
|
shape: {
|
|
@@ -668,7 +698,7 @@ declare namespace FabiPlay {
|
|
|
668
698
|
*
|
|
669
699
|
* @example
|
|
670
700
|
* FabiPlay.on('jump', ({ player }) => hop(player))
|
|
671
|
-
* FabiPlay.on('hands', ({ player,
|
|
701
|
+
* FabiPlay.on('hands', ({ player, aim }) => aim.right && touch(player, aim.right))
|
|
672
702
|
* await FabiPlay.break('level')
|
|
673
703
|
* FabiPlay.end(120)
|
|
674
704
|
*/
|
|
@@ -736,6 +766,10 @@ declare namespace FabiPlay {
|
|
|
736
766
|
}): Promise<PictureResult>;
|
|
737
767
|
/** The screen's sound is off (see StartInfo.muted); false before start. */
|
|
738
768
|
readonly muted: boolean;
|
|
769
|
+
/** The screen's orientation at the start (see Play.orientation); 'landscape' before start. */
|
|
770
|
+
readonly orientation: Orientation;
|
|
771
|
+
/** What a phone's notch and home bar cover, fractions of the viewport (see Play.safeArea); zeros before start. */
|
|
772
|
+
readonly safeArea: Readonly<SafeArea>;
|
|
739
773
|
/** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
|
|
740
774
|
readonly view: BackdropView | null;
|
|
741
775
|
/** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
|
|
@@ -747,6 +781,13 @@ declare namespace FabiPlay {
|
|
|
747
781
|
* video or on their avatar. Draw hands and effects at toScreen(wrist), in every mode. Unchanged without a view.
|
|
748
782
|
*/
|
|
749
783
|
toScreen(p: Point): Point;
|
|
784
|
+
/**
|
|
785
|
+
* Where a player's hand aims on the screen, in fractions (0–1), by player id or from a frame: use it for what the
|
|
786
|
+
* hand touches; toScreen(wrist) only to draw on the body. On a TV or a tablet the hand on the body (the avatar's,
|
|
787
|
+
* or the real one over the video); on a phone held upright the wrist stretched to the whole screen, so raised
|
|
788
|
+
* hands reach the top. See Play.aim. null when not seen, and before start.
|
|
789
|
+
*/
|
|
790
|
+
aim(player: number | PlayerFrame, side: HandSide): Point | null;
|
|
750
791
|
/**
|
|
751
792
|
* The avatar id a player picked on the phone, by player id or from a frame (players[i]); the slot's default
|
|
752
793
|
* (fox, lion, dragon, unicorn: the 6–8 band's, as no age is known here) until the first frame says otherwise.
|
|
@@ -898,6 +939,13 @@ declare namespace FabiPlay {
|
|
|
898
939
|
* the screen turns.
|
|
899
940
|
*/
|
|
900
941
|
readonly orientation: Orientation;
|
|
942
|
+
/**
|
|
943
|
+
* What a phone's notch, rounded corners and home bar cover, in fractions of the viewport: top and bottom of its
|
|
944
|
+
* height, left and right of its width, each 0–0.25. The game's frame can't read env(safe-area-inset-*), so the
|
|
945
|
+
* screen says it. Keep buttons, scores and targets out of it (e.g. a top bar from safeArea.top × innerHeight down);
|
|
946
|
+
* still paint the background under it. Zeros (frozen) on a TV, a tablet, and from hosts that don't say.
|
|
947
|
+
*/
|
|
948
|
+
readonly safeArea: Readonly<SafeArea>;
|
|
901
949
|
/** The screen's sound is off: start the game muted. */
|
|
902
950
|
readonly muted: boolean;
|
|
903
951
|
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
@@ -920,6 +968,20 @@ declare namespace FabiPlay {
|
|
|
920
968
|
* mode. Unchanged without a view.
|
|
921
969
|
*/
|
|
922
970
|
toScreen(p: Point): Point;
|
|
971
|
+
/**
|
|
972
|
+
* Where a player's hand aims on the game's viewport, in fractions (0–1), by player id (their newest frame) or from
|
|
973
|
+
* a frame: use it for what the hand touches, catches, presses; toScreen(wrist) only to draw on the body. One rule
|
|
974
|
+
* for every game, the protocol's aimPoint: in landscape (a TV, a tablet) the wrist through toScreen, on the
|
|
975
|
+
* avatar's hand (its smoothed body) or, over the video, on the real one; in portrait (a phone held upright, where
|
|
976
|
+
* the camera image is small at the bottom and raised hands leave it) the wrist stretched around the shoulders to
|
|
977
|
+
* the whole screen (as PlayerFrame.leftHand), so arms straight up reach the top (y ≈ 0.15) and arms out the edges,
|
|
978
|
+
* held 300 ms when the camera loses the wrist. null when the hand isn't seen.
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* const p = play.aim(1, 'right')
|
|
982
|
+
* if (p && hit(star, p.x * innerWidth, p.y * innerHeight)) pop(star)
|
|
983
|
+
*/
|
|
984
|
+
aim(player: number | PlayerFrame, side: HandSide): Point | null;
|
|
923
985
|
/**
|
|
924
986
|
* The avatar id a player picked on the phone (fox, lion, robot, …), by player id or from a frame. Each
|
|
925
987
|
* frame carries it; before the first frame, from an older host, or for an id this SDK doesn't know: the slot's
|