rig-play 1.0.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 +187 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +318 -9
- package/dist/index.js.map +1 -1
- package/dist/player.d.ts +103 -0
- package/dist/player.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -245,6 +245,53 @@ frame takes the one-`clip-path`-on-the-root path), and a mesh's `uvs` and
|
|
|
245
245
|
`triangles` are the same arrays from frame to frame for the same texture
|
|
246
246
|
mapping (a new array re-rasters the mesh).
|
|
247
247
|
|
|
248
|
+
### Reading the pose
|
|
249
|
+
|
|
250
|
+
Two reads off the pose the player holds — the last `frame`/`advance`/`seek`,
|
|
251
|
+
or the setup pose before `play` — for framing a stage around a rig:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const head = player.bone('head'); // { x, y, a, b, c, d, rotationX, rotationY, scaleX, scaleY, active }
|
|
255
|
+
const box = player.bounds(); // { x, y, width, height } | null — world units, Y up
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`bone` is the core's world transform for that bone (`rig-c/core`'s `RawBone`,
|
|
259
|
+
unchanged), and an unknown name is refused by name. `bounds` is the box of
|
|
260
|
+
every region's four corners and every mesh's vertices in the frame record the
|
|
261
|
+
renderer takes; a slot under a clip counts only inside the clip polygon's own
|
|
262
|
+
box, an inverse clip removes nothing, and bounding boxes, paths and points do
|
|
263
|
+
not draw and do not count. Both cost nothing until called and read the same
|
|
264
|
+
numbers in `dom` and `canvas` mode.
|
|
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
|
+
|
|
248
295
|
### One load, several players
|
|
249
296
|
|
|
250
297
|
One `loadModel` backs any number of players: the regions are cut once, and
|
|
@@ -493,6 +540,146 @@ on-device (Chromium, dpr 2, stage under `scale(0.25)`, ~2.2 M CSS px on screen):
|
|
|
493
540
|
| 92 meshes | `devicePixelRatio` (2) | 141.1 Mpx | 36–39 |
|
|
494
541
|
| 92 meshes | `dpr × zoom` (0.5) | 9.1 Mpx | 60–61 |
|
|
495
542
|
|
|
543
|
+
## Coming from spine-html 0.8
|
|
544
|
+
|
|
545
|
+
This is not a drop-in upgrade. 0.8 drew a Spine export (`.json` or `.skel`, plus its
|
|
546
|
+
`.atlas`) that `@esotericsoftware/spine-core` parsed and posed: the caller built the
|
|
547
|
+
`Skeleton` and the `AnimationState`, advanced them each frame, and handed the skeleton to
|
|
548
|
+
`renderer.render(skeleton)`. 1.0 loads one file, the `skeleton.model.json` that
|
|
549
|
+
`rigc build` writes, poses it through `rig-c/core` (bundled into the package, with no peer
|
|
550
|
+
dependency), and `RigPlayer` owns the track, so the caller's frame is `player.frame(dt)`.
|
|
551
|
+
A Spine export is refused by name before anything is fetched: the data has to be a rigc
|
|
552
|
+
build, and this package declares no loader for anything else.
|
|
553
|
+
|
|
554
|
+
What did not move: a positioned root element is still the rig's origin (Y up) with its
|
|
555
|
+
layout and scaling left to the caller, the DOM tier keeps its knobs under a new class
|
|
556
|
+
name, and unloading is still elements first, bitmaps second.
|
|
557
|
+
|
|
558
|
+
### 0.8 → 1.0
|
|
559
|
+
|
|
560
|
+
| 0.8.0 (`spine-html`) | 1.0.0 (`rig-play`) | Note |
|
|
561
|
+
| --- | --- | --- |
|
|
562
|
+
| `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. |
|
|
563
|
+
| `loadSkeletonAssets(options)` | `loadModel(options)` | One URL instead of two. The `.atlas` and `skeleton.json` a build writes beside the document are not fetched. |
|
|
564
|
+
| `LoadSkeletonAssetsOptions.atlasUrl`, `skeletonUrl` | `LoadModelOptions.modelUrl` | |
|
|
565
|
+
| `resolvePage(pageName, atlasUrl)` | `resolvePage(pageName, modelUrl)` | The default now resolves a page name against the model URL's directory. |
|
|
566
|
+
| `crossOrigin`, `fetch` | `crossOrigin`, `fetch` | Unchanged. |
|
|
567
|
+
| `scale` (`LoadSkeletonAssetsOptions`, `LoadSkeletonJsonOptions`) | none | See below. |
|
|
568
|
+
| `SkeletonAssets` | `ModelAssets` | |
|
|
569
|
+
| `SkeletonAssets.data` | `ModelAssets.document`, `animations`, `skins`, `stage`, `spec` | `document` is opaque (`ModelDocument`); the player poses it. What a caller used to read off the skeleton data is stated as `animations` (name and duration), `skins` (names) and `stage` (the stage the build declared, or null). |
|
|
570
|
+
| `SkeletonAssets.atlas` | `ModelAssets.pageImages` | The page images, keyed by page name. There is no atlas object; where each region sits is in the document's `pages` section. |
|
|
571
|
+
| `SkeletonAssets.regionImages` | `ModelAssets.regionImages` | Same `Map<string, RegionImage>`. |
|
|
572
|
+
| `SkeletonAssets.dispose()`, `AtlasAssets.dispose()` | `ModelAssets.dispose()` | Same contract: frees the blob URLs the load minted, idempotent, called after the players are disposed. |
|
|
573
|
+
| `loadAtlasAssets` + `loadSkeletonJson` (one atlas, several skeletons) | none | One `loadModel` backs any number of `RigPlayer`s of the same rig. Two rigs are two loads, and each cuts its own regions. See below. |
|
|
574
|
+
| `loadSkeletonBinary` from `spine-html/binary` | none | 1.0.0 has one entry point. A `.skel` URL is refused by name. |
|
|
575
|
+
| `unpackRegions(atlas, pageImages)` | `unpackRegions(pages, pageImages)` | The first argument is the document's `pages` section (`readonly PageLayout[]`) instead of a `TextureAtlas`. |
|
|
576
|
+
| `revokeRegions(images)` | `revokeRegions(images)` | Unchanged. |
|
|
577
|
+
| `RegionImage` | `RegionImage` | Unchanged (`url`, `width`, `height`, in atlas units). |
|
|
578
|
+
| `DomTexture` | none | Not exported; there is no atlas page to attach a texture to. |
|
|
579
|
+
| `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
|
|
580
|
+
| `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. |
|
|
581
|
+
| `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. |
|
|
582
|
+
| 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. |
|
|
583
|
+
| holding a pose (the caller stopped advancing) | `player.pause()`, `player.resume()` | `frame` keeps drawing the held pose. |
|
|
584
|
+
| moving to a time (the caller's own, on the spine-core side) | `player.seek(time)`, `RigPlayerOptions.seekStep`, `player.time` | A seek reopens the animation and steps forward in `seekStep` increments (1/60 s by default). Events and completes crossed on the way are not fired. |
|
|
585
|
+
| events (the caller's own listener on the spine-core side) | `player.on('event', listener)`, `player.on('complete', listener)` | `'event'` hands a `RigEvent` (`name`, `time`, `int`, `float`, `string`); `'complete'` hands `{ animation }`. `on` returns a function that removes the listener. |
|
|
586
|
+
| skin (set by the caller on its `Skeleton`) | `RigPlayerOptions.skin` | A constructor option. The names are in `ModelAssets.skins`. Without it the rig is posed with no skin set. |
|
|
587
|
+
| `SpineHtmlRenderer` | `DomRenderer`, reached as `player.renderer` | In `mode: 'dom'`, the default. `mode: 'canvas'` is new and makes `player.renderer` a `CanvasRenderer`. |
|
|
588
|
+
| `new SpineHtmlRenderer(root, regionImages)` | `new DomRenderer(root, regionImages, pages?)` | Only for drawing your own frames; `RigPlayer` constructs its renderer. `pages` is the page-image map the mesh tier samples (`ModelAssets.pageImages`). A hand-built `regionImages` map is still accepted. |
|
|
589
|
+
| `renderer.render(skeleton)` | `renderer.render(frame)` | Takes a `PosedFrame`, not a skeleton. `player.posedFrame()` returns the one the player would draw. |
|
|
590
|
+
| `renderer.pixelRatio` | `player.renderer.pixelRatio` | On both renderers. Carried over when `player.mode` changes. |
|
|
591
|
+
| `renderer.syncPixelRatio()` | `DomRenderer.syncPixelRatio()` | DOM mode only. `player.renderer` is typed as either renderer, so narrow it (`instanceof DomRenderer`) before calling. |
|
|
592
|
+
| `renderer.meshBackingPixels` | `DomRenderer.meshBackingPixels` | DOM mode only. |
|
|
593
|
+
| `renderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | `DomRenderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | Same values (`'canvas2d'` default, `'webgl'`). Also settable up front as `RigPlayerOptions.meshBackend`. |
|
|
594
|
+
| `renderer.triangleExpand` | `DomRenderer.triangleExpand` | Same default (0.5). |
|
|
595
|
+
| `renderer.clipping` | `player.renderer.clipping` | On both renderers. Carried over when `player.mode` changes. |
|
|
596
|
+
| `clipCount`, `clipSkipCount`, `clipWriteCount` | same names on `DomRenderer` | `clipSkipCount` counts less than it did: a second clip met while one is in force, and a clip on an inactive bone, no longer reach the frame, so they are not counted. |
|
|
597
|
+
| `meshCount`, `meshReuseCount`, `canvasReallocCount`, `triangleCount` | same names on `DomRenderer` | |
|
|
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`. |
|
|
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. |
|
|
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. |
|
|
604
|
+
|
|
605
|
+
### No counterpart in 1.0.0
|
|
606
|
+
|
|
607
|
+
Things a 0.8 caller could do because it held the spine-core objects, or through a 0.8
|
|
608
|
+
export, for which 1.0.0 declares nothing:
|
|
609
|
+
|
|
610
|
+
- **A Spine export as input.** `loadSkeletonJson`, `loadSkeletonBinary`, `loadAtlasAssets`,
|
|
611
|
+
the `spine-html/binary` entry and `DomTexture` have no counterpart, and neither does
|
|
612
|
+
choosing which spine-core generation reads the data. `loadModel` reads a compiled model
|
|
613
|
+
document and nothing else.
|
|
614
|
+
- **Scaling the skeleton as it is read.** `LoadModelOptions` declares no `scale`. Scaling
|
|
615
|
+
the root element is still the caller's.
|
|
616
|
+
- **One unpacked atlas shared by different skeletons.** A document is one rig, and each
|
|
617
|
+
`loadModel` cuts its own regions and owns its own blob URLs. Sharing is per rig: one
|
|
618
|
+
load, several players.
|
|
619
|
+
- **More than one track, mixing between animations, or queueing one after another.**
|
|
620
|
+
`RigPlayer` declares one track; `play` reopens it at the animation's first pose.
|
|
621
|
+
- **Changing the skin after construction.** `skin` is a `RigPlayerOptions` field and
|
|
622
|
+
`RigPlayer` declares no way to change it later; a second player over the same assets
|
|
623
|
+
is the declared route.
|
|
624
|
+
- **Changing one slot from code** (its attachment, its colour). `RigPlayer` declares only
|
|
625
|
+
the whole-rig `color`. The frame record is a plain object, so a caller that draws its
|
|
626
|
+
own frames through `DomRenderer.render` or `CanvasRenderer.render` decides what is in
|
|
627
|
+
it.
|
|
628
|
+
- **Choosing how physics is stepped each frame.** 0.8's loop passed `Physics.update`
|
|
629
|
+
itself; 1.0.0 declares no physics option. `play` and `seek` reset physics at the
|
|
630
|
+
animation's first pose and every step advances it.
|
|
631
|
+
- **Track callbacks other than the two declared.** `on` accepts `'event'` and
|
|
632
|
+
`'complete'`.
|
|
633
|
+
|
|
634
|
+
Knobs on `DomRenderer` that `CanvasRenderer` does not declare:
|
|
635
|
+
|
|
636
|
+
- `syncPixelRatio()`. `CanvasRenderer.pixelRatio` exists and is set by hand
|
|
637
|
+
(`devicePixelRatio * rootScale`).
|
|
638
|
+
- `meshBackend` and `meshBackendActive`. The canvas mode has its own pair, `backend` and
|
|
639
|
+
`backendActive` (type `CanvasBackend`, `'webgl'` by default).
|
|
640
|
+
- `triangleExpand`, `meshBackingPixels`, `clipWriteCount`, `meshCount`, `meshReuseCount`,
|
|
641
|
+
`triangleCount`. The canvas mode's counters are `drawCalls`, `blendFlushes`,
|
|
642
|
+
`trianglesDrawn`, `framesReused` and `blits`.
|
|
643
|
+
|
|
644
|
+
Both declare `pixelRatio`, `clipping`, `clipCount`, `clipSkipCount`, `canvasReallocCount`,
|
|
645
|
+
`render` and `dispose`. Only `pixelRatio` and `clipping` carry over when `player.mode`
|
|
646
|
+
changes.
|
|
647
|
+
|
|
648
|
+
### Before and after
|
|
649
|
+
|
|
650
|
+
0.8.0:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
import { Skeleton, AnimationState, AnimationStateData, Physics } from '@esotericsoftware/spine-core';
|
|
654
|
+
import { loadSkeletonAssets, SpineHtmlRenderer } from 'spine-html';
|
|
655
|
+
const assets = await loadSkeletonAssets({ atlasUrl: '/hero/hero.atlas', skeletonUrl: '/hero/hero.json' });
|
|
656
|
+
const skeleton = new Skeleton(assets.data);
|
|
657
|
+
const state = new AnimationState(new AnimationStateData(assets.data));
|
|
658
|
+
state.setAnimation(0, 'walk', true);
|
|
659
|
+
const renderer = new SpineHtmlRenderer(rootElement, assets.regionImages);
|
|
660
|
+
function frame(delta: number) {
|
|
661
|
+
state.update(delta);
|
|
662
|
+
state.apply(skeleton);
|
|
663
|
+
skeleton.update(delta);
|
|
664
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
665
|
+
renderer.render(skeleton);
|
|
666
|
+
}
|
|
667
|
+
renderer.dispose(); assets.dispose(); // on unload
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
1.0.0:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import { loadModel, RigPlayer } from 'rig-play';
|
|
674
|
+
const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
|
|
675
|
+
const player = new RigPlayer(rootElement, assets);
|
|
676
|
+
player.play('walk', { loop: true });
|
|
677
|
+
function frame(delta: number) {
|
|
678
|
+
player.frame(delta);
|
|
679
|
+
}
|
|
680
|
+
player.dispose(); assets.dispose(); // on unload
|
|
681
|
+
```
|
|
682
|
+
|
|
496
683
|
## Demo (this repository)
|
|
497
684
|
|
|
498
685
|
```bash
|
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 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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,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);
|
|
@@ -4515,6 +4591,8 @@ function rawNumber(v) {
|
|
|
4515
4591
|
function constraintRecords(doc) {
|
|
4516
4592
|
return doc.constraints.flatMap((c) => c.record === void 0 ? [] : [c.record]);
|
|
4517
4593
|
}
|
|
4594
|
+
const NOT_ADMITTED = [];
|
|
4595
|
+
const POSED_BLOCKS = ["setup.bones", "setup.slots", "setup.drawOrder", "setup.attachments", "setup.clips", "setup.clipped"];
|
|
4518
4596
|
const activeOfView = /* @__PURE__ */ new WeakMap();
|
|
4519
4597
|
function activeBones(doc) {
|
|
4520
4598
|
const kept = activeOfView.get(doc);
|
|
@@ -4582,6 +4660,97 @@ function resolveShown(doc, slotName, placeholder) {
|
|
|
4582
4660
|
const agree = rows.every((r) => r.name === rows[0].name && r.path === rows[0].path);
|
|
4583
4661
|
return agree ? filling[0] : { conflict: rows.map((r) => ({ skin: r.skin, shown: r.name, path: r.path })) };
|
|
4584
4662
|
}
|
|
4663
|
+
function poseSetup(doc, plant = {}, physics) {
|
|
4664
|
+
const evaluate = plant.evaluate ?? worldTransforms;
|
|
4665
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
4666
|
+
const colour = plant.colour ?? readColour;
|
|
4667
|
+
const blend = plant.blend ?? readBlend;
|
|
4668
|
+
const round = plant.round ?? gridRound;
|
|
4669
|
+
const bonesWhy = constraintsAbsentWhy(doc, solverRules(plant.solver));
|
|
4670
|
+
let bones = null;
|
|
4671
|
+
let setupWorld = null;
|
|
4672
|
+
const applied = [];
|
|
4673
|
+
if (bonesWhy === null) {
|
|
4674
|
+
const active = activeBones(doc);
|
|
4675
|
+
const records = constraintRecords(doc);
|
|
4676
|
+
setupWorld = applyConstraints(doc.bones, evaluate(doc.bones, active), active, plant.constraints ? plant.constraints(records) : records, null, applied, physics, void 0, solverRules(plant.solver));
|
|
4677
|
+
const world = setupWorld;
|
|
4678
|
+
bones = doc.bones.map((b) => {
|
|
4679
|
+
const t = world.get(b.name);
|
|
4680
|
+
if (t === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
4681
|
+
return [b.name, round(t.worldX), round(t.worldY), round(t.a), round(t.b), round(t.c), round(t.d), active.has(b.name) ? 1 : 0, b.parent ?? null];
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
const conflicts = [];
|
|
4685
|
+
const slotRows = [];
|
|
4686
|
+
const liveBones = activeBones(doc);
|
|
4687
|
+
const slotGate = plant.slotTimelines ?? slotTimelinesApply;
|
|
4688
|
+
for (const slot of doc.slots) {
|
|
4689
|
+
const pose = {
|
|
4690
|
+
placeholder: slot.setup,
|
|
4691
|
+
light: slot.color === void 0 ? [1, 1, 1, 1] : [...colour(slot.color)],
|
|
4692
|
+
dark: slot.dark === void 0 ? null : readColour(slot.dark).slice(0, 3)
|
|
4693
|
+
};
|
|
4694
|
+
if (slotGate(doc, slot, liveBones)) applySliderSlots(slot.name, pose, applied);
|
|
4695
|
+
const posedRecord = { ...slot, setup: pose.placeholder };
|
|
4696
|
+
const shown = pose.placeholder === null ? null : resolve(doc, posedRecord);
|
|
4697
|
+
if (shown !== null && "conflict" in shown) {
|
|
4698
|
+
conflicts.push(`slot "${slot.name}" placeholder "${pose.placeholder}" is filled by skins ${shown.conflict.map((c) => `"${c.skin}" (shows ${JSON.stringify(c.shown)}, path ${JSON.stringify(c.path)})`).join(", ")}`);
|
|
4699
|
+
continue;
|
|
4700
|
+
}
|
|
4701
|
+
const row = shown === null ? null : shownRow(shown);
|
|
4702
|
+
const [r, g, b, a] = pose.light;
|
|
4703
|
+
const dark = pose.dark;
|
|
4704
|
+
slotRows.push([
|
|
4705
|
+
slot.name,
|
|
4706
|
+
row === null ? null : row.name,
|
|
4707
|
+
round(r),
|
|
4708
|
+
round(g),
|
|
4709
|
+
round(b),
|
|
4710
|
+
round(a),
|
|
4711
|
+
dark === null ? null : [round(dark[0]), round(dark[1]), round(dark[2])],
|
|
4712
|
+
row === null ? null : row.path,
|
|
4713
|
+
blend(slot)
|
|
4714
|
+
]);
|
|
4715
|
+
}
|
|
4716
|
+
const slotsWhy = (bonesWhy === null ? null : slidersWhy(doc)) ?? (conflicts.length === 0 ? null : `${conflicts.join("; ")} — under --skin all the LAST of them in the Spine file's skin order wins, and that order is the emitter's (default first, the rest in the editor's order), not the model's; the per-skin dumps (--skin <name>) judge it`);
|
|
4717
|
+
const slots = slotsWhy === null ? slotRows : null;
|
|
4718
|
+
const upstream = [bonesWhy === null ? null : `setup.bones is absent (${bonesWhy}), and every vertex goes through a bone's world matrix`, slotsWhy === null ? null : "setup.slots is absent, so what a slot shows is not posed"].filter((x) => x !== null);
|
|
4719
|
+
const orderWhy = bonesWhy === null ? null : slidersKeyingWhy(doc, "drawOrder");
|
|
4720
|
+
let drawOrder = null;
|
|
4721
|
+
if (orderWhy === null) {
|
|
4722
|
+
let order = doc.slots.map((_s, i) => i);
|
|
4723
|
+
for (const app of applied) order = (plant.drawOrder ?? drawOrderAt)(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
4724
|
+
drawOrder = order.map((i) => doc.slots[i].name);
|
|
4725
|
+
}
|
|
4726
|
+
let attachments = null;
|
|
4727
|
+
let clips = null;
|
|
4728
|
+
let clipsWhy = upstream.length === 0 ? null : upstream.join("; ");
|
|
4729
|
+
let attachmentsWhy = clipsWhy;
|
|
4730
|
+
let clipped = null;
|
|
4731
|
+
let clippedWhy = clipsWhy ?? orderWhy;
|
|
4732
|
+
let shownInOrder = null;
|
|
4733
|
+
if (clipsWhy === null && setupWorld !== null && drawOrder !== null) {
|
|
4734
|
+
const world = setupWorld;
|
|
4735
|
+
const placeholders = new Map(doc.slots.map((sl) => [sl.name, sl.setup]));
|
|
4736
|
+
const states = attachmentStates(doc, resolve, placeholders, null, applied, plant);
|
|
4737
|
+
const rank = new Map(drawOrder.map((n, i) => [n, i]));
|
|
4738
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
4739
|
+
shownInOrder = states.why.length === 0 ? shown : null;
|
|
4740
|
+
const posed = poseGeometry(shown, world, sourceOfDoc(doc), round, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, drawOrder, plant));
|
|
4741
|
+
const stateWhy = states.why.length === 0 ? null : `${states.why.join("; ")}`;
|
|
4742
|
+
attachments = stateWhy === null ? posed.attachments : null;
|
|
4743
|
+
attachmentsWhy = stateWhy ?? posed.attachmentsWhy;
|
|
4744
|
+
clips = stateWhy === null ? posed.clips : null;
|
|
4745
|
+
clipsWhy = stateWhy;
|
|
4746
|
+
clipped = stateWhy === null ? posed.clipped : null;
|
|
4747
|
+
clippedWhy = stateWhy ?? posed.clippedWhy;
|
|
4748
|
+
}
|
|
4749
|
+
const why = { "setup.bones": bonesWhy, "setup.slots": slotsWhy, "setup.drawOrder": orderWhy, "setup.attachments": attachmentsWhy, "setup.clips": clipsWhy, "setup.clipped": clippedWhy };
|
|
4750
|
+
const absent = NOT_ADMITTED.map(([block, reason]) => [block, reason]);
|
|
4751
|
+
for (const block of POSED_BLOCKS) if (why[block] !== null) absent.push([block, why[block]]);
|
|
4752
|
+
return { setup: { bones, slots, drawOrder, attachments, clips, clipped }, absent, world: setupWorld, shown: shownInOrder };
|
|
4753
|
+
}
|
|
4585
4754
|
function drawWalkOf(doc, order, plant = {}) {
|
|
4586
4755
|
const active = activeBones(doc);
|
|
4587
4756
|
return {
|
|
@@ -4601,6 +4770,22 @@ function sourceOfDoc(doc) {
|
|
|
4601
4770
|
return g?.kind === "mesh" ? g.vertices : sourceProblem(doc.skins, skin, slot, source) ?? `the linked mesh's source "${source}" carries no vertices`;
|
|
4602
4771
|
};
|
|
4603
4772
|
}
|
|
4773
|
+
function slidersKeyingWhy(doc, group) {
|
|
4774
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
4775
|
+
if (c.kind !== "slider") return [];
|
|
4776
|
+
const anim = doc.animations.find((a) => a.name === c.animation);
|
|
4777
|
+
return anim !== void 0 && anim.timelines[group].length > 0 ? [`slider "${c.name}" applies animation "${c.animation}", which keys the draw order`] : [];
|
|
4778
|
+
});
|
|
4779
|
+
return keyed.length === 0 ? null : `${keyed.join("; ")} — a slider's time is read off the bones, which are absent`;
|
|
4780
|
+
}
|
|
4781
|
+
function slidersWhy(doc) {
|
|
4782
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
4783
|
+
if (c.kind !== "slider") return [];
|
|
4784
|
+
const slots = doc.animations.find((a) => a.name === c.animation)?.slots ?? [];
|
|
4785
|
+
return slots.length === 0 ? [] : [`slider "${c.name}" applies animation "${c.animation}", which keys slot(s) ${slots.map((x) => `"${x}"`).join(", ")}`];
|
|
4786
|
+
});
|
|
4787
|
+
return keyed.length === 0 ? null : `${keyed.join("; ")} — the oracle's setup applies sliders, and a slider poses the slots its animation keys at a time read off the bones, which are absent`;
|
|
4788
|
+
}
|
|
4604
4789
|
function drawOrderOf(shown, order) {
|
|
4605
4790
|
const rank = new Map(order.map((n, r) => [n, r]));
|
|
4606
4791
|
return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
@@ -4677,6 +4862,22 @@ function rawDrawn(doc, shown, world, order, plant, mode = RAW_MODE) {
|
|
|
4677
4862
|
const clipped = geometry.drawnClipped.map((c) => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clipped vertex of slot "${c[0]}"`, mode)), c[4].map((v) => numberOf(v, `clipped uv of slot "${c[0]}"`, mode)), c[5]]);
|
|
4678
4863
|
return { drawn, clips, clipped };
|
|
4679
4864
|
}
|
|
4865
|
+
function poseRawSetup(doc, plant = {}) {
|
|
4866
|
+
return setupPoseIn(doc, plant, RAW_MODE);
|
|
4867
|
+
}
|
|
4868
|
+
function setupPoseIn(doc, plant, mode) {
|
|
4869
|
+
const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
|
|
4870
|
+
const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
|
|
4871
|
+
return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
|
|
4872
|
+
}
|
|
4873
|
+
function setupPosed(doc, plant, mode) {
|
|
4874
|
+
const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
|
|
4875
|
+
const absent = posed.absent.filter(([block]) => block !== "setup.clipped");
|
|
4876
|
+
if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join("; ")}`);
|
|
4877
|
+
const { setup, world, shown } = posed;
|
|
4878
|
+
if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError("the raw setup pose was not posed");
|
|
4879
|
+
return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
|
|
4880
|
+
}
|
|
4680
4881
|
function loopedTime(trackTime, duration) {
|
|
4681
4882
|
return duration === 0 ? 0 : trackTime % duration;
|
|
4682
4883
|
}
|
|
@@ -4694,16 +4895,16 @@ function animationOf(doc, animation) {
|
|
|
4694
4895
|
if (anim === void 0) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(", ")}]`);
|
|
4695
4896
|
return anim;
|
|
4696
4897
|
}
|
|
4697
|
-
function openWalk(doc, anim, plant, reset, mode, shape) {
|
|
4898
|
+
function openWalk(doc, anim, plant, reset, mode, shape, adjust) {
|
|
4698
4899
|
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
4699
4900
|
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
4700
4901
|
const ctx = freshStepContext(plant.physicsStep);
|
|
4701
4902
|
if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
|
|
4702
|
-
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 };
|
|
4703
4904
|
return { track, first: stepWalk(track, 0, true) };
|
|
4704
4905
|
}
|
|
4705
4906
|
function stepWalk(track, dt, first = false) {
|
|
4706
|
-
const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx } = track;
|
|
4907
|
+
const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx, adjust } = track;
|
|
4707
4908
|
track.trackTime += dt;
|
|
4708
4909
|
const trackTime = track.trackTime;
|
|
4709
4910
|
const last = track.last;
|
|
@@ -4711,7 +4912,7 @@ function stepWalk(track, dt, first = false) {
|
|
|
4711
4912
|
const before = ctx.time;
|
|
4712
4913
|
ctx.time += mode.clock === void 0 ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
|
|
4713
4914
|
const sliders = [];
|
|
4714
|
-
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 });
|
|
4715
4916
|
if (first) ctx.phase = "update";
|
|
4716
4917
|
const placeholders = /* @__PURE__ */ new Map();
|
|
4717
4918
|
const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
|
|
@@ -4731,8 +4932,8 @@ function stepWalk(track, dt, first = false) {
|
|
|
4731
4932
|
track.last = t;
|
|
4732
4933
|
return pose;
|
|
4733
4934
|
}
|
|
4734
|
-
function openRawTrack(doc, animation, plant, reset, mode) {
|
|
4735
|
-
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);
|
|
4736
4937
|
}
|
|
4737
4938
|
function stepRawTrack(track, dt) {
|
|
4738
4939
|
return stepWalk(track, dt);
|
|
@@ -4741,7 +4942,8 @@ const HELD = { loop: false, keep: false };
|
|
|
4741
4942
|
const LOOPED = { loop: true, keep: false, live: true };
|
|
4742
4943
|
function openTrack(doc, animation, options, plant = {}) {
|
|
4743
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`);
|
|
4744
|
-
|
|
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);
|
|
4745
4947
|
return new Track(track, first);
|
|
4746
4948
|
}
|
|
4747
4949
|
class Track {
|
|
@@ -4771,7 +4973,7 @@ class Track {
|
|
|
4771
4973
|
this.latest = pose.animationTime;
|
|
4772
4974
|
return pose;
|
|
4773
4975
|
} catch (err) {
|
|
4774
|
-
|
|
4976
|
+
this.refused = `${n} (${err instanceof Error ? err.message : String(err)})`;
|
|
4775
4977
|
throw err;
|
|
4776
4978
|
}
|
|
4777
4979
|
}
|
|
@@ -7095,6 +7297,12 @@ class RigPlayer {
|
|
|
7095
7297
|
loop = true;
|
|
7096
7298
|
paused = false;
|
|
7097
7299
|
pose = null;
|
|
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
|
+
};
|
|
7098
7306
|
listeners = {
|
|
7099
7307
|
event: /* @__PURE__ */ new Set(),
|
|
7100
7308
|
complete: /* @__PURE__ */ new Set()
|
|
@@ -7108,6 +7316,7 @@ class RigPlayer {
|
|
|
7108
7316
|
this.root = root;
|
|
7109
7317
|
this.assets = assets;
|
|
7110
7318
|
this.meshBackend = options.meshBackend;
|
|
7319
|
+
this.adjustFn = options.adjust ?? null;
|
|
7111
7320
|
this.active = this.create(options.mode ?? "dom");
|
|
7112
7321
|
}
|
|
7113
7322
|
/**
|
|
@@ -7216,6 +7425,106 @@ class RigPlayer {
|
|
|
7216
7425
|
if (this.pose === null) return;
|
|
7217
7426
|
this.renderer.render(this.posedFrame());
|
|
7218
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
|
+
}
|
|
7455
|
+
/**
|
|
7456
|
+
* Where `name` is in the pose the player holds — the pose of the last
|
|
7457
|
+
* `advance`/`frame`/`seek`, or the setup pose before `play`. Read off the
|
|
7458
|
+
* core's pose, so it is the same in `dom` and `canvas` mode and costs nothing
|
|
7459
|
+
* until called. An unknown bone is refused by name.
|
|
7460
|
+
*/
|
|
7461
|
+
bone(name) {
|
|
7462
|
+
const pose = this.posed();
|
|
7463
|
+
for (const bone2 of pose.bones) {
|
|
7464
|
+
if (bone2.name !== name) continue;
|
|
7465
|
+
return {
|
|
7466
|
+
name,
|
|
7467
|
+
active: bone2.active,
|
|
7468
|
+
x: bone2.worldX,
|
|
7469
|
+
y: bone2.worldY,
|
|
7470
|
+
a: bone2.a,
|
|
7471
|
+
b: bone2.b,
|
|
7472
|
+
c: bone2.c,
|
|
7473
|
+
d: bone2.d,
|
|
7474
|
+
rotationX: bone2.rotationX,
|
|
7475
|
+
rotationY: bone2.rotationY,
|
|
7476
|
+
scaleX: bone2.scaleX,
|
|
7477
|
+
scaleY: bone2.scaleY
|
|
7478
|
+
};
|
|
7479
|
+
}
|
|
7480
|
+
throw new Error(`no bone named ${JSON.stringify(name)} in this document (bones: ${pose.bones.map((b) => b.name).join(", ")})`);
|
|
7481
|
+
}
|
|
7482
|
+
/**
|
|
7483
|
+
* The box of what the pose the player holds draws, in world units, Y up:
|
|
7484
|
+
* every region's four corners and every mesh's vertices, in the same frame
|
|
7485
|
+
* record the renderer takes (`posedFrame`). A slot under a clip counts only
|
|
7486
|
+
* inside the clip polygon's own box (an inverse clip removes nothing from
|
|
7487
|
+
* the count); bounding boxes, paths and points are not drawn and do not
|
|
7488
|
+
* count. `null` when nothing draws. The same numbers in `dom` and `canvas`
|
|
7489
|
+
* mode; nothing is computed until called.
|
|
7490
|
+
*/
|
|
7491
|
+
bounds() {
|
|
7492
|
+
const frame2 = this.pose === null ? frameOf(this.source, this.posed(), this.color) : this.posedFrame();
|
|
7493
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
7494
|
+
for (const slot of frame2.order) {
|
|
7495
|
+
const points = slot.kind === "region" ? slot.corners : slot.vertices;
|
|
7496
|
+
let lo = -Infinity, lb = -Infinity, hi = Infinity, hb = Infinity;
|
|
7497
|
+
if (slot.clip && !slot.clip.inverse) {
|
|
7498
|
+
const c = slot.clip.polygon;
|
|
7499
|
+
lo = Infinity;
|
|
7500
|
+
lb = Infinity;
|
|
7501
|
+
hi = -Infinity;
|
|
7502
|
+
hb = -Infinity;
|
|
7503
|
+
for (let i = 0; i < c.length; i += 2) {
|
|
7504
|
+
if (c[i] < lo) lo = c[i];
|
|
7505
|
+
if (c[i] > hi) hi = c[i];
|
|
7506
|
+
if (c[i + 1] < lb) lb = c[i + 1];
|
|
7507
|
+
if (c[i + 1] > hb) hb = c[i + 1];
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7510
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
7511
|
+
const x = Math.min(Math.max(points[i], lo), hi);
|
|
7512
|
+
const y = Math.min(Math.max(points[i + 1], lb), hb);
|
|
7513
|
+
if (x < minX) minX = x;
|
|
7514
|
+
if (x > maxX) maxX = x;
|
|
7515
|
+
if (y < minY) minY = y;
|
|
7516
|
+
if (y > maxY) maxY = y;
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
if (minX === Infinity) return null;
|
|
7520
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
7521
|
+
}
|
|
7522
|
+
/** The pose the reads above see: the track's, or the setup pose before `play` (posed once). */
|
|
7523
|
+
posed() {
|
|
7524
|
+
if (this.pose !== null) return this.pose;
|
|
7525
|
+
if (this.setup === null) this.setup = poseRawSetup(this.view);
|
|
7526
|
+
return this.setup;
|
|
7527
|
+
}
|
|
7219
7528
|
/** The pose the player holds, as the frame record the renderer draws. */
|
|
7220
7529
|
posedFrame() {
|
|
7221
7530
|
if (this.pose === null) return { order: [] };
|
|
@@ -7228,7 +7537,7 @@ class RigPlayer {
|
|
|
7228
7537
|
this.pose = null;
|
|
7229
7538
|
}
|
|
7230
7539
|
open() {
|
|
7231
|
-
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 });
|
|
7232
7541
|
this.pose = this.track.first;
|
|
7233
7542
|
}
|
|
7234
7543
|
/**
|