mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 22:34:53 +08:00
overlay
This commit is contained in:
parent
3b5d74e0e8
commit
2da5b06c2d
@ -3,48 +3,44 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OverlayCanvas Example</title>
|
||||
<title>WebGL Overlay Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* top: 0;
|
||||
left: 0; */
|
||||
}
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.spine-div {
|
||||
border: 1px solid black;
|
||||
/* padding: 20px; */
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
position:relative;
|
||||
touch-action:none;
|
||||
}
|
||||
.spacer {
|
||||
height: 250px;
|
||||
}
|
||||
#canvas {
|
||||
will-change: transform;
|
||||
/* will-change: transform; */
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
background-color: #007bff;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
</style>
|
||||
<!-- <script src="../dist/iife/spine-webgl.js"></script> -->
|
||||
<script src="../dist/iife/spine-webgl.js"></script>
|
||||
<!-- <script src="./spine-webgl.min.js"></script> -->
|
||||
<script src="./spine-webgl.js"></script>
|
||||
<!-- <script src="./spine-webgl.js"></script> -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>Spine Canvas Overlay Example</h1>
|
||||
<span id="fps" style="position: fixed; top: 0; left: 0">a</span>
|
||||
|
||||
<div id="spineboy1" class="spine-div" style="width: 200px; height: 300px; margin-left: 200px; touch-action:none; position:relative;" div-spine>
|
||||
<div id="resizeHandle" class="resize-handle"></div>
|
||||
<div class="content">
|
||||
|
||||
<div id="spineboy1" class="spine-div" style="width: 200px; height: 300px; margin-left: 200px;" div-spine>
|
||||
<h2>Drag and resize me</h2>
|
||||
<h3>Mode: inside</h3>
|
||||
<h4>Spineboy will be resize to remain into the div.</h4>
|
||||
@ -53,7 +49,7 @@
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div id="spineboy2" class="spine-div" style="width: 50%; margin-left: 50%; touch-action:none" div-spine2>
|
||||
<div id="spineboy2" class="spine-div" style="width: 50%; margin-left: 50%;" div-spine2>
|
||||
<h2>Drag me</h2>
|
||||
<h3>Mode: origin</h3>
|
||||
<h4>You can easily change the position using offset or percentage of html element axis (origin is top-left)</h4>
|
||||
@ -62,32 +58,37 @@
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div id="spineboy3" class="spine-div" style="width: 50%; margin-left: 50%; touch-action:none" div-spine2>
|
||||
<div id="spineboy3" class="spine-div" style="width: 50%; margin-left: 50%;" div-spine2>
|
||||
<h3>Skeleton of previous box is being reused here</h3>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="spine-div" div-spine3>
|
||||
<div id="spineboy4" class="spine-div" div-spine3>
|
||||
<h3>Initializer with NodeList</h3>
|
||||
</div>
|
||||
<div class="spine-div" div-spine3>
|
||||
<div id="spineboy5" class="spine-div" div-spine3>
|
||||
<h3>Initializer with NodeList</h3>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="spine-div" style="width: 50%; height: 200px;" div-spine4>
|
||||
<div id="spineboy6" class="spine-div" style="width: 50%; height: 200px;" div-spine4>
|
||||
<h3>Initializer with HTMLElement</h3>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div id="spineboy7" class="spine-div" style="width: 50%; height: 200px;" div-spine5>
|
||||
<h3>Bounds using a Spine ounding box</h3>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<p>End of content.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
const divs = document.querySelectorAll(`[div-spine]`);
|
||||
const overlay = new spine.SpineCanvasOverlay();
|
||||
|
||||
@ -102,32 +103,33 @@
|
||||
{
|
||||
element: divs[0],
|
||||
mode: 'inside',
|
||||
showBounds: true,
|
||||
// showBounds: true,
|
||||
draggable: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const { skeleton, state } = await p;
|
||||
state.setAnimation(0, "run", true);
|
||||
overlay.recalculateBounds(skeleton, state);
|
||||
}, 1000)
|
||||
// setTimeout(async () => {
|
||||
// const { skeleton, state } = await p;
|
||||
// state.setAnimation(0, "portal", true);
|
||||
// overlay.recalculateBounds(skeleton);
|
||||
// }, 1000)
|
||||
|
||||
const divs2 = document.querySelectorAll(`[div-spine2]`);
|
||||
const p2 = overlay.addSkeleton({
|
||||
atlasPath: "assets/celestial-circus-pma.atlas",
|
||||
skeletonPath: "assets/celestial-circus-pro.skel",
|
||||
animation: 'swing',
|
||||
scale: .5,
|
||||
animation: 'wings-and-feet',
|
||||
scale: .25,
|
||||
},
|
||||
[
|
||||
{
|
||||
element: divs2[0],
|
||||
mode: 'origin',
|
||||
showBounds: true,
|
||||
// showBounds: true,
|
||||
xAxis: .5,
|
||||
yAxis: 1,
|
||||
// offsetX: 100
|
||||
draggable: true,
|
||||
},
|
||||
{
|
||||
element: divs2[1],
|
||||
@ -154,7 +156,68 @@
|
||||
skeletonPath: "assets/tank-pro.skel",
|
||||
animation: 'shoot',
|
||||
scale: .5,
|
||||
}, divs4[0]);
|
||||
}, [{
|
||||
element: divs4[0],
|
||||
showBounds: true,
|
||||
}]);
|
||||
|
||||
const divs5 = document.querySelectorAll(`[div-spine5]`);
|
||||
const p5 = overlay.addSkeleton({
|
||||
atlasPath: "assets/spineboy-pma.atlas",
|
||||
skeletonPath: "assets/spineboy-pro.skel",
|
||||
animation: 'walk',
|
||||
update: (_, delta, skeleton, state) => {
|
||||
state.update(delta / 3);
|
||||
state.apply(skeleton);
|
||||
skeleton.update(delta / 3);
|
||||
skeleton.updateWorldTransform(spine.Physics.update);
|
||||
}
|
||||
}, [{
|
||||
element: divs5[0],
|
||||
showBounds: true,
|
||||
mode: 'inside',
|
||||
draggable: true,
|
||||
}]);
|
||||
|
||||
p5.then(({ skeleton }) => {
|
||||
const bbAttachmentSlot = skeleton.findSlot("head-bb");
|
||||
const currentAttachment = bbAttachmentSlot.attachment;
|
||||
skeleton.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(skeleton, { x, y, width, height })
|
||||
bbAttachmentSlot.setAttachment(currentAttachment)
|
||||
})
|
||||
|
||||
|
||||
// setTimeout(async () => {
|
||||
// overlay.dispose();
|
||||
// }, 2000)
|
||||
|
||||
|
||||
makeResizable(document.getElementById('spineboy1'))
|
||||
// makeDraggable(document.getElementById('spineboy1'));
|
||||
makeResizable(document.getElementById('spineboy2'))
|
||||
makeDraggable(document.getElementById('spineboy2'));
|
||||
makeResizable(document.getElementById('spineboy7'))
|
||||
makeDraggable(document.getElementById('spineboy7'));
|
||||
makeResizable(document.getElementById('spineboy3'))
|
||||
makeDraggable(document.getElementById('spineboy3'));
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
@ -173,8 +236,7 @@
|
||||
document.addEventListener('pointerup', stopDragging);
|
||||
|
||||
function startDragging(e) {
|
||||
e.preventDefault();
|
||||
if (e.target === document.getElementById('resizeHandle')) return;
|
||||
if (e.target === element.querySelector('#resizeHandle')) return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
@ -198,17 +260,12 @@
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
element.style.transform = `translate(${originalX + deltaX}px, ${originalY + deltaY}px)`;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function stopDragging(e) {
|
||||
isDragging = false;
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
makeDraggable(document.getElementById('spineboy1'));
|
||||
makeDraggable(document.getElementById('spineboy2'));
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
@ -216,35 +273,50 @@
|
||||
//////////////////////////////////////////////////////
|
||||
// Resize utility
|
||||
|
||||
const resizableDiv = document.getElementById('spineboy1');
|
||||
const resizeHandle = document.getElementById('resizeHandle');
|
||||
let isResizing = false;
|
||||
let startX, startY, startWidth, startHeight;
|
||||
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";
|
||||
|
||||
resizeHandle.addEventListener('pointerdown', initResize);
|
||||
let isResizing = false;
|
||||
let startX, startY, startWidth, startHeight, startPaddingLeft, startPaddingRight, startPaddingTop, startPaddingBottom;
|
||||
|
||||
function initResize(e) {
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startWidth = resizableDiv.offsetWidth;
|
||||
startHeight = resizableDiv.offsetHeight;
|
||||
document.addEventListener('pointermove', resize);
|
||||
document.addEventListener('pointerup', stopResize);
|
||||
}
|
||||
resizeHandle.addEventListener('pointerdown', initResize);
|
||||
|
||||
function resize(e) {
|
||||
if (!isResizing) return;
|
||||
const width = startWidth + (e.clientX - startX);
|
||||
const height = startHeight + (e.clientY - startY);
|
||||
resizableDiv.style.width = width + 'px';
|
||||
resizableDiv.style.height = height + 'px';
|
||||
}
|
||||
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 stopResize() {
|
||||
isResizing = false;
|
||||
document.removeEventListener('pointermove', resize);
|
||||
document.removeEventListener('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);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
669
spine-ts/spine-webgl/example/canvas5.html
Normal file
669
spine-ts/spine-webgl/example/canvas5.html
Normal file
@ -0,0 +1,669 @@
|
||||
<!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>
|
||||
<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: 100vh; */
|
||||
height: 1000px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
background-color: #3498db;
|
||||
}
|
||||
.split {
|
||||
display: flex;
|
||||
}
|
||||
.split-left, .split-right {
|
||||
width: 50%;
|
||||
min-height: 50%;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
border: 1px solid salmon;
|
||||
}
|
||||
.navigation {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.nav-btn {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vertical-split {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.split-top, .split-bottom {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.split-top {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- <span id="fps" style="position: fixed; top: 0; left: 0">FPS</span> -->
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 1 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section1" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left" id="section1-element">
|
||||
</div>
|
||||
<div class="split-right">
|
||||
The skeleton origin is centered into the div by default.
|
||||
The skeleton will be scaled to fit the current animation into the div.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
const overlay = new spine.SpineCanvasOverlay();
|
||||
overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/spineboy-pma.atlas",
|
||||
skeletonPath: "assets/spineboy-pro.skel",
|
||||
animation: 'walk',
|
||||
},
|
||||
document.getElementById(`section1-element`),
|
||||
);
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 1 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 2 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section2" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left">
|
||||
Mode <code>origin</code> uses the HTML element top-left corner as origin for the skeleton. <br>
|
||||
You are responsible to scale the skeleton using this mode. <br>
|
||||
Move the origin by a percentage of the div width and height by using <code>xAxis</code> and <code>yAxis</code> respectively.
|
||||
</div>
|
||||
<div class="split-right" id="section2-element">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/spineboy-pma.atlas",
|
||||
skeletonPath: "assets/spineboy-pro.skel",
|
||||
animation: 'run',
|
||||
scale: .25,
|
||||
},
|
||||
{
|
||||
element: document.getElementById(`section2-element`),
|
||||
mode: 'origin',
|
||||
xAxis: .25,
|
||||
yAxis: .75,
|
||||
},
|
||||
);
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 2 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 3 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section3" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left" id="section3-element">
|
||||
</div>
|
||||
<div class="split-right">
|
||||
Use <code>offsetX</code> and <code>offsetY</code> to move you skeleton left or right by the pixel amount you specify.
|
||||
This works for both mode <code>origin</code> and <code>inside</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/spineboy-pma.atlas",
|
||||
skeletonPath: "assets/spineboy-pro.skel",
|
||||
animation: 'run',
|
||||
},
|
||||
{
|
||||
element: document.getElementById(`section3-element`),
|
||||
mode: 'inside', // default
|
||||
offsetX: 100,
|
||||
offsetY: 50,
|
||||
},
|
||||
);
|
||||
</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">
|
||||
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>
|
||||
If you change animation, you can ask to scale the skeleton based on the new animation.
|
||||
</div>
|
||||
<div class="split-right" id="section4-element">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
// access the skeleton and the state asynchronously
|
||||
const { skeleton, state } = await overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/raptor-pma.atlas",
|
||||
skeletonPath: "assets/raptor-pro.skel",
|
||||
animation: 'walk',
|
||||
},
|
||||
document.getElementById(`section4-element`)
|
||||
);
|
||||
|
||||
let isRoaring = false;
|
||||
setInterval(() => {
|
||||
const newAnimation = isRoaring ? "walk" : "roar";
|
||||
state.setAnimation(0, newAnimation, true);
|
||||
overlay.recalculateBounds(skeleton); // scale the skeleton based on the new animation
|
||||
isRoaring = !isRoaring;
|
||||
}, 4000);
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 4 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 5 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section5" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left">
|
||||
You can also set a custom bounds to center a specific element or area of you animation in the div.
|
||||
</div>
|
||||
<div class="split-right" id="section5-element">
|
||||
TODO
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 5 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 6 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section6" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left">
|
||||
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-right" id="section6-element">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
</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">
|
||||
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="section7-element">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="split-bottom">
|
||||
<pre><code id="code-display">
|
||||
overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/celestial-circus-pma.atlas",
|
||||
skeletonPath: "assets/celestial-circus-pro.skel",
|
||||
animation: 'wings-and-feet',
|
||||
},
|
||||
{
|
||||
element: document.getElementById(`section7-element`),
|
||||
draggable: true,
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 7 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
<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>
|
||||
</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>
|
||||
(async () => {
|
||||
const overlay = new spine.SpineCanvasOverlay();
|
||||
|
||||
overlay.addSkeleton(
|
||||
{
|
||||
atlasPath: "assets/spineboy-pma.atlas",
|
||||
skeletonPath: "assets/spineboy-pro.skel",
|
||||
// animation: 'walk',
|
||||
},
|
||||
{
|
||||
mode: "origin",
|
||||
xAxis: .5,
|
||||
yAxis: 1,
|
||||
element: document.querySelectorAll(`#section1-element`)[0]
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// /////////////////////
|
||||
// // start section 1 //
|
||||
// /////////////////////
|
||||
// overlay.addSkeleton(
|
||||
// {
|
||||
// atlasPath: "assets/spineboy-pma.atlas",
|
||||
// skeletonPath: "assets/spineboy-pro.skel",
|
||||
// animation: 'walk',
|
||||
// },
|
||||
// document.querySelectorAll(`#section1-element`),
|
||||
// );
|
||||
// /////////////////////
|
||||
// // end section 1 //
|
||||
// /////////////////////
|
||||
|
||||
|
||||
|
||||
// /////////////////////
|
||||
// // start section 2 //
|
||||
// /////////////////////
|
||||
// overlay.addSkeleton(
|
||||
// {
|
||||
// atlasPath: "assets/spineboy-pma.atlas",
|
||||
// skeletonPath: "assets/spineboy-pro.skel",
|
||||
// animation: 'run',
|
||||
// scale: .25,
|
||||
// },
|
||||
// {
|
||||
// element: document.getElementById(`section2-element`),
|
||||
// mode: 'origin',
|
||||
// xAxis: .25,
|
||||
// yAxis: .75,
|
||||
// },
|
||||
// );
|
||||
// /////////////////////
|
||||
// // 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 //
|
||||
// /////////////////////
|
||||
// const { skeleton, state } = await overlay.addSkeleton(
|
||||
// {
|
||||
// atlasPath: "assets/raptor-pma.atlas",
|
||||
// skeletonPath: "assets/raptor-pro.skel",
|
||||
// animation: 'walk',
|
||||
// },
|
||||
// document.getElementById(`section4-element`)
|
||||
// );
|
||||
|
||||
// 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 5 //
|
||||
// /////////////////////
|
||||
// /////////////////////
|
||||
// // 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 7 //
|
||||
// /////////////////////
|
||||
// overlay.addSkeleton(
|
||||
// {
|
||||
// atlasPath: "assets/celestial-circus-pma.atlas",
|
||||
// skeletonPath: "assets/celestial-circus-pro.skel",
|
||||
// animation: 'wings-and-feet',
|
||||
// },
|
||||
// {
|
||||
// element: document.getElementById(`section7-element`),
|
||||
// draggable: true,
|
||||
// }
|
||||
// );
|
||||
/////////////////////
|
||||
// end section 7 //
|
||||
/////////////////////
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
// Drag utility
|
||||
|
||||
|
||||
function makeDraggable(element) {
|
||||
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";
|
||||
|
||||
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>
|
||||
@ -36,10 +36,11 @@ export class Input {
|
||||
touch1: Touch | null = null;
|
||||
initialPinchDistance = 0;
|
||||
private listeners = new Array<InputListener>();
|
||||
private eventListeners: Array<{ target: any, event: any, func: any }> = [];
|
||||
private preventDefault: boolean;
|
||||
|
||||
constructor (element: HTMLElement) {
|
||||
constructor (element: HTMLElement, preventDefault = true) {
|
||||
this.element = element;
|
||||
this.preventDefault = preventDefault;
|
||||
this.setupCallbacks(element);
|
||||
}
|
||||
|
||||
@ -50,7 +51,7 @@ export class Input {
|
||||
this.mouseX = ev.clientX - rect.left;;
|
||||
this.mouseY = ev.clientY - rect.top;
|
||||
this.buttonDown = true;
|
||||
this.listeners.map((listener) => { if (listener.down) listener.down(this.mouseX, this.mouseY); });
|
||||
this.listeners.map((listener) => { if (listener.down) listener.down(this.mouseX, this.mouseY, ev); });
|
||||
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
document.addEventListener("mouseup", mouseUp);
|
||||
@ -60,12 +61,12 @@ export class Input {
|
||||
let mouseMove = (ev: UIEvent) => {
|
||||
if (ev instanceof MouseEvent) {
|
||||
let rect = element.getBoundingClientRect();
|
||||
this.mouseX = ev.clientX - rect.left;;
|
||||
this.mouseX = ev.clientX - rect.left;
|
||||
this.mouseY = ev.clientY - rect.top;
|
||||
|
||||
this.listeners.map((listener) => {
|
||||
if (this.buttonDown) {
|
||||
if (listener.dragged) listener.dragged(this.mouseX, this.mouseY);
|
||||
if (listener.dragged) listener.dragged(this.mouseX, this.mouseY, ev);
|
||||
} else {
|
||||
if (listener.moved) listener.moved(this.mouseX, this.mouseY);
|
||||
}
|
||||
@ -86,11 +87,11 @@ export class Input {
|
||||
}
|
||||
}
|
||||
|
||||
let mouseWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
let deltaY = e.deltaY;
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8;
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) deltaY *= 24;
|
||||
let mouseWheel = (ev: WheelEvent) => {
|
||||
if (this.preventDefault) ev.preventDefault();
|
||||
let deltaY = ev.deltaY;
|
||||
if (ev.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8;
|
||||
if (ev.deltaMode == WheelEvent.DOM_DELTA_PAGE) deltaY *= 24;
|
||||
this.listeners.map((listener) => { if (listener.wheel) listener.wheel(e.deltaY); });
|
||||
};
|
||||
|
||||
@ -115,7 +116,7 @@ export class Input {
|
||||
|
||||
if (!this.touch0) {
|
||||
this.touch0 = touch;
|
||||
this.listeners.map((listener) => { if (listener.down) listener.down(touch.x, touch.y) })
|
||||
this.listeners.map((listener) => { if (listener.down) listener.down(touch.x, touch.y, ev) })
|
||||
} else if (!this.touch1) {
|
||||
this.touch1 = touch;
|
||||
let dx = this.touch1.x - this.touch0.x;
|
||||
@ -124,8 +125,8 @@ export class Input {
|
||||
this.listeners.map((listener) => { if (listener.zoom) listener.zoom(this.initialPinchDistance, this.initialPinchDistance) });
|
||||
}
|
||||
}
|
||||
ev.preventDefault();
|
||||
}, false);
|
||||
if (this.preventDefault) ev.preventDefault();
|
||||
}, { passive: this.preventDefault });
|
||||
|
||||
element.addEventListener("touchmove", (ev: TouchEvent) => {
|
||||
if (this.touch0) {
|
||||
@ -139,7 +140,7 @@ export class Input {
|
||||
if (this.touch0.identifier === nativeTouch.identifier) {
|
||||
this.touch0.x = this.mouseX = x;
|
||||
this.touch0.y = this.mouseY = y;
|
||||
this.listeners.map((listener) => { if (listener.dragged) listener.dragged(x, y) });
|
||||
this.listeners.map((listener) => { if (listener.dragged) listener.dragged(x, y, ev) });
|
||||
}
|
||||
if (this.touch1 && this.touch1.identifier === nativeTouch.identifier) {
|
||||
this.touch1.x = this.mouseX = x;
|
||||
@ -153,8 +154,8 @@ export class Input {
|
||||
this.listeners.map((listener) => { if (listener.zoom) listener.zoom(this.initialPinchDistance, distance) });
|
||||
}
|
||||
}
|
||||
ev.preventDefault();
|
||||
}, false);
|
||||
if (this.preventDefault) ev.preventDefault();
|
||||
}, { passive: this.preventDefault });
|
||||
|
||||
let touchEnd = (ev: TouchEvent) => {
|
||||
if (this.touch0) {
|
||||
@ -190,7 +191,7 @@ export class Input {
|
||||
}
|
||||
}
|
||||
}
|
||||
ev.preventDefault();
|
||||
if (this.preventDefault) ev.preventDefault();
|
||||
};
|
||||
element.addEventListener("touchend", touchEnd, false);
|
||||
element.addEventListener("touchcancel", touchEnd);
|
||||
@ -214,10 +215,10 @@ export class Touch {
|
||||
}
|
||||
|
||||
export interface InputListener {
|
||||
down?(x: number, y: number): void;
|
||||
down?(x: number, y: number, ev?: MouseEvent | TouchEvent): void;
|
||||
up?(x: number, y: number): void;
|
||||
moved?(x: number, y: number): void;
|
||||
dragged?(x: number, y: number): void;
|
||||
dragged?(x: number, y: number, ev?: MouseEvent | TouchEvent): void;
|
||||
wheel?(delta: number): void;
|
||||
zoom?(initialDistance: number, distance: number): void;
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ export class SceneRenderer implements Disposable {
|
||||
}
|
||||
|
||||
begin () {
|
||||
// this.camera.update();
|
||||
this.camera.update();
|
||||
this.enableRenderer(this.batcher);
|
||||
}
|
||||
|
||||
|
||||
@ -203,6 +203,8 @@ export class SkeletonRenderer {
|
||||
|
||||
clipper.clipEndWithSlot(slot);
|
||||
}
|
||||
|
||||
// console.log(renderable.vertices[1])
|
||||
clipper.clipEnd();
|
||||
}
|
||||
|
||||
|
||||
@ -76,9 +76,6 @@ export class SpineCanvas {
|
||||
/** The input processor used to listen to mouse, touch, and keyboard events. */
|
||||
readonly input: Input;
|
||||
|
||||
public reqAnimationFrameId?:number;
|
||||
public loop: FrameRequestCallback;
|
||||
|
||||
private disposed = false;
|
||||
|
||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
||||
@ -96,16 +93,16 @@ export class SpineCanvas {
|
||||
|
||||
this.htmlCanvas = canvas;
|
||||
this.context = new ManagedWebGLRenderingContext(canvas, config.webglConfig);
|
||||
this.renderer = new SceneRenderer(canvas, this.context, false);
|
||||
this.renderer = new SceneRenderer(canvas, this.context);
|
||||
this.gl = this.context.gl;
|
||||
this.assetManager = new AssetManager(this.context, config.pathPrefix);
|
||||
this.input = new Input(canvas);
|
||||
|
||||
if (config.app.loadAssets) config.app.loadAssets(this);
|
||||
|
||||
this.loop = () => {
|
||||
let loop = () => {
|
||||
if (this.disposed) return;
|
||||
this.reqAnimationFrameId = requestAnimationFrame(this.loop);
|
||||
requestAnimationFrame(loop);
|
||||
this.time.update();
|
||||
if (config.app.update) config.app.update(this, this.time.delta);
|
||||
if (config.app.render) config.app.render(this);
|
||||
@ -118,13 +115,13 @@ export class SpineCanvas {
|
||||
if (config.app.error) config.app.error(this, this.assetManager.getErrors());
|
||||
} else {
|
||||
if (config.app.initialize) config.app.initialize(this);
|
||||
this.loop(0);
|
||||
loop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.reqAnimationFrameId = requestAnimationFrame(waitForAssets);
|
||||
requestAnimationFrame(waitForAssets);
|
||||
}
|
||||
this.reqAnimationFrameId = requestAnimationFrame(waitForAssets);
|
||||
requestAnimationFrame(waitForAssets);
|
||||
}
|
||||
|
||||
/** Clears the canvas with the given color. The color values are given in the range [0,1]. */
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* 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 } from "./index.js";
|
||||
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,
|
||||
@ -42,8 +42,11 @@ interface OverlaySkeletonOptions {
|
||||
scale: number,
|
||||
animation?: string,
|
||||
skeletonData?: SkeletonData,
|
||||
update?: UpdateSpineFunction;
|
||||
}
|
||||
|
||||
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
||||
|
||||
interface OverlayHTMLOptions {
|
||||
element: HTMLElement,
|
||||
mode?: OverlayElementMode,
|
||||
@ -52,8 +55,11 @@ interface OverlayHTMLOptions {
|
||||
offsetY?: number,
|
||||
xAxis?: number,
|
||||
yAxis?: number,
|
||||
draggable?: boolean,
|
||||
}
|
||||
|
||||
type OverlayHTMLElement = Required<OverlayHTMLOptions> & { element: HTMLElement, worldOffsetX: number, worldOffsetY: number, dragging: boolean, dragX: number, dragY: number };
|
||||
|
||||
type OverlayElementMode = 'inside' | 'origin';
|
||||
|
||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasOverlay}. */
|
||||
@ -61,161 +67,50 @@ export class SpineCanvasOverlay {
|
||||
|
||||
private spineCanvas:SpineCanvas;
|
||||
private canvas:HTMLCanvasElement;
|
||||
private input:Input;
|
||||
|
||||
private skeletonList = new Array<{
|
||||
skeleton: Skeleton,
|
||||
state: AnimationState,
|
||||
bounds: Rectangle,
|
||||
htmlOptionsList: Array<OverlayHTMLOptions>,
|
||||
htmlOptionsList: Array<OverlayHTMLElement>,
|
||||
update?: UpdateSpineFunction,
|
||||
}>();
|
||||
|
||||
private resizeObserver:ResizeObserver;
|
||||
private disposed = false;
|
||||
|
||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
||||
constructor () {
|
||||
this.canvas = document.createElement('canvas');
|
||||
document.body.appendChild(this.canvas); // adds the canvas to the body element
|
||||
document.body.appendChild(this.canvas);
|
||||
this.canvas.style.position = "absolute";
|
||||
this.canvas.style.top = "0";
|
||||
this.canvas.style.left = "0";
|
||||
this.canvas.style.display = "inline";
|
||||
this.canvas.style.setProperty("pointer-events", "none");
|
||||
// this.canvas.style.width = "100%";
|
||||
// this.canvas.style.height = "100%";
|
||||
this.updateCanvasSize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.updateCanvasSize();
|
||||
this.spineCanvas.renderer.resize(ResizeMode.Expand);
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
this.resizeObserver.observe(document.body);
|
||||
|
||||
const red = new Color(1, 0, 0, 1);
|
||||
const blue = new Color(0, 0, 1, 1);
|
||||
const spineCanvasApp: SpineCanvasApp = {
|
||||
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
||||
|
||||
update: (canvas: SpineCanvas, delta: number) => {
|
||||
this.skeletonList.forEach(({ skeleton, state, htmlOptionsList }) => {
|
||||
if (htmlOptionsList.length === 0) return;
|
||||
state.update(delta);
|
||||
state.apply(skeleton);
|
||||
skeleton.update(delta);
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
});
|
||||
},
|
||||
|
||||
render: (canvas: SpineCanvas) => {
|
||||
let renderer = canvas.renderer;
|
||||
renderer.begin();
|
||||
|
||||
// webgl canvas center
|
||||
const vec3 = new Vector3(0, 0);
|
||||
renderer.camera.worldToScreen(vec3, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio;
|
||||
const tempVector = new Vector3();
|
||||
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
|
||||
if (htmlOptionsList.length === 0) return;
|
||||
|
||||
let { x: ax, y: ay, width: aw, height: ah } = bounds;
|
||||
|
||||
htmlOptionsList.forEach(({ element, mode, showBounds, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0 }) => {
|
||||
|
||||
const divBounds = element.getBoundingClientRect();
|
||||
let x = 0, y = 0;
|
||||
if (mode === 'inside') {
|
||||
// scale ratio
|
||||
const scaleWidth = divBounds.width * devicePixelRatio / aw;
|
||||
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
||||
|
||||
// attempt to use width ratio
|
||||
let ratio = scaleWidth;
|
||||
let scaledW = aw * ratio;
|
||||
let scaledH = ah * ratio;
|
||||
|
||||
// if scaled height is bigger than div height, use height ratio instead
|
||||
if (scaledH > divBounds.height * devicePixelRatio) ratio = scaleHeight;
|
||||
|
||||
const scaledX = (ax + aw / 2) * ratio;
|
||||
const scaledY = (ay + ah / 2) * ratio;
|
||||
|
||||
const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
|
||||
const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
|
||||
|
||||
tempVector.set(divX, divY, 0);
|
||||
renderer.camera.screenToWorld(tempVector, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
||||
|
||||
x = tempVector.x - scaledX;
|
||||
y = tempVector.y - scaledY;
|
||||
|
||||
skeleton.scaleX = ratio;
|
||||
skeleton.scaleY = ratio;
|
||||
|
||||
if (showBounds) {
|
||||
renderer.circle(true, tempVector.x, tempVector.y, 10, blue);
|
||||
renderer.rect(false, ax * ratio + x + offsetX, ay * ratio + y + offsetY, aw * ratio, ah * ratio, blue);
|
||||
}
|
||||
|
||||
} else {
|
||||
const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
|
||||
const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
|
||||
|
||||
tempVector.set(divX, divY, 0);
|
||||
renderer.camera.screenToWorld(tempVector, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
||||
|
||||
x = tempVector.x;
|
||||
y = tempVector.y;
|
||||
|
||||
if (showBounds) {
|
||||
// show skeleton root
|
||||
const root = skeleton.getRootBone()!;
|
||||
renderer.circle(true, x + root.x + offsetX, y + root.y + offsetY, 10, red);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
||||
for (let i = 0; i < size; i+=vertexSize) {
|
||||
vertices[i] = vertices[i] + x + offsetX;
|
||||
vertices[i+1] = vertices[i+1] + y + offsetY;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Complete rendering.
|
||||
renderer.end();
|
||||
},
|
||||
}
|
||||
|
||||
this.spineCanvas = new SpineCanvas(this.canvas, {
|
||||
app: spineCanvasApp,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Reject error
|
||||
public async loadBinary(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadBinary(path, () => resolve(null));
|
||||
});
|
||||
}
|
||||
|
||||
public async loadJson(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadJson(path, () => resolve(null));
|
||||
});
|
||||
}
|
||||
|
||||
public async loadTextureAtlas(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadTextureAtlas(path, () => resolve(null));
|
||||
});
|
||||
this.input = new Input(document.body, false);
|
||||
this.setupDragUtility();
|
||||
}
|
||||
|
||||
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
||||
public async addSkeleton(
|
||||
skeletonOptions: OverlaySkeletonOptions,
|
||||
htmlOptionsList: Array<OverlayHTMLOptions> | Array<HTMLElement> | HTMLElement | NodeList = [],
|
||||
htmlOptionsList: Array<OverlayHTMLOptions> | OverlayHTMLOptions | Array<HTMLElement> | HTMLElement | NodeList = [],
|
||||
) {
|
||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput } = skeletonOptions;
|
||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
||||
const isBinary = skeletonPath.endsWith(".skel");
|
||||
await Promise.all([
|
||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
||||
@ -245,14 +140,15 @@ export class SpineCanvasOverlay {
|
||||
let list: Array<OverlayHTMLOptions>;
|
||||
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) {
|
||||
list = htmlOptionsList.map(element => ({ element: element } as OverlayHTMLOptions));
|
||||
list = htmlOptionsList.map(element => ({ element } as OverlayHTMLOptions));
|
||||
} else {
|
||||
list = htmlOptionsList as Array<OverlayHTMLOptions>;
|
||||
}
|
||||
|
||||
const mapList = list.map(({ element, mode: givenMode, showBounds = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0 }, i) => {
|
||||
const mapList = list.map(({ element, mode: givenMode, showBounds = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
|
||||
const mode = givenMode ?? 'inside';
|
||||
if (mode == 'inside' && i > 0) {
|
||||
console.warn("inside option works with multiple html elements only if the elements have the same dimension"
|
||||
@ -260,24 +156,39 @@ export class SpineCanvasOverlay {
|
||||
+ "You can call addSkeleton several time (skeleton data can be reuse, if given).");
|
||||
}
|
||||
return {
|
||||
element,
|
||||
element: element as HTMLElement,
|
||||
mode,
|
||||
showBounds,
|
||||
offsetX,
|
||||
offsetY,
|
||||
xAxis,
|
||||
yAxis,
|
||||
draggable,
|
||||
dragX: 0,
|
||||
dragY: 0,
|
||||
worldOffsetX: 0,
|
||||
worldOffsetY: 0,
|
||||
dragging: false,
|
||||
}
|
||||
});
|
||||
this.skeletonList.push({ skeleton, state, bounds, htmlOptionsList: mapList });
|
||||
this.skeletonList.push({ skeleton, state, update, bounds, htmlOptionsList: mapList });
|
||||
|
||||
return { skeleton, state }
|
||||
return { skeleton, state };
|
||||
}
|
||||
|
||||
public recalculateBounds(skeleton: Skeleton, state: AnimationState) {
|
||||
const track = state.getCurrent(0);
|
||||
// calculate bounds of the current animation on track 0, then set it
|
||||
public recalculateBounds(skeleton: Skeleton) {
|
||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
||||
if (!element) return;
|
||||
const track = element.state.getCurrent(0);
|
||||
const animation = track?.animation as (Animation | undefined);
|
||||
const bounds = this.calculateAnimationViewport(skeleton, animation);
|
||||
this.setBounds(skeleton, 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(skeleton: Skeleton, bounds: Rectangle) {
|
||||
bounds.x /= skeleton.scaleX;
|
||||
bounds.y /= skeleton.scaleY;
|
||||
bounds.width /= skeleton.scaleX;
|
||||
@ -288,6 +199,276 @@ export class SpineCanvasOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load assets utilities
|
||||
*/
|
||||
|
||||
public async loadBinary(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadBinary(path,
|
||||
(_, binary) => resolve(binary),
|
||||
(_, message) => reject(message),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async loadJson(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadJson(path,
|
||||
(_, object) => resolve(object),
|
||||
(_, message) => reject(message),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async loadTextureAtlas(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.spineCanvas.assetManager.loadTextureAtlas(path,
|
||||
(_, atlas) => resolve(atlas),
|
||||
(_, message) => reject(message),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Init utilities
|
||||
*/
|
||||
|
||||
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, htmlOptionsList }) => {
|
||||
if (htmlOptionsList.length === 0) return;
|
||||
if (update) update(canvas, delta, skeleton, state)
|
||||
else {
|
||||
state.update(delta);
|
||||
state.apply(skeleton);
|
||||
skeleton.update(delta);
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
}
|
||||
});
|
||||
// (document.body.querySelector("#fps")! as HTMLElement).innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
|
||||
},
|
||||
|
||||
render: (canvas: SpineCanvas) => {
|
||||
let renderer = canvas.renderer;
|
||||
renderer.begin();
|
||||
|
||||
// console.log(canvas.gl.getParameter(canvas.gl.MAX_RENDERBUFFER_SIZE));
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio;
|
||||
const tempVector = new Vector3();
|
||||
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
|
||||
if (htmlOptionsList.length === 0) return;
|
||||
|
||||
let { x: ax, y: ay, width: aw, height: ah } = bounds;
|
||||
|
||||
htmlOptionsList.forEach((list) => {
|
||||
const { element, mode, showBounds, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
|
||||
const divBounds = element.getBoundingClientRect();
|
||||
|
||||
// console.log(divBounds.x, divBounds.y, divBounds.width, divBounds.height)
|
||||
|
||||
let x = 0, y = 0;
|
||||
if (mode === 'inside') {
|
||||
// scale ratio
|
||||
const scaleWidth = divBounds.width * devicePixelRatio / aw;
|
||||
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
||||
|
||||
// attempt to use width ratio
|
||||
let ratio = scaleWidth;
|
||||
let scaledW = aw * ratio;
|
||||
let scaledH = ah * ratio;
|
||||
|
||||
// if scaled height is bigger than div height, use height ratio instead
|
||||
if (scaledH > divBounds.height * devicePixelRatio) ratio = scaleHeight;
|
||||
|
||||
// get the center of the bounds
|
||||
const boundsX = (ax + aw / 2) * ratio;
|
||||
const boundsY = (ay + ah / 2) * ratio;
|
||||
|
||||
// get the center of the div in world coordinate
|
||||
const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
|
||||
const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
|
||||
this.screenToWorld(tempVector, divX, divY);
|
||||
|
||||
// get vertices offset: calculate the distance between div center and bounds center
|
||||
x = tempVector.x - boundsX;
|
||||
y = tempVector.y - boundsY;
|
||||
|
||||
// scale the skeleton
|
||||
skeleton.scaleX = ratio;
|
||||
skeleton.scaleY = ratio;
|
||||
} else {
|
||||
|
||||
// TODO: window.devicePixelRatio to manage browser zoom
|
||||
|
||||
// get the center of the div in world coordinate
|
||||
const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
|
||||
const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
|
||||
this.screenToWorld(tempVector, divX, divY);
|
||||
// console.log(tempVector.x, tempVector.y)
|
||||
// console.log(window.devicePixelRatio)
|
||||
|
||||
// get vertices offset
|
||||
x = tempVector.x;
|
||||
y = tempVector.y;
|
||||
}
|
||||
|
||||
|
||||
list.worldOffsetX = x + offsetX + dragX;
|
||||
list.worldOffsetY = y + offsetY + dragY;
|
||||
|
||||
console.log(list.worldOffsetY)
|
||||
// console.log("----")
|
||||
|
||||
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
||||
for (let i = 0; i < size; i+=vertexSize) {
|
||||
vertices[i] = vertices[i] + list.worldOffsetX;
|
||||
vertices[i+1] = vertices[i+1] + list.worldOffsetY;
|
||||
}
|
||||
});
|
||||
|
||||
// drawing debug stuff
|
||||
if (showBounds) {
|
||||
// show bounds and its center
|
||||
renderer.rect(false,
|
||||
ax * skeleton.scaleX + list.worldOffsetX,
|
||||
ay * skeleton.scaleY + list.worldOffsetY,
|
||||
aw * skeleton.scaleX,
|
||||
ah * skeleton.scaleY,
|
||||
blue);
|
||||
const bbCenterX = (ax + aw / 2) * skeleton.scaleX + list.worldOffsetX;
|
||||
const bbCenterY = (ay + ah / 2) * skeleton.scaleY + list.worldOffsetY;
|
||||
renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
|
||||
|
||||
// show skeleton root
|
||||
const root = skeleton.getRootBone()!;
|
||||
renderer.circle(true, root.x + list.worldOffsetX, root.y + list.worldOffsetY, 10, red);
|
||||
|
||||
// show shifted origin
|
||||
const originX = list.worldOffsetX - dragX - offsetX;
|
||||
const originY = list.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();
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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, originalEvent.pageY, 0);
|
||||
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
|
||||
htmlOptionsList.forEach((element) => {
|
||||
if (!element.draggable) return;
|
||||
|
||||
const { worldOffsetX, worldOffsetY } = element;
|
||||
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 (this.inside(tempVectorInput, newBounds)) {
|
||||
element.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, originalEvent.pageY, 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(({ htmlOptionsList, bounds, skeleton }) => {
|
||||
htmlOptionsList.forEach((element) => {
|
||||
const { dragging } = element;
|
||||
|
||||
if (dragging) {
|
||||
skeleton.physicsTranslate(dragX, dragY);
|
||||
element.dragX += dragX;
|
||||
element.dragY += dragY;
|
||||
ev?.preventDefault();
|
||||
ev?.stopPropagation()
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
prevX = tempVectorInput.x;
|
||||
prevY = tempVectorInput.y;
|
||||
},
|
||||
up: () => {
|
||||
this.skeletonList.forEach(({ htmlOptionsList }) => {
|
||||
htmlOptionsList.forEach((element) => {
|
||||
element.dragging = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Resize utilities
|
||||
*/
|
||||
|
||||
private updateCanvasSize() {
|
||||
const pageSize = this.getPageSize();
|
||||
this.canvas.style.width = pageSize.width + "px";
|
||||
this.canvas.style.height = pageSize.height + "px";
|
||||
}
|
||||
|
||||
private getPageSize() {
|
||||
const width = Math.max(
|
||||
document.body.scrollWidth,
|
||||
document.documentElement.scrollWidth,
|
||||
document.body.offsetWidth,
|
||||
document.documentElement.offsetWidth,
|
||||
document.documentElement.clientWidth
|
||||
);
|
||||
|
||||
const height = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.offsetHeight,
|
||||
document.documentElement.clientHeight
|
||||
);
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/*
|
||||
* Other utilities
|
||||
*/
|
||||
|
||||
private calculateAnimationViewport (skeleton: Skeleton, animation?: Animation): Rectangle {
|
||||
skeleton.setToSetupPose();
|
||||
|
||||
@ -328,34 +509,26 @@ export class SpineCanvasOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
private updateCanvasSize() {
|
||||
const pageSize = this.getPageSize();
|
||||
this.canvas.style.width = pageSize.width + "px";
|
||||
this.canvas.style.height = pageSize.height + "px";
|
||||
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);
|
||||
// console.log(this.canvas.clientWidth, this.canvas.clientHeight);
|
||||
}
|
||||
|
||||
private getPageSize() {
|
||||
const width = Math.max(
|
||||
document.body.scrollWidth,
|
||||
document.documentElement.scrollWidth,
|
||||
document.body.offsetWidth,
|
||||
document.documentElement.offsetWidth,
|
||||
document.documentElement.clientWidth
|
||||
private 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
|
||||
);
|
||||
|
||||
const height = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.offsetHeight,
|
||||
document.documentElement.clientHeight
|
||||
);
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// TODO
|
||||
dispose () {
|
||||
this.spineCanvas.dispose();
|
||||
this.canvas.remove();
|
||||
this.disposed = true;
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user