@fabi-play/engine 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.
@@ -1,4 +1,4 @@
1
- /*! @fabi-play/engine 0.5.0 (full) | Fabi Play Kit License | https://docs.fabiplay.app */
1
+ /*! @fabi-play/engine 0.7.0 (full) | Fabi Play Kit License | https://docs.fabiplay.app */
2
2
  (function (root, factory) {
3
3
  var script = typeof document !== 'undefined' ? document.currentScript : null;
4
4
  if (typeof define === 'function' && define.amd) define([], function () { return factory(script); });
@@ -119,7 +119,77 @@ var defaultAvatarFor = (slot, ageRange) => {
119
119
  return list[Number.isInteger(slot) && slot >= 0 ? slot % list.length : 0];
120
120
  };
121
121
  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 };
122
+ var AIM_VISIBLE = 0.5;
123
+ var HAND_SPACE = { left: 1.6, top: 1.6, w: 3.2, h: 3 };
124
+ var TORSO_PER_SHOULDER_WIDTH = 1.2;
125
+ function aimPoint(who, side, opts = {}) {
126
+ const body = Array.isArray(who) ? who : who.body;
127
+ if (!Array.isArray(body)) return null;
128
+ const w2 = side === "left" ? P.leftWrist : P.rightWrist;
129
+ const at = (i) => isNumber(body[i * 3]) && isNumber(body[i * 3 + 1]) && (body[i * 3 + 2] ?? 0) >= AIM_VISIBLE ? i : -1;
130
+ if (at(w2) < 0) return null;
131
+ const wx = body[w2 * 3];
132
+ const wy = body[w2 * 3 + 1];
133
+ if (opts.orientation !== "portrait") return toScreen(opts.view, { x: wx, y: wy });
134
+ if (at(P.leftShoulder) < 0 || at(P.rightShoulder) < 0) return null;
135
+ const ls = P.leftShoulder * 3;
136
+ const rs = P.rightShoulder * 3;
137
+ const sx = (body[ls] + body[rs]) / 2;
138
+ const sy = (body[ls + 1] + body[rs + 1]) / 2;
139
+ const lh = P.leftHip * 3;
140
+ const rh = P.rightHip * 3;
141
+ const hx = (body[lh] + body[rh]) / 2;
142
+ const hy = (body[lh + 1] + body[rh + 1]) / 2;
143
+ let torso = isNumber(hx) && isNumber(hy) ? Math.hypot(hx - sx, hy - sy) : 0;
144
+ 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]));
145
+ torso = Math.max(torso, 0.01);
146
+ return {
147
+ x: clamp01((wx - sx + HAND_SPACE.left * torso) / (HAND_SPACE.w * torso)),
148
+ y: clamp01((wy - sy + HAND_SPACE.top * torso) / (HAND_SPACE.h * torso))
149
+ };
150
+ }
151
+ var AIM_HOLD_MS = 300;
152
+ var AimHold = class {
153
+ constructor(holdMs = AIM_HOLD_MS) {
154
+ __publicField(this, "holdMs", holdMs);
155
+ __publicField(this, "last", /* @__PURE__ */ new Map());
156
+ }
157
+ /** A number for a player's hand. */
158
+ static key(player, side) {
159
+ return player * 2 + (side === "right" ? 1 : 0);
160
+ }
161
+ /** The aim to use at time t (ms) for the hand key, given what aimPoint said now (p, or null if lost). */
162
+ at(key, p, t) {
163
+ const l = this.last.get(key);
164
+ if (p) {
165
+ if (l) {
166
+ l.x = p.x;
167
+ l.y = p.y;
168
+ l.t = t;
169
+ } else this.last.set(key, { x: p.x, y: p.y, t });
170
+ return p;
171
+ }
172
+ if (!l) return null;
173
+ if (t - l.t <= this.holdMs) return { x: l.x, y: l.y };
174
+ this.last.delete(key);
175
+ return null;
176
+ }
177
+ /** Forgets every hand. */
178
+ clear() {
179
+ this.last.clear();
180
+ }
181
+ };
182
+ var MAX_SAFE_INSET = 0.25;
183
+ var NO_SAFE_AREA = Object.freeze({ top: 0, bottom: 0, left: 0, right: 0 });
184
+ function parseSafeArea(v) {
185
+ if (!isObject(v)) return void 0;
186
+ const f2 = (n) => isNumber(n) ? Math.min(Math.max(n, 0), MAX_SAFE_INSET) : 0;
187
+ return { top: f2(v.top), bottom: f2(v.bottom), left: f2(v.left), right: f2(v.right) };
188
+ }
122
189
  var COVER_ASPECT = 16 / 9;
190
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
191
+ var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
192
+ var clamp01 = (n) => Math.min(Math.max(n, 0), 1);
123
193
  var POSE_HEADER = 12;
124
194
  var POSE_BODY_BYTES = LANDMARKS * 4;
125
195
  var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
@@ -6763,11 +6833,20 @@ function wrists(body) {
6763
6833
  return out;
6764
6834
  }
6765
6835
  var playerColor = (player) => PLAYER_COLORS[((player - 1) % PLAYER_COLORS.length + PLAYER_COLORS.length) % PLAYER_COLORS.length];
6766
- function handsOf(frames, toScreen2, width, height, bodyOf = () => null) {
6836
+ var SIDES2 = ["left", "right"];
6837
+ function handsOf(frames, toScreen2, width, height, bodyOf = () => null, aim = {}) {
6767
6838
  const out = [];
6768
6839
  for (const f2 of frames) {
6769
6840
  const body = bodyOf(f2) ?? f2.body;
6770
6841
  if (!Array.isArray(body)) continue;
6842
+ if (aim.orientation === "portrait") {
6843
+ for (const side of SIDES2) {
6844
+ let p = aimPoint(body, side, { orientation: "portrait" });
6845
+ if (aim.hold) p = aim.hold.at(AimHold.key(f2.id, side), p, aim.t ?? 0);
6846
+ if (p) out.push({ player: f2.id, side, x: p.x * width, y: p.y * height, color: playerColor(f2.id) });
6847
+ }
6848
+ continue;
6849
+ }
6771
6850
  for (const w2 of wrists(body)) {
6772
6851
  const s = toScreen2(w2);
6773
6852
  out.push({ player: f2.id, side: w2.side, x: s.x * width, y: s.y * height, color: playerColor(f2.id) });
@@ -7429,6 +7508,10 @@ var HandMarkers = class extends Node {
7429
7508
  }
7430
7509
  };
7431
7510
  var now2 = () => performance.now();
7511
+ var frozenSafeArea = (v) => {
7512
+ const sa = parseSafeArea(v);
7513
+ return sa && Object.freeze(sa);
7514
+ };
7432
7515
  async function createEngine(opts = {}) {
7433
7516
  const play = opts.play ?? null;
7434
7517
  let canvas = opts.canvas ?? null;
@@ -7563,6 +7646,8 @@ async function createEngine(opts = {}) {
7563
7646
  ro?.observe(canvas);
7564
7647
  let backdrop = play?.backdrop === "video" ? "video" : "none";
7565
7648
  let view = play?.view ?? null;
7649
+ let orientation = play?.orientation === "portrait" ? "portrait" : "landscape";
7650
+ let safeArea = frozenSafeArea(play?.safeArea) ?? NO_SAFE_AREA;
7566
7651
  let livePlayers = [];
7567
7652
  let lastPlayersAt = -Infinity;
7568
7653
  let hostPaused = false;
@@ -7590,7 +7675,7 @@ async function createEngine(opts = {}) {
7590
7675
  lastPlayersAt = now2();
7591
7676
  }
7592
7677
  };
7593
- const hold = (on) => {
7678
+ const hold2 = (on) => {
7594
7679
  hostPaused = on;
7595
7680
  syncSound();
7596
7681
  };
@@ -7604,6 +7689,11 @@ async function createEngine(opts = {}) {
7604
7689
  };
7605
7690
  unlisten.push(
7606
7691
  play.on("start", (m) => lateHint(m?.device)),
7692
+ play.on("start", (m) => {
7693
+ if (m?.orientation === "portrait" || m?.orientation === "landscape") orientation = m.orientation;
7694
+ const sa = frozenSafeArea(m?.safeArea);
7695
+ if (sa) safeArea = sa;
7696
+ }),
7607
7697
  // The simple API knows muted only from the start (its getter is false before): read it again then.
7608
7698
  play.on("start", (m) => {
7609
7699
  if (typeof m?.muted === "boolean") sound.muted = m.muted;
@@ -7612,11 +7702,11 @@ async function createEngine(opts = {}) {
7612
7702
  play.on("players", (m) => setPlayers(m?.players)),
7613
7703
  play.on("backdrop", (m) => setBackdrop(m?.backdrop, m?.view)),
7614
7704
  play.on("start", (m) => setBackdrop(m?.backdrop, m?.view ?? play.view)),
7615
- play.on("pause", () => hold(true)),
7616
- play.on("resume", () => hold(false)),
7617
- play.on("break-start", () => hold(true)),
7618
- play.on("break-done", () => hold(false)),
7619
- play.on("break-end", () => hold(false))
7705
+ play.on("pause", () => hold2(true)),
7706
+ play.on("resume", () => hold2(false)),
7707
+ play.on("break-start", () => hold2(true)),
7708
+ play.on("break-done", () => hold2(false)),
7709
+ play.on("break-end", () => hold2(false))
7620
7710
  );
7621
7711
  sound.muted = play.muted === true;
7622
7712
  }
@@ -7627,11 +7717,13 @@ async function createEngine(opts = {}) {
7627
7717
  let tick = 0;
7628
7718
  const live = () => now2() - lastPlayersAt < 1e3 ? livePlayers : [];
7629
7719
  const toScreen2 = (p) => play ? play.toScreen(p) : toScreen(view, p);
7720
+ const hold = new AimHold();
7630
7721
  const allHands = () => {
7631
7722
  if (handsAt === tick) return handsCache;
7632
7723
  handsAt = tick;
7633
- const bodyOf = backdrop === "video" || !play?.smoothedBody ? () => null : (f2) => play.smoothedBody(f2);
7634
- handsCache = handsOf(live(), toScreen2, stageW, STAGE_HEIGHT, bodyOf);
7724
+ const portrait = orientation === "portrait";
7725
+ const bodyOf = backdrop === "video" && !portrait || !play?.smoothedBody ? () => null : (f2) => play.smoothedBody(f2);
7726
+ handsCache = handsOf(live(), toScreen2, stageW, STAGE_HEIGHT, bodyOf, portrait ? { orientation, hold, t: now2() } : void 0);
7635
7727
  return handsCache;
7636
7728
  };
7637
7729
  const box = { x: 0, y: 0, w: 1, h: 1 };
@@ -7794,6 +7886,12 @@ async function createEngine(opts = {}) {
7794
7886
  get view() {
7795
7887
  return view;
7796
7888
  },
7889
+ get orientation() {
7890
+ return orientation;
7891
+ },
7892
+ get safeArea() {
7893
+ return safeArea;
7894
+ },
7797
7895
  get paused() {
7798
7896
  return hostPaused || userPaused;
7799
7897
  },
@@ -8653,7 +8751,7 @@ ${msg}`);
8653
8751
  };
8654
8752
 
8655
8753
  // src/filters.ts
8656
- var clamp01 = (v) => Math.min(Math.max(v, 0), 1);
8754
+ var clamp012 = (v) => Math.min(Math.max(v, 0), 1);
8657
8755
  var pos = (v) => Math.max(v, 0);
8658
8756
  var Base = class {
8659
8757
  constructor() {
@@ -8705,11 +8803,11 @@ var Contrast = class extends Amount {
8705
8803
  };
8706
8804
  var Invert = class extends Amount {
8707
8805
  toMatrix(m) {
8708
- const a = clamp01(this.amount);
8806
+ const a = clamp012(this.amount);
8709
8807
  linear(m, 1 - 2 * a, a);
8710
8808
  }
8711
8809
  css() {
8712
- return `invert(${clamp01(this.amount)})`;
8810
+ return `invert(${clamp012(this.amount)})`;
8713
8811
  }
8714
8812
  };
8715
8813
  var Saturate = class extends Amount {
@@ -8732,7 +8830,7 @@ var Saturate = class extends Amount {
8732
8830
  };
8733
8831
  var Grayscale = class extends Amount {
8734
8832
  toMatrix(m) {
8735
- const k = 1 - clamp01(this.amount);
8833
+ const k = 1 - clamp012(this.amount);
8736
8834
  m9[0] = 0.2126 + 0.7874 * k;
8737
8835
  m9[1] = 0.7152 - 0.7152 * k;
8738
8836
  m9[2] = 0.0722 - 0.0722 * k;
@@ -8745,12 +8843,12 @@ var Grayscale = class extends Amount {
8745
8843
  rgb3(m, m9);
8746
8844
  }
8747
8845
  css() {
8748
- return `grayscale(${clamp01(this.amount)})`;
8846
+ return `grayscale(${clamp012(this.amount)})`;
8749
8847
  }
8750
8848
  };
8751
8849
  var Sepia = class extends Amount {
8752
8850
  toMatrix(m) {
8753
- const k = 1 - clamp01(this.amount);
8851
+ const k = 1 - clamp012(this.amount);
8754
8852
  m9[0] = 0.393 + 0.607 * k;
8755
8853
  m9[1] = 0.769 - 0.769 * k;
8756
8854
  m9[2] = 0.189 - 0.189 * k;
@@ -8763,7 +8861,7 @@ var Sepia = class extends Amount {
8763
8861
  rgb3(m, m9);
8764
8862
  }
8765
8863
  css() {
8766
- return `sepia(${clamp01(this.amount)})`;
8864
+ return `sepia(${clamp012(this.amount)})`;
8767
8865
  }
8768
8866
  };
8769
8867
  var HueRotate = class extends Amount {
@@ -8812,7 +8910,7 @@ var Tint = class extends Amount {
8812
8910
  return i + 2;
8813
8911
  }
8814
8912
  toMatrix(m) {
8815
- const a = clamp01(this.amount);
8913
+ const a = clamp012(this.amount);
8816
8914
  linear(m, 1 - a, 0);
8817
8915
  m[4] = (this.c >> 16 & 255) / 255 * a;
8818
8916
  m[9] = (this.c >> 8 & 255) / 255 * a;
@@ -8823,7 +8921,7 @@ var Tint = class extends Amount {
8823
8921
  return null;
8824
8922
  }
8825
8923
  paint2d(ctx, x, y, w2, h) {
8826
- const a = clamp01(this.amount);
8924
+ const a = clamp012(this.amount);
8827
8925
  if (a <= 0) return;
8828
8926
  ctx.save();
8829
8927
  ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -8889,7 +8987,7 @@ var DropShadow = class extends Base {
8889
8987
  return this.a;
8890
8988
  }
8891
8989
  set opacity(v) {
8892
- this.a = clamp01(v);
8990
+ this.a = clamp012(v);
8893
8991
  }
8894
8992
  get color() {
8895
8993
  return this.rgb;