@fabi-play/protocol 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 +12 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,18 @@ and offers only games that list `'portrait'`; the TV and the tablet are landscap
|
|
|
66
66
|
game starts in with `init.orientation` (`orientationOf(width, height)`: width ≥ height is `'landscape'`); absent from
|
|
67
67
|
older hosts, it means `'landscape'`. It is informational: a game lays out from its real viewport size.
|
|
68
68
|
|
|
69
|
+
**Aiming.** `aimPoint(body | frame, side, { orientation, view })` is the one rule for where a hand aims on the
|
|
70
|
+
screen (what it touches), in fractions: in landscape `toScreen(view, wrist)`; in portrait, where the camera image is
|
|
71
|
+
small at the bottom of a phone, the wrist stretched around the shoulders to the whole screen (`HAND_SPACE`, the
|
|
72
|
+
stretch of `PlayerFrame.leftHand`, clamped to 0–1; with the hips unseen the torso is at least
|
|
73
|
+
`TORSO_PER_SHOULDER_WIDTH` shoulder widths). null when the wrist (in portrait, the shoulders too) isn't seen
|
|
74
|
+
(`AIM_VISIBLE`). `AimHold` keeps a lost hand where it was for `AIM_HOLD_MS` (300). The SDK's `play.aim` and the
|
|
75
|
+
engine's `hands()` apply it.
|
|
76
|
+
|
|
77
|
+
**Safe area.** `init.safeArea` (optional) is what a phone's notch, rounded corners and home bar cover, in fractions
|
|
78
|
+
of the game's viewport: `{ top, bottom, left, right }` (`SafeArea`). `parseSafeArea` clamps each side to 0–0.25
|
|
79
|
+
(`MAX_SAFE_INSET`), 0 when missing; `NO_SAFE_AREA` is all zeros, what hosts that don't send it mean.
|
|
80
|
+
|
|
69
81
|
## The carousel on two screens
|
|
70
82
|
|
|
71
83
|
The phone can show the TV's carousel of games, and both move together. While the grown-up swipes, the phone sends
|
package/dist/index.d.ts
CHANGED
|
@@ -611,6 +611,87 @@ export interface BackdropView {
|
|
|
611
611
|
export declare function parseView(v: unknown): BackdropView | undefined;
|
|
612
612
|
/** A point of player space (a body point) on the game's viewport, in fractions; unchanged without a view. */
|
|
613
613
|
export declare const toScreen: (view: BackdropView | null | undefined, p: Point) => Point;
|
|
614
|
+
/** A hand: the child's own left or right. */
|
|
615
|
+
export type HandSide = "left" | "right";
|
|
616
|
+
/** aimPoint's options: the screen's orientation (absent = 'landscape') and the view (see BackdropView). */
|
|
617
|
+
export interface AimOptions {
|
|
618
|
+
orientation?: Orientation;
|
|
619
|
+
view?: BackdropView | null;
|
|
620
|
+
}
|
|
621
|
+
/** A body point counts for aiming when the camera sees it this well (as the avatars and the engine do). */
|
|
622
|
+
export declare const AIM_VISIBLE = 0.5;
|
|
623
|
+
/**
|
|
624
|
+
* The space the arms reach, around the shoulders, in torsos (shoulders to hips): HAND_SPACE.left and .top from the
|
|
625
|
+
* shoulders' midpoint, HAND_SPACE.w × HAND_SPACE.h wide and high. It is stretched to the whole screen: the space of
|
|
626
|
+
* PlayerFrame.leftHand and rightHand (@fabi-play/motion's hands() and handSpace()), and of aimPoint in portrait.
|
|
627
|
+
*/
|
|
628
|
+
export declare const HAND_SPACE: {
|
|
629
|
+
readonly left: 1.6;
|
|
630
|
+
readonly top: 1.6;
|
|
631
|
+
readonly w: 3.2;
|
|
632
|
+
readonly h: 3;
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* The torso when the hips are not seen (a phone close by, hips out of the image, where their points are guesses
|
|
636
|
+
* clamped to its edge): at least this many shoulder widths, so the stretch does not blow up.
|
|
637
|
+
*/
|
|
638
|
+
export declare const TORSO_PER_SHOULDER_WIDTH = 1.2;
|
|
639
|
+
/**
|
|
640
|
+
* Where a hand aims on the game's viewport, in fractions (0–1): what the hand touches, picks, catches. One rule for
|
|
641
|
+
* every game, by orientation:
|
|
642
|
+
*
|
|
643
|
+
* - 'landscape' (a TV, a tablet; the default): on the body, toScreen(view, wrist), where the child sees their hand
|
|
644
|
+
* (in the video, or the avatar's when body is the smoothed body the avatar is drawn with). Not clamped.
|
|
645
|
+
* - 'portrait' (a phone held upright): the camera image is small (the view sits at the bottom) and raised hands leave
|
|
646
|
+
* it, so the wrist is stretched instead (HAND_SPACE, as PlayerFrame.leftHand): the space the arms reach around the
|
|
647
|
+
* shoulders becomes the whole screen, clamped to 0–1. Arms straight up land near the top (y ≈ 0.15), arms out to
|
|
648
|
+
* the sides at the edges (x ≈ 0.02 and 0.98), the wrist on the shoulder line at y ≈ 0.53, hands down by the hips
|
|
649
|
+
* near the bottom (y ≈ 0.87). The view does not matter. When the hips are not seen the torso is at least
|
|
650
|
+
* TORSO_PER_SHOULDER_WIDTH shoulder widths.
|
|
651
|
+
*
|
|
652
|
+
* null when the camera doesn't see the wrist well enough (AIM_VISIBLE), and in portrait the shoulders too. Pure:
|
|
653
|
+
* AimHold keeps a hand that is lost for a moment. Draw on the body with toScreen(wrist); aim with this.
|
|
654
|
+
*/
|
|
655
|
+
export declare function aimPoint(who: Body | Pick<PlayerFrame, "body">, side: HandSide, opts?: AimOptions): Point | null;
|
|
656
|
+
/** How long AimHold keeps a lost hand where it last was, ms: a wrist the camera misses for a few frames. */
|
|
657
|
+
export declare const AIM_HOLD_MS = 300;
|
|
658
|
+
/**
|
|
659
|
+
* Keeps each hand's aim through short losses: a hand the camera misses for up to holdMs stays where it last was,
|
|
660
|
+
* so it does not blink out and back (and a touch is not lost and made again). Past that it is gone (null). One per
|
|
661
|
+
* game; a key per hand, e.g. AimHold.key(player, side).
|
|
662
|
+
*/
|
|
663
|
+
export declare class AimHold {
|
|
664
|
+
readonly holdMs: number;
|
|
665
|
+
private readonly last;
|
|
666
|
+
constructor(holdMs?: number);
|
|
667
|
+
/** A number for a player's hand. */
|
|
668
|
+
static key(player: number, side: HandSide): number;
|
|
669
|
+
/** The aim to use at time t (ms) for the hand key, given what aimPoint said now (p, or null if lost). */
|
|
670
|
+
at(key: number, p: Point | null, t: number): Point | null;
|
|
671
|
+
/** Forgets every hand. */
|
|
672
|
+
clear(): void;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* The parts of the game's viewport a phone's notch, rounded corners or home bar may cover, in fractions of the
|
|
676
|
+
* viewport (top and bottom of its height, left and right of its width; each 0–0.25). The game's frame cannot read
|
|
677
|
+
* env(safe-area-inset-*), so the screen sends it (init.safeArea): keep buttons, scores and targets out of it, and
|
|
678
|
+
* still paint the background under it. Zeros where nothing is covered (a TV, a tablet, older hosts).
|
|
679
|
+
*/
|
|
680
|
+
export interface SafeArea {
|
|
681
|
+
top: number;
|
|
682
|
+
bottom: number;
|
|
683
|
+
left: number;
|
|
684
|
+
right: number;
|
|
685
|
+
}
|
|
686
|
+
/** The largest inset parseSafeArea keeps, a fraction of the viewport. */
|
|
687
|
+
export declare const MAX_SAFE_INSET = 0.25;
|
|
688
|
+
/** No inset: what a game gets from hosts that don't send one. */
|
|
689
|
+
export declare const NO_SAFE_AREA: Readonly<SafeArea>;
|
|
690
|
+
/**
|
|
691
|
+
* A safe area, or undefined if it is not an object: each of top, bottom, left, right a finite number clamped to
|
|
692
|
+
* 0–MAX_SAFE_INSET, 0 if missing or not a number. Extra fields are dropped.
|
|
693
|
+
*/
|
|
694
|
+
export declare function parseSafeArea(v: unknown): SafeArea | undefined;
|
|
614
695
|
/** Quality tiers for drawing, best first: what @fabi-play/engine calls engine.tier.name. */
|
|
615
696
|
export type DeviceTier = "high" | "medium" | "low";
|
|
616
697
|
export declare const DEVICE_TIERS: DeviceTier[];
|
|
@@ -673,6 +754,10 @@ export type HostMessage =
|
|
|
673
754
|
* only a game whose manifest lists 'portrait' gets 'portrait'. Informational, to choose a layout: lay out from the
|
|
674
755
|
* real viewport size all the same. Absent (older hosts) = 'landscape'.
|
|
675
756
|
*/
|
|
757
|
+
/**
|
|
758
|
+
* safeArea: what a phone's notch, rounded corners and home bar cover, in fractions of the viewport (see SafeArea;
|
|
759
|
+
* parseSafeArea clamps each side to 0–0.25). Absent (a TV, a tablet, older hosts) = nothing covered.
|
|
760
|
+
*/
|
|
676
761
|
{
|
|
677
762
|
type: "init";
|
|
678
763
|
v: number;
|
|
@@ -690,6 +775,7 @@ export type HostMessage =
|
|
|
690
775
|
pointer?: boolean;
|
|
691
776
|
pictures?: boolean;
|
|
692
777
|
orientation?: Orientation;
|
|
778
|
+
safeArea?: SafeArea;
|
|
693
779
|
}
|
|
694
780
|
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
|
|
695
781
|
| {
|
package/dist/index.js
CHANGED
|
@@ -406,6 +406,73 @@ function parseView(v) {
|
|
|
406
406
|
return isNumber(x) && isNumber(y) && isNumber(w) && isNumber(h) && w > 0 && h > 0 ? { x, y, w, h } : void 0;
|
|
407
407
|
}
|
|
408
408
|
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 };
|
|
409
|
+
var AIM_VISIBLE = 0.5;
|
|
410
|
+
var HAND_SPACE = { left: 1.6, top: 1.6, w: 3.2, h: 3 };
|
|
411
|
+
var TORSO_PER_SHOULDER_WIDTH = 1.2;
|
|
412
|
+
function aimPoint(who, side, opts = {}) {
|
|
413
|
+
const body = Array.isArray(who) ? who : who.body;
|
|
414
|
+
if (!Array.isArray(body)) return null;
|
|
415
|
+
const w = side === "left" ? P.leftWrist : P.rightWrist;
|
|
416
|
+
const at = (i) => isNumber(body[i * 3]) && isNumber(body[i * 3 + 1]) && (body[i * 3 + 2] ?? 0) >= AIM_VISIBLE ? i : -1;
|
|
417
|
+
if (at(w) < 0) return null;
|
|
418
|
+
const wx = body[w * 3];
|
|
419
|
+
const wy = body[w * 3 + 1];
|
|
420
|
+
if (opts.orientation !== "portrait") return toScreen(opts.view, { x: wx, y: wy });
|
|
421
|
+
if (at(P.leftShoulder) < 0 || at(P.rightShoulder) < 0) return null;
|
|
422
|
+
const ls = P.leftShoulder * 3;
|
|
423
|
+
const rs = P.rightShoulder * 3;
|
|
424
|
+
const sx = (body[ls] + body[rs]) / 2;
|
|
425
|
+
const sy = (body[ls + 1] + body[rs + 1]) / 2;
|
|
426
|
+
const lh = P.leftHip * 3;
|
|
427
|
+
const rh = P.rightHip * 3;
|
|
428
|
+
const hx = (body[lh] + body[rh]) / 2;
|
|
429
|
+
const hy = (body[lh + 1] + body[rh + 1]) / 2;
|
|
430
|
+
let torso = isNumber(hx) && isNumber(hy) ? Math.hypot(hx - sx, hy - sy) : 0;
|
|
431
|
+
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]));
|
|
432
|
+
torso = Math.max(torso, 0.01);
|
|
433
|
+
return {
|
|
434
|
+
x: clamp012((wx - sx + HAND_SPACE.left * torso) / (HAND_SPACE.w * torso)),
|
|
435
|
+
y: clamp012((wy - sy + HAND_SPACE.top * torso) / (HAND_SPACE.h * torso))
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
var AIM_HOLD_MS = 300;
|
|
439
|
+
var AimHold = class {
|
|
440
|
+
constructor(holdMs = AIM_HOLD_MS) {
|
|
441
|
+
__publicField(this, "holdMs", holdMs);
|
|
442
|
+
__publicField(this, "last", /* @__PURE__ */ new Map());
|
|
443
|
+
}
|
|
444
|
+
/** A number for a player's hand. */
|
|
445
|
+
static key(player, side) {
|
|
446
|
+
return player * 2 + (side === "right" ? 1 : 0);
|
|
447
|
+
}
|
|
448
|
+
/** The aim to use at time t (ms) for the hand key, given what aimPoint said now (p, or null if lost). */
|
|
449
|
+
at(key, p, t) {
|
|
450
|
+
const l = this.last.get(key);
|
|
451
|
+
if (p) {
|
|
452
|
+
if (l) {
|
|
453
|
+
l.x = p.x;
|
|
454
|
+
l.y = p.y;
|
|
455
|
+
l.t = t;
|
|
456
|
+
} else this.last.set(key, { x: p.x, y: p.y, t });
|
|
457
|
+
return p;
|
|
458
|
+
}
|
|
459
|
+
if (!l) return null;
|
|
460
|
+
if (t - l.t <= this.holdMs) return { x: l.x, y: l.y };
|
|
461
|
+
this.last.delete(key);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
/** Forgets every hand. */
|
|
465
|
+
clear() {
|
|
466
|
+
this.last.clear();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var MAX_SAFE_INSET = 0.25;
|
|
470
|
+
var NO_SAFE_AREA = Object.freeze({ top: 0, bottom: 0, left: 0, right: 0 });
|
|
471
|
+
function parseSafeArea(v) {
|
|
472
|
+
if (!isObject(v)) return void 0;
|
|
473
|
+
const f = (n) => isNumber(n) ? Math.min(Math.max(n, 0), MAX_SAFE_INSET) : 0;
|
|
474
|
+
return { top: f(v.top), bottom: f(v.bottom), left: f(v.left), right: f(v.right) };
|
|
475
|
+
}
|
|
409
476
|
var DEVICE_TIERS = ["high", "medium", "low"];
|
|
410
477
|
function parseDeviceHint(v) {
|
|
411
478
|
if (!isObject(v)) return void 0;
|
|
@@ -565,6 +632,7 @@ var voiceUrl = (base, key, voice = FABI_VOICE) => `${base.replace(/\/+$/, "")}/v
|
|
|
565
632
|
var gamesFor = (games, range, players = 1) => games.filter((g) => g.ageRanges.includes(range) && players >= g.players.min && players <= g.players.max);
|
|
566
633
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
567
634
|
var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
635
|
+
var clamp012 = (n) => Math.min(Math.max(n, 0), 1);
|
|
568
636
|
var SEGMENT = "[A-Za-z0-9_][A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*";
|
|
569
637
|
var isGamePath = (v, ext) => typeof v === "string" && v.length <= 200 && new RegExp(`^(?:${SEGMENT}/)*${SEGMENT}\\.(?:${ext})$`).test(v);
|
|
570
638
|
var isGameId = (v) => /^[a-z0-9]+(-[a-z0-9]+)*$/.test(v) && v.length <= 60;
|
|
@@ -800,6 +868,9 @@ async function migrateStorageKeyAsync(store, newKey, oldKey = legacyStorageKey(n
|
|
|
800
868
|
}
|
|
801
869
|
export {
|
|
802
870
|
AGE_RANGES,
|
|
871
|
+
AIM_HOLD_MS,
|
|
872
|
+
AIM_VISIBLE,
|
|
873
|
+
AimHold,
|
|
803
874
|
BREAK_REASONS,
|
|
804
875
|
COVER_ASPECT,
|
|
805
876
|
COVER_ASPECT_TOLERANCE,
|
|
@@ -814,6 +885,7 @@ export {
|
|
|
814
885
|
GAME_BACKDROPS,
|
|
815
886
|
GAME_FRAMINGS,
|
|
816
887
|
GAME_INPUTS,
|
|
888
|
+
HAND_SPACE,
|
|
817
889
|
INTRO_MAX_CHARS,
|
|
818
890
|
INTRO_MAX_STEPS,
|
|
819
891
|
INTRO_MOVES,
|
|
@@ -824,8 +896,10 @@ export {
|
|
|
824
896
|
MAX_PICTURE_SIDE,
|
|
825
897
|
MAX_PICTURE_TITLE,
|
|
826
898
|
MAX_PLAYERS,
|
|
899
|
+
MAX_SAFE_INSET,
|
|
827
900
|
MIRROR_ENABLED,
|
|
828
901
|
MOVEMENTS,
|
|
902
|
+
NO_SAFE_AREA,
|
|
829
903
|
NS,
|
|
830
904
|
ORIENTATIONS,
|
|
831
905
|
P,
|
|
@@ -845,9 +919,11 @@ export {
|
|
|
845
919
|
SCREEN_SIZES,
|
|
846
920
|
SCREEN_SIZE_KEY,
|
|
847
921
|
TOO_CLOSE_KEY,
|
|
922
|
+
TORSO_PER_SHOULDER_WIDTH,
|
|
848
923
|
VIDEO_BLOCKS,
|
|
849
924
|
VIDEO_STATES,
|
|
850
925
|
acceptsBinaryPose,
|
|
926
|
+
aimPoint,
|
|
851
927
|
checkPicture,
|
|
852
928
|
chunkPicture,
|
|
853
929
|
decodePose,
|
|
@@ -882,6 +958,7 @@ export {
|
|
|
882
958
|
parsePictureMessage,
|
|
883
959
|
parsePictureResult,
|
|
884
960
|
parsePictureTitle,
|
|
961
|
+
parseSafeArea,
|
|
885
962
|
parseScreenMessage,
|
|
886
963
|
parseTurnOrder,
|
|
887
964
|
parseTurnPlayers,
|