@flighthq/skeleton2d 0.3.0-next.1714.3bd86f1 → 0.3.0-next.1729.f927a3b

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.
@@ -1 +1 @@
1
- {"version":3,"file":"applyAnimationClipToSkeleton2D.d.ts","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,aAAa,EACb,UAAU,EAIX,MAAM,0BAA0B,CAAC;AA0BlC,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC7B,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC3B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,IAAI,CA6CN"}
1
+ {"version":3,"file":"applyAnimationClipToSkeleton2D.d.ts","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAA6B,MAAM,0BAA0B,CAAC;AA2BrG,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC7B,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC3B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,IAAI,CAkBN"}
@@ -1,5 +1,4 @@
1
- import { sampleAnimationTrack } from '@flighthq/animation/contract';
2
- import { Skeleton2DAnimationPath, Skeleton2DSlotAnimationPath } from '@flighthq/types/contract';
1
+ import { getSkeleton2DAnimationTargetBinder } from './skeleton2dAnimationTarget';
3
2
  // Poses `pose` from an AnimationClip at `time` by COMPOSING each channel's sampled value onto the
4
3
  // corresponding `setup` bone — the 2D-skeletal analogue of @flighthq/scene3d's `applyAnimationClipToScene3D`,
5
4
  // but relative rather than absolute. This mirrors Spine's SkeletonData(setup)/Skeleton(instance) split:
@@ -9,15 +8,15 @@ import { Skeleton2DAnimationPath, Skeleton2DSlotAnimationPath } from '@flighthq/
9
8
  // is what makes clips blendable — a mixer accumulates deltas as `pose = setup + Σ wᵢ·deltaᵢ`, which averaging
10
9
  // baked-absolute poses cannot express — and is why re-applying a clip does not accumulate across frames.
11
10
  //
12
- // A clip may also carry SLOT channels (a `Skeleton2DSlotAnimationTarget` rather than a bone target). Those
13
- // are dispatched by target SHAPE and follow different rules: a slot colour is an ABSOLUTE authored value, so
14
- // it is written rather than composed, and `setup` is not consulted at all on that path. See
15
- // applySkeleton2DSlotChannel.
11
+ // One clip carries channels for several families at once — bone transforms, slot appearance, and whatever
12
+ // else has registered a binder — so this walks the channels ONCE and hands each to the binder its target
13
+ // kind names. Not every family follows the compose rule: a slot colour is an ABSOLUTE authored value that
14
+ // its binder writes without reading `setup` at all.
16
15
  //
17
- // It writes only the bones a channel targets; untouched pose bones keep their clone-of-setup values. The
18
- // caller runs `computeSkeleton2DWorldTransforms(pose, …)` (then the deform) afterward, so a whole clip
19
- // applies before one world propagation. Module scratch, allocation-free. A channel whose target is foreign
20
- // (not a Skeleton2DAnimationTarget) or out of range is skipped (sentinel guard, no throw).
16
+ // It writes only what a channel targets; untouched pose bones keep their clone-of-setup values. The caller
17
+ // runs `computeSkeleton2DWorldTransforms(pose, …)` (then the deform) afterward, so a whole clip applies
18
+ // before one world propagation. A channel whose target is foreign, unregistered, or out of range is
19
+ // skipped (sentinel guard, no throw).
21
20
  //
22
21
  // This is the documented exception to the out-param aliasing rule: `setup` is read across the whole pass
23
22
  // while `pose` is written, so they cannot be the same object. Passing one skeleton as both would clobber
@@ -28,107 +27,18 @@ export function applyAnimationClipToSkeleton2D(clip, setup, pose, time) {
28
27
  throw new Error('applyAnimationClipToSkeleton2D: setup and pose must be distinct skeletons — pass a cloneSkeleton2D(setup) as pose');
29
28
  }
30
29
  const channels = clip.channels;
31
- const setupBones = setup.bones;
32
- const poseBones = pose.bones;
33
30
  for (let i = 0; i < channels.length; i++) {
34
31
  const channel = channels[i];
35
32
  const target = channel.targetRef;
36
33
  if (target === null || typeof target !== 'object')
37
34
  continue;
38
- // `targetRef` is opaque, so the shape itself says which array a channel addresses: a bone target names
39
- // a bone index, a slot target names a slot index. Probing (rather than a discriminator field) is what
40
- // lets slot channels be purely additive to a bone-only clip format.
41
- if (typeof target.boneIndex !== 'number') {
42
- applySkeleton2DSlotChannel(channel, pose, target, time);
35
+ // `targetRef` is opaque, so the target's own kind says which family owns it. A registry rather than a
36
+ // shape probe: the two 2D-skeletal formats animate eight things that are not bone transforms, and at
37
+ // that width a probe on overlapping field names reroutes a channel silently.
38
+ const bind = getSkeleton2DAnimationTargetBinder(target.kind);
39
+ if (bind === null)
43
40
  continue;
44
- }
45
- const boneIndex = target.boneIndex;
46
- if (boneIndex < 0 || boneIndex >= poseBones.length || boneIndex >= setupBones.length)
47
- continue;
48
- sampleAnimationTrack(_scratch, channel.track, time);
49
- const setupBone = setupBones[boneIndex];
50
- const poseBone = poseBones[boneIndex];
51
- switch (target.path) {
52
- case Skeleton2DAnimationPath.Translation:
53
- poseBone.x = setupBone.x + _scratch[0];
54
- poseBone.y = setupBone.y + _scratch[1];
55
- break;
56
- case Skeleton2DAnimationPath.Rotation:
57
- poseBone.rotation = setupBone.rotation + _scratch[0];
58
- break;
59
- case Skeleton2DAnimationPath.Scale:
60
- poseBone.scaleX = setupBone.scaleX * _scratch[0];
61
- poseBone.scaleY = setupBone.scaleY * _scratch[1];
62
- break;
63
- case Skeleton2DAnimationPath.Shear:
64
- poseBone.shearX = setupBone.shearX + _scratch[0];
65
- poseBone.shearY = setupBone.shearY + _scratch[1];
66
- break;
67
- default:
68
- break;
69
- }
41
+ bind(channel, setup, pose, target, time);
70
42
  }
71
43
  }
72
- // Writes one slot channel's sampled value onto the pose slot. Unlike a bone channel this COMPOSES NOTHING:
73
- // Spine and DragonBones both author slot colour as an ABSOLUTE value, so the sample replaces the slot's
74
- // colour rather than tinting the setup colour again. There is therefore no `setup` read on this path.
75
- //
76
- // Channels are NORMALIZED 0..1 in R,G,B,A order and scaled to bytes here. The track holds 0..1 rather than
77
- // 0..255 because that is the space Spine authors colour CURVE control points in — a byte-scaled track would
78
- // rebase every colour easing against the wrong range. Samples are CLAMPED: a bezier segment may legitimately
79
- // overshoot its endpoints (anticipation curves do), and an out-of-range channel would otherwise wrap when
80
- // packed and flip the colour.
81
- function applySkeleton2DSlotChannel(channel, pose, target, time) {
82
- const slots = pose.slots;
83
- if (slots === undefined || slots === null)
84
- return;
85
- const slotIndex = target.slotIndex;
86
- if (typeof slotIndex !== 'number' || slotIndex < 0 || slotIndex >= slots.length)
87
- return;
88
- if (target.path === Skeleton2DSlotAnimationPath.Attachment) {
89
- applySkeleton2DSlotAttachment(channel, slots[slotIndex], target, time);
90
- return;
91
- }
92
- if (target.path !== Skeleton2DSlotAnimationPath.Color)
93
- return;
94
- sampleAnimationTrack(_scratch, channel.track, time);
95
- slots[slotIndex].color =
96
- ((clampColorChannel(_scratch[0]) << 24) |
97
- (clampColorChannel(_scratch[1]) << 16) |
98
- (clampColorChannel(_scratch[2]) << 8) |
99
- clampColorChannel(_scratch[3])) >>>
100
- 0;
101
- }
102
- // Swaps the slot's attachment by looking the sampled INDEX up in the target's table.
103
- //
104
- // It reads the track with its own STEP walk rather than `sampleAnimationTrack`, and that is deliberate
105
- // rather than duplication: an attachment index must never be interpolated. If a track were built (or later
106
- // edited) as Linear, sampling would blend between two TABLE INDICES and hand back something between them —
107
- // a plausible-looking index that names the wrong art, or a fractional one. Forcing the step here means the
108
- // channel is correct regardless of what the track claims its interpolation is.
109
- //
110
- // An index outside the table, or the `-1` Spine writes for "no attachment", clears the slot.
111
- function applySkeleton2DSlotAttachment(channel, slot, target, time) {
112
- const table = target.attachments;
113
- if (table === undefined || table === null)
114
- return;
115
- const times = channel.track.times;
116
- const count = times.length;
117
- if (count === 0)
118
- return;
119
- // The last keyframe at or before `time`; before the first keyframe the first value holds.
120
- let keyframe = 0;
121
- for (let i = count - 1; i >= 0; i--) {
122
- if (times[i] <= time) {
123
- keyframe = i;
124
- break;
125
- }
126
- }
127
- const index = Math.round(channel.track.values[keyframe * channel.track.components]);
128
- slot.attachment = index >= 0 && index < table.length ? table[index] : null;
129
- }
130
- function clampColorChannel(value) {
131
- return value <= 0 ? 0 : value >= 1 ? 255 : Math.round(value * 255);
132
- }
133
- const _scratch = [0, 0, 0, 0];
134
44
  //# sourceMappingURL=applyAnimationClipToSkeleton2D.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"applyAnimationClipToSkeleton2D.js","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"applyAnimationClipToSkeleton2D.js","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAC;AAEjF,kGAAkG;AAClG,8GAA8G;AAC9G,wGAAwG;AACxG,8GAA8G;AAC9G,yGAAyG;AACzG,2GAA2G;AAC3G,8GAA8G;AAC9G,yGAAyG;AACzG,EAAE;AACF,0GAA0G;AAC1G,yGAAyG;AACzG,0GAA0G;AAC1G,oDAAoD;AACpD,EAAE;AACF,2GAA2G;AAC3G,wGAAwG;AACxG,oGAAoG;AACpG,sCAAsC;AACtC,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,yGAAyG;AACzG,wGAAwG;AACxG,MAAM,UAAU,8BAA8B,CAC5C,IAA6B,EAC7B,KAA2B,EAC3B,IAAgB,EAChB,IAAY;IAEZ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,mHAAmH,CACpH,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,SAA6C,CAAC;QACrE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,SAAS;QAC5D,sGAAsG;QACtG,qGAAqG;QACrG,6EAA6E;QAC7E,MAAM,IAAI,GAAG,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS;QAC5B,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC"}
@@ -2,4 +2,5 @@ export * from './skeleton2d';
2
2
  export * from './deformMeshAttachment2D';
3
3
  export * from './regionAttachment2D';
4
4
  export * from './applyAnimationClipToSkeleton2D';
5
+ export * from './skeleton2dAnimationTarget';
5
6
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,6BAA6B,CAAC"}
package/dist/contract.js CHANGED
@@ -2,4 +2,5 @@ export * from './skeleton2d';
2
2
  export * from './deformMeshAttachment2D';
3
3
  export * from './regionAttachment2D';
4
4
  export * from './applyAnimationClipToSkeleton2D';
5
+ export * from './skeleton2dAnimationTarget';
5
6
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,6BAA6B,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { applyAnimationClipToSkeleton2D, cloneSkeleton2D, computeSkeleton2DBoneMatrices, computeSkeleton2DRegionAttachmentVertices, computeSkeleton2DWorldTransforms, createSkeleton2D, deformSkeleton2DMeshAttachment, disposeSkeleton2D, equalsSkeleton2D, getSkeleton2DBoneIndexByName, getSkeleton2DBoneWorldMatrix, setSkeleton2DBindPose, validateSkeleton2D, } from './contract';
1
+ export { applyAnimationClipToSkeleton2D, cloneSkeleton2D, computeSkeleton2DBoneMatrices, computeSkeleton2DRegionAttachmentVertices, computeSkeleton2DWorldTransforms, createSkeleton2D, createSkeleton2DBoneAnimationTarget, createSkeleton2DSlotAnimationTarget, deformSkeleton2DMeshAttachment, disposeSkeleton2D, equalsSkeleton2D, getSkeleton2DBoneIndexByName, getSkeleton2DBoneWorldMatrix, registerSkeleton2DAnimationTargetBinder, setSkeleton2DBindPose, validateSkeleton2D, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,eAAe,EACf,6BAA6B,EAC7B,yCAAyC,EACzC,gCAAgC,EAChC,gBAAgB,EAChB,8BAA8B,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,4BAA4B,EAC5B,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,eAAe,EACf,6BAA6B,EAC7B,yCAAyC,EACzC,gCAAgC,EAChC,gBAAgB,EAChB,mCAAmC,EACnC,mCAAmC,EACnC,8BAA8B,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,4BAA4B,EAC5B,uCAAuC,EACvC,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { applyAnimationClipToSkeleton2D, cloneSkeleton2D, computeSkeleton2DBoneMatrices, computeSkeleton2DRegionAttachmentVertices, computeSkeleton2DWorldTransforms, createSkeleton2D, deformSkeleton2DMeshAttachment, disposeSkeleton2D, equalsSkeleton2D, getSkeleton2DBoneIndexByName, getSkeleton2DBoneWorldMatrix, setSkeleton2DBindPose, validateSkeleton2D, } from './contract';
1
+ export { applyAnimationClipToSkeleton2D, cloneSkeleton2D, computeSkeleton2DBoneMatrices, computeSkeleton2DRegionAttachmentVertices, computeSkeleton2DWorldTransforms, createSkeleton2D, createSkeleton2DBoneAnimationTarget, createSkeleton2DSlotAnimationTarget, deformSkeleton2DMeshAttachment, disposeSkeleton2D, equalsSkeleton2D, getSkeleton2DBoneIndexByName, getSkeleton2DBoneWorldMatrix, registerSkeleton2DAnimationTargetBinder, setSkeleton2DBindPose, validateSkeleton2D, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,eAAe,EACf,6BAA6B,EAC7B,yCAAyC,EACzC,gCAAgC,EAChC,gBAAgB,EAChB,8BAA8B,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,4BAA4B,EAC5B,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,eAAe,EACf,6BAA6B,EAC7B,yCAAyC,EACzC,gCAAgC,EAChC,gBAAgB,EAChB,mCAAmC,EACnC,mCAAmC,EACnC,8BAA8B,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,4BAA4B,EAC5B,uCAAuC,EACvC,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Attachment2D, Skeleton2DAnimationPath, Skeleton2DAnimationTarget, Skeleton2DAnimationTargetBinder, Skeleton2DAnimationTargetKind, Skeleton2DSlotAnimationPath, Skeleton2DSlotAnimationTarget } from '@flighthq/types/contract';
2
+ export declare function createSkeleton2DBoneAnimationTarget(boneIndex: number, path: Skeleton2DAnimationPath): Skeleton2DAnimationTarget;
3
+ export declare function createSkeleton2DSlotAnimationTarget(slotIndex: number, path: Skeleton2DSlotAnimationPath, attachments?: readonly (Attachment2D | null)[] | null): Skeleton2DSlotAnimationTarget;
4
+ export declare function getSkeleton2DAnimationTargetBinder(kind: Skeleton2DAnimationTargetKind): Skeleton2DAnimationTargetBinder | null;
5
+ export declare function registerSkeleton2DAnimationTargetBinder(kind: Skeleton2DAnimationTargetKind, bind: Skeleton2DAnimationTargetBinder): void;
6
+ export declare function unregisterSkeleton2DAnimationTargetBinder(kind: Skeleton2DAnimationTargetKind): void;
7
+ //# sourceMappingURL=skeleton2dAnimationTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton2dAnimationTarget.d.ts","sourceRoot":"","sources":["../src/skeleton2dAnimationTarget.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,YAAY,EAEZ,uBAAuB,EACvB,yBAAyB,EACzB,+BAA+B,EAC/B,6BAA6B,EAC7B,2BAA2B,EAC3B,6BAA6B,EAE9B,MAAM,0BAA0B,CAAC;AASlC,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,uBAAuB,GAC5B,yBAAyB,CAE3B;AAID,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,2BAA2B,EACjC,WAAW,GAAE,SAAS,CAAC,YAAY,GAAG,IAAI,CAAC,EAAE,GAAG,IAAW,GAC1D,6BAA6B,CAE/B;AAID,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,6BAA6B,GAClC,+BAA+B,GAAG,IAAI,CAExC;AAMD,wBAAgB,uCAAuC,CACrD,IAAI,EAAE,6BAA6B,EACnC,IAAI,EAAE,+BAA+B,GACpC,IAAI,CAEN;AAED,wBAAgB,yCAAyC,CAAC,IAAI,EAAE,6BAA6B,GAAG,IAAI,CAEnG"}
@@ -0,0 +1,136 @@
1
+ import { sampleAnimationTrack } from '@flighthq/animation/contract';
2
+ import { Skeleton2DAnimationPath as BonePath, Skeleton2DAnimationTargetKind as TargetKind, Skeleton2DSlotAnimationPath as SlotPath, } from '@flighthq/types/contract';
3
+ // The binding target for a bone transform channel. Prefer this over a literal: `kind` is what the binder
4
+ // dispatches on, and a target that omits it binds to nothing.
5
+ export function createSkeleton2DBoneAnimationTarget(boneIndex, path) {
6
+ return { boneIndex, kind: TargetKind.Bone, path };
7
+ }
8
+ // The binding target for a slot appearance channel. `attachments` is the lookup table an Attachment
9
+ // channel's index track resolves through, and is left null on a colour channel.
10
+ export function createSkeleton2DSlotAnimationTarget(slotIndex, path, attachments = null) {
11
+ return { attachments, kind: TargetKind.Slot, path, slotIndex };
12
+ }
13
+ // The binder registered for a target kind, or null when nothing claims it — the sentinel a channel with a
14
+ // foreign or unregistered target is skipped on.
15
+ export function getSkeleton2DAnimationTargetBinder(kind) {
16
+ return _binders.get(kind) ?? null;
17
+ }
18
+ // Claims a target kind for a binder, so a family this package does not own — a constraint solver's
19
+ // channels, a vendor's own rig feature — poses through the same single pass as bones and slots instead of
20
+ // needing a second one. Last write wins, which is what lets a caller replace a built-in binding; collisions
21
+ // between third parties are avoided by the vendor-prefix convention rather than by a guard.
22
+ export function registerSkeleton2DAnimationTargetBinder(kind, bind) {
23
+ _binders.set(kind, bind);
24
+ }
25
+ export function unregisterSkeleton2DAnimationTargetBinder(kind) {
26
+ _binders.delete(kind);
27
+ }
28
+ // Composes one bone channel's sampled delta onto the setup bone. Translate, rotate and shear ADD; scale
29
+ // MULTIPLIES. Reading from `setup` rather than from `pose` is what keeps a clip blendable and what stops
30
+ // re-applying it from accumulating across frames.
31
+ function bindSkeleton2DBoneChannel(channel, setup, pose, target, time) {
32
+ const boneTarget = target;
33
+ const boneIndex = boneTarget.boneIndex;
34
+ const setupBones = setup.bones;
35
+ const poseBones = pose.bones;
36
+ if (typeof boneIndex !== 'number')
37
+ return;
38
+ if (boneIndex < 0 || boneIndex >= poseBones.length || boneIndex >= setupBones.length)
39
+ return;
40
+ sampleAnimationTrack(_scratch, channel.track, time);
41
+ const setupBone = setupBones[boneIndex];
42
+ const poseBone = poseBones[boneIndex];
43
+ switch (boneTarget.path) {
44
+ case BonePath.Translation:
45
+ poseBone.x = setupBone.x + _scratch[0];
46
+ poseBone.y = setupBone.y + _scratch[1];
47
+ break;
48
+ case BonePath.Rotation:
49
+ poseBone.rotation = setupBone.rotation + _scratch[0];
50
+ break;
51
+ case BonePath.Scale:
52
+ poseBone.scaleX = setupBone.scaleX * _scratch[0];
53
+ poseBone.scaleY = setupBone.scaleY * _scratch[1];
54
+ break;
55
+ case BonePath.Shear:
56
+ poseBone.shearX = setupBone.shearX + _scratch[0];
57
+ poseBone.shearY = setupBone.shearY + _scratch[1];
58
+ break;
59
+ default:
60
+ break;
61
+ }
62
+ }
63
+ // Writes one slot channel's sampled value onto the pose slot. Unlike a bone channel this COMPOSES NOTHING:
64
+ // Spine and DragonBones both author slot colour as an ABSOLUTE value, so the sample replaces the slot's
65
+ // colour rather than tinting the setup colour again. There is therefore no `setup` read on this path.
66
+ //
67
+ // Channels are NORMALIZED 0..1 in R,G,B,A order and scaled to bytes here. The track holds 0..1 rather than
68
+ // 0..255 because that is the space Spine authors colour CURVE control points in — a byte-scaled track would
69
+ // rebase every colour easing against the wrong range. Samples are CLAMPED: a bezier segment may legitimately
70
+ // overshoot its endpoints (anticipation curves do), and an out-of-range channel would otherwise wrap when
71
+ // packed and flip the colour.
72
+ function bindSkeleton2DSlotChannel(channel, _setup, pose, target, time) {
73
+ const slotTarget = target;
74
+ const slots = pose.slots;
75
+ if (slots === undefined || slots === null)
76
+ return;
77
+ const slotIndex = slotTarget.slotIndex;
78
+ if (typeof slotIndex !== 'number' || slotIndex < 0 || slotIndex >= slots.length)
79
+ return;
80
+ if (slotTarget.path === SlotPath.Attachment) {
81
+ bindSkeleton2DSlotAttachment(channel, slots[slotIndex], slotTarget, time);
82
+ return;
83
+ }
84
+ if (slotTarget.path !== SlotPath.Color)
85
+ return;
86
+ sampleAnimationTrack(_scratch, channel.track, time);
87
+ slots[slotIndex].color =
88
+ ((clampColorChannel(_scratch[0]) << 24) |
89
+ (clampColorChannel(_scratch[1]) << 16) |
90
+ (clampColorChannel(_scratch[2]) << 8) |
91
+ clampColorChannel(_scratch[3])) >>>
92
+ 0;
93
+ }
94
+ // Swaps the slot's attachment by looking the sampled INDEX up in the target's table.
95
+ //
96
+ // It reads the track with its own STEP walk rather than `sampleAnimationTrack`, and that is deliberate
97
+ // rather than duplication: an attachment index must never be interpolated. If a track were built (or later
98
+ // edited) as Linear, sampling would blend between two TABLE INDICES and hand back something between them —
99
+ // a plausible-looking index that names the wrong art, or a fractional one. Forcing the step here means the
100
+ // channel is correct regardless of what the track claims its interpolation is.
101
+ //
102
+ // An index outside the table, or the `-1` Spine writes for "no attachment", clears the slot.
103
+ function bindSkeleton2DSlotAttachment(channel, slot, target, time) {
104
+ const table = target.attachments;
105
+ if (table === undefined || table === null)
106
+ return;
107
+ const times = channel.track.times;
108
+ const count = times.length;
109
+ if (count === 0)
110
+ return;
111
+ // The last keyframe at or before `time`; before the first keyframe the first value holds.
112
+ let keyframe = 0;
113
+ for (let i = count - 1; i >= 0; i--) {
114
+ if (times[i] <= time) {
115
+ keyframe = i;
116
+ break;
117
+ }
118
+ }
119
+ const index = Math.round(channel.track.values[keyframe * channel.track.components]);
120
+ slot.attachment = index >= 0 && index < table.length ? table[index] : null;
121
+ }
122
+ function clampColorChannel(value) {
123
+ return value <= 0 ? 0 : value >= 1 ? 255 : Math.round(value * 255);
124
+ }
125
+ // The bone and slot binders are entered here rather than through an `enable*` call the caller has to
126
+ // remember, because posing a rig from a clip is what this package IS — a skeleton2d bundle that shed the
127
+ // bone binder would leave `applyAnimationClipToSkeleton2D` a silent no-op. Every family beyond those two
128
+ // registers explicitly and shakes out when unused, which is the part the registry buys. Filling a private
129
+ // map with this module's own functions reaches nothing outside the module, so importing it still registers
130
+ // nothing observable.
131
+ const _binders = new Map([
132
+ [TargetKind.Bone, bindSkeleton2DBoneChannel],
133
+ [TargetKind.Slot, bindSkeleton2DSlotChannel],
134
+ ]);
135
+ const _scratch = [0, 0, 0, 0];
136
+ //# sourceMappingURL=skeleton2dAnimationTarget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton2dAnimationTarget.js","sourceRoot":"","sources":["../src/skeleton2dAnimationTarget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAapE,OAAO,EACL,uBAAuB,IAAI,QAAQ,EACnC,6BAA6B,IAAI,UAAU,EAC3C,2BAA2B,IAAI,QAAQ,GACxC,MAAM,0BAA0B,CAAC;AAElC,yGAAyG;AACzG,8DAA8D;AAC9D,MAAM,UAAU,mCAAmC,CACjD,SAAiB,EACjB,IAA6B;IAE7B,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,oGAAoG;AACpG,gFAAgF;AAChF,MAAM,UAAU,mCAAmC,CACjD,SAAiB,EACjB,IAAiC,EACjC,cAAuD,IAAI;IAE3D,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AACjE,CAAC;AAED,0GAA0G;AAC1G,gDAAgD;AAChD,MAAM,UAAU,kCAAkC,CAChD,IAAmC;IAEnC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AACpC,CAAC;AAED,mGAAmG;AACnG,0GAA0G;AAC1G,4GAA4G;AAC5G,4FAA4F;AAC5F,MAAM,UAAU,uCAAuC,CACrD,IAAmC,EACnC,IAAqC;IAErC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,yCAAyC,CAAC,IAAmC;IAC3F,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,wGAAwG;AACxG,yGAAyG;AACzG,kDAAkD;AAClD,SAAS,yBAAyB,CAChC,OAAmC,EACnC,KAA2B,EAC3B,IAAgB,EAChB,MAAe,EACf,IAAY;IAEZ,MAAM,UAAU,GAAG,MAA6C,CAAC;IACjE,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC;IAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC;IAC7B,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO;IAC1C,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,IAAI,SAAS,IAAI,UAAU,CAAC,MAAM;QAAE,OAAO;IAC7F,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,QAAQ,UAAU,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,QAAQ,CAAC,WAAW;YACvB,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM;QACR,KAAK,QAAQ,CAAC,QAAQ;YACpB,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACrD,MAAM;QACR,KAAK,QAAQ,CAAC,KAAK;YACjB,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjD,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM;QACR,KAAK,QAAQ,CAAC,KAAK;YACjB,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjD,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM;QACR;YACE,MAAM;IACV,CAAC;AACH,CAAC;AAED,2GAA2G;AAC3G,wGAAwG;AACxG,sGAAsG;AACtG,EAAE;AACF,2GAA2G;AAC3G,4GAA4G;AAC5G,6GAA6G;AAC7G,0GAA0G;AAC1G,8BAA8B;AAC9B,SAAS,yBAAyB,CAChC,OAAmC,EACnC,MAA4B,EAC5B,IAAgB,EAChB,MAAe,EACf,IAAY;IAEZ,MAAM,UAAU,GAAG,MAAiD,CAAC;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO;IAClD,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,KAAK,CAAC,MAAM;QAAE,OAAO;IACxF,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC5C,4BAA4B,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QAC1E,OAAO;IACT,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK;QAAE,OAAO;IAC/C,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACpD,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK;QACpB,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACtC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACrC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YACjC,CAAC,CAAC;AACN,CAAC;AAED,qFAAqF;AACrF,EAAE;AACF,uGAAuG;AACvG,2GAA2G;AAC3G,2GAA2G;AAC3G,2GAA2G;AAC3G,+EAA+E;AAC/E,EAAE;AACF,6FAA6F;AAC7F,SAAS,4BAA4B,CACnC,OAAmC,EACnC,IAAY,EACZ,MAA+C,EAC/C,IAAY;IAEZ,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;IACjC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO;IAClD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;IAClC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAC3B,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO;IACxB,0FAA0F;IAC1F,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,QAAQ,GAAG,CAAC,CAAC;YACb,MAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IACpF,IAAI,CAAC,UAAU,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7E,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;AACrE,CAAC;AAED,qGAAqG;AACrG,yGAAyG;AACzG,yGAAyG;AACzG,0GAA0G;AAC1G,2GAA2G;AAC3G,sBAAsB;AACtB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAiE;IACvF,CAAC,UAAU,CAAC,IAAI,EAAE,yBAAyB,CAAC;IAC5C,CAAC,UAAU,CAAC,IAAI,EAAE,yBAAyB,CAAC;CAC7C,CAAC,CAAC;AACH,MAAM,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/skeleton2d",
3
- "version": "0.3.0-next.1714.3bd86f1",
3
+ "version": "0.3.0-next.1729.f927a3b",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -36,11 +36,11 @@
36
36
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
37
  },
38
38
  "dependencies": {
39
- "@flighthq/animation": "0.3.0-next.1714.3bd86f1",
40
- "@flighthq/entity": "0.3.0-next.1714.3bd86f1",
41
- "@flighthq/geometry": "0.3.0-next.1714.3bd86f1",
42
- "@flighthq/math": "0.3.0-next.1714.3bd86f1",
43
- "@flighthq/types": "0.3.0-next.1714.3bd86f1"
39
+ "@flighthq/animation": "0.3.0-next.1729.f927a3b",
40
+ "@flighthq/entity": "0.3.0-next.1729.f927a3b",
41
+ "@flighthq/geometry": "0.3.0-next.1729.f927a3b",
42
+ "@flighthq/math": "0.3.0-next.1729.f927a3b",
43
+ "@flighthq/types": "0.3.0-next.1729.f927a3b"
44
44
  },
45
45
  "devDependencies": {
46
46
  "typescript": "^5.3.0"
@@ -10,6 +10,7 @@ import { describe, expect, it } from 'vitest';
10
10
 
11
11
  import { applyAnimationClipToSkeleton2D } from './applyAnimationClipToSkeleton2D';
12
12
  import { cloneSkeleton2D, createSkeleton2D } from './skeleton2d';
13
+ import { createSkeleton2DBoneAnimationTarget, createSkeleton2DSlotAnimationTarget } from './skeleton2dAnimationTarget';
13
14
 
14
15
  function makeBone(overrides: Partial<Bone2D> = {}): Bone2D {
15
16
  return {
@@ -37,7 +38,10 @@ describe('applyAnimationClipToSkeleton2D', () => {
37
38
  const setup = createSkeleton2D([makeBone({ rotation: 10 })]);
38
39
  const pose = cloneSkeleton2D(setup);
39
40
  const clip = createAnimationClip([
40
- createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 0, path: Skeleton2DAnimationPath.Rotation }),
41
+ createAnimationChannel(
42
+ scalarTrack([0, 1], [0, 90], 1),
43
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
44
+ ),
41
45
  ]);
42
46
  applyAnimationClipToSkeleton2D(clip, setup, pose, 0.5);
43
47
  expect(pose.bones[0].rotation).toBeCloseTo(55, 5); // setup 10 + delta 45 (linear 0→90 at t=0.5)
@@ -48,18 +52,18 @@ describe('applyAnimationClipToSkeleton2D', () => {
48
52
  const setup = createSkeleton2D([makeBone({ x: 5, y: -2, scaleX: 2, scaleY: 3, shearX: 1, shearY: 4 })]);
49
53
  const pose = cloneSkeleton2D(setup);
50
54
  const clip = createAnimationClip([
51
- createAnimationChannel(scalarTrack([0, 1], [0, 0, 10, 20], 2), {
52
- boneIndex: 0,
53
- path: Skeleton2DAnimationPath.Translation,
54
- }),
55
- createAnimationChannel(scalarTrack([0, 1], [1, 1, 3, 0.5], 2), {
56
- boneIndex: 0,
57
- path: Skeleton2DAnimationPath.Scale,
58
- }),
59
- createAnimationChannel(scalarTrack([0, 1], [0, 0, 5, 6], 2), {
60
- boneIndex: 0,
61
- path: Skeleton2DAnimationPath.Shear,
62
- }),
55
+ createAnimationChannel(
56
+ scalarTrack([0, 1], [0, 0, 10, 20], 2),
57
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Translation),
58
+ ),
59
+ createAnimationChannel(
60
+ scalarTrack([0, 1], [1, 1, 3, 0.5], 2),
61
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Scale),
62
+ ),
63
+ createAnimationChannel(
64
+ scalarTrack([0, 1], [0, 0, 5, 6], 2),
65
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Shear),
66
+ ),
63
67
  ]);
64
68
  applyAnimationClipToSkeleton2D(clip, setup, pose, 1);
65
69
  expect(pose.bones[0].x).toBeCloseTo(15, 5); // 5 + 10
@@ -74,7 +78,10 @@ describe('applyAnimationClipToSkeleton2D', () => {
74
78
  const setup = createSkeleton2D([makeBone({ rotation: 10 })]);
75
79
  const pose = cloneSkeleton2D(setup);
76
80
  const clip = createAnimationClip([
77
- createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 0, path: Skeleton2DAnimationPath.Rotation }),
81
+ createAnimationChannel(
82
+ scalarTrack([0, 1], [0, 90], 1),
83
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
84
+ ),
78
85
  ]);
79
86
  applyAnimationClipToSkeleton2D(clip, setup, pose, 1); // 10 + 90 = 100
80
87
  applyAnimationClipToSkeleton2D(clip, setup, pose, 1); // composes from setup again → still 100, not 190
@@ -84,7 +91,10 @@ describe('applyAnimationClipToSkeleton2D', () => {
84
91
  it('throws when setup and pose are the same skeleton (aliasing would corrupt the rest pose)', () => {
85
92
  const s = createSkeleton2D([makeBone({ rotation: 10 })]);
86
93
  const clip = createAnimationClip([
87
- createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 0, path: Skeleton2DAnimationPath.Rotation }),
94
+ createAnimationChannel(
95
+ scalarTrack([0, 1], [0, 90], 1),
96
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
97
+ ),
88
98
  ]);
89
99
  expect(() => applyAnimationClipToSkeleton2D(clip, s, s, 1)).toThrow(/distinct skeletons/);
90
100
  });
@@ -93,8 +103,11 @@ describe('applyAnimationClipToSkeleton2D', () => {
93
103
  const setup = createSkeleton2D([makeBone({ rotation: 7 })]);
94
104
  const pose = cloneSkeleton2D(setup);
95
105
  const clip = createAnimationClip([
96
- createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { someOtherTarget: true }),
97
- createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 9, path: Skeleton2DAnimationPath.Rotation }),
106
+ createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { kind: 'acme.Unclaimed' }),
107
+ createAnimationChannel(
108
+ scalarTrack([0, 1], [0, 90], 1),
109
+ createSkeleton2DBoneAnimationTarget(9, Skeleton2DAnimationPath.Rotation),
110
+ ),
98
111
  ]);
99
112
  applyAnimationClipToSkeleton2D(clip, setup, pose, 1);
100
113
  expect(pose.bones[0].rotation).toBe(7); // untouched
@@ -107,10 +120,10 @@ describe('applyAnimationClipToSkeleton2D slot channels', () => {
107
120
  const setup = createSkeleton2D([makeBone()], [{ attachment: null, boneIndex: 0, color: 0x112233ff, name: 's' }]);
108
121
  const pose = cloneSkeleton2D(setup);
109
122
  const clip = createAnimationClip([
110
- createAnimationChannel(createAnimationTrack({ components: 4, times: [0, 1], values: [1, 0, 0, 1, 0, 0, 1, 1] }), {
111
- path: Skeleton2DSlotAnimationPath.Color,
112
- slotIndex: 0,
113
- }),
123
+ createAnimationChannel(
124
+ createAnimationTrack({ components: 4, times: [0, 1], values: [1, 0, 0, 1, 0, 0, 1, 1] }),
125
+ createSkeleton2DSlotAnimationTarget(0, Skeleton2DSlotAnimationPath.Color),
126
+ ),
114
127
  ]);
115
128
  applyAnimationClipToSkeleton2D(clip, setup, pose, 0);
116
129
  expect(pose.slots![0].color).toBe(0xff0000ff);
@@ -123,10 +136,10 @@ describe('applyAnimationClipToSkeleton2D slot channels', () => {
123
136
  const setup = createSkeleton2D([makeBone()], [{ attachment: null, boneIndex: 0, color: 0, name: 's' }]);
124
137
  const pose = cloneSkeleton2D(setup);
125
138
  const clip = createAnimationClip([
126
- createAnimationChannel(createAnimationTrack({ components: 4, times: [0, 1], values: [0, 0, 0, 0, 1, 1, 1, 1] }), {
127
- path: Skeleton2DSlotAnimationPath.Color,
128
- slotIndex: 0,
129
- }),
139
+ createAnimationChannel(
140
+ createAnimationTrack({ components: 4, times: [0, 1], values: [0, 0, 0, 0, 1, 1, 1, 1] }),
141
+ createSkeleton2DSlotAnimationTarget(0, Skeleton2DSlotAnimationPath.Color),
142
+ ),
130
143
  ]);
131
144
  applyAnimationClipToSkeleton2D(clip, setup, pose, 0.5);
132
145
  expect(pose.slots![0].color).toBe(0x80808080); // 0.5 -> 127.5 rounds to 128 on every channel
@@ -137,10 +150,10 @@ describe('applyAnimationClipToSkeleton2D slot channels', () => {
137
150
  const setup = createSkeleton2D([makeBone()], [{ attachment: null, boneIndex: 0, color: 0, name: 's' }]);
138
151
  const pose = cloneSkeleton2D(setup);
139
152
  const clip = createAnimationClip([
140
- createAnimationChannel(createAnimationTrack({ components: 4, times: [0], values: [1.4, -0.2, 1, 1] }), {
141
- path: Skeleton2DSlotAnimationPath.Color,
142
- slotIndex: 0,
143
- }),
153
+ createAnimationChannel(
154
+ createAnimationTrack({ components: 4, times: [0], values: [1.4, -0.2, 1, 1] }),
155
+ createSkeleton2DSlotAnimationTarget(0, Skeleton2DSlotAnimationPath.Color),
156
+ ),
144
157
  ]);
145
158
  applyAnimationClipToSkeleton2D(clip, setup, pose, 0);
146
159
  expect(pose.slots![0].color).toBe(0xff00ffff);
@@ -150,10 +163,10 @@ describe('applyAnimationClipToSkeleton2D slot channels', () => {
150
163
  const setup = createSkeleton2D([makeBone()], [{ attachment: null, boneIndex: 0, color: 0x010203ff, name: 's' }]);
151
164
  const pose = cloneSkeleton2D(setup);
152
165
  const clip = createAnimationClip([
153
- createAnimationChannel(createAnimationTrack({ components: 4, times: [0], values: [1, 1, 1, 1] }), {
154
- path: Skeleton2DSlotAnimationPath.Color,
155
- slotIndex: 9,
156
- }),
166
+ createAnimationChannel(
167
+ createAnimationTrack({ components: 4, times: [0], values: [1, 1, 1, 1] }),
168
+ createSkeleton2DSlotAnimationTarget(9, Skeleton2DSlotAnimationPath.Color),
169
+ ),
157
170
  ]);
158
171
  expect(() => applyAnimationClipToSkeleton2D(clip, setup, pose, 0)).not.toThrow();
159
172
  expect(pose.slots![0].color).toBe(0x010203ff);
@@ -163,21 +176,21 @@ describe('applyAnimationClipToSkeleton2D slot channels', () => {
163
176
  expect(() => applyAnimationClipToSkeleton2D(clip, boneOnly, bonePose, 0)).not.toThrow();
164
177
  });
165
178
 
166
- it('drives bone and slot channels from ONE clip, dispatching on target shape', () => {
179
+ it('drives bone and slot channels from ONE clip, dispatching on target kind', () => {
167
180
  const setup = createSkeleton2D(
168
181
  [makeBone({ rotation: 10 })],
169
182
  [{ attachment: null, boneIndex: 0, color: 0, name: 's' }],
170
183
  );
171
184
  const pose = cloneSkeleton2D(setup);
172
185
  const clip = createAnimationClip([
173
- createAnimationChannel(createAnimationTrack({ components: 1, times: [0], values: [90] }), {
174
- boneIndex: 0,
175
- path: Skeleton2DAnimationPath.Rotation,
176
- }),
177
- createAnimationChannel(createAnimationTrack({ components: 4, times: [0], values: [1, 1, 1, 1] }), {
178
- path: Skeleton2DSlotAnimationPath.Color,
179
- slotIndex: 0,
180
- }),
186
+ createAnimationChannel(
187
+ createAnimationTrack({ components: 1, times: [0], values: [90] }),
188
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
189
+ ),
190
+ createAnimationChannel(
191
+ createAnimationTrack({ components: 4, times: [0], values: [1, 1, 1, 1] }),
192
+ createSkeleton2DSlotAnimationTarget(0, Skeleton2DSlotAnimationPath.Color),
193
+ ),
181
194
  ]);
182
195
  applyAnimationClipToSkeleton2D(clip, setup, pose, 0);
183
196
  expect(pose.bones[0].rotation).toBeCloseTo(100, 5); // bone still COMPOSES onto setup
@@ -0,0 +1,143 @@
1
+ import { createAnimationChannel, createAnimationClip, createAnimationTrack } from '@flighthq/animation/contract';
2
+ import type { Bone2D, Skeleton2D } from '@flighthq/types/contract';
3
+ import {
4
+ Skeleton2DAnimationPath,
5
+ Skeleton2DAnimationTargetKind,
6
+ Skeleton2DSlotAnimationPath,
7
+ TransformMode2D,
8
+ } from '@flighthq/types/contract';
9
+ import { describe, expect, it } from 'vitest';
10
+
11
+ import { applyAnimationClipToSkeleton2D } from './applyAnimationClipToSkeleton2D';
12
+ import { cloneSkeleton2D, createSkeleton2D } from './skeleton2d';
13
+ import {
14
+ createSkeleton2DBoneAnimationTarget,
15
+ createSkeleton2DSlotAnimationTarget,
16
+ getSkeleton2DAnimationTargetBinder,
17
+ registerSkeleton2DAnimationTargetBinder,
18
+ unregisterSkeleton2DAnimationTargetBinder,
19
+ } from './skeleton2dAnimationTarget';
20
+
21
+ describe('createSkeleton2DBoneAnimationTarget', () => {
22
+ it('stamps the bone kind, which is what the binder dispatches on', () => {
23
+ expect(createSkeleton2DBoneAnimationTarget(3, Skeleton2DAnimationPath.Rotation)).toEqual({
24
+ boneIndex: 3,
25
+ kind: Skeleton2DAnimationTargetKind.Bone,
26
+ path: Skeleton2DAnimationPath.Rotation,
27
+ });
28
+ });
29
+ });
30
+
31
+ describe('createSkeleton2DSlotAnimationTarget', () => {
32
+ it('stamps the slot kind and leaves the attachment table null for a colour channel', () => {
33
+ expect(createSkeleton2DSlotAnimationTarget(2, Skeleton2DSlotAnimationPath.Color)).toEqual({
34
+ attachments: null,
35
+ kind: Skeleton2DAnimationTargetKind.Slot,
36
+ path: Skeleton2DSlotAnimationPath.Color,
37
+ slotIndex: 2,
38
+ });
39
+ });
40
+
41
+ it('carries the lookup table an attachment channel resolves its indices through', () => {
42
+ const table = [null];
43
+
44
+ expect(createSkeleton2DSlotAnimationTarget(0, Skeleton2DSlotAnimationPath.Attachment, table).attachments).toBe(
45
+ table,
46
+ );
47
+ });
48
+ });
49
+
50
+ describe('getSkeleton2DAnimationTargetBinder', () => {
51
+ it('resolves the bone and slot binders without any caller opting in', () => {
52
+ expect(getSkeleton2DAnimationTargetBinder(Skeleton2DAnimationTargetKind.Bone)).not.toBeNull();
53
+ expect(getSkeleton2DAnimationTargetBinder(Skeleton2DAnimationTargetKind.Slot)).not.toBeNull();
54
+ });
55
+
56
+ it('returns null for a kind nothing has claimed', () => {
57
+ expect(getSkeleton2DAnimationTargetBinder('acme.RopeTarget')).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe('registerSkeleton2DAnimationTargetBinder', () => {
62
+ it('binds a family this package does not own from the same single pass as bones', () => {
63
+ const seen: number[] = [];
64
+ registerSkeleton2DAnimationTargetBinder('acme.RopeTarget', (channel, _setup, _pose, target, time) => {
65
+ seen.push(channel.track.times.length, (target as { slack: number }).slack, time);
66
+ });
67
+ const setup = makeSkeleton();
68
+ const pose = cloneSkeleton2D(setup);
69
+ const clip = createAnimationClip([
70
+ createAnimationChannel(
71
+ track([0, 1], [0, 90], 1),
72
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
73
+ ),
74
+ createAnimationChannel(track([0], [0], 1), { kind: 'acme.RopeTarget', slack: 7 }),
75
+ ]);
76
+
77
+ applyAnimationClipToSkeleton2D(clip, setup, pose, 1);
78
+ unregisterSkeleton2DAnimationTargetBinder('acme.RopeTarget');
79
+
80
+ // The bone channel still posed, so registering a foreign family adds a lane rather than replacing one.
81
+ expect(pose.bones[0].rotation).toBeCloseTo(90, 5);
82
+ expect(seen).toEqual([1, 7, 1]);
83
+ });
84
+
85
+ it('takes the last registration for a kind, so a caller can replace a built-in binding', () => {
86
+ const original = getSkeleton2DAnimationTargetBinder(Skeleton2DAnimationTargetKind.Bone)!;
87
+ let called = 0;
88
+ registerSkeleton2DAnimationTargetBinder(Skeleton2DAnimationTargetKind.Bone, () => {
89
+ called++;
90
+ });
91
+ const setup = makeSkeleton();
92
+ const pose = cloneSkeleton2D(setup);
93
+ const clip = createAnimationClip([
94
+ createAnimationChannel(
95
+ track([0, 1], [0, 90], 1),
96
+ createSkeleton2DBoneAnimationTarget(0, Skeleton2DAnimationPath.Rotation),
97
+ ),
98
+ ]);
99
+
100
+ applyAnimationClipToSkeleton2D(clip, setup, pose, 1);
101
+ registerSkeleton2DAnimationTargetBinder(Skeleton2DAnimationTargetKind.Bone, original);
102
+
103
+ expect(called).toBe(1);
104
+ expect(pose.bones[0].rotation).toBe(0);
105
+ });
106
+ });
107
+
108
+ describe('unregisterSkeleton2DAnimationTargetBinder', () => {
109
+ it('releases a kind, after which its channels are skipped rather than throwing', () => {
110
+ registerSkeleton2DAnimationTargetBinder('acme.RopeTarget', () => {});
111
+ unregisterSkeleton2DAnimationTargetBinder('acme.RopeTarget');
112
+ const setup = makeSkeleton();
113
+ const pose = cloneSkeleton2D(setup);
114
+ const clip = createAnimationClip([createAnimationChannel(track([0], [0], 1), { kind: 'acme.RopeTarget' })]);
115
+
116
+ expect(() => applyAnimationClipToSkeleton2D(clip, setup, pose, 0)).not.toThrow();
117
+ expect(getSkeleton2DAnimationTargetBinder('acme.RopeTarget')).toBeNull();
118
+ });
119
+ });
120
+
121
+ function makeBone(): Bone2D {
122
+ return {
123
+ length: 0,
124
+ name: null,
125
+ parentIndex: -1,
126
+ rotation: 0,
127
+ scaleX: 1,
128
+ scaleY: 1,
129
+ shearX: 0,
130
+ shearY: 0,
131
+ transformMode: TransformMode2D.Normal,
132
+ x: 0,
133
+ y: 0,
134
+ };
135
+ }
136
+
137
+ function makeSkeleton(): Skeleton2D {
138
+ return createSkeleton2D([makeBone()], [{ attachment: null, boneIndex: 0, color: 0xffffffff, name: 's' }]);
139
+ }
140
+
141
+ function track(times: number[], values: number[], components: number) {
142
+ return createAnimationTrack({ components, times, values });
143
+ }