@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 (lite: load fabi-play.js first) | Fabi Play Kit License | https://docs.fabiplay.app */
1
+ /*! @fabi-play/engine 0.7.0 (lite: load fabi-play.js first) | 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); });
@@ -113,6 +113,77 @@ var DEFAULT_AVATARS_BY_AGE = {
113
113
  };
114
114
  var DEFAULT_AVATARS = DEFAULT_AVATARS_BY_AGE["6-8"];
115
115
  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 };
116
+ var AIM_VISIBLE = 0.5;
117
+ var HAND_SPACE = { left: 1.6, top: 1.6, w: 3.2, h: 3 };
118
+ var TORSO_PER_SHOULDER_WIDTH = 1.2;
119
+ function aimPoint(who, side, opts = {}) {
120
+ const body = Array.isArray(who) ? who : who.body;
121
+ if (!Array.isArray(body)) return null;
122
+ const w2 = side === "left" ? P.leftWrist : P.rightWrist;
123
+ const at = (i) => isNumber(body[i * 3]) && isNumber(body[i * 3 + 1]) && (body[i * 3 + 2] ?? 0) >= AIM_VISIBLE ? i : -1;
124
+ if (at(w2) < 0) return null;
125
+ const wx = body[w2 * 3];
126
+ const wy = body[w2 * 3 + 1];
127
+ if (opts.orientation !== "portrait") return toScreen(opts.view, { x: wx, y: wy });
128
+ if (at(P.leftShoulder) < 0 || at(P.rightShoulder) < 0) return null;
129
+ const ls = P.leftShoulder * 3;
130
+ const rs = P.rightShoulder * 3;
131
+ const sx = (body[ls] + body[rs]) / 2;
132
+ const sy = (body[ls + 1] + body[rs + 1]) / 2;
133
+ const lh = P.leftHip * 3;
134
+ const rh = P.rightHip * 3;
135
+ const hx = (body[lh] + body[rh]) / 2;
136
+ const hy = (body[lh + 1] + body[rh + 1]) / 2;
137
+ let torso = isNumber(hx) && isNumber(hy) ? Math.hypot(hx - sx, hy - sy) : 0;
138
+ 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]));
139
+ torso = Math.max(torso, 0.01);
140
+ return {
141
+ x: clamp01((wx - sx + HAND_SPACE.left * torso) / (HAND_SPACE.w * torso)),
142
+ y: clamp01((wy - sy + HAND_SPACE.top * torso) / (HAND_SPACE.h * torso))
143
+ };
144
+ }
145
+ var AIM_HOLD_MS = 300;
146
+ var AimHold = class {
147
+ constructor(holdMs = AIM_HOLD_MS) {
148
+ __publicField(this, "holdMs", holdMs);
149
+ __publicField(this, "last", /* @__PURE__ */ new Map());
150
+ }
151
+ /** A number for a player's hand. */
152
+ static key(player, side) {
153
+ return player * 2 + (side === "right" ? 1 : 0);
154
+ }
155
+ /** The aim to use at time t (ms) for the hand key, given what aimPoint said now (p, or null if lost). */
156
+ at(key, p, t) {
157
+ const l = this.last.get(key);
158
+ if (p) {
159
+ if (l) {
160
+ l.x = p.x;
161
+ l.y = p.y;
162
+ l.t = t;
163
+ } else this.last.set(key, { x: p.x, y: p.y, t });
164
+ return p;
165
+ }
166
+ if (!l) return null;
167
+ if (t - l.t <= this.holdMs) return { x: l.x, y: l.y };
168
+ this.last.delete(key);
169
+ return null;
170
+ }
171
+ /** Forgets every hand. */
172
+ clear() {
173
+ this.last.clear();
174
+ }
175
+ };
176
+ var MAX_SAFE_INSET = 0.25;
177
+ var NO_SAFE_AREA = Object.freeze({ top: 0, bottom: 0, left: 0, right: 0 });
178
+ function parseSafeArea(v) {
179
+ if (!isObject(v)) return void 0;
180
+ const f = (n) => isNumber(n) ? Math.min(Math.max(n, 0), MAX_SAFE_INSET) : 0;
181
+ return { top: f(v.top), bottom: f(v.bottom), left: f(v.left), right: f(v.right) };
182
+ }
183
+ var COVER_ASPECT = 16 / 9;
184
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
185
+ var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
186
+ var clamp01 = (n) => Math.min(Math.max(n, 0), 1);
116
187
  var POSE_HEADER = 12;
117
188
  var POSE_BODY_BYTES = LANDMARKS * 4;
118
189
  var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
@@ -4427,11 +4498,20 @@ function wrists(body) {
4427
4498
  return out;
4428
4499
  }
4429
4500
  var playerColor = (player) => PLAYER_COLORS[((player - 1) % PLAYER_COLORS.length + PLAYER_COLORS.length) % PLAYER_COLORS.length];
4430
- function handsOf(frames, toScreen2, width, height, bodyOf = () => null) {
4501
+ var SIDES = ["left", "right"];
4502
+ function handsOf(frames, toScreen2, width, height, bodyOf = () => null, aim = {}) {
4431
4503
  const out = [];
4432
4504
  for (const f of frames) {
4433
4505
  const body = bodyOf(f) ?? f.body;
4434
4506
  if (!Array.isArray(body)) continue;
4507
+ if (aim.orientation === "portrait") {
4508
+ for (const side of SIDES) {
4509
+ let p = aimPoint(body, side, { orientation: "portrait" });
4510
+ if (aim.hold) p = aim.hold.at(AimHold.key(f.id, side), p, aim.t ?? 0);
4511
+ if (p) out.push({ player: f.id, side, x: p.x * width, y: p.y * height, color: playerColor(f.id) });
4512
+ }
4513
+ continue;
4514
+ }
4435
4515
  for (const w2 of wrists(body)) {
4436
4516
  const s = toScreen2(w2);
4437
4517
  out.push({ player: f.id, side: w2.side, x: s.x * width, y: s.y * height, color: playerColor(f.id) });
@@ -5093,6 +5173,10 @@ var HandMarkers = class extends Node {
5093
5173
  }
5094
5174
  };
5095
5175
  var now = () => performance.now();
5176
+ var frozenSafeArea = (v) => {
5177
+ const sa = parseSafeArea(v);
5178
+ return sa && Object.freeze(sa);
5179
+ };
5096
5180
  async function createEngine(opts = {}) {
5097
5181
  const play = opts.play ?? null;
5098
5182
  let canvas = opts.canvas ?? null;
@@ -5227,6 +5311,8 @@ async function createEngine(opts = {}) {
5227
5311
  ro?.observe(canvas);
5228
5312
  let backdrop = play?.backdrop === "video" ? "video" : "none";
5229
5313
  let view = play?.view ?? null;
5314
+ let orientation = play?.orientation === "portrait" ? "portrait" : "landscape";
5315
+ let safeArea = frozenSafeArea(play?.safeArea) ?? NO_SAFE_AREA;
5230
5316
  let livePlayers = [];
5231
5317
  let lastPlayersAt = -Infinity;
5232
5318
  let hostPaused = false;
@@ -5254,7 +5340,7 @@ async function createEngine(opts = {}) {
5254
5340
  lastPlayersAt = now();
5255
5341
  }
5256
5342
  };
5257
- const hold = (on) => {
5343
+ const hold2 = (on) => {
5258
5344
  hostPaused = on;
5259
5345
  syncSound();
5260
5346
  };
@@ -5268,6 +5354,11 @@ async function createEngine(opts = {}) {
5268
5354
  };
5269
5355
  unlisten.push(
5270
5356
  play.on("start", (m) => lateHint(m?.device)),
5357
+ play.on("start", (m) => {
5358
+ if (m?.orientation === "portrait" || m?.orientation === "landscape") orientation = m.orientation;
5359
+ const sa = frozenSafeArea(m?.safeArea);
5360
+ if (sa) safeArea = sa;
5361
+ }),
5271
5362
  // The simple API knows muted only from the start (its getter is false before): read it again then.
5272
5363
  play.on("start", (m) => {
5273
5364
  if (typeof m?.muted === "boolean") sound.muted = m.muted;
@@ -5276,11 +5367,11 @@ async function createEngine(opts = {}) {
5276
5367
  play.on("players", (m) => setPlayers(m?.players)),
5277
5368
  play.on("backdrop", (m) => setBackdrop(m?.backdrop, m?.view)),
5278
5369
  play.on("start", (m) => setBackdrop(m?.backdrop, m?.view ?? play.view)),
5279
- play.on("pause", () => hold(true)),
5280
- play.on("resume", () => hold(false)),
5281
- play.on("break-start", () => hold(true)),
5282
- play.on("break-done", () => hold(false)),
5283
- play.on("break-end", () => hold(false))
5370
+ play.on("pause", () => hold2(true)),
5371
+ play.on("resume", () => hold2(false)),
5372
+ play.on("break-start", () => hold2(true)),
5373
+ play.on("break-done", () => hold2(false)),
5374
+ play.on("break-end", () => hold2(false))
5284
5375
  );
5285
5376
  sound.muted = play.muted === true;
5286
5377
  }
@@ -5291,11 +5382,13 @@ async function createEngine(opts = {}) {
5291
5382
  let tick = 0;
5292
5383
  const live = () => now() - lastPlayersAt < 1e3 ? livePlayers : [];
5293
5384
  const toScreen2 = (p) => play ? play.toScreen(p) : toScreen(view, p);
5385
+ const hold = new AimHold();
5294
5386
  const allHands = () => {
5295
5387
  if (handsAt === tick) return handsCache;
5296
5388
  handsAt = tick;
5297
- const bodyOf = backdrop === "video" || !play?.smoothedBody ? () => null : (f) => play.smoothedBody(f);
5298
- handsCache = handsOf(live(), toScreen2, stageW, STAGE_HEIGHT, bodyOf);
5389
+ const portrait = orientation === "portrait";
5390
+ const bodyOf = backdrop === "video" && !portrait || !play?.smoothedBody ? () => null : (f) => play.smoothedBody(f);
5391
+ handsCache = handsOf(live(), toScreen2, stageW, STAGE_HEIGHT, bodyOf, portrait ? { orientation, hold, t: now() } : void 0);
5299
5392
  return handsCache;
5300
5393
  };
5301
5394
  const box = { x: 0, y: 0, w: 1, h: 1 };
@@ -5458,6 +5551,12 @@ async function createEngine(opts = {}) {
5458
5551
  get view() {
5459
5552
  return view;
5460
5553
  },
5554
+ get orientation() {
5555
+ return orientation;
5556
+ },
5557
+ get safeArea() {
5558
+ return safeArea;
5559
+ },
5461
5560
  get paused() {
5462
5561
  return hostPaused || userPaused;
5463
5562
  },
@@ -6317,7 +6416,7 @@ ${msg}`);
6317
6416
  };
6318
6417
 
6319
6418
  // src/filters.ts
6320
- var clamp01 = (v) => Math.min(Math.max(v, 0), 1);
6419
+ var clamp012 = (v) => Math.min(Math.max(v, 0), 1);
6321
6420
  var pos = (v) => Math.max(v, 0);
6322
6421
  var Base = class {
6323
6422
  constructor() {
@@ -6369,11 +6468,11 @@ var Contrast = class extends Amount {
6369
6468
  };
6370
6469
  var Invert = class extends Amount {
6371
6470
  toMatrix(m) {
6372
- const a = clamp01(this.amount);
6471
+ const a = clamp012(this.amount);
6373
6472
  linear(m, 1 - 2 * a, a);
6374
6473
  }
6375
6474
  css() {
6376
- return `invert(${clamp01(this.amount)})`;
6475
+ return `invert(${clamp012(this.amount)})`;
6377
6476
  }
6378
6477
  };
6379
6478
  var Saturate = class extends Amount {
@@ -6396,7 +6495,7 @@ var Saturate = class extends Amount {
6396
6495
  };
6397
6496
  var Grayscale = class extends Amount {
6398
6497
  toMatrix(m) {
6399
- const k = 1 - clamp01(this.amount);
6498
+ const k = 1 - clamp012(this.amount);
6400
6499
  m9[0] = 0.2126 + 0.7874 * k;
6401
6500
  m9[1] = 0.7152 - 0.7152 * k;
6402
6501
  m9[2] = 0.0722 - 0.0722 * k;
@@ -6409,12 +6508,12 @@ var Grayscale = class extends Amount {
6409
6508
  rgb3(m, m9);
6410
6509
  }
6411
6510
  css() {
6412
- return `grayscale(${clamp01(this.amount)})`;
6511
+ return `grayscale(${clamp012(this.amount)})`;
6413
6512
  }
6414
6513
  };
6415
6514
  var Sepia = class extends Amount {
6416
6515
  toMatrix(m) {
6417
- const k = 1 - clamp01(this.amount);
6516
+ const k = 1 - clamp012(this.amount);
6418
6517
  m9[0] = 0.393 + 0.607 * k;
6419
6518
  m9[1] = 0.769 - 0.769 * k;
6420
6519
  m9[2] = 0.189 - 0.189 * k;
@@ -6427,7 +6526,7 @@ var Sepia = class extends Amount {
6427
6526
  rgb3(m, m9);
6428
6527
  }
6429
6528
  css() {
6430
- return `sepia(${clamp01(this.amount)})`;
6529
+ return `sepia(${clamp012(this.amount)})`;
6431
6530
  }
6432
6531
  };
6433
6532
  var HueRotate = class extends Amount {
@@ -6476,7 +6575,7 @@ var Tint = class extends Amount {
6476
6575
  return i + 2;
6477
6576
  }
6478
6577
  toMatrix(m) {
6479
- const a = clamp01(this.amount);
6578
+ const a = clamp012(this.amount);
6480
6579
  linear(m, 1 - a, 0);
6481
6580
  m[4] = (this.c >> 16 & 255) / 255 * a;
6482
6581
  m[9] = (this.c >> 8 & 255) / 255 * a;
@@ -6487,7 +6586,7 @@ var Tint = class extends Amount {
6487
6586
  return null;
6488
6587
  }
6489
6588
  paint2d(ctx, x, y, w2, h) {
6490
- const a = clamp01(this.amount);
6589
+ const a = clamp012(this.amount);
6491
6590
  if (a <= 0) return;
6492
6591
  ctx.save();
6493
6592
  ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -6553,7 +6652,7 @@ var DropShadow = class extends Base {
6553
6652
  return this.a;
6554
6653
  }
6555
6654
  set opacity(v) {
6556
- this.a = clamp01(v);
6655
+ this.a = clamp012(v);
6557
6656
  }
6558
6657
  get color() {
6559
6658
  return this.rgb;