diff --git a/spine-ts/spine-webcomponents/example/tutorial.html b/spine-ts/spine-webcomponents/example/tutorial.html
index bc9287393..6fdbbf9d0 100644
--- a/spine-ts/spine-webcomponents/example/tutorial.html
+++ b/spine-ts/spine-webcomponents/example/tutorial.html
@@ -231,11 +231,11 @@
>
- The origin mode centers the animation's world origin with the center of the HTML element.
-
- You are responsible for scaling the skeleton when using this mode.
-
-
Move the origin by a percentage of the div's width and height using the x-axis and y-axis attributes, respectively.
@@ -375,7 +370,7 @@
atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel"
animation="grow"
- mode="origin"
+ fit="origin"
scale=".5"
y-axis="-.5"
>
@@ -390,7 +385,7 @@
atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel"
animation="grow"
- mode="origin"
+ fit="origin"
scale=".5"
y-axis="-.5"
>
@@ -772,11 +767,11 @@
mixDuration: the mix duration between this animation and the previous one (not used for the first animation on a track)
-
To loop a track once it reaches the end, add the special group [loop, trackNumber, holdDurationLastAnimation], where:
+
To loop a track once it reaches the end, add the special group [loop, trackNumber, repeatDelay], where:
loop: identifies this as a loop instruction
trackNumber: the number of the track to loop
- holdDurationLastAnimation: the number of seconds to wait after the last animation is completed before repeating the loop
+ repeatDelay: the number of seconds to wait after the last animation is completed before repeating the loop
The parameters of the first group on each track are passed to the setAnimation method, while the remaining groups use addAnimation.
@@ -1427,6 +1422,8 @@ function toggleSpinner(element) {
It's very easy to display your different skins and animations. Simply create a table and use the skin and animation attributes.
+
+ skin accepts a comma separated list of skin names. The skins will be combined in a new one, from the first to the last. If multiple skins set the same slot, the latest in the list will be used.
@@ -3279,7 +3276,7 @@ const darkPicker = document.getElementById("dark-picker");
followOpacity: the element opacity is connected to the slot alpha
followScale: the element scale is connected to the slot scale
followRotation: the element rotation is connected to the slot rotation
- followAttachmentAttach: the element is shown/hidden depending if the slot contains an attachment or not
+ followVisibility: the element is shown/hidden depending if the slot contains an attachment or not
hideAttachment: the slot attachment is hidden as if the element replaced the attachment
@@ -3304,10 +3301,10 @@ const darkPicker = document.getElementById("dark-picker");
@@ -3331,10 +3328,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => {
const widget = await spine.getSpineWidget("potty").whenReady;
- widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followAttachmentAttach: false, hideAttachment: true });
+ widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followVisibility: false, hideAttachment: true });
})();`);
@@ -3378,10 +3375,10 @@ const darkPicker = document.getElementById("dark-picker");
@@ -3405,10 +3402,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => {
const widget = await spine.getSpineWidget("potty2").whenReady;
- widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followAttachmentAttach: false, hideAttachment: true });
- widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followAttachmentAttach: false, hideAttachment: true });
+ widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followVisibility: false, hideAttachment: true });
+ widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followVisibility: false, hideAttachment: true });
})();`);
diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts
index 56fe8d5a1..8ef026d50 100644
--- a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts
+++ b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts
@@ -483,7 +483,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
const tempVector = new Vector3();
for (const widget of this.widgets) {
- const { skeleton, pma, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, noSpinner, loading, clip, isDraggable } = widget;
+ const { skeleton, pma, bounds, debug, offsetX, offsetY, dragX, dragY, fit, noSpinner, loading, clip, isDraggable } = widget;
if (widget.isOffScreenAndWasMoved()) continue;
const elementRef = widget.getHostElement();
@@ -497,7 +497,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
divBounds.y -= offsetTopForOverlay;
}
- const { padLeft, padRight, padTop, padBottom } = widget
+ const { padLeft, padRight, padTop, padBottom, xAxis, yAxis } = widget
const paddingShiftHorizontal = (padLeft - padRight) / 2;
const paddingShiftVertical = (padTop - padBottom) / 2;
@@ -525,7 +525,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
}
if (skeleton) {
- if (mode === "inside") {
+ if (fit !== "origin") {
let { x: ax, y: ay, width: aw, height: ah } = bounds;
if (aw <= 0 || ah <= 0) continue;
@@ -591,8 +591,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
}
}
- const worldOffsetX = divOriginX + offsetX + dragX;
- const worldOffsetY = divOriginY + offsetY + dragY;
+ // const worldOffsetX = divOriginX + offsetX + dragX;
+ const worldOffsetX = divOriginX + offsetX * window.devicePixelRatio + dragX;
+ const worldOffsetY = divOriginY + offsetY * window.devicePixelRatio + dragY;
widget.worldX = worldOffsetX;
widget.worldY = worldOffsetY;
@@ -634,12 +635,10 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
// show shifted origin
- const originX = worldOffsetX - dragX - offsetX;
- const originY = worldOffsetY - dragY - offsetY;
- renderer.circle(true, originX, originY, 10, green);
+ renderer.circle(true, divOriginX, divOriginY, 10, green);
// show line from origin to bounds center
- renderer.line(originX, originY, bbCenterX, bbCenterY, green);
+ renderer.line(divOriginX, divOriginY, bbCenterX, bbCenterY, green);
}
if (clip) endScissor();
@@ -654,7 +653,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue;
for (const boneFollower of widget.boneFollowerList) {
- const { slot, bone, element, followAttachmentAttach, followRotation, followOpacity, followScale } = boneFollower;
+ const { slot, bone, element, followVisibility, followRotation, followOpacity, followScale } = boneFollower;
const { worldX, worldY } = widget;
this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
@@ -675,7 +674,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
element.style.display = ""
- if (followAttachmentAttach && !slot.attachment) {
+ if (followVisibility && !slot.attachment) {
element.style.opacity = "0";
} else if (followOpacity) {
element.style.opacity = `${slot.color.a}`;
@@ -951,7 +950,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
private updateWidgetScales () {
for (const widget of this.widgets) {
// inside mode scale automatically to fit the skeleton within its parent
- if (widget.mode !== "origin" && widget.fit !== "none") continue;
+ if (widget.fit !== "origin" && widget.fit !== "none") continue;
const skeleton = widget.skeleton;
if (!skeleton) continue;
diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
index c9644e1ce..34d17dba0 100644
--- a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
+++ b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
@@ -49,6 +49,7 @@ import {
RegionAttachment,
MeshAttachment,
Bone,
+ Skin,
} from "@esotericsoftware/spine-webgl";
import { AttributeTypes, castValue, isBase64, Rectangle } from "./wcUtils.js";
import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
@@ -56,11 +57,10 @@ import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
-export type ModeType = "inside" | "origin";
-export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown";
+export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown" | "origin";
export type AnimationsInfo = Record
}>;
export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number };
@@ -77,9 +77,8 @@ interface WidgetAttributes {
animation?: string
animations?: AnimationsInfo
defaultMix?: number
- skin?: string
+ skin?: string[]
fit: FitType
- mode: ModeType
xAxis: number
yAxis: number
offsetX: number
@@ -221,14 +220,14 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Optional: The name of the skin to be set
* Connected to `skin` attribute.
*/
- public get skin (): string | undefined {
+ public get skin (): string[] | undefined {
return this._skin;
}
- public set skin (value: string | undefined) {
+ public set skin (value: string[] | undefined) {
this._skin = value;
this.initWidget();
}
- private _skin?: string
+ private _skin?: string[]
/**
* Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`.
@@ -240,19 +239,11 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* - `cover`: as small as possible while still covering the entire element container.
* - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container.
* - `none`: display the skeleton without autoscaling it.
+ * - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
* Connected to `fit` attribute.
*/
public fit: FitType = "contain";
- /**
- * Specify the way the skeleton is centered within the element container:
- * - `inside`: the skeleton bounds center is centered with the element container (Default)
- * - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
- * Origin does not allow to specify any {@link fit} type and guarantee the skeleton to not be autoscaled.
- * Connected to `mode` attribute.
- */
- public mode: ModeType = "inside";
-
/**
* The x offset of the skeleton world origin x axis as a percentage of the element container width
* Connected to `x-axis` attribute.
@@ -705,7 +696,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
animations: { propertyName: "animations", type: "animationsInfo", defaultValue: undefined },
"animation-bounds": { propertyName: "animationsBound", type: "array-string", defaultValue: undefined },
"default-mix": { propertyName: "defaultMix", type: "number", defaultValue: 0 },
- skin: { propertyName: "skin", type: "string" },
+ skin: { propertyName: "skin", type: "array-string" },
width: { propertyName: "width", type: "number", defaultValue: -1 },
height: { propertyName: "height", type: "number", defaultValue: -1 },
isdraggable: { propertyName: "isDraggable", type: "boolean" },
@@ -731,7 +722,6 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
clip: { propertyName: "clip", type: "boolean" },
pages: { propertyName: "pages", type: "array-number" },
fit: { propertyName: "fit", type: "fitType", defaultValue: "contain" },
- mode: { propertyName: "mode", type: "modeType", defaultValue: "inside" },
offscreen: { propertyName: "offScreenUpdateBehaviour", type: "offScreenUpdateBehaviourType", defaultValue: "pause" },
}
@@ -1008,18 +998,28 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
// skeleton.scaleX = this.dprScale;
// skeleton.scaleY = this.dprScale;
+ this.loading = false;
+
// the bounds are calculated the first time, if no custom bound is provided
this.initWidget(this.bounds.width <= 0 || this.bounds.height <= 0);
- this.loading = false;
return this;
}
private initWidget (forceRecalculate = false) {
+ if (this.loading) return;
+
const { skeleton, state, animation, animations: animationsInfo, skin, defaultMix } = this;
if (skin) {
- skeleton?.setSkinByName(skin);
+ if (skin.length === 1) {
+ skeleton?.setSkinByName(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();
}
@@ -1027,7 +1027,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
state.data.defaultMix = defaultMix;
if (animationsInfo) {
- for (const [trackIndexString, { cycle, animations, holdDurationLastAnimation }] of Object.entries(animationsInfo)) {
+ for (const [trackIndexString, { cycle, animations, repeatDelay }] of Object.entries(animationsInfo)) {
const cycleFn = () => {
const trackIndex = Number(trackIndexString);
for (const [index, { animationName, delay, loop, mixDuration }] of animations.entries()) {
@@ -1051,8 +1051,8 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
if (cycle && index === animations.length - 1) {
track.listener = {
complete: () => {
- if (holdDurationLastAnimation)
- setTimeout(() => cycleFn(), 1000 * holdDurationLastAnimation);
+ if (repeatDelay)
+ setTimeout(() => cycleFn(), 1000 * repeatDelay);
else
cycleFn();
delete track.listener?.complete;
@@ -1231,10 +1231,10 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Other utilities
*/
- public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followAttachmentAttach: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = [];
- public followSlot (slotName: string | Slot, element: HTMLElement, options: { followAttachmentAttach?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) {
+ public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followVisibility: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = [];
+ public followSlot (slotName: string | Slot, element: HTMLElement, options: { followVisibility?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) {
const {
- followAttachmentAttach = false,
+ followVisibility = false,
followRotation = true,
followOpacity = true,
followScale = true,
@@ -1253,7 +1253,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
element.style.left = '0px';
element.style.display = 'none';
- this.boneFollowerList.push({ slot, bone: slot.bone, element, followAttachmentAttach, followRotation, followOpacity, followScale, hideAttachment });
+ this.boneFollowerList.push({ slot, bone: slot.bone, element, followVisibility, followRotation, followOpacity, followScale, hideAttachment });
this.overlay.addSlotFollowerElement(element);
}
public unfollowSlot (element: HTMLElement): HTMLElement | undefined {
diff --git a/spine-ts/spine-webcomponents/src/wcUtils.ts b/spine-ts/spine-webcomponents/src/wcUtils.ts
index fbf7bbd2d..3e8d0aaa8 100644
--- a/spine-ts/spine-webcomponents/src/wcUtils.ts
+++ b/spine-ts/spine-webcomponents/src/wcUtils.ts
@@ -27,10 +27,10 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import { AnimationsInfo, FitType, ModeType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js";
+import { AnimationsInfo, FitType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js";
const animatonTypeRegExp = /\[([^\]]+)\]/g;
-export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo";
+export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "offScreenUpdateBehaviourType" | "animationsInfo";
export function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
switch (type) {
@@ -48,8 +48,6 @@ export function castValue (type: AttributeTypes, value: string | null, defaultVa
return castObject(value, defaultValue);
case "fitType":
return isFitType(value) ? value : defaultValue;
- case "modeType":
- return isModeType(value) ? value : defaultValue;
case "offScreenUpdateBehaviourType":
return isOffScreenUpdateBehaviourType(value) ? value : defaultValue;
case "animationsInfo":
@@ -104,7 +102,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
if (!matches) return undefined;
return matches.reduce((obj, group) => {
- const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loopOrHoldDurationLastAnimation, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
+ const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loopOrRepeatDelay, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
if (trackIndexStringOrLoopDefinition === "loop") {
if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
@@ -113,12 +111,12 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
animationInfoObject.cycle = true;
- if (loopOrHoldDurationLastAnimation !== undefined) {
- const holdDurationLastAnimation = Number(loopOrHoldDurationLastAnimation);
- if (Number.isNaN(holdDurationLastAnimation)) {
- throw new Error(`If present, duration of last animation of cycle in ${group} must be a positive integer number, instead it is ${loopOrHoldDurationLastAnimation}. Original value: ${value}`);
+ if (loopOrRepeatDelay !== undefined) {
+ const repeatDelay = Number(loopOrRepeatDelay);
+ if (Number.isNaN(repeatDelay)) {
+ throw new Error(`If present, duration of last animation of cycle in ${group} must be a positive integer number, instead it is ${loopOrRepeatDelay}. Original value: ${value}`);
}
- animationInfoObject.holdDurationLastAnimation = holdDurationLastAnimation;
+ animationInfoObject.repeatDelay = repeatDelay;
}
return obj;
@@ -148,7 +146,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
animationInfoObject.animations.push({
animationName: animationNameOrTrackIndexStringCycle,
- loop: loopOrHoldDurationLastAnimation.trim().toLowerCase() === "true",
+ loop: (loopOrRepeatDelay || "").trim().toLowerCase() === "true",
delay,
mixDuration,
});
@@ -164,7 +162,8 @@ function isFitType (value: string | null): value is FitType {
value === "contain" ||
value === "cover" ||
value === "none" ||
- value === "scaleDown"
+ value === "scaleDown" ||
+ value === "origin"
);
}
@@ -176,12 +175,6 @@ function isOffScreenUpdateBehaviourType (value: string | null): value is OffScre
);
}
-function isModeType (value: string | null): value is ModeType {
- return (
- value === "inside" ||
- value === "origin"
- );
-}
const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/;
export function isBase64 (str: string) {
return base64RegExp.test(str);