mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import { AnimationState, Physics, Skeleton, SkeletonClipping, Skin } from "@esotericsoftware/spine-core";
|
|
|
|
interface Rectangle {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
interface GameObject {
|
|
skeleton?: Skeleton,
|
|
state?: AnimationState,
|
|
}
|
|
|
|
export interface SpineBoundsProvider {
|
|
/** Returns the bounding box for the skeleton, in skeleton space. */
|
|
calculateBounds (gameObject: GameObject): Rectangle;
|
|
}
|
|
|
|
export class AABBRectangleBoundsProvider implements SpineBoundsProvider {
|
|
constructor (
|
|
private x: number,
|
|
private y: number,
|
|
private width: number,
|
|
private height: number,
|
|
) { }
|
|
calculateBounds () {
|
|
return { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
}
|
|
}
|
|
|
|
export class SetupPoseBoundsProvider implements SpineBoundsProvider {
|
|
/**
|
|
* @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
|
|
*/
|
|
constructor (private clipping = false) { }
|
|
|
|
calculateBounds (gameObject: GameObject) {
|
|
if (!gameObject.skeleton) return { x: 0, y: 0, width: 0, height: 0 };
|
|
// Make a copy of animation state and skeleton as this might be called while
|
|
// the skeleton in the GameObject has already been heavily modified. We can not
|
|
// reconstruct that state.
|
|
const skeleton = new Skeleton(gameObject.skeleton.data);
|
|
skeleton.setupPose();
|
|
skeleton.updateWorldTransform(Physics.update);
|
|
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
|
|
return bounds.width === Number.NEGATIVE_INFINITY
|
|
? { x: 0, y: 0, width: 0, height: 0 }
|
|
: bounds;
|
|
}
|
|
}
|
|
|
|
export class SkinsAndAnimationBoundsProvider implements SpineBoundsProvider {
|
|
/**
|
|
* @param animation The animation to use for calculating the bounds. If null, the setup pose is used.
|
|
* @param skins The skins to use for calculating the bounds. If empty, the default skin is used.
|
|
* @param timeStep The time step to use for calculating the bounds. A smaller time step means more precision, but slower calculation.
|
|
* @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
|
|
*/
|
|
constructor (
|
|
private animation?: string,
|
|
private skins: string[] = [],
|
|
private timeStep: number = 0.05,
|
|
private clipping = false,
|
|
) { }
|
|
|
|
calculateBounds (gameObject: GameObject): {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
} {
|
|
if (!gameObject.skeleton || !gameObject.state)
|
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
// Make a copy of animation state and skeleton as this might be called while
|
|
// the skeleton in the GameObject has already been heavily modified. We can not
|
|
// reconstruct that state.
|
|
const animationState = new AnimationState(gameObject.state.data);
|
|
const skeleton = new Skeleton(gameObject.skeleton.data);
|
|
const clipper = this.clipping ? new SkeletonClipping() : undefined;
|
|
const data = skeleton.data;
|
|
if (this.skins.length > 0) {
|
|
const customSkin = new Skin("custom-skin");
|
|
for (const skinName of this.skins) {
|
|
const skin = data.findSkin(skinName);
|
|
if (skin == null) continue;
|
|
customSkin.addSkin(skin);
|
|
}
|
|
skeleton.setSkin(customSkin);
|
|
}
|
|
skeleton.setupPose();
|
|
|
|
const animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
|
|
|
if (animation == null) {
|
|
skeleton.updateWorldTransform(Physics.update);
|
|
const bounds = skeleton.getBoundsRect(clipper);
|
|
return bounds.width === Number.NEGATIVE_INFINITY
|
|
? { x: 0, y: 0, width: 0, height: 0 }
|
|
: bounds;
|
|
} else {
|
|
let minX = Number.POSITIVE_INFINITY,
|
|
minY = Number.POSITIVE_INFINITY,
|
|
maxX = Number.NEGATIVE_INFINITY,
|
|
maxY = Number.NEGATIVE_INFINITY;
|
|
animationState.clearTracks();
|
|
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;
|
|
animationState.update(delta);
|
|
animationState.apply(skeleton);
|
|
skeleton.update(delta);
|
|
skeleton.updateWorldTransform(Physics.update);
|
|
|
|
const bounds = skeleton.getBoundsRect(clipper);
|
|
minX = Math.min(minX, bounds.x);
|
|
minY = Math.min(minY, bounds.y);
|
|
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
}
|
|
const bounds = {
|
|
x: minX,
|
|
y: minY,
|
|
width: maxX - minX,
|
|
height: maxY - minY,
|
|
};
|
|
return bounds.width === Number.NEGATIVE_INFINITY
|
|
? { x: 0, y: 0, width: 0, height: 0 }
|
|
: bounds;
|
|
}
|
|
}
|
|
} |