@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.
- package/dist/applyAnimationClipToSkeleton2D.d.ts.map +1 -1
- package/dist/applyAnimationClipToSkeleton2D.js +15 -105
- package/dist/applyAnimationClipToSkeleton2D.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/skeleton2dAnimationTarget.d.ts +7 -0
- package/dist/skeleton2dAnimationTarget.d.ts.map +1 -0
- package/dist/skeleton2dAnimationTarget.js +136 -0
- package/dist/skeleton2dAnimationTarget.js.map +1 -0
- package/package.json +6 -6
- package/src/applyAnimationClipToSkeleton2D.test.ts +55 -42
- package/src/skeleton2dAnimationTarget.test.ts +143 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applyAnimationClipToSkeleton2D.d.ts","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
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":"
|
|
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"}
|
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -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
package/dist/contract.js.map
CHANGED
|
@@ -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
|
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,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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/skeleton2d",
|
|
3
|
-
"version": "0.3.0-next.
|
|
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.
|
|
40
|
-
"@flighthq/entity": "0.3.0-next.
|
|
41
|
-
"@flighthq/geometry": "0.3.0-next.
|
|
42
|
-
"@flighthq/math": "0.3.0-next.
|
|
43
|
-
"@flighthq/types": "0.3.0-next.
|
|
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(
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
createAnimationChannel(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
createAnimationChannel(
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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(
|
|
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), {
|
|
97
|
-
createAnimationChannel(
|
|
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(
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
141
|
-
|
|
142
|
-
|
|
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(
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
createAnimationChannel(
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|