spine-runtimes/spine-ts/spine-webgl/example/webcomponent-tutorial.html
2025-05-09 15:40:38 +02:00

3988 lines
154 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../dist/iife/spine-webgl.js"></script>
<!-- <script src="./spine-webgl.min.js"></script> -->
<title>Webcomponent Tutorial</title>
<style>
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
font-size: 16px;
}
.section {
display: flex;
justify-content: center;
align-items: center;
color: white;
background-color: #3498db;
}
.split {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.full-width {
width: 100%;
}
.split-left, .split-right {
width: 50%;
min-height: 300px;
padding: 1rem;
margin: 1rem;
border: 1px solid salmon;
}
.split-nosize {
border: 1px solid salmon;
}
.split-size {
padding: 1rem;
margin: 1rem;
}
.navigation {
display: flex;
position: fixed;
left: 20px;
bottom: 20px;
transform: translateY(-50%);
}
.nav-btn {
display: block;
margin: 0px 5px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.7);
border: none;
cursor: pointer;
}
.vertical-split {
display: flex;
flex-direction: column;
}
.high-page {
height: 600px;
}
.split-top {
width: 100%;
min-height: 600px;
}
.split-bottom {
width: 100%;
/* height: 600px; */
}
.split-bottom {
background-color: #1e1e1e;
color: #d4d4d4;
overflow: auto;
}
.split-bottom pre {
height: 100%;
margin: 0;
}
.split-bottom code {
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
display: block;
padding: 1rem;
}
.skin-grid {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-evenly;
padding: 20px;
box-sizing: border-box;
}
.skin-grid-element {
border: 1px solid #ccc;
width: 150px;
aspect-ratio: 3 / 3;
}
.overflow-grid-container {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(4, 100px);
gap: 10px;
}
.overflow-grid-item {
background-color: lightblue;;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<script>
function escapeHTMLandInject(text) {
const escaped = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
document.currentScript.parentElement.innerHTML = escaped;
}
</script>
</head>
<body>
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
></spine-widget>
</div>
<div class="split-right">
The <code>&lt;spine-widget&gt;</code> tag allows you to place your Spine animations into a web page.
<br>
<br>
By default, the animation bounds are calculated using the given animation, or the setup pose if no animation is provided.
<br>
The bounds is centered and scaled to fit the parent container.
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
></spine-widget>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="full-width">
<div class="split">
<div class="split-left" style="height: 300px;">
<spine-widget
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
fit="fill"
></spine-widget>
</div>
<div class="split-right">
You can change the fit mode of your Spine animation using the <code>fit</code> attribute.
<br>
<br>
This is <code>fit="fill"</code>. Default fit value is <code>fit="contain"</code>.
</div>
</div>
<div class="split">
<div class="split-left" style="height: 300px;">
<spine-widget
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
scale=".125"
fit="none"
></spine-widget>
</div>
<div class="split-right">
If you want to preserve the original scale, you can use the <code>fit="none"</code>.
In combination with that, you can use the <code>scale</code> attribute to choose you desired scale.
<br>
<br>
Other fit modes are <code>width</code>, <code>width</code>, <code>height</code>, <code>cover</code>,and <code>scaleDown</code>.
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
fit="fill"
></spine-widget>
<spine-widget
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
scale=".125"
fit="none"
></spine-widget>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
height="150"
width="150"
></spine-widget>
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
style="
width: 150px;
height: 150px;
border: 1px solid red;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
"
></spine-widget>
</div>
<div class="split-right">
If you want to manually size the Spine widget, specify the attributes <code>width</code> and <code>height</code> in pixels (without the px unit).
<br>
<br>
If you prefer you can style the component using the <code>style</code> attribute. There you have more styling options.
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<div>
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
height="150"
width="150"
></spine-widget>
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
style="
width: 150px;
height: 150px;
border: 1px solid red;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
"
></spine-widget>
</div>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
Mode <code>origin</code> center the animation world origin with the center of the HTML element.
<br>
You are responsible to scale the skeleton using this mode.
<br>
<br>
Move the origin by a percentage of the div width and height by using <code>x-axis</code> and <code>y-axis</code> respectively.
</div>
<div class="split-right">
<spine-widget
atlas="assets/vine-pma.atlas"
skeleton="assets/vine-pro.skel"
animation="grow"
mode="origin"
scale=".5"
y-axis="-.5"
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/vine-pma.atlas"
skeleton="assets/vine-pro.skel"
animation="grow"
mode="origin"
scale=".5"
y-axis="-.5"
></spine-widget>
`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
atlas="assets/snowglobe-pma.atlas"
skeleton="assets/snowglobe-pro.skel"
animation="shake"
offset-x="100"
offset-y="-100"
></spine-widget>
</div>
<div class="split-right">
Use <code>offset-x</code> and <code>offset-y</code> to move you skeleton left or right by the pixel amount you specify.
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/snowglobe-pma.atlas"
skeleton="assets/snowglobe-pro.skel"
animation="shake"
offset-x="100"
offset-y="-100"
></spine-widget>
`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
atlas="assets/goblins-pma.atlas"
skeleton="assets/goblins-pro.skel"
skin="goblingirl"
animation="walk"
pad-left=".25"
pad-right=".25"
pad-top=".25"
pad-bottom=".25"
></spine-widget>
</div>
<div class="split-right">
You can virtually add a padding to the element container by using <code>pad-left</code>, <code>pad-right</code>, <code>pad-top</code>, <code>pad-bottom</code>.
<br>
<br>
As a value you can use a percentage of the width for left and right, and of the height for top and bottom.
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/goblins-pma.atlas"
skeleton="assets/goblins-pro.skel"
skin="goblingirl"
animation="walk"
pad-left=".25"
pad-right=".25"
pad-top=".25"
pad-bottom=".25"
></spine-widget>`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
You can customize the bounds, for example to focus on certain details of your animation.
<br>
<br>
The <code>bounds-x</code>, <code>bounds-y</code>, <code>bounds-width</code> and <code>bounds-height</code> allows to define custom bounds.
<br>
<br>
In this example we're zooming in into Celeste's face. You probably want to use <code>clip</code> in this case to avoid the skeleton overflow.
</div>
<div class="split-right">
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
bounds-x="-155"
bounds-y="650"
bounds-width="300"
bounds-height="350"
clip
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
bounds-x="-155"
bounds-y="650"
bounds-width="300"
bounds-height="350"
clip
></spine-widget>`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
Give an <code>identifier</code> to your widget to get it by using the <code>spine.getSpineWidget</code> function.
You can easily access the <code>Skeleton</code> and the <code>AnimationState</code> of your character, and use them as if you were using <code>spine-webgl</code>.
<br>
<br>
If you change animation, you can ask the widget to scale the skeleton based on the new animation.
</div>
<div class="split-right">
<spine-widget
identifier="raptor"
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
></spine-widget>
</div>
</div>
<script>
(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>
<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 //
/////////////////////
-->
<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 //
/////////////////////
-->
<div class="section vertical-split">
<div class="split high-page" style="flex-direction: column;">
<div class="split-nosize full-width" style="width: 80%; padding: 1em;">
Moving the div will move the skeleton origin.
<br>
Resizing the div will resize the skeleton in <code>inside</code> mode, but not in <code>origin</code> mode.
<br>
Interact with the example above dragging the div and resizing it
</div>
<div class="split-nosize full-width" style="width: 80%; height: 50%; margin: 1em; padding: 1em;" id="section7-element">
<spine-widget
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
You can view the skeleton world origin (green), the root bone position (red), and the bounds rectangle and center (blue) by setting <code>debug</code> to <code>true</code>.
</div>
<div class="split-right">
<spine-widget
atlas="assets/sack-pma.atlas"
skeleton="assets/sack-pro.skel"
animation="cape-follow-example"
debug
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/sack-pma.atlas"
skeleton="assets/sack-pro.skel"
animation="cape-follow-example"
debug
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
If you want to embed your assets within the page, you can inline them using their base64 version. Use a stringified json object containing as keys the assets name and as values their base64 version.
</div>
<div class="split-right">
<spine-widget
atlas="inline.atlas"
skeleton="inline.skel"
animation="animation"
raw-data='{
"inline.atlas":"aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=",
"inline.skel":"/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==",
"inline.png":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
}'
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/inline.atlas"
skeleton="assets/inline.skel"
animation="animation"
raw-data='{
"assets/inline.atlas":"aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=",
"assets/inline.skel":"/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==",
"assets/inline.png":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
}'
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
identifier="widget-loading"
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
spinner
></spine-widget>
</div>
<div class="split-right">
A loading spinner is shown during assets loading. Click the button below to simulate a 2 seconds loading:
<br>
<br>
<input type="button" value="Simulate reload" onclick="reloadWidget(this)">
<br>
<br>
If you do not want to show the loading spinner, set <code>spinner="false"</code>.
<br>
Click the button below to toggle the spinner.
<br>
<br>
<input type="button" value="Spinner ON" onclick="toggleSpinner(this)">
</div>
</div>
<script>
const widget = spine.getSpineWidget("widget-loading");
async function reloadWidget(element) {
element.disabled = true;
await widget.loadingPromise;
const skeleton = widget.skeleton;
widget.loading = true;
setTimeout(() => {
element.disabled = false;
widget.loading = false;
}, 1000)
}
function toggleSpinner(element) {
widget.loadingSpinner = !widget.loadingSpinner;
element.value = widget.loadingSpinner ? "Spinner ON" : "Spinner OFF";
}
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
spinner
></spine-widget>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
It's super easy to show your different skins and animations. Just make a table and use the <code>skin</code> and <code>animation</code> attributes.
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl"
></spine-widget>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl-spring-dress"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl-blue-cape"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="idle"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="dress-up"
skin="full-skins/girl"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/mix-and-match-pma.atlas"
skeleton="assets/mix-and-match-pro.skel"
animation="walk"
skin="full-skins/girl"
></spine-widget>
</div>
</div>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
If you have many atlas pages, for example one for each skin, and you want to show only some of the skins,
pass to the <code>pages</code> the atlas pages you want to load as a comma concatenated list of indices.
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="nate"
pages="0,1,4,6"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="erikari"
pages="0,1,4,6"
></spine-widget>
</div>
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/wave"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="movement/trot-left"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/idea"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
atlas="assets/chibi-stickers-pma.atlas"
skeleton="assets/chibi-stickers.skel"
animation="emotes/hooray"
skin="mario"
pages="0,1,4,6"
></spine-widget>
</div>
</div>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
Let's do the same thing above, but programmatically!
Create two arrays, one for the skin and the other for the animations, and loop over them.
<br>
<br>
<code>spine.createSpineWidget</code> allows you to create a spine widget.
<br>
<br>
By default, assets are loaded immeaditely. You can postpone that by setting <code>manual-start="false"</code>.
Then add the widget to the dom using the asynchronous method <code>appendTo</code>. It's your responsibility to call <code>start()</code> on the widget.
As usual, just wait on the <code>loadingPromise</code> to act on the <code>skeleton</code> or the <code>state</code>.
</div>
<script>
const element = document.currentScript.parentElement;
const skins = ["soeren", "sinisa", "luke"];
const animations = ["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"];
const pages = [3, 7, 8];
skins.forEach((skin, i) => {
// create the table (one for each skin)
const grid = document.createElement("div");
grid.classList.add("skin-grid");
element.appendChild(grid);
animations.forEach(async (animation, j) => {
// craete the div where to place the widget (one for each animation)
const gridElement = document.createElement("div");
gridElement.classList.add("skin-grid-element");
grid.appendChild(gridElement);
// create the widget
const widgetSection = spine.createSpineWidget({
atlasPath: "assets/chibi-stickers-pma.atlas",
skeletonPath: "assets/chibi-stickers.json",
animation,
skin,
pages,
manualStart: true,
});
// append the widget to the grid element
await widgetSection.appendTo(gridElement);
// manually start the widget
widgetSection.start();
// access the state of the first widget and change the animation
if (i === 0 && j === 0) {
await widgetSection.loadingPromise;
widgetSection.state.setAnimation(0, "emotes/angry", true);
}
})
})
</script>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
const element = document.currentScript.parentElement;
const skins = ["soeren", "sinisa", "luke"];
const animations = ["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"];
const pages = [3, 7, 8];
skins.forEach((skin, i) => {
// create the table (one for each skin)
const grid = document.createElement("div");
grid.classList.add("skin-grid");
element.appendChild(grid);
animations.forEach(async (animation, j) => {
// craete the div where to place the widget (one for each animation)
const gridElement = document.createElement("div");
gridElement.classList.add("skin-grid-element");
grid.appendChild(gridElement);
// create the widget
const widgetSection = spine.createSpineWidget({
atlasPath: "assets/chibi-stickers-pma.atlas",
skeletonPath: "assets/chibi-stickers.json",
animation,
skin,
pages,
manualStart: true,
});
// append the widget to the grid element
gridElement.appendChild(widgetSection);
// manually start the widget
widgetSection.start();
// access the state of the first widget and change the animation
if (i === 0 && j === 0) {
await widgetSection.loadingPromise;
widgetSection.state.setAnimation(0, "emotes/angry", true);
}
})
})
`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
When the widget (or the parent element) enters in the viewport, the callback <code>onScreenFunction</code> is invoked.
<br>
<br>
By default, the callback does two things:
<ul>
<li>set <code>onScreenAtLeastOnce</code> to <code>true</code> when the widget enters the viewport the first time</li>
<li>if <code>manual-start</code> and <code>on-screen-manual-start</code> are set the widget <code>start</code> is invoked
the first time the widget enters the viewport and the assets are loaded only in that moment.</li>
</ul>
<br>
The assets of the coin below are loaded only when the widget enters the viewport.
<br>
<br>
You can overwrite the <code>onScreenFunction</code> behaviour. For example, the raptor below changes animation everytime the widget enters the viewport.
</div>
<div class="skin-grid">
<div class="skin-grid-element">
<spine-widget
atlas="assets/coin-pma.atlas"
skeleton="assets/coin-pro.skel"
animation="animation"
manual-start
on-screen-manual-start
></spine-widget>
</div>
<div class="skin-grid-element">
<spine-widget
identifier="coin"
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
></spine-widget>
<script>
(async () => {
const coinWidget = spine.getSpineWidget("coin");
await coinWidget.loadingPromise;
let raptorWalking = true;
coinWidget.onScreenFunction = widget => {
raptorWalking = !raptorWalking;
widget.state.setAnimation(0, raptorWalking ? "walk" : "jump", true);
}
})();
</script>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
identifier="coin"
atlas="assets/raptor-pma.atlas"
skeleton="assets/raptor-pro.skel"
animation="walk"
></spine-widget>
...
(async () => {
const coinWidget = spine.getSpineWidget("coin");
await coinWidget.loadingPromise;
let raptorWalking = true;
coinWidget.onScreenFunction = widget => {
raptorWalking = !raptorWalking;
widget.state.setAnimation(0, raptorWalking ? "walk" : "jump", true);
}
})();
`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
<spine-widget
identifier="dragon"
atlas="assets/dragon-pma.atlas"
skeleton="assets/dragon-ess.skel"
animation="flying"
pages=""
></spine-widget>
</div>
<div class="split-right">
If you want to load textures programmatically, you can just pass as pages to load an empty value liek this <code>pages=""</code>.
<br>
<br>
In this way the skeleton and the atlas are loaded, but not the textures.
<br>
Then you can loads the textures whenever you want.
<br>
<br>
<input type="button" value="Load page 0" onclick="loadPageDragon(0)">
<br>
<br>
<input type="button" value="Load page 1" onclick="loadPageDragon(1)">
<br>
<br>
<input type="button" value="Load page 2" onclick="loadPageDragon(2)">
<br>
<br>
<input type="button" value="Load page 3" onclick="loadPageDragon(3)">
<br>
<br>
<input type="button" value="Load page 4" onclick="loadPageDragon(4)">
</div>
</div>
<script>
const dragon = spine.getSpineWidget("dragon");
function loadPageDragon(pageIndex) {
if (!dragon.pages) {
dragon.pages = [];
}
if (!dragon.pages.includes(pageIndex)) {
dragon.pages.push(pageIndex);
dragon.loadTexturesInPagesAttribute(dragon.textureAtlas);
}
}
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
identifier="dragon"
atlas="assets/dragon-pma.atlas"
skeleton="assets/dragon-ess.skel"
animation="flying"
pages=""
></spine-widget>
<input type="button" value="Load page 0" onclick="loadPageDragon(0)">
<input type="button" value="Load page 1" onclick="loadPageDragon(1)">
<input type="button" value="Load page 2" onclick="loadPageDragon(2)">
<input type="button" value="Load page 3" onclick="loadPageDragon(3)">
<input type="button" value="Load page 4" onclick="loadPageDragon(4)">
...
const dragon = spine.getSpineWidget("dragon");
function loadPageDragon(pageIndex) {
if (!dragon.pages) {
dragon.pages = [];
}
if (!dragon.pages.includes(pageIndex)) {
dragon.pages.push(pageIndex);
dragon.loadTexturesInPagesAttribute(dragon.textureAtlas);
}
}`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
Widgets are not rendered while they are off screen.
<br>
<br>
The state and skeleton <code>update</code>, and the skeleton <code>apply</code> and the skeleton <code>updateWorldTransform</code> functions are not invoked when the widget is off screen.
<br>
<br>
If you want the update functions to be invoked in any case, set <code>offscreen=update</code>.
<br>
If you want all the functions to be invoked in any case, set <code>offscreen=pose</code>.
<br>
<br>
You can also overwrite the update function. Just assign a function to the <code>update</code> property of the widget.
In that it's your responsibility to skip the update/apply. You can use the <code>onScreen</code> property for convinience.
</div>
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px; min-height: 0;">
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
</div>
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px; min-height: 0;">
<spine-widget
identifier="stretchyman"
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
</div>
<script>
const stretchyman = spine.getSpineWidget("stretchyman");
stretchyman.update = (delta, skeleton, state) => {
// skin logiv if widget is off screen
if (!stretchyman.onScreen) return;
delta = delta * 2;
state.update(delta);
skeleton.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform(spine.Physics.update);
};
</script>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
<spine-widget
identifier="stretchyman"
atlas="assets/stretchyman-pma.atlas"
skeleton="assets/stretchyman-pro.skel"
animation="sneak"
offscreen="pose"
></spine-widget>
...
const stretchyman = spine.getSpineWidget("stretchyman");
stretchyman.update = (canvas, delta, skeleton, state) => {
// skin logiv if widget is off screen
if (!stretchyman.onScreen) return;
delta = delta * 2;
state.update(delta);
skeleton.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform(spine.Physics.update);
};`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
If for some reason your skeleton bounds go outside the div,
you can use the <code>clip</code> property to clip everything is outside the html container.
<br>
<br>
Be aware that this will break batching!
</div>
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px; min-height: 0;">
<spine-widget
identifier="tank2"
atlas="assets/tank-pma.atlas"
skeleton="assets/tank-pro.skel"
animation="drive"
></spine-widget>
</div>
<div class="split-left" style="width: 30%; box-sizing: border-box; height: 150px; min-height: 0;">
<spine-widget
identifier="tank"
atlas="assets/tank-pma.atlas"
skeleton="assets/tank-pro.skel"
animation="drive"
fit="none"
clip="true"
></spine-widget>
</div>
<script>
(async () => {
const tank = spine.getSpineWidget("tank");
const tank2 = spine.getSpineWidget("tank2");
await Promise.all([tank.loadingPromise, tank2.loadingPromise]);
tank.beforeUpdateWorldTransforms = (skeleton, state) => {
if (!tank.onScreen) return;
tank.skeleton.scaleX = tank2.skeleton.scaleX;
tank.skeleton.scaleY = tank2.skeleton.scaleY;
}
})();
</script>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<div class="split-left" style="width: 80%; box-sizing: border-box; height: 150px;">
<spine-widget
identifier="tank2"
atlas="assets/tank-pma.atlas"
skeleton="assets/tank-pro.skel"
animation="drive"
></spine-widget>
</div>
<div class="split-left" style="width: 30%; box-sizing: border-box; height: 150px;">
<spine-widget
identifier="tank"
atlas="assets/tank-pma.atlas"
skeleton="assets/tank-pro.skel"
animation="drive"
fit="none"
clip="true"
></spine-widget>
</div>
...
(async () => {
const tank = spine.getSpineWidget("tank");
const tank2 = spine.getSpineWidget("tank2");
await Promise.all([tank.loadingPromise, tank2.loadingPromise]);
// since we want the tank to overflow the div, we set fit to none
// then we "sync" the tank scale to the one of the tank above
tank.beforeUpdateWorldTransforms = (canvas, delta, skeleton, state) => {
if (!tank.onScreen) return;
tank.skeleton.scaleX = tank2.skeleton.scaleX;
tank.skeleton.scaleY = tank2.skeleton.scaleY;
}
})();`);
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
More examples for <code>clip</code> attribute.
</div>
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-top split" style="align-items: stretch">
<div class="split-left">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="3"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="1.5"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="1"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
scale="0.5"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
identifier="celeste"
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
clip
isdraggable
></spine-widget>
<script>
(async () => {
const celeste = spine.getSpineWidget("celeste");
await celeste.loadingPromise;
celeste.state.setAnimation(0, "swing", true);
})();
</script>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<div class="split-left">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="3"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="1.5"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
scale="1"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
scale="0.5"
fit="none"
clip
></spine-widget>
</div>
<div class="split-right">
<spine-widget
identifier="celeste"
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
clip
isdraggable
></spine-widget>
</div>
...
(async () => {
const celeste = spine.getSpineWidget("celeste");
await celeste.loadingPromise;
celeste.state.setAnimation(0, "swing", true);
})();`);
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-left">
If you use a spine widget in a element that has an ancestor that does not follow the webpage scroll, the effect might not be the desired one. You might encounter these problems:
<br>
<br>
1) For scrollable containers, the widget will be slightly slower to scroll than the html behind. The effect is more evident for lower refresh rate display.<br>
2) For scrollable containers, the widget will overflow the container bounds until the widget html element container is visible <br>
3) For fixed containers, the widget will scroll in a jerky way <br>
<br>
In order to fix this behaviour, it is necessary to insert a dedicated <code>spine-overlay</code> webcomponent as a direct child of the container.
Moreover, it is necessary to perform the following actions: <br>
<br>
1) The scrollable container must have a <code>transform</code> css attribute. If it hasn't this attribute the <code>spine-overlay</code> will add it for you.
If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: <code>transform: translateZ(0);</code>), set the <code>scrollable-tweak-off</code> to the <code>spine-overlay</code>.
<br>
2) The <code>spine-overlay</code> must have the <code>scrollable</code> attribute
<br>
3) The <code>spine-overlay</code> must have an <code>overlay-id</code> attribute. Choose the value you prefer.
<br>
4) Each <code>spine-widget</code> must have an <code>overlay-id</code> attribute. The same as the hosting <code>spine-overlay</code>.
<br>
<br>
Additionally, you can set <code>overflow-top</code>, <code>overflow-bottom</code>, <code>overflow-left</code>, <code>overflow-right</code> attributes to the <code>spine-overlay</code> in order to make the canvas bigger and prevent scrolling artifacts.
<br>
<br>
See the two examples below:
<br>
- Click the following button to open two elements with fixed positioned. The left one does not have a dedicated overlay and will scroll in a jerky way. <button id="popup-overlay-button-open">Open Popup</button>
<br>
- Below there are two scrolling items. The left one does not have a dedicated overlay, it will lag on scroll and the widgets will overflow the container.
<br>
<br>
</div>
<div id="popup-overlay" style="
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center; align-items: center; flex-direction: column;
color: black;
">
<div style="display: flex; justify-content: center; align-items: center; flex-direction: column; gap: 10px; margin-bottom: 10px;">
<div id="popup" style="background-color: white; padding: 20px; text-align: center;">
<spine-widget
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
style="width: 200px; height: 200px; border: 1px solid black;"
></spine-widget>
<br>
This fixed element lags when scrolling.
</div>
<div id="popup" style="background-color: white; padding: 20px; text-align: center;">
<spine-overlay
overlay-id="popup"
scrollable
></spine-overlay>
<spine-widget
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
overlay-id="popup"
style="width: 200px; height: 200px; border: 1px solid black;"
></spine-widget>
<br>
This fixed element does not lag when scrolling.
</div>
</div>
<button id="popup-overlay-button-close">Close Popup</button>
</div>
<script>
// Get the buttons and popup elements
const openPopupButton = document.getElementById('popup-overlay-button-open');
const closePopupButton = document.getElementById('popup-overlay-button-close');
const popupOverlay = document.getElementById('popup-overlay');
// Event listener to open the popup
openPopupButton.addEventListener('click', function() {
popupOverlay.style.display = 'flex';
});
// Event listener to close the popup
closePopupButton.addEventListener('click', function() {
popupOverlay.style.display = 'none';
});
</script>
<div class="split-top split" style="justify-content: space-between">
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px;">
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
</div>
</div>
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px; transform: translateZ(0);">
<spine-overlay
overlay-id="scroll"
scrollable
scrollable-tweak-off
overflow-top=".2"
overflow-bottom=".2"
overflow-left=".2"
overflow-right=".2"
></spine-overlay>
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
// POPUP EXAMPLE
<div id="popup-overlay" style="
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center; align-items: center; flex-direction: column;
color: black;
">
<div style="display: flex; justify-content: center; align-items: center; flex-direction: column; gap: 10px; margin-bottom: 10px;">
<div id="popup" style="background-color: white; padding: 20px; text-align: center;">
<spine-widget
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
style="width: 200px; height: 200px; border: 1px solid black;"
></spine-widget>
<br>
This fixed element lags when scrolling.
</div>
<div id="popup" style="background-color: white; padding: 20px; text-align: center;">
<spine-overlay
overlay-id="popup"
scrollable
scrollable-tweak-off
></spine-overlay>
<spine-widget
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
overlay-id="popup"
style="width: 200px; height: 200px; border: 1px solid black;"
></spine-widget>
<br>
This fixed element does not lag when scrolling.
</div>
</div>
<button id="popup-overlay-button-close">Close Popup</button>
</div>
// SCROLLABLE CONTAINER EXAMPLE
<div class="split-top split" style="justify-content: space-between">
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px;">
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
</div>
</div>
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px;">
<spine-overlay
overlay-id="scroll"
scrollable
scrollable-tweak-off
overflow-top=".2"
overflow-bottom=".2"
overflow-left=".2"
overflow-right=".2"
></spine-overlay>
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
</div>
</div>
</div>
`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split" id="above-popup">
<div class="split-top split">
<div class="split-left">
As a bonus item, you can move you skeleton around just by setting the <code>isdraggable</code> property to <code>true</code>.
</div>
<div class="split-right">
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
isdraggable
></spine-widget>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
isdraggable="true"
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split" >
<div class="split-left">
If you need to determine the cursor position in the overlay world, you might find useful the following properties.
<br>
For <code>spine-widget</code>:
<ul>
<li><code>cursorWorldX</code> and <code>cursorWorldY</code> are the x and y of the cursor relative to the skeleton root (spine world).</li>
<li><code>worldX</code> and <code>worldY</code> are the x and y of the root relative to the canvas/webgl context origin (spine world).</li>
</ul>
For <code>spine-overlay</code>:
<ul>
<li><code>cursorCanvasX</code> and <code>cursorCanvasY</code> are the x and y of the cursor relative to the canvas top-left corner (screen world).</li>
<li><code>cursorWorldX</code> and <code>cursorWorldY</code> are the x and y of the cursor relative to the canvas/webgl context origin (spine world).</li>
</ul>
You can use these property to interact with your widget. See the following examples where the owl eyes will follow the cursor, even if you drag the owls in another position.
Exaggerate the movement by deselection the checkbox below.
<br>
<br>
<label>
<input type="checkbox" id="owl-checkbox" checked> Limit control bone movement
</label>
<br>
<br>
This feature is experimental and might be removed in the future.
</div>
<div id="section-owls"></div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<div id="section-owls"></div>
...
function createCircleOfDivs(numDivs = 8) {
const container = document.createElement('div');
container.style.position = 'relative';
container.style.width = '400px';
container.style.height = '400px';
container.style.backgroundColor = '#f3f4f6';
container.style.borderRadius = '50%';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
const radius = 150;
for (let i = 0; i < numDivs; i++) {
const angle = (i / numDivs) * 2 * Math.PI;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.width = '100px';
div.style.height = '100px';
div.style.backgroundColor = '#3b82f6';
div.style.borderRadius = '8px';
div.style.display = 'flex';
div.style.justifyContent = 'center';
div.style.alignItems = 'center';
div.style.color = 'white';
div.style.fontWeight = 'bold';
div.style.transform = \`translate(\${x}px, \${y}px)\`;
div.innerHTML = \`
<spine-widget
identifier="owl\${i}"
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="owl"
animation="idle"
isdraggable
></spine-widget>
\`;
container.appendChild(div);
customElements.whenDefined('spine-widget').then(async () => {
const widget = spine.getSpineWidget(\`owl\${i}\`);
await widget.loadingPromise;
widget.state.setAnimation(1, "blink", true);
const control = widget.skeleton.findBone("control");
const tempVector = new spine.Vector3();
const mouseX = Smooth(0, 200);
const mouseY = Smooth(0, 200);
widget.afterUpdateWorldTransforms = () => {
updateControl(widget, control, mouseX, mouseY, tempVector);
}
});
}
return container;
}
document.getElementById('section-owls').appendChild(createCircleOfDivs(8));
const checkbox = document.getElementById('owl-checkbox');
let limitOwl = true;
checkbox.addEventListener('change', () => limitOwl = checkbox.checked);
const updateControl = (widget, controlBone, mouseX, mouseY, tempVector) => {
controlBone.parent.worldToLocal(tempVector.set(
widget.cursorWorldX,
widget.cursorWorldY,
));
let x = tempVector.x;
let y = tempVector.y;
if (limitOwl) {
x = x / widget.overlay.canvas.width * 30;
y = y / widget.overlay.canvas.height * 30;
}
controlBone.x = controlBone.data.x + mouseX(x);
controlBone.y = controlBone.data.y + mouseY(y);
}
`
);</script>
</code></pre>
</div>
</div>
<script>
function createCircleOfDivs(numDivs = 8) {
const container = document.createElement('div');
container.style.position = 'relative';
container.style.width = '350px';
container.style.height = '350px';
container.style.backgroundColor = '#f3f4f6';
container.style.borderRadius = '50%';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
const radius = 140;
for (let i = 0; i < numDivs; i++) {
const angle = (i / numDivs) * 2 * Math.PI;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.width = '90px';
div.style.height = '90px';
div.style.backgroundColor = '#3b82f6';
div.style.borderRadius = '8px';
div.style.display = 'flex';
div.style.justifyContent = 'center';
div.style.alignItems = 'center';
div.style.color = 'white';
div.style.fontWeight = 'bold';
div.style.transform = `translate(${x}px, ${y}px)`;
div.innerHTML = `
<spine-widget
identifier="owl${i}"
atlas="../demos/assets/atlas2.atlas"
skeleton="../demos/assets/demos.json"
json-skeleton-key="owl"
animation="idle"
isdraggable
></spine-widget>
`;
container.appendChild(div);
customElements.whenDefined('spine-widget').then(async () => {
const widget = spine.getSpineWidget(`owl${i}`);
await widget.loadingPromise;
widget.state.setAnimation(1, "blink", true);
const control = widget.skeleton.findBone("control");
const tempVector = new spine.Vector3();
const mouseX = Smooth(0, 200);
const mouseY = Smooth(0, 200);
widget.afterUpdateWorldTransforms = () => {
updateControl(widget, control, mouseX, mouseY, tempVector);
}
});
}
return container;
}
document.getElementById('section-owls').appendChild(createCircleOfDivs(8));
const checkbox = document.getElementById('owl-checkbox');
let limitOwl = true;
checkbox.addEventListener('change', () => limitOwl = checkbox.checked);
const updateControl = (widget, controlBone, mouseX, mouseY, tempVector) => {
controlBone.parent.worldToLocal(tempVector.set(
widget.cursorWorldX,
widget.cursorWorldY,
));
let x = tempVector.x;
let y = tempVector.y;
if (limitOwl) {
x = x / widget.overlay.canvas.width * 30;
y = y / widget.overlay.canvas.height * 30;
}
controlBone.x = controlBone.data.x + mouseX(x);
controlBone.y = controlBone.data.y + mouseY(y);
}
</script>
<script>
function Smooth(f,t){var p=performance,b=p.now(),o=f,s=0,m,n,x,d,k=f;return function(v){n=p.now()-b;m=t*t/n/n;d=o-f;x=s*t+d+d;if(n>0)k=n<t?o-x/m/t*n+(x+x-d)/m-s*n-d:o;if(v!=void 0&&v!=o){f=k;b+=n;o=v;s=n>0&&n<t?s+3*x/m/t-(4*x-d-d)/m/n:0}return k}}
</script>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split" id="above-popup">
<div class="split-left" style="width: 80%; box-sizing: border-box;">
You can attach callback to your widget to react at pointer interactions. Just make it <code>isinteractive</code>.
<br>
<br>
You can attach a callback for interactions with the widget <code>bounds</code> or with <code>slots</code>.
The available events are <code>down</code>, <code>up</code>, <code>enter</code>, <code>leave</code>, <code>move</code>, and <code>drag</code>.
<br>
<br>
In the following example, if the cursor enters the bounds, the jump animation is set, while the wave animation is set when the cursor leaves.
<br>
If you click on the <code>head-base</code> slot (the face), you can change the normal and dark tint with the colors selected in the two following selectors.
<div style="display: flex; align-items: center; justify-content: space-around">
<div>
<p>
Tint normal: <input type="color" id="color-picker" value="#ffffff" />
</p>
<p>
Tint black: <input type="color" id="dark-picker" value="#000000" />
</p>
</div>
<spine-widget
identifier="interactive0"
atlas="assets/chibi-stickers-pma.atlas",
skeleton="assets/chibi-stickers.json",
skin="mario"
animation="emotes/wave"
isinteractive
style="width: 150px; height: 150px;"
></spine-widget>
<spine-widget
identifier="interactive1"
atlas="assets/chibi-stickers-pma.atlas",
skeleton="assets/chibi-stickers.json",
skin="nate"
animation="emotes/wave"
isinteractive
style="width: 150px; height: 150px;"
></spine-widget>
</div>
</div>
<script>
const colorPicker = document.getElementById("color-picker");
const darkPicker = document.getElementById("dark-picker");
[0, 1].forEach(async (i) => {
const widget = await spine.getSpineWidget(`interactive${i}`).loadingPromise;
widget.cursorBoundsEventCallback = (event) => {
if (event === "enter") widget.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
if (event === "leave") widget.state.setAnimation(0, "emotes/wave", true).mixDuration = .25;
}
const tempColor = new spine.Color();
const slot = widget.skeleton.findSlot("head-base");
slot.darkColor = new spine.Color(0, 0, 0, 1);
widget.addCursorSlotEventCallbacks(slot, (slot, event) => {
if (event === "down") {
slot.darkColor.setFromColor(spine.Color.fromString(darkPicker.value, tempColor));
slot.color.setFromColor(spine.Color.fromString(colorPicker.value, tempColor));
}
});
})
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
identifier="interactive0"
atlas="assets/chibi-stickers-pma.atlas",
skeleton="assets/chibi-stickers.json",
skin="mario"
animation="emotes/wave"
isinteractive
style="width: 150px; height: 150px;"
></spine-widget>
<spine-widget
identifier="interactive1"
atlas="assets/chibi-stickers-pma.atlas",
skeleton="assets/chibi-stickers.json",
skin="nate"
animation="emotes/wave"
isinteractive
style="width: 150px; height: 150px;"
></spine-widget>
[0, 1].forEach(async (i) => {
const widget = await spine.getSpineWidget(\`interactive\${i}\`).loadingPromise;
widget.cursorBoundsEventCallback = (event) => {
if (event === "enter") widget.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
if (event === "leave") widget.state.setAnimation(0, "emotes/wave", true).mixDuration = .25;
}
const tempColor = new spine.Color();
const slot = widget.skeleton.findSlot("head-base");
slot.darkColor = new spine.Color(0, 0, 0, 1);
widget.addCursorSlotEventCallbacks(slot, (slot, event) => {
if (event === "down") {
slot.darkColor.setFromColor(spine.Color.fromString(darkPicker.value, tempColor));
slot.color.setFromColor(spine.Color.fromString(colorPicker.value, tempColor));
}
});
})`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
You can make your <code>HTMLElement</code>s follow slots. This feature is convenient when you need to generate dynamic text or content that integrates with your animation.
<p>
Invoke the `followSlot` function that takes as input:
<ol>
<li>The <code>Slot</code> or the slot name to follow</li>
<li>The <code>HTMLElement</code> that follows the slot</li>
<li>
An object with the following options:
<ul>
<li><code>followOpacity</code>: the element opacity is connected to the slot alpha</li>
<li><code>followScale</code>: the element scale is connected to the slot scale</li>
<li><code>followRotation</code>: the element rotation is connected to the slot rotation</li>
<li><code>followAttachmentAttach</code>: the element is shown/hidden depending if the slots contains an attachment or not</li>
<li><code>hideAttachment</code>: the slot attachment is hidden as if the element replaced the attachment</li>
</ul>
</li>
</ol>
</p>
</div>
<div class="split-right">
<spine-widget
identifier="potty"
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-widget>
</div>
</div>
<div id="rain/rain-color" style="font-size: 50px; display: none;">A</div>
<div id="rain/rain-white" style="font-size: 50px; display: none;">B</div>
<div id="rain/rain-blue" style="font-size: 50px; display: none;">C</div>
<div id="rain/rain-green" style="font-size: 50px; display: none;">D</div>
<script>
(async () => {
const widget = await spine.getSpineWidget("potty").loadingPromise;
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 });
})();
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
identifier="potty"
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-widget>
<div id="rain/rain-color" style="font-size: 50px; display: none;">A</div>
<div id="rain/rain-white" style="font-size: 50px; display: none;">B</div>
<div id="rain/rain-blue" style="font-size: 50px; display: none;">C</div>
<div id="rain/rain-green" style="font-size: 50px; display: none;">D</div>
...
(async () => {
const widget = await spine.getSpineWidget("potty").loadingPromise;
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 });
})();`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
///////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left">
`followSlot` works even with other spine widgets! It works even if you drag it :D
</div>
<div class="split-right">
<spine-widget
identifier="potty2"
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="rain"
isdraggable
></spine-widget>
</div>
</div>
<spine-widget identifier="potty2-1" atlas="assets/raptor-pma.atlas" skeleton="assets/raptor-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-2" atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-3" atlas="assets/celestial-circus-pma.atlas" skeleton="assets/celestial-circus-pro.skel" animation="wings-and-feet" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-4" atlas="assets/goblins-pma.atlas" skeleton="assets/goblins-pro.skel" skin="goblingirl" animation="walk" style="height:200px; width: 200px;"></spine-widget>
<script>
(async () => {
const widget = await spine.getSpineWidget("potty2").loadingPromise;
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 });
})();
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
identifier="potty2"
atlas="assets/cloud-pot-pma.atlas"
skeleton="assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-widget>
<spine-widget identifier="potty2-1" atlas="assets/raptor-pma.atlas" skeleton="assets/raptor-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-2" atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-3" atlas="assets/celestial-circus-pma.atlas" skeleton="assets/celestial-circus-pro.skel" animation="wings-and-feet" style="height:200px; width: 200px;"></spine-widget>
<spine-widget identifier="potty2-4" atlas="assets/goblins-pma.atlas" skeleton="assets/goblins-pro.skel" skin="goblingirl" animation="walk" style="height:200px; width: 200px;"></spine-widget>
...
(async () => {
const widget = await spine.getSpineWidget("potty2").loadingPromise;
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 });
})();`);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
///////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-left" style="min-height: 0;">
<p>You could use some Spine widgets to show your team.</p>
<p>Hover to the widgets to make them jump, or click on them to change randomly the default animation.</p>
</div>
</div>
<div style=" background-color: #fff;">
<h2 style="text-align: center; padding: 2rem; color: #333;">Meet our team!</h2>
<div id="team-container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; width: 100%;">
</div>
</div>
<script>
const container = document.getElementById("team-container");
const members = [
{ skin: "mario", name: "Mario", role: "Developer" },
{ skin: "misaki", name: "Misaki", role: "Artist" },
{ skin: "erikari", name: "Erika", role: "Artist" },
{ skin: "nate", name: "Nate", role: "Developer" },
{ skin: "luke", name: "Luke", role: "Support" },
{ skin: "sinisa", name: "Sinisa", role: "Artist" },
{ skin: "harri", name: "Harri", role: "Developer" },
{ skin: "soeren", name: "Sören", role: "Artist" },
{ skin: "spineboy", name: "Spineboy", role: "Mascotte" },
];
const style = document.createElement("style");
style.innerHTML = `
.team-member:hover {
transform: scale(1.25);
transition: transform 0.3s ease;
}
`;
document.head.appendChild(style);
members.forEach(async (member, i) => {
const div1 = document.createElement("div");
container.appendChild(div1);
div1.style.display = "flex";
div1.style.flexDirection = "column";
div1.style.alignItems = "center";
div1.style.textAlign = "center";
div1.style.backgroundColor = "#f9f9f9";
div1.style.padding = "1rem";
div1.style.borderRadius = "8px";
div1.style.boxShadow = "0 4px 6px rgba(0,0,0,0.1)";
div1.style.transition = "transform 0.3s ease";
div1.classList.add("team-member");
const div2 = document.createElement("div");
div1.appendChild(div2);
div2.style.width = "150px";
div2.style.height = "150px";
div2.style.border = "4px solid #333";
const widget = spine.createSpineWidget({
identifier: member.skin,
atlasPath: "assets/pwd/chibi-stickers-pro-pwd-test.atlas",
skeletonPath: "assets/pwd/chibi-stickers.json",
skin: member.skin,
animation: "emotes/wave",
padTop: "0.05",
padBottom: "0.05",
isInteractive: true,
});
div2.appendChild(widget);
const div3 = document.createElement("div");
div1.appendChild(div3);
div3.style.marginTop = "15px";
div3.style.width = "100%";
const div4 = document.createElement("div");
div3.appendChild(div4);
div4.innerHTML = member.name;
div4.style.fontWeight = "bold";
div4.style.fontSize = "1.1rem";
div4.style.marginBottom = "5px";
div4.style.color = "#333";
const div5 = document.createElement("div");
div3.appendChild(div5);
div5.classList.add("team-member-role");
div5.innerHTML = member.role;
div5.style.fontSize = "0.9rem";
div5.style.color = "#666";
await widget.loadingPromise;
const emotes = widget.skeleton.data.animations.reduce((acc, { name }) => name.startsWith("emotes") ? [...acc, name] : acc, []);
let leaveAnimation = "emotes/wave";
widget.cursorBoundsEventCallback = (event) => {
if (event === "enter") widget.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
else if (event === "leave") widget.state.setAnimation(0, leaveAnimation, true).mixDuration = .25;
else if (event === "down") {
leaveAnimation = emotes[Math.floor(Math.random() * emotes.length)];
widget.state.setAnimation(0, leaveAnimation, true).mixDuration = .25;
}
}
})
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
TODO`
);</script>
</code></pre>
</div>
<!--
/////////////////////
// end section //
///////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-left">
A login UI made using the chibi stickers and a button made using Spine.
<p>
The chibi sticker does the following:
<ul>
<li>It looks at the cursor when no input field is selected</li>
<li>Look at the caret when username input field is selected</li>
<li>Cover its eyes when password input field is selected</li>
<li>React in two different ways depending on the password</li>
</ul>
</p>
<p>
The button does the following:
<ul>
<li>Starts some animation when cursor enters, leaves, stays, or click the button</li>
<li>Appends a div containing the <code>LOGIN</code> text to a slot</li>
<li>Submits the form on click</li>
</ul>
</p>
</div>
<span id="ruler" style="visibility: hidden; white-space: nowrap; position: absolute"></span>
<div style="background-color: white; width: 250px; padding: 30px; text-align: center;">
<div style="display: flex; justify-content: center;">
<div style="width: 150px; height:150px; border-radius: 5%; border: 1px solid rgb(113, 113, 113); background-color: rgb(211, 211, 211); margin-bottom: 30px;">
<spine-widget
identifier="spineboy-login"
atlas="assets/pwd/chibi-stickers-pro-pwd-test.atlas"
skeleton="assets/pwd/chibi-stickers.json"
skin="spineboy"
bounds-x="-177"
bounds-y="238"
bounds-width="364"
bounds-height="412"
animation="interactive/head/idle"
clip
isinteractive
></spine-widget>
</div>
</div>
<div class="input-group" style="margin-bottom: 15px;"><select id="skinSelect"></select></div>
<form id="loginForm">
<div class="input-group" style="margin-bottom: 15px;">
<input style="width: 100%; padding: 10px; box-sizing: border-box;" type="text" id="username" name="username" placeholder="Username" autocomplete="off" required>
</div>
<div class="input-group" style="margin-bottom: 15px;">
<input style="width: 100%; padding: 10px; box-sizing: border-box;" type="password" id="password" name="password" placeholder="Password" required>
</div>
<div style="margin-bottom: 10px; color: black">Password is <code>password</code></div>
<div style="height: 75px; cursor: pointer;">
<div id="button-text" style="font-size: xx-large; cursor: pointer; user-select: none; display: none;"> LOGIN </div>
<spine-widget
identifier="button-login"
atlas="assets/pwd/button.atlas"
skeleton="assets/pwd/button.json"
animation="idle"
isinteractive
fit="fill"
></spine-widget>
</div>
</form>
</div>
<script>
const mouseX = Smooth(0, 200);
const mouseY = Smooth(0, 200);
(async () => {
const form = document.getElementById('loginForm');
const widgetButton = spine.getSpineWidget("button-login");
await widgetButton.loadingPromise;
widgetButton.skeleton.color.set(.85, .85, .85, 1);
widgetButton.cursorBoundsEventCallback = (event, originalEvent) => {
if (event === "enter") {
widgetButton.state.setAnimation(0, "enhance-in", false);
widgetButton.state.setAnimation(1, "shadow-in", false);
widgetButton.state.setAnimation(2, "jump", true);
};
if (event === "leave") {
widgetButton.state.setAnimation(0, "enhance-out", false).timeScale = 2;
widgetButton.state.setAnimation(1, "shadow-out", false);
widgetButton.state.setEmptyAnimation(2, 0).mixDuration = .25;
};
if (event === "down") {
widgetButton.state.setAnimation(0, "enhance-in", false).timeScale = 2;
originalEvent.preventDefault();
if (form.reportValidity()) {
if (passwordInput.value === "password") {
widget.state.setAnimation(0, "interactive/pwd/hooray", 0);
widget.state.addAnimation(0, "interactive/head/idle", true);
} else {
widget.state.setAnimation(0, "interactive/pwd/sad", 0);
widget.state.addAnimation(0, "interactive/head/idle", true);
}
}
};
}
const textSlot = widgetButton.skeleton.findSlot("CLICK ME");
textSlot.setAttachment(null);
const divText = document.getElementById("button-text");
widgetButton.followSlot("CLICK ME", divText, { followScale: false });
const widget = spine.getSpineWidget("spineboy-login");
await widget.loadingPromise;
// default settings
widget.state.data.defaultMix = 0.15;
// Skin selection
const skinSelect = document.getElementById('skinSelect');
widget.skeleton.data.skins.forEach(({ name }) => {
if (name === "default") return;
skinSelect.add(new Option(name, name, name === "spineboy"));
});
skinSelect.value = "spineboy";
skinSelect.addEventListener('change', (e) => widget.skin = e.target.value);
// click on spineboy
widget.cursorBoundsEventCallback = (event) => {
if (event === "down") {
widget.state.setAnimation(0, "interactive/pwd/touch", false);
widget.state.addAnimation(0, "interactive/head/idle", true);
}
if (event === "enter") widget.state.setAnimation(0, "emotes/wave", true);
if (event === "leave") widget.state.setAnimation(0, "interactive/head/idle", false);
}
// Head follow logic
let focusInput = false;
// Password input field
const passwordInput = document.getElementById('password');
passwordInput.addEventListener('focus', () => {
focusInput = true;
resetBlendTracks();
widget.state.setAnimation(0, "interactive/pwd/hide", 0);
});
passwordInput.addEventListener('blur', () => {
focusInput = false;
widget.state.setAnimation(0, "interactive/head/idle", 0);
});
// Username input field
const usernameInput = document.getElementById('username');
usernameInput.addEventListener('input', () => {
setBlendTracks();
});
usernameInput.addEventListener('focus', () => {
focusInput = true;
setBlendTracks();
});
usernameInput.addEventListener('blur', () => {
focusInput = false;
setBlendTracks();
});
// Animation left/down blending
const ALPHA_LEFT_RANGE = 4;
const ALPHA_DOWN = .8;
const ALPHA_DOWN_RANGE = 2;
const moveVector = [0, 0];
const leftTrack = widget.state.setAnimation(1, "interactive/head/left", true);
const downTrack = widget.state.setAnimation(2, "interactive/head/down", true);
leftTrack.mixBlend = spine.MixBlend.add;
downTrack.mixBlend = spine.MixBlend.add;
const setBlendTracks = () => {
moveVector[0] = getAlphaLeft();
moveVector[1] = ALPHA_DOWN;
}
const resetBlendTracks = () => {
moveVector[0] = 0;
moveVector[1] = 0;
}
resetBlendTracks();
const getWidthOfText = (txt) => {
const ruler = document.getElementById("ruler");
ruler.innerHTML = txt;
return ruler.offsetWidth;
}
const getAlphaLeft = () => {
const { width } = usernameInput.getBoundingClientRect();
const length = getWidthOfText(usernameInput.value);
const alpha = Math.min(1, length / width) * 2
return 1 - alpha;
};
widget.afterUpdateWorldTransforms = () => {
if (focusInput) {
blendTo(moveVector[0], moveVector[1]);
} else {
const { x, y, width, height} = widget.getHTMLElementReference().getBoundingClientRect();
const l = -((widget.overlay.cursorCanvasX - (x + width)) / window.innerWidth) * ALPHA_LEFT_RANGE;
const d = ((widget.overlay.cursorCanvasY - (y + height)) / window.innerHeight) * ALPHA_DOWN_RANGE;
blendTo(l, d);
}
};
const blendTo = (x, y) => {
leftTrack.alpha = mouseX(x);
downTrack.alpha = mouseY(y);
}
})();
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
TODO`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split">
<div class="split-left" style="padding: 0;">
<div id="container-game" style="display: flex; flex-direction: column; height: 400px; position: relative;">
<div
id="win-panel"
style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; display: none; align-items: center; justify-content: center; background-color: #000000aa; z-index: 1; color: white; font-size: xx-large;">
YOU LOSE!
</div>
<div class="bottom" style="flex: 80%; background-color: rgb(24, 149, 89); display: flex; align-items: center; justify-content: center;">
<spine-widget
identifier="windmill-game"
atlas="assets/windmill-ess.atlas"
skeleton="assets/windmill-ess.json"
animation="animation"
isinteractive
></spine-widget>
<spine-widget
identifier="spineboy-game"
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.json"
animation="hoverboard"
fit="none"
></spine-widget>
</div>
</div>
</div>
<div class="split-right">
<div class="top" style="flex: 20%; flex-direction: column; gap: 10px; display: flex; align-items: center; justify-content: center; align-items: stretch; height: 100%;">
<div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
Use WASD to move around!
</div>
<div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
<div id="killed" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
<div style="flex: 70%;"">Save the flowers from the white pest by shooting them</div>
</div>
<div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
<div id="ammo" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
<div>Go to the red colored rooster of bush when ammo is low</div>
</div>
<div style="display: flex; flex-direction: column; border: 3px solid black; border-radius: 10px; box-shadow: 5px 5px 15px grey; padding: 10px; ">
<div id="level" style="flex: 30%; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid white; text-align: center;"></div>
<div>Reach level 10 to win the game</div>
</div>
</div>
</div>
</div>
<script>
const winPanel = document.getElementById("win-panel");
const killedSpan = document.getElementById("killed");
const ammoSpan = document.getElementById("ammo");
const levelSpan = document.getElementById("level");
const containerGame = document.getElementById("container-game");
(async () => {
const spineboy = spine.getSpineWidget("spineboy-game");
const windmill = spine.getSpineWidget("windmill-game");
await Promise.all([spineboy.loadingPromise, windmill.loadingPromise]);
spineboy.state.setAnimation(2, "aim", true);
spineboy.skeleton.slots.forEach(slot => {
if (slot.data.name === "gun") {
spineboy.addCursorSlotEventCallbacks(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(1, "shoot", false);
}
});
}
if (slot.data.name === "torso") {
spineboy.addCursorSlotEventCallbacks(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(0, "jump", false).mixDuration = 0.2;
spineboy.state.addAnimation(0, "walk", true).mixDuration = 0.2;
}
});
}
if (slot.data.name === "head") {
spineboy.addCursorSlotEventCallbacks(slot, (slot,event) => {
if (event === "down") {
spineboy.state.setAnimation(1, "run", true).mixDuration = 0.2;
} else {
if (event === "up") {
spineboy.state.setEmptyAnimation(1, 1);
spineboy.state.clearTrack(1);
}
}
});
}
});
const tempVector = new spine.Vector2();
const crosshairSlot = spineboy.skeleton.findSlot("crosshair");
crosshairSlot.color.a = 0;
const crosshairBone = crosshairSlot.bone;
let points = 0;
let ammo = 5;
let killed = 0;
let level = 1;
ammoSpan.innerText = `Ammo: ${ammo}`;
killedSpan.innerText = `Saved: ${killed}`;
levelSpan.innerText = `Level: ${level}`;
const flowers = [];
const ammoLocations = [];
windmill.skeleton.slots.forEach(slot => {
if (slot.data.name === "rooster" || slot.data.name === "bush1") {
ammoLocations.push(slot)
}
if (!Number.isNaN(parseInt(slot.data.name.replace("flower", ""))) || slot.data.name === "flower") {
slot.color.set(1, 0, 0, 1);
flowers.push(slot);
windmill.addCursorSlotEventCallbacks(slot, (slot, event) => {
if (ammo === 0) return;
if (event !== "down") return;
if (slot.color.g === 0) return;
spineboy.state.setAnimation(1, "shoot", false);
ammo--;
points++;
killed++;
if (points === 10) {
level++
points = 0
maxTime -= 100
}
ammoSpan.innerText = `Ammo: ${ammo}`;
killedSpan.innerText = `Saved: ${killed}`;
tempVector.x = slot.bone.x;
tempVector.y = slot.bone.y;
slot.bone.parentToWorld(tempVector);
crosshairBone.x = (tempVector.x + (windmill.worldX - spineboy.worldX) - spineboy.skeleton.x) / spineboy.skeleton.scaleX;
crosshairBone.y = (tempVector.y + (windmill.worldY - spineboy.worldY) - spineboy.skeleton.y) / spineboy.skeleton.scaleY;
slot.color.set(1, 0, 0, 1);
});
}
});
let time = 0;
let maxTime = 1000;
let ammoLocationActive = false;
let gameVectorTemp = new spine.Vector3();
let interval = setInterval(() => {
if (!ammoLocationActive && ammo <= 2) {
ammoLocationActive = true;
spineboy.overlay.worldToScreen(gameVectorTemp, spineboy.worldX + spineboy.skeleton.x, spineboy.worldY + spineboy.skeleton.y)
const { x, width } = containerGame.getBoundingClientRect();
const containerGameMiddle = x + width / 2;
const left = gameVectorTemp.x < containerGameMiddle;
const ammoLocation = ammoLocations[left ? 1 : 0];
(ammoLocation.darkColor ||= new spine.Color()).set(1, 0, 0, 1);
levelSpan.innerText = `Level: ${level} / 10`;
};
if (time >= maxTime) {
time = 0;
const flower = random(flowers);
flower.color.set(1, 1, 1, 1);
}
if (checkLoseCondition()) {
endGame(false, interval);
return;
}
if (checkWinCondition()) {
endGame(true, interval);
return;
}
time += 100;
}, 100);
const checkWinCondition = () => level === 10;
const checkLoseCondition = () => flowers.every((flowerSlot) => flowerSlot.color.g !== 0);
const endGame = (win, interval) => {
clearInterval(interval);
winPanel.style.display = "flex";
winPanel.innerText = win ? "YOU WIN!" : "YOU LOSE!"
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
keys.w = false;
keys.a = false;
keys.s = false;
keys.d = false;
spineboy.state.setAnimation(1, win ? "jump" : "death", win);
}
const random = array => array[Math.floor(Math.random() * array.length)];
const MOVE_SPEED = 2.5;
const keys = {
w: false,
a: false,
s: false,
d: false
};
function handleKeyDown(e) {
const key = e.key.toLowerCase();
if (key in keys) {
keys[key] = true;
e.preventDefault();
}
}
function handleKeyUp(e) {
const key = e.key.toLowerCase();
if (key in keys) keys[key] = false;
}
spineboy.skeleton.x += 0
spineboy.skeleton.y -= 330
let direction = 1;
spineboy.beforeUpdateWorldTransforms = () => {
let posX = 0;
let posY = 0;
// Move based on pressed keys
const inc = MOVE_SPEED * windmill.skeleton.scaleX;
if (keys.w) posY -= inc;
if (keys.a) posX -= inc;
if (keys.s) posY += inc;
if (keys.d) posX += inc;
// Update visual position
spineboy.skeleton.x += posX;
spineboy.skeleton.y -= posY;
direction = posX < 0 ? direction = -1 : posX > 0 ? 1 : direction;
spineboy.skeleton.scaleX = .25 * windmill.skeleton.scaleX * direction;
spineboy.skeleton.scaleY = .25 * windmill.skeleton.scaleY;
const spineboyPosition = {
x: spineboy.worldX + spineboy.skeleton.x - spineboy.bounds.width / 2 * spineboy.skeleton.scaleX * direction,
y: spineboy.worldY + spineboy.skeleton.y ,
width: spineboy.bounds.width * spineboy.skeleton.scaleX * direction,
height: spineboy.bounds.height * spineboy.skeleton.scaleY,
}
ammoLocations.forEach(element => {
const width = element.attachment.region.width * windmill.skeleton.scaleX;
const height = element.attachment.region.height * windmill.skeleton.scaleY;
const x = windmill.worldX + element.bone.worldX - width / 2;
const y = windmill.worldY + element.bone.worldY - height / 2;
if (isIntersecting(spineboyPosition, { x, y, width, height })) {
if (element.darkColor) {
ammo = 5;
element.darkColor = null;
ammoLocationActive = false;
}
}
});
}
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
})();
function isIntersecting(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
</script>
<div class="split-bottom">
<pre><code id="code-display">
<script>
escapeHTMLandInject(`
<spine-widget
atlas="assets/spineboy-pma.atlas"
skeleton="assets/spineboy-pro.skel"
animation="walk"
></spine-widget>`)
</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<script>
spine.SpineWebComponentWidget.SHOW_FPS = true;
</script>
<script>
// Drag and resize utility for section 6
function makeDraggable(element) {
element.style["touch-action"] = "none";
let isDragging = false;
let startX, startY;
let originalX, originalY;
element.addEventListener('pointerdown', startDragging);
document.addEventListener('pointermove', drag);
document.addEventListener('pointerup', stopDragging);
function startDragging(e) {
if (e.target === element.querySelector('#resizeHandle')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const translate = element.style.transform;
if (translate !== '') {
const translateValues = translate.match(/translate\(([^)]+)\)/)[1].split(', ');
originalX = parseFloat(translateValues[0]);
originalY = parseFloat(translateValues[1]);
} else {
originalX = 0;
originalY = 0;
}
}
function drag(e) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
element.style.transform = `translate(${originalX + deltaX}px, ${originalY + deltaY}px)`;
}
function stopDragging(e) {
isDragging = false;
}
}
function makeResizable(element) {
const resizeHandle = document.createElement('div');
element.appendChild(resizeHandle);
resizeHandle.id = "resizeHandle";
resizeHandle.style.width = "20%";
resizeHandle.style.height = "20%";
resizeHandle.style.bottom = "0";
resizeHandle.style.right = "0";
resizeHandle.style.position = "absolute";
resizeHandle.style["background-color"] = "#007bff";
resizeHandle.style["cursor"] = "se-resize";
element.style["position"] = "relative";
element.style["touch-action"] = "none";
let isResizing = false;
let startX, startY, startWidth, startHeight, startPaddingLeft, startPaddingRight, startPaddingTop, startPaddingBottom;
resizeHandle.addEventListener('pointerdown', initResize);
function initResize(e) {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = element.offsetWidth;
startHeight = element.offsetHeight;
startPaddingLeft = parseFloat(window.getComputedStyle(element).paddingLeft);
startPaddingRight = parseFloat(window.getComputedStyle(element).paddingRight);
startPaddingTop = parseFloat(window.getComputedStyle(element).paddingTop);
startPaddingBottom = parseFloat(window.getComputedStyle(element).paddingBottom);
document.addEventListener('pointermove', resize);
document.addEventListener('pointerup', stopResize);
}
function resize(e) {
if (!isResizing) return;
const width = startWidth + (e.clientX - startX) - startPaddingLeft - startPaddingRight;
const height = startHeight + (e.clientY - startY) - startPaddingTop - startPaddingBottom;
element.style.width = width + 'px';
element.style.height = height + 'px';
}
function stopResize() {
isResizing = false;
document.removeEventListener('pointermove', resize);
document.removeEventListener('pointerup', stopResize);
}
}
makeDraggable(document.getElementById(`section7-element`));
makeResizable(document.getElementById(`section7-element`));
</script>
</body>
</html>