Parcourir la source

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

Mario Zechner il y a 3 ans
Parent
commit
58f0bc0d0c

+ 2 - 0
CHANGELOG.md

@@ -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`.

+ 1 - 0
spine-ts/index.html

@@ -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>

+ 56 - 151
spine-ts/spine-canvas/example/index.html

@@ -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%
-	}
 
-	canvas {
-		position: absolute;
-		width: 100%;
-		height: 100%;
-	}
-</style>
+<head>
+	<!--<script src="https://unpkg.com/@esotericsoftware/[email protected].*/dist/iife/spine-canvas.js"></script>-->
+	<script src="../dist/iife/spine-canvas.js"></script>
+</head>
 
-<body>
-	<canvas id="canvas"></canvas>
+<body style="margin: 0; padding: 0;">
+	<canvas id="canvas" style="width: 100%; height: 100vh;"></canvas>
 </body>
-<script>
-
-	var lastFrameTime = Date.now() / 1000;
-	var canvas, context;
-	var assetManager;
-	var skeleton, state, bounds;
-	var skeletonRenderer;
 
-	var skelName = "spineboy-ess";
-	var animName = "walk";
+<script>
+	let lastFrameTime = Date.now() / 1000;
+	let canvas, context;
+	let assetManager;
+	let skeleton, animationState, bounds;
+	let skeletonRenderer;
 
-	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/");
 
-		assetManager.loadText(skelName + ".json");
-		assetManager.loadText(skelName.replace("-pro", "").replace("-ess", "") + ".atlas");
-		assetManager.loadTexture(skelName.replace("-pro", "").replace("-ess", "") + ".png");
+		// 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();
 
-		requestAnimationFrame(load);
-	}
+		// 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"));
 
-	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();
-
-		context.save();
-		context.setTransform(1, 0, 0, 1, 0, 0);
-		context.fillStyle = "#cccccc";
-		context.fillRect(0, 0, canvas.width, canvas.height);
-		context.restore();
-
-		state.update(delta);
-		state.apply(skeleton);
+		// 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);
 
-		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>

+ 111 - 0
spine-ts/spine-canvas/example/mouse-click.html

@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+	<!--<script src="https://unpkg.com/@esotericsoftware/[email protected].*/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>

+ 15 - 0
spine-ts/spine-core/src/AssetManagerBase.ts

@@ -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;
 	}

+ 9 - 0
spine-ts/spine-core/src/Skeleton.ts

@@ -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.