mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
web component
This commit is contained in:
parent
d92046f325
commit
221e3f6624
@ -37,6 +37,11 @@
|
|||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
border: 1px solid salmon;
|
border: 1px solid salmon;
|
||||||
}
|
}
|
||||||
|
.split-nosize {
|
||||||
|
/* padding: 1rem; */
|
||||||
|
/* margin: 1rem; */
|
||||||
|
border: 1px solid salmon;
|
||||||
|
}
|
||||||
.navigation {
|
.navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -96,7 +101,38 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span id="fps" style="position: fixed; top: 0; left: 0">FPS</span>
|
<span id="fps" style="position: fixed; top: 0; left: 0">FPS</span>
|
||||||
<!-- <img src="assets/spineboy-pma.png" alt=""> -->
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 0 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section0" class="section vertical-split">
|
||||||
|
|
||||||
|
<div class="split-top split">
|
||||||
|
<div class="split-left">
|
||||||
|
aaa
|
||||||
|
</div>
|
||||||
|
<div class="split-right" id="section0-element">
|
||||||
|
<spine identifier="section0" createDiv="true" width="220" height="50"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="split-bottom">
|
||||||
|
<pre><code id="code-display">
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// end section 0 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
/////////////////////
|
/////////////////////
|
||||||
// start section 1 //
|
// start section 1 //
|
||||||
@ -108,7 +144,7 @@
|
|||||||
<div class="split-top split">
|
<div class="split-top split">
|
||||||
<div class="split-left" id="section1-element">
|
<div class="split-left" id="section1-element">
|
||||||
</div>
|
</div>
|
||||||
<div class="split-right" id="section1-element-x">
|
<div class="split-right">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -473,7 +509,6 @@ overlay.addSkeleton(
|
|||||||
// }
|
// }
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
/////////////////////
|
||||||
// start section 1 //
|
// start section 1 //
|
||||||
/////////////////////
|
/////////////////////
|
||||||
@ -483,144 +518,194 @@ overlay.addSkeleton(
|
|||||||
skeletonPath: "assets/spineboy-pro.skel",
|
skeletonPath: "assets/spineboy-pro.skel",
|
||||||
animation: 'walk',
|
animation: 'walk',
|
||||||
},
|
},
|
||||||
document.querySelectorAll(`#section1-element`),
|
[
|
||||||
|
{
|
||||||
|
identifier: `section0`,
|
||||||
|
debug: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
);
|
);
|
||||||
/////////////////////
|
/////////////////////
|
||||||
// end section 1 //
|
// end section 1 //
|
||||||
/////////////////////
|
/////////////////////
|
||||||
|
|
||||||
|
// /////////////////////
|
||||||
|
// // start section 1 //
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
// start section 2 //
|
// overlay.addSkeleton(
|
||||||
/////////////////////
|
// {
|
||||||
overlay.addSkeleton(
|
// atlasPath: "assets/spineboy-pma.atlas",
|
||||||
{
|
// skeletonPath: "assets/spineboy-pro.skel",
|
||||||
atlasPath: "assets/spineboy-pma.atlas",
|
// animation: 'walk',
|
||||||
skeletonPath: "assets/spineboy-pro.skel",
|
// },
|
||||||
animation: 'run',
|
// document.querySelectorAll(`#section1-element`),
|
||||||
scale: .25,
|
// );
|
||||||
},
|
// /////////////////////
|
||||||
{
|
// // end section 1 //
|
||||||
element: document.getElementById(`section2-element`),
|
// /////////////////////
|
||||||
mode: 'origin',
|
|
||||||
xAxis: .5,
|
|
||||||
yAxis: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
/////////////////////
|
|
||||||
// end section 2 //
|
|
||||||
/////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
|
||||||
// start section 3 //
|
|
||||||
/////////////////////
|
|
||||||
overlay.addSkeleton(
|
|
||||||
{
|
|
||||||
atlasPath: "assets/spineboy-pma.atlas",
|
|
||||||
skeletonPath: "assets/spineboy-pro.skel",
|
|
||||||
animation: 'jump',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: document.getElementById(`section3-element`),
|
|
||||||
mode: 'inside', // default
|
|
||||||
offsetX: 100,
|
|
||||||
offsetY: 50,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
/////////////////////
|
|
||||||
// end section 3 //
|
|
||||||
/////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
// start section 4 //
|
// // start section 2 //
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
const { skeleton, state } = await overlay.addSkeleton(
|
// overlay.addSkeleton(
|
||||||
{
|
// {
|
||||||
atlasPath: "assets/raptor-pma.atlas",
|
// atlasPath: "assets/spineboy-pma.atlas",
|
||||||
skeletonPath: "assets/raptor-pro.skel",
|
// skeletonPath: "assets/spineboy-pro.skel",
|
||||||
animation: 'walk',
|
// animation: 'run',
|
||||||
},
|
// scale: .25,
|
||||||
document.getElementById(`section4-element`)
|
// },
|
||||||
);
|
// {
|
||||||
|
// element: document.getElementById(`section2-element`),
|
||||||
|
// mode: 'origin',
|
||||||
|
// xAxis: .5,
|
||||||
|
// yAxis: 1,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// /////////////////////
|
||||||
|
// // end section 2 //
|
||||||
|
// /////////////////////
|
||||||
|
|
||||||
let isRoaring = false;
|
|
||||||
setInterval(() => {
|
|
||||||
const newAnimation = isRoaring ? "walk" : "roar";
|
|
||||||
state.setAnimation(0, newAnimation, true);
|
|
||||||
overlay.recalculateBounds(skeleton);
|
|
||||||
isRoaring = !isRoaring;
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
// end section 4 //
|
// // start section 3 //
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
|
// overlay.addSkeleton(
|
||||||
/////////////////////
|
// {
|
||||||
// start section 5 //
|
// atlasPath: "assets/spineboy-pma.atlas",
|
||||||
/////////////////////
|
// skeletonPath: "assets/spineboy-pro.skel",
|
||||||
/////////////////////
|
// animation: 'jump',
|
||||||
// end section 5 //
|
// },
|
||||||
/////////////////////
|
// {
|
||||||
|
// element: document.getElementById(`section3-element`),
|
||||||
/////////////////////
|
// mode: 'inside', // default
|
||||||
// start section 6 //
|
// offsetX: 100,
|
||||||
/////////////////////
|
// offsetY: 50,
|
||||||
overlay.addSkeleton(
|
// },
|
||||||
{
|
// );
|
||||||
atlasPath: "assets/cloud-pot-pma.atlas",
|
// /////////////////////
|
||||||
skeletonPath: "assets/cloud-pot.skel",
|
// // end section 3 //
|
||||||
animation: 'playing-in-the-rain',
|
// /////////////////////
|
||||||
},
|
|
||||||
document.getElementById(`section6-element`)
|
|
||||||
);
|
|
||||||
/////////////////////
|
|
||||||
// end section 6 //
|
|
||||||
/////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
// start section 7 //
|
// // start section 4 //
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
overlay.addSkeleton(
|
// const { skeleton, state } = await overlay.addSkeleton(
|
||||||
{
|
// {
|
||||||
atlasPath: "assets/owl-pma.atlas",
|
// atlasPath: "assets/raptor-pma.atlas",
|
||||||
skeletonPath: "assets/owl-pro.skel",
|
// skeletonPath: "assets/raptor-pro.skel",
|
||||||
animation: 'idle',
|
// animation: 'walk',
|
||||||
},
|
// },
|
||||||
{
|
// document.getElementById(`section4-element`)
|
||||||
element: document.getElementById(`section7-element`),
|
// );
|
||||||
debug: true,
|
|
||||||
}
|
// let isRoaring = false;
|
||||||
);
|
// setInterval(() => {
|
||||||
//////////////////////
|
// const newAnimation = isRoaring ? "walk" : "roar";
|
||||||
// end section 7 //
|
// state.setAnimation(0, newAnimation, true);
|
||||||
//////////////////////
|
// overlay.recalculateBounds(skeleton);
|
||||||
|
// isRoaring = !isRoaring;
|
||||||
|
// }, 4000);
|
||||||
|
|
||||||
|
// /////////////////////
|
||||||
|
// // end section 4 //
|
||||||
|
// /////////////////////
|
||||||
|
|
||||||
|
// /////////////////////
|
||||||
|
// // start section 5 //
|
||||||
|
// /////////////////////
|
||||||
|
// // const { skeleton: skeleton5 } = await overlay.addSkeleton(
|
||||||
|
// // {
|
||||||
|
// // atlasPath: "assets/spineboy-pma.atlas",
|
||||||
|
// // skeletonPath: "assets/spineboy-pro.skel",
|
||||||
|
// // animation: 'walk',
|
||||||
|
// // },
|
||||||
|
// // document.getElementById(`section5-element`)
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // const bbAttachmentSlot = skeleton5.findSlot("head-bb");
|
||||||
|
// // const currentAttachment = bbAttachmentSlot.attachment;
|
||||||
|
// // skeleton5.setAttachment("head-bb", "head");
|
||||||
|
// // const bbAttachment = bbAttachmentSlot.attachment;
|
||||||
|
|
||||||
|
// // const computedVertices = [];
|
||||||
|
// // bbAttachment.computeWorldVertices(bbAttachmentSlot, 0, bbAttachment.worldVerticesLength, computedVertices, 0, 2);
|
||||||
|
// // const vertices = computedVertices;
|
||||||
|
// // let x = Infinity, maxX = -Infinity, y = Infinity, maxY = -Infinity;
|
||||||
|
// // for (let i = 0; i < vertices.length; i+=2) {
|
||||||
|
// // x = Math.min(vertices[i], x);
|
||||||
|
// // y = Math.min(vertices[i+1], y);
|
||||||
|
// // maxX = Math.max(vertices[i], maxX);
|
||||||
|
// // maxY = Math.max(vertices[i+1], maxY);
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // const width = maxX - x;
|
||||||
|
// // const height = maxY - y;
|
||||||
|
// // console.log({ x, y, width, height })
|
||||||
|
// // overlay.setBounds(skeleton5, { x, y, width, height })
|
||||||
|
// // bbAttachmentSlot.setAttachment(currentAttachment)
|
||||||
|
// /////////////////////
|
||||||
|
// // end section 5 //
|
||||||
|
// /////////////////////
|
||||||
|
|
||||||
|
// /////////////////////
|
||||||
|
// // start section 6 //
|
||||||
|
// /////////////////////
|
||||||
|
// overlay.addSkeleton(
|
||||||
|
// {
|
||||||
|
// atlasPath: "assets/cloud-pot-pma.atlas",
|
||||||
|
// skeletonPath: "assets/cloud-pot.skel",
|
||||||
|
// animation: 'playing-in-the-rain',
|
||||||
|
// },
|
||||||
|
// document.getElementById(`section6-element`)
|
||||||
|
// );
|
||||||
|
// /////////////////////
|
||||||
|
// // end section 6 //
|
||||||
|
// /////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
// start section 8 //
|
// // start section 7 //
|
||||||
/////////////////////
|
// /////////////////////
|
||||||
overlay.addSkeleton(
|
// overlay.addSkeleton(
|
||||||
{
|
// {
|
||||||
atlasPath: "assets/celestial-circus-pma.atlas",
|
// atlasPath: "assets/owl-pma.atlas",
|
||||||
skeletonPath: "assets/celestial-circus-pro.skel",
|
// skeletonPath: "assets/owl-pro.skel",
|
||||||
animation: 'wings-and-feet',
|
// animation: 'idle',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
element: document.getElementById(`section8-element`),
|
// element: document.getElementById(`section7-element`),
|
||||||
draggable: true,
|
// debug: true,
|
||||||
debug: true,
|
// }
|
||||||
}
|
// );
|
||||||
);
|
// //////////////////////
|
||||||
//////////////////////
|
// // end section 7 //
|
||||||
// end section 8 //
|
// //////////////////////
|
||||||
//////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
|
// /////////////////////
|
||||||
|
// // start section 8 //
|
||||||
|
// /////////////////////
|
||||||
|
// overlay.addSkeleton(
|
||||||
|
// {
|
||||||
|
// atlasPath: "assets/celestial-circus-pma.atlas",
|
||||||
|
// skeletonPath: "assets/celestial-circus-pro.skel",
|
||||||
|
// animation: 'wings-and-feet',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// element: document.getElementById(`section8-element`),
|
||||||
|
// draggable: true,
|
||||||
|
// debug: true,
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// //////////////////////
|
||||||
|
// // end section 8 //
|
||||||
|
// //////////////////////
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
741
spine-ts/spine-webgl/example/canvas6.html
Normal file
741
spine-ts/spine-webgl/example/canvas6.html
Normal file
@ -0,0 +1,741 @@
|
|||||||
|
<!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.js"></script> -->
|
||||||
|
<title>JS Library Showcase</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
/* height: 100lvh; */
|
||||||
|
/* height: 800px; */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function escapeHTMLandInject(text) {
|
||||||
|
const escaped = text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
document.currentScript.parentElement.innerHTML = escaped;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 0 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- <div id="section0" class="section vertical-split">
|
||||||
|
|
||||||
|
<div class="split-top split">
|
||||||
|
<div class="split-left">
|
||||||
|
aaa
|
||||||
|
</div>
|
||||||
|
<div class="split-right" id="section0-element">
|
||||||
|
<spine identifier="section0" createDiv="true" width="220" height="50"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="split-bottom">
|
||||||
|
<pre><code id="code-display">
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// end section 0 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 1 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section1" 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><spine-widget></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 1 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 2 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section1" 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"
|
||||||
|
debug="true"
|
||||||
|
></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>fitWidth</code>, <code>fitWidth</code>, <code>fitHeight</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 2 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 3 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section2" 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 3 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 3 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section3" 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 3 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 4 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section4" 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 = document.querySelector("spine-widget[identifier=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 4 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 6 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section6" class="section vertical-split">
|
||||||
|
|
||||||
|
<div class="full-width high-page" style="padding: 1em;">
|
||||||
|
<div class="split-nosize" style="padding: 1em; margin-bottom: 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.
|
||||||
|
</div>
|
||||||
|
<div class="split-nosize" style="width: 100%; height: 50%;" id="section6-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 6 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 7 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section7" 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" id="section7-element">
|
||||||
|
<spine-widget
|
||||||
|
atlas="assets/sack-pma.atlas"
|
||||||
|
skeleton="assets/sack-pro.skel"
|
||||||
|
animation="cape-follow-example"
|
||||||
|
debug="true"
|
||||||
|
></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="true"
|
||||||
|
></spine-widget>`
|
||||||
|
);</script>
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// end section 7 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// start section 8 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="section8" class="section vertical-split">
|
||||||
|
|
||||||
|
<div class="split-top split">
|
||||||
|
<div class="split-left">
|
||||||
|
As a bonus item, you can move you skeleton around just by setting the <code>draggable</code> property to <code>true</code>.
|
||||||
|
</div>
|
||||||
|
<div class="split-right" id="section8-element">
|
||||||
|
<spine-widget
|
||||||
|
atlas="assets/celestial-circus-pma.atlas"
|
||||||
|
skeleton="assets/celestial-circus-pro.skel"
|
||||||
|
animation="wings-and-feet"
|
||||||
|
draggable="true"
|
||||||
|
></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"
|
||||||
|
draggable="true"
|
||||||
|
></spine-widget>`
|
||||||
|
);</script>
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/////////////////////
|
||||||
|
// end section 8 //
|
||||||
|
/////////////////////
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="navigation">
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section1')">1</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section2')">2</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section3')">3</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section4')">4</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section5')">5</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section6')">6</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section7')">7</button>
|
||||||
|
<button class="nav-btn" onclick="scrollToSection('section8')">8</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function scrollToSection(id) {
|
||||||
|
document.getElementById(id).scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = document.querySelectorAll('.section');
|
||||||
|
let currentSection = 0;
|
||||||
|
|
||||||
|
// window.addEventListener('wheel', (e) => {
|
||||||
|
// if (e.deltaY > 0 && currentSection < sections.length - 1) {
|
||||||
|
// currentSection++;
|
||||||
|
// } else if (e.deltaY < 0 && currentSection > 0) {
|
||||||
|
// currentSection--;
|
||||||
|
// }
|
||||||
|
// sections[currentSection].scrollIntoView({ behavior: 'smooth' });
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// Drag utility
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// Resize utility
|
||||||
|
|
||||||
|
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(`section6-element`));
|
||||||
|
makeResizable(document.getElementById(`section6-element`));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -37,8 +37,8 @@ interface Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OverlaySkeletonOptions {
|
interface OverlaySkeletonOptions {
|
||||||
atlasPath: string,
|
atlas: string,
|
||||||
skeletonPath: string,
|
skeleton: string,
|
||||||
scale: number,
|
scale: number,
|
||||||
animation?: string,
|
animation?: string,
|
||||||
skeletonData?: SkeletonData,
|
skeletonData?: SkeletonData,
|
||||||
@ -48,7 +48,8 @@ interface OverlaySkeletonOptions {
|
|||||||
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
||||||
|
|
||||||
interface OverlayHTMLOptions {
|
interface OverlayHTMLOptions {
|
||||||
element: HTMLElement,
|
identifier: string,
|
||||||
|
createDivInElement?: boolean,
|
||||||
mode?: OverlayElementMode,
|
mode?: OverlayElementMode,
|
||||||
debug?: boolean,
|
debug?: boolean,
|
||||||
offsetX?: number,
|
offsetX?: number,
|
||||||
@ -58,7 +59,7 @@ interface OverlayHTMLOptions {
|
|||||||
draggable?: boolean,
|
draggable?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type OverlayHTMLElement = Required<OverlayHTMLOptions> & { element: HTMLElement, scaleDpi: number, worldOffsetX: number, worldOffsetY: number, dragging: boolean, dragX: number, dragY: number };
|
type OverlayHTMLElement = Required<Omit<OverlayHTMLOptions, "identifier">> & { element: HTMLElement, scaleDpi: number, worldOffsetX: number, worldOffsetY: number, dragging: boolean, dragX: number, dragY: number };
|
||||||
|
|
||||||
type OverlayElementMode = 'inside' | 'origin';
|
type OverlayElementMode = 'inside' | 'origin';
|
||||||
|
|
||||||
@ -108,7 +109,6 @@ export class SpineCanvasOverlay {
|
|||||||
this.canvas.style.left = "0";
|
this.canvas.style.left = "0";
|
||||||
this.canvas.style.setProperty("pointer-events", "none");
|
this.canvas.style.setProperty("pointer-events", "none");
|
||||||
this.canvas.style.transform =`translate(0px,0px)`;
|
this.canvas.style.transform =`translate(0px,0px)`;
|
||||||
// this.canvas.style.display = "inline";
|
|
||||||
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
||||||
|
|
||||||
// resize and zoom
|
// resize and zoom
|
||||||
@ -130,8 +130,6 @@ export class SpineCanvasOverlay {
|
|||||||
window.addEventListener('scroll', this.scrollHandler);
|
window.addEventListener('scroll', this.scrollHandler);
|
||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
|
|
||||||
// zoom
|
|
||||||
|
|
||||||
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
||||||
|
|
||||||
this.input = new Input(document.body, false);
|
this.input = new Input(document.body, false);
|
||||||
@ -141,17 +139,17 @@ export class SpineCanvasOverlay {
|
|||||||
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
||||||
public async addSkeleton(
|
public async addSkeleton(
|
||||||
skeletonOptions: OverlaySkeletonOptions,
|
skeletonOptions: OverlaySkeletonOptions,
|
||||||
htmlOptionsList: Array<OverlayHTMLOptions> | OverlayHTMLOptions | Array<HTMLElement> | HTMLElement | NodeList = [],
|
htmlOptionsList: Array<OverlayHTMLOptions>,
|
||||||
) {
|
) {
|
||||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
const { atlas, skeleton: skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
||||||
const isBinary = skeletonPath.endsWith(".skel");
|
const isBinary = skeletonPath.endsWith(".skel");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
||||||
this.loadTextureAtlas(atlasPath),
|
this.loadTextureAtlas(atlas),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const atlas = this.spineCanvas.assetManager.require(atlasPath);
|
const atlasLoaded = this.spineCanvas.assetManager.require(atlas);
|
||||||
const atlasLoader = new AtlasAttachmentLoader(atlas);
|
const atlasLoader = new AtlasAttachmentLoader(atlasLoaded);
|
||||||
|
|
||||||
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
||||||
skeletonLoader.scale = scale;
|
skeletonLoader.scale = scale;
|
||||||
@ -170,44 +168,56 @@ export class SpineCanvasOverlay {
|
|||||||
}
|
}
|
||||||
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
||||||
|
|
||||||
let list: Array<OverlayHTMLOptions>;
|
const halfDpi = window.devicePixelRatio / 2;
|
||||||
if (htmlOptionsList instanceof HTMLElement) htmlOptionsList = [htmlOptionsList] as Array<HTMLElement>;
|
|
||||||
if (htmlOptionsList instanceof NodeList) htmlOptionsList = Array.from(htmlOptionsList) as Array<HTMLElement>;
|
|
||||||
if ('element' in htmlOptionsList) htmlOptionsList = [htmlOptionsList] as Array<OverlayHTMLOptions>;
|
|
||||||
|
|
||||||
if (htmlOptionsList.length > 0 && htmlOptionsList[0] instanceof HTMLElement) {
|
const { identifier, createDivInElement = false, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, } = htmlOptionsList[0];
|
||||||
list = htmlOptionsList.map(element => ({ element } as OverlayHTMLOptions));
|
|
||||||
} else {
|
const mode = givenMode ?? 'inside';
|
||||||
list = htmlOptionsList as Array<OverlayHTMLOptions>;
|
|
||||||
|
const el = document.querySelector(`spine[identifier="${identifier}"]`) as HTMLElement;
|
||||||
|
if (!el) {
|
||||||
|
throw new Error("Element not found with identifier: " + identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
const halfDpi = window.devicePixelRatio / 2;
|
let parent = el.parentElement;
|
||||||
const mapList = list.map(({ element, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
|
if (createDivInElement) {
|
||||||
const mode = givenMode ?? 'inside';
|
const width = el.getAttribute('width');
|
||||||
if (mode == 'inside' && i > 0) {
|
const height = el.getAttribute('height');
|
||||||
console.warn("inside option works with multiple html elements only if the elements have the same dimension"
|
parent = el;
|
||||||
+ "This is because the skeleton is scaled to stay into the div."
|
parent.style.width = `${width}px`;
|
||||||
+ "You can call addSkeleton several time (skeleton data can be reuse, if given).");
|
parent.style.height = `${height}px`;
|
||||||
}
|
parent.style.display = 'block';
|
||||||
return {
|
if (debug) parent.style.backgroundColor = "rgba(0, 0, 0, .5)";
|
||||||
element: element as HTMLElement,
|
}
|
||||||
mode,
|
|
||||||
debug,
|
if (!parent) {
|
||||||
offsetX,
|
throw new Error("Parent of element not found");
|
||||||
offsetY,
|
}
|
||||||
xAxis,
|
|
||||||
yAxis,
|
console.log(el)
|
||||||
draggable,
|
console.log(parent)
|
||||||
dragX: 0,
|
|
||||||
dragY: 0,
|
const obj = {
|
||||||
worldOffsetX: 0,
|
element: parent,
|
||||||
worldOffsetY: 0,
|
createDivInElement,
|
||||||
// change this name to something like initialScaleDpi
|
mode,
|
||||||
scaleDpi: halfDpi,
|
debug,
|
||||||
// scaleDpi: 1,
|
offsetX,
|
||||||
dragging: false,
|
offsetY,
|
||||||
}
|
xAxis,
|
||||||
});
|
yAxis,
|
||||||
|
draggable,
|
||||||
|
dragX: 0,
|
||||||
|
dragY: 0,
|
||||||
|
worldOffsetX: 0,
|
||||||
|
worldOffsetY: 0,
|
||||||
|
// change this name to something like initialScaleDpi
|
||||||
|
scaleDpi: halfDpi,
|
||||||
|
// scaleDpi: 1,
|
||||||
|
dragging: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapList = [obj];
|
||||||
|
|
||||||
skeleton.scaleX = halfDpi;
|
skeleton.scaleX = halfDpi;
|
||||||
skeleton.scaleY = halfDpi;
|
skeleton.scaleY = halfDpi;
|
||||||
@ -312,6 +322,7 @@ export class SpineCanvasOverlay {
|
|||||||
divBounds.x += this.overflowLeftSize;
|
divBounds.x += this.overflowLeftSize;
|
||||||
divBounds.y += this.overflowTopSize;
|
divBounds.y += this.overflowTopSize;
|
||||||
|
|
||||||
|
const fit: "fill" | "fitWidth" | "fitHeight" | "contain" | "cover" | "none" | "scaleDown" = "scaleDown";
|
||||||
let x = 0, y = 0;
|
let x = 0, y = 0;
|
||||||
if (mode === 'inside') {
|
if (mode === 'inside') {
|
||||||
// scale ratio
|
// scale ratio
|
||||||
@ -319,16 +330,52 @@ export class SpineCanvasOverlay {
|
|||||||
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
||||||
|
|
||||||
// attempt to use width ratio
|
// attempt to use width ratio
|
||||||
let ratio = scaleWidth;
|
let ratioW = 1;
|
||||||
let scaledW = aw * ratio;
|
let ratioH = 1;
|
||||||
let scaledH = ah * ratio;
|
|
||||||
|
|
||||||
// if scaled height is bigger than div height, use height ratio instead
|
if (fit === "fill") { // Fill the target box by distorting the source's aspect ratio.
|
||||||
if (scaledH > divBounds.height * devicePixelRatio) ratio = scaleHeight;
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else if (fit === "fitWidth") {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
} else if (fit === "fitHeight") {
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else if (fit === "contain") {
|
||||||
|
// if scaled height is bigger than div height, use height ratio instead
|
||||||
|
if (ah * scaleWidth > divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
} else if (fit === "cover") {
|
||||||
|
if (ah * scaleWidth < divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
} else if (fit === "scaleDown") {
|
||||||
|
if (aw > divBounds.width * devicePixelRatio || ah > divBounds.height * devicePixelRatio) {
|
||||||
|
if (ah * scaleWidth > divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fit === "none") {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// get the center of the bounds
|
// get the center of the bounds
|
||||||
const boundsX = (ax + aw / 2) * ratio;
|
const boundsX = (ax + aw / 2) * ratioW;
|
||||||
const boundsY = (ay + ah / 2) * ratio;
|
const boundsY = (ay + ah / 2) * ratioH;
|
||||||
|
|
||||||
// get the center of the div in world coordinate
|
// get the center of the div in world coordinate
|
||||||
const divX = divBounds.x + divBounds.width / 2;
|
const divX = divBounds.x + divBounds.width / 2;
|
||||||
@ -340,8 +387,8 @@ export class SpineCanvasOverlay {
|
|||||||
y = tempVector.y - boundsY;
|
y = tempVector.y - boundsY;
|
||||||
|
|
||||||
// scale the skeleton
|
// scale the skeleton
|
||||||
skeleton.scaleX = ratio;
|
skeleton.scaleX = ratioW;
|
||||||
skeleton.scaleY = ratio;
|
skeleton.scaleY = ratioH;
|
||||||
} else {
|
} else {
|
||||||
// get the center of the div in world coordinate
|
// get the center of the div in world coordinate
|
||||||
const divX = divBounds.x + divBounds.width * xAxis;
|
const divX = divBounds.x + divBounds.width * xAxis;
|
||||||
|
|||||||
806
spine-ts/spine-webgl/src/SpineWebComponent.ts
Normal file
806
spine-ts/spine-webgl/src/SpineWebComponent.ts
Normal file
@ -0,0 +1,806 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* Spine Runtimes License Agreement
|
||||||
|
* Last updated July 28, 2023. Replaces all prior versions.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2023, Esoteric Software LLC
|
||||||
|
*
|
||||||
|
* Integration of the Spine Runtimes into software or otherwise creating
|
||||||
|
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||||
|
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||||
|
* http://esotericsoftware.com/spine-editor-license
|
||||||
|
*
|
||||||
|
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
|
||||||
|
* otherwise create derivative works of the Spine Runtimes (collectively,
|
||||||
|
* "Products"), provided that each user of the Products must obtain their own
|
||||||
|
* Spine Editor license and redistribution of the Products in any form must
|
||||||
|
* include this license and copyright notice.
|
||||||
|
*
|
||||||
|
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||||
|
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
|
||||||
|
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input } from "./index.js";
|
||||||
|
|
||||||
|
interface Rectangle {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
||||||
|
|
||||||
|
type ModeType = 'inside' | 'origin';
|
||||||
|
function isModeType(value: string): value is ModeType {
|
||||||
|
return (
|
||||||
|
value === "inside" ||
|
||||||
|
value === "origin"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FitType = "fill" | "fitWidth" | "fitHeight" | "contain" | "cover" | "none" | "scaleDown";
|
||||||
|
function isFitType(value: string): value is FitType {
|
||||||
|
return (
|
||||||
|
value === "fill" ||
|
||||||
|
value === "fitWidth" ||
|
||||||
|
value === "fitHeight" ||
|
||||||
|
value === "contain" ||
|
||||||
|
value === "cover" ||
|
||||||
|
value === "none" ||
|
||||||
|
value === "scaleDown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetLayoutOptions {
|
||||||
|
mode: ModeType
|
||||||
|
debug: boolean
|
||||||
|
offsetX: number
|
||||||
|
offsetY: number
|
||||||
|
xAxis: number
|
||||||
|
yAxis: number
|
||||||
|
draggable: boolean
|
||||||
|
fit: FitType
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
identifier: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetPublicState {
|
||||||
|
skeleton: Skeleton
|
||||||
|
state: AnimationState
|
||||||
|
bounds: Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetInternalState {
|
||||||
|
currentScaleDpi: number
|
||||||
|
worldOffsetX: number
|
||||||
|
worldOffsetY: number
|
||||||
|
dragging: boolean
|
||||||
|
dragX: number
|
||||||
|
dragY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions, WidgetInternalState, Partial<WidgetPublicState> {
|
||||||
|
|
||||||
|
// skeleton options
|
||||||
|
public atlasPath: string;
|
||||||
|
public skeletonPath: string;
|
||||||
|
public scale = 1;
|
||||||
|
public animation?: string;
|
||||||
|
skeletonData?: SkeletonData; // TODO
|
||||||
|
update?: UpdateSpineFunction; // TODO
|
||||||
|
|
||||||
|
// layout options
|
||||||
|
public fit: FitType = "contain";
|
||||||
|
public mode: ModeType = "inside";
|
||||||
|
public offsetX = 0;
|
||||||
|
public offsetY = 0;
|
||||||
|
public xAxis = 0;
|
||||||
|
public yAxis = 0;
|
||||||
|
public width = 0;
|
||||||
|
public height = 0;
|
||||||
|
public draggable = false;
|
||||||
|
public debug = false;
|
||||||
|
public identifier = "";
|
||||||
|
|
||||||
|
// state
|
||||||
|
public skeleton?: Skeleton;
|
||||||
|
public state?: AnimationState;
|
||||||
|
public bounds?: Rectangle;
|
||||||
|
public loadingPromise?: Promise<WidgetPublicState>;
|
||||||
|
|
||||||
|
// TODO: makes the interface exposes getter, make getter and make these private
|
||||||
|
// internal state
|
||||||
|
public currentScaleDpi = 1;
|
||||||
|
public worldOffsetX = 0;
|
||||||
|
public worldOffsetY = 0;
|
||||||
|
public dragX = 0;
|
||||||
|
public dragY = 0;
|
||||||
|
public dragging = false;
|
||||||
|
|
||||||
|
private root: ShadowRoot;
|
||||||
|
private overlay: SpineWebComponentOverlay;
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return ["atlas", "skeleton", "scale", "animation", "fit", "width", "height", "draggable", "mode", "x-axis", "y-axis", "identifier", "offset-x", "offset-y", "debug"];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.root = this.attachShadow({ mode: "open" });
|
||||||
|
this.overlay = this.initializeOverlay();
|
||||||
|
this.atlasPath = "TODO";
|
||||||
|
this.skeletonPath = "TODO";
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (!this.atlasPath) {
|
||||||
|
throw new Error("Missing atlas attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.skeletonPath) {
|
||||||
|
throw new Error("Missing skeleton attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingPromise = this.loadSkeleton();
|
||||||
|
this.loadingPromise.then(() => {
|
||||||
|
this.overlay.skeletonList.push(this);
|
||||||
|
}); // async
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.loadingPromise?.then(() => {
|
||||||
|
const index = this.overlay.skeletonList.indexOf(this);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.overlay.skeletonList.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
||||||
|
if (newValue) {
|
||||||
|
if (name === "identifier") {
|
||||||
|
this.identifier = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "atlas") {
|
||||||
|
this.atlasPath = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "skeleton") {
|
||||||
|
this.skeletonPath = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "fit") {
|
||||||
|
this.fit = isFitType(newValue) ? newValue : "contain";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "mode") {
|
||||||
|
this.mode = isModeType(newValue) ? newValue : "inside";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "x-axis") {
|
||||||
|
let float = this.xAxis;
|
||||||
|
float = parseFloat(newValue);
|
||||||
|
this.xAxis = float;
|
||||||
|
}
|
||||||
|
if (name === "y-axis") {
|
||||||
|
let float = this.yAxis;
|
||||||
|
float = parseFloat(newValue);
|
||||||
|
this.yAxis = float;
|
||||||
|
}
|
||||||
|
if (name === "offset-x") {
|
||||||
|
let float = 0;
|
||||||
|
float = parseInt(newValue);
|
||||||
|
this.offsetX = float;
|
||||||
|
}
|
||||||
|
if (name === "offset-y") {
|
||||||
|
let float = 0;
|
||||||
|
float = parseInt(newValue);
|
||||||
|
this.offsetY = float;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "scale") {
|
||||||
|
let scaleFloat = 1;
|
||||||
|
scaleFloat = parseFloat(newValue);
|
||||||
|
this.scale = scaleFloat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "width") {
|
||||||
|
let widthFloat = 1;
|
||||||
|
widthFloat = parseFloat(newValue);
|
||||||
|
this.width = widthFloat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "height") {
|
||||||
|
let heightFloat = 1;
|
||||||
|
heightFloat = parseFloat(newValue);
|
||||||
|
this.height = heightFloat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "animation") {
|
||||||
|
this.animation = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "draggable") {
|
||||||
|
this.draggable = Boolean(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "debug") {
|
||||||
|
this.debug = Boolean(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate bounds of the current animation on track 0, then set it
|
||||||
|
public recalculateBounds() {
|
||||||
|
const { skeleton, state } = this;
|
||||||
|
if (!skeleton || !state) return;
|
||||||
|
const track = state.getCurrent(0);
|
||||||
|
const animation = track?.animation as (Animation | undefined);
|
||||||
|
const bounds = this.calculateAnimationViewport(animation);
|
||||||
|
this.setBounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the given bounds on the current skeleton
|
||||||
|
// bounds is used to center the skeleton in inside mode and as a input area for click events
|
||||||
|
public setBounds(bounds: Rectangle) {
|
||||||
|
const { skeleton } = this;
|
||||||
|
if (!skeleton) return;
|
||||||
|
bounds.x /= skeleton.scaleX;
|
||||||
|
bounds.y /= skeleton.scaleY;
|
||||||
|
bounds.width /= skeleton.scaleX;
|
||||||
|
bounds.height /= skeleton.scaleY;
|
||||||
|
this.bounds = bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeOverlay(): SpineWebComponentOverlay {
|
||||||
|
let overlay = document.querySelector("spine-overlay") as SpineWebComponentOverlay;
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
||||||
|
private async loadSkeleton() {
|
||||||
|
// if (this.identifier !== "TODELETE") return Promise.reject();
|
||||||
|
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = this;
|
||||||
|
const isBinary = skeletonPath.endsWith(".skel");
|
||||||
|
|
||||||
|
// TODO: when multiple component are loaded, they do no reuse the asset manager cache.
|
||||||
|
// TODO: we need to reuse the same texture atlas to allow batching when skeletons use the same texture
|
||||||
|
await Promise.all([
|
||||||
|
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
||||||
|
this.loadTextureAtlas(atlasPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const atlas = this.overlay.spineCanvas.assetManager.require(atlasPath);
|
||||||
|
const atlasLoader = new AtlasAttachmentLoader(atlas);
|
||||||
|
|
||||||
|
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
||||||
|
skeletonLoader.scale = scale;
|
||||||
|
|
||||||
|
const skeletonFile = this.overlay.spineCanvas.assetManager.require(skeletonPath);
|
||||||
|
const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile);
|
||||||
|
|
||||||
|
const skeleton = new Skeleton(skeletonData);
|
||||||
|
const animationStateData = new AnimationStateData(skeletonData);
|
||||||
|
const state = new AnimationState(animationStateData);
|
||||||
|
|
||||||
|
let animationData;
|
||||||
|
if (animation) {
|
||||||
|
state.setAnimation(0, animation, true);
|
||||||
|
animationData = animation ? skeleton.data.findAnimation(animation)! : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ideally we would know the dpi and the zoom, however they are combined
|
||||||
|
// to simplify we just assume that the user wants to load the skeleton at scale 1
|
||||||
|
// at the current browser zoom level
|
||||||
|
this.currentScaleDpi = window.devicePixelRatio;
|
||||||
|
// skeleton.scaleX = this.currentScaleDpi;
|
||||||
|
// skeleton.scaleY = this.currentScaleDpi;
|
||||||
|
|
||||||
|
this.skeleton = skeleton;
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
const bounds = this.calculateAnimationViewport(animationData);
|
||||||
|
this.bounds = bounds;
|
||||||
|
return { skeleton, state, bounds };
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHTMLElementReference(): HTMLElement {
|
||||||
|
return this.width <= 0 || this.width <= 0
|
||||||
|
? this.parentElement!
|
||||||
|
: this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(): void {
|
||||||
|
this.root.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
width: ${this.width}px;
|
||||||
|
height: ${this.height}px;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load assets utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
private async loadBinary(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.overlay.spineCanvas.assetManager.loadBinary(path,
|
||||||
|
(_, binary) => resolve(binary),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadJson(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.overlay.spineCanvas.assetManager.loadJson(path,
|
||||||
|
(_, object) => resolve(object),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadTextureAtlas(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.overlay.spineCanvas.assetManager.loadTextureAtlas(path,
|
||||||
|
(_, atlas) => resolve(atlas),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Other utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
private calculateAnimationViewport (animation?: Animation): Rectangle {
|
||||||
|
const renderer = this.overlay.spineCanvas.renderer;
|
||||||
|
const { skeleton } = this;
|
||||||
|
if (!skeleton) return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
skeleton.setToSetupPose();
|
||||||
|
|
||||||
|
let offset = new Vector2(), size = new Vector2();
|
||||||
|
const tempArray = new Array<number>(2);
|
||||||
|
if (!animation) {
|
||||||
|
skeleton.updateWorldTransform(Physics.update);
|
||||||
|
skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping());
|
||||||
|
return {
|
||||||
|
x: offset.x,
|
||||||
|
y: offset.y,
|
||||||
|
width: size.x,
|
||||||
|
height: size.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
|
||||||
|
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
|
||||||
|
for (let i = 0; i < steps; i++, time += stepTime) {
|
||||||
|
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn);
|
||||||
|
skeleton.updateWorldTransform(Physics.update);
|
||||||
|
skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping());
|
||||||
|
|
||||||
|
if (!isNaN(offset.x) && !isNaN(offset.y) && !isNaN(size.x) && !isNaN(size.y)) {
|
||||||
|
minX = Math.min(offset.x, minX);
|
||||||
|
maxX = Math.max(offset.x + size.x, maxX);
|
||||||
|
minY = Math.min(offset.y, minY);
|
||||||
|
maxY = Math.max(offset.y + size.y, maxY);
|
||||||
|
} else
|
||||||
|
console.error("Animation bounds are invalid: " + animation.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpineWebComponentOverlay extends HTMLElement {
|
||||||
|
private root: ShadowRoot;
|
||||||
|
|
||||||
|
public spineCanvas:SpineCanvas;
|
||||||
|
private div: HTMLDivElement;
|
||||||
|
private canvas:HTMLCanvasElement;
|
||||||
|
private fps: HTMLSpanElement;
|
||||||
|
|
||||||
|
public skeletonList = new Array<SpineWebComponentWidget>();
|
||||||
|
|
||||||
|
private resizeObserver:ResizeObserver;
|
||||||
|
private input: Input;
|
||||||
|
|
||||||
|
// how many pixels to add to the
|
||||||
|
private overflowTop = .1;
|
||||||
|
private overflowBottom = .2;
|
||||||
|
private overflowLeft = .1;
|
||||||
|
private overflowRight = .1;
|
||||||
|
private overflowLeftSize: number
|
||||||
|
private overflowTopSize: number;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.root = this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
this.div = document.createElement('div');
|
||||||
|
this.div.style.position = "absolute";
|
||||||
|
this.div.style.top = "0";
|
||||||
|
this.div.style.left = "0";
|
||||||
|
this.div.style.setProperty("pointer-events", "none");
|
||||||
|
this.div.style.overflow = "hidden"
|
||||||
|
// this.div.style.backgroundColor = "rgba(0, 255, 0, 0.3)";
|
||||||
|
|
||||||
|
this.root.appendChild(this.div);
|
||||||
|
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.div.appendChild(this.canvas);
|
||||||
|
this.canvas.style.position = "absolute";
|
||||||
|
this.canvas.style.top = "0";
|
||||||
|
this.canvas.style.left = "0";
|
||||||
|
this.canvas.style.setProperty("pointer-events", "none");
|
||||||
|
this.canvas.style.transform =`translate(0px,0px)`;
|
||||||
|
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
||||||
|
|
||||||
|
this.fps = document.createElement('span');
|
||||||
|
this.fps.style.position = "fixed";
|
||||||
|
this.fps.style.top = "0";
|
||||||
|
this.fps.style.left = "0";
|
||||||
|
this.root.appendChild(this.fps);
|
||||||
|
|
||||||
|
|
||||||
|
// resize and zoom
|
||||||
|
// TODO: should I use the resize event?
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.updateCanvasSize();
|
||||||
|
this.zoomHandler();
|
||||||
|
this.spineCanvas.renderer.resize(ResizeMode.Expand);
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(document.body);
|
||||||
|
|
||||||
|
this.updateCanvasSize();
|
||||||
|
this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth;
|
||||||
|
this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight;
|
||||||
|
|
||||||
|
this.zoomHandler();
|
||||||
|
|
||||||
|
// scroll
|
||||||
|
window.addEventListener('scroll', this.scrollHandler);
|
||||||
|
this.scrollHandler();
|
||||||
|
|
||||||
|
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
||||||
|
this.input = new Input(document.body, false);
|
||||||
|
this.setupDragUtility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSpineCanvasApp(): SpineCanvasApp {
|
||||||
|
const red = new Color(1, 0, 0, 1);
|
||||||
|
const green = new Color(0, 1, 0, 1);
|
||||||
|
const blue = new Color(0, 0, 1, 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
update: (canvas: SpineCanvas, delta: number) => {
|
||||||
|
this.skeletonList.forEach(({ skeleton, state, update }) => {
|
||||||
|
if (!skeleton || !state) return;
|
||||||
|
if (update) update(canvas, delta, skeleton, state)
|
||||||
|
else {
|
||||||
|
state.update(delta);
|
||||||
|
state.apply(skeleton);
|
||||||
|
skeleton.update(delta);
|
||||||
|
skeleton.updateWorldTransform(Physics.update);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.fps.innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
|
||||||
|
},
|
||||||
|
|
||||||
|
render: (canvas: SpineCanvas) => {
|
||||||
|
// canvas.clear(0, 0 , 0, 0);
|
||||||
|
let renderer = canvas.renderer;
|
||||||
|
renderer.begin();
|
||||||
|
|
||||||
|
const devicePixelRatio = window.devicePixelRatio;
|
||||||
|
const tempVector = new Vector3();
|
||||||
|
this.skeletonList.forEach((widget) => {
|
||||||
|
const { skeleton, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit } = widget;
|
||||||
|
if (!skeleton) return;
|
||||||
|
|
||||||
|
const divBounds = widget.getHTMLElementReference().getBoundingClientRect();
|
||||||
|
divBounds.x += this.overflowLeftSize;
|
||||||
|
divBounds.y += this.overflowTopSize;
|
||||||
|
|
||||||
|
// get the desired point into the the div (center by default) in world coordinate
|
||||||
|
const divX = divBounds.x + divBounds.width * (xAxis + .5);
|
||||||
|
const divY = divBounds.y + divBounds.height * (-yAxis + .5);
|
||||||
|
this.screenToWorld(tempVector, divX, divY);
|
||||||
|
|
||||||
|
let x = tempVector.x;
|
||||||
|
let y = tempVector.y;
|
||||||
|
if (mode === 'inside') {
|
||||||
|
let { x: ax, y: ay, width: aw, height: ah } = bounds!;
|
||||||
|
|
||||||
|
// scale ratio
|
||||||
|
const scaleWidth = divBounds.width * devicePixelRatio / aw;
|
||||||
|
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
||||||
|
|
||||||
|
let ratioW = skeleton.scaleX;
|
||||||
|
let ratioH = skeleton.scaleY;
|
||||||
|
|
||||||
|
if (fit === "fill") { // Fill the target box by distorting the source's aspect ratio.
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else if (fit === "fitWidth") {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
} else if (fit === "fitHeight") {
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else if (fit === "contain") {
|
||||||
|
// if scaled height is bigger than div height, use height ratio instead
|
||||||
|
if (ah * scaleWidth > divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
} else if (fit === "cover") {
|
||||||
|
if (ah * scaleWidth < divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
} else if (fit === "scaleDown") {
|
||||||
|
if (aw > divBounds.width * devicePixelRatio || ah > divBounds.height * devicePixelRatio) {
|
||||||
|
if (ah * scaleWidth > divBounds.height * devicePixelRatio){
|
||||||
|
ratioW = scaleHeight;
|
||||||
|
ratioH = scaleHeight;
|
||||||
|
} else {
|
||||||
|
ratioW = scaleWidth;
|
||||||
|
ratioH = scaleWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the center of the bounds
|
||||||
|
const boundsX = (ax + aw / 2) * ratioW;
|
||||||
|
const boundsY = (ay + ah / 2) * ratioH;
|
||||||
|
|
||||||
|
// get vertices offset: calculate the distance between div center and bounds center
|
||||||
|
x = tempVector.x - boundsX;
|
||||||
|
y = tempVector.y - boundsY;
|
||||||
|
|
||||||
|
if (fit !== "none") {
|
||||||
|
// scale the skeleton
|
||||||
|
skeleton.scaleX = ratioW;
|
||||||
|
skeleton.scaleY = ratioH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.worldOffsetX = x + offsetX + dragX;
|
||||||
|
widget.worldOffsetY = y + offsetY + dragY;
|
||||||
|
|
||||||
|
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
||||||
|
for (let i = 0; i < size; i+=vertexSize) {
|
||||||
|
vertices[i] = vertices[i] + widget.worldOffsetX;
|
||||||
|
vertices[i+1] = vertices[i+1] + widget.worldOffsetY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// drawing debug stuff
|
||||||
|
if (debug) {
|
||||||
|
// if (true) {
|
||||||
|
let { x: ax, y: ay, width: aw, height: ah } = bounds!;
|
||||||
|
|
||||||
|
// show bounds and its center
|
||||||
|
renderer.rect(false,
|
||||||
|
ax * skeleton.scaleX + widget.worldOffsetX,
|
||||||
|
ay * skeleton.scaleY + widget.worldOffsetY,
|
||||||
|
aw * skeleton.scaleX,
|
||||||
|
ah * skeleton.scaleY,
|
||||||
|
blue);
|
||||||
|
const bbCenterX = (ax + aw / 2) * skeleton.scaleX + widget.worldOffsetX;
|
||||||
|
const bbCenterY = (ay + ah / 2) * skeleton.scaleY + widget.worldOffsetY;
|
||||||
|
renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
|
||||||
|
|
||||||
|
// show skeleton root
|
||||||
|
const root = skeleton.getRootBone()!;
|
||||||
|
renderer.circle(true, root.x + widget.worldOffsetX, root.y + widget.worldOffsetY, 10, red);
|
||||||
|
|
||||||
|
// show shifted origin
|
||||||
|
const originX = widget.worldOffsetX - dragX - offsetX;
|
||||||
|
const originY = widget.worldOffsetY - dragY - offsetY;
|
||||||
|
renderer.circle(true, originX, originY, 10, green);
|
||||||
|
|
||||||
|
// show line from origin to bounds center
|
||||||
|
renderer.line(originX, originY, bbCenterX, bbCenterY, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.end();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupDragUtility() {
|
||||||
|
// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y
|
||||||
|
const tempVectorInput = new Vector3();
|
||||||
|
|
||||||
|
let prevX = 0;
|
||||||
|
let prevY = 0;
|
||||||
|
this.input.addListener({
|
||||||
|
down: (x, y, ev) => {
|
||||||
|
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
||||||
|
tempVectorInput.set(originalEvent.pageX - window.scrollX + this.overflowLeftSize, originalEvent.pageY - window.scrollY + this.overflowTopSize, 0);
|
||||||
|
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
this.skeletonList.forEach(widget => {
|
||||||
|
if (!widget.draggable) return;
|
||||||
|
|
||||||
|
const { worldOffsetX, worldOffsetY } = widget;
|
||||||
|
const bounds = widget.bounds!;
|
||||||
|
const skeleton = widget.skeleton!;
|
||||||
|
const newBounds: Rectangle = {
|
||||||
|
x: bounds.x * skeleton.scaleX + worldOffsetX,
|
||||||
|
y: bounds.y * skeleton.scaleY + worldOffsetY,
|
||||||
|
width: bounds.width * skeleton.scaleX,
|
||||||
|
height: bounds.height * skeleton.scaleY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inside(tempVectorInput, newBounds)) {
|
||||||
|
widget.dragging = true;
|
||||||
|
ev?.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
prevX = tempVectorInput.x;
|
||||||
|
prevY = tempVectorInput.y;
|
||||||
|
},
|
||||||
|
dragged: (x, y, ev) => {
|
||||||
|
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
||||||
|
tempVectorInput.set(originalEvent.pageX - window.scrollX + this.overflowLeftSize, originalEvent.pageY - window.scrollY + this.overflowTopSize, 0);
|
||||||
|
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
let dragX = tempVectorInput.x - prevX;
|
||||||
|
let dragY = tempVectorInput.y - prevY;
|
||||||
|
this.skeletonList.forEach(widget => {
|
||||||
|
if (widget.dragging) {
|
||||||
|
const skeleton = widget.skeleton!;
|
||||||
|
skeleton.physicsTranslate(dragX, dragY);
|
||||||
|
widget.dragX += dragX;
|
||||||
|
widget.dragY += dragY;
|
||||||
|
ev?.preventDefault();
|
||||||
|
ev?.stopPropagation()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
prevX = tempVectorInput.x;
|
||||||
|
prevY = tempVectorInput.y;
|
||||||
|
},
|
||||||
|
up: () => {
|
||||||
|
this.skeletonList.forEach(widget => {
|
||||||
|
widget.dragging = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resize/scroll utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
private updateCanvasSize() {
|
||||||
|
// resize canvas
|
||||||
|
this.resizeCanvas();
|
||||||
|
|
||||||
|
// recalculate overflow left and size since canvas size changed
|
||||||
|
// we could keep the initial values, avoid this and the translation below - even though we don't have a great gain
|
||||||
|
this.translateCanvas();
|
||||||
|
|
||||||
|
// temporarely remove the div to get the page size without considering the div
|
||||||
|
// this is necessary otherwise if the bigger element in the page is remove and the div
|
||||||
|
// was the second bigger element, now it would be the div to dtermine the page size
|
||||||
|
this.div.remove();
|
||||||
|
const { width, height } = this.getPageSize();
|
||||||
|
this.root.appendChild(this.div);
|
||||||
|
|
||||||
|
this.div.style.width = width + "px";
|
||||||
|
this.div.style.height = height + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeCanvas() {
|
||||||
|
const displayWidth = document.documentElement.clientWidth;
|
||||||
|
const displayHeight = document.documentElement.clientHeight;
|
||||||
|
this.canvas.style.width = displayWidth * (1 + (this.overflowLeft + this.overflowRight)) + "px";
|
||||||
|
this.canvas.style.height = displayHeight * (1 + (this.overflowTop + this.overflowBottom)) + "px";
|
||||||
|
if (this.spineCanvas) this.spineCanvas.renderer.resize(ResizeMode.Expand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollHandler = () => {
|
||||||
|
this.translateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateCanvas() {
|
||||||
|
const displayWidth = document.documentElement.clientWidth;
|
||||||
|
const displayHeight = document.documentElement.clientHeight;
|
||||||
|
|
||||||
|
this.overflowLeftSize = this.overflowLeft * displayWidth;
|
||||||
|
this.overflowTopSize = this.overflowTop * displayHeight;
|
||||||
|
|
||||||
|
const scrollPositionX = window.scrollX - this.overflowLeftSize;
|
||||||
|
const scrollPositionY = window.scrollY - this.overflowTopSize;
|
||||||
|
this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private zoomHandler = () => {
|
||||||
|
this.skeletonList.forEach((widget) => {
|
||||||
|
// inside mode scale automatically to fit the skeleton within its parent
|
||||||
|
if (widget.mode !== 'origin' && widget.fit !== 'none') return;
|
||||||
|
|
||||||
|
const skeleton = widget.skeleton;
|
||||||
|
if (!skeleton) return;
|
||||||
|
const scale = window.devicePixelRatio;
|
||||||
|
skeleton.scaleX = skeleton.scaleX / widget.currentScaleDpi * scale;
|
||||||
|
skeleton.scaleY = skeleton.scaleY / widget.currentScaleDpi * scale;
|
||||||
|
widget.currentScaleDpi = scale;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPageSize() {
|
||||||
|
// we need the bounding client rect otherwise decimals won't be returned
|
||||||
|
// this means that during zoom it might occurs that the div would be resized
|
||||||
|
// rounded 1px more making a scrollbar appear
|
||||||
|
return document.body.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Other utilities
|
||||||
|
*/
|
||||||
|
private screenToWorld(vec: Vector3, x: number, y: number) {
|
||||||
|
vec.set(x, y, 0);
|
||||||
|
this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inside = (point: { x: number; y: number }, rectangle: Rectangle): boolean => {
|
||||||
|
return (
|
||||||
|
point.x >= rectangle.x &&
|
||||||
|
point.x <= rectangle.x + rectangle.width &&
|
||||||
|
point.y >= rectangle.y &&
|
||||||
|
point.y <= rectangle.y + rectangle.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('spine-widget', SpineWebComponentWidget);
|
||||||
|
customElements.define('spine-overlay', SpineWebComponentOverlay);
|
||||||
|
|
||||||
|
export function getSpineWidget(identifier: string) {
|
||||||
|
return document.querySelector(`spine-widget[identifier=${identifier}]`);
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ export * from "./SkeletonDebugRenderer.js";
|
|||||||
export * from "./SkeletonRenderer.js";
|
export * from "./SkeletonRenderer.js";
|
||||||
export * from "./SpineCanvas.js";
|
export * from "./SpineCanvas.js";
|
||||||
export * from "./SpineCanvasOverlay.js";
|
export * from "./SpineCanvasOverlay.js";
|
||||||
|
export * from "./SpineWebComponent.js";
|
||||||
export * from "./Vector3.js";
|
export * from "./Vector3.js";
|
||||||
export * from "./WebGL.js";
|
export * from "./WebGL.js";
|
||||||
export * from "@esotericsoftware/spine-core";
|
export * from "@esotericsoftware/spine-core";
|
||||||
Loading…
x
Reference in New Issue
Block a user