diff --git a/spine-ts/spine-webgl/example/webcomponent.html b/spine-ts/spine-webgl/example/webcomponent.html
index 35ed464a0..a644769bb 100644
--- a/spine-ts/spine-webgl/example/webcomponent.html
+++ b/spine-ts/spine-webgl/example/webcomponent.html
@@ -1901,6 +1901,62 @@ stretchyman.update = (canvas, delta, skeleton, state) => {
/////////////////////
-->
+
+
+
+
+
+
+ You can customize the bounds, for example to focus on certain details of your animation.
+
+
+ The bounds-x, bounds-y, bounds-width and bounds-height allows to define custom bounds.
+
+
+ In this example we're zooming in into Celeste's face. You probably want to use clip in this case to avoid the skeleton overflow.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts
index 8327b1a5e..e33cff3bc 100644
--- a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts
+++ b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts
@@ -114,6 +114,11 @@ interface WidgetAttributes {
padRight: number
padTop: number
padBottom: number
+ boundsX: number
+ boundsY: number
+ boundsWidth: number
+ boundsHeight: number
+ autoRecalculateBounds: boolean
width: number
height: number
isDraggable: boolean
@@ -280,6 +285,69 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
*/
public padBottom = 0;
+ /**
+ * A rectangle representing the bounds used to fit the skeleton within the element container.
+ * The rectangle coordinates and size are expressed in the Spine world space, not the screen space.
+ * It is automatically calculated using the `skin` and `animation` provided by the user during loading.
+ * If no skin is provided, it is used the default skin.
+ * If no animation is provided, it is used the setup pose.
+ * Bounds are not automatically recalculated.when the animation or skin change.
+ * Invoke {@link recalculateBounds} to recalculate them, or set {@link autoRecalculateBounds} to true.
+ * Use `setBounds` to set you desired bounds. Bounding Box might be useful to determine the bounds to be used.
+ * If the skeleton overflow the element container consider setting {@link clip} to `true`.
+ */
+ public bounds: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
+
+ /**
+ * The x of the bounds in Spine world coordinates
+ * Connected to `bound-x` attribute.
+ */
+ get boundsX(): number {
+ return this.bounds.x;
+ }
+ set boundsX(value: number) {
+ this.bounds.x = value;
+ }
+
+ /**
+ * The y of the bounds in Spine world coordinates
+ * Connected to `bound-y` attribute.
+ */
+ get boundsY(): number {
+ return this.bounds.y;
+ }
+ set boundsY(value: number) {
+ this.bounds.y = value;
+ }
+
+ /**
+ * The width of the bounds in Spine world coordinates
+ * Connected to `bound-width` attribute.
+ */
+ get boundsWidth(): number {
+ return this.bounds.width;
+ }
+ set boundsWidth(value: number) {
+ this.bounds.width = value;
+ }
+
+ /**
+ * The height of the bounds in Spine world coordinates
+ * Connected to `bound-height` attribute.
+ */
+ get boundsHeight(): number {
+ return this.bounds.height;
+ }
+ set boundsHeight(value: number) {
+ this.bounds.height = value;
+ }
+
+ /**
+ * Whether or not the bounds are recalculated when an animation or a skin is changed. `false` by default.
+ * Connected to `auto-recalculate-bounds` attribute.
+ */
+ public autoRecalculateBounds = false;
+
/**
* Specify a fixed width for the widget. If at least one of `width` and `height` is > 0,
* the widget will have an actual size and the element container reference is the widget itself, not the element container parent.
@@ -416,17 +484,6 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
*/
public textureAtlas?: TextureAtlas;
- /**
- * A rectangle representing the bounds used to fit the skeleton within the element container.
- * The rectangle coordinates and size are expressed in the Spine world space, not the screen space.
- * It is automatically calculated using the `skin` and `animation` provided by the user during loading.
- * If no skin is provided, it is used the default skin.
- * If no animation is provided, it is used the setup pose.
- * Once loaded, the bounds are not automatically recalculated, but {@link recalculateBounds} need to be invoked.
- * Use `setBounds` to set you desired bounds. Bounding Box might be useful to determine the bounds to be used.
- */
- public bounds?: Rectangle;
-
/**
* A Promise that resolve to the widget itself once assets loading is terminated.
* Useful to safely access {@link skeleton} and {@link state} after a new widget has been just created.
@@ -531,6 +588,11 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
"pad-right": { propertyName: "padRight", type: "number" },
"pad-top": { propertyName: "padTop", type: "number" },
"pad-bottom": { propertyName: "padBottom", type: "number" },
+ "bounds-x": { propertyName: "boundsX", type: "number" },
+ "bounds-y": { propertyName: "boundsY", type: "number" },
+ "bounds-width": { propertyName: "boundsWidth", type: "number" },
+ "bounds-height": { propertyName: "boundsHeight", type: "number" },
+ "auto-recalculate-bounds": { propertyName: "autoRecalculateBounds", type: "boolean" },
identifier: { propertyName: "identifier", type: "string" },
debug: { propertyName: "debug", type: "boolean" },
"manual-start": { propertyName: "manualStart", type: "boolean" },
@@ -603,7 +665,9 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
}
this.started = true;
- this.loadingPromise = customElements.whenDefined("spine-overlay").then(() => this.loadSkeleton());
+ if (!this.loadingPromise) {
+ this.loadingPromise = customElements.whenDefined("spine-overlay").then(() => this.loadSkeleton());
+ }
this.loadingPromise.then(() => {
this.loading = false;
@@ -642,30 +706,16 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
}
/**
- * Recalculates and sets the bounds of the current animation on track 0.
- * Useful when animations or skins are set programmatically.
- * @returns void
- */
+ * Recalculates and sets the bounds of the current animation on track 0.
+ * Useful when animations or skins are set programmatically.
+ * @returns void
+ */
public recalculateBounds (): void {
const { skeleton, state } = this;
if (!skeleton || !state) return;
const track = state.getCurrent(0);
const animation = track?.animation as (Animation | undefined);
const bounds = this.calculateAnimationViewport(animation);
- this.setBounds(bounds);
- }
-
- /**
- * Set the given bounds on the current skeleton.
- * Useful when you want you skeleton to have a fixed size, or you want to
- * focus a certain detail of the skeleton. If the skeleton overflow the element container
- * consider setting {@link clip} to `true`.
- * @param bounds
- * @returns
- */
- public setBounds (bounds: Rectangle): void {
- const { skeleton } = this;
- if (!skeleton) return;
bounds.x /= skeleton.scaleX;
bounds.y /= skeleton.scaleY;
bounds.width /= skeleton.scaleX;
@@ -677,7 +727,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
private async loadSkeleton () {
this.loading = true;
- const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, skin } = this;
+ const { atlasPath, skeletonPath, scale, skeletonData: skeletonDataInput } = this;
if (!atlasPath || !skeletonPath) {
throw new Error(`Missing atlas path or skeleton path. Assets cannot be loaded: atlas: ${atlasPath}, skeleton: ${skeletonPath}`);
}
@@ -718,18 +768,19 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
// skeleton.scaleX = this.currentScaleDpi;
// skeleton.scaleY = this.currentScaleDpi;
- this.initWidget();
+ // the bounds are calculated the first time, if no custom bound is provided
+ this.initWidget(this.bounds.width === 0 || this.bounds.height === 0);
return this;
}
- private initWidget () {
+ private initWidget (forceRecalculate = false) {
const { skeleton, state, animation, skin } = this;
if (skin) skeleton?.setSkinByName(skin);
if (animation) state?.setAnimation(0, animation, true);
- this.recalculateBounds();
+ if (forceRecalculate || this.autoRecalculateBounds) this.recalculateBounds();
}
private render (): void {
@@ -1151,7 +1202,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
if (skeleton) {
if (mode === "inside") {
- let { x: ax, y: ay, width: aw, height: ah } = bounds!;
+ let { x: ax, y: ay, width: aw, height: ah } = bounds;
// scale ratio
const scaleWidth = divWidthWorld / aw;
@@ -1227,7 +1278,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
// store the draggable surface to make drag logic easier
if (isDraggable) {
- let { x: ax, y: ay, width: aw, height: ah } = bounds!;
+ let { x: ax, y: ay, width: aw, height: ah } = bounds;
this.worldToScreen(tempVector, ax * skeleton.scaleX + worldOffsetX, ay * skeleton.scaleY + worldOffsetY);
widget.dragBoundsRectangle.x = tempVector.x + window.scrollX;
widget.dragBoundsRectangle.y = tempVector.y - this.worldToScreenLength(ah * skeleton.scaleY) + window.scrollY;
@@ -1243,7 +1294,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
// drawing debug stuff
if (debug) {
// if (true) {
- let { x: ax, y: ay, width: aw, height: ah } = bounds!;
+ let { x: ax, y: ay, width: aw, height: ah } = bounds;
// show bounds and its center
renderer.rect(false,