@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 (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,7 +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
+ }
116
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);
117
187
  var POSE_HEADER = 12;
118
188
  var POSE_BODY_BYTES = LANDMARKS * 4;
119
189
  var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
@@ -4428,11 +4498,20 @@ function wrists(body) {
4428
4498
  return out;
4429
4499
  }
4430
4500
  var playerColor = (player) => PLAYER_COLORS[((player - 1) % PLAYER_COLORS.length + PLAYER_COLORS.length) % PLAYER_COLORS.length];
4431
- function handsOf(frames, toScreen2, width, height, bodyOf = () => null) {
4501
+ var SIDES = ["left", "right"];
4502
+ function handsOf(frames, toScreen2, width, height, bodyOf = () => null, aim = {}) {
4432
4503
  const out = [];
4433
4504
  for (const f of frames) {
4434
4505
  const body = bodyOf(f) ?? f.body;
4435
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
+ }
4436
4515
  for (const w2 of wrists(body)) {
4437
4516
  const s = toScreen2(w2);
4438
4517
  out.push({ player: f.id, side: w2.side, x: s.x * width, y: s.y * height, color: playerColor(f.id) });
@@ -5094,6 +5173,10 @@ var HandMarkers = class extends Node {
5094
5173
  }
5095
5174
  };
5096
5175
  var now = () => performance.now();
5176
+ var frozenSafeArea = (v) => {
5177
+ const sa = parseSafeArea(v);
5178
+ return sa && Object.freeze(sa);
5179
+ };
5097
5180
  async function createEngine(opts = {}) {
5098
5181
  const play = opts.play ?? null;
5099
5182
  let canvas = opts.canvas ?? null;
@@ -5228,6 +5311,8 @@ async function createEngine(opts = {}) {
5228
5311
  ro?.observe(canvas);
5229
5312
  let backdrop = play?.backdrop === "video" ? "video" : "none";
5230
5313
  let view = play?.view ?? null;
5314
+ let orientation = play?.orientation === "portrait" ? "portrait" : "landscape";
5315
+ let safeArea = frozenSafeArea(play?.safeArea) ?? NO_SAFE_AREA;
5231
5316
  let livePlayers = [];
5232
5317
  let lastPlayersAt = -Infinity;
5233
5318
  let hostPaused = false;
@@ -5255,7 +5340,7 @@ async function createEngine(opts = {}) {
5255
5340
  lastPlayersAt = now();
5256
5341
  }
5257
5342
  };
5258
- const hold = (on) => {
5343
+ const hold2 = (on) => {
5259
5344
  hostPaused = on;
5260
5345
  syncSound();
5261
5346
  };
@@ -5269,6 +5354,11 @@ async function createEngine(opts = {}) {
5269
5354
  };
5270
5355
  unlisten.push(
5271
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
+ }),
5272
5362
  // The simple API knows muted only from the start (its getter is false before): read it again then.
5273
5363
  play.on("start", (m) => {
5274
5364
  if (typeof m?.muted === "boolean") sound.muted = m.muted;
@@ -5277,11 +5367,11 @@ async function createEngine(opts = {}) {
5277
5367
  play.on("players", (m) => setPlayers(m?.players)),
5278
5368
  play.on("backdrop", (m) => setBackdrop(m?.backdrop, m?.view)),
5279
5369
  play.on("start", (m) => setBackdrop(m?.backdrop, m?.view ?? play.view)),
5280
- play.on("pause", () => hold(true)),
5281
- play.on("resume", () => hold(false)),
5282
- play.on("break-start", () => hold(true)),
5283
- play.on("break-done", () => hold(false)),
5284
- 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))
5285
5375
  );
5286
5376
  sound.muted = play.muted === true;
5287
5377
  }
@@ -5292,11 +5382,13 @@ async function createEngine(opts = {}) {
5292
5382
  let tick = 0;
5293
5383
  const live = () => now() - lastPlayersAt < 1e3 ? livePlayers : [];
5294
5384
  const toScreen2 = (p) => play ? play.toScreen(p) : toScreen(view, p);
5385
+ const hold = new AimHold();
5295
5386
  const allHands = () => {
5296
5387
  if (handsAt === tick) return handsCache;
5297
5388
  handsAt = tick;
5298
- const bodyOf = backdrop === "video" || !play?.smoothedBody ? () => null : (f) => play.smoothedBody(f);
5299
- 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);
5300
5392
  return handsCache;
5301
5393
  };
5302
5394
  const box = { x: 0, y: 0, w: 1, h: 1 };
@@ -5459,6 +5551,12 @@ async function createEngine(opts = {}) {
5459
5551
  get view() {
5460
5552
  return view;
5461
5553
  },
5554
+ get orientation() {
5555
+ return orientation;
5556
+ },
5557
+ get safeArea() {
5558
+ return safeArea;
5559
+ },
5462
5560
  get paused() {
5463
5561
  return hostPaused || userPaused;
5464
5562
  },
@@ -6318,7 +6416,7 @@ ${msg}`);
6318
6416
  };
6319
6417
 
6320
6418
  // src/filters.ts
6321
- var clamp01 = (v) => Math.min(Math.max(v, 0), 1);
6419
+ var clamp012 = (v) => Math.min(Math.max(v, 0), 1);
6322
6420
  var pos = (v) => Math.max(v, 0);
6323
6421
  var Base = class {
6324
6422
  constructor() {
@@ -6370,11 +6468,11 @@ var Contrast = class extends Amount {
6370
6468
  };
6371
6469
  var Invert = class extends Amount {
6372
6470
  toMatrix(m) {
6373
- const a = clamp01(this.amount);
6471
+ const a = clamp012(this.amount);
6374
6472
  linear(m, 1 - 2 * a, a);
6375
6473
  }
6376
6474
  css() {
6377
- return `invert(${clamp01(this.amount)})`;
6475
+ return `invert(${clamp012(this.amount)})`;
6378
6476
  }
6379
6477
  };
6380
6478
  var Saturate = class extends Amount {
@@ -6397,7 +6495,7 @@ var Saturate = class extends Amount {
6397
6495
  };
6398
6496
  var Grayscale = class extends Amount {
6399
6497
  toMatrix(m) {
6400
- const k = 1 - clamp01(this.amount);
6498
+ const k = 1 - clamp012(this.amount);
6401
6499
  m9[0] = 0.2126 + 0.7874 * k;
6402
6500
  m9[1] = 0.7152 - 0.7152 * k;
6403
6501
  m9[2] = 0.0722 - 0.0722 * k;
@@ -6410,12 +6508,12 @@ var Grayscale = class extends Amount {
6410
6508
  rgb3(m, m9);
6411
6509
  }
6412
6510
  css() {
6413
- return `grayscale(${clamp01(this.amount)})`;
6511
+ return `grayscale(${clamp012(this.amount)})`;
6414
6512
  }
6415
6513
  };
6416
6514
  var Sepia = class extends Amount {
6417
6515
  toMatrix(m) {
6418
- const k = 1 - clamp01(this.amount);
6516
+ const k = 1 - clamp012(this.amount);
6419
6517
  m9[0] = 0.393 + 0.607 * k;
6420
6518
  m9[1] = 0.769 - 0.769 * k;
6421
6519
  m9[2] = 0.189 - 0.189 * k;
@@ -6428,7 +6526,7 @@ var Sepia = class extends Amount {
6428
6526
  rgb3(m, m9);
6429
6527
  }
6430
6528
  css() {
6431
- return `sepia(${clamp01(this.amount)})`;
6529
+ return `sepia(${clamp012(this.amount)})`;
6432
6530
  }
6433
6531
  };
6434
6532
  var HueRotate = class extends Amount {
@@ -6477,7 +6575,7 @@ var Tint = class extends Amount {
6477
6575
  return i + 2;
6478
6576
  }
6479
6577
  toMatrix(m) {
6480
- const a = clamp01(this.amount);
6578
+ const a = clamp012(this.amount);
6481
6579
  linear(m, 1 - a, 0);
6482
6580
  m[4] = (this.c >> 16 & 255) / 255 * a;
6483
6581
  m[9] = (this.c >> 8 & 255) / 255 * a;
@@ -6488,7 +6586,7 @@ var Tint = class extends Amount {
6488
6586
  return null;
6489
6587
  }
6490
6588
  paint2d(ctx, x, y, w2, h) {
6491
- const a = clamp01(this.amount);
6589
+ const a = clamp012(this.amount);
6492
6590
  if (a <= 0) return;
6493
6591
  ctx.save();
6494
6592
  ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -6554,7 +6652,7 @@ var DropShadow = class extends Base {
6554
6652
  return this.a;
6555
6653
  }
6556
6654
  set opacity(v) {
6557
- this.a = clamp01(v);
6655
+ this.a = clamp012(v);
6558
6656
  }
6559
6657
  get color() {
6560
6658
  return this.rgb;