mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 14:24:53 +08:00
Add animations, animations-bound and default-mix attributes.
This commit is contained in:
parent
a636ef0964
commit
a46e76b1d7
@ -633,6 +633,307 @@
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left">
|
||||
<p>To change animation, we could also just change the animation attribute. The widget will reinitiate itself and change animation.</p>
|
||||
<p>In this case you would use <code>auto-recalculate-bounds</code> to ask the widget to always recalculate the bounds, as in the top example.</p>
|
||||
<p>If want to keep the scale consistent, but fit multiple animations in the container, you can use the <code>animation-bounds</code> attribute to define a bounds containing a list of animations, as in the bottom example.</p>
|
||||
|
||||
</div>
|
||||
<div class="split-right" style="display: flex; flex-direction: column;">
|
||||
<spine-widget
|
||||
style="width: 100%; flex: 1; border: 1px solid black; box-sizing: border-box;"
|
||||
identifier="spineboy-change-animation"
|
||||
atlas="assets/spineboy-pma.atlas"
|
||||
skeleton="assets/spineboy-pro.skel"
|
||||
animation="jump"
|
||||
auto-recalculate-bounds
|
||||
></spine-widget>
|
||||
<spine-widget
|
||||
style="width: 100%; flex: 1; border: 1px solid black; box-sizing: border-box;"
|
||||
identifier="spineboy-change-animation2"
|
||||
atlas="assets/spineboy-pma.atlas"
|
||||
skeleton="assets/spineboy-pro.skel"
|
||||
animation="jump"
|
||||
animation-bounds="jump,death"
|
||||
></spine-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
|
||||
{
|
||||
const widget = await spine.getSpineWidget("spineboy-change-animation").loadingPromise;
|
||||
let toogleAnimation = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = toogleAnimation ? "jump" : "death";
|
||||
widget.setAttribute("animation", newAnimation)
|
||||
toogleAnimation = !toogleAnimation;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
{
|
||||
const widget = await spine.getSpineWidget("spineboy-change-animation2").loadingPromise;
|
||||
let toogleAnimation = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = toogleAnimation ? "jump" : "death";
|
||||
widget.setAttribute("animation", newAnimation)
|
||||
toogleAnimation = !toogleAnimation;
|
||||
}, 4000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
<script>escapeHTMLandInject(`
|
||||
// access the spine widget
|
||||
<spine-widget
|
||||
style="width: 100%; height: 150px; border: 1px solid black;"
|
||||
identifier="spineboy-change-animation"
|
||||
atlas="assets/spineboy-pma.atlas"
|
||||
skeleton="assets/spineboy-pro.skel"
|
||||
animation="jump"
|
||||
auto-recalculate-bounds
|
||||
></spine-widget>
|
||||
<spine-widget
|
||||
style="width: 100%; height: 150px; border: 1px solid black;"
|
||||
identifier="spineboy-change-animation2"
|
||||
atlas="assets/spineboy-pma.atlas"
|
||||
skeleton="assets/spineboy-pro.skel"
|
||||
animation="jump"
|
||||
animation-bounds="jump,death"
|
||||
></spine-widget>
|
||||
|
||||
...
|
||||
|
||||
// using js, access the skeleton and the state asynchronously
|
||||
{
|
||||
const widget = document.querySelector('[identifier="spineboy-change-animation"]');
|
||||
let toogleAnimation = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = toogleAnimation ? "jump" : "death";
|
||||
widget.setAttribute("animation", newAnimation)
|
||||
toogleAnimation = !toogleAnimation;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
{
|
||||
const widget = document.querySelector('[identifier="spineboy-change-animation2"]');
|
||||
let toogleAnimation = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = toogleAnimation ? "jump" : "death";
|
||||
widget.setAttribute("animation", newAnimation)
|
||||
toogleAnimation = !toogleAnimation;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
`);</script>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div class="section vertical-split">
|
||||
|
||||
<div class="split" style="flex-direction: column;">
|
||||
<div class="split-nosize full-width" style="width: 80%; padding: 1em;">
|
||||
<p>If you want to display a sequence of animations without using js or on multiple tracks, you can use the <code>animations</code> attribute.</p>
|
||||
|
||||
<p>It accepts a string composed of groups surrounded by square brackets, like this: <code>[...][...][...]</code></p>
|
||||
|
||||
<p>Each square bracket represents an animation to play, with some parameters. It contains a comma separated list with the following:
|
||||
<ol>
|
||||
<li><code>track</code>: the number of the track on which to play the animation</li>
|
||||
<li><code>animation name</code>: the name of the animation to play</li>
|
||||
<li><code>loop</code>: true, if this animation has to loop. False, otherwise</li>
|
||||
<li><code>delay</code>: the seconds to wait after the start of the previous animation, to play the animatino of this group (not available for the first animation on this track)</li>
|
||||
<li><code>mixDuration</code>: the mix duration between this animation and the previous (not available for the first animation on this track)</li>
|
||||
</ol>
|
||||
</p>
|
||||
|
||||
<p>Once you composed your animation, if you that is loops once it reaches the end, you can add the special group <code>[loop, trackNumber]</code>, where:
|
||||
<ul>
|
||||
<li><code>loop</code>: is the "loop" string to identify this special group</li>
|
||||
<li><code>trackNumber</code>: is the number of the track you want to be looped</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The parameters of the first group of each track are passed to the `setAnimation` method, while the remaining groups on the track use `addAnimation`.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can use respectively use `setEmptyAnimation` or `addEmptyAnimation`, by using the string <code>#EMPTY#</code> as animation name. In this case the <code>loop</code> parameter is ignored.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <code>default-mix</code> attribute allow to the the default mix of the <code>AnimationState</code>.
|
||||
</p>
|
||||
|
||||
<p>Have a look at the two examples below.</p>
|
||||
</div>
|
||||
|
||||
<div class="split-nosize full-width" style="width: 80%; min-height: 300px; margin: 1em; padding: 1em;">
|
||||
<spine-widget
|
||||
atlas="assets/spineboy-pma.atlas"
|
||||
skeleton="assets/spineboy-pro.skel"
|
||||
animation-bounds="jump,death"
|
||||
default-mix="0.05"
|
||||
animations="
|
||||
[loop, 0]
|
||||
[0, idle, true]
|
||||
[0, run, false, 2, 0.25]
|
||||
[0, run, false]
|
||||
[0, run, false]
|
||||
[0, run-to-idle, false, 0, 0.15]
|
||||
[0, idle, true]
|
||||
[0, jump, false, 0, 0.15]
|
||||
[0, walk, false, 0, 0.05]
|
||||
[0, death, false, 0, 0.05]
|
||||
"
|
||||
></spine-widget>
|
||||
</div>
|
||||
|
||||
<div class="split-nosize full-width" style="width: 80%; padding: 1em;">
|
||||
<p>Spineboy here uses the following value for <code>animations</code> attribute.</p>
|
||||
<p>
|
||||
<textarea style="font-size: 0.6rem; width: 100%;" rows="10" readonly>
|
||||
[loop, 0]
|
||||
[0, idle, true]
|
||||
[0, run, false, 2, 0.25]
|
||||
[0, run, false]
|
||||
[0, run, false]
|
||||
[0, run-to-idle, false, 0, 0.15]
|
||||
[0, idle, true]
|
||||
[0, jump, false, 0, 0.15]
|
||||
[0, walk, false, 0, 0.05]
|
||||
[0, death, false, 0, 0.05]</textarea>
|
||||
</p>
|
||||
|
||||
We use a single track for this animation. Let's analyze it:
|
||||
<ol>
|
||||
<li><code>[loop, 0]</code>: when the track 0 reaches the end, start from the beginning</li>
|
||||
<li><code>[0, idle, true]</code>: set the idle animation in loop</li>
|
||||
<li><code>[0, run, true, 2, 0.25]</code>: queue a cycle of the run animation, start it after 2 seconds from the beginning of the previous one, set a mix of 0.25 seconds from the previous one.</li>
|
||||
<li><code>[0, run, false]</code>: queue a cycle of run animation</li>
|
||||
<li><code>[0, run, false]</code>: queue a cycle of run animation</li>
|
||||
<li><code>[0, run-to-idle, false, 0, 0.15]</code>: queue a cycle of run-to-idle animation, with no delay, and a mix of 0.15 seconds</li>
|
||||
<li><code>[0, idle, true]</code>: queue the idle animation in loop</li>
|
||||
<li><code>[0, jump, false, 0, 0.15]</code>: queue a cycle of jump animation in loop, with no delay, and a mix of 0.15 seconds</li>
|
||||
<li><code>[0, walk, false, 0, 0.05]</code>: queue a cycle of walk animation in loop, with no delay, and a mix of 0.05 seconds</li>
|
||||
<li><code>[0, death, false, 0, 0.05]</code>: queue a cycle of death animation in loop, with no delay, and a mix of 0.05 seconds</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="split-nosize full-width" style="width: 80%; min-height: 300px; margin: 1em; padding: 1em;">
|
||||
<spine-widget
|
||||
identifier="celeste-animations"
|
||||
atlas="assets/celestial-circus-pma.atlas"
|
||||
skeleton="assets/celestial-circus-pro.skel"
|
||||
animations="
|
||||
[0, wings-and-feet, true]
|
||||
[loop, 1]
|
||||
[1, #EMPTY#, false]
|
||||
[1, eyeblink, false, 2]
|
||||
"
|
||||
></spine-widget>
|
||||
</div>
|
||||
|
||||
<div class="split-nosize full-width" style="width: 80%; padding: 1em;">
|
||||
<p>Celeste here uses the following value for <code>animations</code> attribute.</p>
|
||||
<p>
|
||||
<textarea id="celeste-animations-text-area" style="font-size: 0.6rem; width: 100%;" rows="5">
|
||||
[0, wings-and-feet, true]
|
||||
[loop, 1]
|
||||
[1, #EMPTY#, false]
|
||||
[1, eyeblink, false, 2]</textarea>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It uses two tracks. In track 0 we simply set the wings-and-feet animation. <br>
|
||||
In track 1 we loop over the entire animation, set an empty animation and queue an eyeblink animation with a 2 seconds delay.
|
||||
</p>
|
||||
|
||||
<p>You can modify the textarea above and experiment with the values. For example, change the delay from 2 to 0.5, or add the swing animation to track 0 like this <code>[0, swing, true, 5, .5]</code> with a delay of 5 seconds and a mix of 0.5 seconds. Click the button below and Celeste will start to blink the eyes more frequently.</p>
|
||||
|
||||
<input type="button" value="Update animation" onclick="updateCelesteAnimations(this)">
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function updateCelesteAnimations() {
|
||||
const celesteAnimations = await spine.getSpineWidget("celeste-animations").loadingPromise;
|
||||
var celesteAnimationsTextArea = document.getElementById("celeste-animations-text-area");
|
||||
celesteAnimations.setAttribute("animations", celesteAnimationsTextArea.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
<script>escapeHTMLandInject(`
|
||||
// access the spine widget
|
||||
<spine-widget
|
||||
identifier="raptor"
|
||||
atlas="assets/raptor-pma.atlas"
|
||||
skeleton="assets/raptor-pro.skel"
|
||||
animation="walk"
|
||||
></spine-widget>
|
||||
|
||||
...
|
||||
|
||||
// using js, access the skeleton and the state asynchronously
|
||||
(async () => {
|
||||
const widget = spine.getSpineWidget("raptor");
|
||||
const { state } = await widget.loadingPromise;
|
||||
let isRoaring = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = isRoaring ? "walk" : "roar";
|
||||
state.setAnimation(0, newAnimation, true);
|
||||
widget.recalculateBounds(); // scale the skeleton based on the new animation
|
||||
isRoaring = !isRoaring;
|
||||
}, 4000);
|
||||
})();
|
||||
|
||||
`);</script>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section //
|
||||
|
||||
@ -101,7 +101,64 @@ function isFitType (value: string | null): value is FitType {
|
||||
);
|
||||
}
|
||||
|
||||
export type AttributeTypes = "string" | "number" | "boolean" | "string-number" | "fitType" | "modeType" | "offScreenUpdateBehaviourType";
|
||||
const animatonTypeRegExp = /\[([^\]]+)\]/g;
|
||||
|
||||
export type AnimationsInfo = Record<string, { cycle?: boolean, animations: Array<AnimationsType> }>;
|
||||
export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number };
|
||||
|
||||
function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined {
|
||||
if (value === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const matches = value.match(animatonTypeRegExp);
|
||||
if (!matches) return undefined;
|
||||
|
||||
return matches.reduce((obj, group) => {
|
||||
const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loop, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
|
||||
|
||||
if (trackIndexStringOrLoopDefinition === "loop") {
|
||||
if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
|
||||
throw new Error(`Track index of cycle in ${group} must be a positive integer number, instead it is ${animationNameOrTrackIndexStringCycle}. Original value: ${value}`);
|
||||
}
|
||||
const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
|
||||
animationInfoObject.cycle = true;
|
||||
return obj;
|
||||
}
|
||||
|
||||
const trackIndex = Number(trackIndexStringOrLoopDefinition);
|
||||
if (!Number.isInteger(trackIndex)) {
|
||||
throw new Error(`Track index in ${group} must be a positive integer number, instead it is ${trackIndexStringOrLoopDefinition}. Original value: ${value}`);
|
||||
}
|
||||
|
||||
let delay;
|
||||
if (delayString !== undefined) {
|
||||
delay = parseFloat(delayString);
|
||||
if (isNaN(delay)) {
|
||||
throw new Error(`Delay in ${group} must be a positive number, instead it is ${delayString}. Original value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
let mixDuration;
|
||||
if (mixDurationString !== undefined) {
|
||||
mixDuration = parseFloat(mixDurationString);
|
||||
if (isNaN(mixDuration)) {
|
||||
throw new Error(`mixDuration in ${group} must be a positive number, instead it is ${mixDurationString}. Original value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
|
||||
animationInfoObject.animations.push({
|
||||
animationName: animationNameOrTrackIndexStringCycle,
|
||||
loop: loop.trim().toLowerCase() === "true",
|
||||
delay,
|
||||
mixDuration,
|
||||
});
|
||||
return obj;
|
||||
}, {} as AnimationsInfo);
|
||||
}
|
||||
|
||||
export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo";
|
||||
|
||||
export type CursorEventTypes = "down" | "up" | "enter" | "leave" | "move" | "drag";
|
||||
export type CursorEventTypesInput = Exclude<CursorEventTypes, "enter" | "leave">;
|
||||
@ -113,6 +170,8 @@ interface WidgetAttributes {
|
||||
jsonSkeletonKey?: string
|
||||
scale: number
|
||||
animation?: string
|
||||
animations?: AnimationsInfo
|
||||
defaultMix?: number
|
||||
skin?: string
|
||||
fit: FitType
|
||||
mode: ModeType
|
||||
@ -124,6 +183,7 @@ interface WidgetAttributes {
|
||||
padRight: number
|
||||
padTop: number
|
||||
padBottom: number
|
||||
animationsBound?: string[]
|
||||
boundsX: number
|
||||
boundsY: number
|
||||
boundsWidth: number
|
||||
@ -221,7 +281,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
public scale = 1;
|
||||
|
||||
/**
|
||||
* Optional: The name of the animation to be played
|
||||
* Optional: The name of the animation to be played. When set, the widget is reinitialized.
|
||||
* Connected to `animation` attribute.
|
||||
*/
|
||||
public get animation (): string | undefined {
|
||||
@ -234,6 +294,37 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
}
|
||||
private _animation?: string
|
||||
|
||||
/**
|
||||
* An {@link AnimationsInfo} that describes a sequence of animations on different tracks.
|
||||
* Connected to `animations` attribute, but since attributes are string, there's a different form to pass it.
|
||||
* It is a string composed of groups surrounded by square brackets. Each group has 5 parameters, the firsts 2 mandatory. They corresponds to: track, animation name, loop, delay, mix time.
|
||||
* For the first group on a track {@link AnimationState.setAnimation} is used, while {@link AnimationState.addAnimation} is used for the others.
|
||||
* If you use the special token #EMPTY# as animation name {@link AnimationState.setEmptyAnimation} and {@link AnimationState.addEmptyAnimation} iare used respectively.
|
||||
* Use the special group [loop, trackNumber], to allow the animation of the track on the given trackNumber to restart from the beginning once finished.
|
||||
*/
|
||||
public get animations (): AnimationsInfo | undefined {
|
||||
return this._animations;
|
||||
}
|
||||
public set animations (value: AnimationsInfo | undefined) {
|
||||
if (value === undefined) value = undefined;
|
||||
this._animations = value;
|
||||
this.initWidget();
|
||||
}
|
||||
public _animations?: AnimationsInfo
|
||||
|
||||
/**
|
||||
* Optional: The default mix set to the {@link AnimationStateData.defaultMix}.
|
||||
* Connected to `default-mix` attribute.
|
||||
*/
|
||||
public get defaultMix (): number {
|
||||
return this._defaultMix;
|
||||
}
|
||||
public set defaultMix (value: number | undefined) {
|
||||
if (value === undefined) value = 0;
|
||||
this._defaultMix = value;
|
||||
}
|
||||
public _defaultMix = 0;
|
||||
|
||||
/**
|
||||
* Optional: The name of the skin to be set
|
||||
* Connected to `skin` attribute.
|
||||
@ -377,6 +468,12 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
if (value <= 0) this.initWidget(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: an array of animation names that are used to calculate the bounds of the skeleton.
|
||||
* Connected to `animations-bound` attribute.
|
||||
*/
|
||||
public animationsBound?: string[];
|
||||
|
||||
/**
|
||||
* Whether or not the bounds are recalculated when an animation or a skin is changed. `false` by default.
|
||||
* Connected to `auto-recalculate-bounds` attribute.
|
||||
@ -457,7 +554,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
public cursorBoundsEventCallback = (event: CursorEventTypes) => {}
|
||||
|
||||
/**
|
||||
* This methods allows to associate to a Slot a callback. For these slots, ff the widget is interactive,
|
||||
* This methods allows to associate to a Slot a callback. For these slots, if the widget is interactive,
|
||||
* when the cursor performs actions within the slot's attachment the associated callback is invoked with
|
||||
* a {@link CursorEventTypes} (for example, it enter or leaves the slot's attachment bounds).
|
||||
* This is an experimental property and might be removed in the future.
|
||||
@ -662,6 +759,9 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
"json-skeleton-key": { propertyName: "jsonSkeletonKey", type: "string" },
|
||||
scale: { propertyName: "scale", type: "number" },
|
||||
animation: { propertyName: "animation", type: "string", defaultValue: undefined },
|
||||
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" },
|
||||
width: { propertyName: "width", type: "number", defaultValue: -1 },
|
||||
height: { propertyName: "height", type: "number", defaultValue: -1 },
|
||||
@ -686,7 +786,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
"on-screen-manual-start": { propertyName: "onScreenManualStart", type: "boolean" },
|
||||
spinner: { propertyName: "loadingSpinner", type: "boolean" },
|
||||
clip: { propertyName: "clip", type: "boolean" },
|
||||
pages: { propertyName: "pages", type: "string-number" },
|
||||
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" },
|
||||
@ -825,12 +925,35 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
* Useful when animations or skins are set programmatically.
|
||||
* @returns void
|
||||
*/
|
||||
public recalculateBounds (): void {
|
||||
public recalculateBounds (forcedRecalculate = false): 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);
|
||||
|
||||
let bounds: Rectangle;
|
||||
|
||||
if (this.animationsBound && forcedRecalculate) {
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
|
||||
this.animationsBound.forEach((animationName) => {
|
||||
const animation = this.skeleton?.data.animations.find(({ name }) => animationName === name)
|
||||
const { x, y, width, height } = this.calculateAnimationViewport(animation);
|
||||
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x + width);
|
||||
maxY = Math.max(maxY, y + height);
|
||||
});
|
||||
|
||||
bounds = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
};
|
||||
} else {
|
||||
bounds = this.calculateAnimationViewport(state.getCurrent(0)?.animation as (Animation | undefined));
|
||||
}
|
||||
|
||||
bounds.x /= skeleton.scaleX;
|
||||
bounds.y /= skeleton.scaleY;
|
||||
bounds.width /= skeleton.scaleX;
|
||||
@ -892,19 +1015,56 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
}
|
||||
|
||||
private initWidget (forceRecalculate = false) {
|
||||
const { skeleton, state, animation, skin } = this;
|
||||
const { skeleton, state, animation, animations: animationsInfo, skin, defaultMix } = this;
|
||||
|
||||
if (skin) {
|
||||
skeleton?.setSkinByName(skin);
|
||||
skeleton?.setSlotsToSetupPose();
|
||||
}
|
||||
if (animation) {
|
||||
state?.setAnimation(0, animation, true);
|
||||
} else {
|
||||
state?.setEmptyAnimation(0);
|
||||
|
||||
if (state) {
|
||||
state.data.defaultMix = defaultMix;
|
||||
|
||||
if (animationsInfo) {
|
||||
Object.entries(animationsInfo).forEach(([trackIndexString, { cycle, animations }]) => {
|
||||
|
||||
const cycleFn = () => {
|
||||
const trackIndex = Number(trackIndexString);
|
||||
animations.forEach(({ animationName, delay, loop, mixDuration }, index) => {
|
||||
let track;
|
||||
if (index === 0) {
|
||||
if (animationName === "#EMPTY#") {
|
||||
track = state.setEmptyAnimation(trackIndex, mixDuration);
|
||||
} else {
|
||||
track = state.setAnimation(trackIndex, animationName, loop);
|
||||
}
|
||||
} else {
|
||||
if (animationName === "#EMPTY#") {
|
||||
track = state.addEmptyAnimation(trackIndex, mixDuration, delay);
|
||||
} else {
|
||||
track = state.addAnimation(trackIndex, animationName, loop, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (mixDuration) track.mixDuration = mixDuration;
|
||||
|
||||
if (cycle && index === animations.length - 1) {
|
||||
track.listener = { complete: () => cycleFn() };
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
cycleFn();
|
||||
});
|
||||
} else if (animation) {
|
||||
state.setAnimation(0, animation, true);
|
||||
} else {
|
||||
state.setEmptyAnimation(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceRecalculate || this.autoRecalculateBounds) this.recalculateBounds();
|
||||
if (forceRecalculate || this.autoRecalculateBounds) this.recalculateBounds(forceRecalculate);
|
||||
}
|
||||
|
||||
private render (): void {
|
||||
@ -1145,6 +1305,8 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
|
||||
}
|
||||
}
|
||||
|
||||
skeleton.setToSetupPose();
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
@ -2081,6 +2243,11 @@ function castArrayNumber (value: string | null, defaultValue = undefined) {
|
||||
}, [] as Array<number>);
|
||||
}
|
||||
|
||||
function castArrayString (value: string | null, defaultValue = undefined) {
|
||||
if (value === null) return defaultValue;
|
||||
return value.split(",");
|
||||
}
|
||||
|
||||
function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
|
||||
switch (type) {
|
||||
case "string":
|
||||
@ -2089,14 +2256,18 @@ function castValue (type: AttributeTypes, value: string | null, defaultValue?: a
|
||||
return castNumber(value, defaultValue);
|
||||
case "boolean":
|
||||
return castBoolean(value, defaultValue);
|
||||
case "string-number":
|
||||
case "array-number":
|
||||
return castArrayNumber(value, defaultValue);
|
||||
case "array-string":
|
||||
return castArrayString(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":
|
||||
return castToAnimationsInfo(value) || defaultValue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user