rig-play 1.7.0 → 1.8.1

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/NOTICE.md CHANGED
@@ -3,8 +3,8 @@
3
3
  ## Spine Runtimes
4
4
 
5
5
  **The published package links nothing from Esoteric Software.** An install of
6
- `rig-play` (or of `spine-html`, the same files under the name the package first
7
- shipped as) carries one JavaScript module, `dist/index.js`, plus type
6
+ `@rig-c/play` (or of `rig-play` or `spine-html`, the same files under the names the
7
+ package shipped as before) carries one JavaScript module, `dist/index.js`, plus type
8
8
  declarations, and that module bundles two things: the renderer, and
9
9
  `rig-c/core` (MIT), rigc's own posing core, which poses the compiled model
10
10
  document `rigc build` writes. No part of the
@@ -28,7 +28,7 @@ it, and it is neither committed nor published.
28
28
 
29
29
  ### What that means, as a chain of facts
30
30
 
31
- rig-play's own code is MIT (see `LICENSE`). The following is a restatement of
31
+ @rig-c/play's own code is MIT (see `LICENSE`). The following is a restatement of
32
32
  Esoteric Software's terms, not a licence term of this project:
33
33
 
34
34
  1. rigc writes two things side by side: the model document this package plays,
@@ -46,7 +46,7 @@ Esoteric Software's terms, not a licence term of this project:
46
46
  because the package links none.
47
47
 
48
48
  > **What a consumer does with rigc's Spine-format output is governed by
49
- > Esoteric Software's terms**, not by this package — rig-play neither adds those
49
+ > Esoteric Software's terms**, not by this package — @rig-c/play neither adds those
50
50
  > terms nor removes them.
51
51
 
52
52
  ## Example assets
package/README.md CHANGED
@@ -1,18 +1,21 @@
1
1
  <p align="center">
2
- <img src="assets/banner.svg" alt="rig-play - plays rigc's compiled model as DOM elements or one canvas" width="100%" />
2
+ <img src="assets/banner.svg" alt="@rig-c/play - plays rigc's compiled model as DOM elements or one canvas" width="100%" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <a href="https://www.npmjs.com/package/rig-play"><img src="https://img.shields.io/npm/v/rig-play.svg?style=flat-square&color=FF6B4A" alt="npm version" /></a>
7
- <a href="https://www.npmjs.com/package/rig-play"><img src="https://img.shields.io/npm/dm/rig-play.svg?style=flat-square&color=EC4899" alt="npm downloads" /></a>
6
+ <a href="https://www.npmjs.com/package/@rig-c/play"><img src="https://img.shields.io/npm/v/@rig-c/play.svg?style=flat-square&color=FF6B4A" alt="npm version" /></a>
7
+ <a href="https://www.npmjs.com/package/@rig-c/play"><img src="https://img.shields.io/npm/dm/@rig-c/play.svg?style=flat-square&color=EC4899" alt="npm downloads" /></a>
8
8
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-38BDF8.svg?style=flat-square" alt="license" /></a>
9
9
  </p>
10
10
 
11
- Plays the rigs [rigc](https://github.com/firejune/rigc) builds — the compiled model
11
+ Plays the rigs [rigc](https://github.com/rig-c/core) builds — the compiled model
12
12
  document `rigc build` writes, `skeleton.model.json` — as **plain DOM**: one
13
13
  absolutely-positioned `<img>` per rigid slot, posed with a single CSS `matrix()` write
14
14
  per frame, deforming meshes on small per-part canvases; or as **one canvas** per player.
15
- `spine-html` is the same package under the name it first shipped as.
15
+ `rig-play` and `spine-html` are the same package under the names it shipped as before.
16
+ The toolchain lives under one organisation: the compiler [rig-c/core](https://github.com/rig-c/core)
17
+ (npm `rig-c`), the part generator [rig-c/parts](https://github.com/rig-c/parts)
18
+ (npm `@rig-c/parts`), and this player, [rig-c/play](https://github.com/rig-c/play).
16
19
 
17
20
  ## Why
18
21
 
@@ -29,8 +32,9 @@ At 1.0 it stopped being a Spine runtime. It plays the model document `rigc build
29
32
  poses it through `rig-c/core` — rigc's own posing core, bundled into the package — and
30
33
  links nothing from Esoteric Software; a Spine export is refused by name. It gained a
31
34
  second render mode that draws the whole frame into one canvas. And it took the name
32
- `rig-play`, which says what it is; `spine-html` keeps being published with the same
33
- files, so links to the old name keep working.
35
+ `rig-play`, which says what it is; after 1.8 it moved with the rest of the toolchain to
36
+ `@rig-c/play`. `rig-play` and `spine-html` keep being published with the same files, so
37
+ links to either earlier name keep working.
34
38
 
35
39
  ### Measured
36
40
 
@@ -159,11 +163,11 @@ default stays `dom`.
159
163
  ## Install
160
164
 
161
165
  ```bash
162
- npm i rig-play
163
- npm i -D rig-c # the build tool: `rigc build` writes the model rig-play loads
166
+ npm i @rig-c/play
167
+ npm i -D rig-c # the build tool: `rigc build` writes the model the player loads
164
168
  ```
165
169
 
166
- The package plays rigs built by [rigc](https://github.com/firejune/rigc):
170
+ The package plays rigs built by [rigc](https://github.com/rig-c/core):
167
171
  `rigc build` writes a compiled model document, `skeleton.model.json` (spec
168
172
  `rigc-compiled/3`), next to the Spine pair and the atlas pages. That document
169
173
  is what the player loads, and it states everything posing and drawing need —
@@ -173,7 +177,7 @@ the skeleton, the animations, and where every region sits on its page — so the
173
177
  is nothing else to install.
174
178
 
175
179
  ```ts
176
- import { loadModel, RigPlayer } from 'rig-play';
180
+ import { loadModel, RigPlayer } from '@rig-c/play';
177
181
 
178
182
  const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
179
183
 
@@ -212,7 +216,7 @@ else. A document whose `spec` is not one `rig-c/core` reads is refused with the
212
216
  core's own sentence plus "build it with `rigc build`", and so is a
213
217
  `rigc-compiled/1` document, which predates the `pages` section.
214
218
 
215
- `rig-play` is a third-party renderer and is not affiliated with or endorsed by
219
+ `@rig-c/play` is a third-party renderer and is not affiliated with or endorsed by
216
220
  Esoteric Software.
217
221
 
218
222
  ### Drawing your own frames
@@ -221,7 +225,7 @@ Esoteric Software.
221
225
  record, so anything that can produce one can draw through it:
222
226
 
223
227
  ```ts
224
- import { DomRenderer, type PosedFrame } from 'rig-play';
228
+ import { DomRenderer, type PosedFrame } from '@rig-c/play';
225
229
 
226
230
  const renderer = new DomRenderer(rootElement, assets.regionImages, assets.pageImages);
227
231
  renderer.render(frame); // frame: PosedFrame
@@ -300,9 +304,25 @@ player.play('wave', { track: 2, loop: false, mix: 0.1 }); // a one-shot…
300
304
  player.queueClear(2, { mix: 0.1 }); // …mixed back out over its last 0.1 s
301
305
  player.queue('idle_b', { track: 0, mix: 0.3 }); // after the current idle cycle completes
302
306
  player.clear(1, { mix: 0.2 }); // the layer gone, the base showing through
303
- player.entry(2); // { animation, loop, trackTime, animationTime, duration } | null
307
+ player.entry(2); // { animation, loop, trackTime, animationTime, duration, alpha, additive } | null
308
+ player.play('blink', { track: 3, alpha: 0.6 }); // the same clip at a weight…
309
+ player.setAlpha(3, 0.85); // …changed while it plays
310
+ player.play('breath', { track: 4, additive: true }); // adds to what the tracks below posed
304
311
  ```
305
312
 
313
+ **Weight and additive layers** (rig-c 2.39.0, the runtime's `TrackEntry.alpha` and
314
+ `additive`): `alpha` (0..1, default 1) multiplies every timeline the entry applies;
315
+ `setAlpha(track, alpha)` changes the current entry's weight from the next pose and
316
+ re-poses the held pose. It does not touch events — an entry fires at any alpha, 0
317
+ included, and one mixing out fires none — or attachments: the current entry keeps
318
+ its attachments at any alpha, one mixing out keeps none. `additive: true`, fixed
319
+ when the entry is set or queued, adds the entry's keyed bone and deform values to
320
+ what the tracks below posed instead of replacing them, so a layer keying the same
321
+ bone channels as an idle does not cover its motion; a rotation is added as it
322
+ stands, with no shortest-angle wrap, and an additive entry mixing out fades with its
323
+ alpha and is never held. Colour, attachment and event timelines apply as on a
324
+ replacing entry. A weight below 1 or an additive entry composes, as a mix does.
325
+
306
326
  `play(name)` keeps meaning what it meant: a plain play on track 0 runs on rig-c's
307
327
  single live track, which poses everything the core poses. The first call that
308
328
  composes — a `mix`, a `track` above 0, a `queue`, a `clear` — moves the player onto
@@ -637,9 +657,9 @@ name, and unloading is still elements first, bitmaps second.
637
657
 
638
658
  ### 0.8 → 1.0
639
659
 
640
- | 0.8.0 (`spine-html`) | 1.0.0 (`rig-play`) | Note |
660
+ | 0.8.0 (`spine-html`) | 1.x (`@rig-c/play`, formerly `rig-play`) | Note |
641
661
  | --- | --- | --- |
642
- | `npm i spine-html @esotericsoftware/spine-core` | `npm i rig-play`, and `npm i -D rig-c` for the build tool | 0.8 declared spine-core as a peer dependency (`>=4.0.0 <4.4.0`). 1.0.0 declares no dependencies and no peer dependencies. |
662
+ | `npm i spine-html @esotericsoftware/spine-core` | `npm i @rig-c/play`, and `npm i -D rig-c` for the build tool | 0.8 declared spine-core as a peer dependency (`>=4.0.0 <4.4.0`). 1.0.0 declares no dependencies and no peer dependencies. |
643
663
  | `loadSkeletonAssets(options)` | `loadModel(options)` | One URL instead of two. The `.atlas` and `skeleton.json` a build writes beside the document are not fetched. |
644
664
  | `LoadSkeletonAssetsOptions.atlasUrl`, `skeletonUrl` | `LoadModelOptions.modelUrl` | |
645
665
  | `resolvePage(pageName, atlasUrl)` | `resolvePage(pageName, modelUrl)` | The default now resolves a page name against the model URL's directory. |
@@ -659,7 +679,7 @@ name, and unloading is still elements first, bitmaps second.
659
679
  | `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
660
680
  | `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
681
  | `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*. |
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*. |
682
+ | `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*. |
663
683
  | `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. |
664
684
  | 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. |
665
685
  | holding a pose (the caller stopped advancing) | `player.pause()`, `player.resume()` | `frame` keeps drawing the held pose. |
@@ -745,7 +765,7 @@ renderer.dispose(); assets.dispose(); // on unload
745
765
  1.0.0:
746
766
 
747
767
  ```ts
748
- import { loadModel, RigPlayer } from 'rig-play';
768
+ import { loadModel, RigPlayer } from '@rig-c/play';
749
769
  const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
750
770
  const player = new RigPlayer(rootElement, assets);
751
771
  player.play('walk', { loop: true });
@@ -918,10 +938,10 @@ perf oracle is the demo's stats line on a real device.
918
938
 
919
939
  ## License
920
940
 
921
- rig-play's own code is MIT (see [LICENSE](LICENSE)). That says nothing about Spine,
941
+ @rig-c/play's own code is MIT (see [LICENSE](LICENSE)). That says nothing about Spine,
922
942
  so here is what is true of this package and this repository, stated plainly:
923
943
 
924
- 1. The published package — `rig-play`, and `spine-html`, the same files — **links
944
+ 1. The published package — `@rig-c/play`, and `rig-play` and `spine-html`, the same files — **links
925
945
  nothing from Esoteric Software**. Its one module bundles the renderer and
926
946
  `rig-c/core` (MIT), which poses rigc's model document; no part of the
927
947
  [Spine Runtimes](https://github.com/EsotericSoftware/spine-runtimes) is in it or
@@ -940,7 +960,7 @@ so here is what is true of this package and this repository, stated plainly:
940
960
  this project.
941
961
 
942
962
  > **What a consumer does with rigc's Spine-format output is governed by Esoteric
943
- > Software's terms**, not by this package — rig-play neither adds those terms nor
963
+ > Software's terms**, not by this package — @rig-c/play neither adds those terms nor
944
964
  > removes them.
945
965
 
946
966
  No example assets of Esoteric Software's are fetched or redistributed. See
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");
@@ -8438,9 +8520,12 @@ class RigPlayer {
8438
8520
  const track = options.track ?? 0;
8439
8521
  const loop = options.loop ?? true;
8440
8522
  const mix = options.mix ?? 0;
8523
+ const alpha = options.alpha ?? 1;
8524
+ const additive = options.additive ?? false;
8441
8525
  this.paused = false;
8442
- if (track !== 0 || mix !== 0) this.beyondBase = true;
8443
- if (this.tracks === null && track === 0 && mix === 0 && !this.trackMode) {
8526
+ const plain = track === 0 && mix === 0 && alpha === 1 && !additive;
8527
+ if (!plain) this.beyondBase = true;
8528
+ if (this.tracks === null && plain && !this.trackMode) {
8444
8529
  this.animation = name;
8445
8530
  this.loop = loop;
8446
8531
  this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook });
@@ -8448,7 +8533,7 @@ class RigPlayer {
8448
8533
  return;
8449
8534
  }
8450
8535
  const tracks = this.compose();
8451
- tracks.setAnimation(track, name, { loop, mixDuration: mix });
8536
+ tracks.setAnimation(track, name, { loop, mixDuration: mix, alpha, additive });
8452
8537
  this.trackCount = Math.max(this.trackCount, track + 1);
8453
8538
  if (track === 0) {
8454
8539
  this.animation = name;
@@ -8466,7 +8551,7 @@ class RigPlayer {
8466
8551
  const track = options.track ?? 0;
8467
8552
  this.beyondBase = true;
8468
8553
  const tracks = this.compose();
8469
- 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 });
8470
8555
  this.trackCount = Math.max(this.trackCount, track + 1);
8471
8556
  this.pose = tracks.step(0);
8472
8557
  }
@@ -8495,15 +8580,28 @@ class RigPlayer {
8495
8580
  this.trackCount = Math.max(this.trackCount, track + 1);
8496
8581
  this.pose = tracks.step(0);
8497
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
+ }
8498
8596
  /** `track`'s current entry, or null where it plays nothing (and before `play`). */
8499
8597
  entry(track = 0) {
8500
8598
  if (this.tracks !== null) {
8501
8599
  const view = this.tracks.current(track);
8502
- 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 };
8503
8601
  }
8504
8602
  const single = this.track;
8505
8603
  if (track !== 0 || single === null) return null;
8506
- 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 };
8507
8605
  }
8508
8606
  /** Whether the player is composed: on the live tracks rather than the single track. */
8509
8607
  get composed() {