rig-play 1.6.0 → 1.8.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
@@ -300,15 +300,37 @@ player.play('wave', { track: 2, loop: false, mix: 0.1 }); // a one-shot…
300
300
  player.queueClear(2, { mix: 0.1 }); // …mixed back out over its last 0.1 s
301
301
  player.queue('idle_b', { track: 0, mix: 0.3 }); // after the current idle cycle completes
302
302
  player.clear(1, { mix: 0.2 }); // the layer gone, the base showing through
303
- player.entry(2); // { animation, loop, trackTime, animationTime, duration } | null
303
+ player.entry(2); // { animation, loop, trackTime, animationTime, duration, alpha, additive } | null
304
+ player.play('blink', { track: 3, alpha: 0.6 }); // the same clip at a weight…
305
+ player.setAlpha(3, 0.85); // …changed while it plays
306
+ player.play('breath', { track: 4, additive: true }); // adds to what the tracks below posed
304
307
  ```
305
308
 
309
+ **Weight and additive layers** (rig-c 2.39.0, the runtime's `TrackEntry.alpha` and
310
+ `additive`): `alpha` (0..1, default 1) multiplies every timeline the entry applies;
311
+ `setAlpha(track, alpha)` changes the current entry's weight from the next pose and
312
+ re-poses the held pose. It does not touch events — an entry fires at any alpha, 0
313
+ included, and one mixing out fires none — or attachments: the current entry keeps
314
+ its attachments at any alpha, one mixing out keeps none. `additive: true`, fixed
315
+ when the entry is set or queued, adds the entry's keyed bone and deform values to
316
+ what the tracks below posed instead of replacing them, so a layer keying the same
317
+ bone channels as an idle does not cover its motion; a rotation is added as it
318
+ stands, with no shortest-angle wrap, and an additive entry mixing out fades with its
319
+ alpha and is never held. Colour, attachment and event timelines apply as on a
320
+ replacing entry. A weight below 1 or an additive entry composes, as a mix does.
321
+
306
322
  `play(name)` keeps meaning what it meant: a plain play on track 0 runs on rig-c's
307
323
  single live track, which poses everything the core poses. The first call that
308
324
  composes — a `mix`, a `track` above 0, a `queue`, a `clear` — moves the player onto
309
- the tracks, carrying the single track over by replay (its animation set, pose 0
310
- taken, stepped to its time); from then on `seek` and `skin` are refused by name,
311
- since the player cannot replay a composition to a time — step it forward instead.
325
+ the tracks, carrying the single track over by **replay** (its animation set, pose 0
326
+ taken, stepped to its time in `seekStep` increments — one pose per step played, so
327
+ on a stage that idled a while that is one long frame; the core offers no hand-over
328
+ of a live track's state). To never pay it, stand the player on the tracks from the
329
+ start: `new RigPlayer(root, assets, { tracks: true })`. Pick the single track for a
330
+ rig whose animations key what the tracks do not mix (below); pick the tracks for
331
+ layering. Either way, after the first `mix`, `track` above 0, `queue` or `clear`,
332
+ `seek` and `skin` are refused by name, since the player cannot replay a composition
333
+ to a time — step it forward instead.
312
334
  Every entry mixes from the setup pose each step, so `adjust` writes still last one
313
335
  pose. **What the tracks do not mix**, in rig-c's words at `play`/`queue`: an
314
336
  animation keying a constraint (ik, transform, path, physics, slider), a sequence,
@@ -653,7 +675,7 @@ name, and unloading is still elements first, bitmaps second.
653
675
  | `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
654
676
  | `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. |
655
677
  | `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*. |
656
- | `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*. |
678
+ | `state.setAnimation(1, 'smile', false)` with a mix, `addAnimation`, `setEmptyAnimation`, `addEmptyAnimation`, `getCurrent(track)`, `entry.alpha`, `entry.additive` | `player.play(name, { track, loop, mix, alpha, additive })`, `player.queue(name, { track, loop, mix, delay, alpha, additive })`, `player.clear(track, { mix })`, `player.queueClear(track, { mix, delay })`, `player.setAlpha(track, alpha)`, `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*. |
657
679
  | `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. |
658
680
  | 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. |
659
681
  | holding a pose (the caller stopped advancing) | `player.pause()`, `player.resume()` | `frame` keeps drawing the held pose. |
package/dist/index.js CHANGED
@@ -379,7 +379,7 @@ function timelineIdentity(slot, shown) {
379
379
  const nameOf = (a) => `${a.skin}/${a.slot}/${a.attachment}`;
380
380
  function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}, mixed) {
381
381
  const evalDeform = plant.deform ?? deformAt;
382
- const blendOf = plant.deformBlend ?? blendDeform;
382
+ const blendOf2 = plant.deformBlend ?? blendDeform;
383
383
  const evalFrame = plant.sequence ?? sequenceFrameAt;
384
384
  const shown = [];
385
385
  const why = [];
@@ -410,7 +410,7 @@ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {
410
410
  if (!seen.includes(now2.identity)) seen.push(now2.identity);
411
411
  if (a.deform !== null && now2.vertices !== null) {
412
412
  const target = evalDeform(now2.vertices, a.deform, t);
413
- if (target !== null) deform = blend === null ? target : blendOf(now2.vertices, deform, target, blend.alpha, blend.additive);
413
+ if (target !== null) deform = blend === null ? target : blendOf2(now2.vertices, deform, target, blend.alpha, blend.additive);
414
414
  }
415
415
  if (a.sequence !== null && now2.count !== void 0) {
416
416
  const f = evalFrame(a.sequence, now2.count, t);
@@ -5072,6 +5072,9 @@ const FIRST = 2;
5072
5072
  const MODE = 3;
5073
5073
  const HOLD = 4;
5074
5074
  const EMPTY = { name: null, duration: 0, timelines: [], ids: /* @__PURE__ */ new Set() };
5075
+ function addable(tl) {
5076
+ return tl.type === "deform" || tl.type === "bone" && tl.kind !== "inherit";
5077
+ }
5075
5078
  const BONE_PROPERTIES = {
5076
5079
  rotate: ["rotate"],
5077
5080
  translate: ["x", "y"],
@@ -5141,6 +5144,8 @@ function newEntry(anim, loop) {
5141
5144
  mixTime: 0,
5142
5145
  mixDuration: 0,
5143
5146
  totalAlpha: 0,
5147
+ alpha: 1,
5148
+ additive: false,
5144
5149
  keepHold: false,
5145
5150
  timelineMode: [],
5146
5151
  timelineHoldMix: [],
@@ -5317,6 +5322,19 @@ class Mixer {
5317
5322
  }
5318
5323
  return this.plant.mixReversed === true ? 1 - m : m;
5319
5324
  }
5325
+ /** `TrackEntry.alpha` — inverted or ignored under the plants. */
5326
+ alphaOf(e) {
5327
+ if (this.plant.alphaIgnored === true) return 1;
5328
+ return this.plant.alphaInverted === true ? 1 - e.alpha : e.alpha;
5329
+ }
5330
+ /** `TrackEntry.additive` — never under the plant that applies an additive entry as a replacing one. */
5331
+ additiveOf(e) {
5332
+ return e.additive && this.plant.additiveAsReplace !== true;
5333
+ }
5334
+ /** Whether `computeHold` reads an entry as additive — never under the plant that holds as if none were. */
5335
+ holdsAdditive(e) {
5336
+ return e !== null && this.additiveOf(e) && this.plant.additiveHeld !== true;
5337
+ }
5320
5338
  update(delta) {
5321
5339
  const tracks = this.tracks;
5322
5340
  for (let i = 0, n = tracks.length; i < n; i++) {
@@ -5387,7 +5405,7 @@ class Mixer {
5387
5405
  for (let i = 0, n = tracks.length; i < n; i++) {
5388
5406
  const current = tracks[i];
5389
5407
  if (current === null || current.delay > 0) continue;
5390
- let alpha = 1;
5408
+ let alpha = this.alphaOf(current);
5391
5409
  if (current.mixingFrom !== null) alpha *= this.applyMixingFrom(current, pose, i);
5392
5410
  else if (current.trackTime >= current.trackEnd && current.next === null) alpha = 0;
5393
5411
  const animationLast = current.animationLast;
@@ -5397,18 +5415,22 @@ class Mixer {
5397
5415
  if (i === 0 && alpha === 1) {
5398
5416
  for (const tl of timelines) {
5399
5417
  if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, SETUP, true);
5400
- else this.applyTimeline(pose, tl, animationLast, animationTime, true, 1, SETUP, false);
5418
+ else this.applyTimeline(pose, tl, animationLast, animationTime, true, 1, SETUP, false, false);
5401
5419
  }
5402
5420
  } else {
5403
5421
  const modes = current.timelineMode;
5404
- const retain = alpha >= 0;
5405
- const firstFrame = current.timelinesRotation.length !== count << 1;
5422
+ const retain = this.plant.attachmentsBelowHalfAlpha === true ? alpha >= 0.5 : alpha >= 0;
5423
+ const add = this.additiveOf(current);
5424
+ const shortest = add && this.plant.additiveRotateRemembered !== true;
5425
+ const firstFrame = !shortest && current.timelinesRotation.length !== count << 1;
5406
5426
  if (firstFrame) current.timelinesRotation.length = count << 1;
5407
5427
  timelines.forEach((tl, ii) => {
5408
5428
  const from = this.fromOf(modes[ii] & MODE, i);
5409
- if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, from, current.timelinesRotation, ii << 1, firstFrame);
5410
- else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, from, retain);
5411
- else this.applyTimeline(pose, tl, animationLast, animationTime, true, alpha, from, false);
5429
+ if (!shortest && tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, from, current.timelinesRotation, ii << 1, firstFrame);
5430
+ else if (tl.type === "attachment") {
5431
+ if (add && this.plant.additiveAttachmentsDirect === true) this.applyAttachmentDirect(pose, tl, animationTime, from, false);
5432
+ else this.applyAttachment(pose, tl, animationTime, from, retain);
5433
+ } else this.applyTimeline(pose, tl, animationLast, animationTime, true, alpha, from, false, add);
5412
5434
  });
5413
5435
  }
5414
5436
  if (i === 0) base = animationTime;
@@ -5428,15 +5450,17 @@ class Mixer {
5428
5450
  const from = to.mixingFrom;
5429
5451
  const fromMix = from.mixingFrom !== null ? this.applyMixingFrom(from, pose, track) : 1;
5430
5452
  const mix = this.mixOf(to);
5431
- const a = 1 * fromMix;
5432
- const keep = 1 - mix * 1;
5453
+ const a = this.alphaOf(from) * fromMix;
5454
+ const keep = 1 - mix * (this.plant.holdIgnoresEntryAlpha === true ? 1 : this.alphaOf(to));
5433
5455
  const alphaMix = a * (1 - mix);
5434
5456
  const alphaHold = keep > 0 ? alphaMix / keep : a;
5435
5457
  const timelines = from.anim.timelines;
5436
5458
  const count = timelines.length;
5437
5459
  const modes = from.timelineMode;
5438
5460
  const holdMix = from.timelineHoldMix;
5439
- const firstFrame = from.timelinesRotation.length !== count << 1;
5461
+ const add = this.additiveOf(from);
5462
+ const shortest = add && this.plant.additiveRotateRemembered !== true;
5463
+ const firstFrame = !shortest && from.timelinesRotation.length !== count << 1;
5440
5464
  if (firstFrame) from.timelinesRotation.length = count << 1;
5441
5465
  const animationLast = from.animationLast;
5442
5466
  const animationTime = animationTimeOf(from);
@@ -5451,9 +5475,11 @@ class Mixer {
5451
5475
  alpha = h === null || h === void 0 ? alphaHold : alphaHold * (1 - this.mixOf(h));
5452
5476
  } else alpha = alphaMix;
5453
5477
  from.totalAlpha += alpha;
5454
- if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, mixFrom, from.timelinesRotation, i << 1, firstFrame);
5455
- else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, mixFrom, false);
5456
- else this.applyTimeline(pose, tl, animationLast, animationTime, fire, alpha, mixFrom, true);
5478
+ if (!shortest && tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, mixFrom, from.timelinesRotation, i << 1, firstFrame);
5479
+ else if (tl.type === "attachment") {
5480
+ if (add && this.plant.additiveAttachmentsDirect === true) this.applyAttachmentDirect(pose, tl, animationTime, mixFrom, true);
5481
+ else this.applyAttachment(pose, tl, animationTime, mixFrom, false);
5482
+ } else this.applyTimeline(pose, tl, animationLast, animationTime, fire, alpha, mixFrom, true, add);
5457
5483
  });
5458
5484
  from.nextAnimationLast = animationTime;
5459
5485
  from.nextTrackLast = from.trackTime;
@@ -5480,14 +5506,20 @@ class Mixer {
5480
5506
  const holdMix = entry.timelineHoldMix;
5481
5507
  holdMix.length = 0;
5482
5508
  const to = entry.mixingTo;
5509
+ const add = this.holdsAdditive(entry);
5510
+ const toAdds = this.holdsAdditive(to);
5483
5511
  timelines.forEach((tl, i) => {
5484
5512
  const from = this.propertyFrom(track, tl.ids);
5513
+ if (add && addable(tl)) {
5514
+ modes[i] = from;
5515
+ return;
5516
+ }
5485
5517
  let mode;
5486
- if (to === null || tl.instant || !hasAny(to.anim, tl.ids)) mode = from;
5518
+ if (to === null || tl.instant || toAdds && addable(tl) || !hasAny(to.anim, tl.ids)) mode = from;
5487
5519
  else {
5488
5520
  mode = from | HOLD;
5489
5521
  for (let next = to.mixingTo; next !== null; next = next.mixingTo) {
5490
- if (!hasAny(next.anim, tl.ids)) {
5522
+ if (this.holdsAdditive(next) && addable(tl) || !hasAny(next.anim, tl.ids)) {
5491
5523
  if (next.mixDuration > 0) holdMix[i] = next;
5492
5524
  break;
5493
5525
  }
@@ -5523,15 +5555,15 @@ class Mixer {
5523
5555
  const channel2 = this.plant.channel ?? channelAt;
5524
5556
  return keys[index].values.map((_v, c) => channel2(keys, index, c, t));
5525
5557
  }
5526
- /** `Timeline.apply` for every timeline but an attachment's and a rotation mixed below full weight. */
5527
- applyTimeline(pose, tl, lastTime, time, fire, alpha, from, out) {
5558
+ /** `Timeline.apply` for every timeline but an attachment's and a rotation mixed below full weight; `add` is the entry's, read only by the timelines the runtime adds (`addable`). */
5559
+ applyTimeline(pose, tl, lastTime, time, fire, alpha, from, out, add) {
5528
5560
  switch (tl.type) {
5529
5561
  case "bone":
5530
- return this.applyBone(pose, tl, time, alpha, from, out);
5562
+ return this.applyBone(pose, tl, time, alpha, from, out, add);
5531
5563
  case "colour":
5532
5564
  return this.applyColour(pose, tl, time, alpha, from);
5533
5565
  case "deform":
5534
- return this.applyDeform(pose, tl, time, alpha, from);
5566
+ return this.applyDeform(pose, tl, time, alpha, from, add);
5535
5567
  case "events":
5536
5568
  if (fire) pose.events.push(...eventsFired(tl.keys, lastTime, time, rawNumber, time < lastTime));
5537
5569
  return;
@@ -5539,7 +5571,7 @@ class Mixer {
5539
5571
  return;
5540
5572
  }
5541
5573
  }
5542
- applyBone(pose, tl, time, alpha, from, out) {
5574
+ applyBone(pose, tl, time, alpha, from, out, add) {
5543
5575
  const bone2 = pose.bone(tl.bone);
5544
5576
  if (bone2 === null) return;
5545
5577
  const setup = this.doc.bones[tl.bone];
@@ -5562,7 +5594,7 @@ class Mixer {
5562
5594
  const relative = (field, i) => {
5563
5595
  const s = setup[field] ?? 0;
5564
5596
  const c = bone2[field] ?? 0;
5565
- bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : from === SETUP ? s + v[i] * alpha : c + (v[i] + s - c) * alpha;
5597
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : from === SETUP ? s + v[i] * alpha : c + (add ? v[i] : v[i] + s - c) * alpha;
5566
5598
  };
5567
5599
  const pair = (fx, fy) => {
5568
5600
  const sx = setup[fx] ?? 0;
@@ -5580,6 +5612,9 @@ class Mixer {
5580
5612
  if (from === SETUP) {
5581
5613
  bone2[fx] = sx + v[0] * alpha;
5582
5614
  bone2[fy] = sy + v[1] * alpha;
5615
+ } else if (add) {
5616
+ bone2[fx] = (bone2[fx] ?? 0) + v[0] * alpha;
5617
+ bone2[fy] = (bone2[fy] ?? 0) + v[1] * alpha;
5583
5618
  } else {
5584
5619
  bone2[fx] = (bone2[fx] ?? 0) + (sx + v[0] - (bone2[fx] ?? 0)) * alpha;
5585
5620
  bone2[fy] = (bone2[fy] ?? 0) + (sy + v[1] - (bone2[fy] ?? 0)) * alpha;
@@ -5588,7 +5623,7 @@ class Mixer {
5588
5623
  const scale = (field, i) => {
5589
5624
  const s = setup[field] ?? 1;
5590
5625
  const c = bone2[field] ?? 1;
5591
- bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : scaleValue(v[i] * s, alpha, from, out, c, s);
5626
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : scaleValue(v[i] * s, alpha, from, out, add, c, s);
5592
5627
  };
5593
5628
  switch (tl.kind) {
5594
5629
  case "rotate":
@@ -5624,8 +5659,8 @@ class Mixer {
5624
5659
  bone2.scaleY = (bone2.scaleY ?? 1) + (sy - (bone2.scaleY ?? 1)) * alpha;
5625
5660
  }
5626
5661
  } else {
5627
- bone2.scaleX = scaleValue(v[0] * (setup.scaleX ?? 1), alpha, from, out, bone2.scaleX ?? 1, setup.scaleX ?? 1);
5628
- bone2.scaleY = scaleValue(v[1] * (setup.scaleY ?? 1), alpha, from, out, bone2.scaleY ?? 1, setup.scaleY ?? 1);
5662
+ bone2.scaleX = scaleValue(v[0] * (setup.scaleX ?? 1), alpha, from, out, add, bone2.scaleX ?? 1, setup.scaleX ?? 1);
5663
+ bone2.scaleY = scaleValue(v[1] * (setup.scaleY ?? 1), alpha, from, out, add, bone2.scaleY ?? 1, setup.scaleY ?? 1);
5629
5664
  }
5630
5665
  break;
5631
5666
  case "scalex":
@@ -5640,7 +5675,7 @@ class Mixer {
5640
5675
  applyRotate(pose, tl, time, alpha, from, rotations, i, firstFrame) {
5641
5676
  if (firstFrame) rotations[i] = 0;
5642
5677
  if (alpha === 1) {
5643
- this.applyBone(pose, tl, time, 1, from, false);
5678
+ this.applyBone(pose, tl, time, 1, from, false, false);
5644
5679
  return;
5645
5680
  }
5646
5681
  const bone2 = pose.bone(tl.bone);
@@ -5712,6 +5747,14 @@ class Mixer {
5712
5747
  if (retain) s.attachmentState = ATTACH_RETAIN;
5713
5748
  else if (!setup) s.attachmentState = ATTACH_SETUP;
5714
5749
  }
5750
+ /** `AttachmentTimeline.apply` — the plain timeline, writing no attachment state: only `additiveAttachmentsDirect` reaches it, the reading `CO52` rejects. */
5751
+ applyAttachmentDirect(pose, tl, time, from, out) {
5752
+ if (!pose.slotLive(tl.slot)) return;
5753
+ const index = (this.plant.search ?? keyIndexAt)(tl.keys, time);
5754
+ if (out || index < 0) {
5755
+ if (from !== CURRENT) pose.setAttachment(tl.slot, this.doc.slots[tl.slot].setup);
5756
+ } else pose.setAttachment(tl.slot, tl.keys[index].name ?? null);
5757
+ }
5715
5758
  applyColour(pose, tl, time, alpha, from) {
5716
5759
  if (!pose.slotLive(tl.slot)) return;
5717
5760
  const slot = this.doc.slots[tl.slot];
@@ -5807,7 +5850,7 @@ class Mixer {
5807
5850
  }
5808
5851
  }
5809
5852
  /** `DeformTimeline.apply`: every live slot showing the record the timeline keys, its deform array mixed. */
5810
- applyDeform(pose, tl, time, alpha, from) {
5853
+ applyDeform(pose, tl, time, alpha, from, add) {
5811
5854
  const keys = tl.timeline.deform;
5812
5855
  if (keys === null) return;
5813
5856
  pose.slots.forEach((s, k) => {
@@ -5837,12 +5880,26 @@ class Mixer {
5837
5880
  }
5838
5881
  return;
5839
5882
  }
5840
- if (alpha === 1) s.deform = [...target];
5841
- else if (mixFrom === SETUP) {
5883
+ const fromSetup = mixFrom === SETUP;
5884
+ if (alpha === 1) {
5885
+ if (add && !fromSetup) {
5886
+ const current = s.deform;
5887
+ if (!vertices.weighted) {
5888
+ const setup = setupArray(vertices);
5889
+ s.deform = current.map((c, i) => c + (target[i] - setup[i]));
5890
+ } else s.deform = current.map((c, i) => c + target[i]);
5891
+ } else s.deform = [...target];
5892
+ } else if (fromSetup) {
5842
5893
  if (!vertices.weighted) {
5843
5894
  const setup = setupArray(vertices);
5844
5895
  s.deform = target.map((v, i) => setup[i] + (v - setup[i]) * alpha);
5845
5896
  } else s.deform = target.map((v) => v * alpha);
5897
+ } else if (add) {
5898
+ const current = s.deform;
5899
+ if (!vertices.weighted) {
5900
+ const setup = setupArray(vertices);
5901
+ s.deform = current.map((c, i) => c + (target[i] - setup[i]) * alpha);
5902
+ } else s.deform = current.map((c, i) => c + target[i] * alpha);
5846
5903
  } else {
5847
5904
  const current = s.deform;
5848
5905
  s.deform = current.map((c, i) => c + (target[i] - c) * alpha);
@@ -5855,9 +5912,10 @@ function beforeFirstKey(from, alpha, current, setup) {
5855
5912
  if (from === FIRST) return current + (setup - current) * alpha;
5856
5913
  return current;
5857
5914
  }
5858
- function scaleValue(value, alpha, from, out, current, setup) {
5859
- if (alpha === 1) return value;
5915
+ function scaleValue(value, alpha, from, out, add, current, setup) {
5916
+ if (alpha === 1 && !add) return value;
5860
5917
  let base = from === SETUP ? setup : current;
5918
+ if (add) return base + (value - setup) * alpha;
5861
5919
  if (out) return base + (Math.abs(value) * Math.sign(base) - base) * alpha;
5862
5920
  base = Math.abs(base) * Math.sign(value);
5863
5921
  return base + (value - base) * alpha;
@@ -5878,6 +5936,16 @@ function loopOf(options, call) {
5878
5936
  if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live tracks' ${call} options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
5879
5937
  return options.loop;
5880
5938
  }
5939
+ function alphaValue(v, what, call) {
5940
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) throw new CoreInputError(`the live tracks' ${call} ${what} is ${typeof v === "number" ? String(v) : JSON.stringify(v)}, not a number from 0 to 1`);
5941
+ return v;
5942
+ }
5943
+ function blendOf(options, call) {
5944
+ const alpha = options.alpha === void 0 ? 1 : alphaValue(options.alpha, "options.alpha", call);
5945
+ const additive = options.additive;
5946
+ if (additive !== void 0 && typeof additive !== "boolean") throw new CoreInputError(`the live tracks' ${call} options.additive is ${JSON.stringify(additive)}, not true or false — an entry adds or it does not`);
5947
+ return { alpha, additive: additive === true };
5948
+ }
5881
5949
  function delayOf(v, call) {
5882
5950
  if (typeof v !== "number" || !Number.isFinite(v)) throw new CoreInputError(`the live tracks' ${call} delay is ${String(v)}, not a finite number`);
5883
5951
  return v;
@@ -5911,8 +5979,11 @@ class Tracks {
5911
5979
  const i = trackIndex(track, "setAnimation");
5912
5980
  const loop = loopOf(options, "setAnimation");
5913
5981
  const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "setAnimation");
5982
+ const blend = blendOf(options, "setAnimation");
5914
5983
  const entry = this.mixer.setAnimation(i, this.mixer.animation(animation), loop);
5915
5984
  entry.mixDuration = mix;
5985
+ entry.alpha = blend.alpha;
5986
+ entry.additive = blend.additive;
5916
5987
  }
5917
5988
  setEmptyAnimation(track, mixDuration) {
5918
5989
  this.live("setEmptyAnimation");
@@ -5928,8 +5999,11 @@ class Tracks {
5928
5999
  const loop = loopOf(options, "addAnimation");
5929
6000
  const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "addAnimation");
5930
6001
  const delay = delayOf(options.delay, "addAnimation");
6002
+ const blend = blendOf(options, "addAnimation");
5931
6003
  const entry = this.mixer.addAnimation(i, this.mixer.animation(animation), loop, delay);
5932
6004
  this.mixer.setMixDuration(entry, mix, delay);
6005
+ entry.alpha = blend.alpha;
6006
+ entry.additive = blend.additive;
5933
6007
  }
5934
6008
  addEmptyAnimation(track, mixDuration, delay) {
5935
6009
  this.live("addEmptyAnimation");
@@ -5941,10 +6015,18 @@ class Tracks {
5941
6015
  entry.mixDuration = mix;
5942
6016
  entry.trackEnd = mix;
5943
6017
  }
6018
+ setAlpha(track, alpha) {
6019
+ this.live("setAlpha");
6020
+ const i = trackIndex(track, "setAlpha");
6021
+ const a = alphaValue(alpha, "alpha", "setAlpha");
6022
+ const e = this.mixer.tracks[i] ?? null;
6023
+ if (e === null) throw new CoreInputError(`the live tracks' setAlpha names track ${i}, which plays nothing — there is no entry to weigh`);
6024
+ e.alpha = a;
6025
+ }
5944
6026
  current(track) {
5945
6027
  const e = this.mixer.tracks[trackIndex(track, "current")] ?? null;
5946
6028
  if (e === null) return null;
5947
- return { animation: e.anim.name, loop: e.loop, trackTime: e.trackTime, animationTime: animationTimeOf(e), duration: e.anim.duration };
6029
+ return { animation: e.anim.name, loop: e.loop, trackTime: e.trackTime, animationTime: animationTimeOf(e), duration: e.anim.duration, alpha: e.alpha, additive: e.additive };
5948
6030
  }
5949
6031
  start() {
5950
6032
  this.live("start");
@@ -8342,6 +8424,10 @@ class RigPlayer {
8342
8424
  loop = true;
8343
8425
  /** One past the highest track touched — how many `current(track)` reads a step takes. */
8344
8426
  trackCount = 0;
8427
+ /** `RigPlayerOptions.tracks`: every play opens the live tracks, never the single track. */
8428
+ trackMode;
8429
+ /** 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. */
8430
+ beyondBase = false;
8345
8431
  paused = false;
8346
8432
  pose = null;
8347
8433
  setup = null;
@@ -8364,6 +8450,7 @@ class RigPlayer {
8364
8450
  this.view = options.skin === void 0 ? underNoSkin(read.doc) : underSkin(read.doc, options.skin);
8365
8451
  this.source = frameSource(this.view, read.parsed);
8366
8452
  this.seekStep = options.seekStep ?? 1 / 60;
8453
+ this.trackMode = options.tracks === true;
8367
8454
  if (!(this.seekStep > 0)) throw new Error(`seekStep must be a positive number of seconds, got ${String(options.seekStep)}`);
8368
8455
  this.root = root;
8369
8456
  this.assets = assets;
@@ -8433,8 +8520,12 @@ class RigPlayer {
8433
8520
  const track = options.track ?? 0;
8434
8521
  const loop = options.loop ?? true;
8435
8522
  const mix = options.mix ?? 0;
8523
+ const alpha = options.alpha ?? 1;
8524
+ const additive = options.additive ?? false;
8436
8525
  this.paused = false;
8437
- if (this.tracks === null && track === 0 && mix === 0) {
8526
+ const plain = track === 0 && mix === 0 && alpha === 1 && !additive;
8527
+ if (!plain) this.beyondBase = true;
8528
+ if (this.tracks === null && plain && !this.trackMode) {
8438
8529
  this.animation = name;
8439
8530
  this.loop = loop;
8440
8531
  this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook });
@@ -8442,7 +8533,7 @@ class RigPlayer {
8442
8533
  return;
8443
8534
  }
8444
8535
  const tracks = this.compose();
8445
- tracks.setAnimation(track, name, { loop, mixDuration: mix });
8536
+ tracks.setAnimation(track, name, { loop, mixDuration: mix, alpha, additive });
8446
8537
  this.trackCount = Math.max(this.trackCount, track + 1);
8447
8538
  if (track === 0) {
8448
8539
  this.animation = name;
@@ -8458,8 +8549,9 @@ class RigPlayer {
8458
8549
  */
8459
8550
  queue(name, options = {}) {
8460
8551
  const track = options.track ?? 0;
8552
+ this.beyondBase = true;
8461
8553
  const tracks = this.compose();
8462
- tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0 });
8554
+ tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0, alpha: options.alpha ?? 1, additive: options.additive ?? false });
8463
8555
  this.trackCount = Math.max(this.trackCount, track + 1);
8464
8556
  this.pose = tracks.step(0);
8465
8557
  }
@@ -8469,6 +8561,7 @@ class RigPlayer {
8469
8561
  * setup pose. The pose the player holds is re-posed at once.
8470
8562
  */
8471
8563
  clear(track = 0, options = {}) {
8564
+ this.beyondBase = true;
8472
8565
  const tracks = this.compose();
8473
8566
  tracks.setEmptyAnimation(track, options.mix ?? 0);
8474
8567
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8481,31 +8574,48 @@ class RigPlayer {
8481
8574
  * its last `mix` seconds.
8482
8575
  */
8483
8576
  queueClear(track = 0, options = {}) {
8577
+ this.beyondBase = true;
8484
8578
  const tracks = this.compose();
8485
8579
  tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8486
8580
  this.trackCount = Math.max(this.trackCount, track + 1);
8487
8581
  this.pose = tracks.step(0);
8488
8582
  }
8583
+ /**
8584
+ * Sets the weight of `track`'s current entry, from 0 to 1, from the next pose
8585
+ * on — the pose the player holds is re-posed at once, so a held player shows
8586
+ * it on its next `draw`. An entry it is mixing from keeps its own weight. A
8587
+ * track playing nothing, or a weight outside 0..1, is refused in the core's
8588
+ * words. On the single track this composes: the player moves onto the tracks.
8589
+ */
8590
+ setAlpha(track, alpha) {
8591
+ this.beyondBase = true;
8592
+ const tracks = this.compose();
8593
+ tracks.setAlpha(track, alpha);
8594
+ this.pose = tracks.step(0);
8595
+ }
8489
8596
  /** `track`'s current entry, or null where it plays nothing (and before `play`). */
8490
8597
  entry(track = 0) {
8491
8598
  if (this.tracks !== null) {
8492
8599
  const view = this.tracks.current(track);
8493
- return view === null ? null : { animation: view.animation, loop: view.loop, trackTime: view.trackTime, animationTime: view.animationTime, duration: view.duration };
8600
+ return view === null ? null : { animation: view.animation, loop: view.loop, trackTime: view.trackTime, animationTime: view.animationTime, duration: view.duration, alpha: view.alpha, additive: view.additive };
8494
8601
  }
8495
8602
  const single = this.track;
8496
8603
  if (track !== 0 || single === null) return null;
8497
- return { animation: this.animation, loop: this.loop, trackTime: single.trackTime, animationTime: single.animationTime, duration: single.duration };
8604
+ return { animation: this.animation, loop: this.loop, trackTime: single.trackTime, animationTime: single.animationTime, duration: single.duration, alpha: 1, additive: false };
8498
8605
  }
8499
8606
  /** Whether the player is composed: on the live tracks rather than the single track. */
8500
8607
  get composed() {
8501
8608
  return this.tracks !== null;
8502
8609
  }
8503
8610
  /**
8504
- * The live tracks, opened by the first composing call. A single track in
8505
- * play is carried over by replay: track 0 set to its animation, pose 0 taken
8506
- * by `start()` (the player's — nothing steps before it) and the tracks
8507
- * stepped to the single track's time in `seekStep` increments, so a physics
8508
- * rig's history restarts there and nothing is fired on the way.
8611
+ * The live tracks, opened by the first composing call (or by the first
8612
+ * `play` under `tracks: true`). A single track in play is carried over by
8613
+ * replay: track 0 set to its animation, pose 0 taken by `start()` (the
8614
+ * player's — nothing steps before it) and the tracks stepped to the single
8615
+ * track's time in `seekStep` increments, so a physics rig's history
8616
+ * restarts there and nothing is fired on the way. The core offers no
8617
+ * hand-over of a live track's state, so that replay costs one pose per
8618
+ * `seekStep` of time played — which `tracks: true` avoids entirely.
8509
8619
  */
8510
8620
  compose() {
8511
8621
  if (this.tracks !== null) return this.tracks;
@@ -8542,12 +8652,20 @@ class RigPlayer {
8542
8652
  seek(time) {
8543
8653
  if (this.animation === null) throw new Error("seek before play: no animation is open");
8544
8654
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
8545
- 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");
8655
+ 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");
8546
8656
  this.reopen();
8547
8657
  this.stepTo(time);
8548
8658
  }
8549
- /** The single track opened again with track 0's animation as `play` last set it, at pose 0. */
8659
+ /** 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`. */
8550
8660
  reopen() {
8661
+ if (this.tracks !== null) {
8662
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook });
8663
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
8664
+ this.trackCount = 1;
8665
+ this.tracks = tracks;
8666
+ this.pose = tracks.start();
8667
+ return;
8668
+ }
8551
8669
  this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
8552
8670
  this.pose = this.track.first;
8553
8671
  }
@@ -8616,14 +8734,14 @@ class RigPlayer {
8616
8734
  }
8617
8735
  set skin(name) {
8618
8736
  if (name === this.skinName) return;
8619
- 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");
8737
+ 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");
8620
8738
  const view = name === void 0 ? underNoSkin(this.read.doc) : underSkin(this.read.doc, name);
8621
8739
  this.view = view;
8622
8740
  this.source = frameSource(view, this.read.parsed);
8623
8741
  this.skinName = name;
8624
8742
  this.setup = null;
8625
- if (this.animation !== null && this.track !== null) {
8626
- const time = this.track.trackTime;
8743
+ if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
8744
+ const time = this.time;
8627
8745
  this.reopen();
8628
8746
  this.stepTo(time);
8629
8747
  }