rig-play 1.5.0 → 1.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 CHANGED
@@ -263,6 +263,30 @@ box, an inverse clip removes nothing, and bounding boxes, paths and points do
263
263
  not draw and do not count. Both cost nothing until called and read the same
264
264
  numbers in `dom` and `canvas` mode.
265
265
 
266
+ ### Drawing some slots only
267
+
268
+ `slots` is a draw filter — an allow list of slot names or a predicate — settable up
269
+ front and on a live player. The pose is the whole rig's whatever the filter:
270
+ bones, constraints, physics, `bone()`, `adjust`, events and tracks see no filter;
271
+ only the draw does, in both modes, and `bounds()` reads the drawn slots. A
272
+ clipping slot the filter leaves out does not clip. An allow list naming a slot
273
+ the document does not have is refused by name.
274
+
275
+ ```ts
276
+ const hand = new RigPlayer(frontRoot, assets, { slots: ['hand_f', 'arm_f_lo'] });
277
+ player.slots = (slot) => !slot.startsWith('weapon');
278
+ ```
279
+
280
+ Two players over one `ModelAssets` share the load but each poses the rig. To
281
+ pose it once and draw it twice — a hand drawn again in front of another
282
+ character — give a second renderer this player's pose under another filter:
283
+
284
+ ```ts
285
+ const front = new DomRenderer(frontRoot, assets.regionImages, assets.pageImages);
286
+ // each frame, after player.frame(dt):
287
+ front.render(player.posedFrame(['hand_f', 'arm_f_lo']));
288
+ ```
289
+
266
290
  ### Several tracks
267
291
 
268
292
  A base on track 0, a layer over it, a one-shot played once and mixed back out —
@@ -282,9 +306,15 @@ player.entry(2); // { animation, loop
282
306
  `play(name)` keeps meaning what it meant: a plain play on track 0 runs on rig-c's
283
307
  single live track, which poses everything the core poses. The first call that
284
308
  composes — a `mix`, a `track` above 0, a `queue`, a `clear` — moves the player onto
285
- the tracks, carrying the single track over by replay (its animation set, pose 0
286
- taken, stepped to its time); from then on `seek` and `skin` are refused by name,
287
- since the player cannot replay a composition to a time — step it forward instead.
309
+ the tracks, carrying the single track over by **replay** (its animation set, pose 0
310
+ taken, stepped to its time in `seekStep` increments — one pose per step played, so
311
+ on a stage that idled a while that is one long frame; the core offers no hand-over
312
+ of a live track's state). To never pay it, stand the player on the tracks from the
313
+ start: `new RigPlayer(root, assets, { tracks: true })`. Pick the single track for a
314
+ rig whose animations key what the tracks do not mix (below); pick the tracks for
315
+ layering. Either way, after the first `mix`, `track` above 0, `queue` or `clear`,
316
+ `seek` and `skin` are refused by name, since the player cannot replay a composition
317
+ to a time — step it forward instead.
288
318
  Every entry mixes from the setup pose each step, so `adjust` writes still last one
289
319
  pose. **What the tracks do not mix**, in rig-c's words at `play`/`queue`: an
290
320
  animation keying a constraint (ik, transform, path, physics, slider), a sequence,
@@ -628,6 +658,7 @@ name, and unloading is still elements first, bitmaps second.
628
658
  | `DomTexture` | none | Not exported; there is no atlas page to attach a texture to. |
629
659
  | `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
630
660
  | `state.setAnimation(0, 'walk', true)` | `player.play('walk', { loop: true })` | `loop` defaults to true. `play` opens the animation at its first pose with the physics reset there. `player.current` names what is playing. |
661
+ | `skeleton.findSlot('x').attachment = null` to hide a slot, or a second skeleton with slots emptied | `RigPlayerOptions.slots`, `player.slots`, `player.posedFrame(slots)` | A draw filter since 1.6.0 — the pose is the whole rig's; a clipping slot left out does not clip. See *Drawing some slots only*. |
631
662
  | `state.setAnimation(1, 'smile', false)` with a mix, `addAnimation`, `setEmptyAnimation`, `addEmptyAnimation`, `getCurrent(track)` | `player.play(name, { track, loop, mix })`, `player.queue(name, { track, loop, mix, delay })`, `player.clear(track, { mix })`, `player.queueClear(track, { mix, delay })`, `player.entry(track)` | Since 1.5.0, rig-c's live tracks: the runtime's mixing at tolerance 0. Constraint, sequence, draw-order and path-deform timelines are not mixed and are refused on the tracks. See *Several tracks*. |
632
663
  | `state.update(delta)`, `state.apply(skeleton)`, `skeleton.update(delta)`, `skeleton.updateWorldTransform(Physics.update)`, `renderer.render(skeleton)` | `player.frame(dt)` | Or `player.advance(dt)` then `player.draw()`, to time the two apart. |
633
664
  | time scale (the caller's own, on the spine-core side) | `player.timeScale` | Multiplies every step. 0 holds the pose without re-posing it; a negative value is not stepped. |
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend } from './DomRenderer.js';
3
3
  export { type PageLayout, type PageRegion, type RegionEncoding, type RegionImage, revokeRegions, type UnpackRegionsOptions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument, type RegionLoading } from './loadModel.js';
5
- export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEvent, RigPlayer, type RigPlayerOptions, type TrackEntry } from './player.js';
5
+ export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type TrackEntry } from './player.js';
6
6
  export type { BlendMode, PosedClip, PosedFrame, PosedMesh, PosedRegion, PosedSlot, } from './posedFrame.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5H,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChQ,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5H,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACjR,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -8216,7 +8216,7 @@ function trianglesOf(source, drawn) {
8216
8216
  }
8217
8217
  return kept;
8218
8218
  }
8219
- function clipCover(pose, active) {
8219
+ function clipCover(pose, active, draws = () => true) {
8220
8220
  const polygons = new Map(pose.clips.map((c) => [c[0], { end: c[2], polygon: c[3] }]));
8221
8221
  const shownOf = new Map(pose.shown.map((s) => [s.slot, s]));
8222
8222
  const cover = /* @__PURE__ */ new Map();
@@ -8227,7 +8227,7 @@ function clipCover(pose, active) {
8227
8227
  const clip = g?.kind === "clipping" ? polygons.get(slot) : void 0;
8228
8228
  if (shown !== void 0 && g?.kind === "clipping" && clip !== void 0) {
8229
8229
  if (current !== null && current.end === slot) current = null;
8230
- if (active.has(shown.bone) && current === null) {
8230
+ if (active.has(shown.bone) && current === null && draws(slot)) {
8231
8231
  current = { end: clip.end, clip: { polygon: clip.polygon, inverse: g.inverse, convex: g.convex } };
8232
8232
  }
8233
8233
  continue;
@@ -8237,16 +8237,17 @@ function clipCover(pose, active) {
8237
8237
  }
8238
8238
  return cover;
8239
8239
  }
8240
- function frameOf(source, pose, color = [1, 1, 1, 1]) {
8240
+ function frameOf(source, pose, color = [1, 1, 1, 1], draws = () => true) {
8241
8241
  const { drawn: regions, why } = drawnRegions(source.doc, pose.shown, pose.drawOrder, source.uv);
8242
8242
  if (regions === null) throw new CoreInputError(`the page UVs are not posed: ${why}`);
8243
8243
  const regionOf = new Map(regions.map((r) => [r.slot, r]));
8244
8244
  const rows = new Map(pose.slots.map((row) => [row[0], row]));
8245
8245
  const z = new Map(pose.drawOrder.map((slot, index) => [slot, index]));
8246
- const cover = clipCover(pose, source.active);
8246
+ const cover = clipCover(pose, source.active, draws);
8247
8247
  const [sr, sg, sb, sa] = color;
8248
8248
  const order = [];
8249
8249
  for (const d of pose.drawn) {
8250
+ if (!draws(d.slot)) continue;
8250
8251
  const bone2 = source.boneOf.get(d.slot);
8251
8252
  if (bone2 === void 0 || !source.active.has(bone2)) continue;
8252
8253
  const found = regionOf.get(d.slot);
@@ -8294,6 +8295,10 @@ function frameOf(source, pose, color = [1, 1, 1, 1]) {
8294
8295
  }
8295
8296
  return { order };
8296
8297
  }
8298
+ function predicateOf(names) {
8299
+ const allowed = new Set(names);
8300
+ return (slot) => allowed.has(slot);
8301
+ }
8297
8302
  class RigPlayer {
8298
8303
  /**
8299
8304
  * The renderer this player draws through — its knobs and counters are the
@@ -8337,10 +8342,17 @@ class RigPlayer {
8337
8342
  loop = true;
8338
8343
  /** One past the highest track touched — how many `current(track)` reads a step takes. */
8339
8344
  trackCount = 0;
8345
+ /** `RigPlayerOptions.tracks`: every play opens the live tracks, never the single track. */
8346
+ trackMode;
8347
+ /** Set once anything beyond a plain play on track 0 happened: a mix, another track, a queue, a clear. `seek` and `skin` then cannot replay and refuse. */
8348
+ beyondBase = false;
8340
8349
  paused = false;
8341
8350
  pose = null;
8342
8351
  setup = null;
8343
8352
  adjustFn = null;
8353
+ slotFilter = null;
8354
+ /** `slotFilter` as a predicate; every slot when null. */
8355
+ draws = () => true;
8344
8356
  /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
8345
8357
  adjustHook = (locals) => {
8346
8358
  if (this.adjustFn !== null) this.adjustFn(locals);
@@ -8356,11 +8368,13 @@ class RigPlayer {
8356
8368
  this.view = options.skin === void 0 ? underNoSkin(read.doc) : underSkin(read.doc, options.skin);
8357
8369
  this.source = frameSource(this.view, read.parsed);
8358
8370
  this.seekStep = options.seekStep ?? 1 / 60;
8371
+ this.trackMode = options.tracks === true;
8359
8372
  if (!(this.seekStep > 0)) throw new Error(`seekStep must be a positive number of seconds, got ${String(options.seekStep)}`);
8360
8373
  this.root = root;
8361
8374
  this.assets = assets;
8362
8375
  this.meshBackend = options.meshBackend;
8363
8376
  this.adjustFn = options.adjust ?? null;
8377
+ if (options.slots !== void 0 && options.slots !== null) this.slots = options.slots;
8364
8378
  this.active = this.create(options.mode ?? "dom");
8365
8379
  }
8366
8380
  /**
@@ -8425,7 +8439,8 @@ class RigPlayer {
8425
8439
  const loop = options.loop ?? true;
8426
8440
  const mix = options.mix ?? 0;
8427
8441
  this.paused = false;
8428
- if (this.tracks === null && track === 0 && mix === 0) {
8442
+ if (track !== 0 || mix !== 0) this.beyondBase = true;
8443
+ if (this.tracks === null && track === 0 && mix === 0 && !this.trackMode) {
8429
8444
  this.animation = name;
8430
8445
  this.loop = loop;
8431
8446
  this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook });
@@ -8449,6 +8464,7 @@ class RigPlayer {
8449
8464
  */
8450
8465
  queue(name, options = {}) {
8451
8466
  const track = options.track ?? 0;
8467
+ this.beyondBase = true;
8452
8468
  const tracks = this.compose();
8453
8469
  tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0 });
8454
8470
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8460,6 +8476,7 @@ class RigPlayer {
8460
8476
  * setup pose. The pose the player holds is re-posed at once.
8461
8477
  */
8462
8478
  clear(track = 0, options = {}) {
8479
+ this.beyondBase = true;
8463
8480
  const tracks = this.compose();
8464
8481
  tracks.setEmptyAnimation(track, options.mix ?? 0);
8465
8482
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8472,6 +8489,7 @@ class RigPlayer {
8472
8489
  * its last `mix` seconds.
8473
8490
  */
8474
8491
  queueClear(track = 0, options = {}) {
8492
+ this.beyondBase = true;
8475
8493
  const tracks = this.compose();
8476
8494
  tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8477
8495
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8492,11 +8510,14 @@ class RigPlayer {
8492
8510
  return this.tracks !== null;
8493
8511
  }
8494
8512
  /**
8495
- * The live tracks, opened by the first composing call. A single track in
8496
- * play is carried over by replay: track 0 set to its animation, pose 0 taken
8497
- * by `start()` (the player's — nothing steps before it) and the tracks
8498
- * stepped to the single track's time in `seekStep` increments, so a physics
8499
- * rig's history restarts there and nothing is fired on the way.
8513
+ * The live tracks, opened by the first composing call (or by the first
8514
+ * `play` under `tracks: true`). A single track in play is carried over by
8515
+ * replay: track 0 set to its animation, pose 0 taken by `start()` (the
8516
+ * player's — nothing steps before it) and the tracks stepped to the single
8517
+ * track's time in `seekStep` increments, so a physics rig's history
8518
+ * restarts there and nothing is fired on the way. The core offers no
8519
+ * hand-over of a live track's state, so that replay costs one pose per
8520
+ * `seekStep` of time played — which `tracks: true` avoids entirely.
8500
8521
  */
8501
8522
  compose() {
8502
8523
  if (this.tracks !== null) return this.tracks;
@@ -8533,12 +8554,20 @@ class RigPlayer {
8533
8554
  seek(time) {
8534
8555
  if (this.animation === null) throw new Error("seek before play: no animation is open");
8535
8556
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
8536
- if (this.tracks !== null) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
8557
+ if (this.beyondBase) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
8537
8558
  this.reopen();
8538
8559
  this.stepTo(time);
8539
8560
  }
8540
- /** The single track opened again with track 0's animation as `play` last set it, at pose 0. */
8561
+ /** Whatever is open, opened again with track 0's animation as `play` last set it, at pose 0: the single track, or the tracks under `tracks: true`. */
8541
8562
  reopen() {
8563
+ if (this.tracks !== null) {
8564
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook });
8565
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
8566
+ this.trackCount = 1;
8567
+ this.tracks = tracks;
8568
+ this.pose = tracks.start();
8569
+ return;
8570
+ }
8542
8571
  this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
8543
8572
  this.pose = this.track.first;
8544
8573
  }
@@ -8607,18 +8636,49 @@ class RigPlayer {
8607
8636
  }
8608
8637
  set skin(name) {
8609
8638
  if (name === this.skinName) return;
8610
- if (this.tracks !== null) throw new Error("skin cannot change after a mix, another track, a queue or a clear: the player cannot replay those to the current time — set it before composing, or play again");
8639
+ if (this.beyondBase) throw new Error("skin cannot change after a mix, another track, a queue or a clear: the player cannot replay those to the current time — set it before composing, or play again");
8611
8640
  const view = name === void 0 ? underNoSkin(this.read.doc) : underSkin(this.read.doc, name);
8612
8641
  this.view = view;
8613
8642
  this.source = frameSource(view, this.read.parsed);
8614
8643
  this.skinName = name;
8615
8644
  this.setup = null;
8616
- if (this.animation !== null && this.track !== null) {
8617
- const time = this.track.trackTime;
8645
+ if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
8646
+ const time = this.time;
8618
8647
  this.reopen();
8619
8648
  this.stepTo(time);
8620
8649
  }
8621
8650
  }
8651
+ /**
8652
+ * Which slots this player draws (`SlotFilter`), or null for every slot.
8653
+ * Settable on a live player: the next `draw` shows it, and `bounds()` reads
8654
+ * the drawn slots. An allow list naming a slot the document does not have
8655
+ * is refused by name. The pose is the whole rig's: `bone()`, `adjust`, the
8656
+ * events and the tracks see no filter.
8657
+ */
8658
+ get slots() {
8659
+ return this.slotFilter;
8660
+ }
8661
+ set slots(filter) {
8662
+ if (filter === null) {
8663
+ this.slotFilter = null;
8664
+ this.draws = () => true;
8665
+ return;
8666
+ }
8667
+ if (typeof filter === "function") {
8668
+ this.slotFilter = filter;
8669
+ this.draws = filter;
8670
+ return;
8671
+ }
8672
+ if (!Array.isArray(filter)) throw new Error(`slots must be an array of slot names, a function, or null, got ${typeof filter}`);
8673
+ const known = new Set(this.read.doc.slots.map((slot) => slot.name));
8674
+ const unknown = filter.filter((name) => !known.has(name));
8675
+ if (unknown.length > 0) {
8676
+ throw new Error(`no slot named ${unknown.map((n) => JSON.stringify(n)).join(", ")} in this document (slots: ${[...known].join(", ")})`);
8677
+ }
8678
+ const allowed = new Set(filter);
8679
+ this.slotFilter = [...filter];
8680
+ this.draws = (slot) => allowed.has(slot);
8681
+ }
8622
8682
  /**
8623
8683
  * The per-pose adjustment of bone locals (`BoneAdjust`), or null for none.
8624
8684
  * It is applied by every track the player opens — `play`, the reopen inside
@@ -8684,7 +8744,7 @@ class RigPlayer {
8684
8744
  * mode; nothing is computed until called.
8685
8745
  */
8686
8746
  bounds() {
8687
- const frame2 = this.pose === null ? frameOf(this.source, this.posed(), this.color) : this.posedFrame();
8747
+ const frame2 = this.pose === null ? frameOf(this.source, this.posed(), this.color, this.draws) : this.posedFrame();
8688
8748
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
8689
8749
  for (const slot of frame2.order) {
8690
8750
  const points = slot.kind === "region" ? slot.corners : slot.vertices;
@@ -8720,10 +8780,17 @@ class RigPlayer {
8720
8780
  if (this.setup === null) this.setup = poseRawSetup(this.view);
8721
8781
  return this.setup;
8722
8782
  }
8723
- /** The pose the player holds, as the frame record the renderer draws. */
8724
- posedFrame() {
8783
+ /**
8784
+ * The pose the player holds, as the frame record the renderer draws — under
8785
+ * this player's `slots` filter, or under `slots` given here instead (a
8786
+ * second renderer over the same assets drawing this player's pose on
8787
+ * another root: `other.render(player.posedFrame(['hand_f']))`, the one rig
8788
+ * posed once).
8789
+ */
8790
+ posedFrame(slots) {
8725
8791
  if (this.pose === null) return { order: [] };
8726
- return frameOf(this.source, this.pose, this.color);
8792
+ const draws = slots === void 0 ? this.draws : slots === null ? () => true : typeof slots === "function" ? slots : predicateOf(slots);
8793
+ return frameOf(this.source, this.pose, this.color, draws);
8727
8794
  }
8728
8795
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
8729
8796
  dispose() {