rig-play 1.1.0 → 1.2.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,35 @@ 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
+ ### Adjusting bones
267
+
268
+ A per-pose write to bone locals — a breath's scale, a look's rotation, a mouth
269
+ driven while the body holds still:
270
+
271
+ ```ts
272
+ const player = new RigPlayer(root, assets, {
273
+ adjust: (bones) => { bones.bone('mouth').scaleY = 1 + openness; },
274
+ });
275
+ player.adjust = null; // or set it later; setting it re-poses the held pose at once
276
+ ```
277
+
278
+ The function runs once per pose, after the animation's timelines and before the
279
+ world transforms, constraints and physics — `rig-c/core`'s `openTrack({ adjust })`,
280
+ which rig-c holds to the Spine runtime at tolerance 0. **A write lasts one
281
+ pose.** The core poses every step from the setup pose, so a value you want held
282
+ is written again each pose and nothing compounds; a 0.8 caller that multiplied a
283
+ channel on a skeleton it kept between frames writes the value it wants, every
284
+ pose. It runs on every pose the animation is applied to, the first one after
285
+ `play` included, and `seek` and a `mode` change keep it.
286
+
287
+ A paused player and a zero step re-pose nothing, so a change the function would
288
+ make is not seen until the next step — unless you ask: setting `player.adjust`
289
+ re-poses the held pose at the same track time, and `player.repose()` does the
290
+ same for a function whose own state changed (the mouth above), without stepping,
291
+ firing events or drawing. A non-finite value, a bone the document does not have,
292
+ or an error the function throws refuses that step in the core's words, and the
293
+ track is reopened by the next `play` or `seek`.
294
+
266
295
  ### One load, several players
267
296
 
268
297
  One `loadModel` backs any number of players: the regions are cut once, and
@@ -569,6 +598,9 @@ name, and unloading is still elements first, bitmaps second.
569
598
  | tint: skeleton × slot × attachment colour | `player.color` × slot × attachment | `player.color` is `[r, g, b, a]` and takes the place of the skeleton's colour. The product arrives as `tint` on each `PosedSlot`. |
570
599
  | dark (two-colour) tint: unsupported | `DomRenderer`: unsupported. `CanvasRenderer`: drawn | Carried as `dark` on a `PosedSlot`. The canvas mode's canvas2d fallback draws alpha only. |
571
600
  | `renderer.dispose()` | `player.dispose()` | Then `assets.dispose()`, as before. `DomRenderer.dispose()` and `CanvasRenderer.dispose()` exist for a renderer you constructed yourself. |
601
+ | `skeleton.findBone('head').worldX`, `.worldY`, `.a` … `.d` | `player.bone('head')` | `{ x, y, a, b, c, d, rotationX, rotationY, scaleX, scaleY, active }`, the core's world transform for the pose the player holds (the setup pose before `play`). An unknown name is refused by name. Since 1.1.0. |
602
+ | `bone.scaleY = …` on a `Skeleton` between `state.apply` and `updateWorldTransform` | `RigPlayerOptions.adjust`, `player.adjust`, `player.repose()` | A function called once per pose with the pose's bone locals (`bones.bone('mouth').scaleY = v`). A write lasts one pose: the core poses every step from the setup pose, so write the value you want, every pose — nothing compounds. See *Adjusting bones*. Since 1.2.0. |
603
+ | `skeleton.getBounds(offset, size)` | `player.bounds()` | `{ x, y, width, height }` or null, world units, Y up: the box of every drawn region's corners and mesh's vertices, a slot under a clip counted inside the clip's own box. `ModelAssets.stage` remains the declared framing, not this box. Since 1.1.0. |
572
604
 
573
605
  ### No counterpart in 1.0.0
574
606
 
@@ -586,13 +618,6 @@ export, for which 1.0.0 declares nothing:
586
618
  load, several players.
587
619
  - **More than one track, mixing between animations, or queueing one after another.**
588
620
  `RigPlayer` declares one track; `play` reopens it at the animation's first pose.
589
- - **Reading or writing a bone.** No bone is exposed: nothing sets a bone's local values
590
- between the step and the draw, and nothing returns a bone's world transform. What is
591
- readable is the posed slots: `player.posedFrame()` carries each drawn slot's world
592
- `corners` or `vertices`.
593
- - **A bounds query for the current pose.** None is declared. `ModelAssets.stage` is the
594
- stage the document states (or null), which is the rig's declared framing and not the
595
- box of the pose being drawn.
596
621
  - **Changing the skin after construction.** `skin` is a `RigPlayerOptions` field and
597
622
  `RigPlayer` declares no way to change it later; a second player over the same assets
598
623
  is the declared route.
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 RegionImage, revokeRegions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument } from './loadModel.js';
5
- export { type BoneWorld, type PoseBounds, type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } from './player.js';
5
+ export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PoseBounds, type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } 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,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChI,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,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxL,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -3819,6 +3819,81 @@ function posedBones(doc, timelines, t, plant = {}) {
3819
3819
  return bone2;
3820
3820
  });
3821
3821
  }
3822
+ const LOCAL_CHANNELS = { rotation: 0, x: 0, y: 0, scaleX: 1, scaleY: 1, shearX: 0, shearY: 0 };
3823
+ class StepLocals {
3824
+ constructor(bones, index, setup) {
3825
+ this.bones = bones;
3826
+ this.index = index;
3827
+ this.setup = setup;
3828
+ }
3829
+ read(channel2) {
3830
+ return this.bones[this.index][channel2] ?? LOCAL_CHANNELS[channel2];
3831
+ }
3832
+ write(channel2, value) {
3833
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new CoreInputError(`the adjustment wrote bone "${this.setup.name}" ${channel2} = ${String(value)}, not a finite number`);
3834
+ if (this.bones[this.index] === this.setup) this.bones[this.index] = { ...this.setup };
3835
+ this.bones[this.index][channel2] = value;
3836
+ }
3837
+ get rotation() {
3838
+ return this.read("rotation");
3839
+ }
3840
+ set rotation(v) {
3841
+ this.write("rotation", v);
3842
+ }
3843
+ get x() {
3844
+ return this.read("x");
3845
+ }
3846
+ set x(v) {
3847
+ this.write("x", v);
3848
+ }
3849
+ get y() {
3850
+ return this.read("y");
3851
+ }
3852
+ set y(v) {
3853
+ this.write("y", v);
3854
+ }
3855
+ get scaleX() {
3856
+ return this.read("scaleX");
3857
+ }
3858
+ set scaleX(v) {
3859
+ this.write("scaleX", v);
3860
+ }
3861
+ get scaleY() {
3862
+ return this.read("scaleY");
3863
+ }
3864
+ set scaleY(v) {
3865
+ this.write("scaleY", v);
3866
+ }
3867
+ get shearX() {
3868
+ return this.read("shearX");
3869
+ }
3870
+ set shearX(v) {
3871
+ this.write("shearX", v);
3872
+ }
3873
+ get shearY() {
3874
+ return this.read("shearY");
3875
+ }
3876
+ set shearY(v) {
3877
+ this.write("shearY", v);
3878
+ }
3879
+ }
3880
+ function adjustLocals(doc, bones, adjust) {
3881
+ const views = /* @__PURE__ */ new Map();
3882
+ const names = doc.bones.map((b) => b.name);
3883
+ adjust({
3884
+ names,
3885
+ bone(name) {
3886
+ let view = views.get(name);
3887
+ if (view === void 0) {
3888
+ const index = names.indexOf(name);
3889
+ if (index < 0) throw new CoreInputError(`the adjustment asked for bone ${JSON.stringify(name)}, which is not one of this document's bones`);
3890
+ view = new StepLocals(bones, index, doc.bones[index]);
3891
+ views.set(name, view);
3892
+ }
3893
+ return view;
3894
+ }
3895
+ });
3896
+ }
3822
3897
  const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3823
3898
  function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
3824
3899
  const resolve = plant.shown ?? shownAttachment;
@@ -3895,6 +3970,7 @@ function stepRows(posed) {
3895
3970
  function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
3896
3971
  const active = activeBones(doc);
3897
3972
  const bones = posedBones(doc, timelines, t, plant);
3973
+ if (step?.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
3898
3974
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
3899
3975
  if (constraints !== void 0 && step !== void 0) {
3900
3976
  const records = stepPhysicsRecords(pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, active, step.ctx, step.before);
@@ -4819,16 +4895,16 @@ function animationOf(doc, animation) {
4819
4895
  if (anim === void 0) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(", ")}]`);
4820
4896
  return anim;
4821
4897
  }
4822
- function openWalk(doc, anim, plant, reset, mode, shape) {
4898
+ function openWalk(doc, anim, plant, reset, mode, shape, adjust) {
4823
4899
  const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
4824
4900
  if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
4825
4901
  const ctx = freshStepContext(plant.physicsStep);
4826
4902
  if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
4827
- const track = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, trackTime: 0, last: -1 };
4903
+ const track = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, adjust, trackTime: 0, last: -1 };
4828
4904
  return { track, first: stepWalk(track, 0, true) };
4829
4905
  }
4830
4906
  function stepWalk(track, dt, first = false) {
4831
- const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx } = track;
4907
+ const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx, adjust } = track;
4832
4908
  track.trackTime += dt;
4833
4909
  const trackTime = track.trackTime;
4834
4910
  const last = track.last;
@@ -4836,7 +4912,7 @@ function stepWalk(track, dt, first = false) {
4836
4912
  const before = ctx.time;
4837
4913
  ctx.time += mode.clock === void 0 ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
4838
4914
  const sliders = [];
4839
- const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
4915
+ const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust });
4840
4916
  if (first) ctx.phase = "update";
4841
4917
  const placeholders = /* @__PURE__ */ new Map();
4842
4918
  const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
@@ -4856,8 +4932,8 @@ function stepWalk(track, dt, first = false) {
4856
4932
  track.last = t;
4857
4933
  return pose;
4858
4934
  }
4859
- function openRawTrack(doc, animation, plant, reset, mode) {
4860
- return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE);
4935
+ function openRawTrack(doc, animation, plant, reset, mode, adjust) {
4936
+ return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
4861
4937
  }
4862
4938
  function stepRawTrack(track, dt) {
4863
4939
  return stepWalk(track, dt);
@@ -4866,7 +4942,8 @@ const HELD = { loop: false, keep: false };
4866
4942
  const LOOPED = { loop: true, keep: false, live: true };
4867
4943
  function openTrack(doc, animation, options, plant = {}) {
4868
4944
  if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live track's options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
4869
- const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD);
4945
+ if (options.adjust !== void 0 && typeof options.adjust !== "function") throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
4946
+ const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD, options.adjust);
4870
4947
  return new Track(track, first);
4871
4948
  }
4872
4949
  class Track {
@@ -4896,7 +4973,7 @@ class Track {
4896
4973
  this.latest = pose.animationTime;
4897
4974
  return pose;
4898
4975
  } catch (err) {
4899
- if (err instanceof CoreInputError) this.refused = `${n} (${err.message})`;
4976
+ this.refused = `${n} (${err instanceof Error ? err.message : String(err)})`;
4900
4977
  throw err;
4901
4978
  }
4902
4979
  }
@@ -7221,6 +7298,11 @@ class RigPlayer {
7221
7298
  paused = false;
7222
7299
  pose = null;
7223
7300
  setup = null;
7301
+ adjustFn = null;
7302
+ /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
7303
+ adjustHook = (locals) => {
7304
+ if (this.adjustFn !== null) this.adjustFn(locals);
7305
+ };
7224
7306
  listeners = {
7225
7307
  event: /* @__PURE__ */ new Set(),
7226
7308
  complete: /* @__PURE__ */ new Set()
@@ -7234,6 +7316,7 @@ class RigPlayer {
7234
7316
  this.root = root;
7235
7317
  this.assets = assets;
7236
7318
  this.meshBackend = options.meshBackend;
7319
+ this.adjustFn = options.adjust ?? null;
7237
7320
  this.active = this.create(options.mode ?? "dom");
7238
7321
  }
7239
7322
  /**
@@ -7342,6 +7425,33 @@ class RigPlayer {
7342
7425
  if (this.pose === null) return;
7343
7426
  this.renderer.render(this.posedFrame());
7344
7427
  }
7428
+ /**
7429
+ * The per-pose adjustment of bone locals (`BoneAdjust`), or null for none.
7430
+ * It is applied by every track the player opens — `play`, the reopen inside
7431
+ * `seek` — and by the pose drawn again after `mode` changes, on every pose
7432
+ * the animation is applied to, the first one included. Setting it re-poses
7433
+ * the pose the player holds at once (`repose`), so a held or paused player
7434
+ * shows the change on its next `draw` without a step.
7435
+ */
7436
+ get adjust() {
7437
+ return this.adjustFn;
7438
+ }
7439
+ set adjust(fn) {
7440
+ if (fn !== null && typeof fn !== "function") throw new Error(`adjust must be a function or null, got ${typeof fn}`);
7441
+ this.adjustFn = fn;
7442
+ this.repose();
7443
+ }
7444
+ /**
7445
+ * Poses the held pose again at the same track time, through the current
7446
+ * `adjust`, without stepping, firing or drawing. A paused player and a zero
7447
+ * step re-pose nothing, so a caller whose adjustment reads its own state
7448
+ * (a mouth driven while the body is held) calls this when that state
7449
+ * changed, then `draw`. Nothing before `play`.
7450
+ */
7451
+ repose() {
7452
+ if (this.track === null || this.pose === null) return;
7453
+ this.pose = this.track.step(0);
7454
+ }
7345
7455
  /**
7346
7456
  * Where `name` is in the pose the player holds — the pose of the last
7347
7457
  * `advance`/`frame`/`seek`, or the setup pose before `play`. Read off the
@@ -7427,7 +7537,7 @@ class RigPlayer {
7427
7537
  this.pose = null;
7428
7538
  }
7429
7539
  open() {
7430
- this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation" });
7540
+ this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
7431
7541
  this.pose = this.track.first;
7432
7542
  }
7433
7543
  /**