mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
[ts] Port to 4.3 (WIP)
This commit is contained in:
parent
c99cf65b32
commit
a2547097c2
@ -37,7 +37,7 @@
|
||||
|
||||
// Instantiate a new skeleton based on the atlas and skeleton data.
|
||||
skeleton = new spine.Skeleton(skeletonData);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(spine.Physics.update);
|
||||
bounds = skeleton.getBoundsRect();
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
// Instantiate a new skeleton based on the atlas and skeleton data.
|
||||
skeleton = new spine.Skeleton(skeletonData);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(spine.Physics.update);
|
||||
bounds = skeleton.getBoundsRect();
|
||||
|
||||
|
||||
@ -61,18 +61,19 @@ export class SkeletonRenderer {
|
||||
if (this.debugRendering) ctx.strokeStyle = "green";
|
||||
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
let slot = drawOrder[i];
|
||||
const slot = drawOrder[i];
|
||||
let bone = slot.bone;
|
||||
if (!bone.active) continue;
|
||||
|
||||
let attachment = slot.getAttachment();
|
||||
let pose = slot.pose;
|
||||
let attachment = pose.attachment;
|
||||
if (!(attachment instanceof RegionAttachment)) continue;
|
||||
attachment.computeWorldVertices(slot, worldVertices, 0, 2);
|
||||
let region: TextureRegion = <TextureRegion>attachment.region;
|
||||
|
||||
let image: HTMLImageElement = (<CanvasTexture>region.texture).getImage() as HTMLImageElement;
|
||||
|
||||
let slotColor = slot.color;
|
||||
let slotColor = pose.color;
|
||||
let regionColor = attachment.color;
|
||||
color.set(skeletonColor.r * slotColor.r * regionColor.r,
|
||||
skeletonColor.g * slotColor.g * regionColor.g,
|
||||
@ -80,7 +81,8 @@ export class SkeletonRenderer {
|
||||
skeletonColor.a * slotColor.a * regionColor.a);
|
||||
|
||||
ctx.save();
|
||||
ctx.transform(bone.a, bone.c, bone.b, bone.d, bone.worldX, bone.worldY);
|
||||
const boneApplied = bone.applied;
|
||||
ctx.transform(boneApplied.a, boneApplied.c, boneApplied.b, boneApplied.d, boneApplied.worldX, boneApplied.worldY);
|
||||
ctx.translate(attachment.offset[0], attachment.offset[1]);
|
||||
ctx.rotate(attachment.rotation * Math.PI / 180);
|
||||
|
||||
@ -116,8 +118,9 @@ export class SkeletonRenderer {
|
||||
let triangles: Array<number> | null = null;
|
||||
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
let slot = drawOrder[i];
|
||||
let attachment = slot.getAttachment();
|
||||
const slot = drawOrder[i];
|
||||
let pose = slot.pose;
|
||||
let attachment = pose.attachment;
|
||||
|
||||
let texture: HTMLImageElement;
|
||||
let region: TextureAtlasRegion;
|
||||
@ -137,7 +140,7 @@ export class SkeletonRenderer {
|
||||
if (texture) {
|
||||
if (slot.data.blendMode != blendMode) blendMode = slot.data.blendMode;
|
||||
|
||||
let slotColor = slot.color;
|
||||
let slotColor = pose.color;
|
||||
let attachmentColor = attachment.color;
|
||||
color.set(skeletonColor.r * slotColor.r * attachmentColor.r,
|
||||
skeletonColor.g * slotColor.g * attachmentColor.g,
|
||||
@ -225,8 +228,8 @@ export class SkeletonRenderer {
|
||||
}
|
||||
|
||||
private computeRegionVertices (slot: Slot, region: RegionAttachment, pma: boolean) {
|
||||
let skeletonColor = slot.bone.skeleton.color;
|
||||
let slotColor = slot.color;
|
||||
let skeletonColor = slot.skeleton.color;
|
||||
let slotColor = slot.pose.color;
|
||||
let regionColor = region.color;
|
||||
let alpha = skeletonColor.a * slotColor.a * regionColor.a;
|
||||
let multiplier = pma ? alpha : 1;
|
||||
@ -273,8 +276,9 @@ export class SkeletonRenderer {
|
||||
}
|
||||
|
||||
private computeMeshVertices (slot: Slot, mesh: MeshAttachment, pma: boolean) {
|
||||
let skeletonColor = slot.bone.skeleton.color;
|
||||
let slotColor = slot.color;
|
||||
let skeleton = slot.skeleton;
|
||||
let skeletonColor = skeleton.color;
|
||||
let slotColor = slot.pose.color;
|
||||
let regionColor = mesh.color;
|
||||
let alpha = skeletonColor.a * slotColor.a * regionColor.a;
|
||||
let multiplier = pma ? alpha : 1;
|
||||
@ -287,7 +291,7 @@ export class SkeletonRenderer {
|
||||
let vertexCount = mesh.worldVerticesLength / 2;
|
||||
let vertices = this.vertices;
|
||||
if (vertices.length < mesh.worldVerticesLength) this.vertices = vertices = Utils.newFloatArray(mesh.worldVerticesLength);
|
||||
mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE);
|
||||
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE);
|
||||
|
||||
let uvs = mesh.uvs;
|
||||
for (let i = 0, u = 0, v = 2; i < vertexCount; i++) {
|
||||
|
||||
@ -24,7 +24,6 @@ import {
|
||||
} from "@esotericsoftware/spine-core";
|
||||
import {
|
||||
Canvas,
|
||||
Surface,
|
||||
CanvasKit,
|
||||
Image,
|
||||
Paint,
|
||||
@ -232,11 +231,12 @@ export class SkeletonRenderer {
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
let slot = drawOrder[i];
|
||||
if (!slot.bone.active) {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
let attachment = slot.getAttachment();
|
||||
let pose = slot.pose;
|
||||
let attachment = pose.attachment;
|
||||
let positions = this.scratchPositions;
|
||||
let colors = this.scratchColors;
|
||||
let uvs: NumberArrayLike;
|
||||
@ -261,6 +261,7 @@ export class SkeletonRenderer {
|
||||
: positions;
|
||||
numVertices = mesh.worldVerticesLength >> 1;
|
||||
mesh.computeWorldVertices(
|
||||
skeleton,
|
||||
slot,
|
||||
0,
|
||||
mesh.worldVerticesLength,
|
||||
@ -274,10 +275,10 @@ export class SkeletonRenderer {
|
||||
attachmentColor = mesh.color;
|
||||
} else if (attachment instanceof ClippingAttachment) {
|
||||
let clip = attachment as ClippingAttachment;
|
||||
clipper.clipStart(slot, clip);
|
||||
clipper.clipStart(skeleton, slot, clip);
|
||||
continue;
|
||||
} else {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -294,7 +295,7 @@ export class SkeletonRenderer {
|
||||
triangles = clipper.clippedTriangles;
|
||||
}
|
||||
|
||||
let slotColor = slot.color;
|
||||
let slotColor = pose.color;
|
||||
let finalColor = this.tempColor;
|
||||
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
|
||||
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
|
||||
@ -338,7 +339,7 @@ export class SkeletonRenderer {
|
||||
vertices.delete();
|
||||
}
|
||||
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
}
|
||||
clipper.clipEnd();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -40,16 +40,13 @@ import { Event } from "./Event.js";
|
||||
*
|
||||
* See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide. */
|
||||
export class AnimationState {
|
||||
static _emptyAnimation = new Animation("<empty>", [], 0);
|
||||
private static emptyAnimation (): Animation {
|
||||
return AnimationState._emptyAnimation;
|
||||
}
|
||||
static readonly emptyAnimation = new Animation("<empty>", [], 0);
|
||||
|
||||
/** The AnimationStateData to look up mix durations. */
|
||||
data: AnimationStateData;
|
||||
|
||||
/** The list of tracks that currently have animations, which may contain null entries. */
|
||||
tracks = new Array<TrackEntry | null>();
|
||||
readonly tracks = new Array<TrackEntry | null>();
|
||||
|
||||
/** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower
|
||||
* or faster. Defaults to 1.
|
||||
@ -58,8 +55,8 @@ export class AnimationState {
|
||||
timeScale = 1;
|
||||
unkeyedState = 0;
|
||||
|
||||
events = new Array<Event>();
|
||||
listeners = new Array<AnimationStateListener>();
|
||||
readonly events = new Array<Event>();
|
||||
readonly listeners = new Array<AnimationStateListener>();
|
||||
queue = new EventQueue(this);
|
||||
propertyIDs = new StringSet();
|
||||
animationsChanged = false;
|
||||
@ -197,11 +194,11 @@ export class AnimationState {
|
||||
// to sometimes stop rendering when using color correction, as their RGBA values become NaN.
|
||||
// (https://github.com/pixijs/pixi-spine/issues/302)
|
||||
Utils.webkit602BugfixHelper(alpha, blend);
|
||||
var timeline = timelines[ii];
|
||||
const timeline = timelines[ii];
|
||||
if (timeline instanceof AttachmentTimeline)
|
||||
this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments);
|
||||
else
|
||||
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.mixIn);
|
||||
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false);
|
||||
}
|
||||
} else {
|
||||
let timelineMode = current.timelineMode;
|
||||
@ -220,7 +217,7 @@ export class AnimationState {
|
||||
} else {
|
||||
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
|
||||
Utils.webkit602BugfixHelper(alpha, blend);
|
||||
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.mixIn);
|
||||
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,13 +230,13 @@ export class AnimationState {
|
||||
// Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so
|
||||
// subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or
|
||||
// the time is before the first key).
|
||||
var setupState = this.unkeyedState + SETUP;
|
||||
var slots = skeleton.slots;
|
||||
for (var i = 0, n = skeleton.slots.length; i < n; i++) {
|
||||
var slot = slots[i];
|
||||
const setupState = this.unkeyedState + SETUP;
|
||||
const slots = skeleton.slots;
|
||||
for (let i = 0, n = skeleton.slots.length; i < n; i++) {
|
||||
const slot = slots[i];
|
||||
if (slot.attachmentState == setupState) {
|
||||
var attachmentName = slot.data.attachmentName;
|
||||
slot.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||
const attachmentName = slot.data.attachmentName;
|
||||
slot.pose.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||
}
|
||||
}
|
||||
this.unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.
|
||||
@ -275,7 +272,7 @@ export class AnimationState {
|
||||
|
||||
if (blend == MixBlend.add) {
|
||||
for (let i = 0; i < timelineCount; i++)
|
||||
timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.mixOut);
|
||||
timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out, false);
|
||||
} else {
|
||||
let timelineMode = from.timelineMode;
|
||||
let timelineHoldMix = from.timelineHoldMix;
|
||||
@ -287,7 +284,7 @@ export class AnimationState {
|
||||
from.totalAlpha = 0;
|
||||
for (let i = 0; i < timelineCount; i++) {
|
||||
let timeline = timelines[i];
|
||||
let direction = MixDirection.mixOut;
|
||||
let direction = MixDirection.out;
|
||||
let timelineBlend: MixBlend;
|
||||
let alpha = 0;
|
||||
switch (timelineMode[i]) {
|
||||
@ -308,7 +305,7 @@ export class AnimationState {
|
||||
timelineBlend = MixBlend.setup;
|
||||
alpha = alphaHold;
|
||||
break;
|
||||
default:
|
||||
default: // HOLD_MIX
|
||||
timelineBlend = MixBlend.setup;
|
||||
let holdMix = timelineHoldMix[i];
|
||||
alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
|
||||
@ -324,8 +321,8 @@ export class AnimationState {
|
||||
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
|
||||
Utils.webkit602BugfixHelper(alpha, blend);
|
||||
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
|
||||
direction = MixDirection.mixIn;
|
||||
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
|
||||
direction = MixDirection.in;
|
||||
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -339,21 +336,21 @@ export class AnimationState {
|
||||
}
|
||||
|
||||
applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, attachments: boolean) {
|
||||
var slot = skeleton.slots[timeline.slotIndex];
|
||||
const slot = skeleton.slots[timeline.slotIndex];
|
||||
if (!slot.bone.active) return;
|
||||
|
||||
if (time < timeline.frames[0]) { // Time is before first frame.
|
||||
if (blend == MixBlend.setup || blend == MixBlend.first)
|
||||
this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
|
||||
} else
|
||||
this.setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search1(timeline.frames, time)], attachments);
|
||||
this.setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments);
|
||||
|
||||
// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
|
||||
if (slot.attachmentState <= this.unkeyedState) slot.attachmentState = this.unkeyedState + SETUP;
|
||||
}
|
||||
|
||||
setAttachment (skeleton: Skeleton, slot: Slot, attachmentName: string | null, attachments: boolean) {
|
||||
slot.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||
slot.pose.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||
if (attachments) slot.attachmentState = this.unkeyedState + CURRENT;
|
||||
}
|
||||
|
||||
@ -363,27 +360,28 @@ export class AnimationState {
|
||||
if (firstFrame) timelinesRotation[i] = 0;
|
||||
|
||||
if (alpha == 1) {
|
||||
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn);
|
||||
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in, false);
|
||||
return;
|
||||
}
|
||||
|
||||
let bone = skeleton.bones[timeline.boneIndex];
|
||||
if (!bone.active) return;
|
||||
const pose = bone.pose, setup = bone.data.setup;
|
||||
let frames = timeline.frames;
|
||||
let r1 = 0, r2 = 0;
|
||||
if (time < frames[0]) {
|
||||
switch (blend) {
|
||||
case MixBlend.setup:
|
||||
bone.rotation = bone.data.rotation;
|
||||
pose.rotation = setup.rotation;
|
||||
default:
|
||||
return;
|
||||
case MixBlend.first:
|
||||
r1 = bone.rotation;
|
||||
r2 = bone.data.rotation;
|
||||
r1 = pose.rotation;
|
||||
r2 = setup.rotation;
|
||||
}
|
||||
} else {
|
||||
r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
|
||||
r2 = bone.data.rotation + timeline.getCurveValue(time);
|
||||
r1 = blend == MixBlend.setup ? setup.rotation : pose.rotation;
|
||||
r2 = setup.rotation + timeline.getCurveValue(time);
|
||||
}
|
||||
|
||||
// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
|
||||
@ -416,7 +414,7 @@ export class AnimationState {
|
||||
timelinesRotation[i] = total;
|
||||
}
|
||||
timelinesRotation[i + 1] = diff;
|
||||
bone.rotation = r1 + total * alpha;
|
||||
pose.rotation = r1 + total * alpha;
|
||||
}
|
||||
|
||||
queueEvents (entry: TrackEntry, animationTime: number) {
|
||||
@ -519,26 +517,47 @@ export class AnimationState {
|
||||
}
|
||||
|
||||
/** Sets an animation by name.
|
||||
*
|
||||
* See {@link #setAnimationWith()}. */
|
||||
setAnimation (trackIndex: number, animationName: string, loop: boolean = false) {
|
||||
let animation = this.data.skeletonData.findAnimation(animationName);
|
||||
if (!animation) throw new Error("Animation not found: " + animationName);
|
||||
return this.setAnimationWith(trackIndex, animation, loop);
|
||||
*
|
||||
* See {@link setAnimation}. */
|
||||
setAnimation (trackIndex: number, animationName: string, loop?: boolean): TrackEntry;
|
||||
|
||||
/** Sets the current animation for a track, discarding any queued animations.
|
||||
*
|
||||
* If the formerly current track entry is for the same animation and was never applied to a skeleton, it is replaced (not mixed
|
||||
* from).
|
||||
* @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
|
||||
* duration. In either case {@link TrackEntry.trackEnd} determines when the track is cleared.
|
||||
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener.dispose} event occurs. */
|
||||
setAnimation (trackIndex: number, animation: Animation, loop?: boolean): TrackEntry;
|
||||
|
||||
setAnimation (trackIndex: number, animationNameOrAnimation: string | Animation, loop = false): TrackEntry {
|
||||
if (typeof animationNameOrAnimation === "string")
|
||||
return this.setAnimation1(trackIndex, animationNameOrAnimation, loop);
|
||||
return this.setAnimation2(trackIndex, animationNameOrAnimation, loop);
|
||||
}
|
||||
|
||||
/** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
|
||||
* applied to a skeleton, it is replaced (not mixed from).
|
||||
private setAnimation1 (trackIndex: number, animationName: string, loop: boolean = false) {
|
||||
let animation = this.data.skeletonData.findAnimation(animationName);
|
||||
if (!animation) throw new Error("Animation not found: " + animationName);
|
||||
return this.setAnimation2(trackIndex, animation, loop);
|
||||
}
|
||||
|
||||
/** Sets the current animation for a track, discarding any queued animations.
|
||||
* <p>
|
||||
* If the formerly current track entry is for the same animation and was never applied to a skeleton, it is replaced (not mixed
|
||||
* from).
|
||||
* @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
|
||||
* duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared.
|
||||
* @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener#dispose()} event occurs. */
|
||||
setAnimationWith (trackIndex: number, animation: Animation, loop: boolean = false) {
|
||||
* duration. In either case {@link TrackEntry#getTrackEnd()} determines when the track is cleared.
|
||||
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
|
||||
private setAnimation2 (trackIndex: number, animation: Animation, loop: boolean = false) {
|
||||
if (trackIndex < 0) throw new Error("trackIndex must be >= 0.");
|
||||
if (!animation) throw new Error("animation cannot be null.");
|
||||
let interrupt = true;
|
||||
let current = this.expandToIndex(trackIndex);
|
||||
if (current) {
|
||||
if (current.nextTrackLast == -1) {
|
||||
if (current.nextTrackLast === -1 && current.animation === animation) {
|
||||
// Don't mix from an entry that was never applied.
|
||||
this.tracks[trackIndex] = current.mixingFrom;
|
||||
this.queue.interrupt(current);
|
||||
@ -557,22 +576,32 @@ export class AnimationState {
|
||||
|
||||
/** Queues an animation by name.
|
||||
*
|
||||
* See {@link #addAnimationWith()}. */
|
||||
addAnimation (trackIndex: number, animationName: string, loop: boolean = false, delay: number = 0) {
|
||||
let animation = this.data.skeletonData.findAnimation(animationName);
|
||||
if (!animation) throw new Error("Animation not found: " + animationName);
|
||||
return this.addAnimationWith(trackIndex, animation, loop, delay);
|
||||
}
|
||||
* See {@link addAnimation}. */
|
||||
addAnimation (trackIndex: number, animationName: string, loop?: boolean, delay?: number): TrackEntry;
|
||||
|
||||
/** Adds an animation to be played after the current or last queued animation for a track. If the track has no entries, this is
|
||||
* equivalent to calling {@link #setAnimationWith()}.
|
||||
* @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry
|
||||
* minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix
|
||||
* ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the
|
||||
* equivalent to calling {@link setAnimation}.
|
||||
* @param delay If > 0, sets {@link TrackEntry.delay}. If <= 0, the delay set is the duration of the previous track entry
|
||||
* minus any mix duration (from the {@link AnimationStateData}) plus the specified <code>delay</code> (ie the mix
|
||||
* ends at (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the
|
||||
* previous entry is looping, its next loop completion is used instead of its duration.
|
||||
* @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener#dispose()} event occurs. */
|
||||
addAnimationWith (trackIndex: number, animation: Animation, loop: boolean = false, delay: number = 0) {
|
||||
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener.dispose} event occurs. */
|
||||
addAnimation (trackIndex: number, animation: Animation, loop?: boolean, delay?: number): TrackEntry;
|
||||
|
||||
addAnimation (trackIndex: number, animationNameOrAnimation: string | Animation, loop = false, delay: number = 0): TrackEntry {
|
||||
if (typeof animationNameOrAnimation === "string")
|
||||
return this.addAnimation1(trackIndex, animationNameOrAnimation, loop, delay);
|
||||
return this.addAnimation2(trackIndex, animationNameOrAnimation, loop, delay);
|
||||
}
|
||||
|
||||
private addAnimation1 (trackIndex: number, animationName: string, loop: boolean = false, delay: number = 0) {
|
||||
let animation = this.data.skeletonData.findAnimation(animationName);
|
||||
if (!animation) throw new Error("Animation not found: " + animationName);
|
||||
return this.addAnimation2(trackIndex, animation, loop, delay);
|
||||
}
|
||||
|
||||
private addAnimation2 (trackIndex: number, animation: Animation, loop: boolean = false, delay: number = 0) {
|
||||
if (!animation) throw new Error("animation cannot be null.");
|
||||
|
||||
let last = this.expandToIndex(trackIndex);
|
||||
@ -611,11 +640,11 @@ export class AnimationState {
|
||||
* {@link TrackEntry#setMixDuration()}. Mixing from an empty animation causes the new animation to be applied more and
|
||||
* more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the
|
||||
* setup pose value if no lower tracks key the property to the value keyed in the new animation.
|
||||
* <p>
|
||||
*
|
||||
* See <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
|
||||
* Runtimes Guide. */
|
||||
setEmptyAnimation (trackIndex: number, mixDuration: number = 0) {
|
||||
let entry = this.setAnimationWith(trackIndex, AnimationState.emptyAnimation(), false);
|
||||
let entry = this.setAnimation(trackIndex, AnimationState.emptyAnimation, false);
|
||||
entry.mixDuration = mixDuration;
|
||||
entry.trackEnd = mixDuration;
|
||||
return entry;
|
||||
@ -624,7 +653,7 @@ export class AnimationState {
|
||||
/** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
|
||||
* {@link TrackEntry#getMixDuration()}. If the track has no entries, it is equivalent to calling
|
||||
* {@link #setEmptyAnimation(int, float)}.
|
||||
* <p>
|
||||
*
|
||||
* See {@link #setEmptyAnimation(int, float)} and
|
||||
* <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
|
||||
* Runtimes Guide.
|
||||
@ -635,7 +664,7 @@ export class AnimationState {
|
||||
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
|
||||
* after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
|
||||
addEmptyAnimation (trackIndex: number, mixDuration: number = 0, delay: number = 0) {
|
||||
let entry = this.addAnimationWith(trackIndex, AnimationState.emptyAnimation(), false, delay);
|
||||
let entry = this.addAnimation(trackIndex, AnimationState.emptyAnimation, false, delay);
|
||||
if (delay <= 0) entry.delay = Math.max(entry.delay + entry.mixDuration - mixDuration, 0);
|
||||
entry.mixDuration = mixDuration;
|
||||
entry.trackEnd = mixDuration;
|
||||
@ -643,7 +672,7 @@ export class AnimationState {
|
||||
}
|
||||
|
||||
/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
|
||||
* <p>
|
||||
*
|
||||
* See <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
|
||||
* Runtimes Guide. */
|
||||
setEmptyAnimations (mixDuration: number = 0) {
|
||||
@ -951,18 +980,17 @@ export class TrackEntry {
|
||||
* When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the
|
||||
* {@link #delay} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set
|
||||
* afterward. */
|
||||
_mixDuration: number = 0; interruptAlpha: number = 0; totalAlpha: number = 0;
|
||||
mixDuration: number = 0;
|
||||
|
||||
get mixDuration () {
|
||||
return this._mixDuration;
|
||||
}
|
||||
interruptAlpha: number = 0; totalAlpha: number = 0;
|
||||
|
||||
set mixDuration (mixDuration: number) {
|
||||
this._mixDuration = mixDuration;
|
||||
}
|
||||
|
||||
setMixDurationWithDelay (mixDuration: number, delay: number) {
|
||||
this._mixDuration = mixDuration;
|
||||
/** Sets both {@link #getMixDuration()} and {@link #getDelay()}.
|
||||
* @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track
|
||||
* entry minus the specified mix duration plus the specified <code>delay</code> (ie the mix ends at
|
||||
* (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the previous
|
||||
* entry is looping, its next loop completion is used instead of its duration. */
|
||||
setMixDuration (mixDuration: number, delay: number) {
|
||||
this.mixDuration = mixDuration;
|
||||
if (delay <= 0) {
|
||||
if (this.previous != null)
|
||||
delay = Math.max(delay + this.previous.getTrackComplete() - mixDuration, 0);
|
||||
@ -1096,41 +1124,41 @@ export class EventQueue {
|
||||
}
|
||||
|
||||
drain () {
|
||||
if (this.drainDisabled) return;
|
||||
if (this.drainDisabled) return; // Not reentrant.
|
||||
this.drainDisabled = true;
|
||||
|
||||
let objects = this.objects;
|
||||
let listeners = this.animState.listeners;
|
||||
const listeners = this.animState.listeners;
|
||||
const objects = this.objects;
|
||||
|
||||
for (let i = 0; i < objects.length; i += 2) {
|
||||
let type = objects[i] as EventType;
|
||||
let entry = objects[i + 1] as TrackEntry;
|
||||
const type = objects[i] as EventType;
|
||||
const entry = objects[i + 1] as TrackEntry;
|
||||
switch (type) {
|
||||
case EventType.start:
|
||||
if (entry.listener && entry.listener.start) entry.listener.start(entry);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.start) listener.start(entry);
|
||||
}
|
||||
break;
|
||||
case EventType.interrupt:
|
||||
if (entry.listener && entry.listener.interrupt) entry.listener.interrupt(entry);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.interrupt) listener.interrupt(entry);
|
||||
}
|
||||
break;
|
||||
case EventType.end:
|
||||
if (entry.listener && entry.listener.end) entry.listener.end(entry);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.end) listener.end(entry);
|
||||
}
|
||||
// Fall through.
|
||||
case EventType.dispose:
|
||||
if (entry.listener && entry.listener.dispose) entry.listener.dispose(entry);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.dispose) listener.dispose(entry);
|
||||
}
|
||||
this.animState.trackEntryPool.free(entry);
|
||||
@ -1138,15 +1166,15 @@ export class EventQueue {
|
||||
case EventType.complete:
|
||||
if (entry.listener && entry.listener.complete) entry.listener.complete(entry);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.complete) listener.complete(entry);
|
||||
}
|
||||
break;
|
||||
case EventType.event:
|
||||
let event = objects[i++ + 2] as Event;
|
||||
const event = objects[i++ + 2] as Event;
|
||||
if (entry.listener && entry.listener.event) entry.listener.event(entry, event);
|
||||
for (let ii = 0; ii < listeners.length; ii++) {
|
||||
let listener = listeners[ii];
|
||||
const listener = listeners[ii];
|
||||
if (listener.event) listener.event(entry, event);
|
||||
}
|
||||
break;
|
||||
@ -1175,11 +1203,11 @@ export enum EventType {
|
||||
* See TrackEntry {@link TrackEntry#listener} and AnimationState
|
||||
* {@link AnimationState#addListener}. */
|
||||
export interface AnimationStateListener {
|
||||
/** Invoked when this entry has been set as the current entry. {@link #end(TrackEntry)} will occur when this entry will no
|
||||
/** Invoked when this entry has been set as the current entry. {@link end} will occur when this entry will no
|
||||
* longer be applied.
|
||||
* <p>
|
||||
* When this event is triggered by calling {@link AnimationState#setAnimation}, take care not to
|
||||
* call {@link AnimationState#update} until after the TrackEntry has been configured. */
|
||||
*
|
||||
* When this event is triggered by calling {@link AnimationState.setAnimation}, take care not to
|
||||
* call {@link AnimationState.update} until after the TrackEntry has been configured. */
|
||||
start?: (entry: TrackEntry) => void;
|
||||
|
||||
/** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for
|
||||
|
||||
@ -27,413 +27,37 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { BoneData, Inherit } from "./BoneData.js";
|
||||
import { Physics, Skeleton } from "./Skeleton.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { MathUtils, Vector2 } from "./Utils.js";
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { BoneLocal } from "./BoneLocal.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { PosedActive } from "./PosedActive.js";
|
||||
|
||||
/** Stores a bone's current pose.
|
||||
/** The current pose for a bone, before constraints are applied.
|
||||
*
|
||||
* A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
|
||||
* local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
|
||||
* constraint or application code modifies the world transform after it was computed from the local transform. */
|
||||
export class Bone implements Updatable {
|
||||
/** The bone's setup pose data. */
|
||||
data: BoneData;
|
||||
|
||||
/** The skeleton this bone belongs to. */
|
||||
skeleton: Skeleton;
|
||||
|
||||
export class Bone extends PosedActive<BoneData, BoneLocal, BonePose> {
|
||||
/** The parent bone, or null if this is the root bone. */
|
||||
parent: Bone | null = null;
|
||||
|
||||
/** The immediate children of this bone. */
|
||||
children = new Array<Bone>();
|
||||
|
||||
/** The local x translation. */
|
||||
x = 0;
|
||||
|
||||
/** The local y translation. */
|
||||
y = 0;
|
||||
|
||||
/** The local rotation in degrees, counter clockwise. */
|
||||
rotation = 0;
|
||||
|
||||
/** The local scaleX. */
|
||||
scaleX = 0;
|
||||
|
||||
/** The local scaleY. */
|
||||
scaleY = 0;
|
||||
|
||||
/** The local shearX. */
|
||||
shearX = 0;
|
||||
|
||||
/** The local shearY. */
|
||||
shearY = 0;
|
||||
|
||||
/** The applied local x translation. */
|
||||
ax = 0;
|
||||
|
||||
/** The applied local y translation. */
|
||||
ay = 0;
|
||||
|
||||
/** The applied local rotation in degrees, counter clockwise. */
|
||||
arotation = 0;
|
||||
|
||||
/** The applied local scaleX. */
|
||||
ascaleX = 0;
|
||||
|
||||
/** The applied local scaleY. */
|
||||
ascaleY = 0;
|
||||
|
||||
/** The applied local shearX. */
|
||||
ashearX = 0;
|
||||
|
||||
/** The applied local shearY. */
|
||||
ashearY = 0;
|
||||
|
||||
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
a = 0;
|
||||
|
||||
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
b = 0;
|
||||
|
||||
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
c = 0;
|
||||
|
||||
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
d = 0;
|
||||
|
||||
/** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
worldY = 0;
|
||||
|
||||
/** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */
|
||||
worldX = 0;
|
||||
|
||||
inherit: Inherit = Inherit.Normal;
|
||||
|
||||
sorted = false;
|
||||
active = false;
|
||||
|
||||
/** @param parent May be null. */
|
||||
constructor (data: BoneData, skeleton: Skeleton, parent: Bone | null) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
this.data = data;
|
||||
this.skeleton = skeleton;
|
||||
constructor (data: BoneData, parent: Bone | null) {
|
||||
super(data, new BonePose(), new BonePose());
|
||||
this.parent = parent;
|
||||
this.setToSetupPose();
|
||||
this.applied.bone = this;
|
||||
this.constrained.bone = this;
|
||||
}
|
||||
|
||||
/** Returns false when the bone has not been computed because {@link BoneData#skinRequired} is true and the
|
||||
* {@link Skeleton#skin active skin} does not {@link Skin#bones contain} this bone. */
|
||||
isActive () {
|
||||
return this.active;
|
||||
/** Make a copy of the bone. Does not copy the {@link #getChildren()} bones. */
|
||||
copy (parent: Bone | null): Bone {
|
||||
const copy = new Bone(this.data, parent);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** Computes the world transform using the parent bone and this bone's local applied transform. */
|
||||
update (physics: Physics | null) {
|
||||
this.updateWorldTransformWith(this.ax, this.ay, this.arotation, this.ascaleX, this.ascaleY, this.ashearX, this.ashearY);
|
||||
}
|
||||
|
||||
/** Computes the world transform using the parent bone and this bone's local transform.
|
||||
*
|
||||
* See {@link #updateWorldTransformWith()}. */
|
||||
updateWorldTransform () {
|
||||
this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY);
|
||||
}
|
||||
|
||||
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
|
||||
* specified local transform. Child bones are not updated.
|
||||
*
|
||||
* See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
|
||||
* Runtimes Guide. */
|
||||
updateWorldTransformWith (x: number, y: number, rotation: number, scaleX: number, scaleY: number, shearX: number, shearY: number) {
|
||||
this.ax = x;
|
||||
this.ay = y;
|
||||
this.arotation = rotation;
|
||||
this.ascaleX = scaleX;
|
||||
this.ascaleY = scaleY;
|
||||
this.ashearX = shearX;
|
||||
this.ashearY = shearY;
|
||||
|
||||
let parent = this.parent;
|
||||
if (!parent) { // Root bone.
|
||||
let skeleton = this.skeleton;
|
||||
const sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
this.a = Math.cos(rx) * scaleX * sx;
|
||||
this.b = Math.cos(ry) * scaleY * sx;
|
||||
this.c = Math.sin(rx) * scaleX * sy;
|
||||
this.d = Math.sin(ry) * scaleY * sy;
|
||||
this.worldX = x * sx + skeleton.x;
|
||||
this.worldY = y * sy + skeleton.y;
|
||||
return;
|
||||
}
|
||||
|
||||
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
|
||||
this.worldX = pa * x + pb * y + parent.worldX;
|
||||
this.worldY = pc * x + pd * y + parent.worldY;
|
||||
|
||||
switch (this.inherit) {
|
||||
case Inherit.Normal: {
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
const la = Math.cos(rx) * scaleX;
|
||||
const lb = Math.cos(ry) * scaleY;
|
||||
const lc = Math.sin(rx) * scaleX;
|
||||
const ld = Math.sin(ry) * scaleY;
|
||||
this.a = pa * la + pb * lc;
|
||||
this.b = pa * lb + pb * ld;
|
||||
this.c = pc * la + pd * lc;
|
||||
this.d = pc * lb + pd * ld;
|
||||
return;
|
||||
}
|
||||
case Inherit.OnlyTranslation: {
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
this.a = Math.cos(rx) * scaleX;
|
||||
this.b = Math.cos(ry) * scaleY;
|
||||
this.c = Math.sin(rx) * scaleX;
|
||||
this.d = Math.sin(ry) * scaleY;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoRotationOrReflection: {
|
||||
let sx = 1 / this.skeleton.scaleX, sy = 1 / this.skeleton.scaleY;
|
||||
pa *= sx;
|
||||
pc *= sy;
|
||||
let s = pa * pa + pc * pc;
|
||||
let prx = 0;
|
||||
if (s > 0.0001) {
|
||||
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
|
||||
pb = pc * s;
|
||||
pd = pa * s;
|
||||
prx = Math.atan2(pc, pa) * MathUtils.radDeg;
|
||||
} else {
|
||||
pa = 0;
|
||||
pc = 0;
|
||||
prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg;
|
||||
}
|
||||
const rx = (rotation + shearX - prx) * MathUtils.degRad;
|
||||
const ry = (rotation + shearY - prx + 90) * MathUtils.degRad;
|
||||
const la = Math.cos(rx) * scaleX;
|
||||
const lb = Math.cos(ry) * scaleY;
|
||||
const lc = Math.sin(rx) * scaleX;
|
||||
const ld = Math.sin(ry) * scaleY;
|
||||
this.a = pa * la - pb * lc;
|
||||
this.b = pa * lb - pb * ld;
|
||||
this.c = pc * la + pd * lc;
|
||||
this.d = pc * lb + pd * ld;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoScale:
|
||||
case Inherit.NoScaleOrReflection: {
|
||||
rotation *= MathUtils.degRad;
|
||||
const cos = Math.cos(rotation), sin = Math.sin(rotation);
|
||||
let za = (pa * cos + pb * sin) / this.skeleton.scaleX;
|
||||
let zc = (pc * cos + pd * sin) / this.skeleton.scaleY;
|
||||
let s = Math.sqrt(za * za + zc * zc);
|
||||
if (s > 0.00001) s = 1 / s;
|
||||
za *= s;
|
||||
zc *= s;
|
||||
s = Math.sqrt(za * za + zc * zc);
|
||||
if (this.inherit == Inherit.NoScale
|
||||
&& (pa * pd - pb * pc < 0) != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0)) s = -s;
|
||||
rotation = Math.PI / 2 + Math.atan2(zc, za);
|
||||
const zb = Math.cos(rotation) * s;
|
||||
const zd = Math.sin(rotation) * s;
|
||||
shearX *= MathUtils.degRad;
|
||||
shearY = (90 + shearY) * MathUtils.degRad;
|
||||
const la = Math.cos(shearX) * scaleX;
|
||||
const lb = Math.cos(shearY) * scaleY;
|
||||
const lc = Math.sin(shearX) * scaleX;
|
||||
const ld = Math.sin(shearY) * scaleY;
|
||||
this.a = za * la + zb * lc;
|
||||
this.b = za * lb + zb * ld;
|
||||
this.c = zc * la + zd * lc;
|
||||
this.d = zc * lb + zd * ld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.a *= this.skeleton.scaleX;
|
||||
this.b *= this.skeleton.scaleX;
|
||||
this.c *= this.skeleton.scaleY;
|
||||
this.d *= this.skeleton.scaleY;
|
||||
}
|
||||
|
||||
/** Sets this bone's local transform to the setup pose. */
|
||||
setToSetupPose () {
|
||||
let data = this.data;
|
||||
this.x = data.x;
|
||||
this.y = data.y;
|
||||
this.rotation = data.rotation;
|
||||
this.scaleX = data.scaleX;
|
||||
this.scaleY = data.scaleY;
|
||||
this.shearX = data.shearX;
|
||||
this.shearY = data.shearY;
|
||||
this.inherit = data.inherit;
|
||||
}
|
||||
|
||||
/** Computes the applied transform values from the world transform.
|
||||
*
|
||||
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
|
||||
* the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply other
|
||||
* constraints).
|
||||
*
|
||||
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
|
||||
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
|
||||
updateAppliedTransform () {
|
||||
let parent = this.parent;
|
||||
if (!parent) {
|
||||
this.ax = this.worldX - this.skeleton.x;
|
||||
this.ay = this.worldY - this.skeleton.y;
|
||||
this.arotation = Math.atan2(this.c, this.a) * MathUtils.radDeg;
|
||||
this.ascaleX = Math.sqrt(this.a * this.a + this.c * this.c);
|
||||
this.ascaleY = Math.sqrt(this.b * this.b + this.d * this.d);
|
||||
this.ashearX = 0;
|
||||
this.ashearY = Math.atan2(this.a * this.b + this.c * this.d, this.a * this.d - this.b * this.c) * MathUtils.radDeg;
|
||||
return;
|
||||
}
|
||||
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
|
||||
let pid = 1 / (pa * pd - pb * pc);
|
||||
let ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
|
||||
let dx = this.worldX - parent.worldX, dy = this.worldY - parent.worldY;
|
||||
this.ax = (dx * ia - dy * ib);
|
||||
this.ay = (dy * id - dx * ic);
|
||||
|
||||
let ra, rb, rc, rd;
|
||||
if (this.inherit == Inherit.OnlyTranslation) {
|
||||
ra = this.a;
|
||||
rb = this.b;
|
||||
rc = this.c;
|
||||
rd = this.d;
|
||||
} else {
|
||||
switch (this.inherit) {
|
||||
case Inherit.NoRotationOrReflection: {
|
||||
let s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
|
||||
pb = -pc * this.skeleton.scaleX * s / this.skeleton.scaleY;
|
||||
pd = pa * this.skeleton.scaleY * s / this.skeleton.scaleX;
|
||||
pid = 1 / (pa * pd - pb * pc);
|
||||
ia = pd * pid;
|
||||
ib = pb * pid;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoScale:
|
||||
case Inherit.NoScaleOrReflection:
|
||||
let cos = MathUtils.cosDeg(this.rotation), sin = MathUtils.sinDeg(this.rotation);
|
||||
pa = (pa * cos + pb * sin) / this.skeleton.scaleX;
|
||||
pc = (pc * cos + pd * sin) / this.skeleton.scaleY;
|
||||
let s = Math.sqrt(pa * pa + pc * pc);
|
||||
if (s > 0.00001) s = 1 / s;
|
||||
pa *= s;
|
||||
pc *= s;
|
||||
s = Math.sqrt(pa * pa + pc * pc);
|
||||
if (this.inherit == Inherit.NoScale && pid < 0 != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0)) s = -s;
|
||||
let r = MathUtils.PI / 2 + Math.atan2(pc, pa);
|
||||
pb = Math.cos(r) * s;
|
||||
pd = Math.sin(r) * s;
|
||||
pid = 1 / (pa * pd - pb * pc);
|
||||
ia = pd * pid;
|
||||
ib = pb * pid;
|
||||
ic = pc * pid;
|
||||
id = pa * pid;
|
||||
}
|
||||
ra = ia * this.a - ib * this.c;
|
||||
rb = ia * this.b - ib * this.d;
|
||||
rc = id * this.c - ic * this.a;
|
||||
rd = id * this.d - ic * this.b;
|
||||
}
|
||||
|
||||
this.ashearX = 0;
|
||||
this.ascaleX = Math.sqrt(ra * ra + rc * rc);
|
||||
if (this.ascaleX > 0.0001) {
|
||||
let det = ra * rd - rb * rc;
|
||||
this.ascaleY = det / this.ascaleX;
|
||||
this.ashearY = -Math.atan2(ra * rb + rc * rd, det) * MathUtils.radDeg;
|
||||
this.arotation = Math.atan2(rc, ra) * MathUtils.radDeg;
|
||||
} else {
|
||||
this.ascaleX = 0;
|
||||
this.ascaleY = Math.sqrt(rb * rb + rd * rd);
|
||||
this.ashearY = 0;
|
||||
this.arotation = 90 - Math.atan2(rd, rb) * MathUtils.radDeg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
|
||||
getWorldRotationX () {
|
||||
return Math.atan2(this.c, this.a) * MathUtils.radDeg;
|
||||
}
|
||||
|
||||
/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
|
||||
getWorldRotationY () {
|
||||
return Math.atan2(this.d, this.b) * MathUtils.radDeg;
|
||||
}
|
||||
|
||||
/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
|
||||
getWorldScaleX () {
|
||||
return Math.sqrt(this.a * this.a + this.c * this.c);
|
||||
}
|
||||
|
||||
/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
|
||||
getWorldScaleY () {
|
||||
return Math.sqrt(this.b * this.b + this.d * this.d);
|
||||
}
|
||||
|
||||
/** Transforms a point from world coordinates to the bone's local coordinates. */
|
||||
worldToLocal (world: Vector2) {
|
||||
let invDet = 1 / (this.a * this.d - this.b * this.c);
|
||||
let x = world.x - this.worldX, y = world.y - this.worldY;
|
||||
world.x = x * this.d * invDet - y * this.b * invDet;
|
||||
world.y = y * this.a * invDet - x * this.c * invDet;
|
||||
return world;
|
||||
}
|
||||
|
||||
/** Transforms a point from the bone's local coordinates to world coordinates. */
|
||||
localToWorld (local: Vector2) {
|
||||
let x = local.x, y = local.y;
|
||||
local.x = x * this.a + y * this.b + this.worldX;
|
||||
local.y = x * this.c + y * this.d + this.worldY;
|
||||
return local;
|
||||
}
|
||||
|
||||
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
|
||||
worldToParent (world: Vector2) {
|
||||
if (world == null) throw new Error("world cannot be null.");
|
||||
return this.parent == null ? world : this.parent.worldToLocal(world);
|
||||
}
|
||||
|
||||
/** Transforms a point from the parent bone's coordinates to world coordinates. */
|
||||
parentToWorld (world: Vector2) {
|
||||
if (world == null) throw new Error("world cannot be null.");
|
||||
return this.parent == null ? world : this.parent.localToWorld(world);
|
||||
}
|
||||
|
||||
/** Transforms a world rotation to a local rotation. */
|
||||
worldToLocalRotation (worldRotation: number) {
|
||||
let sin = MathUtils.sinDeg(worldRotation), cos = MathUtils.cosDeg(worldRotation);
|
||||
return Math.atan2(this.a * sin - this.c * cos, this.d * cos - this.b * sin) * MathUtils.radDeg + this.rotation - this.shearX;
|
||||
}
|
||||
|
||||
/** Transforms a local rotation to a world rotation. */
|
||||
localToWorldRotation (localRotation: number) {
|
||||
localRotation -= this.rotation - this.shearX;
|
||||
let sin = MathUtils.sinDeg(localRotation), cos = MathUtils.cosDeg(localRotation);
|
||||
return Math.atan2(cos * this.c + sin * this.d, cos * this.a + sin * this.b) * MathUtils.radDeg;
|
||||
}
|
||||
|
||||
/** Rotates the world transform the specified amount.
|
||||
* <p>
|
||||
* After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and
|
||||
* {@link #update(Physics)} will need to be called on any child bones, recursively. */
|
||||
rotateWorld (degrees: number) {
|
||||
degrees *= MathUtils.degRad;
|
||||
const sin = Math.sin(degrees), cos = Math.cos(degrees);
|
||||
const ra = this.a, rb = this.b;
|
||||
this.a = cos * ra - sin * this.c;
|
||||
this.b = cos * rb - sin * this.d;
|
||||
this.c = sin * ra + cos * this.c;
|
||||
this.d = sin * rb + cos * this.d;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,15 +27,16 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { BoneLocal } from "./BoneLocal.js";
|
||||
import { PosedData } from "./PosedData.js";
|
||||
import { Color } from "./Utils.js";
|
||||
|
||||
/** Stores the setup pose for a {@link Bone}. */
|
||||
export class BoneData {
|
||||
/** The index of the bone in {@link Skeleton#getBones()}. */
|
||||
index: number = 0;
|
||||
import type { Skeleton } from "./Skeleton.js";
|
||||
|
||||
/** The name of the bone, which is unique across all bones in the skeleton. */
|
||||
name: string;
|
||||
/** The setup pose for a bone. */
|
||||
export class BoneData extends PosedData<BoneLocal> {
|
||||
/** The index of the bone in {@link Skeleton.getBones}. */
|
||||
index: number = 0;
|
||||
|
||||
/** @returns May be null. */
|
||||
parent: BoneData | null = null;
|
||||
@ -43,38 +44,10 @@ export class BoneData {
|
||||
/** The bone's length. */
|
||||
length: number = 0;
|
||||
|
||||
/** The local x translation. */
|
||||
x = 0;
|
||||
|
||||
/** The local y translation. */
|
||||
y = 0;
|
||||
|
||||
/** The local rotation in degrees, counter clockwise. */
|
||||
rotation = 0;
|
||||
|
||||
/** The local scaleX. */
|
||||
scaleX = 1;
|
||||
|
||||
/** The local scaleY. */
|
||||
scaleY = 1;
|
||||
|
||||
/** The local shearX. */
|
||||
shearX = 0;
|
||||
|
||||
/** The local shearX. */
|
||||
shearY = 0;
|
||||
|
||||
/** The transform mode for how parent world transforms affect this bone. */
|
||||
inherit = Inherit.Normal;
|
||||
|
||||
/** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#skin} contains this
|
||||
* bone.
|
||||
* @see Skin#bones */
|
||||
skinRequired = false;
|
||||
|
||||
// Nonessential.
|
||||
/** The color of the bone as it was in Spine. Available only when nonessential data was exported. Bones are not usually
|
||||
* rendered at runtime. */
|
||||
color = new Color();
|
||||
readonly color = new Color();
|
||||
|
||||
/** The bone icon as it was in Spine, or null if nonessential data was not exported. */
|
||||
icon?: string;
|
||||
@ -83,12 +56,19 @@ export class BoneData {
|
||||
visible = false;
|
||||
|
||||
constructor (index: number, name: string, parent: BoneData | null) {
|
||||
super(name, new BoneLocal());
|
||||
if (index < 0) throw new Error("index must be >= 0.");
|
||||
if (!name) throw new Error("name cannot be null.");
|
||||
this.index = index;
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
copy (parent: BoneData | null): BoneData {
|
||||
const copy = new BoneData(this.index, this.name, parent);
|
||||
copy.length = this.length;
|
||||
copy.setup.set(this.setup);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines how a bone inherits world transforms from parent bones. */
|
||||
|
||||
92
spine-ts/spine-core/src/BoneLocal.ts
Normal file
92
spine-ts/spine-core/src/BoneLocal.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Inherit } from "./BoneData";
|
||||
import { Pose } from "./Pose"
|
||||
|
||||
/** Stores a bone's local pose. */
|
||||
export class BoneLocal implements Pose<BoneLocal> {
|
||||
|
||||
/** The local x translation. */
|
||||
x = 0;
|
||||
|
||||
/** The local y translation. */
|
||||
y = 0;
|
||||
|
||||
/** The local rotation in degrees, counter clockwise. */
|
||||
rotation = 0;
|
||||
|
||||
/** The local scaleX. */
|
||||
scaleX = 0;
|
||||
|
||||
/** The local scaleY. */
|
||||
scaleY = 0;
|
||||
|
||||
/** The local shearX. */
|
||||
shearX = 0;
|
||||
|
||||
/** The local shearY. */
|
||||
shearY = 0;
|
||||
|
||||
inherit = Inherit.Normal;
|
||||
|
||||
set (pose: BoneLocal): void {
|
||||
if (pose == null) throw new Error("pose cannot be null.");
|
||||
this.x = pose.x;
|
||||
this.y = pose.y;
|
||||
this.rotation = pose.rotation;
|
||||
this.scaleX = pose.scaleX;
|
||||
this.scaleY = pose.scaleY;
|
||||
this.shearX = pose.shearX;
|
||||
this.shearY = pose.shearY;
|
||||
this.inherit = pose.inherit;
|
||||
}
|
||||
|
||||
setPosition (x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
setScale(scaleX: number, scaleY: number): void;
|
||||
setScale(scale: number): void;
|
||||
setScale(scaleOrX: number, scaleY?: number): void {
|
||||
this.scaleX = scaleOrX;
|
||||
this.scaleY = scaleY === undefined ? scaleOrX : scaleY;
|
||||
}
|
||||
|
||||
/** Determines how parent world transforms affect this bone. */
|
||||
public getInherit (): Inherit {
|
||||
return this.inherit;
|
||||
}
|
||||
|
||||
public setInherit (inherit: Inherit): void {
|
||||
if (inherit == null) throw new Error("inherit cannot be null.");
|
||||
this.inherit = inherit;
|
||||
}
|
||||
}
|
||||
400
spine-ts/spine-core/src/BonePose.ts
Normal file
400
spine-ts/spine-core/src/BonePose.ts
Normal file
@ -0,0 +1,400 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Bone } from "./Bone";
|
||||
import { Inherit } from "./BoneData";
|
||||
import { BoneLocal } from "./BoneLocal";
|
||||
import { Physics } from "./Physics";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import { Update } from "./Update";
|
||||
import { MathUtils, Vector2 } from "./Utils";
|
||||
|
||||
/** The applied pose for a bone. This is the {@link Bone} pose with constraints applied and the world transform computed by
|
||||
* {@link Skeleton#updateWorldTransform()}. */
|
||||
export class BonePose extends BoneLocal implements Update {
|
||||
bone!: Bone;
|
||||
|
||||
/** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */
|
||||
a = 0;
|
||||
|
||||
/** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */
|
||||
b = 0;
|
||||
|
||||
/** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */
|
||||
c = 0;
|
||||
|
||||
/** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */
|
||||
d = 0;
|
||||
|
||||
/** The world X position. If changed, {@link updateLocalTransform()} should be called. */
|
||||
worldY = 0;
|
||||
|
||||
/** The world Y position. If changed, {@link updateLocalTransform()} should be called. */
|
||||
worldX = 0;
|
||||
|
||||
world = 0;
|
||||
local = 0;
|
||||
|
||||
/** Called by {@link Skeleton#updateCache()} to compute the world transform, if needed. */
|
||||
public update (skeleton: Skeleton, physics: Physics): void {
|
||||
if (this.world != skeleton._update) this.updateWorldTransform(skeleton);
|
||||
}
|
||||
|
||||
/** Computes the world transform using the parent bone's applied pose and this pose. Child bones are not updated.
|
||||
* <p>
|
||||
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
|
||||
* Runtimes Guide. */
|
||||
updateWorldTransform (skeleton: Skeleton): void {
|
||||
if (this.local == skeleton._update)
|
||||
this.updateLocalTransform(skeleton);
|
||||
else
|
||||
this.world = skeleton._update;
|
||||
|
||||
const rotation = this.rotation;
|
||||
const scaleX = this.scaleX;
|
||||
const scaleY = this.scaleY;
|
||||
const shearX = this.shearX;
|
||||
const shearY = this.shearY;
|
||||
if (this.bone.parent == null) { // Root bone.
|
||||
const sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
this.a = Math.cos(rx) * scaleX * sx;
|
||||
this.b = Math.cos(ry) * scaleY * sx;
|
||||
this.c = Math.sin(rx) * scaleX * sy;
|
||||
this.d = Math.sin(ry) * scaleY * sy;
|
||||
this.worldX = this.x * sx + skeleton.x;
|
||||
this.worldY = this.y * sy + skeleton.y;
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = this.bone.parent.applied;
|
||||
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
|
||||
this.worldX = pa * this.x + pb * this.y + parent.worldX;
|
||||
this.worldY = pc * this.x + pd * this.y + parent.worldY;
|
||||
|
||||
switch (this.inherit) {
|
||||
case Inherit.Normal: {
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
const la = Math.cos(rx) * scaleX;
|
||||
const lb = Math.cos(ry) * scaleY;
|
||||
const lc = Math.sin(rx) * scaleX;
|
||||
const ld = Math.sin(ry) * scaleY;
|
||||
this.a = pa * la + pb * lc;
|
||||
this.b = pa * lb + pb * ld;
|
||||
this.c = pc * la + pd * lc;
|
||||
this.d = pc * lb + pd * ld;
|
||||
return;
|
||||
}
|
||||
case Inherit.OnlyTranslation: {
|
||||
const rx = (rotation + shearX) * MathUtils.degRad;
|
||||
const ry = (rotation + 90 + shearY) * MathUtils.degRad;
|
||||
this.a = Math.cos(rx) * scaleX;
|
||||
this.b = Math.cos(ry) * scaleY;
|
||||
this.c = Math.sin(rx) * scaleX;
|
||||
this.d = Math.sin(ry) * scaleY;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoRotationOrReflection: {
|
||||
let sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY;
|
||||
pa *= sx;
|
||||
pc *= sy;
|
||||
let s = pa * pa + pc * pc;
|
||||
let prx = 0;
|
||||
if (s > 0.0001) {
|
||||
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
|
||||
pb = pc * s;
|
||||
pd = pa * s;
|
||||
prx = MathUtils.atan2Deg(pc, pa);
|
||||
} else {
|
||||
pa = 0;
|
||||
pc = 0;
|
||||
prx = 90 - MathUtils.atan2Deg(pd, pb);
|
||||
}
|
||||
const rx = (rotation + shearX - prx) * MathUtils.degRad;
|
||||
const ry = (rotation + shearY - prx + 90) * MathUtils.degRad;
|
||||
const la = Math.cos(rx) * scaleX;
|
||||
const lb = Math.cos(ry) * scaleY;
|
||||
const lc = Math.sin(rx) * scaleX;
|
||||
const ld = Math.sin(ry) * scaleY;
|
||||
this.a = pa * la - pb * lc;
|
||||
this.b = pa * lb - pb * ld;
|
||||
this.c = pc * la + pd * lc;
|
||||
this.d = pc * lb + pd * ld;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoScale:
|
||||
case Inherit.NoScaleOrReflection: {
|
||||
this.rotation *= MathUtils.degRad;
|
||||
const cos = Math.cos(rotation), sin = Math.sin(rotation);
|
||||
let za = (pa * cos + pb * sin) / skeleton.scaleX;
|
||||
let zc = (pc * cos + pd * sin) / skeleton.scaleY;
|
||||
let s = Math.sqrt(za * za + zc * zc);
|
||||
if (s > 0.00001) s = 1 / s;
|
||||
za *= s;
|
||||
zc *= s;
|
||||
s = Math.sqrt(za * za + zc * zc);
|
||||
if (this.inherit == Inherit.NoScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
|
||||
this.rotation = Math.PI / 2 + Math.atan2(zc, za);
|
||||
const zb = Math.cos(this.rotation) * s;
|
||||
const zd = Math.sin(this.rotation) * s;
|
||||
this.shearX *= MathUtils.degRad;
|
||||
this.shearY = (90 + shearY) * MathUtils.degRad;
|
||||
const la = Math.cos(shearX) * scaleX;
|
||||
const lb = Math.cos(shearY) * scaleY;
|
||||
const lc = Math.sin(shearX) * scaleX;
|
||||
const ld = Math.sin(shearY) * scaleY;
|
||||
this.a = za * la + zb * lc;
|
||||
this.b = za * lb + zb * ld;
|
||||
this.c = zc * la + zd * lc;
|
||||
this.d = zc * lb + zd * ld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.a *= skeleton.scaleX;
|
||||
this.b *= skeleton.scaleX;
|
||||
this.c *= skeleton.scaleY;
|
||||
this.d *= skeleton.scaleY;
|
||||
}
|
||||
|
||||
/** Computes the local transform values from the world transform.
|
||||
* <p>
|
||||
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
|
||||
* the local transform matches the world transform. The local transform may be needed by other code (eg to apply another
|
||||
* constraint).
|
||||
* <p>
|
||||
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The local transform after
|
||||
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
|
||||
public updateLocalTransform (skeleton: Skeleton): void {
|
||||
this.local = 0;
|
||||
this.world = skeleton._update;
|
||||
|
||||
if (!this.bone.parent) {
|
||||
this.x = this.worldX - skeleton.x;
|
||||
this.y = this.worldY - skeleton.y;
|
||||
let a = this.a, b = this.b, c = this.c, d = this.d;
|
||||
this.rotation = MathUtils.atan2Deg(c, a);
|
||||
this.scaleX = Math.sqrt(a * a + c * c);
|
||||
this.scaleY = Math.sqrt(b * b + d * d);
|
||||
this.shearX = 0;
|
||||
this.shearY = MathUtils.atan2Deg(a * b + c * d, a * d - b * c);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = this.bone.parent.applied;
|
||||
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
|
||||
let pid = 1 / (pa * pd - pb * pc);
|
||||
let ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
|
||||
let dx = this.worldX - parent.worldX, dy = this.worldY - parent.worldY;
|
||||
this.x = (dx * ia - dy * ib);
|
||||
this.y = (dy * id - dx * ic);
|
||||
|
||||
let ra, rb, rc, rd;
|
||||
if (this.inherit == Inherit.OnlyTranslation) {
|
||||
ra = this.a;
|
||||
rb = this.b;
|
||||
rc = this.c;
|
||||
rd = this.d;
|
||||
} else {
|
||||
switch (this.inherit) {
|
||||
case Inherit.NoRotationOrReflection: {
|
||||
let s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
|
||||
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
|
||||
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
|
||||
pid = 1 / (pa * pd - pb * pc);
|
||||
ia = pd * pid;
|
||||
ib = pb * pid;
|
||||
break;
|
||||
}
|
||||
case Inherit.NoScale:
|
||||
case Inherit.NoScaleOrReflection:
|
||||
let r = this.rotation * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
|
||||
pa = (pa * cos + pb * sin) / skeleton.scaleX;
|
||||
pc = (pc * cos + pd * sin) / skeleton.scaleY;
|
||||
let s = Math.sqrt(pa * pa + pc * pc);
|
||||
if (s > 0.00001) s = 1 / s;
|
||||
pa *= s;
|
||||
pc *= s;
|
||||
s = Math.sqrt(pa * pa + pc * pc);
|
||||
if (this.inherit == Inherit.NoScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
|
||||
r = MathUtils.PI / 2 + Math.atan2(pc, pa);
|
||||
pb = Math.cos(r) * s;
|
||||
pd = Math.sin(r) * s;
|
||||
pid = 1 / (pa * pd - pb * pc);
|
||||
ia = pd * pid;
|
||||
ib = pb * pid;
|
||||
ic = pc * pid;
|
||||
id = pa * pid;
|
||||
}
|
||||
ra = ia * this.a - ib * this.c;
|
||||
rb = ia * this.b - ib * this.d;
|
||||
rc = id * this.c - ic * this.a;
|
||||
rd = id * this.d - ic * this.b;
|
||||
}
|
||||
|
||||
this.shearX = 0;
|
||||
this.scaleX = Math.sqrt(ra * ra + rc * rc);
|
||||
if (this.scaleX > 0.0001) {
|
||||
let det = ra * rd - rb * rc;
|
||||
this.scaleY = det / this.scaleX;
|
||||
this.shearY = -MathUtils.atan2Deg(ra * rb + rc * rd, det);
|
||||
this.rotation = MathUtils.atan2Deg(rc, ra);
|
||||
} else {
|
||||
this.scaleX = 0;
|
||||
this.scaleY = Math.sqrt(rb * rb + rd * rd);
|
||||
this.shearY = 0;
|
||||
this.rotation = 90 - MathUtils.atan2Deg(rd, rb);
|
||||
}
|
||||
}
|
||||
|
||||
/** If the world transform has been modified and the local transform no longer matches, {@link #updateLocalTransform(Skeleton)}
|
||||
* is called. */
|
||||
public validateLocalTransform (skeleton: Skeleton): void {
|
||||
if (this.local === skeleton._update) this.updateLocalTransform(skeleton);
|
||||
}
|
||||
|
||||
modifyLocal (skeleton: Skeleton): void {
|
||||
if (this.local === skeleton._update) this.updateLocalTransform(skeleton);
|
||||
this.world = 0;
|
||||
this.resetWorld(skeleton._update);
|
||||
}
|
||||
|
||||
modifyWorld (update: number): void {
|
||||
this.local = update;
|
||||
this.world = update;
|
||||
this.resetWorld(update);
|
||||
}
|
||||
|
||||
resetWorld (update: number): void {
|
||||
const children = this.bone.children;
|
||||
for (let i = 0, n = children.length; i < n; i++) {
|
||||
const child = children[i].applied;
|
||||
if (child.world === update) {
|
||||
child.world = 0;
|
||||
child.local = 0;
|
||||
child.resetWorld(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The world rotation for the X axis, calculated using {@link a} and {@link c}. */
|
||||
public getWorldRotationX (): number {
|
||||
return MathUtils.atan2Deg(this.c, this.a);
|
||||
}
|
||||
|
||||
/** The world rotation for the Y axis, calculated using {@link b} and {@link d}. */
|
||||
public getWorldRotationY (): number {
|
||||
return MathUtils.atan2Deg(this.d, this.b);
|
||||
}
|
||||
|
||||
/** The magnitude (always positive) of the world scale X, calculated using {@link a} and {@link c}. */
|
||||
public getWorldScaleX (): number {
|
||||
return Math.sqrt(this.a * this.a + this.c * this.c);
|
||||
}
|
||||
|
||||
/** The magnitude (always positive) of the world scale Y, calculated using {@link b} and {@link d}. */
|
||||
public getWorldScaleY (): number {
|
||||
return Math.sqrt(this.b * this.b + this.d * this.d);
|
||||
}
|
||||
|
||||
// public Matrix3 getWorldTransform (Matrix3 worldTransform) {
|
||||
// if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
|
||||
// float[] val = worldTransform.val;
|
||||
// val[M00] = a;
|
||||
// val[M01] = b;
|
||||
// val[M10] = c;
|
||||
// val[M11] = d;
|
||||
// val[M02] = worldX;
|
||||
// val[M12] = worldY;
|
||||
// val[M20] = 0;
|
||||
// val[M21] = 0;
|
||||
// val[M22] = 1;
|
||||
// return worldTransform;
|
||||
// }
|
||||
|
||||
/** Transforms a point from world coordinates to the bone's local coordinates. */
|
||||
public worldToLocal (world: Vector2): Vector2 {
|
||||
if (world == null) throw new Error("world cannot be null.");
|
||||
let det = this.a * this.d - this.b * this.c;
|
||||
let x = world.x - this.worldX, y = world.y - this.worldY;
|
||||
world.x = (x * this.d - y * this.b) / det;
|
||||
world.y = (y * this.a - x * this.c) / det;
|
||||
return world;
|
||||
}
|
||||
|
||||
/** Transforms a point from the bone's local coordinates to world coordinates. */
|
||||
public localToWorld (local: Vector2): Vector2 {
|
||||
if (local == null) throw new Error("local cannot be null.");
|
||||
let x = local.x, y = local.y;
|
||||
local.x = x * this.a + y * this.b + this.worldX;
|
||||
local.y = x * this.c + y * this.d + this.worldY;
|
||||
return local;
|
||||
}
|
||||
|
||||
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
|
||||
public worldToParent (world: Vector2): Vector2 {
|
||||
if (world == null) throw new Error("world cannot be null.");
|
||||
return this.bone.parent == null ? world : this.bone.parent.applied.worldToLocal(world);
|
||||
}
|
||||
|
||||
/** Transforms a point from the parent bone's coordinates to world coordinates. */
|
||||
public parentToWorld (world: Vector2): Vector2 {
|
||||
if (world == null) throw new Error("world cannot be null.");
|
||||
return this.bone.parent == null ? world : this.bone.parent.applied.localToWorld(world);
|
||||
}
|
||||
|
||||
/** Transforms a world rotation to a local rotation. */
|
||||
public worldToLocalRotation (worldRotation: number): number {
|
||||
worldRotation *= MathUtils.degRad;
|
||||
let sin = Math.sin(worldRotation), cos = Math.cos(worldRotation);
|
||||
return MathUtils.atan2Deg(this.a * sin - this.c * cos, this.d * cos - this.b * sin) + this.rotation - this.shearX;
|
||||
}
|
||||
|
||||
/** Transforms a local rotation to a world rotation. */
|
||||
localToWorldRotation (localRotation: number): number {
|
||||
localRotation = (localRotation - this.rotation - this.shearX) * MathUtils.degRad;
|
||||
let sin = Math.sin(localRotation), cos = Math.cos(localRotation);
|
||||
return MathUtils.atan2Deg(cos * this.c + sin * this.d, cos * this.a + sin * this.b);
|
||||
}
|
||||
|
||||
/** Rotates the world transform the specified amount.
|
||||
* <p>
|
||||
* After changes are made to the world transform, {@link updateLocalTransform} should be called on this bone and any
|
||||
* child bones, recursively. */
|
||||
rotateWorld (degrees: number) {
|
||||
degrees *= MathUtils.degRad;
|
||||
const sin = Math.sin(degrees), cos = Math.cos(degrees);
|
||||
const ra = this.a, rb = this.b;
|
||||
this.a = cos * ra - sin * this.c;
|
||||
this.b = cos * rb - sin * this.d;
|
||||
this.c = sin * ra + cos * this.c;
|
||||
this.d = sin * rb + cos * this.d;
|
||||
}
|
||||
}
|
||||
56
spine-ts/spine-core/src/Constraint.ts
Normal file
56
spine-ts/spine-core/src/Constraint.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { ConstraintData } from "./ConstraintData";
|
||||
import { Physics } from "./Physics";
|
||||
import { Pose } from "./Pose";
|
||||
import { PosedActive } from "./PosedActive";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import { Update } from "./Update";
|
||||
|
||||
export abstract class Constraint<
|
||||
T extends Constraint<T, D, P>,
|
||||
D extends ConstraintData<T, P>,
|
||||
P extends Pose<any>>
|
||||
extends PosedActive<D, P, P> implements Update {
|
||||
|
||||
constructor (data: D, pose: P, constrained: P) {
|
||||
super(data, pose, constrained);
|
||||
}
|
||||
|
||||
abstract copy (skeleton: Skeleton): T;
|
||||
|
||||
abstract sort (skeleton: Skeleton): void;
|
||||
|
||||
abstract update (skeleton: Skeleton, physics: Physics): void;
|
||||
|
||||
isSourceActive (): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,20 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Constraint } from "./Constraint";
|
||||
import { Pose } from "./Pose";
|
||||
import { PosedData } from "./PosedData";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
|
||||
/** The base class for all constraint datas. */
|
||||
export abstract class ConstraintData {
|
||||
constructor (public name: string, public order: number, public skinRequired: boolean) { }
|
||||
export abstract class ConstraintData<
|
||||
T extends Constraint<any, any, any>,
|
||||
P extends Pose<any>>
|
||||
extends PosedData<P> {
|
||||
|
||||
constructor (name: string, setup: P) {
|
||||
super(name, setup);
|
||||
}
|
||||
|
||||
abstract create (skeleton: Skeleton): T;
|
||||
}
|
||||
|
||||
@ -29,13 +29,16 @@
|
||||
|
||||
import { EventData } from "./EventData.js";
|
||||
|
||||
import type { Timeline } from "./Animation.js";
|
||||
import type { AnimationStateListener } from "./AnimationState.js";
|
||||
|
||||
/** Stores the current pose values for an {@link Event}.
|
||||
*
|
||||
* See Timeline {@link Timeline#apply()},
|
||||
* AnimationStateListener {@link AnimationStateListener#event()}, and
|
||||
* See Timeline {@link Timeline.apply()},
|
||||
* AnimationStateListener {@link AnimationStateListener.event()}, and
|
||||
* [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */
|
||||
export class Event {
|
||||
data: EventData;
|
||||
readonly data: EventData;
|
||||
intValue: number = 0;
|
||||
floatValue: number = 0;
|
||||
stringValue: string | null = null;
|
||||
|
||||
@ -29,109 +29,109 @@
|
||||
|
||||
import { Bone } from "./Bone.js";
|
||||
import { Inherit } from "./BoneData.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { Constraint } from "./Constraint.js";
|
||||
import { IkConstraintData } from "./IkConstraintData.js";
|
||||
import { Physics, Skeleton } from "./Skeleton.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { IkConstraintPose } from "./IkConstraintPose.js";
|
||||
import { Physics } from "./Physics.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { MathUtils } from "./Utils.js";
|
||||
|
||||
/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of
|
||||
* the last bone is as close to the target bone as possible.
|
||||
*
|
||||
* See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */
|
||||
export class IkConstraint implements Updatable {
|
||||
/** The IK constraint's setup pose data. */
|
||||
data: IkConstraintData;
|
||||
|
||||
/** The bones that will be modified by this IK constraint. */
|
||||
bones: Array<Bone>;
|
||||
export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkConstraintPose> {
|
||||
/** The 1 or 2 bones that will be modified by this IK constraint. */
|
||||
readonly bones: Array<BonePose>;
|
||||
|
||||
/** The bone that is the IK target. */
|
||||
target: Bone;
|
||||
|
||||
/** Controls the bend direction of the IK bones, either 1 or -1. */
|
||||
bendDirection = 0;
|
||||
|
||||
/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
|
||||
compress = false;
|
||||
|
||||
/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
|
||||
* and the parent bone has local nonuniform scale, stretch is not applied. */
|
||||
stretch = false;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
|
||||
mix = 1;
|
||||
|
||||
/** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */
|
||||
softness = 0;
|
||||
active = false;
|
||||
|
||||
constructor (data: IkConstraintData, skeleton: Skeleton) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
super(data, new IkConstraintPose(), new IkConstraintPose());
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
this.data = data;
|
||||
|
||||
this.bones = new Array<Bone>();
|
||||
for (let i = 0; i < data.bones.length; i++) {
|
||||
let bone = skeleton.findBone(data.bones[i].name);
|
||||
if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}`);
|
||||
this.bones.push(bone);
|
||||
}
|
||||
let target = skeleton.findBone(data.target.name);
|
||||
if (!target) throw new Error(`Couldn't find bone ${data.target.name}`);
|
||||
this.bones = new Array<BonePose>();
|
||||
for (const boneData of data.bones)
|
||||
this.bones.push(skeleton.bones[boneData.index].constrained);
|
||||
|
||||
this.target = target;
|
||||
this.mix = data.mix;
|
||||
this.softness = data.softness;
|
||||
this.bendDirection = data.bendDirection;
|
||||
this.compress = data.compress;
|
||||
this.stretch = data.stretch;
|
||||
this.target = skeleton.bones[data.target.index];
|
||||
}
|
||||
|
||||
isActive () {
|
||||
return this.active;
|
||||
copy (skeleton: Skeleton): IkConstraint {
|
||||
var copy = new IkConstraint(this.data, skeleton);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
setToSetupPose () {
|
||||
const data = this.data;
|
||||
this.mix = data.mix;
|
||||
this.softness = data.softness;
|
||||
this.bendDirection = data.bendDirection;
|
||||
this.compress = data.compress;
|
||||
this.stretch = data.stretch;
|
||||
}
|
||||
|
||||
update (physics: Physics) {
|
||||
if (this.mix == 0) return;
|
||||
let target = this.target;
|
||||
update (skeleton: Skeleton, physics: Physics) {
|
||||
const p = this.applied;
|
||||
if (p.mix === 0) return;
|
||||
let target = this.target.applied;
|
||||
let bones = this.bones;
|
||||
switch (bones.length) {
|
||||
case 1:
|
||||
this.apply1(bones[0], target.worldX, target.worldY, this.compress, this.stretch, this.data.uniform, this.mix);
|
||||
IkConstraint.apply(skeleton, bones[0], target.worldX, target.worldY, p.compress, p.stretch, this.data.uniform, p.mix);
|
||||
break;
|
||||
case 2:
|
||||
this.apply2(bones[0], bones[1], target.worldX, target.worldY, this.bendDirection, this.stretch, this.data.uniform, this.softness, this.mix);
|
||||
IkConstraint.apply(skeleton, bones[0], bones[1], target.worldX, target.worldY, p.bendDirection, p.stretch, this.data.uniform,
|
||||
p.softness, p.mix);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sort (skeleton: Skeleton) {
|
||||
skeleton.sortBone(this.target);
|
||||
const parent = this.bones[0].bone;
|
||||
skeleton.sortBone(parent);
|
||||
skeleton._updateCache.push(this);
|
||||
parent.sorted = false;
|
||||
skeleton.sortReset(parent.children);
|
||||
skeleton.constrained(parent);
|
||||
if (this.bones.length > 1) skeleton.constrained(this.bones[1].bone);
|
||||
}
|
||||
|
||||
isSourceActive () {
|
||||
return this.target.active;
|
||||
}
|
||||
|
||||
/** Applies 1 bone IK. The target is specified in the world coordinate system. */
|
||||
apply1 (bone: Bone, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, alpha: number) {
|
||||
let p = bone.parent;
|
||||
if (!p) throw new Error("IK bone must have parent.");
|
||||
public static apply (skeleton: Skeleton, bone: BonePose, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, mix: number): void;
|
||||
|
||||
/** Applies 2 bone IK. The target is specified in the world coordinate system.
|
||||
* @param child A direct descendant of the parent bone. */
|
||||
public static apply (skeleton: Skeleton, parent: BonePose, child: BonePose, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, mix: number): void;
|
||||
|
||||
public static apply (skeleton: Skeleton, boneOrParent: BonePose, targetXorChild: number | BonePose, targetYOrTargetX: number, compressOrTargetY: boolean | number,
|
||||
stretchOrBendDir: boolean | number, uniformOrStretch: boolean, mixOrUniform: number | boolean, softness?: number, mix?: number) {
|
||||
|
||||
if (typeof targetXorChild === "number")
|
||||
this.apply1(skeleton, boneOrParent, targetXorChild, targetYOrTargetX, compressOrTargetY as boolean, stretchOrBendDir as boolean, uniformOrStretch, mixOrUniform as number);
|
||||
else
|
||||
this.apply2(skeleton, boneOrParent, targetXorChild as BonePose, targetYOrTargetX, compressOrTargetY as number, stretchOrBendDir as number,
|
||||
uniformOrStretch, mixOrUniform as boolean, softness as number, mix as number);
|
||||
}
|
||||
|
||||
private static apply1 (skeleton: Skeleton, bone: BonePose, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, mix: number) {
|
||||
bone.modifyLocal(skeleton);
|
||||
|
||||
let p = bone.bone.parent!.applied;
|
||||
|
||||
let pa = p.a, pb = p.b, pc = p.c, pd = p.d;
|
||||
let rotationIK = -bone.ashearX - bone.arotation, tx = 0, ty = 0;
|
||||
let rotationIK = -bone.shearX - bone.rotation, tx = 0, ty = 0;
|
||||
|
||||
switch (bone.inherit) {
|
||||
case Inherit.OnlyTranslation:
|
||||
tx = (targetX - bone.worldX) * MathUtils.signum(bone.skeleton.scaleX);
|
||||
ty = (targetY - bone.worldY) * MathUtils.signum(bone.skeleton.scaleY);
|
||||
tx = (targetX - bone.worldX) * MathUtils.signum(skeleton.scaleX);
|
||||
ty = (targetY - bone.worldY) * MathUtils.signum(skeleton.scaleY);
|
||||
break;
|
||||
case Inherit.NoRotationOrReflection:
|
||||
let s = Math.abs(pa * pd - pb * pc) / Math.max(0.0001, pa * pa + pc * pc);
|
||||
let sa = pa / bone.skeleton.scaleX;
|
||||
let sc = pc / bone.skeleton.scaleY;
|
||||
pb = -sc * s * bone.skeleton.scaleX;
|
||||
pd = sa * s * bone.skeleton.scaleY;
|
||||
let sa = pa / skeleton.scaleX;
|
||||
let sc = pc / skeleton.scaleY;
|
||||
pb = -sc * s * skeleton.scaleX;
|
||||
pd = sa * s * skeleton.scaleY;
|
||||
rotationIK += Math.atan2(sc, sa) * MathUtils.radDeg;
|
||||
// Fall through
|
||||
default:
|
||||
@ -141,17 +141,17 @@ export class IkConstraint implements Updatable {
|
||||
tx = 0;
|
||||
ty = 0;
|
||||
} else {
|
||||
tx = (x * pd - y * pb) / d - bone.ax;
|
||||
ty = (y * pa - x * pc) / d - bone.ay;
|
||||
tx = (x * pd - y * pb) / d - bone.x;
|
||||
ty = (y * pa - x * pc) / d - bone.y;
|
||||
}
|
||||
}
|
||||
rotationIK += Math.atan2(ty, tx) * MathUtils.radDeg;
|
||||
if (bone.ascaleX < 0) rotationIK += 180;
|
||||
rotationIK += MathUtils.atan2Deg(ty, tx);
|
||||
if (bone.scaleX < 0) rotationIK += 180;
|
||||
if (rotationIK > 180)
|
||||
rotationIK -= 360;
|
||||
else if (rotationIK < -180)
|
||||
rotationIK += 360;
|
||||
let sx = bone.ascaleX, sy = bone.ascaleY;
|
||||
bone.rotation += rotationIK * mix;
|
||||
if (compress || stretch) {
|
||||
switch (bone.inherit) {
|
||||
case Inherit.NoScale:
|
||||
@ -159,25 +159,25 @@ export class IkConstraint implements Updatable {
|
||||
tx = targetX - bone.worldX;
|
||||
ty = targetY - bone.worldY;
|
||||
}
|
||||
const b = bone.data.length * sx;
|
||||
const b = bone.bone.data.length * bone.scaleX;
|
||||
if (b > 0.0001) {
|
||||
const dd = tx * tx + ty * ty;
|
||||
if ((compress && dd < b * b) || (stretch && dd > b * b)) {
|
||||
const s = (Math.sqrt(dd) / b - 1) * alpha + 1;
|
||||
sx *= s;
|
||||
if (uniform) sy *= s;
|
||||
const s = (Math.sqrt(dd) / b - 1) * mix + 1;
|
||||
bone.scaleX *= s;
|
||||
if (uniform) bone.scaleY *= s;
|
||||
}
|
||||
}
|
||||
}
|
||||
bone.updateWorldTransformWith(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX,
|
||||
bone.ashearY);
|
||||
}
|
||||
|
||||
/** Applies 2 bone IK. The target is specified in the world coordinate system.
|
||||
* @param child A direct descendant of the parent bone. */
|
||||
apply2 (parent: Bone, child: Bone, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, alpha: number) {
|
||||
private static apply2 (skeleton: Skeleton, parent: BonePose, child: BonePose, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, mix: number) {
|
||||
if (parent.inherit != Inherit.Normal || child.inherit != Inherit.Normal) return;
|
||||
let px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX;
|
||||
parent.modifyLocal(skeleton);
|
||||
child.modifyLocal(skeleton);
|
||||
let px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, sx = psx, sy = psy, csx = child.scaleX;
|
||||
let os1 = 0, os2 = 0, s2 = 0;
|
||||
if (psx < 0) {
|
||||
psx = -psx;
|
||||
@ -196,19 +196,17 @@ export class IkConstraint implements Updatable {
|
||||
os2 = 180;
|
||||
} else
|
||||
os2 = 0;
|
||||
let cx = child.ax, cy = 0, cwx = 0, cwy = 0, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
|
||||
let cwx = 0, cwy = 0, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
|
||||
let u = Math.abs(psx - psy) <= 0.0001;
|
||||
if (!u || stretch) {
|
||||
cy = 0;
|
||||
cwx = a * cx + parent.worldX;
|
||||
cwy = c * cx + parent.worldY;
|
||||
child.y = 0;
|
||||
cwx = a * child.x + parent.worldX;
|
||||
cwy = c * child.x + parent.worldY;
|
||||
} else {
|
||||
cy = child.ay;
|
||||
cwx = a * cx + b * cy + parent.worldX;
|
||||
cwy = c * cx + d * cy + parent.worldY;
|
||||
cwx = a * child.x + b * child.y + parent.worldX;
|
||||
cwy = c * child.x + d * child.y + parent.worldY;
|
||||
}
|
||||
let pp = parent.parent;
|
||||
if (!pp) throw new Error("IK parent must itself have a parent.");
|
||||
let pp = parent.bone.parent!.applied;
|
||||
a = pp.a;
|
||||
b = pp.b;
|
||||
c = pp.c;
|
||||
@ -216,10 +214,10 @@ export class IkConstraint implements Updatable {
|
||||
let id = a * d - b * c, x = cwx - pp.worldX, y = cwy - pp.worldY;
|
||||
id = Math.abs(id) <= 0.0001 ? 0 : 1 / id;
|
||||
let dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
|
||||
let l1 = Math.sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
|
||||
let l1 = Math.sqrt(dx * dx + dy * dy), l2 = child.bone.data.length * csx, a1, a2;
|
||||
if (l1 < 0.0001) {
|
||||
this.apply1(parent, targetX, targetY, false, stretch, false, alpha);
|
||||
child.updateWorldTransformWith(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
|
||||
IkConstraint.apply(skeleton, parent, targetX, targetY, false, stretch, false, mix);
|
||||
child.rotation = 0;
|
||||
return;
|
||||
}
|
||||
x = targetX - pp.worldX;
|
||||
@ -248,9 +246,9 @@ export class IkConstraint implements Updatable {
|
||||
cos = 1;
|
||||
a2 = 0;
|
||||
if (stretch) {
|
||||
a = (Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
|
||||
sx *= a;
|
||||
if (uniform) sy *= a;
|
||||
a = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1;
|
||||
parent.scaleX *= a;
|
||||
if (uniform) parent.scaleY *= a;
|
||||
}
|
||||
} else
|
||||
a2 = Math.acos(cos) * bendDir;
|
||||
@ -307,20 +305,18 @@ export class IkConstraint implements Updatable {
|
||||
a2 = maxAngle * bendDir;
|
||||
}
|
||||
}
|
||||
let os = Math.atan2(cy, cx) * s2;
|
||||
let rotation = parent.arotation;
|
||||
a1 = (a1 - os) * MathUtils.radDeg + os1 - rotation;
|
||||
let os = Math.atan2(child.y, child.x) * s2;
|
||||
a1 = (a1 - os) * MathUtils.radDeg + os1 - parent.rotation;
|
||||
if (a1 > 180)
|
||||
a1 -= 360;
|
||||
else if (a1 < -180) //
|
||||
a1 += 360;
|
||||
parent.updateWorldTransformWith(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
|
||||
rotation = child.arotation;
|
||||
a2 = ((a2 + os) * MathUtils.radDeg - child.ashearX) * s2 + os2 - rotation;
|
||||
parent.rotation += a1 * mix;
|
||||
a2 = ((a2 + os) * MathUtils.radDeg - child.shearX) * s2 + os2 - child.rotation;
|
||||
if (a2 > 180)
|
||||
a2 -= 360;
|
||||
else if (a2 < -180) //
|
||||
a2 += 360;
|
||||
child.updateWorldTransformWith(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
|
||||
child.rotation += a2 * mix;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,44 +29,34 @@
|
||||
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { ConstraintData } from "./ConstraintData.js";
|
||||
|
||||
import { IkConstraint } from "./IkConstraint.js";
|
||||
import { IkConstraintPose } from "./IkConstraintPose.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
|
||||
/** Stores the setup pose for an {@link IkConstraint}.
|
||||
* <p>
|
||||
*
|
||||
* See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */
|
||||
export class IkConstraintData extends ConstraintData {
|
||||
export class IkConstraintData extends ConstraintData<IkConstraint, IkConstraintPose> {
|
||||
/** The bones that are constrained by this IK constraint. */
|
||||
bones = new Array<BoneData>();
|
||||
|
||||
/** The bone that is the IK target. */
|
||||
private _target: BoneData | null = null;
|
||||
/** The bone that is the IK target. */
|
||||
public set target (boneData: BoneData) { this._target = boneData; }
|
||||
public get target () {
|
||||
if (!this._target) throw new Error("BoneData not set.")
|
||||
if (!this._target) throw new Error("target cannot be null.")
|
||||
else return this._target;
|
||||
}
|
||||
|
||||
/** Controls the bend direction of the IK bones, either 1 or -1. */
|
||||
bendDirection = 0;
|
||||
|
||||
/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
|
||||
compress = false;
|
||||
|
||||
/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
|
||||
* and the parent bone has local nonuniform scale, stretch is not applied. */
|
||||
stretch = false;
|
||||
|
||||
/** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone
|
||||
* is scaled on both the X and Y axes. */
|
||||
/** When true and {@link IkConstraintPose.compress} or {@link IkConstraintPose.stretch} is used, the bone is scaled
|
||||
* on both the X and Y axes. */
|
||||
uniform = false;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
|
||||
mix = 0;
|
||||
|
||||
/** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */
|
||||
softness = 0;
|
||||
|
||||
constructor (name: string) {
|
||||
super(name, 0, false);
|
||||
super(name, new IkConstraintPose());
|
||||
}
|
||||
|
||||
public create (skeleton: Skeleton) {
|
||||
return new IkConstraint(this, skeleton);
|
||||
}
|
||||
}
|
||||
|
||||
62
spine-ts/spine-core/src/IkConstraintPose.ts
Normal file
62
spine-ts/spine-core/src/IkConstraintPose.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
|
||||
/** Stores the current pose for an IK constraint. */
|
||||
export class IkConstraintPose implements Pose<IkConstraintPose> {
|
||||
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
|
||||
bendDirection = 0;
|
||||
|
||||
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
|
||||
compress = false;
|
||||
|
||||
/** When true and the target is out of range, the parent bone is scaled to reach it.
|
||||
*
|
||||
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link softness} is
|
||||
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
|
||||
stretch = false;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
|
||||
*
|
||||
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
|
||||
mix = 0;
|
||||
|
||||
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
|
||||
* will not straighten completely until the target is this far out of range. */
|
||||
softness = 0;
|
||||
|
||||
public set (pose: IkConstraintPose) {
|
||||
this.mix = pose.mix;
|
||||
this.softness = pose.softness;
|
||||
this.bendDirection = pose.bendDirection;
|
||||
this.compress = pose.compress;
|
||||
this.stretch = pose.stretch;
|
||||
}
|
||||
}
|
||||
@ -27,12 +27,17 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Attachment } from "./attachments/Attachment.js";
|
||||
import { PathAttachment } from "./attachments/PathAttachment.js";
|
||||
import { Bone } from "./Bone.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { Constraint } from "./Constraint.js";
|
||||
import { PathConstraintData, RotateMode, SpacingMode, PositionMode } from "./PathConstraintData.js";
|
||||
import { Physics, Skeleton } from "./Skeleton.js";
|
||||
import { PathConstraintPose } from "./PathConstraintPose.js";
|
||||
import { Physics } from "./Physics.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { Skin, SkinEntry } from "./Skin.js";
|
||||
import { Slot } from "./Slot.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { Utils, MathUtils } from "./Utils.js";
|
||||
|
||||
|
||||
@ -40,7 +45,7 @@ import { Utils, MathUtils } from "./Utils.js";
|
||||
* constrained bones so they follow a {@link PathAttachment}.
|
||||
*
|
||||
* See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */
|
||||
export class PathConstraint implements Updatable {
|
||||
export class PathConstraint extends Constraint<PathConstraint, PathConstraintData, PathConstraintPose> {
|
||||
static NONE = -1; static BEFORE = -2; static AFTER = -3;
|
||||
static epsilon = 0.00001;
|
||||
|
||||
@ -48,70 +53,40 @@ export class PathConstraint implements Updatable {
|
||||
data: PathConstraintData;
|
||||
|
||||
/** The bones that will be modified by this path constraint. */
|
||||
bones: Array<Bone>;
|
||||
bones: Array<BonePose>;
|
||||
|
||||
/** The slot whose path attachment will be used to constrained the bones. */
|
||||
slot: Slot;
|
||||
|
||||
/** The position along the path. */
|
||||
position = 0;
|
||||
|
||||
/** The spacing between bones. */
|
||||
spacing = 0;
|
||||
|
||||
mixRotate = 0;
|
||||
|
||||
mixX = 0;
|
||||
|
||||
mixY = 0;
|
||||
|
||||
spaces = new Array<number>(); positions = new Array<number>();
|
||||
world = new Array<number>(); curves = new Array<number>(); lengths = new Array<number>();
|
||||
segments = new Array<number>();
|
||||
|
||||
active = false;
|
||||
|
||||
constructor (data: PathConstraintData, skeleton: Skeleton) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
super(data, new PathConstraintPose(), new PathConstraintPose());
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
this.data = data;
|
||||
|
||||
this.bones = new Array<Bone>();
|
||||
for (let i = 0, n = data.bones.length; i < n; i++) {
|
||||
let bone = skeleton.findBone(data.bones[i].name);
|
||||
if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}.`);
|
||||
this.bones.push(bone);
|
||||
}
|
||||
let target = skeleton.findSlot(data.slot.name);
|
||||
if (!target) throw new Error(`Couldn't find target bone ${data.slot.name}`);
|
||||
this.slot = target;
|
||||
this.bones = new Array<BonePose>();
|
||||
for (const boneData of this.data.bones)
|
||||
this.bones.push(skeleton.bones[boneData.index].constrained);
|
||||
|
||||
this.position = data.position;
|
||||
this.spacing = data.spacing;
|
||||
this.mixRotate = data.mixRotate;
|
||||
this.mixX = data.mixX;
|
||||
this.mixY = data.mixY;
|
||||
this.slot = skeleton.slots[data.slot.index];;
|
||||
}
|
||||
|
||||
isActive () {
|
||||
return this.active;
|
||||
public copy (skeleton: Skeleton) {
|
||||
var copy = new PathConstraint(this.data, skeleton);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
setToSetupPose () {
|
||||
const data = this.data;
|
||||
this.position = data.position;
|
||||
this.spacing = data.spacing;
|
||||
this.mixRotate = data.mixRotate;
|
||||
this.mixX = data.mixX;
|
||||
this.mixY = data.mixY;
|
||||
}
|
||||
|
||||
update (physics: Physics) {
|
||||
let attachment = this.slot.getAttachment();
|
||||
update (skeleton: Skeleton, physics: Physics) {
|
||||
let attachment = this.slot.applied.attachment;
|
||||
if (!(attachment instanceof PathAttachment)) return;
|
||||
|
||||
let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY;
|
||||
if (mixRotate == 0 && mixX == 0 && mixY == 0) return;
|
||||
const p = this.applied;
|
||||
let mixRotate = p.mixRotate, mixX = p.mixX, mixY = p.mixY;
|
||||
if (mixRotate === 0 && mixX === 0 && mixY === 0) return;
|
||||
|
||||
let data = this.data;
|
||||
let tangents = data.rotateMode == RotateMode.Tangent, scale = data.rotateMode == RotateMode.ChainScale;
|
||||
@ -119,14 +94,14 @@ export class PathConstraint implements Updatable {
|
||||
let bones = this.bones;
|
||||
let boneCount = bones.length, spacesCount = tangents ? boneCount : boneCount + 1;
|
||||
let spaces = Utils.setArraySize(this.spaces, spacesCount), lengths: Array<number> = scale ? this.lengths = Utils.setArraySize(this.lengths, boneCount) : [];
|
||||
let spacing = this.spacing;
|
||||
let spacing = p.spacing;
|
||||
|
||||
switch (data.spacingMode) {
|
||||
case SpacingMode.Percent:
|
||||
if (scale) {
|
||||
for (let i = 0, n = spacesCount - 1; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
let setupLength = bone.data.length;
|
||||
let setupLength = bone.bone.data.length;
|
||||
let x = setupLength * bone.a, y = setupLength * bone.c;
|
||||
lengths[i] = Math.sqrt(x * x + y * y);
|
||||
}
|
||||
@ -137,7 +112,7 @@ export class PathConstraint implements Updatable {
|
||||
let sum = 0;
|
||||
for (let i = 0, n = spacesCount - 1; i < n;) {
|
||||
let bone = bones[i];
|
||||
let setupLength = bone.data.length;
|
||||
let setupLength = bone.bone.data.length;
|
||||
if (setupLength < PathConstraint.epsilon) {
|
||||
if (scale) lengths[i] = 0;
|
||||
spaces[++i] = spacing;
|
||||
@ -159,7 +134,7 @@ export class PathConstraint implements Updatable {
|
||||
let lengthSpacing = data.spacingMode == SpacingMode.Length;
|
||||
for (let i = 0, n = spacesCount - 1; i < n;) {
|
||||
let bone = bones[i];
|
||||
let setupLength = bone.data.length;
|
||||
let setupLength = bone.bone.data.length;
|
||||
if (setupLength < PathConstraint.epsilon) {
|
||||
if (scale) lengths[i] = 0;
|
||||
spaces[++i] = spacing;
|
||||
@ -167,26 +142,26 @@ export class PathConstraint implements Updatable {
|
||||
let x = setupLength * bone.a, y = setupLength * bone.c;
|
||||
let length = Math.sqrt(x * x + y * y);
|
||||
if (scale) lengths[i] = length;
|
||||
spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength;
|
||||
spaces[++i] = (lengthSpacing ? Math.max(0, setupLength + spacing) : spacing) * length / setupLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let positions = this.computeWorldPositions(attachment, spacesCount, tangents);
|
||||
let positions = this.computeWorldPositions(skeleton, attachment, spacesCount, tangents);
|
||||
let boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
|
||||
let tip = false;
|
||||
if (offsetRotation == 0)
|
||||
tip = data.rotateMode == RotateMode.Chain;
|
||||
else {
|
||||
tip = false;
|
||||
let p = this.slot.bone;
|
||||
offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.degRad : -MathUtils.degRad;
|
||||
let bone = this.slot.bone.applied;
|
||||
offsetRotation *= bone.a * bone.d - bone.b * bone.c > 0 ? MathUtils.degRad : -MathUtils.degRad;
|
||||
}
|
||||
for (let i = 0, p = 3; i < boneCount; i++, p += 3) {
|
||||
for (let i = 0, ip = 3, u = skeleton._update; i < boneCount; i++, ip += 3) {
|
||||
let bone = bones[i];
|
||||
bone.worldX += (boneX - bone.worldX) * mixX;
|
||||
bone.worldY += (boneY - bone.worldY) * mixY;
|
||||
let x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
|
||||
let x = positions[ip], y = positions[ip + 1], dx = x - boneX, dy = y - boneY;
|
||||
if (scale) {
|
||||
let length = lengths[i];
|
||||
if (length != 0) {
|
||||
@ -200,16 +175,16 @@ export class PathConstraint implements Updatable {
|
||||
if (mixRotate > 0) {
|
||||
let a = bone.a, b = bone.b, c = bone.c, d = bone.d, r = 0, cos = 0, sin = 0;
|
||||
if (tangents)
|
||||
r = positions[p - 1];
|
||||
r = positions[ip - 1];
|
||||
else if (spaces[i + 1] == 0)
|
||||
r = positions[p + 2];
|
||||
r = positions[ip + 2];
|
||||
else
|
||||
r = Math.atan2(dy, dx);
|
||||
r -= Math.atan2(c, a);
|
||||
if (tip) {
|
||||
cos = Math.cos(r);
|
||||
sin = Math.sin(r);
|
||||
let length = bone.data.length;
|
||||
let length = bone.bone.data.length;
|
||||
boneX += (length * (cos * a - sin * c) - dx) * mixRotate;
|
||||
boneY += (length * (sin * a + cos * c) - dy) * mixRotate;
|
||||
} else {
|
||||
@ -227,13 +202,13 @@ export class PathConstraint implements Updatable {
|
||||
bone.c = sin * a + cos * c;
|
||||
bone.d = sin * b + cos * d;
|
||||
}
|
||||
bone.updateAppliedTransform();
|
||||
bone.modifyWorld(u);
|
||||
}
|
||||
}
|
||||
|
||||
computeWorldPositions (path: PathAttachment, spacesCount: number, tangents: boolean) {
|
||||
computeWorldPositions (skeleton: Skeleton, path: PathAttachment, spacesCount: number, tangents: boolean) {
|
||||
let slot = this.slot;
|
||||
let position = this.position;
|
||||
let position = this.applied.position;
|
||||
let spaces = this.spaces, out = Utils.setArraySize(this.positions, spacesCount * 3 + 2), world: Array<number> = this.world;
|
||||
let closed = path.closed;
|
||||
let verticesLength = path.worldVerticesLength, curveCount = verticesLength / 6, prevCurve = PathConstraint.NONE;
|
||||
@ -246,15 +221,11 @@ export class PathConstraint implements Updatable {
|
||||
|
||||
let multiplier;
|
||||
switch (this.data.spacingMode) {
|
||||
case SpacingMode.Percent:
|
||||
multiplier = pathLength;
|
||||
break;
|
||||
case SpacingMode.Proportional:
|
||||
multiplier = pathLength / spacesCount;
|
||||
break;
|
||||
default:
|
||||
multiplier = 1;
|
||||
case SpacingMode.Percent: multiplier = pathLength; break;
|
||||
case SpacingMode.Proportional: multiplier = pathLength / spacesCount; break;
|
||||
default: multiplier = 1;
|
||||
}
|
||||
|
||||
world = Utils.setArraySize(this.world, 8);
|
||||
for (let i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
|
||||
let space = spaces[i] * multiplier;
|
||||
@ -268,14 +239,14 @@ export class PathConstraint implements Updatable {
|
||||
} else if (p < 0) {
|
||||
if (prevCurve != PathConstraint.BEFORE) {
|
||||
prevCurve = PathConstraint.BEFORE;
|
||||
path.computeWorldVertices(slot, 2, 4, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, 2, 4, world, 0, 2);
|
||||
}
|
||||
this.addBeforePosition(p, world, 0, out, o);
|
||||
continue;
|
||||
} else if (p > pathLength) {
|
||||
if (prevCurve != PathConstraint.AFTER) {
|
||||
prevCurve = PathConstraint.AFTER;
|
||||
path.computeWorldVertices(slot, verticesLength - 6, 4, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, verticesLength - 6, 4, world, 0, 2);
|
||||
}
|
||||
this.addAfterPosition(p - pathLength, world, 0, out, o);
|
||||
continue;
|
||||
@ -296,10 +267,10 @@ export class PathConstraint implements Updatable {
|
||||
if (curve != prevCurve) {
|
||||
prevCurve = curve;
|
||||
if (closed && curve == curveCount) {
|
||||
path.computeWorldVertices(slot, verticesLength - 4, 4, world, 0, 2);
|
||||
path.computeWorldVertices(slot, 0, 4, world, 4, 2);
|
||||
path.computeWorldVertices(skeleton, slot, verticesLength - 4, 4, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, 0, 4, world, 4, 2);
|
||||
} else
|
||||
path.computeWorldVertices(slot, curve * 6 + 2, 8, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, curve * 6 + 2, 8, world, 0, 2);
|
||||
}
|
||||
this.addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
|
||||
tangents || (i > 0 && space == 0));
|
||||
@ -311,15 +282,15 @@ export class PathConstraint implements Updatable {
|
||||
if (closed) {
|
||||
verticesLength += 2;
|
||||
world = Utils.setArraySize(this.world, verticesLength);
|
||||
path.computeWorldVertices(slot, 2, verticesLength - 4, world, 0, 2);
|
||||
path.computeWorldVertices(slot, 0, 2, world, verticesLength - 4, 2);
|
||||
path.computeWorldVertices(skeleton, slot, 2, verticesLength - 4, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, 0, 2, world, verticesLength - 4, 2);
|
||||
world[verticesLength - 2] = world[0];
|
||||
world[verticesLength - 1] = world[1];
|
||||
} else {
|
||||
curveCount--;
|
||||
verticesLength -= 4;
|
||||
world = Utils.setArraySize(this.world, verticesLength);
|
||||
path.computeWorldVertices(slot, 2, verticesLength, world, 0, 2);
|
||||
path.computeWorldVertices(skeleton, slot, 2, verticesLength, world, 0, 2);
|
||||
}
|
||||
|
||||
// Curve lengths.
|
||||
@ -363,14 +334,9 @@ export class PathConstraint implements Updatable {
|
||||
|
||||
let multiplier;
|
||||
switch (this.data.spacingMode) {
|
||||
case SpacingMode.Percent:
|
||||
multiplier = pathLength;
|
||||
break;
|
||||
case SpacingMode.Proportional:
|
||||
multiplier = pathLength / spacesCount;
|
||||
break;
|
||||
default:
|
||||
multiplier = 1;
|
||||
case SpacingMode.Percent: multiplier = pathLength; break;
|
||||
case SpacingMode.Proportional: multiplier = pathLength / spacesCount; break;
|
||||
default: multiplier = 1;
|
||||
}
|
||||
|
||||
let segments = this.segments;
|
||||
@ -384,6 +350,7 @@ export class PathConstraint implements Updatable {
|
||||
p %= pathLength;
|
||||
if (p < 0) p += pathLength;
|
||||
curve = 0;
|
||||
segment = 0;
|
||||
} else if (p < 0) {
|
||||
this.addBeforePosition(p, world, 0, out, o);
|
||||
continue;
|
||||
@ -498,4 +465,53 @@ export class PathConstraint implements Updatable {
|
||||
out[o + 2] = Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
|
||||
}
|
||||
}
|
||||
|
||||
sort (skeleton: Skeleton) {
|
||||
const slotIndex = this.slot.data.index;
|
||||
const slotBone = this.slot.bone;
|
||||
if (skeleton.skin != null) this.sortPathSlot(skeleton, skeleton.skin, slotIndex, slotBone);
|
||||
if (skeleton.data.defaultSkin != null && skeleton.data.defaultSkin != skeleton.skin)
|
||||
this.sortPathSlot(skeleton, skeleton.data.defaultSkin, slotIndex, slotBone);
|
||||
this.sortPath(skeleton, this.slot.pose.attachment, slotBone);
|
||||
const bones = this.bones;
|
||||
const boneCount = this.bones.length;
|
||||
for (let i = 0; i < boneCount; i++) {
|
||||
const bone = bones[i].bone;
|
||||
skeleton.sortBone(bone);
|
||||
skeleton.constrained(bone);
|
||||
}
|
||||
skeleton._updateCache.push(this);
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
skeleton.sortReset(bones[i].bone.children);
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
bones[i].bone.sorted = true;
|
||||
}
|
||||
|
||||
private sortPathSlot (skeleton: Skeleton, skin: Skin, slotIndex: number, slotBone: Bone) {
|
||||
const entries = skin.getAttachments();
|
||||
for (let i = 0, n = entries.length; i < n; i++) {
|
||||
const entry = entries[i];
|
||||
if (entry.slotIndex == slotIndex) this.sortPath(skeleton, entry.attachment, slotBone);
|
||||
}
|
||||
}
|
||||
|
||||
private sortPath (skeleton: Skeleton, attachment: Attachment | null, slotBone: Bone) {
|
||||
if (!(attachment instanceof PathAttachment)) return;
|
||||
const pathBones = attachment.bones;
|
||||
if (pathBones == null)
|
||||
skeleton.sortBone(slotBone);
|
||||
else {
|
||||
const bones = skeleton.bones;
|
||||
for (let i = 0, n = pathBones.length; i < n;) {
|
||||
let nn = pathBones[i++];
|
||||
nn += i;
|
||||
while (i < nn)
|
||||
skeleton.sortBone(bones[pathBones[i++]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSourceActive () {
|
||||
return this.slot.bone.active;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,24 +29,26 @@
|
||||
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { ConstraintData } from "./ConstraintData.js";
|
||||
import { PathConstraint } from "./PathConstraint.js";
|
||||
import { PathConstraintPose } from "./PathConstraintPose.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { SlotData } from "./SlotData.js";
|
||||
|
||||
|
||||
/** Stores the setup pose for a {@link PathConstraint}.
|
||||
*
|
||||
* See [path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */
|
||||
export class PathConstraintData extends ConstraintData {
|
||||
|
||||
export class PathConstraintData extends ConstraintData<PathConstraint, PathConstraintPose> {
|
||||
/** The bones that will be modified by this path constraint. */
|
||||
bones = new Array<BoneData>();
|
||||
|
||||
/** The slot whose path attachment will be used to constrained the bones. */
|
||||
private _slot: SlotData | null = null;
|
||||
public set slot (slotData: SlotData) { this._slot = slotData; }
|
||||
public get slot () {
|
||||
if (!this._slot) throw new Error("SlotData not set.")
|
||||
else return this._slot;
|
||||
else return this._slot;
|
||||
}
|
||||
private _slot: SlotData | null = null;
|
||||
|
||||
/** The mode for positioning the first bone on the path. */
|
||||
positionMode: PositionMode = PositionMode.Fixed;
|
||||
@ -60,18 +62,12 @@ export class PathConstraintData extends ConstraintData {
|
||||
/** An offset added to the constrained bone rotation. */
|
||||
offsetRotation: number = 0;
|
||||
|
||||
/** The position along the path. */
|
||||
position: number = 0;
|
||||
|
||||
/** The spacing between bones. */
|
||||
spacing: number = 0;
|
||||
|
||||
mixRotate = 0;
|
||||
mixX = 0;
|
||||
mixY = 0;
|
||||
|
||||
constructor (name: string) {
|
||||
super(name, 0, false);
|
||||
super(name, new PathConstraintPose());
|
||||
}
|
||||
|
||||
public create (skeleton: Skeleton) {
|
||||
return new PathConstraint(this, skeleton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
spine-ts/spine-core/src/PathConstraintPose.ts
Normal file
57
spine-ts/spine-core/src/PathConstraintPose.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose"
|
||||
|
||||
/** Stores a pose for a path constraint. */
|
||||
export class PathConstraintPose implements Pose<PathConstraintPose> {
|
||||
/** The position along the path. */
|
||||
position: number = 0;
|
||||
|
||||
/** The spacing between bones. */
|
||||
spacing: number = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
|
||||
mixRotate = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
|
||||
mixX = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
|
||||
mixY = 0;
|
||||
|
||||
public set (pose: PathConstraintPose) {
|
||||
this.position = pose.position;
|
||||
this.spacing = pose.spacing;
|
||||
this.mixRotate = pose.mixRotate;
|
||||
this.mixX = pose.mixX;
|
||||
this.mixY = pose.mixY;
|
||||
}
|
||||
|
||||
}
|
||||
43
spine-ts/spine-core/src/Physics.ts
Normal file
43
spine-ts/spine-core/src/Physics.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
/** Determines how physics and other non-deterministic updates are applied. */
|
||||
export enum Physics {
|
||||
/** Physics are not updated or applied. */
|
||||
none,
|
||||
|
||||
/** Physics are reset to the current pose. */
|
||||
reset,
|
||||
|
||||
/** Physics are updated and the pose from physics is applied. */
|
||||
update,
|
||||
|
||||
/** Physics are not updated but the pose from physics is applied. */
|
||||
pose
|
||||
}
|
||||
@ -27,27 +27,20 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Bone } from "./Bone.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { Constraint } from "./Constraint.js";
|
||||
import { Physics } from "./Physics.js";
|
||||
import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
|
||||
import { Physics, Skeleton } from "./Skeleton.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { PhysicsConstraintPose } from "./PhysicsConstraintPose.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { MathUtils } from "./Utils.js";
|
||||
|
||||
|
||||
/** Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
|
||||
* <p>
|
||||
* See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
|
||||
export class PhysicsConstraint implements Updatable {
|
||||
readonly data: PhysicsConstraintData;
|
||||
bone: Bone;
|
||||
|
||||
inertia = 0;
|
||||
strength = 0;
|
||||
damping = 0;
|
||||
massInverse = 0;
|
||||
wind = 0;
|
||||
gravity = 0;
|
||||
mix = 0;
|
||||
export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsConstraintData, PhysicsConstraintPose> {
|
||||
bone: BonePose;
|
||||
|
||||
_reset = true;
|
||||
ux = 0;
|
||||
@ -57,84 +50,86 @@ export class PhysicsConstraint implements Updatable {
|
||||
tx = 0;
|
||||
ty = 0;
|
||||
xOffset = 0;
|
||||
xLag = 0;
|
||||
xVelocity = 0;
|
||||
yOffset = 0;
|
||||
yLag = 0;
|
||||
yVelocity = 0;
|
||||
rotateOffset = 0;
|
||||
rotateLag = 0;
|
||||
rotateVelocity = 0;
|
||||
scaleOffset = 0
|
||||
scaleLag = 0
|
||||
scaleVelocity = 0;
|
||||
|
||||
active = false;
|
||||
|
||||
readonly skeleton: Skeleton;
|
||||
remaining = 0;
|
||||
lastTime = 0;
|
||||
|
||||
constructor (data: PhysicsConstraintData, skeleton: Skeleton) {
|
||||
this.data = data;
|
||||
this.skeleton = skeleton;
|
||||
super(data, new PhysicsConstraintPose(), new PhysicsConstraintPose());
|
||||
if (skeleton == null) throw new Error("skeleton cannot be null.");
|
||||
|
||||
let bone = skeleton.findBone(data.bone.name);
|
||||
if (!bone) throw new Error(`Couldn't find bone ${data.bone.name}.`);
|
||||
this.bone = skeleton.bones[data.bone.index];
|
||||
|
||||
this.inertia = data.inertia;
|
||||
this.strength = data.strength;
|
||||
this.damping = data.damping;
|
||||
this.massInverse = data.massInverse;
|
||||
this.wind = data.wind;
|
||||
this.gravity = data.gravity;
|
||||
this.mix = data.mix;
|
||||
this.bone = skeleton.bones[data.bone.index].constrained;
|
||||
}
|
||||
|
||||
reset () {
|
||||
public copy (skeleton: Skeleton) {
|
||||
var copy = new PhysicsConstraint(this.data, skeleton);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
reset (skeleton: Skeleton) {
|
||||
this.remaining = 0;
|
||||
this.lastTime = this.skeleton.time;
|
||||
this.lastTime = skeleton.time;
|
||||
this._reset = true;
|
||||
this.xOffset = 0;
|
||||
this.xLag = 0;
|
||||
this.xVelocity = 0;
|
||||
this.yOffset = 0;
|
||||
this.yLag = 0;
|
||||
this.yVelocity = 0;
|
||||
this.rotateOffset = 0;
|
||||
this.rotateLag = 0;
|
||||
this.rotateVelocity = 0;
|
||||
this.scaleOffset = 0;
|
||||
this.scaleLag = 0;
|
||||
this.scaleVelocity = 0;
|
||||
}
|
||||
|
||||
setToSetupPose () {
|
||||
const data = this.data;
|
||||
this.inertia = data.inertia;
|
||||
this.strength = data.strength;
|
||||
this.damping = data.damping;
|
||||
this.massInverse = data.massInverse;
|
||||
this.wind = data.wind;
|
||||
this.gravity = data.gravity;
|
||||
this.mix = data.mix;
|
||||
/** Translates the physics constraint so next {@link update} forces are applied as if the bone moved an
|
||||
* additional amount in world space. */
|
||||
translate (x: number, y: number) {
|
||||
this.ux -= x;
|
||||
this.uy -= y;
|
||||
this.cx -= x;
|
||||
this.cy -= y;
|
||||
}
|
||||
|
||||
isActive () {
|
||||
return this.active;
|
||||
/** Rotates the physics constraint so next {@link update} forces are applied as if the bone rotated around the
|
||||
* specified point in world space. */
|
||||
rotate (x: number, y: number, degrees: number) {
|
||||
const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
|
||||
const dx = this.cx - x, dy = this.cy - y;
|
||||
this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy);
|
||||
}
|
||||
|
||||
/** Applies the constraint to the constrained bones. */
|
||||
update (physics: Physics) {
|
||||
const mix = this.mix;
|
||||
if (mix == 0) return;
|
||||
update (skeleton: Skeleton, physics: Physics) {
|
||||
const p = this.applied;
|
||||
const mix = p.mix;
|
||||
if (mix === 0) return;
|
||||
|
||||
const x = this.data.x > 0, y = this.data.y > 0, rotateOrShearX = this.data.rotate > 0 || this.data.shearX > 0, scaleX = this.data.scaleX > 0;
|
||||
const bone = this.bone;
|
||||
const l = bone.data.length;
|
||||
let l = bone.bone.data.length, t = this.data.step, z = 0;
|
||||
|
||||
switch (physics) {
|
||||
case Physics.none:
|
||||
return;
|
||||
case Physics.reset:
|
||||
this.reset();
|
||||
this.reset(skeleton);
|
||||
// Fall through.
|
||||
case Physics.update:
|
||||
const skeleton = this.skeleton;
|
||||
const delta = Math.max(this.skeleton.time - this.lastTime, 0);
|
||||
const delta = Math.max(skeleton.time - this.lastTime, 0), aa = this.remaining;
|
||||
this.remaining += delta;
|
||||
this.lastTime = skeleton.time;
|
||||
|
||||
@ -144,8 +139,8 @@ export class PhysicsConstraint implements Updatable {
|
||||
this.ux = bx;
|
||||
this.uy = by;
|
||||
} else {
|
||||
let a = this.remaining, i = this.inertia, t = this.data.step, f = this.skeleton.data.referenceScale, d = -1;
|
||||
let qx = this.data.limit * delta, qy = qx * Math.abs(skeleton.scaleY);
|
||||
let a = this.remaining, i = p.inertia, f = skeleton.data.referenceScale, d = -1, m = 0, e = 0, qx = this.data.limit * delta,
|
||||
qy = qx * Math.abs(skeleton.scaleY);
|
||||
qx *= Math.abs(skeleton.scaleX);
|
||||
if (x || y) {
|
||||
if (x) {
|
||||
@ -159,28 +154,34 @@ export class PhysicsConstraint implements Updatable {
|
||||
this.uy = by;
|
||||
}
|
||||
if (a >= t) {
|
||||
d = Math.pow(this.damping, 60 * t);
|
||||
const m = this.massInverse * t, e = this.strength, w = this.wind * f * skeleton.scaleX, g = this.gravity * f * skeleton.scaleY;
|
||||
let xs = this.xOffset, ys = this.yOffset;
|
||||
d = Math.pow(p.damping, 60 * t);
|
||||
m = t * p.massInverse;
|
||||
e = p.strength;
|
||||
let w = f * p.wind * skeleton.scaleX, g = f * p.gravity * skeleton.scaleY,
|
||||
ax = w * skeleton.windX + g * skeleton.gravityX, ay = w * skeleton.windY + g * skeleton.gravityY;
|
||||
do {
|
||||
if (x) {
|
||||
this.xVelocity += (w - this.xOffset * e) * m;
|
||||
this.xVelocity += (ax - this.xOffset * e) * m;
|
||||
this.xOffset += this.xVelocity * t;
|
||||
this.xVelocity *= d;
|
||||
}
|
||||
if (y) {
|
||||
this.yVelocity -= (g + this.yOffset * e) * m;
|
||||
this.yVelocity -= (ay + this.yOffset * e) * m;
|
||||
this.yOffset += this.yVelocity * t;
|
||||
this.yVelocity *= d;
|
||||
}
|
||||
a -= t;
|
||||
} while (a >= t);
|
||||
this.xLag = this.xOffset - xs;
|
||||
this.yLag = this.yOffset - ys;
|
||||
}
|
||||
if (x) bone.worldX += this.xOffset * mix * this.data.x;
|
||||
if (y) bone.worldY += this.yOffset * mix * this.data.y;
|
||||
z = Math.max(0, 1 - a / t);
|
||||
if (x) bone.worldX += (this.xOffset - this.xLag * z) * mix * this.data.x;
|
||||
if (y) bone.worldY += (this.yOffset - this.yLag * z) * mix * this.data.y;
|
||||
}
|
||||
if (rotateOrShearX || scaleX) {
|
||||
let ca = Math.atan2(bone.c, bone.a), c = 0, s = 0, mr = 0;
|
||||
let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY;
|
||||
let ca = Math.atan2(bone.c, bone.a), c, s, mr = 0, dx = this.cx - bone.worldX, dy = this.cy - bone.worldY;
|
||||
if (dx > qx)
|
||||
dx = qx;
|
||||
else if (dx < -qx) //
|
||||
@ -189,11 +190,13 @@ export class PhysicsConstraint implements Updatable {
|
||||
dy = qy;
|
||||
else if (dy < -qy) //
|
||||
dy = -qy;
|
||||
a = this.remaining;
|
||||
if (rotateOrShearX) {
|
||||
mr = (this.data.rotate + this.data.shearX) * mix;
|
||||
let r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr;
|
||||
z = this.rotateLag * Math.max(0, 1 - aa / t);
|
||||
let r = Math.atan2(dy + this.ty, dx + this.tx) - ca - (this.rotateOffset - z) * mr;
|
||||
this.rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i;
|
||||
r = this.rotateOffset * mr + ca;
|
||||
r = (this.rotateOffset - z) * mr + ca;
|
||||
c = Math.cos(r);
|
||||
s = Math.sin(r);
|
||||
if (scaleX) {
|
||||
@ -203,22 +206,28 @@ export class PhysicsConstraint implements Updatable {
|
||||
} else {
|
||||
c = Math.cos(ca);
|
||||
s = Math.sin(ca);
|
||||
const r = l * bone.getWorldScaleX();
|
||||
let r = l * bone.getWorldScaleX() - this.scaleLag * Math.max(0, 1 - aa / t);
|
||||
if (r > 0) this.scaleOffset += (dx * c + dy * s) * i / r;
|
||||
}
|
||||
a = this.remaining;
|
||||
if (a >= t) {
|
||||
if (d == -1) d = Math.pow(this.damping, 60 * t);
|
||||
const m = this.massInverse * t, e = this.strength, w = this.wind, g = (Skeleton.yDown ? -this.gravity : this.gravity), h = l / f;
|
||||
if (d == -1) {
|
||||
d = Math.pow(p.damping, 60 * t);
|
||||
m = t * p.massInverse;
|
||||
e = p.strength;
|
||||
}
|
||||
let rs = this.rotateOffset, ss = this.scaleOffset, h = l / f,
|
||||
ax = p.wind * skeleton.windX + p.gravity * skeleton.gravityX,
|
||||
ay = p.wind * skeleton.windY + p.gravity * skeleton.gravityY;
|
||||
while (true) {
|
||||
a -= t;
|
||||
if (scaleX) {
|
||||
this.scaleVelocity += (w * c - g * s - this.scaleOffset * e) * m;
|
||||
this.scaleVelocity += (ax * c - ay * s - this.scaleOffset * e) * m;
|
||||
this.scaleOffset += this.scaleVelocity * t;
|
||||
this.scaleVelocity *= d;
|
||||
}
|
||||
if (rotateOrShearX) {
|
||||
this.rotateVelocity -= ((w * s + g * c) * h + this.rotateOffset * e) * m;
|
||||
this.rotateVelocity -= ((ax * s + ay * c) * h + this.rotateOffset * e) * m;
|
||||
this.rotateOffset += this.rotateVelocity * t;
|
||||
this.rotateVelocity *= d;
|
||||
if (a < t) break;
|
||||
@ -228,7 +237,10 @@ export class PhysicsConstraint implements Updatable {
|
||||
} else if (a < t) //
|
||||
break;
|
||||
}
|
||||
this.rotateLag = this.rotateOffset - rs;
|
||||
this.scaleLag = this.scaleOffset - ss;
|
||||
}
|
||||
z = Math.max(0, 1 - a / t);
|
||||
}
|
||||
this.remaining = a;
|
||||
}
|
||||
@ -236,12 +248,13 @@ export class PhysicsConstraint implements Updatable {
|
||||
this.cy = bone.worldY;
|
||||
break;
|
||||
case Physics.pose:
|
||||
if (x) bone.worldX += this.xOffset * mix * this.data.x;
|
||||
if (y) bone.worldY += this.yOffset * mix * this.data.y;
|
||||
z = Math.max(0, 1 - this.remaining / t);
|
||||
if (x) bone.worldX += (this.xOffset - this.xLag * z) * mix * this.data.x;
|
||||
if (y) bone.worldY += (this.yOffset - this.yLag * z) * mix * this.data.y;
|
||||
}
|
||||
|
||||
if (rotateOrShearX) {
|
||||
let o = this.rotateOffset * mix, s = 0, c = 0, a = 0;
|
||||
let o = (this.rotateOffset - this.rotateLag * z) * mix, s = 0, c = 0, a = 0;
|
||||
if (this.data.shearX > 0) {
|
||||
let r = 0;
|
||||
if (this.data.rotate > 0) {
|
||||
@ -271,7 +284,7 @@ export class PhysicsConstraint implements Updatable {
|
||||
}
|
||||
}
|
||||
if (scaleX) {
|
||||
const s = 1 + this.scaleOffset * mix * this.data.scaleX;
|
||||
const s = 1 + (this.scaleOffset - this.scaleLag * z) * mix * this.data.scaleX;
|
||||
bone.a *= s;
|
||||
bone.c *= s;
|
||||
}
|
||||
@ -279,23 +292,19 @@ export class PhysicsConstraint implements Updatable {
|
||||
this.tx = l * bone.a;
|
||||
this.ty = l * bone.c;
|
||||
}
|
||||
bone.updateAppliedTransform();
|
||||
bone.modifyWorld(skeleton._update);
|
||||
}
|
||||
|
||||
/** Translates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone moved an additional
|
||||
* amount in world space. */
|
||||
translate (x: number, y: number) {
|
||||
this.ux -= x;
|
||||
this.uy -= y;
|
||||
this.cx -= x;
|
||||
this.cy -= y;
|
||||
sort (skeleton: Skeleton) {
|
||||
const bone = this.bone.bone;
|
||||
skeleton.sortBone(bone);
|
||||
skeleton._updateCache.push(this);
|
||||
skeleton.sortReset(bone.children);
|
||||
skeleton.constrained(bone);
|
||||
}
|
||||
|
||||
/** Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the
|
||||
* specified point in world space. */
|
||||
rotate (x: number, y: number, degrees: number) {
|
||||
const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
|
||||
const dx = this.cx - x, dy = this.cy - y;
|
||||
this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy);
|
||||
isSourceActive () {
|
||||
return this.bone.bone.active;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,19 +29,22 @@
|
||||
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { ConstraintData } from "./ConstraintData.js";
|
||||
import { PhysicsConstraint } from "./PhysicsConstraint.js";
|
||||
import { PhysicsConstraintPose } from "./PhysicsConstraintPose.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
|
||||
|
||||
/** Stores the setup pose for a {@link PhysicsConstraint}.
|
||||
* <p>
|
||||
* See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
|
||||
export class PhysicsConstraintData extends ConstraintData {
|
||||
private _bone: BoneData | null = null;
|
||||
export class PhysicsConstraintData extends ConstraintData<PhysicsConstraint, PhysicsConstraintPose> {
|
||||
/** The bone constrained by this physics constraint. */
|
||||
public set bone (boneData: BoneData) { this._bone = boneData; }
|
||||
public get bone () {
|
||||
if (!this._bone) throw new Error("BoneData not set.")
|
||||
else return this._bone;
|
||||
else return this._bone;
|
||||
}
|
||||
private _bone: BoneData | null = null;
|
||||
|
||||
x = 0;
|
||||
y = 0;
|
||||
@ -50,14 +53,6 @@ export class PhysicsConstraintData extends ConstraintData {
|
||||
shearX = 0;
|
||||
limit = 0;
|
||||
step = 0;
|
||||
inertia = 0;
|
||||
strength = 0;
|
||||
damping = 0;
|
||||
massInverse = 0;
|
||||
wind = 0;
|
||||
gravity = 0;
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
|
||||
mix = 0;
|
||||
inertiaGlobal = false;
|
||||
strengthGlobal = false;
|
||||
dampingGlobal = false;
|
||||
@ -67,6 +62,10 @@ export class PhysicsConstraintData extends ConstraintData {
|
||||
mixGlobal = false;
|
||||
|
||||
constructor (name: string) {
|
||||
super(name, 0, false);
|
||||
super(name, new PhysicsConstraintPose());
|
||||
}
|
||||
|
||||
public create (skeleton: Skeleton) {
|
||||
return new PhysicsConstraint(this, skeleton);
|
||||
}
|
||||
}
|
||||
|
||||
54
spine-ts/spine-core/src/PhysicsConstraintPose.ts
Normal file
54
spine-ts/spine-core/src/PhysicsConstraintPose.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose"
|
||||
|
||||
/** Stores a pose for a physics constraint. */
|
||||
export class PhysicsConstraintPose implements Pose<PhysicsConstraintPose> {
|
||||
inertia = 0;
|
||||
strength = 0;
|
||||
damping = 0;
|
||||
massInverse = 0;
|
||||
wind = 0;
|
||||
gravity = 0;
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
|
||||
mix = 0;
|
||||
|
||||
public set (pose: PhysicsConstraintPose) {
|
||||
this.inertia = pose.inertia;
|
||||
this.strength = pose.strength;
|
||||
this.damping = pose.damping;
|
||||
this.massInverse = pose.massInverse;
|
||||
this.wind = pose.wind;
|
||||
this.gravity = pose.gravity;
|
||||
this.mix = pose.mix;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
32
spine-ts/spine-core/src/Pose.ts
Normal file
32
spine-ts/spine-core/src/Pose.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
export interface Pose<P> {
|
||||
set (pose: P): void;
|
||||
}
|
||||
55
spine-ts/spine-core/src/Posed.ts
Normal file
55
spine-ts/spine-core/src/Posed.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
import { PosedData } from "./PosedData";
|
||||
|
||||
export abstract class Posed<
|
||||
D extends PosedData<P>,
|
||||
P extends Pose<any>,
|
||||
A extends P> {
|
||||
|
||||
/** The constraint's setup pose data. */
|
||||
readonly data: D;
|
||||
readonly pose: P;
|
||||
readonly constrained: A;
|
||||
applied: A;
|
||||
|
||||
constructor (data: D, pose: P, constrained: A) {
|
||||
if (data == null) throw new Error("data cannot be null.");
|
||||
this.data = data;
|
||||
this.pose = pose;
|
||||
this.constrained = constrained;
|
||||
this.applied = pose as A;
|
||||
}
|
||||
|
||||
public setupPose (): void {
|
||||
this.pose.set(this.data.setup);
|
||||
}
|
||||
}
|
||||
59
spine-ts/spine-core/src/PosedActive.ts
Normal file
59
spine-ts/spine-core/src/PosedActive.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
import { Posed } from "./Posed";
|
||||
import { PosedData } from "./PosedData";
|
||||
|
||||
import type { Skeleton } from "./Skeleton";
|
||||
|
||||
export abstract class PosedActive<
|
||||
D extends PosedData<P>,
|
||||
P extends Pose<any>,
|
||||
A extends P>
|
||||
extends Posed<D, P, A> {
|
||||
|
||||
active = false;
|
||||
|
||||
constructor (data: D, pose: P, constrained: A) {
|
||||
super(data, pose, constrained);
|
||||
this.setupPose();
|
||||
}
|
||||
|
||||
/** Returns false when this constraint won't be updated by
|
||||
* {@link Skeleton.updateWorldTransform()} because a skin is required and the
|
||||
* {@link Skeleton.getSkin() active skin} does not contain this item.
|
||||
* @see Skin.getBones()
|
||||
* @see Skin.getConstraints()
|
||||
* @see PosedData.getSkinRequired()
|
||||
* @see Skeleton.updateCache() */
|
||||
public isActive (): boolean {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
51
spine-ts/spine-core/src/PosedData.ts
Normal file
51
spine-ts/spine-core/src/PosedData.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
|
||||
/** The base class for all constrained datas. */
|
||||
export abstract class PosedData<P extends Pose<any>> {
|
||||
/** The constraint's name, which is unique across all constraints in the skeleton of the same type. */
|
||||
readonly name: string;
|
||||
|
||||
readonly setup: P;
|
||||
|
||||
/** When true, {@link Skeleton.updateWorldTransform} only updates this constraint if the {@link Skeleton.skin}
|
||||
* contains this constraint.
|
||||
*
|
||||
* See {@link Skin.constraints}. */
|
||||
skinRequired = false;
|
||||
|
||||
constructor (name: string, setup: P) {
|
||||
if (name == null) throw new Error("name cannot be null.");
|
||||
this.name = name;
|
||||
this.setup = setup;
|
||||
}
|
||||
|
||||
}
|
||||
@ -30,19 +30,18 @@
|
||||
import { Attachment } from "./attachments/Attachment.js";
|
||||
import { ClippingAttachment } from "./attachments/ClippingAttachment.js";
|
||||
import { MeshAttachment } from "./attachments/MeshAttachment.js";
|
||||
import { PathAttachment } from "./attachments/PathAttachment.js";
|
||||
import { RegionAttachment } from "./attachments/RegionAttachment.js";
|
||||
import { Bone } from "./Bone.js";
|
||||
import { IkConstraint } from "./IkConstraint.js";
|
||||
import { PathConstraint } from "./PathConstraint.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { Constraint } from "./Constraint.js";
|
||||
import { Physics } from "./Physics.js";
|
||||
import { PhysicsConstraint } from "./PhysicsConstraint.js";
|
||||
import { Posed } from "./Posed.js";
|
||||
import { SkeletonClipping } from "./SkeletonClipping.js";
|
||||
import { SkeletonData } from "./SkeletonData.js";
|
||||
import { Skin } from "./Skin.js";
|
||||
import { Slot } from "./Slot.js";
|
||||
import { TransformConstraint } from "./TransformConstraint.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { Color, Utils, MathUtils, Vector2, NumberArrayLike } from "./Utils.js";
|
||||
import { Color, Utils, Vector2, NumberArrayLike } from "./Utils.js";
|
||||
|
||||
/** Stores the current pose for a skeleton.
|
||||
*
|
||||
@ -52,47 +51,44 @@ export class Skeleton {
|
||||
static yDown = false;
|
||||
|
||||
/** The skeleton's setup pose data. */
|
||||
data: SkeletonData;
|
||||
readonly data: SkeletonData;
|
||||
|
||||
/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
|
||||
bones: Array<Bone>;
|
||||
readonly bones: Array<Bone>;
|
||||
|
||||
/** The skeleton's slots in the setup pose draw order. */
|
||||
slots: Array<Slot>;
|
||||
/** The skeleton's slots. */
|
||||
readonly slots: Array<Slot>;
|
||||
|
||||
/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
|
||||
drawOrder: Array<Slot>;
|
||||
|
||||
/** The skeleton's IK constraints. */
|
||||
ikConstraints: Array<IkConstraint>;
|
||||
|
||||
/** The skeleton's transform constraints. */
|
||||
transformConstraints: Array<TransformConstraint>;
|
||||
|
||||
/** The skeleton's path constraints. */
|
||||
pathConstraints: Array<PathConstraint>;
|
||||
|
||||
/** The skeleton's constraints. */
|
||||
readonly constraints: Array<Constraint<any, any, any>>;
|
||||
|
||||
/** The skeleton's physics constraints. */
|
||||
physicsConstraints: Array<PhysicsConstraint>;
|
||||
readonly physics: Array<PhysicsConstraint>;
|
||||
|
||||
/** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */
|
||||
_updateCache = new Array<Updatable>();
|
||||
/** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link updateCache()}. */
|
||||
readonly _updateCache = new Array();
|
||||
|
||||
readonly resetCache: Array<Posed<any, any, any>> = new Array();
|
||||
|
||||
/** The skeleton's current skin. May be null. */
|
||||
skin: Skin | null = null;
|
||||
|
||||
/** The color to tint all the skeleton's attachments. */
|
||||
color: Color;
|
||||
readonly color: Color;
|
||||
|
||||
/** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale
|
||||
* inheritance. */
|
||||
/** Scales the entire skeleton on the X axis.
|
||||
*
|
||||
* Bones that do not inherit scale are still affected by this property. */
|
||||
scaleX = 1;
|
||||
|
||||
/** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale
|
||||
* inheritance. */
|
||||
private _scaleY = 1;
|
||||
|
||||
/** Scales the entire skeleton on the Y axis.
|
||||
*
|
||||
* Bones that do not inherit scale are still affected by this property. */
|
||||
public get scaleY () {
|
||||
return Skeleton.yDown ? -this._scaleY : this._scaleY;
|
||||
}
|
||||
@ -101,17 +97,28 @@ export class Skeleton {
|
||||
this._scaleY = scaleY;
|
||||
}
|
||||
|
||||
/** Sets the skeleton X position, which is added to the root bone worldX position. */
|
||||
/** Sets the skeleton X position, which is added to the root bone worldX position.
|
||||
*
|
||||
* Bones that do not inherit translation are still affected by this property. */
|
||||
x = 0;
|
||||
|
||||
/** Sets the skeleton Y position, which is added to the root bone worldY position. */
|
||||
/** Sets the skeleton Y position, which is added to the root bone worldY position.
|
||||
*
|
||||
* Bones that do not inherit translation are still affected by this property. */
|
||||
y = 0;
|
||||
|
||||
/** Returns the skeleton's time. This is used for time-based manipulations, such as {@link PhysicsConstraint}.
|
||||
* <p>
|
||||
* See {@link #update(float)}. */
|
||||
*
|
||||
* See {@link _update()}. */
|
||||
time = 0;
|
||||
|
||||
windX = 1;
|
||||
windY = 0;
|
||||
gravityX = 0;
|
||||
gravityY = 1;
|
||||
|
||||
_update = 0;
|
||||
|
||||
constructor (data: SkeletonData) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
this.data = data;
|
||||
@ -121,10 +128,10 @@ export class Skeleton {
|
||||
let boneData = data.bones[i];
|
||||
let bone: Bone;
|
||||
if (!boneData.parent)
|
||||
bone = new Bone(boneData, this, null);
|
||||
bone = new Bone(boneData, null);
|
||||
else {
|
||||
let parent = this.bones[boneData.parent.index];
|
||||
bone = new Bone(boneData, this, parent);
|
||||
bone = new Bone(boneData, parent);
|
||||
parent.children.push(bone);
|
||||
}
|
||||
this.bones.push(bone);
|
||||
@ -132,55 +139,45 @@ export class Skeleton {
|
||||
|
||||
this.slots = new Array<Slot>();
|
||||
this.drawOrder = new Array<Slot>();
|
||||
for (let i = 0; i < data.slots.length; i++) {
|
||||
let slotData = data.slots[i];
|
||||
let bone = this.bones[slotData.boneData.index];
|
||||
let slot = new Slot(slotData, bone);
|
||||
for (const slotData of this.data.slots) {
|
||||
let slot = new Slot(slotData, this);
|
||||
this.slots.push(slot);
|
||||
this.drawOrder.push(slot);
|
||||
}
|
||||
|
||||
this.ikConstraints = new Array<IkConstraint>();
|
||||
for (let i = 0; i < data.ikConstraints.length; i++) {
|
||||
let ikConstraintData = data.ikConstraints[i];
|
||||
this.ikConstraints.push(new IkConstraint(ikConstraintData, this));
|
||||
}
|
||||
|
||||
this.transformConstraints = new Array<TransformConstraint>();
|
||||
for (let i = 0; i < data.transformConstraints.length; i++) {
|
||||
let transformConstraintData = data.transformConstraints[i];
|
||||
this.transformConstraints.push(new TransformConstraint(transformConstraintData, this));
|
||||
}
|
||||
|
||||
this.pathConstraints = new Array<PathConstraint>();
|
||||
for (let i = 0; i < data.pathConstraints.length; i++) {
|
||||
let pathConstraintData = data.pathConstraints[i];
|
||||
this.pathConstraints.push(new PathConstraint(pathConstraintData, this));
|
||||
}
|
||||
|
||||
this.physicsConstraints = new Array<PhysicsConstraint>();
|
||||
for (let i = 0; i < data.physicsConstraints.length; i++) {
|
||||
let physicsConstraintData = data.physicsConstraints[i];
|
||||
this.physicsConstraints.push(new PhysicsConstraint(physicsConstraintData, this));
|
||||
this.physics = new Array<PhysicsConstraint>();
|
||||
this.constraints = new Array<Constraint<any, any, any>>();
|
||||
for (const constraintData of this.data.constraints) {
|
||||
const constraint = constraintData.create(this);
|
||||
if (constraint instanceof PhysicsConstraint) this.physics.push(constraint);
|
||||
this.constraints.push(constraint);
|
||||
}
|
||||
|
||||
this.color = new Color(1, 1, 1, 1);
|
||||
|
||||
this.updateCache();
|
||||
}
|
||||
|
||||
/** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones,
|
||||
/** Caches information about bones and constraints. Must be called if the {@link getSkin()} is modified or if bones,
|
||||
* constraints, or weighted path attachments are added or removed. */
|
||||
updateCache () {
|
||||
let updateCache = this._updateCache;
|
||||
updateCache.length = 0;
|
||||
this._updateCache.length = 0;
|
||||
this.resetCache.length = 0;
|
||||
|
||||
let slots = this.slots;
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
const slot = slots[i];
|
||||
slot.applied = slot.pose;
|
||||
}
|
||||
|
||||
let bones = this.bones;
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
const boneCount = bones.length;
|
||||
for (let i = 0, n = boneCount; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
bone.sorted = bone.data.skinRequired;
|
||||
bone.active = !bone.sorted;
|
||||
bone.applied = bone.pose as BonePose;
|
||||
}
|
||||
|
||||
if (this.skin) {
|
||||
let skinBones = this.skin.bones;
|
||||
for (let i = 0, n = this.skin.bones.length; i < n; i++) {
|
||||
@ -193,171 +190,39 @@ export class Skeleton {
|
||||
}
|
||||
}
|
||||
|
||||
// IK first, lowest hierarchy depth first.
|
||||
let ikConstraints = this.ikConstraints;
|
||||
let transformConstraints = this.transformConstraints;
|
||||
let pathConstraints = this.pathConstraints;
|
||||
let physicsConstraints = this.physicsConstraints;
|
||||
let ikCount = ikConstraints.length, transformCount = transformConstraints.length, pathCount = pathConstraints.length, physicsCount = this.physicsConstraints.length;
|
||||
let constraintCount = ikCount + transformCount + pathCount + physicsCount;
|
||||
|
||||
outer:
|
||||
for (let i = 0; i < constraintCount; i++) {
|
||||
for (let ii = 0; ii < ikCount; ii++) {
|
||||
let constraint = ikConstraints[ii];
|
||||
if (constraint.data.order == i) {
|
||||
this.sortIkConstraint(constraint);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
for (let ii = 0; ii < transformCount; ii++) {
|
||||
let constraint = transformConstraints[ii];
|
||||
if (constraint.data.order == i) {
|
||||
this.sortTransformConstraint(constraint);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
for (let ii = 0; ii < pathCount; ii++) {
|
||||
let constraint = pathConstraints[ii];
|
||||
if (constraint.data.order == i) {
|
||||
this.sortPathConstraint(constraint);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
for (let ii = 0; ii < physicsCount; ii++) {
|
||||
const constraint = physicsConstraints[ii];
|
||||
if (constraint.data.order == i) {
|
||||
this.sortPhysicsConstraint(constraint);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
let constraints = this.constraints;
|
||||
let n = this.constraints.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const constraint = constraints[i];
|
||||
constraint.applied = constraint.pose;
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
const constraint = constraints[i];
|
||||
constraint.active = constraint.isSourceActive()
|
||||
&& (!constraint.data.skinRequired || (this.skin != null && this.skin.constraints.includes(constraint.data)));
|
||||
if (constraint.active) constraint.sort(this);
|
||||
}
|
||||
|
||||
for (let i = 0, n = bones.length; i < n; i++)
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
this.sortBone(bones[i]);
|
||||
}
|
||||
|
||||
sortIkConstraint (constraint: IkConstraint) {
|
||||
constraint.active = constraint.target.isActive() && (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!;
|
||||
if (!constraint.active) return;
|
||||
|
||||
this.sortBone(constraint.target);
|
||||
|
||||
let constrained = constraint.bones;
|
||||
let parent = constrained[0];
|
||||
this.sortBone(parent);
|
||||
|
||||
if (constrained.length == 1) {
|
||||
this._updateCache.push(constraint);
|
||||
this.sortReset(parent.children);
|
||||
} else {
|
||||
let child = constrained[constrained.length - 1];
|
||||
this.sortBone(child);
|
||||
|
||||
this._updateCache.push(constraint);
|
||||
|
||||
this.sortReset(parent.children);
|
||||
child.sorted = true;
|
||||
}
|
||||
}
|
||||
|
||||
sortPathConstraint (constraint: PathConstraint) {
|
||||
constraint.active = constraint.slot.bone.isActive()
|
||||
&& (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!;
|
||||
if (!constraint.active) return;
|
||||
|
||||
let slot = constraint.slot;
|
||||
let slotIndex = slot.data.index;
|
||||
let slotBone = slot.bone;
|
||||
if (this.skin) this.sortPathConstraintAttachment(this.skin, slotIndex, slotBone);
|
||||
if (this.data.defaultSkin && this.data.defaultSkin != this.skin)
|
||||
this.sortPathConstraintAttachment(this.data.defaultSkin, slotIndex, slotBone);
|
||||
for (let i = 0, n = this.data.skins.length; i < n; i++)
|
||||
this.sortPathConstraintAttachment(this.data.skins[i], slotIndex, slotBone);
|
||||
|
||||
this.sortPathConstraintAttachmentWith(slot.attachment, slotBone);
|
||||
|
||||
let constrained = constraint.bones;
|
||||
let boneCount = constrained.length;
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
this.sortBone(constrained[i]);
|
||||
|
||||
this._updateCache.push(constraint);
|
||||
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
this.sortReset(constrained[i].children);
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
constrained[i].sorted = true;
|
||||
}
|
||||
|
||||
sortTransformConstraint (constraint: TransformConstraint) {
|
||||
constraint.active = constraint.source.isActive() && (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!;
|
||||
if (!constraint.active) return;
|
||||
|
||||
this.sortBone(constraint.source);
|
||||
|
||||
let constrained = constraint.bones;
|
||||
let boneCount = constrained.length;
|
||||
if (constraint.data.localSource) {
|
||||
for (let i = 0; i < boneCount; i++) {
|
||||
let child = constrained[i];
|
||||
this.sortBone(child.parent!);
|
||||
this.sortBone(child);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < boneCount; i++) {
|
||||
this.sortBone(constrained[i]);
|
||||
}
|
||||
n = this._updateCache.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const updateable = this._updateCache[i];
|
||||
if (updateable instanceof Bone) this._updateCache[i] = updateable.applied;
|
||||
}
|
||||
|
||||
this._updateCache.push(constraint);
|
||||
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
this.sortReset(constrained[i].children);
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
constrained[i].sorted = true;
|
||||
}
|
||||
|
||||
sortPathConstraintAttachment (skin: Skin, slotIndex: number, slotBone: Bone) {
|
||||
let attachments = skin.attachments[slotIndex];
|
||||
if (!attachments) return;
|
||||
for (let key in attachments) {
|
||||
this.sortPathConstraintAttachmentWith(attachments[key], slotBone);
|
||||
constrained (object: Posed<any, any, any>) {
|
||||
if (object.pose === object.applied) {
|
||||
object.applied = object.constrained;
|
||||
this.resetCache.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
sortPathConstraintAttachmentWith (attachment: Attachment | null, slotBone: Bone) {
|
||||
if (!(attachment instanceof PathAttachment)) return;
|
||||
let pathBones = attachment.bones;
|
||||
if (!pathBones)
|
||||
this.sortBone(slotBone);
|
||||
else {
|
||||
let bones = this.bones;
|
||||
for (let i = 0, n = pathBones.length; i < n;) {
|
||||
let nn = pathBones[i++];
|
||||
nn += i;
|
||||
while (i < nn)
|
||||
this.sortBone(bones[pathBones[i++]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sortPhysicsConstraint (constraint: PhysicsConstraint) {
|
||||
const bone = constraint.bone;
|
||||
constraint.active = bone.active && (!constraint.data.skinRequired || (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true)));
|
||||
if (!constraint.active) return;
|
||||
|
||||
this.sortBone(bone);
|
||||
|
||||
this._updateCache.push(constraint);
|
||||
|
||||
this.sortReset(bone.children);
|
||||
bone.sorted = true;
|
||||
}
|
||||
|
||||
sortBone (bone: Bone) {
|
||||
if (!bone) return;
|
||||
if (bone.sorted) return;
|
||||
if (bone.sorted || !bone.active) return;
|
||||
let parent = bone.parent;
|
||||
if (parent) this.sortBone(parent);
|
||||
bone.sorted = true;
|
||||
@ -367,149 +232,112 @@ export class Skeleton {
|
||||
sortReset (bones: Array<Bone>) {
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
if (!bone.active) continue;
|
||||
if (bone.sorted) this.sortReset(bone.children);
|
||||
bone.sorted = false;
|
||||
if (!bone.active) {
|
||||
if (bone.sorted) this.sortReset(bone.children);
|
||||
bone.sorted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the world transform for each bone and applies all constraints.
|
||||
*
|
||||
* See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
|
||||
* <p>
|
||||
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
|
||||
* Runtimes Guide. */
|
||||
updateWorldTransform (physics: Physics) {
|
||||
if (physics === undefined || physics === null) throw new Error("physics is undefined");
|
||||
let bones = this.bones;
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
bone.ax = bone.x;
|
||||
bone.ay = bone.y;
|
||||
bone.arotation = bone.rotation;
|
||||
bone.ascaleX = bone.scaleX;
|
||||
bone.ascaleY = bone.scaleY;
|
||||
bone.ashearX = bone.shearX;
|
||||
bone.ashearY = bone.shearY;
|
||||
updateWorldTransform(physics: Physics): void {
|
||||
this._update++;
|
||||
|
||||
const resetCache = this.resetCache;
|
||||
for (let i = 0, n = this.resetCache.length; i < n; i++) {
|
||||
const object = resetCache[i];
|
||||
object.applied.set(object.pose);
|
||||
}
|
||||
|
||||
let updateCache = this._updateCache;
|
||||
for (let i = 0, n = updateCache.length; i < n; i++)
|
||||
updateCache[i].update(physics);
|
||||
}
|
||||
|
||||
updateWorldTransformWith (physics: Physics, parent: Bone) {
|
||||
if (!parent) throw new Error("parent cannot be null.");
|
||||
|
||||
let bones = this.bones;
|
||||
for (let i = 1, n = bones.length; i < n; i++) { // Skip root bone.
|
||||
let bone = bones[i];
|
||||
bone.ax = bone.x;
|
||||
bone.ay = bone.y;
|
||||
bone.arotation = bone.rotation;
|
||||
bone.ascaleX = bone.scaleX;
|
||||
bone.ascaleY = bone.scaleY;
|
||||
bone.ashearX = bone.shearX;
|
||||
bone.ashearY = bone.shearY;
|
||||
}
|
||||
|
||||
// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
|
||||
let rootBone = this.getRootBone();
|
||||
if (!rootBone) throw new Error("Root bone must not be null.");
|
||||
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
|
||||
rootBone.worldX = pa * this.x + pb * this.y + parent.worldX;
|
||||
rootBone.worldY = pc * this.x + pd * this.y + parent.worldY;
|
||||
|
||||
const rx = (rootBone.rotation + rootBone.shearX) * MathUtils.degRad;
|
||||
const ry = (rootBone.rotation + 90 + rootBone.shearY) * MathUtils.degRad;
|
||||
const la = Math.cos(rx) * rootBone.scaleX;
|
||||
const lb = Math.cos(ry) * rootBone.scaleY;
|
||||
const lc = Math.sin(rx) * rootBone.scaleX;
|
||||
const ld = Math.sin(ry) * rootBone.scaleY;
|
||||
rootBone.a = (pa * la + pb * lc) * this.scaleX;
|
||||
rootBone.b = (pa * lb + pb * ld) * this.scaleX;
|
||||
rootBone.c = (pc * la + pd * lc) * this.scaleY;
|
||||
rootBone.d = (pc * lb + pd * ld) * this.scaleY;
|
||||
|
||||
// Update everything except root bone.
|
||||
let updateCache = this._updateCache;
|
||||
for (let i = 0, n = updateCache.length; i < n; i++) {
|
||||
let updatable = updateCache[i];
|
||||
if (updatable != rootBone) updatable.update(physics);
|
||||
}
|
||||
const updateCache = this._updateCache;
|
||||
for (let i = 0, n = this._updateCache.length; i < n; i++)
|
||||
updateCache[i].update(this, physics);
|
||||
}
|
||||
|
||||
/** Sets the bones, constraints, and slots to their setup pose values. */
|
||||
setToSetupPose () {
|
||||
this.setBonesToSetupPose();
|
||||
this.setSlotsToSetupPose();
|
||||
setupPose () {
|
||||
this.setupPoseBones();
|
||||
this.setupPoseSlots();
|
||||
}
|
||||
|
||||
/** Sets the bones and constraints to their setup pose values. */
|
||||
setBonesToSetupPose () {
|
||||
for (const bone of this.bones) bone.setToSetupPose();
|
||||
for (const constraint of this.ikConstraints) constraint.setToSetupPose();
|
||||
for (const constraint of this.transformConstraints) constraint.setToSetupPose();
|
||||
for (const constraint of this.pathConstraints) constraint.setToSetupPose();
|
||||
for (const constraint of this.physicsConstraints) constraint.setToSetupPose();
|
||||
setupPoseBones () {
|
||||
const bones = this.bones;
|
||||
for (let i = 0, n = bones.length; i < n; i++)
|
||||
bones[i].setupPose();
|
||||
|
||||
const constraints = this.constraints;
|
||||
for (let i = 0, n = constraints.length; i < n; i++)
|
||||
constraints[i].setupPose();
|
||||
}
|
||||
|
||||
/** Sets the slots and draw order to their setup pose values. */
|
||||
setSlotsToSetupPose () {
|
||||
setupPoseSlots () {
|
||||
let slots = this.slots;
|
||||
Utils.arrayCopy(slots, 0, this.drawOrder, 0, slots.length);
|
||||
for (let i = 0, n = slots.length; i < n; i++)
|
||||
slots[i].setToSetupPose();
|
||||
slots[i].setupPose();
|
||||
}
|
||||
|
||||
/** @returns May return null. */
|
||||
/** Returns the root bone, or null if the skeleton has no bones. */
|
||||
getRootBone () {
|
||||
if (this.bones.length == 0) return null;
|
||||
return this.bones[0];
|
||||
}
|
||||
|
||||
/** @returns May be null. */
|
||||
/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
|
||||
* repeatedly. */
|
||||
findBone (boneName: string) {
|
||||
if (!boneName) throw new Error("boneName cannot be null.");
|
||||
let bones = this.bones;
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
if (bone.data.name == boneName) return bone;
|
||||
}
|
||||
for (let i = 0, n = bones.length; i < n; i++)
|
||||
if (bones[i].data.name == boneName) return bones[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
|
||||
* repeatedly.
|
||||
* @returns May be null. */
|
||||
* repeatedly. */
|
||||
findSlot (slotName: string) {
|
||||
if (!slotName) throw new Error("slotName cannot be null.");
|
||||
let slots = this.slots;
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (slot.data.name == slotName) return slot;
|
||||
}
|
||||
for (let i = 0, n = slots.length; i < n; i++)
|
||||
if (slots[i].data.name == slotName) return slots[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Sets a skin by name.
|
||||
*
|
||||
* See {@link #setSkin()}. */
|
||||
setSkinByName (skinName: string) {
|
||||
* See {@link setSkin()}. */
|
||||
setSkin (skinName: string): void;
|
||||
|
||||
/** Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default skin}. If the
|
||||
* skin is changed, {@link updateCache} is called.
|
||||
* <p>
|
||||
* Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
|
||||
* old skin, each slot's setup mode attachment is attached from the new skin.
|
||||
* <p>
|
||||
* After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
|
||||
* {@link setupPoseSlots()}. Also, often {@link AnimationState.apply(Skeleton)} is called before the next time the skeleton is
|
||||
* rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. */
|
||||
setSkin (newSkin: Skin): void;
|
||||
|
||||
setSkin (newSkin: Skin | string): void {
|
||||
if (newSkin instanceof Skin)
|
||||
this.setSkinBySkin(newSkin);
|
||||
else
|
||||
this.setSkinByName(newSkin);
|
||||
};
|
||||
|
||||
private setSkinByName (skinName: string) {
|
||||
let skin = this.data.findSkin(skinName);
|
||||
if (!skin) throw new Error("Skin not found: " + skinName);
|
||||
this.setSkin(skin);
|
||||
}
|
||||
|
||||
/** Sets the skin used to look up attachments before looking in the {@link SkeletonData#defaultSkin default skin}. If the
|
||||
* skin is changed, {@link #updateCache()} is called.
|
||||
*
|
||||
* Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
|
||||
* old skin, each slot's setup mode attachment is attached from the new skin.
|
||||
*
|
||||
* After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
|
||||
* {@link #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply()} is called before the next time the
|
||||
* skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin.
|
||||
* @param newSkin May be null. */
|
||||
setSkin (newSkin: Skin) {
|
||||
private setSkinBySkin (newSkin: Skin) {
|
||||
if (newSkin == this.skin) return;
|
||||
if (newSkin) {
|
||||
if (this.skin)
|
||||
@ -521,7 +349,7 @@ export class Skeleton {
|
||||
let name = slot.data.attachmentName;
|
||||
if (name) {
|
||||
let attachment = newSkin.getAttachment(i, name);
|
||||
if (attachment) slot.setAttachment(attachment);
|
||||
if (attachment) slot.pose.setAttachment(attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -530,13 +358,30 @@ export class Skeleton {
|
||||
this.updateCache();
|
||||
}
|
||||
|
||||
/** Finds an attachment by looking in the {@link skin} and {@link SkeletonData.defaultSkin} using the slot name and attachment
|
||||
* name.
|
||||
*
|
||||
* See {@link getAttachment(number, string)}. */
|
||||
getAttachment (slotName: string, attachmentName: string): Attachment | null;
|
||||
|
||||
/** Finds an attachment by looking in the {@link skin} and {@link SkeletonData.defaultSkin} using the slot index and
|
||||
* attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
|
||||
*
|
||||
* See <a href="https://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
|
||||
getAttachment (slotIndex: number, attachmentName: string): Attachment | null;
|
||||
|
||||
getAttachment (slotNameOrIndex: string | number, attachmentName: string): Attachment | null {
|
||||
if (typeof slotNameOrIndex === 'string')
|
||||
return this.getAttachmentByName(slotNameOrIndex, attachmentName);
|
||||
return this.getAttachmentByIndex(slotNameOrIndex, attachmentName);
|
||||
}
|
||||
|
||||
/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment
|
||||
* name.
|
||||
*
|
||||
* See {@link #getAttachment()}.
|
||||
* @returns May be null. */
|
||||
getAttachmentByName (slotName: string, attachmentName: string): Attachment | null {
|
||||
private getAttachmentByName (slotName: string, attachmentName: string): Attachment | null {
|
||||
let slot = this.data.findSlot(slotName);
|
||||
if (!slot) throw new Error(`Can't find slot with name ${slotName}`);
|
||||
return this.getAttachment(slot.index, attachmentName);
|
||||
@ -547,7 +392,7 @@ export class Skeleton {
|
||||
*
|
||||
* See [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide.
|
||||
* @returns May be null. */
|
||||
getAttachment (slotIndex: number, attachmentName: string): Attachment | null {
|
||||
private getAttachmentByIndex (slotIndex: number, attachmentName: string): Attachment | null {
|
||||
if (!attachmentName) throw new Error("attachmentName cannot be null.");
|
||||
if (this.skin) {
|
||||
let attachment = this.skin.getAttachment(slotIndex, attachmentName);
|
||||
@ -557,57 +402,31 @@ export class Skeleton {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A convenience method to set an attachment by finding the slot with {@link #findSlot()}, finding the attachment with
|
||||
* {@link #getAttachment()}, then setting the slot's {@link Slot#attachment}.
|
||||
/** A convenience method to set an attachment by finding the slot with {@link findSlot()}, finding the attachment with
|
||||
* {@link getAttachment()}, then setting the slot's {@link Slot.attachment}.
|
||||
* @param attachmentName May be null to clear the slot's attachment. */
|
||||
setAttachment (slotName: string, attachmentName: string) {
|
||||
if (!slotName) throw new Error("slotName cannot be null.");
|
||||
let slots = this.slots;
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (slot.data.name == slotName) {
|
||||
let attachment: Attachment | null = null;
|
||||
if (attachmentName) {
|
||||
attachment = this.getAttachment(i, attachmentName);
|
||||
if (!attachment) throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName);
|
||||
}
|
||||
slot.setAttachment(attachment);
|
||||
return;
|
||||
}
|
||||
const slot = this.findSlot(slotName);
|
||||
if (!slot) throw new Error("Slot not found: " + slotName);
|
||||
let attachment: Attachment | null = null;
|
||||
if (attachmentName) {
|
||||
attachment = this.getAttachment(slot.data.index, attachmentName);
|
||||
if (!attachment)
|
||||
throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName);
|
||||
}
|
||||
throw new Error("Slot not found: " + slotName);
|
||||
slot.pose.setAttachment(attachment);
|
||||
}
|
||||
|
||||
|
||||
/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
|
||||
* than to call it repeatedly.
|
||||
* @return May be null. */
|
||||
findIkConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
return this.ikConstraints.find((constraint) => constraint.data.name == constraintName) ?? null;
|
||||
}
|
||||
|
||||
/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
|
||||
* this method than to call it repeatedly.
|
||||
* @return May be null. */
|
||||
findTransformConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
return this.transformConstraints.find((constraint) => constraint.data.name == constraintName) ?? null;
|
||||
}
|
||||
|
||||
/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
|
||||
* than to call it repeatedly.
|
||||
* @return May be null. */
|
||||
findPathConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
return this.pathConstraints.find((constraint) => constraint.data.name == constraintName) ?? null;
|
||||
}
|
||||
|
||||
/** Finds a physics constraint by comparing each physics constraint's name. It is more efficient to cache the results of this
|
||||
* method than to call it repeatedly. */
|
||||
findPhysicsConstraint (constraintName: string) {
|
||||
findConstraint<T extends Constraint<any, any, any>> (constraintName: string, type: new () => T): T | null {
|
||||
if (constraintName == null) throw new Error("constraintName cannot be null.");
|
||||
return this.physicsConstraints.find((constraint) => constraint.data.name == constraintName) ?? null;
|
||||
if (type == null) throw new Error("type cannot be null.");
|
||||
const constraints = this.constraints;
|
||||
for (let i = 0, n = constraints.length; i < n; i++) {
|
||||
const constraint = constraints[i];
|
||||
if (constraint instanceof type && constraint.data.name === constraintName) return constraint as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose as `{ x: number, y: number, width: number, height: number }`.
|
||||
@ -635,71 +454,76 @@ export class Skeleton {
|
||||
let verticesLength = 0;
|
||||
let vertices: NumberArrayLike | null = null;
|
||||
let triangles: NumberArrayLike | null = null;
|
||||
let attachment = slot.getAttachment();
|
||||
if (attachment instanceof RegionAttachment) {
|
||||
verticesLength = 8;
|
||||
vertices = Utils.setArraySize(temp, verticesLength, 0);
|
||||
attachment.computeWorldVertices(slot, vertices, 0, 2);
|
||||
triangles = Skeleton.quadTriangles;
|
||||
} else if (attachment instanceof MeshAttachment) {
|
||||
verticesLength = attachment.worldVerticesLength;
|
||||
vertices = Utils.setArraySize(temp, verticesLength, 0);
|
||||
attachment.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2);
|
||||
triangles = attachment.triangles;
|
||||
} else if (attachment instanceof ClippingAttachment && clipper != null) {
|
||||
clipper.clipStart(slot, attachment);
|
||||
continue;
|
||||
}
|
||||
if (vertices && triangles) {
|
||||
if (clipper != null && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) {
|
||||
vertices = clipper.clippedVertices;
|
||||
verticesLength = clipper.clippedVertices.length;
|
||||
let attachment = slot.pose.attachment;
|
||||
if (attachment) {
|
||||
if (attachment instanceof RegionAttachment) {
|
||||
verticesLength = 8;
|
||||
vertices = Utils.setArraySize(temp, verticesLength, 0);
|
||||
attachment.computeWorldVertices(slot, vertices, 0, 2);
|
||||
triangles = Skeleton.quadTriangles;
|
||||
} else if (attachment instanceof MeshAttachment) {
|
||||
verticesLength = attachment.worldVerticesLength;
|
||||
vertices = Utils.setArraySize(temp, verticesLength, 0);
|
||||
attachment.computeWorldVertices(this, slot, 0, verticesLength, vertices, 0, 2);
|
||||
triangles = attachment.triangles;
|
||||
} else if (attachment instanceof ClippingAttachment && clipper) {
|
||||
clipper.clipEnd(slot);
|
||||
clipper.clipStart(this, slot, attachment);
|
||||
continue;
|
||||
}
|
||||
for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) {
|
||||
let x = vertices[ii], y = vertices[ii + 1];
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
if (vertices && triangles) {
|
||||
if (clipper && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) {
|
||||
vertices = clipper.clippedVertices;
|
||||
verticesLength = clipper.clippedVertices.length;
|
||||
}
|
||||
for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) {
|
||||
let x = vertices[ii], y = vertices[ii + 1];
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clipper != null) clipper.clipEndWithSlot(slot);
|
||||
if (clipper) clipper.clipEnd(slot);
|
||||
}
|
||||
if (clipper != null) clipper.clipEnd();
|
||||
if (clipper) clipper.clipEnd();
|
||||
offset.set(minX, minY);
|
||||
size.set(maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
/** Scales the entire skeleton on the X and Y axes.
|
||||
*
|
||||
* Bones that do not inherit scale are still affected by this property. */
|
||||
public setScale (scaleX: number, scaleY: number) {
|
||||
this.scaleX = scaleX;
|
||||
this.scaleY = scaleY;
|
||||
}
|
||||
|
||||
/** Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position.
|
||||
*
|
||||
* Bones that do not inherit translation are still affected by this property. */
|
||||
public setPosition (x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/** Increments the skeleton's {@link #time}. */
|
||||
update (delta: number) {
|
||||
this.time += delta;
|
||||
}
|
||||
|
||||
/** Calls {@link PhysicsConstraint.translate} for each physics constraint. */
|
||||
physicsTranslate (x: number, y: number) {
|
||||
const physicsConstraints = this.physicsConstraints;
|
||||
for (let i = 0, n = physicsConstraints.length; i < n; i++)
|
||||
physicsConstraints[i].translate(x, y);
|
||||
const constraints = this.physics;
|
||||
for (let i = 0, n = constraints.length; i < n; i++)
|
||||
constraints[i].translate(x, y);
|
||||
}
|
||||
|
||||
/** Calls {@link PhysicsConstraint#rotate(float, float, float)} for each physics constraint. */
|
||||
/** Calls {@link PhysicsConstraint.rotate} for each physics constraint. */
|
||||
physicsRotate (x: number, y: number, degrees: number) {
|
||||
const physicsConstraints = this.physicsConstraints;
|
||||
for (let i = 0, n = physicsConstraints.length; i < n; i++)
|
||||
physicsConstraints[i].rotate(x, y, degrees);
|
||||
const constraints = this.physics;
|
||||
for (let i = 0, n = constraints.length; i < n; i++)
|
||||
constraints[i].rotate(x, y, degrees);
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines how physics and other non-deterministic updates are applied. */
|
||||
export enum Physics {
|
||||
/** Physics are not updated or applied. */
|
||||
none,
|
||||
|
||||
/** Physics are reset to the current pose. */
|
||||
reset,
|
||||
|
||||
/** Physics are updated and the pose from physics is applied. */
|
||||
update,
|
||||
|
||||
/** Physics are not updated but the pose from physics is applied. */
|
||||
pose
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline2, CurveTimeline, SequenceTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline } from "./Animation.js";
|
||||
import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline, SequenceTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline, BoneTimeline2, SliderTimeline, SliderMixTimeline } from "./Animation.js";
|
||||
import { VertexAttachment, Attachment } from "./attachments/Attachment.js";
|
||||
import { AttachmentLoader } from "./attachments/AttachmentLoader.js";
|
||||
import { HasTextureRegion } from "./attachments/HasTextureRegion.js";
|
||||
@ -37,10 +37,12 @@ import { BoneData } from "./BoneData.js";
|
||||
import { Event } from "./Event.js";
|
||||
import { EventData } from "./EventData.js";
|
||||
import { IkConstraintData } from "./IkConstraintData.js";
|
||||
import { PathConstraint } from "./PathConstraint.js";
|
||||
import { PathConstraintData, PositionMode, SpacingMode } from "./PathConstraintData.js";
|
||||
import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
|
||||
import { SkeletonData } from "./SkeletonData.js";
|
||||
import { Skin } from "./Skin.js";
|
||||
import { SliderData } from "./SliderData.js";
|
||||
import { SlotData } from "./SlotData.js";
|
||||
import { FromProperty, FromRotate, FromScaleX, FromScaleY, FromShearY, FromX, FromY, ToProperty, ToRotate, ToScaleX, ToScaleY, ToShearY, ToX, ToY, TransformConstraintData } from "./TransformConstraintData.js";
|
||||
import { Color, Utils } from "./Utils.js";
|
||||
@ -99,28 +101,30 @@ export class SkeletonBinary {
|
||||
}
|
||||
|
||||
// Bones.
|
||||
const bones = skeletonData.bones;
|
||||
n = input.readInt(true)
|
||||
for (let i = 0; i < n; i++) {
|
||||
let name = input.readString();
|
||||
if (!name) throw new Error("Bone name must not be null.");
|
||||
let parent = i == 0 ? null : skeletonData.bones[input.readInt(true)];
|
||||
let parent = i == 0 ? null : bones[input.readInt(true)];
|
||||
let data = new BoneData(i, name, parent);
|
||||
data.rotation = input.readFloat();
|
||||
data.x = input.readFloat() * scale;
|
||||
data.y = input.readFloat() * scale;
|
||||
data.scaleX = input.readFloat();
|
||||
data.scaleY = input.readFloat();
|
||||
data.shearX = input.readFloat();
|
||||
data.shearY = input.readFloat();
|
||||
const setup = data.setup;
|
||||
setup.rotation = input.readFloat();
|
||||
setup.x = input.readFloat() * scale;
|
||||
setup.y = input.readFloat() * scale;
|
||||
setup.scaleX = input.readFloat();
|
||||
setup.scaleY = input.readFloat();
|
||||
setup.shearX = input.readFloat();
|
||||
setup.shearY = input.readFloat();
|
||||
setup.inherit = input.readByte();
|
||||
data.length = input.readFloat() * scale;
|
||||
data.inherit = input.readByte();
|
||||
data.skinRequired = input.readBoolean();
|
||||
if (nonessential) {
|
||||
Color.rgba8888ToColor(data.color, input.readInt32());
|
||||
data.icon = input.readString() ?? undefined;
|
||||
data.visible = input.readBoolean();
|
||||
}
|
||||
skeletonData.bones.push(data);
|
||||
bones.push(data);
|
||||
}
|
||||
|
||||
// Slots.
|
||||
@ -128,12 +132,12 @@ export class SkeletonBinary {
|
||||
for (let i = 0; i < n; i++) {
|
||||
let slotName = input.readString();
|
||||
if (!slotName) throw new Error("Slot name must not be null.");
|
||||
let boneData = skeletonData.bones[input.readInt(true)];
|
||||
let boneData = bones[input.readInt(true)];
|
||||
let data = new SlotData(i, slotName, boneData);
|
||||
Color.rgba8888ToColor(data.color, input.readInt32());
|
||||
Color.rgba8888ToColor(data.setup.color, input.readInt32());
|
||||
|
||||
let darkColor = input.readInt32();
|
||||
if (darkColor != -1) Color.rgb888ToColor(data.darkColor = new Color(), darkColor);
|
||||
if (darkColor != -1) Color.rgb888ToColor(data.setup.darkColor = new Color(), darkColor);
|
||||
|
||||
data.attachmentName = input.readStringRef();
|
||||
data.blendMode = input.readInt(true);
|
||||
@ -141,156 +145,203 @@ export class SkeletonBinary {
|
||||
skeletonData.slots.push(data);
|
||||
}
|
||||
|
||||
// IK constraints.
|
||||
n = input.readInt(true);
|
||||
for (let i = 0, nn; i < n; i++) {
|
||||
// Constraints.
|
||||
const constraints = skeletonData.constraints;
|
||||
const constraintCount = input.readInt(true);
|
||||
for (let i = 0; i < constraintCount; i++) {
|
||||
let name = input.readString();
|
||||
if (!name) throw new Error("IK constraint data name must not be null.");
|
||||
let data = new IkConstraintData(name);
|
||||
data.order = input.readInt(true);
|
||||
nn = input.readInt(true);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(skeletonData.bones[input.readInt(true)]);
|
||||
data.target = skeletonData.bones[input.readInt(true)];
|
||||
let flags = input.readByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
data.bendDirection = (flags & 2) != 0 ? 1 : -1;
|
||||
data.compress = (flags & 4) != 0;
|
||||
data.stretch = (flags & 8) != 0;
|
||||
data.uniform = (flags & 16) != 0;
|
||||
if ((flags & 32) != 0) data.mix = (flags & 64) != 0 ? input.readFloat() : 1;
|
||||
if ((flags & 128) != 0) data.softness = input.readFloat() * scale;
|
||||
skeletonData.ikConstraints.push(data);
|
||||
}
|
||||
|
||||
// Transform constraints.
|
||||
n = input.readInt(true);
|
||||
for (let i = 0, nn; i < n; i++) {
|
||||
let name = input.readString();
|
||||
if (!name) throw new Error("Transform constraint data name must not be null.");
|
||||
let data = new TransformConstraintData(name);
|
||||
data.order = input.readInt(true);
|
||||
nn = input.readInt(true);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(skeletonData.bones[input.readInt(true)]);
|
||||
data.source = skeletonData.bones[input.readInt(true)];
|
||||
let flags = input.readUnsignedByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
data.localSource = (flags & 2) != 0;
|
||||
data.localTarget = (flags & 4) != 0;
|
||||
data.additive = (flags & 8) != 0;
|
||||
data.clamp = (flags & 16) != 0;
|
||||
|
||||
nn = flags >> 5;
|
||||
for (let ii = 0, tn; ii < nn; ii++) {
|
||||
let from: FromProperty | null;
|
||||
let type = input.readByte();
|
||||
switch (type) {
|
||||
case 0: from = new FromRotate(); break;
|
||||
case 1: from = new FromX(); break;
|
||||
case 2: from = new FromY(); break;
|
||||
case 3: from = new FromScaleX(); break;
|
||||
case 4: from = new FromScaleY(); break;
|
||||
case 5: from = new FromShearY(); break;
|
||||
default: from = null;
|
||||
if (!name) throw new Error("Constraint data name must not be null.");
|
||||
let nn = input.readInt(true);
|
||||
switch (input.readByte()) {
|
||||
case CONSTRAINT_IK: {
|
||||
let data = new IkConstraintData(name);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(bones[input.readInt(true)]);
|
||||
data.target = bones[input.readInt(true)];
|
||||
let flags = input.readByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
data.uniform = (flags & 2) != 0;
|
||||
const setup = data.setup;
|
||||
setup.bendDirection = (flags & 4) != 0 ? 1 : -1;
|
||||
setup.compress = (flags & 8) != 0;
|
||||
setup.stretch = (flags & 16) != 0;
|
||||
if ((flags & 32) != 0) setup.mix = (flags & 64) != 0 ? input.readFloat() : 1;
|
||||
if ((flags & 128) != 0) setup.softness = input.readFloat() * scale;
|
||||
constraints.push(data);
|
||||
break;
|
||||
}
|
||||
if (!from) continue;
|
||||
from.offset = input.readFloat() * scale;
|
||||
tn = input.readByte();
|
||||
for (let t = 0; t < tn; t++) {
|
||||
let to: ToProperty | null;
|
||||
type = input.readByte();
|
||||
switch (type) {
|
||||
case 0: to = new ToRotate(); break;
|
||||
case 1: to = new ToX(); break;
|
||||
case 2: to = new ToY(); break;
|
||||
case 3: to = new ToScaleX(); break;
|
||||
case 4: to = new ToScaleY(); break;
|
||||
case 5: to = new ToShearY(); break;
|
||||
default: to = null;
|
||||
case CONSTRAINT_TRANSFORM: {
|
||||
let data = new TransformConstraintData(name);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(bones[input.readInt(true)]);
|
||||
data.source = bones[input.readInt(true)];
|
||||
let flags = input.readUnsignedByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
data.localSource = (flags & 2) != 0;
|
||||
data.localTarget = (flags & 4) != 0;
|
||||
data.additive = (flags & 8) != 0;
|
||||
data.clamp = (flags & 16) != 0;
|
||||
|
||||
nn = flags >> 5;
|
||||
for (let ii = 0, tn; ii < nn; ii++) {
|
||||
let fromScale = 1;
|
||||
let from: FromProperty | null;
|
||||
switch (input.readByte()) {
|
||||
case 0: from = new FromRotate(); break;
|
||||
case 1: {
|
||||
fromScale = scale;
|
||||
from = new FromX();
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
fromScale = scale;
|
||||
from = new FromY();
|
||||
break;
|
||||
}
|
||||
case 3: from = new FromScaleX(); break;
|
||||
case 4: from = new FromScaleY(); break;
|
||||
case 5: from = new FromShearY(); break;
|
||||
default: from = null;
|
||||
}
|
||||
if (!from) continue;
|
||||
from.offset = input.readFloat() * fromScale;
|
||||
tn = input.readByte();
|
||||
for (let t = 0; t < tn; t++) {
|
||||
let toScale = 1;
|
||||
let to: ToProperty | null;
|
||||
switch (input.readByte()) {
|
||||
case 0: to = new ToRotate(); break;
|
||||
case 1: {
|
||||
toScale = scale;
|
||||
to = new ToX();
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
toScale = scale;
|
||||
to = new ToY();
|
||||
break;
|
||||
}
|
||||
case 3: to = new ToScaleX(); break;
|
||||
case 4: to = new ToScaleY(); break;
|
||||
case 5: to = new ToShearY(); break;
|
||||
default: to = null;
|
||||
}
|
||||
if (!to) continue;
|
||||
to.offset = input.readFloat() * scale;
|
||||
to.max = input.readFloat() * scale;
|
||||
to.scale = input.readFloat();
|
||||
from.to[t] = to;
|
||||
}
|
||||
data.properties[ii] = from;
|
||||
}
|
||||
if (!to) continue;
|
||||
to.offset = input.readFloat() * scale;
|
||||
to.max = input.readFloat() * scale;
|
||||
to.scale = input.readFloat();
|
||||
from.to[t] = to;
|
||||
flags = input.readByte();
|
||||
if ((flags & 1) != 0) data.offsets[0] = input.readFloat();
|
||||
if ((flags & 2) != 0) data.offsets[1] = input.readFloat() * scale;
|
||||
if ((flags & 4) != 0) data.offsets[2] = input.readFloat() * scale;
|
||||
if ((flags & 8) != 0) data.offsets[3] = input.readFloat();
|
||||
if ((flags & 16) != 0) data.offsets[4] = input.readFloat();
|
||||
if ((flags & 32) != 0) data.offsets[5] = input.readFloat();
|
||||
flags = input.readByte();
|
||||
const setup = data.setup;
|
||||
if ((flags & 1) != 0) setup.mixRotate = input.readFloat();
|
||||
if ((flags & 2) != 0) setup.mixX = input.readFloat();
|
||||
if ((flags & 4) != 0) setup.mixY = input.readFloat();
|
||||
if ((flags & 8) != 0) setup.mixScaleX = input.readFloat();
|
||||
if ((flags & 16) != 0) setup.mixScaleY = input.readFloat();
|
||||
if ((flags & 32) != 0) setup.mixShearY = input.readFloat();
|
||||
constraints.push(data);
|
||||
break;
|
||||
}
|
||||
case CONSTRAINT_PATH: {
|
||||
let data = new PathConstraintData(name);
|
||||
data.skinRequired = input.readBoolean();
|
||||
nn = input.readInt(true);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(bones[input.readInt(true)]);
|
||||
data.slot = skeletonData.slots[input.readInt(true)];
|
||||
const flags = input.readByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
data.positionMode = (flags >> 1) & 2;
|
||||
data.spacingMode = (flags >> 2) & 3;
|
||||
data.rotateMode = (flags >> 4) & 3;
|
||||
if ((flags & 128) != 0) data.offsetRotation = input.readFloat();
|
||||
const setup = data.setup;
|
||||
setup.position = input.readFloat();
|
||||
if (data.positionMode == PositionMode.Fixed) setup.position *= scale;
|
||||
setup.spacing = input.readFloat();
|
||||
if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) setup.spacing *= scale;
|
||||
setup.mixRotate = input.readFloat();
|
||||
setup.mixX = input.readFloat();
|
||||
setup.mixY = input.readFloat();
|
||||
constraints.push(data);
|
||||
break;
|
||||
}
|
||||
case CONSTRAINT_PHYSICS: {
|
||||
const data = new PhysicsConstraintData(name);
|
||||
data.bone = bones[nn];
|
||||
let flags = input.readByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
if ((flags & 2) != 0) data.x = input.readFloat();
|
||||
if ((flags & 4) != 0) data.y = input.readFloat();
|
||||
if ((flags & 8) != 0) data.rotate = input.readFloat();
|
||||
if ((flags & 16) != 0) data.scaleX = input.readFloat();
|
||||
if ((flags & 32) != 0) data.shearX = input.readFloat();
|
||||
data.limit = ((flags & 64) != 0 ? input.readFloat() : 5000) * scale;
|
||||
data.step = 1 / input.readUnsignedByte();
|
||||
const setup = data.setup;
|
||||
setup.inertia = input.readFloat();
|
||||
setup.strength = input.readFloat();
|
||||
setup.damping = input.readFloat();
|
||||
setup.massInverse = (flags & 128) != 0 ? input.readFloat() : 1;
|
||||
setup.wind = input.readFloat();
|
||||
setup.gravity = input.readFloat();
|
||||
flags = input.readByte();
|
||||
if ((flags & 1) != 0) data.inertiaGlobal = true;
|
||||
if ((flags & 2) != 0) data.strengthGlobal = true;
|
||||
if ((flags & 4) != 0) data.dampingGlobal = true;
|
||||
if ((flags & 8) != 0) data.massGlobal = true;
|
||||
if ((flags & 16) != 0) data.windGlobal = true;
|
||||
if ((flags & 32) != 0) data.gravityGlobal = true;
|
||||
if ((flags & 64) != 0) data.mixGlobal = true;
|
||||
setup.mix = (flags & 128) != 0 ? input.readFloat() : 1;
|
||||
constraints.push(data);
|
||||
break;
|
||||
}
|
||||
case CONSTRAINT_PHYSICS: {
|
||||
const data = new SliderData(name);
|
||||
data.skinRequired = (nn & 1) != 0;
|
||||
data.loop = (nn & 2) != 0;
|
||||
data.additive = (nn & 4) != 0;
|
||||
if ((nn & 8) != 0) data.setup.time = input.readFloat();
|
||||
if ((nn & 16) != 0) data.setup.mix = (nn & 32) != 0 ? input.readFloat() : 1;
|
||||
if ((nn & 64) != 0) {
|
||||
data.local = (nn & 128) != 0;
|
||||
data.bone = bones[input.readInt(true)];
|
||||
let offset = input.readFloat();
|
||||
switch (input.readByte()) {
|
||||
case 0: data.property = new FromRotate(); break;
|
||||
case 1: {
|
||||
offset *= scale;
|
||||
data.property = new FromX();
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
offset *= scale;
|
||||
data.property = new FromY();
|
||||
break;
|
||||
}
|
||||
case 3: data.property = new FromScaleX(); break;
|
||||
case 4: data.property = new FromScaleY(); break;
|
||||
case 5: data.property = new FromShearY(); break;
|
||||
default: continue;
|
||||
};
|
||||
data.property.offset = offset;
|
||||
data.scale = input.readFloat();
|
||||
}
|
||||
constraints.push(data);
|
||||
break;
|
||||
}
|
||||
data.properties[ii] = from;
|
||||
}
|
||||
|
||||
flags = input.readByte();
|
||||
if ((flags & 1) != 0) data.offsetX = input.readFloat();
|
||||
if ((flags & 2) != 0) data.offsetY = input.readFloat();
|
||||
if ((flags & 4) != 0) data.mixRotate = input.readFloat();
|
||||
if ((flags & 8) != 0) data.mixX = input.readFloat();
|
||||
if ((flags & 16) != 0) data.mixY = input.readFloat();
|
||||
if ((flags & 32) != 0) data.mixScaleX = input.readFloat();
|
||||
if ((flags & 64) != 0) data.mixScaleY = input.readFloat();
|
||||
if ((flags & 128) != 0) data.mixShearY = input.readFloat();
|
||||
|
||||
skeletonData.transformConstraints.push(data);
|
||||
}
|
||||
|
||||
// Path constraints.
|
||||
n = input.readInt(true);
|
||||
for (let i = 0, nn; i < n; i++) {
|
||||
let name = input.readString();
|
||||
if (!name) throw new Error("Path constraint data name must not be null.");
|
||||
let data = new PathConstraintData(name);
|
||||
data.order = input.readInt(true);
|
||||
data.skinRequired = input.readBoolean();
|
||||
nn = input.readInt(true);
|
||||
for (let ii = 0; ii < nn; ii++)
|
||||
data.bones.push(skeletonData.bones[input.readInt(true)]);
|
||||
data.slot = skeletonData.slots[input.readInt(true)];
|
||||
const flags = input.readByte();
|
||||
data.positionMode = flags & 1;
|
||||
data.spacingMode = (flags >> 1) & 3;
|
||||
data.rotateMode = (flags >> 3) & 3;
|
||||
if ((flags & 128) != 0) data.offsetRotation = input.readFloat();
|
||||
data.position = input.readFloat();
|
||||
if (data.positionMode == PositionMode.Fixed) data.position *= scale;
|
||||
data.spacing = input.readFloat();
|
||||
if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale;
|
||||
data.mixRotate = input.readFloat();
|
||||
data.mixX = input.readFloat();
|
||||
data.mixY = input.readFloat();
|
||||
skeletonData.pathConstraints.push(data);
|
||||
}
|
||||
|
||||
// Physics constraints.
|
||||
n = input.readInt(true);
|
||||
for (let i = 0, nn; i < n; i++) {
|
||||
const name = input.readString();
|
||||
if (!name) throw new Error("Physics constraint data name must not be null.");
|
||||
const data = new PhysicsConstraintData(name);
|
||||
data.order = input.readInt(true);
|
||||
data.bone = skeletonData.bones[input.readInt(true)];
|
||||
let flags = input.readByte();
|
||||
data.skinRequired = (flags & 1) != 0;
|
||||
if ((flags & 2) != 0) data.x = input.readFloat();
|
||||
if ((flags & 4) != 0) data.y = input.readFloat();
|
||||
if ((flags & 8) != 0) data.rotate = input.readFloat();
|
||||
if ((flags & 16) != 0) data.scaleX = input.readFloat();
|
||||
if ((flags & 32) != 0) data.shearX = input.readFloat();
|
||||
data.limit = ((flags & 64) != 0 ? input.readFloat() : 5000) * scale;
|
||||
data.step = 1 / input.readUnsignedByte();
|
||||
data.inertia = input.readFloat();
|
||||
data.strength = input.readFloat();
|
||||
data.damping = input.readFloat();
|
||||
data.massInverse = (flags & 128) != 0 ? input.readFloat() : 1;
|
||||
data.wind = input.readFloat();
|
||||
data.gravity = input.readFloat();
|
||||
flags = input.readByte();
|
||||
if ((flags & 1) != 0) data.inertiaGlobal = true;
|
||||
if ((flags & 2) != 0) data.strengthGlobal = true;
|
||||
if ((flags & 4) != 0) data.dampingGlobal = true;
|
||||
if ((flags & 8) != 0) data.massGlobal = true;
|
||||
if ((flags & 16) != 0) data.windGlobal = true;
|
||||
if ((flags & 32) != 0) data.gravityGlobal = true;
|
||||
if ((flags & 64) != 0) data.mixGlobal = true;
|
||||
data.mix = (flags & 128) != 0 ? input.readFloat() : 1;
|
||||
skeletonData.physicsConstraints.push(data);
|
||||
}
|
||||
|
||||
// Default skin.
|
||||
@ -343,12 +394,19 @@ export class SkeletonBinary {
|
||||
}
|
||||
|
||||
// Animations.
|
||||
const animations = skeletonData.animations;
|
||||
n = input.readInt(true);
|
||||
for (let i = 0; i < n; i++) {
|
||||
let animationName = input.readString();
|
||||
if (!animationName) throw new Error("Animatio name must not be null.");
|
||||
skeletonData.animations.push(this.readAnimation(input, animationName, skeletonData));
|
||||
if (!animationName) throw new Error("Animation name must not be null.");
|
||||
animations.push(this.readAnimation(input, animationName, skeletonData));
|
||||
}
|
||||
|
||||
for (let i = 0; i < constraintCount; i++) {
|
||||
const constraint = constraints[i];
|
||||
if (constraint instanceof SliderData) constraint.animation = animations[input.readInt(true)];
|
||||
}
|
||||
|
||||
return skeletonData;
|
||||
}
|
||||
|
||||
@ -364,19 +422,19 @@ export class SkeletonBinary {
|
||||
let skinName = input.readString();
|
||||
if (!skinName) throw new Error("Skin name must not be null.");
|
||||
skin = new Skin(skinName);
|
||||
if (nonessential) Color.rgba8888ToColor(skin.color, input.readInt32());
|
||||
skin.bones.length = input.readInt(true);
|
||||
for (let i = 0, n = skin.bones.length; i < n; i++)
|
||||
skin.bones[i] = skeletonData.bones[input.readInt(true)];
|
||||
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++)
|
||||
skin.constraints.push(skeletonData.ikConstraints[input.readInt(true)]);
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++)
|
||||
skin.constraints.push(skeletonData.transformConstraints[input.readInt(true)]);
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++)
|
||||
skin.constraints.push(skeletonData.pathConstraints[input.readInt(true)]);
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++)
|
||||
skin.constraints.push(skeletonData.physicsConstraints[input.readInt(true)]);
|
||||
if (nonessential) Color.rgba8888ToColor(skin.color, input.readInt32());
|
||||
|
||||
let n = input.readInt(true);
|
||||
let from: Object[] = skeletonData.bones, to: Object[] = skin.bones;
|
||||
for (let i = 0; i < n; i++)
|
||||
to[i] = from[input.readInt(true)];
|
||||
|
||||
n = input.readInt(true);
|
||||
from = skeletonData.constraints;
|
||||
to = skin.constraints;
|
||||
for (let i = 0; i < n; i++)
|
||||
to[i] = from[input.readInt(true)];
|
||||
|
||||
slotCount = input.readInt(true);
|
||||
}
|
||||
@ -553,7 +611,6 @@ export class SkeletonBinary {
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private readSequence (input: BinaryInput) {
|
||||
@ -838,35 +895,16 @@ export class SkeletonBinary {
|
||||
}
|
||||
let bezierCount = input.readInt(true);
|
||||
switch (type) {
|
||||
case BONE_ROTATE:
|
||||
timelines.push(readTimeline1(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_TRANSLATE:
|
||||
timelines.push(readTimeline2(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale));
|
||||
break;
|
||||
case BONE_TRANSLATEX:
|
||||
timelines.push(readTimeline1(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale));
|
||||
break;
|
||||
case BONE_TRANSLATEY:
|
||||
timelines.push(readTimeline1(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale));
|
||||
break;
|
||||
case BONE_SCALE:
|
||||
timelines.push(readTimeline2(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_SCALEX:
|
||||
timelines.push(readTimeline1(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_SCALEY:
|
||||
timelines.push(readTimeline1(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_SHEAR:
|
||||
timelines.push(readTimeline2(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_SHEARX:
|
||||
timelines.push(readTimeline1(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
break;
|
||||
case BONE_SHEARY:
|
||||
timelines.push(readTimeline1(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1));
|
||||
case BONE_ROTATE: readTimeline(input, timelines, new RotateTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_TRANSLATE: readTimeline(input, timelines, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale); break;
|
||||
case BONE_TRANSLATEX: readTimeline(input, timelines, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale); break;
|
||||
case BONE_TRANSLATEY: readTimeline(input, timelines, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale); break;
|
||||
case BONE_SCALE: readTimeline(input, timelines, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_SCALEX: readTimeline(input, timelines, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_SCALEY: readTimeline(input, timelines, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_SHEAR: readTimeline(input, timelines, new ShearTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_SHEARX: readTimeline(input, timelines, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
case BONE_SHEARY: readTimeline(input, timelines, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -934,19 +972,17 @@ export class SkeletonBinary {
|
||||
// Path constraint timelines.
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++) {
|
||||
let index = input.readInt(true);
|
||||
let data = skeletonData.pathConstraints[index];
|
||||
let data = skeletonData.constraints[index] as PathConstraintData;
|
||||
for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) {
|
||||
const type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true);
|
||||
switch (type) {
|
||||
case PATH_POSITION:
|
||||
timelines
|
||||
.push(readTimeline1(input, new PathConstraintPositionTimeline(frameCount, bezierCount, index),
|
||||
data.positionMode == PositionMode.Fixed ? scale : 1));
|
||||
readTimeline(input, timelines, new PathConstraintPositionTimeline(frameCount, bezierCount, index),
|
||||
data.positionMode == PositionMode.Fixed ? scale : 1);
|
||||
break;
|
||||
case PATH_SPACING:
|
||||
timelines
|
||||
.push(readTimeline1(input, new PathConstraintSpacingTimeline(frameCount, bezierCount, index),
|
||||
data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1));
|
||||
readTimeline(input, timelines, new PathConstraintSpacingTimeline(frameCount, bezierCount, index),
|
||||
data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1);
|
||||
break;
|
||||
case PATH_MIX:
|
||||
let timeline = new PathConstraintMixTimeline(frameCount, bezierCount, index);
|
||||
@ -989,33 +1025,33 @@ export class SkeletonBinary {
|
||||
}
|
||||
const bezierCount = input.readInt(true);
|
||||
switch (type) {
|
||||
case PHYSICS_INERTIA:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintInertiaTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_STRENGTH:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintStrengthTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_DAMPING:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintDampingTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_MASS:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_WIND:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_GRAVITY:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1));
|
||||
break;
|
||||
case PHYSICS_MIX:
|
||||
timelines.push(readTimeline1(input, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1));
|
||||
default:
|
||||
throw new Error("Unknown physics timeline type.");
|
||||
case PHYSICS_INERTIA: readTimeline(input, timelines, new PhysicsConstraintInertiaTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_STRENGTH: readTimeline(input, timelines, new PhysicsConstraintStrengthTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_DAMPING: readTimeline(input, timelines, new PhysicsConstraintDampingTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_MASS: readTimeline(input, timelines, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_WIND: readTimeline(input, timelines, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_GRAVITY: readTimeline(input, timelines, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case PHYSICS_MIX: readTimeline(input, timelines, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1); break;
|
||||
default: throw new Error("Unknown physics timeline type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deform timelines.
|
||||
// Slider timelines.
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++) {
|
||||
const index = input.readInt(true);
|
||||
for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) {
|
||||
const type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true);
|
||||
switch (type) {
|
||||
case SLIDER_TIME: readTimeline(input, timelines, new SliderTimeline(frameCount, bezierCount, index), 1); break;
|
||||
case SLIDER_MIX: readTimeline(input, timelines, new SliderMixTimeline(frameCount, bezierCount, index), 1); break;
|
||||
default: throw new Error("Uknown slider type: " + type);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Attachment timelines.
|
||||
for (let i = 0, n = input.readInt(true); i < n; i++) {
|
||||
let skin = skeletonData.skins[input.readInt(true)];
|
||||
for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) {
|
||||
@ -1267,7 +1303,16 @@ class Vertices {
|
||||
|
||||
enum AttachmentType { Region, BoundingBox, Mesh, LinkedMesh, Path, Point, Clipping }
|
||||
|
||||
function readTimeline1 (input: BinaryInput, timeline: CurveTimeline1, scale: number): CurveTimeline1 {
|
||||
function readTimeline (input: BinaryInput, timelines: Array<Timeline>, timeline: CurveTimeline1, scale: number): void;
|
||||
function readTimeline (input: BinaryInput, timelines: Array<Timeline>, timeline: BoneTimeline2, scale: number): void;
|
||||
function readTimeline (input: BinaryInput, timelines: Array<Timeline>, timeline: CurveTimeline1 | BoneTimeline2, scale: number): void {
|
||||
if (timeline instanceof CurveTimeline1)
|
||||
readTimeline1(input, timelines, timeline, scale);
|
||||
else
|
||||
readTimeline2(input, timelines, timeline, scale);
|
||||
}
|
||||
|
||||
function readTimeline1 (input: BinaryInput, timelines: Array<Timeline>, timeline: CurveTimeline1, scale: number): void {
|
||||
let time = input.readFloat(), value = input.readFloat() * scale;
|
||||
for (let frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1; ; frame++) {
|
||||
timeline.setFrame(frame, time, value);
|
||||
@ -1283,10 +1328,10 @@ function readTimeline1 (input: BinaryInput, timeline: CurveTimeline1, scale: num
|
||||
time = time2;
|
||||
value = value2;
|
||||
}
|
||||
return timeline;
|
||||
timelines.push(timeline);
|
||||
}
|
||||
|
||||
function readTimeline2 (input: BinaryInput, timeline: CurveTimeline2, scale: number): CurveTimeline2 {
|
||||
function readTimeline2 (input: BinaryInput, timelines: Array<Timeline>, timeline: BoneTimeline2, scale: number): void {
|
||||
let time = input.readFloat(), value1 = input.readFloat() * scale, value2 = input.readFloat() * scale;
|
||||
for (let frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1; ; frame++) {
|
||||
timeline.setFrame(frame, time, value1, value2);
|
||||
@ -1304,7 +1349,7 @@ function readTimeline2 (input: BinaryInput, timeline: CurveTimeline2, scale: num
|
||||
value1 = nvalue1;
|
||||
value2 = nvalue2;
|
||||
}
|
||||
return timeline;
|
||||
timelines.push(timeline);
|
||||
}
|
||||
|
||||
function setBezier (input: BinaryInput, timeline: CurveTimeline, bezier: number, frame: number, value: number,
|
||||
@ -1331,6 +1376,12 @@ const SLOT_RGBA2 = 3;
|
||||
const SLOT_RGB2 = 4;
|
||||
const SLOT_ALPHA = 5;
|
||||
|
||||
const CONSTRAINT_IK = 0;
|
||||
const CONSTRAINT_PATH = 1;
|
||||
const CONSTRAINT_TRANSFORM = 2;
|
||||
const CONSTRAINT_PHYSICS = 3;
|
||||
const CONSTRAINT_SLIDER = 4;
|
||||
|
||||
const ATTACHMENT_DEFORM = 0;
|
||||
const ATTACHMENT_SEQUENCE = 1;
|
||||
|
||||
@ -1347,6 +1398,9 @@ const PHYSICS_GRAVITY = 6;
|
||||
const PHYSICS_MIX = 7;
|
||||
const PHYSICS_RESET = 8;
|
||||
|
||||
const SLIDER_TIME = 0;
|
||||
const SLIDER_MIX = 1;
|
||||
|
||||
const CURVE_LINEAR = 0;
|
||||
const CURVE_STEPPED = 1;
|
||||
const CURVE_BEZIER = 2;
|
||||
|
||||
@ -76,7 +76,7 @@ export class SkeletonBounds {
|
||||
for (let i = 0; i < slotCount; i++) {
|
||||
let slot = slots[i];
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
if (attachment instanceof BoundingBoxAttachment) {
|
||||
boundingBoxes.push(attachment);
|
||||
|
||||
@ -85,7 +85,7 @@ export class SkeletonBounds {
|
||||
polygon = Utils.newFloatArray(attachment.worldVerticesLength);
|
||||
}
|
||||
polygons.push(polygon);
|
||||
attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, polygon, 0, 2);
|
||||
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, polygon, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { ClippingAttachment } from "./attachments/ClippingAttachment.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { Slot } from "./Slot.js";
|
||||
import { Triangulator } from "./Triangulator.js";
|
||||
import { Utils, Color, NumberArrayLike } from "./Utils.js";
|
||||
@ -37,20 +38,23 @@ export class SkeletonClipping {
|
||||
private clippingPolygon = new Array<number>();
|
||||
private clipOutput = new Array<number>();
|
||||
clippedVertices = new Array<number>();
|
||||
|
||||
/** An empty array unless {@link clipTrianglesUnpacked} was used. **/
|
||||
clippedUVs = new Array<number>();
|
||||
|
||||
clippedTriangles = new Array<number>();
|
||||
private scratch = new Array<number>();
|
||||
|
||||
private clipAttachment: ClippingAttachment | null = null;
|
||||
private clippingPolygons: Array<Array<number>> | null = null;
|
||||
|
||||
clipStart (slot: Slot, clip: ClippingAttachment): number {
|
||||
clipStart (skeleton: Skeleton, slot: Slot, clip: ClippingAttachment): number {
|
||||
if (this.clipAttachment) return 0;
|
||||
this.clipAttachment = clip;
|
||||
|
||||
let n = clip.worldVerticesLength;
|
||||
let vertices = Utils.setArraySize(this.clippingPolygon, n);
|
||||
clip.computeWorldVertices(slot, 0, n, vertices, 0, 2);
|
||||
clip.computeWorldVertices(skeleton, slot, 0, n, vertices, 0, 2);
|
||||
let clippingPolygon = this.clippingPolygon;
|
||||
SkeletonClipping.makeClockwise(clippingPolygon);
|
||||
let clippingPolygons = this.clippingPolygons = this.triangulator.decompose(clippingPolygon, this.triangulator.triangulate(clippingPolygon));
|
||||
@ -64,12 +68,9 @@ export class SkeletonClipping {
|
||||
return clippingPolygons.length;
|
||||
}
|
||||
|
||||
clipEndWithSlot (slot: Slot) {
|
||||
if (this.clipAttachment && this.clipAttachment.endSlot == slot.data) this.clipEnd();
|
||||
}
|
||||
|
||||
clipEnd () {
|
||||
clipEnd (slot?: Slot) {
|
||||
if (!this.clipAttachment) return;
|
||||
if (slot && this.clipAttachment.endSlot !== slot.data) return;
|
||||
this.clipAttachment = null;
|
||||
this.clippingPolygons = null;
|
||||
this.clippedVertices.length = 0;
|
||||
|
||||
@ -29,13 +29,10 @@
|
||||
|
||||
import { Animation } from "./Animation"
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { ConstraintData } from "./ConstraintData";
|
||||
import { EventData } from "./EventData.js";
|
||||
import { IkConstraintData } from "./IkConstraintData.js";
|
||||
import { PathConstraintData } from "./PathConstraintData.js";
|
||||
import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
|
||||
import { Skin } from "./Skin.js";
|
||||
import { SlotData } from "./SlotData.js";
|
||||
import { TransformConstraintData } from "./TransformConstraintData.js";
|
||||
|
||||
/** Stores the setup pose and all of the stateless data for a skeleton.
|
||||
*
|
||||
@ -67,16 +64,7 @@ export class SkeletonData {
|
||||
animations = new Array<Animation>();
|
||||
|
||||
/** The skeleton's IK constraints. */
|
||||
ikConstraints = new Array<IkConstraintData>();
|
||||
|
||||
/** The skeleton's transform constraints. */
|
||||
transformConstraints = new Array<TransformConstraintData>();
|
||||
|
||||
/** The skeleton's path constraints. */
|
||||
pathConstraints = new Array<PathConstraintData>();
|
||||
|
||||
/** The skeleton's physics constraints. */
|
||||
physicsConstraints = new Array<PhysicsConstraintData>();
|
||||
constraints = new Array<ConstraintData<any, any>>();
|
||||
|
||||
/** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */
|
||||
x: number = 0;
|
||||
@ -116,10 +104,8 @@ export class SkeletonData {
|
||||
findBone (boneName: string) {
|
||||
if (!boneName) throw new Error("boneName cannot be null.");
|
||||
let bones = this.bones;
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
if (bone.name == boneName) return bone;
|
||||
}
|
||||
for (let i = 0, n = bones.length; i < n; i++)
|
||||
if (bones[i].name == boneName) return bones[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -129,10 +115,8 @@ export class SkeletonData {
|
||||
findSlot (slotName: string) {
|
||||
if (!slotName) throw new Error("slotName cannot be null.");
|
||||
let slots = this.slots;
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (slot.name == slotName) return slot;
|
||||
}
|
||||
for (let i = 0, n = slots.length; i < n; i++)
|
||||
if (slots[i].name == slotName) return slots[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -142,10 +126,8 @@ export class SkeletonData {
|
||||
findSkin (skinName: string) {
|
||||
if (!skinName) throw new Error("skinName cannot be null.");
|
||||
let skins = this.skins;
|
||||
for (let i = 0, n = skins.length; i < n; i++) {
|
||||
let skin = skins[i];
|
||||
if (skin.name == skinName) return skin;
|
||||
}
|
||||
for (let i = 0, n = skins.length; i < n; i++)
|
||||
if (skins[i].name == skinName) return skins[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -155,10 +137,8 @@ export class SkeletonData {
|
||||
findEvent (eventDataName: string) {
|
||||
if (!eventDataName) throw new Error("eventDataName cannot be null.");
|
||||
let events = this.events;
|
||||
for (let i = 0, n = events.length; i < n; i++) {
|
||||
let event = events[i];
|
||||
if (event.name == eventDataName) return event;
|
||||
}
|
||||
for (let i = 0, n = events.length; i < n; i++)
|
||||
if (events[i].name == eventDataName) return events[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -168,62 +148,22 @@ export class SkeletonData {
|
||||
findAnimation (animationName: string) {
|
||||
if (!animationName) throw new Error("animationName cannot be null.");
|
||||
let animations = this.animations;
|
||||
for (let i = 0, n = animations.length; i < n; i++) {
|
||||
let animation = animations[i];
|
||||
if (animation.name == animationName) return animation;
|
||||
for (let i = 0, n = animations.length; i < n; i++)
|
||||
if (animations[i].name == animationName) return animations[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Constraints.
|
||||
|
||||
findConstraint<T extends ConstraintData<any, any>> (constraintName: string, type: new (name: string) => T): T | null {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
if (type == null) throw new Error("type cannot be null.");
|
||||
const constraints = this.constraints;
|
||||
for (let i = 0, n = this.constraints.length; i < n; i++) {
|
||||
let constraint = constraints[i];
|
||||
if (constraint instanceof type && constraint.name === constraintName) return constraint as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
|
||||
* than to call it multiple times.
|
||||
* @return May be null. */
|
||||
findIkConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
const ikConstraints = this.ikConstraints;
|
||||
for (let i = 0, n = ikConstraints.length; i < n; i++) {
|
||||
const constraint = ikConstraints[i];
|
||||
if (constraint.name == constraintName) return constraint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
|
||||
* this method than to call it multiple times.
|
||||
* @return May be null. */
|
||||
findTransformConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
const transformConstraints = this.transformConstraints;
|
||||
for (let i = 0, n = transformConstraints.length; i < n; i++) {
|
||||
const constraint = transformConstraints[i];
|
||||
if (constraint.name == constraintName) return constraint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
|
||||
* than to call it multiple times.
|
||||
* @return May be null. */
|
||||
findPathConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
const pathConstraints = this.pathConstraints;
|
||||
for (let i = 0, n = pathConstraints.length; i < n; i++) {
|
||||
const constraint = pathConstraints[i];
|
||||
if (constraint.name == constraintName) return constraint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds a physics constraint by comparing each physics constraint's name. It is more efficient to cache the results of this method
|
||||
* than to call it multiple times.
|
||||
* @return May be null. */
|
||||
findPhysicsConstraint (constraintName: string) {
|
||||
if (!constraintName) throw new Error("constraintName cannot be null.");
|
||||
const physicsConstraints = this.physicsConstraints;
|
||||
for (let i = 0, n = physicsConstraints.length; i < n; i++) {
|
||||
const constraint = physicsConstraints[i];
|
||||
if (constraint.name == constraintName) return constraint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -49,7 +49,7 @@ export class Skin {
|
||||
|
||||
attachments = new Array<StringMap<Attachment>>();
|
||||
bones = Array<BoneData>();
|
||||
constraints = new Array<ConstraintData>();
|
||||
constraints = new Array<ConstraintData<any, any>>();
|
||||
|
||||
/** The color of the skin as it was in Spine, or a default color if nonessential data was not exported. */
|
||||
color = new Color(0.99607843, 0.61960787, 0.30980393, 1); // fe9e4fff
|
||||
@ -192,14 +192,14 @@ export class Skin {
|
||||
let slotIndex = 0;
|
||||
for (let i = 0; i < skeleton.slots.length; i++) {
|
||||
let slot = skeleton.slots[i];
|
||||
let slotAttachment = slot.getAttachment();
|
||||
let slotAttachment = slot.pose.getAttachment();
|
||||
if (slotAttachment && slotIndex < oldSkin.attachments.length) {
|
||||
let dictionary = oldSkin.attachments[slotIndex];
|
||||
for (let key in dictionary) {
|
||||
let skinAttachment: Attachment = dictionary[key];
|
||||
if (slotAttachment == skinAttachment) {
|
||||
let attachment = this.getAttachment(slotIndex, key);
|
||||
if (attachment) slot.setAttachment(attachment);
|
||||
if (attachment) slot.pose.setAttachment(attachment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
117
spine-ts/spine-core/src/Slider.ts
Normal file
117
spine-ts/spine-core/src/Slider.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { isConstraintTimeline, isSlotTimeline, MixBlend, MixDirection, PhysicsConstraintTimeline } from "./Animation";
|
||||
import { Bone } from "./Bone";
|
||||
import { Constraint } from "./Constraint";
|
||||
import { Physics } from "./Physics";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import { SliderData } from "./SliderData";
|
||||
import { SliderPose } from "./SliderPose";
|
||||
|
||||
/** Stores the setup pose for a {@link PhysicsConstraint}.
|
||||
*
|
||||
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
|
||||
export class Slider extends Constraint<Slider, SliderData, SliderPose> {
|
||||
private static readonly offsets = new Array<number>();
|
||||
|
||||
bone: Bone | null = null;
|
||||
|
||||
constructor (data: SliderData, skeleton: Skeleton) {
|
||||
super(data, new SliderPose(), new SliderPose());
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
|
||||
if (data.bone != null) this.bone = skeleton.bones[data.bone.index];
|
||||
}
|
||||
|
||||
public copy (skeleton: Skeleton) {
|
||||
var copy = new Slider(this.data, skeleton);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public update (skeleton: Skeleton, physics: Physics) {
|
||||
const p = this.applied;
|
||||
if (p.mix === 0) return;
|
||||
|
||||
const data = this.data, animation = data.animation, bone = this.bone;
|
||||
if (bone !== null) {
|
||||
if (!bone.active) return;
|
||||
if (data.local) bone.applied.validateLocalTransform(skeleton);
|
||||
p.time = (data.property.value(bone.applied, data.local, Slider.offsets) - data.property.offset) * data.scale;
|
||||
if (data.loop)
|
||||
p.time = animation.duration + (p.time % animation.duration);
|
||||
else
|
||||
p.time = Math.max(0, p.time);
|
||||
}
|
||||
|
||||
const bones = skeleton.bones;
|
||||
const indices = animation.bones;
|
||||
for (let i = 0, n = animation.bones.length; i < n; i++)
|
||||
bones[indices[i]].applied.modifyLocal(skeleton);
|
||||
|
||||
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, data.additive ? MixBlend.add : MixBlend.replace,
|
||||
MixDirection.in, true);
|
||||
}
|
||||
|
||||
sort (skeleton: Skeleton) {
|
||||
const bone = this.bone;
|
||||
const data = this.data;
|
||||
if (bone && data.local) skeleton.sortBone(bone);
|
||||
skeleton._updateCache.push(this);
|
||||
|
||||
const bones = skeleton.bones;
|
||||
const indices = data.animation.bones;
|
||||
for (let i = 0, n = data.animation.bones.length; i < n; i++) {
|
||||
const bone = bones[indices[i]];
|
||||
bone.sorted = false;
|
||||
skeleton.sortReset(bone.children);
|
||||
skeleton.constrained(bone);
|
||||
}
|
||||
|
||||
const timelines = data.animation.timelines;
|
||||
const slots = skeleton.slots;
|
||||
const constraints = skeleton.constraints;
|
||||
const physics = skeleton.physics;
|
||||
const physicsCount = skeleton.physics.length;
|
||||
for (let i = 0, n = data.animation.timelines.length; i < n; i++) {
|
||||
const t = timelines[i];
|
||||
if (isSlotTimeline(t))
|
||||
skeleton.constrained(slots[t.slotIndex]);
|
||||
else if (t instanceof PhysicsConstraintTimeline) {
|
||||
if (t.constraintIndex == -1) {
|
||||
for (let ii = 0; ii < physicsCount; ii++)
|
||||
skeleton.constrained(physics[ii]);
|
||||
} else
|
||||
skeleton.constrained(constraints[t.constraintIndex]);
|
||||
} else if (isConstraintTimeline(t)) //
|
||||
skeleton.constrained(constraints[t.constraintIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
spine-ts/spine-core/src/SliderData.ts
Normal file
57
spine-ts/spine-core/src/SliderData.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Animation } from "./Animation";
|
||||
import { BoneData } from "./BoneData";
|
||||
import { ConstraintData } from "./ConstraintData";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import { Slider } from "./Slider";
|
||||
import { SliderPose } from "./SliderPose";
|
||||
import { FromProperty } from "./TransformConstraintData";
|
||||
|
||||
/** Stores the setup pose for a {@link SliderConstraint}.
|
||||
*
|
||||
* See <a href="https://esotericsoftware.com/spine-slider-constraints">Slider constraints</a> in the Spine User Guide. */
|
||||
export class SliderData extends ConstraintData<Slider, SliderPose> {
|
||||
animation!: Animation;
|
||||
additive = false;
|
||||
loop = false;
|
||||
bone: BoneData | null = null;
|
||||
property!: FromProperty;
|
||||
scale = 0;
|
||||
local = false;
|
||||
|
||||
constructor (name: string) {
|
||||
super(name, new SliderPose());
|
||||
}
|
||||
|
||||
public create (skeleton: Skeleton) {
|
||||
return new Slider(this, skeleton);
|
||||
}
|
||||
}
|
||||
41
spine-ts/spine-core/src/SliderPose.ts
Normal file
41
spine-ts/spine-core/src/SliderPose.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
|
||||
/** Stores a pose for a slider. */
|
||||
export class SliderPose implements Pose<SliderPose> {
|
||||
time = 0;
|
||||
mix = 0;
|
||||
|
||||
set (pose: SliderPose) {
|
||||
this.time = pose.time;
|
||||
this.mix = pose.mix;
|
||||
}
|
||||
}
|
||||
@ -27,86 +27,45 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Attachment, VertexAttachment } from "./attachments/Attachment.js";
|
||||
import { Bone } from "./Bone.js";
|
||||
import { Posed } from "./Posed.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { SlotData } from "./SlotData.js";
|
||||
import { SlotPose } from "./SlotPose.js";
|
||||
import { Color } from "./Utils.js";
|
||||
|
||||
/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
|
||||
* state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
|
||||
* across multiple skeletons. */
|
||||
export class Slot {
|
||||
/** The slot's setup pose data. */
|
||||
data: SlotData;
|
||||
export class Slot extends Posed<SlotData, SlotPose, SlotPose> {
|
||||
readonly skeleton: Skeleton;
|
||||
|
||||
/** The bone this slot belongs to. */
|
||||
bone: Bone;
|
||||
|
||||
/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
|
||||
* color tinting. */
|
||||
color: Color;
|
||||
|
||||
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
|
||||
* color's alpha is not used. */
|
||||
darkColor: Color | null = null;
|
||||
|
||||
attachment: Attachment | null = null;
|
||||
readonly bone: Bone;
|
||||
|
||||
attachmentState: number = 0;
|
||||
|
||||
/** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the
|
||||
* {@link Sequence#getSetupIndex()}. */
|
||||
sequenceIndex: number = -1;
|
||||
|
||||
/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
|
||||
* weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
|
||||
*
|
||||
* See {@link VertexAttachment#computeWorldVertices()} and {@link DeformTimeline}. */
|
||||
deform = new Array<number>();
|
||||
|
||||
constructor (data: SlotData, bone: Bone) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
if (!bone) throw new Error("bone cannot be null.");
|
||||
this.data = data;
|
||||
this.bone = bone;
|
||||
this.color = new Color();
|
||||
this.darkColor = !data.darkColor ? null : new Color();
|
||||
this.setToSetupPose();
|
||||
}
|
||||
|
||||
/** The skeleton this slot belongs to. */
|
||||
getSkeleton (): Skeleton {
|
||||
return this.bone.skeleton;
|
||||
}
|
||||
|
||||
/** The current attachment for the slot, or null if the slot has no attachment. */
|
||||
getAttachment (): Attachment | null {
|
||||
return this.attachment;
|
||||
}
|
||||
|
||||
/** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}.
|
||||
* The deform is not cleared if the old attachment has the same {@link VertexAttachment#getTimelineAttachment()} as the
|
||||
* specified attachment. */
|
||||
setAttachment (attachment: Attachment | null) {
|
||||
if (this.attachment == attachment) return;
|
||||
if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
|
||||
|| attachment.timelineAttachment != this.attachment.timelineAttachment) {
|
||||
this.deform.length = 0;
|
||||
constructor (data: SlotData, skeleton: Skeleton) {
|
||||
super(data, new SlotPose(), new SlotPose());
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
this.skeleton = skeleton;
|
||||
this.bone = skeleton.bones[data.boneData.index];
|
||||
if (data.setup.darkColor != null) {
|
||||
this.pose.darkColor = new Color();
|
||||
this.constrained.darkColor = new Color();
|
||||
}
|
||||
this.attachment = attachment;
|
||||
this.sequenceIndex = -1;
|
||||
this.setupPose();
|
||||
}
|
||||
|
||||
/** Sets this slot to the setup pose. */
|
||||
setToSetupPose () {
|
||||
this.color.setFromColor(this.data.color);
|
||||
if (this.darkColor) this.darkColor.setFromColor(this.data.darkColor!);
|
||||
setupPose () {
|
||||
this.pose.color.setFromColor(this.data.setup.color);
|
||||
if (this.pose.darkColor) this.pose.darkColor.setFromColor(this.data.setup.darkColor!);
|
||||
this.pose.sequenceIndex = this.data.setup.sequenceIndex;
|
||||
if (!this.data.attachmentName)
|
||||
this.attachment = null;
|
||||
this.pose.setAttachment(null);
|
||||
else {
|
||||
this.attachment = null;
|
||||
this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName));
|
||||
this.pose.attachment = null;
|
||||
this.pose.setAttachment(this.skeleton.getAttachment(this.data.index, this.data.attachmentName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,42 +28,34 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { Color } from "./Utils.js";
|
||||
import { PosedData } from "./PosedData.js";
|
||||
import { SlotPose } from "./SlotPose.js";
|
||||
|
||||
import type { Skeleton } from "./Skeleton.js";
|
||||
|
||||
/** Stores the setup pose for a {@link Slot}. */
|
||||
export class SlotData {
|
||||
/** The index of the slot in {@link Skeleton#getSlots()}. */
|
||||
export class SlotData extends PosedData<SlotPose> {
|
||||
/** The index of the slot in {@link Skeleton.getSlots()}. */
|
||||
index: number = 0;
|
||||
|
||||
/** The name of the slot, which is unique across all slots in the skeleton. */
|
||||
name: string;
|
||||
|
||||
/** The bone this slot belongs to. */
|
||||
boneData: BoneData;
|
||||
|
||||
/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
|
||||
* color tinting. */
|
||||
color = new Color(1, 1, 1, 1);
|
||||
|
||||
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
|
||||
* color's alpha is not used. */
|
||||
darkColor: Color | null = null;
|
||||
|
||||
/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
|
||||
attachmentName: string | null = null;
|
||||
|
||||
/** The blend mode for drawing the slot's attachment. */
|
||||
blendMode: BlendMode = BlendMode.Normal;
|
||||
|
||||
// Nonessential.
|
||||
/** False if the slot was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
|
||||
visible = true;
|
||||
|
||||
constructor (index: number, name: string, boneData: BoneData) {
|
||||
super(name, new SlotPose());
|
||||
if (index < 0) throw new Error("index must be >= 0.");
|
||||
if (!name) throw new Error("name cannot be null.");
|
||||
if (!boneData) throw new Error("boneData cannot be null.");
|
||||
this.index = index;
|
||||
this.name = name;
|
||||
this.boneData = boneData;
|
||||
}
|
||||
}
|
||||
|
||||
93
spine-ts/spine-core/src/SlotPose.ts
Normal file
93
spine-ts/spine-core/src/SlotPose.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose";
|
||||
import { Color } from "./Utils";
|
||||
|
||||
import { VertexAttachment } from "./attachments/Attachment";
|
||||
import { Attachment } from "./attachments/Attachment";
|
||||
import type { Sequence } from "./attachments/Sequence";
|
||||
|
||||
/** Stores a slot's pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store state
|
||||
* for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared across
|
||||
* multiple skeletons. */
|
||||
export class SlotPose implements Pose<SlotPose> {
|
||||
/** The color used to tint the slot's attachment. If {@link darkColor} is set, this is used as the light color for two
|
||||
* color tinting. */
|
||||
readonly color = new Color(1, 1, 1, 1);
|
||||
|
||||
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
|
||||
* color's alpha is not used. */
|
||||
darkColor: Color | null = null;
|
||||
|
||||
/** The current attachment for the slot, or null if the slot has no attachment. */
|
||||
attachment: Attachment | null = null; // Not used in setup pose.
|
||||
|
||||
/** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the
|
||||
* {@link Sequence.getSetupIndex()}. */
|
||||
sequenceIndex = 0;
|
||||
|
||||
/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
|
||||
* weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
|
||||
*
|
||||
* See {@link VertexAttachment.computeWorldVertices()} and
|
||||
* {@link DeformTimeline}. */
|
||||
readonly deform = new Array<number>();
|
||||
|
||||
SlotPose () {
|
||||
}
|
||||
|
||||
public set (pose: SlotPose): void {
|
||||
if (pose == null) throw new Error("pose cannot be null.");
|
||||
this.color.setFromColor(pose.color);
|
||||
if (this.darkColor != null && pose.darkColor != null) this.darkColor.setFromColor(pose.darkColor);
|
||||
this.attachment = pose.attachment;
|
||||
this.sequenceIndex = pose.sequenceIndex;
|
||||
this.deform.length = 0;
|
||||
this.deform.push(...pose.deform);
|
||||
}
|
||||
|
||||
/** The current attachment for the slot, or null if the slot has no attachment. */
|
||||
getAttachment (): Attachment | null {
|
||||
return this.attachment;
|
||||
}
|
||||
|
||||
/** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}.
|
||||
* The deform is not cleared if the old attachment has the same {@link VertexAttachment.getTimelineAttachment()} as the
|
||||
* specified attachment. */
|
||||
setAttachment (attachment: Attachment | null): void {
|
||||
if (this.attachment == attachment) return;
|
||||
if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
|
||||
|| attachment.timelineAttachment != this.attachment.timelineAttachment) {
|
||||
this.deform.length = 0;
|
||||
}
|
||||
this.attachment = attachment;
|
||||
this.sequenceIndex = -1;
|
||||
}
|
||||
}
|
||||
@ -28,87 +28,71 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { Bone } from "./Bone.js";
|
||||
import { Physics, Skeleton } from "./Skeleton.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
import { Constraint } from "./Constraint.js";
|
||||
import { Physics } from "./Physics.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { TransformConstraintData } from "./TransformConstraintData.js";
|
||||
import { Updatable } from "./Updatable.js";
|
||||
import { Vector2, MathUtils } from "./Utils.js";
|
||||
import { TransformConstraintPose } from "./TransformConstraintPose.js";
|
||||
import { MathUtils } from "./Utils.js";
|
||||
|
||||
|
||||
/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained
|
||||
* bones to match that of the source bone.
|
||||
*
|
||||
* See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */
|
||||
export class TransformConstraint implements Updatable {
|
||||
|
||||
/** The transform constraint's setup pose data. */
|
||||
data: TransformConstraintData;
|
||||
export class TransformConstraint extends Constraint<TransformConstraint, TransformConstraintData, TransformConstraintPose> {
|
||||
|
||||
/** The bones that will be modified by this transform constraint. */
|
||||
bones: Array<Bone>;
|
||||
bones: Array<BonePose>;
|
||||
|
||||
/** The bone whose world transform will be copied to the constrained bones. */
|
||||
source: Bone;
|
||||
|
||||
mixRotate = 0; mixX = 0; mixY = 0; mixScaleX = 0; mixScaleY = 0; mixShearY = 0;
|
||||
|
||||
temp = new Vector2();
|
||||
active = false;
|
||||
|
||||
constructor (data: TransformConstraintData, skeleton: Skeleton) {
|
||||
if (!data) throw new Error("data cannot be null.");
|
||||
super(data, new TransformConstraintPose(), new TransformConstraintPose());
|
||||
if (!skeleton) throw new Error("skeleton cannot be null.");
|
||||
this.data = data;
|
||||
|
||||
this.bones = new Array<Bone>();
|
||||
for (let i = 0; i < data.bones.length; i++) {
|
||||
let bone = skeleton.findBone(data.bones[i].name);
|
||||
if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}.`);
|
||||
this.bones.push(bone);
|
||||
}
|
||||
let target = skeleton.findBone(data.source.name);
|
||||
if (!target) throw new Error(`Couldn't find target bone ${data.source.name}.`);
|
||||
this.source = target;
|
||||
this.bones = new Array<BonePose>();
|
||||
for (const boneData of data.bones)
|
||||
this.bones.push(skeleton.bones[boneData.index].constrained);
|
||||
|
||||
this.mixRotate = data.mixRotate;
|
||||
this.mixX = data.mixX;
|
||||
this.mixY = data.mixY;
|
||||
this.mixScaleX = data.mixScaleX;
|
||||
this.mixScaleY = data.mixScaleY;
|
||||
this.mixShearY = data.mixShearY;
|
||||
const source = skeleton.bones[data.source.index];
|
||||
if (source == null) throw new Error("source cannot be null.");
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
isActive () {
|
||||
return this.active;
|
||||
public copy (skeleton: Skeleton) {
|
||||
var copy = new TransformConstraint(this.data, skeleton);
|
||||
copy.pose.set(this.pose);
|
||||
return copy;
|
||||
}
|
||||
|
||||
setToSetupPose () {
|
||||
const data = this.data;
|
||||
this.mixRotate = data.mixRotate;
|
||||
this.mixX = data.mixX;
|
||||
this.mixY = data.mixY;
|
||||
this.mixScaleX = data.mixScaleX;
|
||||
this.mixScaleY = data.mixScaleY;
|
||||
this.mixShearY = data.mixShearY;
|
||||
}
|
||||
|
||||
update (physics: Physics) {
|
||||
if (this.mixRotate == 0 && this.mixX == 0 && this.mixY == 0 && this.mixScaleX == 0 && this.mixScaleY == 0 && this.mixShearY == 0) return;
|
||||
update (skeleton: Skeleton, physics: Physics) {
|
||||
const p = this.applied;
|
||||
if (p.mixRotate == 0 && p.mixX == 0 && p.mixY == 0 && p.mixScaleX == 0 && p.mixScaleY == 0 && p.mixShearY == 0) return;
|
||||
|
||||
const data = this.data;
|
||||
const localFrom = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
|
||||
const source = this.source;
|
||||
const localSource = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
|
||||
const offsets = data.offsets;
|
||||
const source = this.source.applied;
|
||||
if (localSource) source.validateLocalTransform(skeleton);
|
||||
const fromItems = data.properties;
|
||||
const fn = data.properties.length;
|
||||
const fn = data.properties.length, update = skeleton._update;
|
||||
const bones = this.bones;
|
||||
for (let i = 0, n = this.bones.length; i < n; i++) {
|
||||
const bone = bones[i];
|
||||
if (localTarget)
|
||||
bone.modifyLocal(skeleton);
|
||||
else
|
||||
bone.modifyWorld(update);
|
||||
for (let f = 0; f < fn; f++) {
|
||||
const from = fromItems[f];
|
||||
const value = from.value(data, source, localFrom) - from.offset;
|
||||
const value = from.value(source, localSource, offsets) - from.offset;
|
||||
const toItems = from.to;
|
||||
for (let t = 0, tn = from.to.length; t < tn; t++) {
|
||||
var to = toItems[t];
|
||||
if (to.mix(this) != 0) {
|
||||
const to = toItems[t];
|
||||
if (to.mix(p) !== 0) {
|
||||
let clamped = to.offset + value * to.scale;
|
||||
if (clamp) {
|
||||
if (to.offset < to.max)
|
||||
@ -116,14 +100,34 @@ export class TransformConstraint implements Updatable {
|
||||
else
|
||||
clamped = MathUtils.clamp(clamped, to.max, to.offset);
|
||||
}
|
||||
to.apply(this, bone, clamped, localTarget, additive);
|
||||
to.apply(p, bone, clamped, localTarget, additive);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (localTarget)
|
||||
bone.update(null);
|
||||
else
|
||||
bone.updateAppliedTransform();
|
||||
}
|
||||
}
|
||||
|
||||
sort (skeleton: Skeleton) {
|
||||
if (!this.data.localSource) skeleton.sortBone(this.source);
|
||||
const bones = this.bones;
|
||||
const boneCount = this.bones.length;
|
||||
const worldTarget = !this.data.localTarget;
|
||||
if (worldTarget) {
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
skeleton.sortBone(bones[i].bone);
|
||||
}
|
||||
skeleton._updateCache.push(this);
|
||||
for (let i = 0; i < boneCount; i++) {
|
||||
const bone = bones[i].bone;
|
||||
skeleton.sortReset(bone.children);
|
||||
skeleton.constrained(bone);
|
||||
}
|
||||
for (let i = 0; i < boneCount; i++)
|
||||
bones[i].bone.sorted = worldTarget;
|
||||
}
|
||||
|
||||
isSourceActive () {
|
||||
return this.source.active;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,25 +29,28 @@
|
||||
|
||||
import { ConstraintData } from "./ConstraintData.js";
|
||||
import { BoneData } from "./BoneData.js";
|
||||
import { Bone } from "./Bone.js";
|
||||
import { TransformConstraint } from "./TransformConstraint.js";
|
||||
import { MathUtils } from "./Utils.js";
|
||||
import { Skeleton } from "./Skeleton.js";
|
||||
import { TransformConstraintPose } from "./TransformConstraintPose.js";
|
||||
import { BonePose } from "./BonePose.js";
|
||||
|
||||
/** Stores the setup pose for a {@link TransformConstraint}.
|
||||
*
|
||||
* See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */
|
||||
export class TransformConstraintData extends ConstraintData {
|
||||
|
||||
export class TransformConstraintData extends ConstraintData<TransformConstraint, TransformConstraintPose> {
|
||||
/** The bones that will be modified by this transform constraint. */
|
||||
bones = new Array<BoneData>();
|
||||
|
||||
/** The bone whose world transform will be copied to the constrained bones. */
|
||||
private _source: BoneData | null = null;
|
||||
public set source (source: BoneData) { this._source = source; }
|
||||
public get source () {
|
||||
if (!this._source) throw new Error("BoneData not set.")
|
||||
else return this._source;
|
||||
else return this._source;
|
||||
}
|
||||
private _source: BoneData | null = null;
|
||||
|
||||
offsets = new Array<number>();
|
||||
|
||||
/** An offset added to the constrained bone X translation. */
|
||||
offsetX = 0;
|
||||
@ -55,24 +58,6 @@ export class TransformConstraintData extends ConstraintData {
|
||||
/** An offset added to the constrained bone Y translation. */
|
||||
offsetY = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
|
||||
mixRotate = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
|
||||
mixX = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
|
||||
mixY = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
|
||||
mixScaleX = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
|
||||
mixScaleY = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
|
||||
mixShearY = 0;
|
||||
|
||||
/** Reads the source bone's local transform instead of its world transform. */
|
||||
localSource = false;
|
||||
|
||||
@ -82,14 +67,72 @@ export class TransformConstraintData extends ConstraintData {
|
||||
/** Adds the source bone transform to the constrained bones instead of setting it absolutely. */
|
||||
additive = false;
|
||||
|
||||
/** Prevents constrained bones from exceeding the ranged defined by {@link ToProperty#offset} and {@link ToProperty#max}. */
|
||||
/** Prevents constrained bones from exceeding the ranged defined by {@link ToProperty.offset} and {@link ToProperty.max}. */
|
||||
clamp = false;
|
||||
|
||||
/** The mapping of transform properties to other transform properties. */
|
||||
readonly properties: Array<FromProperty> = [];
|
||||
|
||||
constructor (name: string) {
|
||||
super(name, 0, false);
|
||||
super(name, new TransformConstraintPose());
|
||||
}
|
||||
|
||||
public create (skeleton: Skeleton) {
|
||||
return new TransformConstraint(this, skeleton);
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone rotation. */
|
||||
getOffsetRotation () {
|
||||
return this.offsets[0];
|
||||
}
|
||||
|
||||
setOffsetRotation (offsetRotation: number) {
|
||||
this.offsets[0] = offsetRotation;
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone X translation. */
|
||||
getOffsetX () {
|
||||
return this.offsets[1];
|
||||
}
|
||||
|
||||
setOffsetX (offsetX: number) {
|
||||
this.offsets[1] = offsetX;
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone Y translation. */
|
||||
getOffsetY () {
|
||||
return this.offsets[2];
|
||||
}
|
||||
|
||||
setOffsetY (offsetY: number) {
|
||||
this.offsets[2] = offsetY;
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone scaleX. */
|
||||
getOffsetScaleX () {
|
||||
return this.offsets[3];
|
||||
}
|
||||
|
||||
setOffsetScaleX (offsetScaleX: number) {
|
||||
this.offsets[3] = offsetScaleX;
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone scaleY. */
|
||||
getOffsetScaleY () {
|
||||
return this.offsets[4];
|
||||
}
|
||||
|
||||
setOffsetScaleY (offsetScaleY: number) {
|
||||
this.offsets[4] = offsetScaleY;
|
||||
}
|
||||
|
||||
/** An offset added to the constrained bone shearY. */
|
||||
getOffsetShearY () {
|
||||
return this.offsets[5];
|
||||
}
|
||||
|
||||
setOffsetShearY (offsetShearY: number) {
|
||||
this.offsets[5] = offsetShearY;
|
||||
}
|
||||
|
||||
}
|
||||
@ -103,7 +146,7 @@ export abstract class FromProperty {
|
||||
readonly to: Array<ToProperty> = [];
|
||||
|
||||
/** Reads this property from the specified bone. */
|
||||
abstract value (data: TransformConstraintData, source: Bone, local: boolean): number;
|
||||
abstract value (source: BonePose, local: boolean, offsets: Array<number>): number;
|
||||
}
|
||||
|
||||
/** Constrained property for a {@link TransformConstraint}. */
|
||||
@ -118,27 +161,31 @@ export abstract class ToProperty {
|
||||
scale = 0;
|
||||
|
||||
/** Reads the mix for this property from the specified constraint. */
|
||||
abstract mix (constraint: TransformConstraint): number;
|
||||
abstract mix (pose: TransformConstraintPose): number;
|
||||
|
||||
/** Applies the value to this property. */
|
||||
abstract apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void;
|
||||
abstract apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void;
|
||||
}
|
||||
|
||||
export class FromRotate extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.arotation : Math.atan2(source.c, source.a) * MathUtils.radDeg;
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
if (local) return source.rotation + offsets[0];
|
||||
let value = Math.atan2(source.c, source.a) * MathUtils.radDeg
|
||||
+ (source.a * source.d - source.b * source.c > 0 ? offsets[0] : -offsets[0]);
|
||||
if (value < 0) value += 360;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class ToRotate extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixRotate;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixRotate;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (!additive) value -= bone.arotation;
|
||||
bone.arotation += value * constraint.mixRotate;
|
||||
if (!additive) value -= bone.rotation;
|
||||
bone.rotation += value * pose.mixRotate;
|
||||
} else {
|
||||
const a = bone.a, b = bone.b, c = bone.c, d = bone.d;
|
||||
value *= MathUtils.degRad;
|
||||
@ -147,7 +194,7 @@ export class ToRotate extends ToProperty {
|
||||
value -= MathUtils.PI2;
|
||||
else if (value < -MathUtils.PI) //
|
||||
value += MathUtils.PI2;
|
||||
value *= constraint.mixRotate;
|
||||
value *= pose.mixRotate;
|
||||
const cos = Math.cos(value), sin = Math.sin(value);
|
||||
bone.a = cos * a - sin * c;
|
||||
bone.b = cos * b - sin * d;
|
||||
@ -158,73 +205,73 @@ export class ToRotate extends ToProperty {
|
||||
}
|
||||
|
||||
export class FromX extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.ax + data.offsetX : data.offsetX * source.a + data.offsetY * source.b + source.worldX;
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
return local ? source.x + offsets[1] : offsets[1] * source.a + offsets[2] * source.b + source.worldX;
|
||||
}
|
||||
}
|
||||
|
||||
export class ToX extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixX;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixX;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (!additive) value -= bone.ax;
|
||||
bone.ax += value * constraint.mixX;
|
||||
if (!additive) value -= bone.x;
|
||||
bone.x += value * pose.mixX;
|
||||
} else {
|
||||
if (!additive) value -= bone.worldX;
|
||||
bone.worldX += value * constraint.mixX;
|
||||
bone.worldX += value * pose.mixX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FromY extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.ay + data.offsetY : data.offsetX * source.c + data.offsetY * source.d + source.worldY;
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
return local ? source.y + offsets[2] : offsets[1] * source.c + offsets[2] * source.d + source.worldY;
|
||||
}
|
||||
}
|
||||
|
||||
export class ToY extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixY;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixY;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (!additive) value -= bone.ay;
|
||||
bone.ay += value * constraint.mixY;
|
||||
if (!additive) value -= bone.y;
|
||||
bone.y += value * pose.mixY;
|
||||
} else {
|
||||
if (!additive) value -= bone.worldY;
|
||||
bone.worldY += value * constraint.mixY;
|
||||
bone.worldY += value * pose.mixY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FromScaleX extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.ascaleX : Math.sqrt(source.a * source.a + source.c * source.c);
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
return local ? source.scaleX : Math.sqrt(source.a * source.a + source.c * source.c) + offsets[3];
|
||||
}
|
||||
}
|
||||
|
||||
export class ToScaleX extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixScaleX;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixScaleX;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (additive)
|
||||
bone.ascaleX *= 1 + ((value - 1) * constraint.mixScaleX);
|
||||
else if (bone.ascaleX != 0)
|
||||
bone.ascaleX = 1 + (value / bone.ascaleX - 1) * constraint.mixScaleX;
|
||||
bone.scaleX *= 1 + ((value - 1) * pose.mixScaleX);
|
||||
else if (bone.scaleX != 0) //
|
||||
bone.scaleX = 1 + (value / bone.scaleX - 1) * pose.mixScaleX;
|
||||
} else {
|
||||
let s: number;
|
||||
if (additive)
|
||||
s = 1 + (value - 1) * constraint.mixScaleX;
|
||||
s = 1 + (value - 1) * pose.mixScaleX;
|
||||
else {
|
||||
s = Math.sqrt(bone.a * bone.a + bone.c * bone.c);
|
||||
if (s != 0) s = 1 + (value / s - 1) * constraint.mixScaleX;
|
||||
if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleX;
|
||||
}
|
||||
bone.a *= s;
|
||||
bone.c *= s;
|
||||
@ -233,29 +280,29 @@ export class ToScaleX extends ToProperty {
|
||||
}
|
||||
|
||||
export class FromScaleY extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.ascaleY : Math.sqrt(source.b * source.b + source.d * source.d);
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
return local ? source.scaleY : Math.sqrt(source.b * source.b + source.d * source.d) + offsets[4];
|
||||
}
|
||||
}
|
||||
|
||||
export class ToScaleY extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixScaleY;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixScaleY;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (additive)
|
||||
bone.ascaleY *= 1 + ((value - 1) * constraint.mixScaleY);
|
||||
else if (bone.ascaleY != 0) //
|
||||
bone.ascaleY = 1 + (value / bone.ascaleY - 1) * constraint.mixScaleY;
|
||||
bone.scaleY *= 1 + ((value - 1) * pose.mixScaleY);
|
||||
else if (bone.scaleY != 0) //
|
||||
bone.scaleY = 1 + (value / bone.scaleY - 1) * pose.mixScaleY;
|
||||
} else {
|
||||
let s: number;
|
||||
if (additive)
|
||||
s = 1 + (value - 1) * constraint.mixScaleY;
|
||||
s = 1 + (value - 1) * pose.mixScaleY;
|
||||
else {
|
||||
s = Math.sqrt(bone.b * bone.b + bone.d * bone.d);
|
||||
if (s != 0) s = 1 + (value / s - 1) * constraint.mixScaleY;
|
||||
if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleY;
|
||||
}
|
||||
bone.b *= s;
|
||||
bone.d *= s;
|
||||
@ -264,20 +311,20 @@ export class ToScaleY extends ToProperty {
|
||||
}
|
||||
|
||||
export class FromShearY extends FromProperty {
|
||||
value (data: TransformConstraintData, source: Bone, local: boolean): number {
|
||||
return local ? source.ashearY : (Math.atan2(source.d, source.b) - Math.atan2(source.c, source.a)) * MathUtils.radDeg - 90;
|
||||
value (source: BonePose, local: boolean, offsets: Array<number>): number {
|
||||
return (local ? source.shearY : (Math.atan2(source.d, source.b) - Math.atan2(source.c, source.a)) * MathUtils.radDeg - 90) + offsets[5];
|
||||
}
|
||||
}
|
||||
|
||||
export class ToShearY extends ToProperty {
|
||||
mix (constraint: TransformConstraint): number {
|
||||
return constraint.mixShearY;
|
||||
mix (pose: TransformConstraintPose): number {
|
||||
return pose.mixShearY;
|
||||
}
|
||||
|
||||
apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void {
|
||||
apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void {
|
||||
if (local) {
|
||||
if (!additive) value -= bone.ashearY;
|
||||
bone.ashearY += value * constraint.mixShearY;
|
||||
if (!additive) value -= bone.shearY;
|
||||
bone.shearY += value * pose.mixShearY;
|
||||
} else {
|
||||
const b = bone.b, d = bone.d, by = Math.atan2(d, b);
|
||||
value = (value + 90) * MathUtils.degRad;
|
||||
@ -290,7 +337,7 @@ export class ToShearY extends ToProperty {
|
||||
else if (value < -MathUtils.PI)
|
||||
value += MathUtils.PI2;
|
||||
}
|
||||
value = by + value * constraint.mixShearY;
|
||||
value = by + value * pose.mixShearY;
|
||||
const s = Math.sqrt(b * b + d * d);
|
||||
bone.b = Math.cos(value) * s;
|
||||
bone.d = Math.sin(value) * s;
|
||||
|
||||
60
spine-ts/spine-core/src/TransformConstraintPose.ts
Normal file
60
spine-ts/spine-core/src/TransformConstraintPose.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Pose } from "./Pose"
|
||||
|
||||
/** Stores a pose for a transform constraint. */
|
||||
export class TransformConstraintPose implements Pose<TransformConstraintPose> {
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
|
||||
mixRotate = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
|
||||
mixX = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
|
||||
mixY = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
|
||||
mixScaleX = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
|
||||
mixScaleY = 0;
|
||||
|
||||
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
|
||||
mixShearY = 0;
|
||||
|
||||
public set (pose: TransformConstraintPose) {
|
||||
this.mixRotate = pose.mixRotate;
|
||||
this.mixX = pose.mixX;
|
||||
this.mixY = pose.mixY;
|
||||
this.mixScaleX = pose.mixScaleX;
|
||||
this.mixScaleY = pose.mixScaleY;
|
||||
this.mixShearY = pose.mixShearY;
|
||||
}
|
||||
}
|
||||
@ -27,19 +27,11 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Physics } from "./Skeleton.js";
|
||||
import type { Physics } from "./Physics.js";
|
||||
import type { Skeleton } from "./Skeleton.js";
|
||||
|
||||
/** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */
|
||||
export interface Updatable {
|
||||
export interface Update {
|
||||
/** @param physics Determines how physics and other non-deterministic updates are applied. */
|
||||
update (physics: Physics): void;
|
||||
|
||||
/** Returns false when this item won't be updated by
|
||||
* {@link Skeleton#updateWorldTransform()} because a skin is required and the
|
||||
* {@link Skeleton#getSkin() active skin} does not contain this item.
|
||||
* @see Skin#getBones()
|
||||
* @see Skin#getConstraints()
|
||||
* @see BoneData#getSkinRequired()
|
||||
* @see ConstraintData#getSkinRequired() */
|
||||
isActive (): boolean;
|
||||
update (skeleton: Skeleton, physics: Physics): void;
|
||||
}
|
||||
@ -203,7 +203,7 @@ export class MathUtils {
|
||||
}
|
||||
|
||||
static atan2Deg (y: number, x: number) {
|
||||
return Math.atan2(y, x) * MathUtils.degRad;
|
||||
return Math.atan2(y, x) * MathUtils.radDeg;
|
||||
}
|
||||
|
||||
static signum (value: number): number {
|
||||
@ -347,8 +347,8 @@ export class Utils {
|
||||
export class DebugUtils {
|
||||
static logBones (skeleton: Skeleton) {
|
||||
for (let i = 0; i < skeleton.bones.length; i++) {
|
||||
let bone = skeleton.bones[i];
|
||||
console.log(bone.data.name + ", " + bone.a + ", " + bone.b + ", " + bone.c + ", " + bone.d + ", " + bone.worldX + ", " + bone.worldY);
|
||||
let bone = skeleton.bones[i].applied;
|
||||
console.log(bone.bone.data.name + ", " + bone.a + ", " + bone.b + ", " + bone.c + ", " + bone.d + ", " + bone.worldX + ", " + bone.worldY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Skeleton } from "src/Skeleton.js";
|
||||
import { Slot } from "../Slot.js";
|
||||
import { NumberArrayLike, Utils } from "../Utils.js";
|
||||
|
||||
@ -43,15 +44,15 @@ export abstract class Attachment {
|
||||
}
|
||||
|
||||
/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's
|
||||
* {@link Slot#deform}. */
|
||||
* {@link SlotPose.deform}. */
|
||||
export abstract class VertexAttachment extends Attachment {
|
||||
private static nextID = 0;
|
||||
|
||||
/** The unique ID for this attachment. */
|
||||
id = VertexAttachment.nextID++;
|
||||
|
||||
/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
|
||||
* the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#bones}. Will be null
|
||||
/** The bones which affect the {@link vertices}. The array entries are, for each vertex, the number of bones affecting
|
||||
* the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton.bones}. Will be null
|
||||
* if this attachment has no weights. */
|
||||
bones: Array<number> | null = null;
|
||||
|
||||
@ -61,7 +62,7 @@ export abstract class VertexAttachment extends Attachment {
|
||||
vertices: NumberArrayLike = [];
|
||||
|
||||
/** The maximum number of world vertex values that can be output by
|
||||
* {@link #computeWorldVertices()} using the `count` parameter. */
|
||||
* {@link computeWorldVertices} using the `count` parameter. */
|
||||
worldVerticesLength = 0;
|
||||
|
||||
/** Timelines for the timeline attachment are also applied to this attachment.
|
||||
@ -72,7 +73,7 @@ export abstract class VertexAttachment extends Attachment {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/** Transforms the attachment's local {@link #vertices} to world coordinates. If the slot's {@link Slot#deform} is
|
||||
/** Transforms the attachment's local {@link #vertices} to world coordinates. If the slot's {@link SlotPose.deform} is
|
||||
* not empty, it is used to deform the vertices.
|
||||
*
|
||||
* See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
|
||||
@ -83,15 +84,16 @@ export abstract class VertexAttachment extends Attachment {
|
||||
* `stride` / 2.
|
||||
* @param offset The `worldVertices` index to begin writing values.
|
||||
* @param stride The number of `worldVertices` entries between the value pairs written. */
|
||||
computeWorldVertices (slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
|
||||
computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number,
|
||||
stride: number) {
|
||||
|
||||
count = offset + (count >> 1) * stride;
|
||||
let skeleton = slot.bone.skeleton;
|
||||
let deformArray = slot.deform;
|
||||
let deformArray = slot.applied.deform;
|
||||
let vertices = this.vertices;
|
||||
let bones = this.bones;
|
||||
if (!bones) {
|
||||
if (deformArray.length > 0) vertices = deformArray;
|
||||
let bone = slot.bone;
|
||||
let bone = slot.bone.applied;
|
||||
let x = bone.worldX;
|
||||
let y = bone.worldY;
|
||||
let a = bone.a, b = bone.b, c = bone.c, d = bone.d;
|
||||
@ -115,7 +117,7 @@ export abstract class VertexAttachment extends Attachment {
|
||||
let n = bones[v++];
|
||||
n += v;
|
||||
for (; v < n; v++, b += 3) {
|
||||
let bone = skeletonBones[bones[v]];
|
||||
let bone = skeletonBones[bones[v]].applied;
|
||||
let vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
|
||||
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
|
||||
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;
|
||||
@ -130,7 +132,7 @@ export abstract class VertexAttachment extends Attachment {
|
||||
let n = bones[v++];
|
||||
n += v;
|
||||
for (; v < n; v++, b += 3, f += 2) {
|
||||
let bone = skeletonBones[bones[v]];
|
||||
let bone = skeletonBones[bones[v]].applied;
|
||||
let vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
|
||||
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
|
||||
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;
|
||||
|
||||
@ -34,6 +34,7 @@ import { VertexAttachment, Attachment } from "./Attachment.js";
|
||||
import { HasTextureRegion } from "./HasTextureRegion.js";
|
||||
import { Sequence } from "./Sequence.js";
|
||||
import { Slot } from "../Slot.js";
|
||||
import { Skeleton } from "src/Skeleton.js";
|
||||
|
||||
/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
|
||||
* supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
|
||||
@ -126,11 +127,12 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
|
||||
uvs[i + 1] = v + regionUVs[i] * height;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
u -= region.offsetX / textureWidth;
|
||||
v -= (region.originalHeight - region.offsetY - region.height) / textureHeight;
|
||||
width = region.originalWidth / textureWidth;
|
||||
height = region.originalHeight / textureHeight;
|
||||
}
|
||||
u -= region.offsetX / textureWidth;
|
||||
v -= (region.originalHeight - region.offsetY - region.height) / textureHeight;
|
||||
width = region.originalWidth / textureWidth;
|
||||
height = region.originalHeight / textureHeight;
|
||||
} else if (!this.region) {
|
||||
u = v = 0;
|
||||
width = height = 1;
|
||||
@ -195,9 +197,9 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
|
||||
return copy;
|
||||
}
|
||||
|
||||
computeWorldVertices (slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
|
||||
if (this.sequence != null) this.sequence.apply(slot, this);
|
||||
super.computeWorldVertices(slot, start, count, worldVertices, offset, stride);
|
||||
computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
|
||||
if (this.sequence != null) this.sequence.apply(slot.applied, this);
|
||||
super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride);
|
||||
}
|
||||
|
||||
/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Bone } from "../Bone.js";
|
||||
import { BonePose } from "src/BonePose.js";
|
||||
import { Color, Vector2, MathUtils } from "../Utils.js";
|
||||
import { VertexAttachment, Attachment } from "./Attachment.js";
|
||||
|
||||
@ -49,17 +49,17 @@ export class PointAttachment extends VertexAttachment {
|
||||
super(name);
|
||||
}
|
||||
|
||||
computeWorldPosition (bone: Bone, point: Vector2) {
|
||||
computeWorldPosition (bone: BonePose, point: Vector2) {
|
||||
point.x = this.x * bone.a + this.y * bone.b + bone.worldX;
|
||||
point.y = this.x * bone.c + this.y * bone.d + bone.worldY;
|
||||
return point;
|
||||
}
|
||||
|
||||
computeWorldRotation (bone: Bone) {
|
||||
computeWorldRotation (bone: BonePose) {
|
||||
const r = this.rotation * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
|
||||
const x = cos * bone.a + sin * bone.b;
|
||||
const y = cos * bone.c + sin * bone.d;
|
||||
return MathUtils.atan2Deg(y, x);
|
||||
return MathUtils.atan2Deg(y, x);
|
||||
}
|
||||
|
||||
copy (): Attachment {
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { Bone } from "../Bone.js";
|
||||
import { TextureRegion } from "../Texture.js";
|
||||
import { Color, MathUtils, NumberArrayLike, Utils } from "../Utils.js";
|
||||
import { Attachment } from "./Attachment.js";
|
||||
@ -159,10 +158,9 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
|
||||
* @param offset The <code>worldVertices</code> index to begin writing values.
|
||||
* @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
|
||||
computeWorldVertices (slot: Slot, worldVertices: NumberArrayLike, offset: number, stride: number) {
|
||||
if (this.sequence != null)
|
||||
this.sequence.apply(slot, this);
|
||||
if (this.sequence) this.sequence.apply(slot.applied, this);
|
||||
|
||||
let bone = slot.bone;
|
||||
let bone = slot.bone.applied;
|
||||
let vertexOffset = this.offset;
|
||||
let x = bone.worldX, y = bone.worldY;
|
||||
let a = bone.a, b = bone.b, c = bone.c, d = bone.d;
|
||||
|
||||
@ -28,9 +28,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { TextureRegion } from "../Texture.js";
|
||||
import { Slot } from "../Slot.js";
|
||||
import { HasTextureRegion } from "./HasTextureRegion.js";
|
||||
import { Utils } from "../Utils.js";
|
||||
import { SlotPose } from "src/SlotPose.js";
|
||||
|
||||
|
||||
export class Sequence {
|
||||
@ -56,7 +56,7 @@ export class Sequence {
|
||||
return copy;
|
||||
}
|
||||
|
||||
apply (slot: Slot, attachment: HasTextureRegion) {
|
||||
apply (slot: SlotPose, attachment: HasTextureRegion) {
|
||||
let index = slot.sequenceIndex;
|
||||
if (index == -1) index = this.setupIndex;
|
||||
if (index >= this.regions.length) index = this.regions.length - 1;
|
||||
|
||||
@ -2,16 +2,29 @@ export * from './Animation.js';
|
||||
export * from './AnimationState.js';
|
||||
export * from './AnimationStateData.js';
|
||||
export * from './AtlasAttachmentLoader.js';
|
||||
export * from './AssetManagerBase.js';
|
||||
export * from './Bone.js';
|
||||
export * from './BoneData.js';
|
||||
export * from './BoneLocal.js';
|
||||
export * from './BonePose.js';
|
||||
export * from './Constraint.js';
|
||||
export * from './ConstraintData.js';
|
||||
export * from './AssetManagerBase.js';
|
||||
export * from './Event.js';
|
||||
export * from './EventData.js';
|
||||
export * from './IkConstraint.js';
|
||||
export * from './IkConstraintData.js';
|
||||
export * from './IkConstraintPose.js';
|
||||
export * from './PathConstraint.js';
|
||||
export * from './PathConstraintData.js';
|
||||
export * from './PathConstraintPose.js';
|
||||
export * from './Physics.js';
|
||||
export * from './PhysicsConstraint.js';
|
||||
export * from './PhysicsConstraintData.js';
|
||||
export * from './PhysicsConstraintPose.js';
|
||||
export * from './Pose.js';
|
||||
export * from './Posed.js';
|
||||
export * from './PosedActive.js';
|
||||
export * from './PosedData.js';
|
||||
export * from './Skeleton.js';
|
||||
export * from './SkeletonBinary.js';
|
||||
export * from './SkeletonBounds.js';
|
||||
@ -19,21 +32,28 @@ export * from './SkeletonClipping.js';
|
||||
export * from './SkeletonData.js';
|
||||
export * from './SkeletonJson.js';
|
||||
export * from './Skin.js';
|
||||
export * from './Slider.js';
|
||||
export * from './SliderData.js';
|
||||
export * from './SliderPose.js';
|
||||
export * from './Slot.js';
|
||||
export * from './SlotData.js';
|
||||
export * from './SlotPose.js';
|
||||
export * from './Texture.js';
|
||||
export * from './TextureAtlas.js';
|
||||
export * from './TransformConstraint.js';
|
||||
export * from './TransformConstraintData.js';
|
||||
export * from './TransformConstraintPose.js';
|
||||
export * from './Triangulator.js';
|
||||
export * from './Updatable.js';
|
||||
export * from './Update.js';
|
||||
export * from './Utils.js';
|
||||
export * from './polyfills.js';
|
||||
export * from './attachments/Attachment.js';
|
||||
export * from './attachments/AttachmentLoader.js';
|
||||
export * from './attachments/BoundingBoxAttachment.js';
|
||||
export * from './attachments/ClippingAttachment.js';
|
||||
export * from './attachments/HasTextureRegion.js';
|
||||
export * from './attachments/MeshAttachment.js';
|
||||
export * from './attachments/PathAttachment.js';
|
||||
export * from './attachments/PointAttachment.js';
|
||||
export * from './attachments/RegionAttachment.js';
|
||||
export * from './attachments/Sequence.js';
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
skin.addSkin(skeletonData.findSkin("accessories/bag"));
|
||||
skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
|
||||
mixAndMatch.skeleton.setSkin(skin);
|
||||
mixAndMatch.skeleton.setToSetupPose();
|
||||
mixAndMatch.skeleton.setupPose();
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@ -96,7 +96,7 @@ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider {
|
||||
// the skeleton in the GameObject has already been heavily modified. We can not
|
||||
// reconstruct that state.
|
||||
const skeleton = new Skeleton(gameObject.skeleton.data);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
|
||||
return bounds.width == Number.NEGATIVE_INFINITY
|
||||
@ -145,7 +145,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
}
|
||||
skeleton.setSkin(customSkin);
|
||||
}
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
const animation =
|
||||
this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||
@ -161,7 +161,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
maxX = Number.NEGATIVE_INFINITY,
|
||||
maxY = Number.NEGATIVE_INFINITY;
|
||||
animationState.clearTracks();
|
||||
animationState.setAnimationWith(0, animation, false);
|
||||
animationState.setAnimation(0, animation, false);
|
||||
const steps = Math.max(animation.duration / this.timeStep, 1.0);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const delta = i > 0 ? this.timeStep : 0;
|
||||
@ -296,9 +296,9 @@ export class SpineGameObject extends DepthMixin(
|
||||
phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
|
||||
this.phaserWorldCoordinatesToSkeleton(point);
|
||||
if (bone.parent) {
|
||||
bone.parent.worldToLocal(point as Vector2);
|
||||
bone.parent.applied.worldToLocal(point as Vector2);
|
||||
} else {
|
||||
bone.worldToLocal(point as Vector2);
|
||||
bone.applied.worldToLocal(point as Vector2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,7 +418,7 @@ export class SpineGameObject extends DepthMixin(
|
||||
skeleton.scaleX = transform.scaleX;
|
||||
skeleton.scaleY = transform.scaleY;
|
||||
let root = skeleton.getRootBone()!;
|
||||
root.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
|
||||
root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
|
||||
this.skeleton.updateWorldTransform(Physics.update);
|
||||
|
||||
context.save();
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
skin.addSkin(skeletonData.findSkin("accessories/bag"));
|
||||
skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
|
||||
mixAndMatch.skeleton.setSkin(skin);
|
||||
mixAndMatch.skeleton.setToSetupPose();
|
||||
mixAndMatch.skeleton.setupPose();
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@ -96,7 +96,7 @@ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider {
|
||||
// the skeleton in the GameObject has already been heavily modified. We can not
|
||||
// reconstruct that state.
|
||||
const skeleton = new Skeleton(gameObject.skeleton.data);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
|
||||
return bounds.width == Number.NEGATIVE_INFINITY
|
||||
@ -145,7 +145,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
}
|
||||
skeleton.setSkin(customSkin);
|
||||
}
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
const animation =
|
||||
this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||
@ -161,7 +161,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
maxX = Number.NEGATIVE_INFINITY,
|
||||
maxY = Number.NEGATIVE_INFINITY;
|
||||
animationState.clearTracks();
|
||||
animationState.setAnimationWith(0, animation, false);
|
||||
animationState.setAnimation(0, animation, false);
|
||||
const steps = Math.max(animation.duration / this.timeStep, 1.0);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const delta = i > 0 ? this.timeStep : 0;
|
||||
@ -296,9 +296,9 @@ export class SpineGameObject extends DepthMixin(
|
||||
phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
|
||||
this.phaserWorldCoordinatesToSkeleton(point);
|
||||
if (bone.parent) {
|
||||
bone.parent.worldToLocal(point as Vector2);
|
||||
bone.parent.applied.worldToLocal(point as Vector2);
|
||||
} else {
|
||||
bone.worldToLocal(point as Vector2);
|
||||
bone.applied.worldToLocal(point as Vector2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -440,7 +440,7 @@ export class SpineGameObject extends DepthMixin(
|
||||
skeleton.scaleX = transform.scaleX;
|
||||
skeleton.scaleY = transform.scaleY;
|
||||
let root = skeleton.getRootBone()!;
|
||||
root.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
|
||||
root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
|
||||
this.skeleton.updateWorldTransform(Physics.update);
|
||||
|
||||
context.save();
|
||||
|
||||
@ -119,8 +119,8 @@
|
||||
|
||||
// resetting the slot with the original attachment
|
||||
setTimeout(() => {
|
||||
frontFist.setToSetupPose();
|
||||
frontFist.bone.setToSetupPose();
|
||||
frontFist.setupPose();
|
||||
frontFist.bone.setupPose();
|
||||
}, 10000);
|
||||
|
||||
// showing an animation with clipping -> Pixi masks will be created
|
||||
|
||||
@ -168,7 +168,7 @@ export class SetupPoseBoundsProvider implements SpineBoundsProvider {
|
||||
// the skeleton in the GameObject has already been heavily modified. We can not
|
||||
// reconstruct that state.
|
||||
const skeleton = new Skeleton(gameObject.skeleton.data);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
|
||||
return bounds.width == Number.NEGATIVE_INFINITY
|
||||
@ -217,7 +217,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
}
|
||||
skeleton.setSkin(customSkin);
|
||||
}
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
const animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||
|
||||
@ -233,7 +233,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
maxX = Number.NEGATIVE_INFINITY,
|
||||
maxY = Number.NEGATIVE_INFINITY;
|
||||
animationState.clearTracks();
|
||||
animationState.setAnimationWith(0, animation, false);
|
||||
animationState.setAnimation(0, animation, false);
|
||||
const steps = Math.max(animation.duration / this.timeStep, 1.0);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const delta = i > 0 ? this.timeStep : 0;
|
||||
@ -367,7 +367,7 @@ export class Spine extends Container {
|
||||
// dark tint can be enabled by options, otherwise is enable if at least one slot has tint black
|
||||
if (options?.darkTint !== undefined || oldOptions?.slotMeshFactory === undefined) {
|
||||
this.darkTint = options?.darkTint === undefined
|
||||
? this.skeleton.slots.some(slot => !!slot.data.darkColor)
|
||||
? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor)
|
||||
: options?.darkTint;
|
||||
if (this.darkTint) this.slotMeshFactory = () => new DarkSlotMesh();
|
||||
} else {
|
||||
@ -390,7 +390,7 @@ export class Spine extends Container {
|
||||
tempSlotMeshFactory.destroy();
|
||||
} else {
|
||||
for (let i = 0; i < this.skeleton.slots.length; i++) {
|
||||
if (this.skeleton.slots[i].data.darkColor) {
|
||||
if (this.skeleton.slots[i].data.setup.darkColor) {
|
||||
this.slotMeshFactory = () => new DarkSlotMesh();
|
||||
this.darkTint = true;
|
||||
break;
|
||||
@ -587,31 +587,33 @@ export class Spine extends Container {
|
||||
private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) {
|
||||
const { container: slotObject, followAttachmentTimeline } = element
|
||||
|
||||
const followAttachmentValue = followAttachmentTimeline ? Boolean(slot.attachment) : true;
|
||||
const pose = slot.pose;
|
||||
const followAttachmentValue = followAttachmentTimeline ? Boolean(pose.attachment) : true;
|
||||
slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue;
|
||||
|
||||
if (slotObject.visible) {
|
||||
slotObject.position.set(slot.bone.worldX, slot.bone.worldY);
|
||||
slotObject.angle = slot.bone.getWorldRotationX();
|
||||
const applied = slot.bone.applied;
|
||||
slotObject.position.set(applied.worldX, applied.worldY);
|
||||
slotObject.angle = applied.getWorldRotationX();
|
||||
|
||||
let bone: Bone | null = slot.bone;
|
||||
let cumulativeScaleX = 1;
|
||||
let cumulativeScaleY = 1;
|
||||
while (bone) {
|
||||
cumulativeScaleX *= bone.scaleX;
|
||||
cumulativeScaleY *= bone.scaleY;
|
||||
cumulativeScaleX *= bone.applied.scaleX;
|
||||
cumulativeScaleY *= bone.applied.scaleY;
|
||||
bone = bone.parent;
|
||||
};
|
||||
|
||||
if (cumulativeScaleX < 0) slotObject.angle -= 180;
|
||||
|
||||
slotObject.scale.set(
|
||||
slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX),
|
||||
slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY),
|
||||
applied.getWorldScaleX() * Math.sign(cumulativeScaleX),
|
||||
applied.getWorldScaleY() * Math.sign(cumulativeScaleY),
|
||||
);
|
||||
|
||||
slotObject.zIndex = zIndex + 1;
|
||||
slotObject.alpha = this.skeleton.color.a * slot.color.a;
|
||||
slotObject.alpha = this.skeleton.color.a * pose.color.a;
|
||||
}
|
||||
}
|
||||
|
||||
@ -625,10 +627,10 @@ export class Spine extends Container {
|
||||
}
|
||||
if (!pixiMaskSource.computed) {
|
||||
pixiMaskSource.computed = true;
|
||||
const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment;
|
||||
const clippingAttachment = pixiMaskSource.slot.pose.attachment as ClippingAttachment;
|
||||
const worldVerticesLength = clippingAttachment.worldVerticesLength;
|
||||
if (this.clippingVertAux.length < worldVerticesLength) this.clippingVertAux = new Float32Array(worldVerticesLength);
|
||||
clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2);
|
||||
clippingAttachment.computeWorldVertices(this.skeleton, pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2);
|
||||
mask.clear().lineStyle(0).beginFill(0x000000);
|
||||
mask.moveTo(this.clippingVertAux[0], this.clippingVertAux[1]);
|
||||
for (let i = 2; i < worldVerticesLength; i += 2) {
|
||||
@ -673,17 +675,19 @@ export class Spine extends Container {
|
||||
this.updateAndSetPixiMask(pixiMaskSource, pixiObject.container);
|
||||
}
|
||||
|
||||
const useDarkColor = slot.darkColor != null;
|
||||
const pose = slot.pose;
|
||||
const useDarkColor = !!pose.darkColor;
|
||||
const vertexSize = useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
|
||||
if (!slot.bone.active) {
|
||||
Spine.clipper.clipEndWithSlot(slot);
|
||||
Spine.clipper.clipEnd(slot);
|
||||
this.pixiMaskCleanup(slot);
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = pose.attachment;
|
||||
let attachmentColor: Color | null;
|
||||
let texture: SpineTexture | null;
|
||||
let numFloats = 0;
|
||||
const skeleton = this.skeleton;
|
||||
if (attachment instanceof RegionAttachment) {
|
||||
const region = attachment;
|
||||
attachmentColor = region.color;
|
||||
@ -699,26 +703,25 @@ export class Spine extends Container {
|
||||
if (numFloats > this.verticesCache.length) {
|
||||
this.verticesCache = Utils.newFloatArray(numFloats);
|
||||
}
|
||||
mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize);
|
||||
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize);
|
||||
triangles = mesh.triangles;
|
||||
uvs = mesh.uvs;
|
||||
texture = <SpineTexture>mesh.region?.texture;
|
||||
} else if (attachment instanceof ClippingAttachment) {
|
||||
Spine.clipper.clipStart(slot, attachment);
|
||||
Spine.clipper.clipStart(skeleton, slot, attachment);
|
||||
pixiMaskSource = { slot, computed: false };
|
||||
continue;
|
||||
} else {
|
||||
if (this.hasMeshForSlot(slot)) {
|
||||
this.getMeshForSlot(slot).visible = false;
|
||||
}
|
||||
Spine.clipper.clipEndWithSlot(slot);
|
||||
Spine.clipper.clipEnd(slot);
|
||||
this.pixiMaskCleanup(slot);
|
||||
continue;
|
||||
}
|
||||
if (texture != null) {
|
||||
const skeleton = slot.bone.skeleton;
|
||||
const skeletonColor = skeleton.color;
|
||||
const slotColor = slot.color;
|
||||
const slotColor = pose.color;
|
||||
const alpha = skeletonColor.a * slotColor.a * attachmentColor.a;
|
||||
// cannot premultiply the colors because the default mesh renderer already does that
|
||||
this.lightColor.set(
|
||||
@ -727,11 +730,11 @@ export class Spine extends Container {
|
||||
skeletonColor.b * slotColor.b * attachmentColor.b,
|
||||
alpha
|
||||
);
|
||||
if (slot.darkColor != null) {
|
||||
if (pose.darkColor != null) {
|
||||
this.darkColor.set(
|
||||
slot.darkColor.r,
|
||||
slot.darkColor.g,
|
||||
slot.darkColor.b,
|
||||
pose.darkColor.r,
|
||||
pose.darkColor.g,
|
||||
pose.darkColor.b,
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
@ -775,7 +778,7 @@ export class Spine extends Container {
|
||||
}
|
||||
|
||||
if (finalVerticesLength == 0 || finalIndicesLength == 0) {
|
||||
Spine.clipper.clipEndWithSlot(slot);
|
||||
Spine.clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -785,7 +788,7 @@ export class Spine extends Container {
|
||||
mesh.updateFromSpineData(texture, slot.data.blendMode, slot.data.name, finalVertices, finalVerticesLength, finalIndices, finalIndicesLength, useDarkColor);
|
||||
}
|
||||
|
||||
Spine.clipper.clipEndWithSlot(slot);
|
||||
Spine.clipper.clipEnd(slot);
|
||||
this.pixiMaskCleanup(slot);
|
||||
}
|
||||
Spine.clipper.clipEnd();
|
||||
@ -852,14 +855,14 @@ export class Spine extends Container {
|
||||
if (!bone) throw Error(`Cannot set bone position, bone ${String(boneAux)} not found`);
|
||||
Spine.vectorAux.set(position.x, position.y);
|
||||
|
||||
const applied = bone.applied;
|
||||
if (bone.parent) {
|
||||
const aux = bone.parent.worldToLocal(Spine.vectorAux);
|
||||
bone.x = aux.x;
|
||||
bone.y = aux.y;
|
||||
}
|
||||
else {
|
||||
bone.x = Spine.vectorAux.x;
|
||||
bone.y = Spine.vectorAux.y;
|
||||
const aux = bone.parent.applied.worldToLocal(Spine.vectorAux);
|
||||
applied.x = aux.x;
|
||||
applied.y = aux.y;
|
||||
} else {
|
||||
applied.x = Spine.vectorAux.x;
|
||||
applied.y = Spine.vectorAux.y;
|
||||
}
|
||||
}
|
||||
|
||||
@ -884,8 +887,8 @@ export class Spine extends Container {
|
||||
outPos = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
outPos.x = bone.worldX;
|
||||
outPos.y = bone.worldY;
|
||||
outPos.x = bone.applied.worldX;
|
||||
outPos.y = bone.applied.worldY;
|
||||
return outPos;
|
||||
}
|
||||
|
||||
@ -903,9 +906,9 @@ export class Spine extends Container {
|
||||
pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
|
||||
this.pixiWorldCoordinatesToSkeleton(point);
|
||||
if (bone.parent) {
|
||||
bone.parent.worldToLocal(point as Vector2);
|
||||
bone.parent.applied.worldToLocal(point as Vector2);
|
||||
} else {
|
||||
bone.worldToLocal(point as Vector2);
|
||||
bone.applied.worldToLocal(point as Vector2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -243,10 +243,11 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
for (let i = 0, len = bones.length; i < len; i++) {
|
||||
const bone = bones[i];
|
||||
const boneLen = bone.data.length;
|
||||
const starX = skeletonX + bone.worldX;
|
||||
const starY = skeletonY + bone.worldY;
|
||||
const endX = skeletonX + boneLen * bone.a + bone.worldX;
|
||||
const endY = skeletonY + boneLen * bone.b + bone.worldY;
|
||||
const applied = bone.applied;
|
||||
const starX = skeletonX + applied.worldX;
|
||||
const starY = skeletonY + applied.worldY;
|
||||
const endX = skeletonX + boneLen * applied.a + applied.worldX;
|
||||
const endY = skeletonY + boneLen * applied.b + applied.worldY;
|
||||
|
||||
if (bone.data.name === "root" || bone.data.parent === null) {
|
||||
continue;
|
||||
@ -337,7 +338,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
|
||||
for (let i = 0, len = slots.length; i < len; i++) {
|
||||
const slot = slots[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment == null || !(attachment instanceof RegionAttachment)) {
|
||||
continue;
|
||||
@ -365,7 +366,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment == null || !(attachment instanceof MeshAttachment)) {
|
||||
continue;
|
||||
@ -377,7 +378,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
const triangles = meshAttachment.triangles;
|
||||
let hullLength = meshAttachment.hullLength;
|
||||
|
||||
meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2);
|
||||
meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2);
|
||||
// draw the skinned mesh (triangle)
|
||||
if (this.drawMeshTriangles) {
|
||||
for (let i = 0, len = triangles.length; i < len; i += 3) {
|
||||
@ -421,7 +422,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment == null || !(attachment instanceof ClippingAttachment)) {
|
||||
continue;
|
||||
@ -432,7 +433,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
const nn = clippingAttachment.worldVerticesLength;
|
||||
const world = new Float32Array(nn);
|
||||
|
||||
clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
clippingAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
debugDisplayObjects.clippingPolygon.drawPolygon(Array.from(world));
|
||||
}
|
||||
}
|
||||
@ -496,7 +497,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment == null || !(attachment instanceof PathAttachment)) {
|
||||
continue;
|
||||
@ -506,7 +507,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
let nn = pathAttachment.worldVerticesLength;
|
||||
const world = new Float32Array(nn);
|
||||
|
||||
pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
pathAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
let x1 = world[2];
|
||||
let y1 = world[3];
|
||||
let x2 = 0;
|
||||
|
||||
@ -118,8 +118,8 @@
|
||||
|
||||
// resetting the slot with the original attachment
|
||||
setTimeout(() => {
|
||||
frontFist.setToSetupPose();
|
||||
frontFist.bone.setToSetupPose();
|
||||
frontFist.setupPose();
|
||||
frontFist.bone.setupPose();
|
||||
}, 10000);
|
||||
|
||||
// showing an animation with clipping -> Pixi masks will be created
|
||||
|
||||
@ -139,7 +139,7 @@ export class SetupPoseBoundsProvider implements SpineBoundsProvider {
|
||||
// the skeleton in the GameObject has already been heavily modified. We can not
|
||||
// reconstruct that state.
|
||||
const skeleton = new Skeleton(gameObject.skeleton.data);
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
|
||||
return bounds.width == Number.NEGATIVE_INFINITY
|
||||
@ -188,7 +188,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
}
|
||||
skeleton.setSkin(customSkin);
|
||||
}
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
const animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||
|
||||
@ -204,7 +204,7 @@ export class SkinsAndAnimationBoundsProvider
|
||||
maxX = Number.NEGATIVE_INFINITY,
|
||||
maxY = Number.NEGATIVE_INFINITY;
|
||||
animationState.clearTracks();
|
||||
animationState.setAnimationWith(0, animation, false);
|
||||
animationState.setAnimation(0, animation, false);
|
||||
const steps = Math.max(animation.duration / this.timeStep, 1.0);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const delta = i > 0 ? this.timeStep : 0;
|
||||
@ -400,7 +400,7 @@ export class Spine extends ViewContainer {
|
||||
|
||||
// dark tint can be enabled by options, otherwise is enable if at least one slot has tint black
|
||||
this.darkTint = options?.darkTint === undefined
|
||||
? this.skeleton.slots.some(slot => !!slot.data.darkColor)
|
||||
? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor)
|
||||
: options?.darkTint;
|
||||
|
||||
const slots = this.skeleton.slots;
|
||||
@ -447,15 +447,16 @@ export class Spine extends ViewContainer {
|
||||
if (!bone) throw Error(`Cant set bone position, bone ${String(boneAux)} not found`);
|
||||
vectorAux.set(position.x, position.y);
|
||||
|
||||
const applied = bone.applied;
|
||||
if (bone.parent) {
|
||||
const aux = bone.parent.worldToLocal(vectorAux);
|
||||
const aux = bone.parent.applied.worldToLocal(vectorAux);
|
||||
|
||||
bone.x = aux.x;
|
||||
bone.y = -aux.y;
|
||||
applied.x = aux.x;
|
||||
applied.y = -aux.y;
|
||||
}
|
||||
else {
|
||||
bone.x = vectorAux.x;
|
||||
bone.y = vectorAux.y;
|
||||
applied.x = vectorAux.x;
|
||||
applied.y = vectorAux.y;
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,8 +483,8 @@ export class Spine extends ViewContainer {
|
||||
outPos = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
outPos.x = bone.worldX;
|
||||
outPos.y = bone.worldY;
|
||||
outPos.x = bone.applied.worldX;
|
||||
outPos.y = bone.applied.worldY;
|
||||
|
||||
return outPos;
|
||||
}
|
||||
@ -541,7 +542,7 @@ export class Spine extends ViewContainer {
|
||||
|
||||
for (let i = 0; i < currentDrawOrder.length; i++) {
|
||||
const slot = currentDrawOrder[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment) {
|
||||
if (attachment !== lastAttachments[index]) {
|
||||
@ -564,7 +565,8 @@ export class Spine extends ViewContainer {
|
||||
private currentClippingSlot: SlotsToClipping | undefined;
|
||||
private updateAndSetPixiMask (slot: Slot, last: boolean) {
|
||||
// assign/create the currentClippingSlot
|
||||
const attachment = slot.attachment;
|
||||
const pose = slot.pose;
|
||||
const attachment = pose.attachment;
|
||||
if (attachment && attachment instanceof ClippingAttachment) {
|
||||
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() });
|
||||
clip.maskComputed = false;
|
||||
@ -577,7 +579,7 @@ export class Spine extends ViewContainer {
|
||||
let slotObject = this._slotsObject[slot.data.name];
|
||||
if (currentClippingSlot && slotObject) {
|
||||
let slotClipping = currentClippingSlot.slot;
|
||||
let clippingAttachment = slotClipping.attachment as ClippingAttachment;
|
||||
let clippingAttachment = slotClipping.pose.attachment as ClippingAttachment;
|
||||
|
||||
// create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot
|
||||
let mask = currentClippingSlot.mask as Graphics;
|
||||
@ -592,7 +594,7 @@ export class Spine extends ViewContainer {
|
||||
currentClippingSlot.maskComputed = true;
|
||||
const worldVerticesLength = clippingAttachment.worldVerticesLength;
|
||||
const vertices = currentClippingSlot.vertices;
|
||||
clippingAttachment.computeWorldVertices(slotClipping, 0, worldVerticesLength, vertices, 0, 2);
|
||||
clippingAttachment.computeWorldVertices(this.skeleton, slotClipping, 0, worldVerticesLength, vertices, 0, 2);
|
||||
mask.clear().poly(vertices).stroke({ width: 0 }).fill({ alpha: .25 });
|
||||
}
|
||||
slotObject.container.mask = mask;
|
||||
@ -602,7 +604,7 @@ export class Spine extends ViewContainer {
|
||||
}
|
||||
|
||||
// if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined
|
||||
if (currentClippingSlot && (currentClippingSlot.slot.attachment as ClippingAttachment).endSlot == slot.data) {
|
||||
if (currentClippingSlot && (currentClippingSlot.slot.pose.attachment as ClippingAttachment).endSlot == slot.data) {
|
||||
this.currentClippingSlot = undefined;
|
||||
}
|
||||
|
||||
@ -610,7 +612,7 @@ export class Spine extends ViewContainer {
|
||||
if (last) {
|
||||
for (const key in this.clippingSlotToPixiMasks) {
|
||||
const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key];
|
||||
if ((!(clippingSlotToPixiMask.slot.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
|
||||
if ((!(clippingSlotToPixiMask.slot.pose.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
|
||||
this.removeChild(clippingSlotToPixiMask.mask);
|
||||
maskPool.free(clippingSlotToPixiMask.mask);
|
||||
clippingSlotToPixiMask.mask = undefined;
|
||||
@ -622,13 +624,15 @@ export class Spine extends ViewContainer {
|
||||
|
||||
private transformAttachments () {
|
||||
const currentDrawOrder = this.skeleton.drawOrder;
|
||||
const skeleton = this.skeleton;
|
||||
|
||||
for (let i = 0; i < currentDrawOrder.length; i++) {
|
||||
const slot = currentDrawOrder[i];
|
||||
|
||||
this.updateAndSetPixiMask(slot, i === currentDrawOrder.length - 1);
|
||||
|
||||
const attachment = slot.getAttachment();
|
||||
const pose = slot.pose;
|
||||
const attachment = pose;
|
||||
|
||||
if (attachment) {
|
||||
if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) {
|
||||
@ -639,6 +643,7 @@ export class Spine extends ViewContainer {
|
||||
}
|
||||
else {
|
||||
attachment.computeWorldVertices(
|
||||
skeleton,
|
||||
slot,
|
||||
0,
|
||||
attachment.worldVerticesLength,
|
||||
@ -656,9 +661,8 @@ export class Spine extends ViewContainer {
|
||||
// need to copy because attachments uvs are shared among skeletons using the same atlas
|
||||
fastCopy((attachment.uvs as Float32Array).buffer, cacheData.uvs.buffer);
|
||||
|
||||
const skeleton = slot.bone.skeleton;
|
||||
const skeletonColor = skeleton.color;
|
||||
const slotColor = slot.color;
|
||||
const slotColor = pose.color;
|
||||
|
||||
const attachmentColor = attachment.color;
|
||||
|
||||
@ -669,8 +673,8 @@ export class Spine extends ViewContainer {
|
||||
skeletonColor.a * slotColor.a * attachmentColor.a,
|
||||
);
|
||||
|
||||
if (slot.darkColor) {
|
||||
cacheData.darkColor.setFromColor(slot.darkColor);
|
||||
if (pose.darkColor) {
|
||||
cacheData.darkColor.setFromColor(pose.darkColor);
|
||||
}
|
||||
|
||||
cacheData.skipRender = cacheData.clipped = false;
|
||||
@ -687,11 +691,11 @@ export class Spine extends ViewContainer {
|
||||
}
|
||||
}
|
||||
else if (attachment instanceof ClippingAttachment) {
|
||||
clipper.clipStart(slot, attachment);
|
||||
clipper.clipStart(skeleton, slot, attachment);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
}
|
||||
clipper.clipEnd();
|
||||
}
|
||||
@ -782,31 +786,33 @@ export class Spine extends ViewContainer {
|
||||
private updateSlotObject (slotAttachment: { slot: Slot, container: Container, followAttachmentTimeline: boolean }) {
|
||||
const { slot, container } = slotAttachment;
|
||||
|
||||
const followAttachmentValue = slotAttachment.followAttachmentTimeline ? Boolean(slot.attachment) : true;
|
||||
const pose = slot.pose;
|
||||
const followAttachmentValue = slotAttachment.followAttachmentTimeline ? Boolean(pose.attachment) : true;
|
||||
container.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue;
|
||||
|
||||
if (container.visible) {
|
||||
let bone: Bone | null = slot.bone;
|
||||
|
||||
container.position.set(bone.worldX, bone.worldY);
|
||||
container.angle = bone.getWorldRotationX();
|
||||
const applied = bone.applied;
|
||||
container.position.set(applied.worldX, applied.worldY);
|
||||
container.angle = applied.getWorldRotationX();
|
||||
|
||||
let cumulativeScaleX = 1;
|
||||
let cumulativeScaleY = 1;
|
||||
while (bone) {
|
||||
cumulativeScaleX *= bone.scaleX;
|
||||
cumulativeScaleY *= bone.scaleY;
|
||||
cumulativeScaleX *= bone.applied.scaleX;
|
||||
cumulativeScaleY *= bone.applied.scaleY;
|
||||
bone = bone.parent;
|
||||
};
|
||||
|
||||
if (cumulativeScaleX < 0) container.angle -= 180;
|
||||
|
||||
container.scale.set(
|
||||
slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX),
|
||||
slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY),
|
||||
applied.getWorldScaleX() * Math.sign(cumulativeScaleX),
|
||||
applied.getWorldScaleY() * Math.sign(cumulativeScaleY),
|
||||
);
|
||||
|
||||
container.alpha = this.skeleton.color.a * slot.color.a;
|
||||
container.alpha = this.skeleton.color.a * pose.color.a;
|
||||
}
|
||||
}
|
||||
|
||||
@ -999,7 +1005,7 @@ export class Spine extends ViewContainer {
|
||||
for (let i = 0; i < drawOrder.length; i++) {
|
||||
const slot = drawOrder[i];
|
||||
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) {
|
||||
const cacheData = this._getCachedData(slot, attachment);
|
||||
@ -1055,10 +1061,10 @@ export class Spine extends ViewContainer {
|
||||
public pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
|
||||
this.pixiWorldCoordinatesToSkeleton(point);
|
||||
if (bone.parent) {
|
||||
bone.parent.worldToLocal(point as Vector2);
|
||||
bone.parent.applied.worldToLocal(point as Vector2);
|
||||
}
|
||||
else {
|
||||
bone.worldToLocal(point as Vector2);
|
||||
bone.applied.worldToLocal(point as Vector2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -259,10 +259,11 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
for (let i = 0, len = bones.length; i < len; i++) {
|
||||
const bone = bones[i];
|
||||
const boneLen = bone.data.length;
|
||||
const starX = skeletonX + bone.worldX;
|
||||
const starY = skeletonY + bone.worldY;
|
||||
const endX = skeletonX + (boneLen * bone.a) + bone.worldX;
|
||||
const endY = skeletonY + (boneLen * bone.b) + bone.worldY;
|
||||
const applied = bone.applied;
|
||||
const starX = skeletonX + applied.worldX;
|
||||
const starY = skeletonY + applied.worldY;
|
||||
const endX = skeletonX + (boneLen * applied.a) + applied.worldX;
|
||||
const endY = skeletonY + (boneLen * applied.b) + applied.worldY;
|
||||
|
||||
if (bone.data.name === 'root' || bone.data.parent === null) {
|
||||
continue;
|
||||
@ -359,7 +360,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
|
||||
for (let i = 0, len = slots.length; i < len; i++) {
|
||||
const slot = slots[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment === null || !(attachment instanceof RegionAttachment)) {
|
||||
continue;
|
||||
@ -390,7 +391,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment === null || !(attachment instanceof MeshAttachment)) {
|
||||
continue;
|
||||
@ -402,7 +403,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
const triangles = meshAttachment.triangles;
|
||||
let hullLength = meshAttachment.hullLength;
|
||||
|
||||
meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2);
|
||||
meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2);
|
||||
// draw the skinned mesh (triangle)
|
||||
if (this.drawMeshTriangles) {
|
||||
for (let i = 0, len = triangles.length; i < len; i += 3) {
|
||||
@ -450,7 +451,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment === null || !(attachment instanceof ClippingAttachment)) {
|
||||
continue;
|
||||
@ -461,7 +462,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
const nn = clippingAttachment.worldVerticesLength;
|
||||
const world = new Float32Array(nn);
|
||||
|
||||
clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
clippingAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
debugDisplayObjects.clippingPolygon.poly(Array.from(world));
|
||||
}
|
||||
|
||||
@ -535,7 +536,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
if (!slot.bone.active) {
|
||||
continue;
|
||||
}
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment === null || !(attachment instanceof PathAttachment)) {
|
||||
continue;
|
||||
@ -545,7 +546,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
|
||||
let nn = pathAttachment.worldVerticesLength;
|
||||
const world = new Float32Array(nn);
|
||||
|
||||
pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
pathAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
let x1 = world[2];
|
||||
let y1 = world[3];
|
||||
let x2 = 0;
|
||||
|
||||
@ -86,7 +86,7 @@ export class SpinePipe implements RenderPipe<Spine> {
|
||||
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
const slot = drawOrder[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) {
|
||||
const cacheData = spine._getCachedData(slot, attachment);
|
||||
@ -122,7 +122,7 @@ export class SpinePipe implements RenderPipe<Spine> {
|
||||
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
const slot = drawOrder[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
const blendMode = spineBlendModeMap[slot.data.blendMode];
|
||||
|
||||
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) {
|
||||
@ -165,7 +165,7 @@ export class SpinePipe implements RenderPipe<Spine> {
|
||||
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
const slot = drawOrder[i];
|
||||
const attachment = slot.getAttachment();
|
||||
const attachment = slot.pose.attachment;
|
||||
|
||||
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) {
|
||||
const cacheData = spine._getCachedData(slot, attachment);
|
||||
|
||||
@ -528,8 +528,8 @@ export class SpinePlayer implements Disposable {
|
||||
if (config.skin) {
|
||||
if (!this.skeleton.data.findSkin(config.skin))
|
||||
this.showError(`Error: Skin does not exist in skeleton: ${config.skin}`);
|
||||
this.skeleton.setSkinByName(config.skin);
|
||||
this.skeleton.setSlotsToSetupPose();
|
||||
this.skeleton.setSkin(config.skin);
|
||||
this.skeleton.setupPoseSlots();
|
||||
}
|
||||
|
||||
// Check if all animations given a viewport exist.
|
||||
@ -609,7 +609,7 @@ export class SpinePlayer implements Disposable {
|
||||
let bone = skeleton.findBone(controlBones[i]);
|
||||
if (!bone) continue;
|
||||
let distance = renderer.camera.worldToScreen(
|
||||
coords.set(bone.worldX, bone.worldY, 0),
|
||||
coords.set(bone.applied.worldX, bone.applied.worldY, 0),
|
||||
canvas.clientWidth, canvas.clientHeight).distance(mouse);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
@ -639,13 +639,14 @@ export class SpinePlayer implements Disposable {
|
||||
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
|
||||
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
|
||||
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
|
||||
const applied = target.applied;
|
||||
if (target.parent) {
|
||||
target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
|
||||
target.x = position.x;
|
||||
target.y = position.y;
|
||||
target.parent.applied.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
|
||||
applied.x = position.x;
|
||||
applied.y = position.y;
|
||||
} else {
|
||||
target.x = coords.x - skeleton.x;
|
||||
target.y = coords.y - skeleton.y;
|
||||
applied.x = coords.x - skeleton.x;
|
||||
applied.y = coords.y - skeleton.y;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -730,13 +731,13 @@ export class SpinePlayer implements Disposable {
|
||||
/* Sets a new animation and viewport on track 0. */
|
||||
setAnimation (animation: string | Animation, loop: boolean = true): TrackEntry {
|
||||
animation = this.setViewport(animation);
|
||||
return this.animationState!.setAnimationWith(0, animation, loop);
|
||||
return this.animationState!.setAnimation(0, animation, loop);
|
||||
}
|
||||
|
||||
/* Adds a new animation and viewport on track 0. */
|
||||
addAnimation (animation: string | Animation, loop: boolean = true, delay: number = 0): TrackEntry {
|
||||
animation = this.setViewport(animation);
|
||||
return this.animationState!.addAnimationWith(0, animation, loop, delay);
|
||||
return this.animationState!.addAnimation(0, animation, loop, delay);
|
||||
}
|
||||
|
||||
/* Sets the viewport for the specified animation. */
|
||||
@ -799,7 +800,7 @@ export class SpinePlayer implements Disposable {
|
||||
}
|
||||
|
||||
private calculateAnimationViewport (animation: Animation, viewport: Viewport) {
|
||||
this.skeleton!.setToSetupPose();
|
||||
this.skeleton!.setupPose();
|
||||
|
||||
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
|
||||
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
|
||||
@ -807,7 +808,7 @@ export class SpinePlayer implements Disposable {
|
||||
|
||||
const tempArray = new Array<number>(2);
|
||||
for (let i = 0; i < steps; i++, time += stepTime) {
|
||||
animation.apply(this.skeleton!, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn);
|
||||
animation.apply(this.skeleton!, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false);
|
||||
this.skeleton!.updateWorldTransform(Physics.update);
|
||||
this.skeleton!.getBounds(offset, size, tempArray, this.sceneRenderer!.skeletonRenderer.getSkeletonClipping());
|
||||
|
||||
@ -943,8 +944,9 @@ export class SpinePlayer implements Disposable {
|
||||
if (!bone) continue;
|
||||
let colorInner = selectedBones[i] ? BONE_INNER_OVER : BONE_INNER;
|
||||
let colorOuter = selectedBones[i] ? BONE_OUTER_OVER : BONE_OUTER;
|
||||
renderer.circle(true, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorInner);
|
||||
renderer.circle(false, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorOuter);
|
||||
const applied = bone.applied;
|
||||
renderer.circle(true, skeleton.x + applied.worldX, skeleton.y + applied.worldY, 20, colorInner);
|
||||
renderer.circle(false, skeleton.x + applied.worldX, skeleton.y + applied.worldY, 20, colorOuter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1055,8 +1057,8 @@ export class SpinePlayer implements Disposable {
|
||||
removeClass(rows.children, "selected");
|
||||
row.classList.add("selected");
|
||||
this.config.skin = skin.name;
|
||||
this.skeleton!.setSkinByName(this.config.skin);
|
||||
this.skeleton!.setSlotsToSetupPose();
|
||||
this.skeleton!.setSkin(this.config.skin);
|
||||
this.skeleton!.setupPose();
|
||||
}
|
||||
});
|
||||
popup.show();
|
||||
|
||||
@ -219,14 +219,13 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
private updateGeometry () {
|
||||
this.clearBatches();
|
||||
|
||||
let tempLight = this.tempLight;
|
||||
let tempDark = this.tempDark;
|
||||
let clipper = this.clipper;
|
||||
|
||||
let vertices: NumberArrayLike = this.vertices;
|
||||
let triangles: Array<number> | null = null;
|
||||
let uvs: NumberArrayLike | null = null;
|
||||
let drawOrder = this.skeleton.drawOrder;
|
||||
const skeleton = this.skeleton;
|
||||
let drawOrder = skeleton.drawOrder;
|
||||
let batch = this.nextBatch();
|
||||
batch.begin();
|
||||
let z = 0;
|
||||
@ -235,10 +234,11 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
let slot = drawOrder[i];
|
||||
if (!slot.bone.active) {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
let attachment = slot.getAttachment();
|
||||
let pose = slot.pose;
|
||||
let attachment = pose.attachment;
|
||||
let attachmentColor: Color | null;
|
||||
let texture: ThreeJsTexture | null;
|
||||
let numFloats = 0;
|
||||
@ -258,6 +258,7 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
vertices = this.vertices = Utils.newFloatArray(numFloats);
|
||||
}
|
||||
attachment.computeWorldVertices(
|
||||
skeleton,
|
||||
slot,
|
||||
0,
|
||||
attachment.worldVerticesLength,
|
||||
@ -269,17 +270,17 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
uvs = attachment.uvs;
|
||||
texture = <ThreeJsTexture>attachment.region!.texture;
|
||||
} else if (attachment instanceof ClippingAttachment) {
|
||||
clipper.clipStart(slot, attachment);
|
||||
clipper.clipEnd(slot);
|
||||
clipper.clipStart(skeleton, slot, attachment);
|
||||
continue;
|
||||
} else {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (texture != null) {
|
||||
let skeleton = slot.bone.skeleton;
|
||||
let skeletonColor = skeleton.color;
|
||||
let slotColor = slot.color;
|
||||
let slotColor = pose.color;
|
||||
let alpha = skeletonColor.a * slotColor.a * attachmentColor.a;
|
||||
let color = this.tempColor;
|
||||
color.set(
|
||||
@ -290,12 +291,12 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
);
|
||||
|
||||
let darkColor = this.tempDarkColor;
|
||||
if (!slot.darkColor)
|
||||
if (!pose.darkColor)
|
||||
darkColor.set(0, 0, 0, 1);
|
||||
else {
|
||||
darkColor.r = slot.darkColor.r * alpha;
|
||||
darkColor.g = slot.darkColor.g * alpha;
|
||||
darkColor.b = slot.darkColor.b * alpha;
|
||||
darkColor.r = pose.darkColor.r * alpha;
|
||||
darkColor.g = pose.darkColor.g * alpha;
|
||||
darkColor.b = pose.darkColor.b * alpha;
|
||||
darkColor.a = 1;
|
||||
}
|
||||
|
||||
@ -304,7 +305,7 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
let finalIndices: NumberArrayLike;
|
||||
let finalIndicesLength: number;
|
||||
|
||||
if (clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length, uvs, color, tempLight, this.twoColorTint, vertexSize)) {
|
||||
if (clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length, uvs, color, darkColor, this.twoColorTint, vertexSize)) {
|
||||
let clippedVertices = clipper.clippedVertices;
|
||||
let clippedTriangles = clipper.clippedTriangles;
|
||||
finalVertices = clippedVertices;
|
||||
@ -344,7 +345,7 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
}
|
||||
|
||||
if (finalVerticesLength == 0 || finalIndicesLength == 0) {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -378,7 +379,7 @@ export class SkeletonMesh extends THREE.Object3D {
|
||||
z += zOffset;
|
||||
}
|
||||
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
}
|
||||
clipper.clipEnd();
|
||||
batch.end();
|
||||
|
||||
@ -642,7 +642,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
|
||||
|
||||
// show skeleton root
|
||||
const root = skeleton.getRootBone()!;
|
||||
renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
|
||||
renderer.circle(true, root.applied.x + worldOffsetX, root.applied.y + worldOffsetY, 10, red);
|
||||
|
||||
// show shifted origin
|
||||
renderer.circle(true, divOriginX, divOriginY, 10, green);
|
||||
@ -665,7 +665,8 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
|
||||
for (const boneFollower of widget.boneFollowerList) {
|
||||
const { slot, bone, element, followVisibility, followRotation, followOpacity, followScale } = boneFollower;
|
||||
const { worldX, worldY } = widget;
|
||||
this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
|
||||
const applied = bone.applied;
|
||||
this.worldToScreen(this.tempFollowBoneVector, applied.worldX + worldX, applied.worldY + worldY);
|
||||
|
||||
if (Number.isNaN(this.tempFollowBoneVector.x)) continue;
|
||||
|
||||
@ -678,16 +679,17 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
|
||||
}
|
||||
|
||||
element.style.transform = `translate(calc(-50% + ${x.toFixed(2)}px),calc(-50% + ${y.toFixed(2)}px))`
|
||||
+ (followRotation ? ` rotate(${-bone.getWorldRotationX()}deg)` : "")
|
||||
+ (followScale ? ` scale(${bone.getWorldScaleX()}, ${bone.getWorldScaleY()})` : "")
|
||||
+ (followRotation ? ` rotate(${-applied.getWorldRotationX()}deg)` : "")
|
||||
+ (followScale ? ` scale(${applied.getWorldScaleX()}, ${applied.getWorldScaleY()})` : "")
|
||||
;
|
||||
|
||||
element.style.display = ""
|
||||
|
||||
if (followVisibility && !slot.attachment) {
|
||||
const pose = slot.pose;
|
||||
if (followVisibility && !pose.attachment) {
|
||||
element.style.opacity = "0";
|
||||
} else if (followOpacity) {
|
||||
element.style.opacity = `${slot.color.a}`;
|
||||
element.style.opacity = `${pose.color.a}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1013,14 +1013,14 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
|
||||
if (skin) {
|
||||
if (skin.length === 1) {
|
||||
skeleton?.setSkinByName(skin[0]);
|
||||
skeleton?.setSkin(skin[0]);
|
||||
} else {
|
||||
const customSkin = new Skin("custom");
|
||||
for (const s of skin) customSkin.addSkin(skeleton?.data.findSkin(s) as Skin);
|
||||
skeleton?.setSkin(customSkin);
|
||||
}
|
||||
|
||||
skeleton?.setSlotsToSetupPose();
|
||||
skeleton?.setupPoseSlots();
|
||||
}
|
||||
|
||||
if (state) {
|
||||
@ -1156,7 +1156,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
private checkSlotInteraction (type: PointerEventTypesInput, originalEvent?: UIEvent) {
|
||||
for (let [slot, interactionState] of this.pointerSlotEventCallbacks) {
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
|
||||
if (!(attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) continue;
|
||||
|
||||
@ -1171,7 +1171,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
|
||||
} else if (attachment instanceof MeshAttachment) {
|
||||
let mesh = <MeshAttachment>attachment;
|
||||
mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, vertices, 0, 2);
|
||||
mesh.computeWorldVertices(this.skeleton!, slot, 0, mesh.worldVerticesLength, vertices, 0, 2);
|
||||
hullLength = mesh.hullLength;
|
||||
}
|
||||
|
||||
@ -1245,7 +1245,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
if (!slot) return;
|
||||
|
||||
if (hideAttachment) {
|
||||
slot.setAttachment(null);
|
||||
slot.pose.setAttachment(null);
|
||||
}
|
||||
|
||||
element.style.position = 'absolute';
|
||||
@ -1271,7 +1271,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
const renderer = this.overlay.renderer;
|
||||
const { skeleton } = this;
|
||||
if (!skeleton) return { x: 0, y: 0, width: 0, height: 0 };
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
let offset = new Vector2(), size = new Vector2();
|
||||
const tempArray = new Array<number>(2);
|
||||
@ -1289,7 +1289,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
|
||||
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
|
||||
for (let i = 0; i < steps; i++, time += stepTime) {
|
||||
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn);
|
||||
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false);
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping());
|
||||
|
||||
@ -1304,7 +1304,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
|
||||
}
|
||||
}
|
||||
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
|
||||
@ -83,4 +83,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>`
|
||||
@ -115,7 +115,7 @@
|
||||
newSkin.addSkin(this.skeletonData.findSkin(skinName));
|
||||
}
|
||||
this.skeleton.setSkin(newSkin);
|
||||
this.skeleton.setToSetupPose();
|
||||
this.skeleton.setupPose();
|
||||
this.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
|
||||
// Calculate the bounds so we can center and zoom
|
||||
@ -153,7 +153,7 @@
|
||||
// Set the skin, then update the skeleton
|
||||
// to the setup pose and calculate the world transforms
|
||||
this.skeleton.setSkin(skin);
|
||||
this.skeleton.setToSetupPose();
|
||||
this.skeleton.setupPose();
|
||||
this.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
|
||||
// Calculate the bounding box enclosing the skeleton.
|
||||
|
||||
@ -223,7 +223,7 @@
|
||||
}
|
||||
|
||||
function calculateSetupPoseBounds(skeleton) {
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
skeleton.updateWorldTransform(spine.Physics.update);
|
||||
let offset = new spine.Vector2();
|
||||
let size = new spine.Vector2();
|
||||
@ -260,7 +260,7 @@
|
||||
let state = skeletons[activeSkeleton][format].state;
|
||||
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
||||
let animationName = $("#animationList option:selected").text();
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.setupPose();
|
||||
state.setAnimation(0, animationName, true);
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
// Center the camera on the skeleton
|
||||
const offset = new spine.Vector2();
|
||||
const size = new spine.Vector2();
|
||||
this.skeleton.setToSetupPose();
|
||||
this.skeleton.setupPose();
|
||||
this.skeleton.update(0);
|
||||
this.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
this.skeleton.getBounds(offset, size);
|
||||
|
||||
@ -76,9 +76,10 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
let bone = bones[i];
|
||||
if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue;
|
||||
if (!bone.parent) continue;
|
||||
let x = bone.data.length * bone.a + bone.worldX;
|
||||
let y = bone.data.length * bone.c + bone.worldY;
|
||||
shapes.rectLine(true, bone.worldX, bone.worldY, x, y, this.boneWidth * this.scale);
|
||||
const boneApplied = bone.applied;
|
||||
let x = bone.data.length * boneApplied.a + boneApplied.worldX;
|
||||
let y = bone.data.length * boneApplied.c + boneApplied.worldY;
|
||||
shapes.rectLine(true, boneApplied.worldX, boneApplied.worldY, x, y, this.boneWidth * this.scale);
|
||||
}
|
||||
if (this.drawSkeletonXY) shapes.x(skeletonX, skeletonY, 4 * this.scale);
|
||||
}
|
||||
@ -89,7 +90,7 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
if (attachment instanceof RegionAttachment) {
|
||||
let vertices = this.vertices;
|
||||
attachment.computeWorldVertices(slot, vertices, 0, 2);
|
||||
@ -106,10 +107,10 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
if (!(attachment instanceof MeshAttachment)) continue;
|
||||
let vertices = this.vertices;
|
||||
attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
|
||||
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
|
||||
let triangles = attachment.triangles;
|
||||
let hullLength = attachment.hullLength;
|
||||
if (this.drawMeshTriangles) {
|
||||
@ -155,11 +156,11 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
if (!(attachment instanceof PathAttachment)) continue;
|
||||
let nn = attachment.worldVerticesLength;
|
||||
let world = this.temp = Utils.setArraySize(this.temp, nn, 0);
|
||||
attachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
let color = this.pathColor;
|
||||
let x1 = world[2], y1 = world[3], x2 = 0, y2 = 0;
|
||||
if (attachment.closed) {
|
||||
@ -193,7 +194,8 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
for (let i = 0, n = bones.length; i < n; i++) {
|
||||
let bone = bones[i];
|
||||
if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue;
|
||||
shapes.circle(true, bone.worldX, bone.worldY, 3 * this.scale, this.boneOriginColor, 8);
|
||||
let boneApplied = bone.applied;
|
||||
shapes.circle(true, boneApplied.worldX, boneApplied.worldY, 3 * this.scale, this.boneOriginColor, 8);
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,11 +205,11 @@ export class SkeletonDebugRenderer implements Disposable {
|
||||
for (let i = 0, n = slots.length; i < n; i++) {
|
||||
let slot = slots[i];
|
||||
if (!slot.bone.active) continue;
|
||||
let attachment = slot.getAttachment();
|
||||
let attachment = slot.pose.attachment;
|
||||
if (!(attachment instanceof ClippingAttachment)) continue;
|
||||
let nn = attachment.worldVerticesLength;
|
||||
let world = this.temp = Utils.setArraySize(this.temp, nn, 0);
|
||||
attachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
|
||||
attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2);
|
||||
for (let i = 0, n = world.length; i < n; i += 2) {
|
||||
let x = world[i];
|
||||
let y = world[i + 1];
|
||||
|
||||
@ -80,7 +80,7 @@ export class SkeletonRenderer {
|
||||
for (let i = 0, n = drawOrder.length; i < n; i++) {
|
||||
let slot = drawOrder[i];
|
||||
if (!slot.bone.active) {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ export class SkeletonRenderer {
|
||||
}
|
||||
|
||||
if (!inRange) {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -97,7 +97,8 @@ export class SkeletonRenderer {
|
||||
inRange = false;
|
||||
}
|
||||
|
||||
let attachment = slot.getAttachment();
|
||||
const pose = slot.pose;
|
||||
const attachment = pose.attachment;
|
||||
let texture: GLTexture;
|
||||
if (attachment instanceof RegionAttachment) {
|
||||
renderable.vertices = this.vertices;
|
||||
@ -116,21 +117,22 @@ export class SkeletonRenderer {
|
||||
if (renderable.numFloats > renderable.vertices.length) {
|
||||
renderable.vertices = this.vertices = Utils.newFloatArray(renderable.numFloats);
|
||||
}
|
||||
attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize);
|
||||
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize);
|
||||
triangles = attachment.triangles;
|
||||
texture = <GLTexture>attachment.region!.texture;
|
||||
uvs = attachment.uvs;
|
||||
attachmentColor = attachment.color;
|
||||
} else if (attachment instanceof ClippingAttachment) {
|
||||
clipper.clipStart(slot, attachment);
|
||||
clipper.clipEnd(slot);
|
||||
clipper.clipStart(skeleton, slot, attachment);
|
||||
continue;
|
||||
} else {
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (texture) {
|
||||
let slotColor = slot.color;
|
||||
let slotColor = pose.color;
|
||||
let finalColor = this.tempColor;
|
||||
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
|
||||
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
|
||||
@ -142,15 +144,15 @@ export class SkeletonRenderer {
|
||||
finalColor.b *= finalColor.a;
|
||||
}
|
||||
let darkColor = this.tempColor2;
|
||||
if (!slot.darkColor)
|
||||
if (!pose.darkColor)
|
||||
darkColor.set(0, 0, 0, 1.0);
|
||||
else {
|
||||
if (premultipliedAlpha) {
|
||||
darkColor.r = slot.darkColor.r * finalColor.a;
|
||||
darkColor.g = slot.darkColor.g * finalColor.a;
|
||||
darkColor.b = slot.darkColor.b * finalColor.a;
|
||||
darkColor.r = pose.darkColor.r * finalColor.a;
|
||||
darkColor.g = pose.darkColor.g * finalColor.a;
|
||||
darkColor.b = pose.darkColor.b * finalColor.a;
|
||||
} else {
|
||||
darkColor.setFromColor(slot.darkColor);
|
||||
darkColor.setFromColor(pose.darkColor);
|
||||
}
|
||||
darkColor.a = premultipliedAlpha ? 1.0 : 0.0;
|
||||
}
|
||||
@ -197,7 +199,7 @@ export class SkeletonRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
clipper.clipEndWithSlot(slot);
|
||||
clipper.clipEnd(slot);
|
||||
}
|
||||
clipper.clipEnd();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user