mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-13 18:48:44 +08:00
Loading
This commit is contained in:
parent
221e3f6624
commit
96282273f8
@ -36,6 +36,7 @@ export class AssetManagerBase implements Disposable {
|
||||
private textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture;
|
||||
private downloader: Downloader;
|
||||
private assets: StringMap<any> = {};
|
||||
private assetsLoaded: StringMap<Promise<any>> = {};
|
||||
private errors: StringMap<string> = {};
|
||||
private toLoad = 0;
|
||||
private loaded = 0;
|
||||
@ -89,10 +90,17 @@ export class AssetManagerBase implements Disposable {
|
||||
error: (path: string, message: string) => void = () => { }) {
|
||||
path = this.start(path);
|
||||
|
||||
this.downloader.downloadBinary(path, (data: Uint8Array): void => {
|
||||
this.success(success, path, data);
|
||||
}, (status: number, responseText: string): void => {
|
||||
this.error(error, path, `Couldn't load binary ${path}: status ${status}, ${responseText}`);
|
||||
if (this.reuseAssets(path, success, error)) return;
|
||||
|
||||
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
|
||||
this.downloader.downloadBinary(path, (data: Uint8Array): void => {
|
||||
this.success(success, path, data);
|
||||
resolve(data);
|
||||
}, (status: number, responseText: string): void => {
|
||||
const errorMsg = `Couldn't load binary ${path}: status ${status}, ${responseText}`;
|
||||
this.error(error, path, errorMsg);
|
||||
reject(errorMsg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,44 +119,80 @@ export class AssetManagerBase implements Disposable {
|
||||
loadJson (path: string,
|
||||
success: (path: string, object: object) => void = () => { },
|
||||
error: (path: string, message: string) => void = () => { }) {
|
||||
path = this.start(path);
|
||||
path = this.start(path);
|
||||
|
||||
this.downloader.downloadJson(path, (data: object): void => {
|
||||
this.success(success, path, data);
|
||||
}, (status: number, responseText: string): void => {
|
||||
this.error(error, path, `Couldn't load JSON ${path}: status ${status}, ${responseText}`);
|
||||
});
|
||||
if (this.reuseAssets(path, success, error)) return;
|
||||
|
||||
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
|
||||
this.downloader.downloadJson(path, (data: object): void => {
|
||||
this.success(success, path, data);
|
||||
resolve(data);
|
||||
}, (status: number, responseText: string): void => {
|
||||
const errorMsg = `Couldn't load JSON ${path}: status ${status}, ${responseText}`;
|
||||
this.error(error, path, errorMsg);
|
||||
reject(errorMsg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: refactor assetsLoaded and assets (we should probably merge them)
|
||||
reuseAssets(path: string,
|
||||
success: (path: string, data: any) => void = () => { },
|
||||
error: (path: string, message: string) => void = () => { }) {
|
||||
const loadedStatus = this.assetsLoaded[path];
|
||||
const alreadyExistsOrLoading = loadedStatus !== undefined;
|
||||
if (alreadyExistsOrLoading) {
|
||||
loadedStatus
|
||||
.then(data => this.success(success, path, data))
|
||||
.catch(errorMsg => this.error(error, path, errorMsg));
|
||||
}
|
||||
return alreadyExistsOrLoading;
|
||||
}
|
||||
|
||||
loadTexture (path: string,
|
||||
success: (path: string, texture: Texture) => void = () => { },
|
||||
error: (path: string, message: string) => void = () => { }) {
|
||||
path = this.start(path);
|
||||
|
||||
let isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
|
||||
let isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined';
|
||||
if (isWebWorker) {
|
||||
fetch(path, { mode: <RequestMode>"cors" }).then((response) => {
|
||||
if (response.ok) return response.blob();
|
||||
this.error(error, path, `Couldn't load image: ${path}`);
|
||||
return null;
|
||||
}).then((blob) => {
|
||||
return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null;
|
||||
}).then((bitmap) => {
|
||||
if (bitmap) this.success(success, path, this.textureLoader(bitmap));
|
||||
path = this.start(path);
|
||||
|
||||
if (this.reuseAssets(path, success, error)) return;
|
||||
|
||||
this.assetsLoaded[path] = new Promise<any>((resolve, reject) => {
|
||||
let isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
|
||||
let isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined';
|
||||
if (isWebWorker) {
|
||||
fetch(path, { mode: <RequestMode>"cors" }).then((response) => {
|
||||
if (response.ok) return response.blob();
|
||||
const errorMsg = `Couldn't load image: ${path}`;
|
||||
this.error(error, path, `Couldn't load image: ${path}`);
|
||||
reject(errorMsg);
|
||||
}).then((blob) => {
|
||||
return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null;
|
||||
}).then((bitmap) => {
|
||||
if (bitmap) {
|
||||
const texture = this.textureLoader(bitmap)
|
||||
this.success(success, path, texture);
|
||||
resolve(texture);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
let image = new Image();
|
||||
image.crossOrigin = "anonymous";
|
||||
image.onload = () => {
|
||||
const texture = this.textureLoader(image)
|
||||
this.success(success, path, texture);
|
||||
setTimeout(() => resolve(texture), 1000)
|
||||
// resolve(texture);
|
||||
};
|
||||
image.onerror = () => {
|
||||
const errorMsg = `Couldn't load image: ${path}`;
|
||||
this.error(error, path, errorMsg);
|
||||
reject(errorMsg);
|
||||
};
|
||||
if (this.downloader.rawDataUris[path]) path = this.downloader.rawDataUris[path];
|
||||
image.src = path;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let image = new Image();
|
||||
image.crossOrigin = "anonymous";
|
||||
image.onload = () => {
|
||||
this.success(success, path, this.textureLoader(image));
|
||||
};
|
||||
image.onerror = () => {
|
||||
this.error(error, path, `Couldn't load image: ${path}`);
|
||||
};
|
||||
if (this.downloader.rawDataUris[path]) path = this.downloader.rawDataUris[path];
|
||||
image.src = path;
|
||||
}
|
||||
}
|
||||
|
||||
loadTextureAtlas (path: string,
|
||||
@ -242,6 +286,7 @@ export class AssetManagerBase implements Disposable {
|
||||
export class Downloader {
|
||||
private callbacks: StringMap<Array<Function>> = {};
|
||||
rawDataUris: StringMap<string> = {};
|
||||
cacheTextures: Record<string, Texture> = {};
|
||||
|
||||
dataUriToString (dataUri: string) {
|
||||
if (!dataUri.startsWith("data:")) {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<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> -->
|
||||
<!-- <script src="./spine-webgl.min.js"></script> -->
|
||||
<title>JS Library Showcase</title>
|
||||
<style>
|
||||
* {
|
||||
@ -117,36 +117,6 @@
|
||||
</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 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
@ -196,7 +166,6 @@
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 2 //
|
||||
@ -214,7 +183,6 @@
|
||||
skeleton="assets/raptor-pro.skel"
|
||||
animation="walk"
|
||||
fit="fill"
|
||||
debug="true"
|
||||
></spine-widget>
|
||||
</div>
|
||||
<div class="split-right">
|
||||
@ -282,7 +250,52 @@
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// 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"
|
||||
height="200"
|
||||
width="200"
|
||||
></spine-widget>
|
||||
</div>
|
||||
<div class="split-right">
|
||||
If you want to manually size the Spine widget, specify the attributes <code>width</code> and <code>height</code> in pixels (without the px unit).
|
||||
</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"
|
||||
height="200"
|
||||
width="200"
|
||||
fit="fill"
|
||||
></spine-widget>`)
|
||||
</script>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// end section 1 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
|
||||
@ -444,7 +457,7 @@
|
||||
|
||||
// using js, access the skeleton and the state asynchronously
|
||||
(async () => {
|
||||
const widget = document.querySelector("spine-widget[identifier=raptor]");
|
||||
const widget = spine.getSpineWidget("raptor");
|
||||
const { state } = await widget.loadingPromise;
|
||||
let isRoaring = false;
|
||||
setInterval(() => {
|
||||
@ -510,6 +523,50 @@
|
||||
-->
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
// start section 7 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
<div id="section7" class="section vertical-split">
|
||||
|
||||
<div class="split-top split">
|
||||
<div class="split-left">
|
||||
Set the desired skin by using the <code>skin</code> attribute.
|
||||
</div>
|
||||
<div class="split-right" id="section7-element">
|
||||
<spine-widget
|
||||
atlas="assets/mix-and-match-pma.atlas"
|
||||
skeleton="assets/mix-and-match-pro.skel"
|
||||
animation="dance"
|
||||
skin="full-skins/girl-spring-dress"
|
||||
></spine-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
/////////////////////
|
||||
|
||||
204
spine-ts/spine-webgl/example/canvas7.html
Normal file
204
spine-ts/spine-webgl/example/canvas7.html
Normal file
@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="../dist/iife/spine-webgl.js"></script>
|
||||
<!-- <script src="./spine-webgl.min.js"></script> -->
|
||||
<title>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 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.json"
|
||||
animation="walk"
|
||||
scale=".125"
|
||||
fit="none"
|
||||
></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.json"
|
||||
animation="walk"
|
||||
fit="fill"
|
||||
></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 //
|
||||
/////////////////////
|
||||
-->
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -110,7 +110,7 @@ export class SpineCanvas {
|
||||
|
||||
let waitForAssets = () => {
|
||||
if (this.disposed) return;
|
||||
if (this.assetManager.isLoadingComplete()) {
|
||||
if (!config.app.loadAssets || this.assetManager.isLoadingComplete()) {
|
||||
if (this.assetManager.hasErrors()) {
|
||||
if (config.app.error) config.app.error(this, this.assetManager.getErrors());
|
||||
} else {
|
||||
|
||||
@ -1,651 +0,0 @@
|
||||
/******************************************************************************
|
||||
* 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,
|
||||
}
|
||||
|
||||
interface OverlaySkeletonOptions {
|
||||
atlas: string,
|
||||
skeleton: string,
|
||||
scale: number,
|
||||
animation?: string,
|
||||
skeletonData?: SkeletonData,
|
||||
update?: UpdateSpineFunction;
|
||||
}
|
||||
|
||||
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
||||
|
||||
interface OverlayHTMLOptions {
|
||||
identifier: string,
|
||||
createDivInElement?: boolean,
|
||||
mode?: OverlayElementMode,
|
||||
debug?: boolean,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
xAxis?: number,
|
||||
yAxis?: number,
|
||||
draggable?: boolean,
|
||||
}
|
||||
|
||||
type OverlayHTMLElement = Required<Omit<OverlayHTMLOptions, "identifier">> & { element: HTMLElement, scaleDpi: number, 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}. */
|
||||
export class SpineCanvasOverlay {
|
||||
|
||||
private spineCanvas:SpineCanvas;
|
||||
private canvas:HTMLCanvasElement;
|
||||
private input:Input;
|
||||
|
||||
private skeletonList = new Array<{
|
||||
skeleton: Skeleton,
|
||||
state: AnimationState,
|
||||
bounds: Rectangle,
|
||||
htmlOptionsList: Array<OverlayHTMLElement>,
|
||||
update?: UpdateSpineFunction,
|
||||
}>();
|
||||
|
||||
private resizeObserver:ResizeObserver;
|
||||
private disposed = false;
|
||||
|
||||
// how many pixels to add to the edges as parcentages (to avoid cut on edge during scrolling)
|
||||
private overflowTop = .1;
|
||||
private overflowBottom = .2;
|
||||
private overflowLeft = .1;
|
||||
private overflowRight = .1;
|
||||
private overflowLeftSize: number;
|
||||
private overflowTopSize: number;
|
||||
|
||||
private div: HTMLDivElement;
|
||||
|
||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
||||
constructor () {
|
||||
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.canvas = document.createElement('canvas');
|
||||
this.div.appendChild(this.canvas);
|
||||
document.body.appendChild(this.div);
|
||||
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
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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>,
|
||||
) {
|
||||
const { atlas, skeleton: skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
||||
const isBinary = skeletonPath.endsWith(".skel");
|
||||
await Promise.all([
|
||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
||||
this.loadTextureAtlas(atlas),
|
||||
]);
|
||||
|
||||
const atlasLoaded = this.spineCanvas.assetManager.require(atlas);
|
||||
const atlasLoader = new AtlasAttachmentLoader(atlasLoaded);
|
||||
|
||||
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
||||
skeletonLoader.scale = scale;
|
||||
|
||||
const skeletonFile = this.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;
|
||||
}
|
||||
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
||||
|
||||
const halfDpi = window.devicePixelRatio / 2;
|
||||
|
||||
const { identifier, createDivInElement = false, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, } = htmlOptionsList[0];
|
||||
|
||||
const mode = givenMode ?? 'inside';
|
||||
|
||||
const el = document.querySelector(`spine[identifier="${identifier}"]`) as HTMLElement;
|
||||
if (!el) {
|
||||
throw new Error("Element not found with identifier: " + identifier);
|
||||
}
|
||||
|
||||
let parent = el.parentElement;
|
||||
if (createDivInElement) {
|
||||
const width = el.getAttribute('width');
|
||||
const height = el.getAttribute('height');
|
||||
parent = el;
|
||||
parent.style.width = `${width}px`;
|
||||
parent.style.height = `${height}px`;
|
||||
parent.style.display = 'block';
|
||||
if (debug) parent.style.backgroundColor = "rgba(0, 0, 0, .5)";
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
throw new Error("Parent of element not found");
|
||||
}
|
||||
|
||||
console.log(el)
|
||||
console.log(parent)
|
||||
|
||||
const obj = {
|
||||
element: parent,
|
||||
createDivInElement,
|
||||
mode,
|
||||
debug,
|
||||
offsetX,
|
||||
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.scaleY = halfDpi;
|
||||
|
||||
this.skeletonList.push({ skeleton, state, update, bounds, htmlOptionsList: mapList });
|
||||
return { skeleton, state };
|
||||
}
|
||||
|
||||
// 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;
|
||||
bounds.height /= skeleton.scaleY;
|
||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
||||
if (element) {
|
||||
element.bounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) => {
|
||||
// canvas.clear(1, 0 , 0, .1);
|
||||
let renderer = canvas.renderer;
|
||||
renderer.begin();
|
||||
|
||||
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, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
|
||||
const divBounds = element.getBoundingClientRect();
|
||||
divBounds.x += this.overflowLeftSize;
|
||||
divBounds.y += this.overflowTopSize;
|
||||
|
||||
const fit: "fill" | "fitWidth" | "fitHeight" | "contain" | "cover" | "none" | "scaleDown" = "scaleDown";
|
||||
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 ratioW = 1;
|
||||
let ratioH = 1;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else if (fit === "none") {
|
||||
|
||||
}
|
||||
|
||||
// get the center of the bounds
|
||||
const boundsX = (ax + aw / 2) * ratioW;
|
||||
const boundsY = (ay + ah / 2) * ratioH;
|
||||
|
||||
// get the center of the div in world coordinate
|
||||
const divX = divBounds.x + divBounds.width / 2;
|
||||
const divY = divBounds.y - 1 + divBounds.height / 2;
|
||||
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 = ratioW;
|
||||
skeleton.scaleY = ratioH;
|
||||
} else {
|
||||
// get the center of the div in world coordinate
|
||||
const divX = divBounds.x + divBounds.width * xAxis;
|
||||
const divY = divBounds.y + divBounds.height * yAxis;
|
||||
this.screenToWorld(tempVector, divX, divY);
|
||||
|
||||
// get vertices offset
|
||||
x = tempVector.x;
|
||||
y = tempVector.y;
|
||||
}
|
||||
|
||||
|
||||
list.worldOffsetX = x + offsetX + dragX;
|
||||
list.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] + list.worldOffsetX;
|
||||
vertices[i+1] = vertices[i+1] + list.worldOffsetY;
|
||||
}
|
||||
});
|
||||
|
||||
// drawing debug stuff
|
||||
if (debug) {
|
||||
// if (true) {
|
||||
// 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 - 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(({ 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 - 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(({ 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/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();
|
||||
document.body.appendChild(this.div);
|
||||
|
||||
this.div.style.width = width + "px";
|
||||
this.div.style.height = height + "px";
|
||||
}
|
||||
|
||||
private scrollHandler = () => {
|
||||
this.translateCanvas();
|
||||
}
|
||||
|
||||
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 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(({ skeleton, htmlOptionsList }) => {
|
||||
htmlOptionsList.forEach((options) => {
|
||||
const { mode, scaleDpi } = options;
|
||||
// inside mode scale automatically to fit the skeleton within its parent
|
||||
if (mode !== 'origin') return;
|
||||
const halfDpi = window.devicePixelRatio / 2;
|
||||
const ratio = (skeleton.scaleX / scaleDpi) * halfDpi;
|
||||
skeleton.scaleX = ratio;
|
||||
skeleton.scaleY = ratio;
|
||||
options.scaleDpi = halfDpi;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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 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 screenToWorld(vec: Vector3, x: number, y: number) {
|
||||
vec.set(x, y, 0);
|
||||
this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// TODO
|
||||
dispose () {
|
||||
this.spineCanvas.dispose();
|
||||
this.canvas.remove();
|
||||
this.disposed = true;
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -13,7 +13,6 @@ export * from "./ShapeRenderer.js";
|
||||
export * from "./SkeletonDebugRenderer.js";
|
||||
export * from "./SkeletonRenderer.js";
|
||||
export * from "./SpineCanvas.js";
|
||||
export * from "./SpineCanvasOverlay.js";
|
||||
export * from "./SpineWebComponent.js";
|
||||
export * from "./Vector3.js";
|
||||
export * from "./WebGL.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user