rig-play 1.9.2 → 1.10.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/README.md CHANGED
@@ -131,9 +131,11 @@ default stays `dom`.
131
131
  not expressible this way and stays out of scope
132
132
  - ✅ DPR-aware mesh canvas backing store: `renderer.pixelRatio` (defaults to
133
133
  `devicePixelRatio`; if you scale the root element, fold that scale in)
134
- - ✅ Dirty-skip: a mesh whose canvas-space vertices didn't change reuses last frame's
135
- raster — the CSS translate still tracks it, so parts that hold a pose (or move by
136
- whole pixels) pay zero raster. 10 frozen spineboys: WebKit ~141 → ~0.5 ms/frame
134
+ - ✅ Dirty-skip: a mesh whose canvas-space vertices match the last raster reuses it;
135
+ whole-pixel translations allow at most half a Float32 ULP at the current world
136
+ coordinate, strictly less than half a pixel. CSS still tracks the translation, while
137
+ stationary axes stay exact and the comparison anchor changes only on a redraw.
138
+ 10 frozen spineboys: WebKit ~141 → ~0.5 ms/frame
137
139
  - ✅ Clipping attachments as element-level CSS `clip-path`: spine-core's semantics
138
140
  (one active clip at a time, applied from the clip's slot through its end slot
139
141
  inclusive), with the polygon expressed in each element's own local frame — the
@@ -213,6 +215,92 @@ player.dispose();
213
215
  assets.dispose();
214
216
  ```
215
217
 
218
+ ### A minimal page
219
+
220
+ Layout is the caller's, so the first page is the place to see it: the root
221
+ element is the rig's world origin, Y up, and scaling it is a CSS transform the
222
+ player cannot see. This one loads a build, fits its stage box to a container,
223
+ tells the renderer about the scale, and plays the first animation:
224
+
225
+ ```html
226
+ <!doctype html>
227
+ <meta charset="utf-8">
228
+ <div id="stage" style="position: relative; width: 480px; height: 640px; overflow: hidden"></div>
229
+ <script type="module">
230
+ import { loadModel, RigPlayer } from 'https://cdn.jsdelivr.net/npm/@rig-c/play@1/dist/index.js';
231
+
232
+ const stage = document.getElementById('stage');
233
+ const assets = await loadModel({ modelUrl: 'rigs/look/skeleton.model.json' });
234
+ const root = stage.appendChild(document.createElement('div'));
235
+ const player = new RigPlayer(root, assets);
236
+ player.play(assets.animations[0].name);
237
+
238
+ // Fit the rig's stage box (world units, Y up) to the container: scale it, and
239
+ // put the origin where the box's left edge and top land.
240
+ const box = assets.stage ?? player.bounds();
241
+ const scale = Math.min(stage.clientWidth / box.width, stage.clientHeight / box.height);
242
+ root.style.cssText = `position: absolute; transform-origin: 0 0; transform: scale(${scale});
243
+ left: ${-box.x * scale}px; top: ${(box.y + box.height) * scale}px`;
244
+ player.renderer.syncPixelRatio(); // raster at the size on screen, not the unscaled one
245
+
246
+ let last = performance.now();
247
+ requestAnimationFrame(function tick(now) {
248
+ player.frame((now - last) / 1000);
249
+ last = now;
250
+ requestAnimationFrame(tick);
251
+ });
252
+ </script>
253
+ ```
254
+
255
+ `syncPixelRatio()` is what makes the scale cheap: without it the mesh canvases
256
+ raster at the device pixel ratio of the *unscaled* rig, `1 / scale` times more
257
+ pixels than the screen shows. Call it again after anything that changes the
258
+ scale. `tests/readme-page.spec.ts` runs this block as written, against the
259
+ package built from this tree.
260
+
261
+ ### Wind, gravity and dragging the rig
262
+
263
+ A rig's physics links answer the world the page sets (#121). `environment()`
264
+ gives them spine-core's skeleton-level wind and gravity — x right, **y down**,
265
+ the runtime's axes; each constraint's authored `wind` / `gravity` is its
266
+ response, so a link stating `wind: 0` feels none — and `moved()` tells them the
267
+ page moved or turned the rig, in the skeleton's world (y up), so a dragged rig
268
+ trails its cloth. Both apply from the next step, at any time. Added to the
269
+ minimal page above:
270
+
271
+ ```ts
272
+ // A wind slider, -1 (left) to 1 (right). The runtime starts at wind [1, 0], so set calm first.
273
+ const wind = stage.appendChild(Object.assign(document.createElement('input'),
274
+ { type: 'range', min: '-1', max: '1', step: '0.01', value: '0' }));
275
+ player.environment({ wind: [0, 0] });
276
+ wind.addEventListener('input', () => player.environment({ wind: [Number(wind.value), 0] }));
277
+
278
+ // Drag the rig: move the root, and tell the physics how far, in world units with y up.
279
+ let held = null; // the pointer's position while a drag is held
280
+ stage.addEventListener('pointerdown', (e) => { if (e.target !== wind) held = { x: e.clientX, y: e.clientY }; });
281
+ addEventListener('pointerup', () => { held = null; });
282
+ stage.addEventListener('pointermove', (e) => {
283
+ if (!held) return;
284
+ const dx = e.clientX - held.x, dy = e.clientY - held.y;
285
+ held = { x: e.clientX, y: e.clientY };
286
+ root.style.left = `${parseFloat(root.style.left) + dx}px`;
287
+ root.style.top = `${parseFloat(root.style.top) + dy}px`;
288
+ player.moved({ x: dx / scale, y: -dy / scale });
289
+ });
290
+ ```
291
+
292
+ The block is plain JavaScript, so it runs inside the page as written.
293
+ `environment(null)` returns to the runtime's start values, and `environment()`
294
+ reads what stands. The state is the player's, not a track's: every track and
295
+ layer answers the same world, and a `seek` replays under the environment as it
296
+ stands — no history is kept. A rig with no physics ignores both.
297
+ `tests/readme-page.spec.ts` runs this block in the minimal page.
298
+
299
+ The demo's `wind.html` is the same thing with controls: wind and gravity
300
+ sliders, a drag on the stage, and the rig named by `?model=` (the `cloth` test
301
+ rig by default; `bun run dev`, then `/wind.html?model=rigs/<name>/skeleton.model.json`).
302
+ `tests/wind-page.spec.ts` holds it.
303
+
216
304
  The player owns one track: `play(name, { loop })` opens an animation at its first
217
305
  pose, `frame(dt)` steps it (or `advance(dt)` then `draw()`, to time the two
218
306
  apart), `pause()` / `resume()` hold and release it, `timeScale` scales every
@@ -1 +1 @@
1
- {"version":3,"file":"DomRenderer.d.ts","sourceRoot":"","sources":["../src/DomRenderer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DomRenderer.d.ts","sourceRoot":"","sources":["../src/DomRenderer.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend, type MeshSourceRect } 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 SlotFilter, 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 RigEnvironment, 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,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,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"}
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,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,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,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtS,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1741,6 +1741,7 @@ function slotBonePlan(bones, active, records, skipped) {
1741
1741
  });
1742
1742
  return plan;
1743
1743
  }
1744
+ const RUNTIME_RAD = RUNTIME_PI / 180;
1744
1745
  const PHYSICS_FIELDS = [
1745
1746
  "bone",
1746
1747
  "scaleY",
@@ -1949,6 +1950,30 @@ function resetPhysicsState(s, time) {
1949
1950
  s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
1950
1951
  s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
1951
1952
  }
1953
+ const DEFAULT_PHYSICS_ENVIRONMENT = Object.freeze({ wind: Object.freeze([1, 0]), gravity: Object.freeze([0, 1]) });
1954
+ function translatePhysics(ctx, x, y) {
1955
+ for (const s of ctx.states.values()) {
1956
+ s.ux -= x;
1957
+ s.uy -= y;
1958
+ s.cx -= x;
1959
+ s.cy -= y;
1960
+ }
1961
+ }
1962
+ function rotatePhysics(ctx, x, y, degrees) {
1963
+ const r = degrees * RUNTIME_RAD;
1964
+ const cos = Math.cos(r);
1965
+ const sin = Math.sin(r);
1966
+ for (const s of ctx.states.values()) {
1967
+ const dx = s.cx - x;
1968
+ const dy = s.cy - y;
1969
+ const tx = dx * cos - dy * sin - dx;
1970
+ const ty = dx * sin + dy * cos - dy;
1971
+ s.ux -= tx;
1972
+ s.uy -= ty;
1973
+ s.cx -= tx;
1974
+ s.cy -= ty;
1975
+ }
1976
+ }
1952
1977
  function physicsState(ctx, name) {
1953
1978
  let s = ctx.states.get(name);
1954
1979
  if (s === void 0) {
@@ -1975,6 +2000,7 @@ function stepPhysics(r, w, length, ctx) {
1975
2000
  s.lastTime = ctx.time;
1976
2001
  const bx = w.worldX;
1977
2002
  const by = w.worldY;
2003
+ const env = ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
1978
2004
  let z = 0;
1979
2005
  if (s.pending) {
1980
2006
  s.pending = false;
@@ -2005,16 +2031,18 @@ function stepPhysics(r, w, length, ctx) {
2005
2031
  const e = r.strength;
2006
2032
  const wind = r.wind * f;
2007
2033
  const g = r.gravity * f;
2034
+ const ax = wind * env.wind[0] + g * env.gravity[0];
2035
+ const ay = wind * env.wind[1] + g * env.gravity[1];
2008
2036
  const xs = s.xOffset;
2009
2037
  const ys = s.yOffset;
2010
2038
  do {
2011
2039
  if (x) {
2012
- s.xVelocity += (wind - s.xOffset * e) * m;
2040
+ s.xVelocity += (ax - s.xOffset * e) * m;
2013
2041
  s.xOffset += s.xVelocity * t;
2014
2042
  s.xVelocity *= d;
2015
2043
  }
2016
2044
  if (y) {
2017
- s.yVelocity -= (g + s.yOffset * e) * m;
2045
+ s.yVelocity -= (ay + s.yOffset * e) * m;
2018
2046
  s.yOffset += s.yVelocity * t;
2019
2047
  s.yVelocity *= d;
2020
2048
  }
@@ -2061,20 +2089,20 @@ function stepPhysics(r, w, length, ctx) {
2061
2089
  if (d === -1) d = Math.pow(r.damping, 60 * t);
2062
2090
  const m = r.massInverse * t;
2063
2091
  const e = r.strength;
2064
- const wind = r.wind;
2065
- const g = r.gravity;
2092
+ const ax = r.wind * env.wind[0] + r.gravity * env.gravity[0];
2093
+ const ay = r.wind * env.wind[1] + r.gravity * env.gravity[1];
2066
2094
  const h = l / f;
2067
2095
  const rs = s.rotateOffset;
2068
2096
  const ss = s.scaleOffset;
2069
2097
  for (; ; ) {
2070
2098
  a -= t;
2071
2099
  if (scaleX) {
2072
- s.scaleVelocity += (wind * c - g * sn - s.scaleOffset * e) * m;
2100
+ s.scaleVelocity += (ax * c - ay * sn - s.scaleOffset * e) * m;
2073
2101
  s.scaleOffset += s.scaleVelocity * t;
2074
2102
  s.scaleVelocity *= d;
2075
2103
  }
2076
2104
  if (rotateOrShearX) {
2077
- s.rotateVelocity -= ((wind * sn + g * c) * h + s.rotateOffset * e) * m;
2105
+ s.rotateVelocity -= ((ax * sn + ay * c) * h + s.rotateOffset * e) * m;
2078
2106
  s.rotateOffset += s.rotateVelocity * t;
2079
2107
  s.rotateVelocity *= d;
2080
2108
  if (a < t) break;
@@ -4938,6 +4966,38 @@ function checkedClock(clock, who) {
4938
4966
  return moved;
4939
4967
  };
4940
4968
  }
4969
+ function setEnvironmentOn(ctx, env, who) {
4970
+ if (typeof env !== "object" || env === null || Array.isArray(env)) throw new CoreInputError(`${who} setEnvironment was given ${env === null ? "null" : Array.isArray(env) ? "an array" : typeof env}, not an object { wind?: [x, y], gravity?: [x, y] }`);
4971
+ const given = env;
4972
+ for (const key of Object.keys(given)) {
4973
+ if (key !== "wind" && key !== "gravity") throw new CoreInputError(`${who} setEnvironment names "${key}", which is not one of [wind, gravity]`);
4974
+ }
4975
+ const pair = (key) => {
4976
+ const v = given[key];
4977
+ if (v === void 0) return void 0;
4978
+ if (!Array.isArray(v) || v.length !== 2 || v.some((n) => typeof n !== "number" || !Number.isFinite(n))) throw new CoreInputError(`${who} setEnvironment's ${key} is ${JSON.stringify(v)}, not a pair of finite numbers [x, y] (x to the right, y down)`);
4979
+ return Object.freeze([v[0], v[1]]);
4980
+ };
4981
+ const wind = pair("wind");
4982
+ const gravity = pair("gravity");
4983
+ const standing = ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
4984
+ ctx.env = Object.freeze({ wind: wind ?? standing.wind, gravity: gravity ?? standing.gravity });
4985
+ }
4986
+ function environmentOf(ctx) {
4987
+ return ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
4988
+ }
4989
+ function translateOn(ctx, x, y, who) {
4990
+ for (const [k, v] of [["x", x], ["y", y]]) {
4991
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsTranslate's ${k} is ${String(v)}, not a finite distance in the skeleton's world`);
4992
+ }
4993
+ translatePhysics(ctx, x, y);
4994
+ }
4995
+ function rotateOn(ctx, x, y, degrees, who) {
4996
+ for (const [k, v] of [["x", x], ["y", y], ["degrees", degrees]]) {
4997
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsRotate's ${k} is ${String(v)}, not a finite number`);
4998
+ }
4999
+ rotatePhysics(ctx, x, y, degrees);
5000
+ }
4941
5001
  function loopedTime(trackTime, duration) {
4942
5002
  return duration === 0 ? 0 : trackTime % duration;
4943
5003
  }
@@ -5065,6 +5125,25 @@ class Track {
5065
5125
  get duration() {
5066
5126
  return this.track.duration;
5067
5127
  }
5128
+ get environment() {
5129
+ return environmentOf(this.track.ctx);
5130
+ }
5131
+ setEnvironment(environment) {
5132
+ this.live("setEnvironment");
5133
+ setEnvironmentOn(this.track.ctx, environment, "the live track's");
5134
+ }
5135
+ physicsTranslate(x, y) {
5136
+ this.live("physicsTranslate");
5137
+ translateOn(this.track.ctx, x, y, "the live track's");
5138
+ }
5139
+ physicsRotate(x, y, degrees) {
5140
+ this.live("physicsRotate");
5141
+ rotateOn(this.track.ctx, x, y, degrees, "the live track's");
5142
+ }
5143
+ /** A refused track refuses every call that would move it, naming the step it was refused at. */
5144
+ live(call) {
5145
+ if (this.refused !== null) throw new CoreInputError(`the live track was refused at step ${this.refused}, after its time and physics had moved; open the track again (${call} not made)`);
5146
+ }
5068
5147
  step(dt) {
5069
5148
  const n = this.steps + 1;
5070
5149
  if (this.refused !== null) throw new CoreInputError(`the live track was refused at step ${this.refused}, after its time and physics had moved; open the track again`);
@@ -5988,6 +6067,21 @@ class Tracks {
5988
6067
  get trackTime() {
5989
6068
  return this.time;
5990
6069
  }
6070
+ get environment() {
6071
+ return environmentOf(this.path.ctx);
6072
+ }
6073
+ setEnvironment(environment) {
6074
+ this.live("setEnvironment");
6075
+ setEnvironmentOn(this.path.ctx, environment, "the live tracks'");
6076
+ }
6077
+ physicsTranslate(x, y) {
6078
+ this.live("physicsTranslate");
6079
+ translateOn(this.path.ctx, x, y, "the live tracks'");
6080
+ }
6081
+ physicsRotate(x, y, degrees) {
6082
+ this.live("physicsRotate");
6083
+ rotateOn(this.path.ctx, x, y, degrees, "the live tracks'");
6084
+ }
5991
6085
  live(call) {
5992
6086
  if (this.refused !== null) throw new CoreInputError(`the live tracks were refused at ${this.refused}, after their entries and physics had moved; open them again (${call} not made)`);
5993
6087
  }
@@ -6909,6 +7003,17 @@ function clipNum(v) {
6909
7003
  return Math.round(v * 1e3) / 1e3;
6910
7004
  }
6911
7005
  const CLIP_COORD_LIMIT = 1e9;
7006
+ function float32HalfUlp(value) {
7007
+ if (!Number.isFinite(value)) return 0;
7008
+ return 2 ** (Math.max(-149, Math.floor(Math.log2(Math.abs(value))) - 23) - 1);
7009
+ }
7010
+ function sameMeshCoordinate(relative, cached, world, offset) {
7011
+ if (relative === cached) return true;
7012
+ if (offset === 0) return false;
7013
+ const tolerance = float32HalfUlp(world);
7014
+ const residual = Math.abs(relative - cached);
7015
+ return residual < 0.5 && residual <= tolerance;
7016
+ }
6912
7017
  const BLEND_CSS = {
6913
7018
  Normal: "",
6914
7019
  Additive: "plus-lighter",
@@ -7288,18 +7393,24 @@ class DomRenderer {
7288
7393
  dirty = true;
7289
7394
  }
7290
7395
  const rel = view.meshVertices;
7291
- for (let v = 0; v < count; v += 2) {
7292
- const x = vertices[v] - minX;
7293
- const y = vertices[v + 1] - minY;
7294
- if (rel[v] !== x || rel[v + 1] !== y) dirty = true;
7295
- rel[v] = x;
7296
- rel[v + 1] = y;
7396
+ if (!dirty) {
7397
+ const offsetX = minX - view.meshOriginX;
7398
+ const offsetY = minY - view.meshOriginY;
7399
+ for (let v = 0; v < count && !dirty; v += 2) {
7400
+ dirty = !sameMeshCoordinate(vertices[v] - minX, rel[v], vertices[v], offsetX) || !sameMeshCoordinate(vertices[v + 1] - minY, rel[v + 1], vertices[v + 1], offsetY);
7401
+ }
7297
7402
  }
7298
7403
  this.setTransform(view, `translate(${minX}px,${minY}px)`);
7299
7404
  this.applyClip(view, 1, 0, 0, 1, minX, minY, view.canvasW / ratio, view.canvasH / ratio);
7300
7405
  if (!dirty) {
7301
7406
  this.meshReuseCount++;
7302
7407
  } else {
7408
+ for (let v = 0; v < count; v += 2) {
7409
+ rel[v] = vertices[v] - minX;
7410
+ rel[v + 1] = vertices[v + 1] - minY;
7411
+ }
7412
+ view.meshOriginX = minX;
7413
+ view.meshOriginY = minY;
7303
7414
  view.meshUvs = uvs;
7304
7415
  view.meshTriangles = triangles;
7305
7416
  view.meshPage = page;
@@ -7668,6 +7779,8 @@ class DomRenderer {
7668
7779
  meshVertexCount: -1,
7669
7780
  meshBackendDrawn: "",
7670
7781
  meshVertices: new Float64Array(0),
7782
+ meshOriginX: 0,
7783
+ meshOriginY: 0,
7671
7784
  drawnIn: 0
7672
7785
  };
7673
7786
  this.views.set(slot, view);
@@ -8448,6 +8561,7 @@ function frameOf(source, pose, color = [1, 1, 1, 1], draws = () => true) {
8448
8561
  }
8449
8562
  return { order };
8450
8563
  }
8564
+ const DEFAULT_ENVIRONMENT = Object.freeze({ wind: Object.freeze([1, 0]), gravity: Object.freeze([0, 1]) });
8451
8565
  function predicateOf(names) {
8452
8566
  const allowed = new Set(names);
8453
8567
  return (slot) => allowed.has(slot);
@@ -8474,6 +8588,8 @@ class RigPlayer {
8474
8588
  read;
8475
8589
  skinName;
8476
8590
  seekStep;
8591
+ /** The environment the page set, or null for the runtime's start values (`environment`). */
8592
+ env = null;
8477
8593
  /**
8478
8594
  * The single live track (`openTrack`): what a plain `play` on track 0 runs
8479
8595
  * on, as every 1.x player did — one animation, every construct the core
@@ -8601,6 +8717,7 @@ class RigPlayer {
8601
8717
  this.loop = loop;
8602
8718
  this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
8603
8719
  this.pose = this.track.first;
8720
+ this.applyEnvironment(this.track);
8604
8721
  return;
8605
8722
  }
8606
8723
  const tracks = this.compose();
@@ -8700,9 +8817,58 @@ class RigPlayer {
8700
8817
  this.tracks = tracks;
8701
8818
  this.track = null;
8702
8819
  this.pose = tracks.start();
8820
+ this.applyEnvironment(tracks);
8703
8821
  if (time > 0) this.stepTo(time);
8704
8822
  return tracks;
8705
8823
  }
8824
+ /**
8825
+ * The world the rig's physics answers (#121): `wind` and `gravity`, as
8826
+ * spine-core's skeleton-level `windX/windY` and `gravityX/gravityY` — x to
8827
+ * the right and **y down**, the runtime's axes — to which each physics
8828
+ * constraint's authored `wind` / `gravity` is the response. The runtime's
8829
+ * start values are `{ wind: [1, 0], gravity: [0, 1] }`; a player never given
8830
+ * one poses exactly as it did without this method. A half left out keeps its
8831
+ * standing value, `null` returns both to the start values, and the call
8832
+ * applies from the next step on, at any time, with no ramp of the player's
8833
+ * own — the springs are the smoothing. It is the player's, not a track's:
8834
+ * every track and layer answers the same world, and a `seek` or a replay
8835
+ * poses under the environment as it stands (no history is kept). Returns the
8836
+ * environment now standing.
8837
+ */
8838
+ environment(environment) {
8839
+ if (environment === null) {
8840
+ this.env = null;
8841
+ this.stepper()?.setEnvironment(DEFAULT_ENVIRONMENT);
8842
+ } else if (environment !== void 0) {
8843
+ const live = this.stepper();
8844
+ if (live) {
8845
+ live.setEnvironment(environment);
8846
+ this.env = live.environment;
8847
+ } else {
8848
+ const base = this.env ?? DEFAULT_ENVIRONMENT;
8849
+ this.env = { wind: environment.wind ?? base.wind, gravity: environment.gravity ?? base.gravity };
8850
+ }
8851
+ }
8852
+ return this.stepper()?.environment ?? this.env ?? DEFAULT_ENVIRONMENT;
8853
+ }
8854
+ /**
8855
+ * The page moved or turned the whole rig since the last step (#121), in the
8856
+ * skeleton's world — units, y up, degrees counter-clockwise — so physics
8857
+ * sees it the way spine-core's `Skeleton.physicsTranslate` /
8858
+ * `physicsRotate(0, 0, Δ)` make it: a rig dragged across the page trails
8859
+ * its cloth. The root element's layout stays the caller's; this tells the
8860
+ * physics what the caller did to it (a page-pixel delta divided by the
8861
+ * root's scale, its y negated). Applies to the next step; before `play`
8862
+ * there is nothing to move.
8863
+ */
8864
+ moved(motion) {
8865
+ const live = this.stepper();
8866
+ if (!live) return;
8867
+ const x = motion.x ?? 0;
8868
+ const y = motion.y ?? 0;
8869
+ if (x !== 0 || y !== 0) live.physicsTranslate(x, y);
8870
+ if (motion.rotation) live.physicsRotate(0, 0, motion.rotation);
8871
+ }
8706
8872
  /** Stops stepping; `frame` keeps drawing the pose it holds. */
8707
8873
  pause() {
8708
8874
  this.paused = true;
@@ -8735,10 +8901,20 @@ class RigPlayer {
8735
8901
  this.trackCount = 1;
8736
8902
  this.tracks = tracks;
8737
8903
  this.pose = tracks.start();
8904
+ this.applyEnvironment(tracks);
8738
8905
  return;
8739
8906
  }
8740
8907
  this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
8741
8908
  this.pose = this.track.first;
8909
+ this.applyEnvironment(this.track);
8910
+ }
8911
+ /** What is open: the tracks, the single track, or nothing before `play`. */
8912
+ stepper() {
8913
+ return this.tracks ?? this.track;
8914
+ }
8915
+ /** A newly opened track or tracks answer the environment the player stands in; one never set leaves it untouched. */
8916
+ applyEnvironment(live) {
8917
+ if (this.env !== null) live.setEnvironment(this.env);
8742
8918
  }
8743
8919
  /** Steps whatever is open forward to `time` in `seekStep` increments, the last one shorter. */
8744
8920
  stepTo(time) {