spine-runtimes/spine-ts/spine-webgl/example/webcomponent-tutorial.html
2024-10-21 15:19:46 +02:00

2638 lines
98 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;
}
.full-width {
width: 100%;
}
.split-left, .split-right {
width: 50%;
min-height: 50%;
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%;
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 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">
<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;">
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;">
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;">
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;">
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;">
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;">
<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;">
<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;">
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;">
<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>
<script>
(async () => {
const tank = spine.getSpineWidget("tank");
const tank2 = spine.getSpineWidget("tank2");
await tank.loadingPromise;
await 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 tank.loadingPromise;
await 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;">
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
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>
<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;">
<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 mouse = Ola({ x: 0, y: 0 }, 200);
widget.afterUpdateWorldTransforms = () => {
updateControl(widget, control, mouse, 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, mouse, 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;
}
mouse.set({ x, y });
controlBone.x = controlBone.data.x + mouse.x;
controlBone.y = controlBone.data.y + mouse.y;
}
`
);</script>
</code></pre>
</div>
</div>
<script>
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 mouse = Ola({ x: 0, y: 0 }, 200);
widget.afterUpdateWorldTransforms = () => {
updateControl(widget, control, mouse, 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, mouse, 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;
}
mouse.set({ x, y });
controlBone.x = controlBone.data.x + mouse.x;
controlBone.y = controlBone.data.y + mouse.y;
}
</script>
<script>
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Ola=factory())})(this,function(){"use strict";const position=(x0,v0,t1,t)=>{const a=(v0*t1+2*x0)/t1**3;const b=-(2*v0*t1+3*x0)/t1**2;const c=v0;const d=x0;return a*t**3+b*t**2+c*t+d};const speed=(x0,v0,t1,t)=>{const a=(v0*t1+2*x0)/t1**3;const b=-(2*v0*t1+3*x0)/t1**2;const c=v0;return 3*a*t**2+2*b*t+c};const each=function(values,cb){const multi=typeof values==="number"?{value:values}:values;Object.entries(multi).map(([key,value])=>cb(value,key))};function Single(init,time){this.start=new Date/1e3;this.time=time;this.from=init;this.current=init;this.to=init;this.speed=0}Single.prototype.get=function(now){const t=now/1e3-this.start;if(t<0){throw new Error("Cannot read in the past")}if(t>=this.time){return this.to}return this.to-position(this.to-this.from,this.speed,this.time,t)};Single.prototype.getSpeed=function(now){const t=now/1e3-this.start;if(t>=this.time){return 0}return speed(this.to-this.from,this.speed,this.time,t)};Single.prototype.set=function(value,time){const now=new Date;const current=this.get(now);this.speed=this.getSpeed(now);this.start=now/1e3;this.from=current;this.to=value;if(time){this.time=time}return current};function Ola(values,time=300){if(typeof values==="number"){values={value:values}}each(values,(init,key)=>{const value=new Single(init,time/1e3);Object.defineProperty(values,"_"+key,{value:value});Object.defineProperty(values,"$"+key,{get:()=>value.to});Object.defineProperty(values,key,{get:()=>value.get(new Date),set:val=>value.set(val),enumerable:true})});Object.defineProperty(values,"get",{get:()=>(function(name="value",now=new Date){return this["_"+name].get(now)})});Object.defineProperty(values,"set",{get:()=>(function(values,time=0){each(values,(value,key)=>{this["_"+key].set(value,time/1e3)})})});return values}return Ola});
</script>
<!--
/////////////////////
// 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>