@fabi-play/engine 0.4.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.4.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,6 +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
+ }
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);
122
193
  var POSE_HEADER = 12;
123
194
  var POSE_BODY_BYTES = LANDMARKS * 4;
124
195
  var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
@@ -6762,11 +6833,20 @@ function wrists(body) {
6762
6833
  return out;
6763
6834
  }
6764
6835
  var playerColor = (player) => PLAYER_COLORS[((player - 1) % PLAYER_COLORS.length + PLAYER_COLORS.length) % PLAYER_COLORS.length];
6765
- function handsOf(frames, toScreen2, width, height, bodyOf = () => null) {
6836
+ var SIDES2 = ["left", "right"];
6837
+ function handsOf(frames, toScreen2, width, height, bodyOf = () => null, aim = {}) {
6766
6838
  const out = [];
6767
6839
  for (const f2 of frames) {
6768
6840
  const body = bodyOf(f2) ?? f2.body;
6769
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
+ }
6770
6850
  for (const w2 of wrists(body)) {
6771
6851
  const s = toScreen2(w2);
6772
6852
  out.push({ player: f2.id, side: w2.side, x: s.x * width, y: s.y * height, color: playerColor(f2.id) });
@@ -7428,6 +7508,10 @@ var HandMarkers = class extends Node {
7428
7508
  }
7429
7509
  };
7430
7510
  var now2 = () => performance.now();
7511
+ var frozenSafeArea = (v) => {
7512
+ const sa = parseSafeArea(v);
7513
+ return sa && Object.freeze(sa);
7514
+ };
7431
7515
  async function createEngine(opts = {}) {
7432
7516
  const play = opts.play ?? null;
7433
7517
  let canvas = opts.canvas ?? null;
@@ -7562,6 +7646,8 @@ async function createEngine(opts = {}) {
7562
7646
  ro?.observe(canvas);
7563
7647
  let backdrop = play?.backdrop === "video" ? "video" : "none";
7564
7648
  let view = play?.view ?? null;
7649
+ let orientation = play?.orientation === "portrait" ? "portrait" : "landscape";
7650
+ let safeArea = frozenSafeArea(play?.safeArea) ?? NO_SAFE_AREA;
7565
7651
  let livePlayers = [];
7566
7652
  let lastPlayersAt = -Infinity;
7567
7653
  let hostPaused = false;
@@ -7589,7 +7675,7 @@ async function createEngine(opts = {}) {
7589
7675
  lastPlayersAt = now2();
7590
7676
  }
7591
7677
  };
7592
- const hold = (on) => {
7678
+ const hold2 = (on) => {
7593
7679
  hostPaused = on;
7594
7680
  syncSound();
7595
7681
  };
@@ -7603,6 +7689,11 @@ async function createEngine(opts = {}) {
7603
7689
  };
7604
7690
  unlisten.push(
7605
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
+ }),
7606
7697
  // The simple API knows muted only from the start (its getter is false before): read it again then.
7607
7698
  play.on("start", (m) => {
7608
7699
  if (typeof m?.muted === "boolean") sound.muted = m.muted;
@@ -7611,11 +7702,11 @@ async function createEngine(opts = {}) {
7611
7702
  play.on("players", (m) => setPlayers(m?.players)),
7612
7703
  play.on("backdrop", (m) => setBackdrop(m?.backdrop, m?.view)),
7613
7704
  play.on("start", (m) => setBackdrop(m?.backdrop, m?.view ?? play.view)),
7614
- play.on("pause", () => hold(true)),
7615
- play.on("resume", () => hold(false)),
7616
- play.on("break-start", () => hold(true)),
7617
- play.on("break-done", () => hold(false)),
7618
- 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))
7619
7710
  );
7620
7711
  sound.muted = play.muted === true;
7621
7712
  }
@@ -7626,11 +7717,13 @@ async function createEngine(opts = {}) {
7626
7717
  let tick = 0;
7627
7718
  const live = () => now2() - lastPlayersAt < 1e3 ? livePlayers : [];
7628
7719
  const toScreen2 = (p) => play ? play.toScreen(p) : toScreen(view, p);
7720
+ const hold = new AimHold();
7629
7721
  const allHands = () => {
7630
7722
  if (handsAt === tick) return handsCache;
7631
7723
  handsAt = tick;
7632
- const bodyOf = backdrop === "video" || !play?.smoothedBody ? () => null : (f2) => play.smoothedBody(f2);
7633
- 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);
7634
7727
  return handsCache;
7635
7728
  };
7636
7729
  const box = { x: 0, y: 0, w: 1, h: 1 };
@@ -7793,6 +7886,12 @@ async function createEngine(opts = {}) {
7793
7886
  get view() {
7794
7887
  return view;
7795
7888
  },
7889
+ get orientation() {
7890
+ return orientation;
7891
+ },
7892
+ get safeArea() {
7893
+ return safeArea;
7894
+ },
7796
7895
  get paused() {
7797
7896
  return hostPaused || userPaused;
7798
7897
  },
@@ -8652,7 +8751,7 @@ ${msg}`);
8652
8751
  };
8653
8752
 
8654
8753
  // src/filters.ts
8655
- var clamp01 = (v) => Math.min(Math.max(v, 0), 1);
8754
+ var clamp012 = (v) => Math.min(Math.max(v, 0), 1);
8656
8755
  var pos = (v) => Math.max(v, 0);
8657
8756
  var Base = class {
8658
8757
  constructor() {
@@ -8704,11 +8803,11 @@ var Contrast = class extends Amount {
8704
8803
  };
8705
8804
  var Invert = class extends Amount {
8706
8805
  toMatrix(m) {
8707
- const a = clamp01(this.amount);
8806
+ const a = clamp012(this.amount);
8708
8807
  linear(m, 1 - 2 * a, a);
8709
8808
  }
8710
8809
  css() {
8711
- return `invert(${clamp01(this.amount)})`;
8810
+ return `invert(${clamp012(this.amount)})`;
8712
8811
  }
8713
8812
  };
8714
8813
  var Saturate = class extends Amount {
@@ -8731,7 +8830,7 @@ var Saturate = class extends Amount {
8731
8830
  };
8732
8831
  var Grayscale = class extends Amount {
8733
8832
  toMatrix(m) {
8734
- const k = 1 - clamp01(this.amount);
8833
+ const k = 1 - clamp012(this.amount);
8735
8834
  m9[0] = 0.2126 + 0.7874 * k;
8736
8835
  m9[1] = 0.7152 - 0.7152 * k;
8737
8836
  m9[2] = 0.0722 - 0.0722 * k;
@@ -8744,12 +8843,12 @@ var Grayscale = class extends Amount {
8744
8843
  rgb3(m, m9);
8745
8844
  }
8746
8845
  css() {
8747
- return `grayscale(${clamp01(this.amount)})`;
8846
+ return `grayscale(${clamp012(this.amount)})`;
8748
8847
  }
8749
8848
  };
8750
8849
  var Sepia = class extends Amount {
8751
8850
  toMatrix(m) {
8752
- const k = 1 - clamp01(this.amount);
8851
+ const k = 1 - clamp012(this.amount);
8753
8852
  m9[0] = 0.393 + 0.607 * k;
8754
8853
  m9[1] = 0.769 - 0.769 * k;
8755
8854
  m9[2] = 0.189 - 0.189 * k;
@@ -8762,7 +8861,7 @@ var Sepia = class extends Amount {
8762
8861
  rgb3(m, m9);
8763
8862
  }
8764
8863
  css() {
8765
- return `sepia(${clamp01(this.amount)})`;
8864
+ return `sepia(${clamp012(this.amount)})`;
8766
8865
  }
8767
8866
  };
8768
8867
  var HueRotate = class extends Amount {
@@ -8811,7 +8910,7 @@ var Tint = class extends Amount {
8811
8910
  return i + 2;
8812
8911
  }
8813
8912
  toMatrix(m) {
8814
- const a = clamp01(this.amount);
8913
+ const a = clamp012(this.amount);
8815
8914
  linear(m, 1 - a, 0);
8816
8915
  m[4] = (this.c >> 16 & 255) / 255 * a;
8817
8916
  m[9] = (this.c >> 8 & 255) / 255 * a;
@@ -8822,7 +8921,7 @@ var Tint = class extends Amount {
8822
8921
  return null;
8823
8922
  }
8824
8923
  paint2d(ctx, x, y, w2, h) {
8825
- const a = clamp01(this.amount);
8924
+ const a = clamp012(this.amount);
8826
8925
  if (a <= 0) return;
8827
8926
  ctx.save();
8828
8927
  ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -8888,7 +8987,7 @@ var DropShadow = class extends Base {
8888
8987
  return this.a;
8889
8988
  }
8890
8989
  set opacity(v) {
8891
- this.a = clamp01(v);
8990
+ this.a = clamp012(v);
8892
8991
  }
8893
8992
  get color() {
8894
8993
  return this.rgb;