mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
overlay 4
This commit is contained in:
parent
9d1109c9dc
commit
3b5d74e0e8
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
.spine-div {
|
.spine-div {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 20px;
|
/* padding: 20px; */
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.spacer {
|
.spacer {
|
||||||
@ -24,39 +24,64 @@
|
|||||||
#canvas {
|
#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>
|
</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.min.js"></script> -->
|
||||||
|
<script src="./spine-webgl.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>OverlayCanvas Example</h1>
|
<h1>Spine Canvas Overlay Example</h1>
|
||||||
|
|
||||||
<p>Scroll down to div.</p>
|
<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="spacer"></div>
|
<h2>Drag and resize me</h2>
|
||||||
|
<h3>Mode: inside</h3>
|
||||||
<div class="spine-div" div-spine>
|
<h4>Spineboy will be resize to remain into the div.</h4>
|
||||||
<h2>Spine Box 1</h2>
|
<h4>Skeleton cannot be reused (side effect on skeleton scale).</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<div id="spineboy2" class="spine-div" style="width: 50%; margin-left: 50%; touch-action:none" div-spine>
|
<div id="spineboy2" class="spine-div" style="width: 50%; margin-left: 50%; touch-action:none" div-spine2>
|
||||||
<h2>Spine Box 2 (drag me)</h2>
|
<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>
|
||||||
|
<h4>Skeleton can be reused.</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<div id="raptor" class="spine-div" style="width: 50%; margin-left: 50%; transition: transform 1s linear;" div-raptor>
|
<div id="spineboy3" class="spine-div" style="width: 50%; margin-left: 50%; touch-action:none" div-spine2>
|
||||||
<h2>Raptor Box</h2>
|
<h3>Skeleton of previous box is being reused here</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<div class="spine-div" style="width: 50%; margin-left: 20%;" div-celeste>
|
<div class="spine-div" div-spine3>
|
||||||
<h2>Celeste Box</h2>
|
<h3>Initializer with NodeList</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div 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>
|
||||||
|
<h3>Initializer with HTMLElement</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<p>End of content.</p>
|
<p>End of content.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -66,18 +91,161 @@
|
|||||||
const divs = document.querySelectorAll(`[div-spine]`);
|
const divs = document.querySelectorAll(`[div-spine]`);
|
||||||
const overlay = new spine.SpineCanvasOverlay();
|
const overlay = new spine.SpineCanvasOverlay();
|
||||||
|
|
||||||
const p = overlay.addSkeleton({
|
const p = overlay.addSkeleton(
|
||||||
atlasPath: "assets/spineboy-pma.atlas",
|
{
|
||||||
skeletonPath: "assets/spineboy-pro.skel",
|
atlasPath: "assets/spineboy-pma.atlas",
|
||||||
|
skeletonPath: "assets/spineboy-pro.skel",
|
||||||
|
scale: .5,
|
||||||
|
animation: 'walk',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: divs[0],
|
||||||
|
mode: 'inside',
|
||||||
|
showBounds: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const { skeleton, state } = await p;
|
||||||
|
state.setAnimation(0, "run", true);
|
||||||
|
overlay.recalculateBounds(skeleton, state);
|
||||||
|
}, 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,
|
scale: .5,
|
||||||
}, divs);
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: divs2[0],
|
||||||
|
mode: 'origin',
|
||||||
|
showBounds: true,
|
||||||
|
xAxis: .5,
|
||||||
|
yAxis: 1,
|
||||||
|
// offsetX: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: divs2[1],
|
||||||
|
mode: 'origin',
|
||||||
|
showBounds: true,
|
||||||
|
offsetX: 100,
|
||||||
|
offsetY: -50
|
||||||
|
},
|
||||||
|
],);
|
||||||
|
|
||||||
|
p2.then(({ state }) => state.setAnimation(1, "eyeblink", true));
|
||||||
|
|
||||||
|
const divs3 = document.querySelectorAll(`[div-spine3]`);
|
||||||
|
const p3 = overlay.addSkeleton({
|
||||||
|
atlasPath: "assets/raptor-pma.atlas",
|
||||||
|
skeletonPath: "assets/raptor-pro.skel",
|
||||||
|
animation: 'walk',
|
||||||
|
scale: .5,
|
||||||
|
}, divs3);
|
||||||
|
|
||||||
|
const divs4 = document.querySelectorAll(`[div-spine4]`);
|
||||||
|
const p4 = overlay.addSkeleton({
|
||||||
|
atlasPath: "assets/tank-pma.atlas",
|
||||||
|
skeletonPath: "assets/tank-pro.skel",
|
||||||
|
animation: 'shoot',
|
||||||
|
scale: .5,
|
||||||
|
}, divs4[0]);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// Drag utility
|
||||||
|
|
||||||
|
|
||||||
p.then(({ skeleton, state }) => {
|
function makeDraggable(element) {
|
||||||
state.setAnimation(0, "walk", true);
|
let isDragging = false;
|
||||||
})
|
let startX, startY;
|
||||||
|
let originalX, originalY;
|
||||||
|
|
||||||
|
element.addEventListener('pointerdown', startDragging);
|
||||||
|
document.addEventListener('pointermove', drag);
|
||||||
|
document.addEventListener('pointerup', stopDragging);
|
||||||
|
|
||||||
|
function startDragging(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target === document.getElementById('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)`;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDragging(e) {
|
||||||
|
isDragging = false;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
makeDraggable(document.getElementById('spineboy1'));
|
||||||
|
makeDraggable(document.getElementById('spineboy2'));
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// Resize utility
|
||||||
|
|
||||||
|
const resizableDiv = document.getElementById('spineboy1');
|
||||||
|
const resizeHandle = document.getElementById('resizeHandle');
|
||||||
|
let isResizing = false;
|
||||||
|
let startX, startY, startWidth, startHeight;
|
||||||
|
|
||||||
|
resizeHandle.addEventListener('pointerdown', initResize);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 stopResize() {
|
||||||
|
isResizing = false;
|
||||||
|
document.removeEventListener('pointermove', resize);
|
||||||
|
document.removeEventListener('pointerup', stopResize);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -27,21 +27,50 @@
|
|||||||
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, AnimationState, AnimationStateData, Physics, Vector3, ResizeMode, Color } from "./index.js";
|
import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData } from "./index.js";
|
||||||
|
|
||||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasApp}. The app loads
|
interface Rectangle {
|
||||||
* assets and initializes itself, then updates and renders its state at the screen refresh rate. */
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlaySkeletonOptions {
|
||||||
|
atlasPath: string,
|
||||||
|
skeletonPath: string,
|
||||||
|
scale: number,
|
||||||
|
animation?: string,
|
||||||
|
skeletonData?: SkeletonData,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlayHTMLOptions {
|
||||||
|
element: HTMLElement,
|
||||||
|
mode?: OverlayElementMode,
|
||||||
|
showBounds?: boolean,
|
||||||
|
offsetX?: number,
|
||||||
|
offsetY?: number,
|
||||||
|
xAxis?: number,
|
||||||
|
yAxis?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverlayElementMode = 'inside' | 'origin';
|
||||||
|
|
||||||
|
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasOverlay}. */
|
||||||
export class SpineCanvasOverlay {
|
export class SpineCanvasOverlay {
|
||||||
|
|
||||||
private spineCanvas:SpineCanvas;
|
private spineCanvas:SpineCanvas;
|
||||||
private canvas:HTMLCanvasElement;
|
private canvas:HTMLCanvasElement;
|
||||||
|
|
||||||
private skeletonList = new Array<{ skeleton: Skeleton, state: AnimationState, htmlElements: Array<HTMLElement>}>();
|
private skeletonList = new Array<{
|
||||||
|
skeleton: Skeleton,
|
||||||
|
state: AnimationState,
|
||||||
|
bounds: Rectangle,
|
||||||
|
htmlOptionsList: Array<OverlayHTMLOptions>,
|
||||||
|
}>();
|
||||||
|
|
||||||
private disposed = false;
|
private disposed = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
||||||
constructor () {
|
constructor () {
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
@ -60,11 +89,12 @@ export class SpineCanvasOverlay {
|
|||||||
resizeObserver.observe(document.body);
|
resizeObserver.observe(document.body);
|
||||||
|
|
||||||
const red = new Color(1, 0, 0, 1);
|
const red = new Color(1, 0, 0, 1);
|
||||||
|
const blue = new Color(0, 0, 1, 1);
|
||||||
const spineCanvasApp: SpineCanvasApp = {
|
const spineCanvasApp: SpineCanvasApp = {
|
||||||
|
|
||||||
update: (canvas: SpineCanvas, delta: number) => {
|
update: (canvas: SpineCanvas, delta: number) => {
|
||||||
this.skeletonList.forEach(({ skeleton, state, htmlElements }) => {
|
this.skeletonList.forEach(({ skeleton, state, htmlOptionsList }) => {
|
||||||
if (htmlElements.length === 0) return;
|
if (htmlOptionsList.length === 0) return;
|
||||||
state.update(delta);
|
state.update(delta);
|
||||||
state.apply(skeleton);
|
state.apply(skeleton);
|
||||||
skeleton.update(delta);
|
skeleton.update(delta);
|
||||||
@ -81,29 +111,73 @@ export class SpineCanvasOverlay {
|
|||||||
renderer.camera.worldToScreen(vec3, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
renderer.camera.worldToScreen(vec3, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
||||||
|
|
||||||
const devicePixelRatio = window.devicePixelRatio;
|
const devicePixelRatio = window.devicePixelRatio;
|
||||||
this.skeletonList.forEach(({ skeleton, htmlElements }) => {
|
const tempVector = new Vector3();
|
||||||
if (htmlElements.length === 0) return;
|
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
|
||||||
|
if (htmlOptionsList.length === 0) return;
|
||||||
|
|
||||||
htmlElements.forEach((div) => {
|
let { x: ax, y: ay, width: aw, height: ah } = bounds;
|
||||||
|
|
||||||
const bounds = div.getBoundingClientRect();
|
htmlOptionsList.forEach(({ element, mode, showBounds, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0 }) => {
|
||||||
const x = (bounds.x + window.scrollX - vec3.x) * devicePixelRatio;
|
|
||||||
const y = (bounds.y + window.scrollY - vec3.y) * devicePixelRatio;
|
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) => {
|
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
||||||
for (let i = 0; i < size; i+=vertexSize) {
|
for (let i = 0; i < size; i+=vertexSize) {
|
||||||
vertices[i] = vertices[i] + x;
|
vertices[i] = vertices[i] + x + offsetX;
|
||||||
vertices[i+1] = vertices[i+1] - y;
|
vertices[i+1] = vertices[i+1] + y + offsetY;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// show skeleton center (root)
|
|
||||||
const root = skeleton.getRootBone()!;
|
|
||||||
const vec3Root = new Vector3(root.x, root.y);
|
|
||||||
renderer.camera.worldToScreen(vec3Root, canvas.htmlCanvas.clientWidth, canvas.htmlCanvas.clientHeight);
|
|
||||||
const rootX = (vec3Root.x - vec3.x) * devicePixelRatio;
|
|
||||||
const rootY = (vec3Root.y - vec3.y) * devicePixelRatio;
|
|
||||||
renderer.circle(true, x + rootX, -y + rootY, 20, red);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -118,6 +192,7 @@ export class SpineCanvasOverlay {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Reject error
|
||||||
public async loadBinary(path: string) {
|
public async loadBinary(path: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.spineCanvas.assetManager.loadBinary(path, () => resolve(null));
|
this.spineCanvas.assetManager.loadBinary(path, () => resolve(null));
|
||||||
@ -137,10 +212,10 @@ export class SpineCanvasOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addSkeleton(
|
public async addSkeleton(
|
||||||
skeletonOptions: { atlasPath: string, skeletonPath: string, scale: number },
|
skeletonOptions: OverlaySkeletonOptions,
|
||||||
elements: Array<HTMLElement> = [],
|
htmlOptionsList: Array<OverlayHTMLOptions> | Array<HTMLElement> | HTMLElement | NodeList = [],
|
||||||
) {
|
) {
|
||||||
const { atlasPath, skeletonPath, scale } = skeletonOptions;
|
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput } = 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),
|
||||||
@ -154,17 +229,104 @@ export class SpineCanvasOverlay {
|
|||||||
skeletonLoader.scale = scale;
|
skeletonLoader.scale = scale;
|
||||||
|
|
||||||
const skeletonFile = this.spineCanvas.assetManager.require(skeletonPath);
|
const skeletonFile = this.spineCanvas.assetManager.require(skeletonPath);
|
||||||
const skeletonData = skeletonLoader.readSkeletonData(skeletonFile);
|
const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile);
|
||||||
|
|
||||||
const skeleton = new Skeleton(skeletonData);
|
const skeleton = new Skeleton(skeletonData);
|
||||||
const animationStateData = new AnimationStateData(skeletonData);
|
const animationStateData = new AnimationStateData(skeletonData);
|
||||||
const state = new AnimationState(animationStateData);
|
const state = new AnimationState(animationStateData);
|
||||||
|
|
||||||
this.skeletonList.push({ skeleton, state, htmlElements: [...elements] });
|
let animationData;
|
||||||
|
if (animation) {
|
||||||
|
state.setAnimation(0, animation, true);
|
||||||
|
animationData = animation ? skeleton.data.findAnimation(animation)! : undefined;
|
||||||
|
}
|
||||||
|
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
||||||
|
|
||||||
|
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 (htmlOptionsList.length > 0 && htmlOptionsList[0] instanceof HTMLElement) {
|
||||||
|
list = htmlOptionsList.map(element => ({ 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 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"
|
||||||
|
+ "This is because the skeleton is scaled to stay into the div."
|
||||||
|
+ "You can call addSkeleton several time (skeleton data can be reuse, if given).");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
mode,
|
||||||
|
showBounds,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.skeletonList.push({ skeleton, state, bounds, htmlOptionsList: mapList });
|
||||||
|
|
||||||
return { skeleton, state }
|
return { skeleton, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public recalculateBounds(skeleton: Skeleton, state: AnimationState) {
|
||||||
|
const track = state.getCurrent(0);
|
||||||
|
const animation = track?.animation as (Animation | undefined);
|
||||||
|
const bounds = this.calculateAnimationViewport(skeleton, animation);
|
||||||
|
bounds.x /= skeleton.scaleX;
|
||||||
|
bounds.y /= skeleton.scaleY;
|
||||||
|
bounds.width /= skeleton.scaleX;
|
||||||
|
bounds.height /= skeleton.scaleY;
|
||||||
|
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
||||||
|
if (element) {
|
||||||
|
element.bounds = bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateAnimationViewport (skeleton: Skeleton, animation?: Animation): Rectangle {
|
||||||
|
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, this.spineCanvas.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, this.spineCanvas.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateCanvasSize() {
|
private updateCanvasSize() {
|
||||||
const pageSize = this.getPageSize();
|
const pageSize = this.getPageSize();
|
||||||
@ -192,7 +354,7 @@ export class SpineCanvasOverlay {
|
|||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Disposes the app, so the update() and render() functions are no longer called. Calls the dispose() callback.*/
|
// TODO
|
||||||
dispose () {
|
dispose () {
|
||||||
this.disposed = true;
|
this.disposed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user