Added promise based AssetManager.loadAll(), Skeleton.getBoundsRect() helper method.

This commit is contained in:
Mario Zechner 2022-03-14 11:23:53 +01:00
parent f4a92fbfae
commit 58f0bc0d0c
6 changed files with 192 additions and 149 deletions

View File

@ -634,6 +634,8 @@
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
* Added `AssetManager.setRawDataURI(path, data)`. Allows to embed data URIs for skeletons, atlases and atlas page images directly in the HTML/JS without needing to load it from a separate file.
* Added `AssetManager.loadAll()` to allow Promise/async/await based waiting for completion of asset load. See the `spine-canvas` examples.
* Added `Skeleton.getBoundRect()` helper method to calculate the bouding rectangle of the current pose, returning the result as `{ x, y, width, height }`. Note that this method will create temporary objects which can add to garbage collection pressure.
### WebGL backend
* `Input` can now take a partially defined implementation of `InputListener`.

View File

@ -14,6 +14,7 @@
<li>Canvas</li>
<ul>
<li><a href="/spine-canvas/example">Example</a></li>
<li><a href="/spine-canvas/example/mouse-click.html">Mouse click</a></li>
</ul>
<li>Player</li>
<ul>

View File

@ -1,181 +1,86 @@
<!DOCTYPE html>
<html>
<script src="../dist/iife/spine-canvas.js"></script>
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
body,
html {
height: 100%
}
<head>
<!--<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.0.*/dist/iife/spine-canvas.js"></script>-->
<script src="../dist/iife/spine-canvas.js"></script>
</head>
canvas {
position: absolute;
width: 100%;
height: 100%;
}
</style>
<body>
<canvas id="canvas"></canvas>
<body style="margin: 0; padding: 0;">
<canvas id="canvas" style="width: 100%; height: 100vh;"></canvas>
</body>
<script>
let lastFrameTime = Date.now() / 1000;
let canvas, context;
let assetManager;
let skeleton, animationState, bounds;
let skeletonRenderer;
var lastFrameTime = Date.now() / 1000;
var canvas, context;
var assetManager;
var skeleton, state, bounds;
var skeletonRenderer;
var skelName = "spineboy-ess";
var animName = "walk";
function init() {
async function load() {
canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context = canvas.getContext("2d");
skeletonRenderer = new spine.SkeletonRenderer(context);
// enable debug rendering
skeletonRenderer.debugRendering = true;
// enable the triangle renderer, supports meshes, but may produce artifacts in some browsers
skeletonRenderer.triangleRendering = false;
assetManager = new spine.AssetManager("assets/");
// Load the assets.
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
assetManager.loadText("spineboy-ess.json");
assetManager.loadTextureAtlas("spineboy.atlas");
await assetManager.loadAll();
assetManager.loadText(skelName + ".json");
assetManager.loadText(skelName.replace("-pro", "").replace("-ess", "") + ".atlas");
assetManager.loadTexture(skelName.replace("-pro", "").replace("-ess", "") + ".png");
// Create the texture atlas and skeleton data.
let atlas = assetManager.require("spineboy.atlas");
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
let skeletonJson = new spine.SkeletonJson(atlasLoader);
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
requestAnimationFrame(load);
}
function load() {
if (assetManager.isLoadingComplete()) {
var data = loadSkeleton(skelName, animName, "default");
skeleton = data.skeleton;
state = data.state;
bounds = data.bounds;
requestAnimationFrame(render);
} else {
requestAnimationFrame(load);
}
}
function loadSkeleton(name, initialAnimation, skin) {
if (skin === undefined) skin = "default";
// Load the texture atlas using name.atlas and name.png from the AssetManager.
// The function passed to TextureAtlas is used to resolve relative paths.
atlas = new spine.TextureAtlas(assetManager.require(name.replace("-pro", "").replace("-ess", "") + ".atlas"));
atlas.setTextures(assetManager);
// Create a AtlasAttachmentLoader, which is specific to the WebGL backend.
atlasLoader = new spine.AtlasAttachmentLoader(atlas);
// Create a SkeletonJson instance for parsing the .json file.
var skeletonJson = new spine.SkeletonJson(atlasLoader);
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
var skeletonData = skeletonJson.readSkeletonData(assetManager.require(name + ".json"));
var skeleton = new spine.Skeleton(skeletonData);
skeleton.scaleY = -1;
var bounds = calculateBounds(skeleton);
skeleton.setSkinByName(skin);
// Create an AnimationState, and set the initial animation in looping mode.
var animationState = new spine.AnimationState(new spine.AnimationStateData(skeleton.data));
animationState.setAnimation(0, initialAnimation, true);
animationState.addListener({
event: function (trackIndex, event) {
// console.log("Event on track " + trackIndex + ": " + JSON.stringify(event));
},
complete: function (trackIndex, loopCount) {
// console.log("Animation on track " + trackIndex + " completed, loop count: " + loopCount);
},
start: function (trackIndex) {
// console.log("Animation on track " + trackIndex + " started");
},
end: function (trackIndex) {
// console.log("Animation on track " + trackIndex + " ended");
}
})
// Pack everything up and return to caller.
return { skeleton: skeleton, state: animationState, bounds: bounds };
}
function calculateBounds(skeleton) {
var data = skeleton.data;
// Instantiate a new skeleton based on the atlas and skeleton data.
skeleton = new spine.Skeleton(skeletonData);
skeleton.setToSetupPose();
skeleton.updateWorldTransform();
var offset = new spine.Vector2();
var size = new spine.Vector2();
skeleton.getBounds(offset, size, []);
return { offset: offset, size: size };
bounds = skeleton.getBoundsRect();
// Setup an animation state with a default mix of 0.2 seconds.
var animationStateData = new spine.AnimationStateData(skeleton.data);
animationStateData.defaultMix = 0.2;
animationState = new spine.AnimationState(animationStateData);
// Start rendering.
requestAnimationFrame(render);
}
function render() {
// Calculate the delta time between this and the last frame in seconds.
var now = Date.now() / 1000;
var delta = now - lastFrameTime;
lastFrameTime = now;
resize();
// Resize the canvas drawing buffer if the canvas CSS width and height changed
// and clear the canvas.
if (canvas.width != canvas.clientWidth || canvas.height != canvas.clientHeight) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.save();
context.setTransform(1, 0, 0, 1, 0, 0);
context.fillStyle = "#cccccc";
context.fillRect(0, 0, canvas.width, canvas.height);
context.restore();
// Center the skeleton and resize it so it fits inside the canvas.
skeleton.x = canvas.width / 2;
skeleton.y = canvas.height - canvas.height * 0.1;
let scale = canvas.height / bounds.height * 0.8;
skeleton.scaleX = scale;
skeleton.scaleY = -scale;
state.update(delta);
state.apply(skeleton);
// Update and apply the animation state, update the skeleton's
// world transforms and render the skeleton.
animationState.update(delta);
animationState.apply(skeleton);
skeleton.updateWorldTransform();
skeletonRenderer.draw(skeleton);
context.strokeStyle = "green";
context.beginPath();
context.moveTo(-1000, 0);
context.lineTo(1000, 0);
context.moveTo(0, -1000);
context.lineTo(0, 1000);
context.stroke();
requestAnimationFrame(render);
}
function resize() {
var w = canvas.clientWidth;
var h = canvas.clientHeight;
if (canvas.width != w || canvas.height != h) {
canvas.width = w;
canvas.height = h;
}
// magic
var centerX = bounds.offset.x + bounds.size.x / 2;
var centerY = bounds.offset.y + bounds.size.y / 2;
var scaleX = bounds.size.x / canvas.width;
var scaleY = bounds.size.y / canvas.height;
var scale = Math.max(scaleX, scaleY) * 1.2;
if (scale < 1) scale = 1;
var width = canvas.width * scale;
var height = canvas.height * scale;
context.setTransform(1, 0, 0, 1, 0, 0);
context.scale(1 / scale, 1 / scale);
context.translate(-centerX, -centerY);
context.translate(width / 2, height / 2);
}
(function () {
init();
}());
load();
</script>
</html>

View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html>
<head>
<!--<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.0.*/dist/iife/spine-canvas.js"></script>-->
<script src="../dist/iife/spine-canvas.js"></script>
</head>
<body style="margin: 0; padding: 0;">
<canvas id="canvas" style="width: 100%; height: 100vh;"></canvas>
</body>
<script>
let lastFrameTime = Date.now() / 1000;
let canvas, context;
let assetManager;
let skeleton, animationState, bounds;
let skeletonRenderer;
async function load() {
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
skeletonRenderer = new spine.SkeletonRenderer(context);
// Load the assets.
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
assetManager.loadText("spineboy-ess.json");
assetManager.loadTextureAtlas("spineboy.atlas");
await assetManager.loadAll();
// Create the texture atlas and skeleton data.
let atlas = assetManager.require("spineboy.atlas");
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
let skeletonJson = new spine.SkeletonJson(atlasLoader);
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
// Instantiate a new skeleton based on the atlas and skeleton data.
skeleton = new spine.Skeleton(skeletonData);
skeleton.setToSetupPose();
skeleton.updateWorldTransform();
bounds = skeleton.getBoundsRect();
// Setup an animation state with a default mix of 0.2 seconds.
var animationStateData = new spine.AnimationStateData(skeleton.data);
animationStateData.defaultMix = 0.2;
animationState = new spine.AnimationState(animationStateData);
// Add a click listener to the canvas which checks if Spineboy's head
// was clicked.
canvas.addEventListener('click', event => {
// Make the mouse click coordinates relative to the canvas.
let canvasRect = canvas.getBoundingClientRect();
var mouseX = event.x - canvasRect.x;
var mouseY = event.y - canvasRect.y;
// Find the "head" bone.
var headBone = skeleton.findBone("head");
// If the mouse pointer is within 100 pixels of the head bone, fire the jump animation event.
// Afterwards, loop the run animation.
if (pointInCircle(mouseX, mouseY, headBone.worldX, headBone.worldY, 100)) {
var jumpEntry = animationState.setAnimation(0, "jump", false);
var walkEntry = animationState.addAnimation(0, "run", true);
}
});
requestAnimationFrame(render);
}
function render() {
// Calculate the delta time between this and the last frame in seconds.
var now = Date.now() / 1000;
var delta = now - lastFrameTime;
lastFrameTime = now;
// Resize the canvas drawing buffer if the canvas CSS width and height changed
// and clear the canvas.
if (canvas.width != canvas.clientWidth || canvas.height != canvas.clientHeight) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
context.clearRect(0, 0, canvas.width, canvas.height);
// Center the skeleton and resize it so it fits inside the canvas.
skeleton.x = canvas.width / 2;
skeleton.y = canvas.height - canvas.height * 0.1;
let scale = canvas.height / bounds.height * 0.8;
skeleton.scaleX = scale;
skeleton.scaleY = -scale;
// Update and apply the animation state, update the skeleton's
// world transforms and render the skeleton.
animationState.update(delta);
animationState.apply(skeleton);
skeleton.updateWorldTransform();
skeletonRenderer.draw(skeleton);
requestAnimationFrame(render);
}
// Checks if the point given by x/y are within the circle.
function pointInCircle(x, y, circleX, circleY, circleRadius) {
var distX = x - circleX;
var distY = y - circleY;
return distX * distX + distY * distY <= circleRadius * circleRadius;
}
load();
</script>
</html>

View File

@ -65,6 +65,21 @@ export class AssetManagerBase implements Disposable {
if (callback) callback(path, message);
}
loadAll () {
let promise = new Promise((resolve: (assetManager: AssetManagerBase) => void, reject: (errors: StringMap<string>) => void) => {
let check = () => {
if (this.isLoadingComplete()) {
if (this.hasErrors()) reject(this.errors);
else resolve(this);
return;
}
requestAnimationFrame(check);
}
requestAnimationFrame(check);
});
return promise;
}
setRawDataURI (path: string, data: string) {
this.downloader.rawDataUris[this.pathPrefix + path] = data;
}

View File

@ -585,6 +585,15 @@ export class Skeleton {
return null;
}
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose as `{ x: number, y: number, width: number, height: number }`.
* Note that this method will create temporary objects which can add to garbage collection pressure. Use `getBounds()` if garbage collection is a concern. */
getBoundsRect () {
let offset = new Vector2();
let size = new Vector2();
this.getBounds(offset, size);
return { x: offset.x, y: offset.y, width: size.x, height: size.y };
}
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
* @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
* @param size An output value, the width and height of the AABB.