Procházet zdrojové kódy

[ts] AssetManager and Player updates to support sharing assets.

* Removed # parsing from SpinePlayer, replaced with config.jsonField.
* Refactored AssetMananger to split downloading from asset creation.
* Allow Downloader to be specified in SpinePlayer config so multiple players don't download the same assets.
* Added AssetManager#loadJson so the text is only parsed once.
* TextureAtlas is still parsed multiple times. :(
* AssetMananger clean up, catch more errors, log error, don't keep rendering after an error.
Nathan Sweet před 4 roky
rodič
revize
43cb6dfe22

+ 2 - 2
spine-ts/canvas/src/AssetManager.ts

@@ -31,8 +31,8 @@
 
 module spine.canvas {
 	export class AssetManager extends spine.AssetManager {
-		constructor (pathPrefix: string = "") {
-			super((image: HTMLImageElement) => { return new spine.canvas.CanvasTexture(image); }, pathPrefix);
+		constructor (pathPrefix: string = "", downloader: Downloader = null) {
+			super((image: HTMLImageElement) => { return new spine.canvas.CanvasTexture(image); }, pathPrefix, downloader);
 		}
 	}
 }

+ 122 - 111
spine-ts/core/src/AssetManager.ts

@@ -31,117 +31,91 @@ module spine {
 	export class AssetManager implements Disposable {
 		private pathPrefix: string;
 		private textureLoader: (image: HTMLImageElement) => any;
+		private downloader: Downloader;
 		private assets: Map<any> = {};
 		private errors: Map<string> = {};
 		private toLoad = 0;
 		private loaded = 0;
-		private rawDataUris: Map<string> = {};
 
-		constructor (textureLoader: (image: HTMLImageElement) => any, pathPrefix: string = "") {
+		constructor (textureLoader: (image: HTMLImageElement) => any, pathPrefix: string = "", downloader: Downloader = null) {
 			this.textureLoader = textureLoader;
 			this.pathPrefix = pathPrefix;
+			this.downloader = downloader || new Downloader();
 		}
 
-		private downloadText (url: string, success: (data: string) => void, error: (status: number, responseText: string) => void) {
-			let request = new XMLHttpRequest();
-			request.overrideMimeType("text/html");
-			if (this.rawDataUris[url]) url = this.rawDataUris[url];
-			request.open("GET", url, true);
-			request.onload = () => {
-				if (request.status == 200) {
-					success(request.responseText);
-				} else {
-					error(request.status, request.responseText);
-				}
-			}
-			request.onerror = () => {
-				error(request.status, request.responseText);
-			}
-			request.send();
+		private start (path: string): string {
+			this.toLoad++;
+			return this.pathPrefix + path;
 		}
 
-		private downloadBinary (url: string, success: (data: Uint8Array) => void, error: (status: number, responseText: string) => void) {
-			let request = new XMLHttpRequest();
-			if (this.rawDataUris[url]) url = this.rawDataUris[url];
-			request.open("GET", url, true);
-			request.responseType = "arraybuffer";
-			request.onload = () => {
-				if (request.status == 200) {
-					success(new Uint8Array(request.response as ArrayBuffer));
-				} else {
-					error(request.status, request.responseText);
-				}
-			}
-			request.onerror = () => {
-				error(request.status, request.responseText);
-			}
-			request.send();
+		private success (path: string, callback: (path: string, data: any) => void, asset: any) {
+			this.toLoad--;
+			this.loaded++;
+			this.assets[path] = asset;
+			if (callback) callback(path, asset);
+		}
+
+		private error (path: string, callback: (path: string, error: string) => void, message: string) {
+			this.toLoad--;
+			this.loaded++;
+			this.errors[path] = message;
+			if (callback) callback(path, message);
 		}
 
 		setRawDataURI(path: string, data: string) {
-			this.rawDataUris[this.pathPrefix + path] = data;
+			this.downloader.rawDataUris[this.pathPrefix + path] = data;
 		}
 
 		loadBinary(path: string,
 			success: (path: string, binary: Uint8Array) => void = null,
 			error: (path: string, error: string) => void = null) {
-			path = this.pathPrefix + path;
-			this.toLoad++;
+			path = this.start(path);
 
-			this.downloadBinary(path, (data: Uint8Array): void => {
-				this.assets[path] = data;
-				if (success) success(path, data);
-				this.toLoad--;
-				this.loaded++;
-			}, (state: number, responseText: string): void => {
-				this.errors[path] = `Couldn't load binary ${path}: status ${status}, ${responseText}`;
-				if (error) error(path, `Couldn't load binary ${path}: status ${status}, ${responseText}`);
-				this.toLoad--;
-				this.loaded++;
+			this.downloader.downloadBinary(path, (data: Uint8Array): void => {
+				this.success(path, success, data);
+			}, (status: number, responseText: string): void => {
+				this.error(path, error, `Couldn't load binary ${path}: status ${status}, ${responseText}`);
 			});
 		}
 
 		loadText(path: string,
 			success: (path: string, text: string) => void = null,
 			error: (path: string, error: string) => void = null) {
-			path = this.pathPrefix + path;
-			this.toLoad++;
+			path = this.start(path);
 
-			this.downloadText(path, (data: string): void => {
-				this.assets[path] = data;
-				if (success) success(path, data);
-				this.toLoad--;
-				this.loaded++;
-			}, (state: number, responseText: string): void => {
-				this.errors[path] = `Couldn't load text ${path}: status ${status}, ${responseText}`;
-				if (error) error(path, `Couldn't load text ${path}: status ${status}, ${responseText}`);
-				this.toLoad--;
-				this.loaded++;
+			this.downloader.downloadText(path, (data: string): void => {
+				this.success(path, success, data);
+			}, (status: number, responseText: string): void => {
+				this.error(path, error, `Couldn't load text ${path}: status ${status}, ${responseText}`);
+			});
+		}
+
+		loadJson(path: string,
+			success: (path: string, object: object) => void = null,
+			error: (path: string, error: string) => void = null) {
+			path = this.start(path);
+
+			this.downloader.downloadJson(path, (data: object): void => {
+				this.success(path, success, data);
+			}, (status: number, responseText: string): void => {
+				this.error(path, error, `Couldn't load JSON ${path}: status ${status}, ${responseText}`);
 			});
 		}
 
 		loadTexture (path: string,
 			success: (path: string, image: HTMLImageElement) => void = null,
 			error: (path: string, error: string) => void = null) {
-			path = this.pathPrefix + path;
-			let storagePath = path;
-			this.toLoad++;
+			path = this.start(path);
+
 			let img = new Image();
 			img.crossOrigin = "anonymous";
 			img.onload = (ev) => {
-				let texture = this.textureLoader(img);
-				this.assets[storagePath] = texture;
-				this.toLoad--;
-				this.loaded++;
-				if (success) success(path, img);
+				this.success(path, success, this.textureLoader(img));
 			}
 			img.onerror = (ev) => {
-				this.errors[path] = `Couldn't load image ${path}`;
-				this.toLoad--;
-				this.loaded++;
-				if (error) error(path, `Couldn't load image ${path}`);
+				this.error(path, error, `Couldn't load image ${path}`);
 			}
-			if (this.rawDataUris[path]) path = this.rawDataUris[path];
+			if (this.downloader.rawDataUris[path]) path = this.downloader.rawDataUris[path];
 			img.src = path;
 		}
 
@@ -149,11 +123,10 @@ module spine {
 			success: (path: string, atlas: TextureAtlas) => void = null,
 			error: (path: string, error: string) => void = null
 		) {
+			path = this.start(path);
 			let parent = path.lastIndexOf("/") >= 0 ? path.substring(0, path.lastIndexOf("/")) : "";
-			path = this.pathPrefix + path;
-			this.toLoad++;
 
-			this.downloadText(path, (atlasData: string): void => {
+			this.downloader.downloadText(path, (atlasData: string): void => {
 				let pagesLoaded: any = { count: 0 };
 				let atlasPages = new Array<string>();
 				try {
@@ -165,11 +138,7 @@ module spine {
 						return new FakeTexture(image);
 					});
 				} catch (e) {
-					let ex = e as Error;
-					this.errors[path] = `Couldn't load texture atlas ${path}: ${ex.message}`;
-					if (error) error(path, `Couldn't load texture atlas ${path}: ${ex.message}`);
-					this.toLoad--;
-					this.loaded++;
+					this.error(path, error, `Couldn't load texture atlas ${path}: ${e.message}`);
 					return;
 				}
 
@@ -181,57 +150,37 @@ module spine {
 						if (pagesLoaded.count == atlasPages.length) {
 							if (!pageLoadError) {
 								try {
-									let atlas = new TextureAtlas(atlasData, (path: string) => {
+									this.success(path, success, new TextureAtlas(atlasData, (path: string) => {
 										return this.get(parent == "" ? path : parent + "/" + path);
-									});
-									this.assets[path] = atlas;
-									if (success) success(path, atlas);
-									this.toLoad--;
-									this.loaded++;
+									}));
 								} catch (e) {
-									let ex = e as Error;
-									this.errors[path] = `Couldn't load texture atlas ${path}: ${ex.message}`;
-									if (error) error(path, `Couldn't load texture atlas ${path}: ${ex.message}`);
-									this.toLoad--;
-									this.loaded++;
+									this.error(path, error, `Couldn't load texture atlas ${path}: ${e.message}`);
 								}
-							} else {
-								this.errors[path] = `Couldn't load texture atlas page ${imagePath}} of atlas ${path}`;
-								if (error) error(path, `Couldn't load texture atlas page ${imagePath} of atlas ${path}`);
-								this.toLoad--;
-								this.loaded++;
-							}
+							} else
+								this.error(path, error, `Couldn't load texture atlas page ${imagePath}} of atlas ${path}`);
 						}
 					}, (imagePath: string, errorMessage: string) => {
 						pageLoadError = true;
 						pagesLoaded.count++;
 
-						if (pagesLoaded.count == atlasPages.length) {
-							this.errors[path] = `Couldn't load texture atlas page ${imagePath}} of atlas ${path}`;
-							if (error) error(path, `Couldn't load texture atlas page ${imagePath} of atlas ${path}`);
-							this.toLoad--;
-							this.loaded++;
-						}
+						if (pagesLoaded.count == atlasPages.length)
+							this.error(path, error, `Couldn't load texture atlas page ${imagePath}} of atlas ${path}`);
 					});
 				}
-			}, (state: number, responseText: string): void => {
-				this.errors[path] = `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`;
-				if (error) error(path, `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`);
-				this.toLoad--;
-				this.loaded++;
+			}, (status: number, responseText: string): void => {
+				this.error(path, error, `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`);
 			});
 		}
 
 		get (path: string) {
-			path = this.pathPrefix + path;
-			return this.assets[path];
+			return this.assets[this.pathPrefix + path];
 		}
 
 		remove (path: string) {
 			path = this.pathPrefix + path;
 			let asset = this.assets[path];
 			if ((<any>asset).dispose) (<any>asset).dispose();
-			this.assets[path] = null;
+			delete this.assets[path];
 		}
 
 		removeAll () {
@@ -266,4 +215,66 @@ module spine {
 			return this.errors;
 		}
 	}
+
+	export class Downloader {
+		private callbacks: Map<Array<Function>> = {};
+		rawDataUris: Map<string> = {};
+
+		downloadText (url: string, success: (data: string) => void, error: (status: number, responseText: string) => void) {
+			if (this.rawDataUris[url]) url = this.rawDataUris[url];
+			if (this.start(url, success, error)) return;
+			let request = new XMLHttpRequest();
+			request.overrideMimeType("text/html");
+			request.open("GET", url, true);
+			let done = () => {
+				this.finish(url, request.status, request.responseText);
+			};
+			request.onload = done;
+			request.onerror = done;
+			request.send();
+		}
+
+		downloadJson (url: string, success: (data: object) => void, error: (status: number, responseText: string) => void) {
+			this.downloadText(url, (data: string): void => {
+				success(JSON.parse(data));
+			}, error);
+		}
+
+		downloadBinary (url: string, success: (data: Uint8Array) => void, error: (status: number, responseText: string) => void) {
+			if (this.rawDataUris[url]) url = this.rawDataUris[url];
+			if (this.start(url, success, error)) return;
+			let request = new XMLHttpRequest();
+			request.open("GET", url, true);
+			request.responseType = "arraybuffer";
+			let onerror = () => {
+				this.finish(url, request.status, request.responseText);
+			};
+			request.onload = () => {
+				if (request.status == 200)
+					this.finish(url, 200, new Uint8Array(request.response as ArrayBuffer));
+				else
+					onerror();
+			};
+			request.onerror = onerror;
+			request.send();
+		}
+
+		private start (url: string, success: any, error: any) {
+			let callbacks = this.callbacks[url];
+			try {
+				if (callbacks) return true;
+				this.callbacks[url] = callbacks = [];
+			} finally {
+				callbacks.push(success, error);
+			}
+		}
+
+		private finish (url: string, status: number, data: any) {
+			let callbacks = this.callbacks[url];
+			delete this.callbacks[url];
+			let args = status == 200 ? [data] : [status, data];
+			for (let i = args.length - 1, n = callbacks.length; i < n; i += 2)
+				callbacks[i].apply(null, args);
+		}
+	}
 }

+ 158 - 159
spine-ts/player/src/Player.ts

@@ -40,13 +40,16 @@ module spine {
 	}
 
 	export interface SpinePlayerConfig {
-		/* the URL of the skeleton .json file */
+		/* The URL of the skeleton JSON file (.json). */
 		jsonUrl: string
 
-		/* the URL of the skeleton .skel file */
+		/* Optional: the name of a field in the JSON that holds the skeleton data. */
+		jsonField: string
+
+		/* The URL of the skeleton binary file (.skel). */
 		skelUrl: string
 
-		/* the URL of the skeleton .atlas file. Atlas page images are automatically resolved. */
+		/* The URL of the skeleton atlas file (.atlas). Atlas page images are automatically resolved. */
 		atlasUrl: string
 
 		/* Raw data URIs, mapping from a path to base 64 encoded raw data. When the player
@@ -132,6 +135,9 @@ module spine {
 
 		/* Optional: callback when the widget could not be loaded. */
 		error: (widget: SpinePlayer, msg: string) => void
+
+		/* Optional: the specified downloader is used for the player's asset manager, allowing multiple players to share assets. */
+		downloader: spine.Downloader
 	}
 
 	class Popup {
@@ -304,6 +310,7 @@ module spine {
 		private loadingScreen: spine.webgl.LoadingScreen;
 		private assetManager: spine.webgl.AssetManager;
 
+		public error: boolean;
 		// Whether the skeleton was loaded
 		public loaded: boolean;
 		// The loaded skeleton
@@ -326,8 +333,7 @@ module spine {
 		private stopRequestAnimationFrame = false;
 
 		constructor(parent: HTMLElement | string, private config: SpinePlayerConfig) {
-			if (typeof parent === "string") this.parent = document.getElementById(parent);
-			else this.parent = parent;
+			this.parent = typeof parent === "string" ? document.getElementById(parent) : parent;
 			this.parent.appendChild(this.render());
 		}
 
@@ -361,7 +367,7 @@ module spine {
 			if (typeof config.debug.meshes === "undefined") config.debug.meshes = false;
 
 			if (config.animations && config.animation) {
-				if (config.animations.indexOf(config.animation) < 0) throw new Error("Default animation '" + config.animation + "' is not contained in the list of selectable animations " + escapeHtml(JSON.stringify(this.config.animations)) + ".");
+				if (config.animations.indexOf(config.animation) < 0) throw new Error("Default animation '" + config.animation + "' is not contained in the list of selectable animations: " + escapeHtml(JSON.stringify(this.config.animations)));
 			}
 
 			if (config.skins && config.skin) {
@@ -370,19 +376,20 @@ module spine {
 
 			if (!config.controlBones) config.controlBones = [];
 
-			if (typeof config.showControls === "undefined")
-				config.showControls = true;
+			if (typeof config.showControls === "undefined") config.showControls = true;
 
-			if (typeof config.defaultMix === "undefined")
-				config.defaultMix = 0.25;
+			if (typeof config.defaultMix === "undefined") config.defaultMix = 0.25;
 
 			return config;
 		}
 
 		showError(error: string) {
+			if (this.error) return;
+			this.error = true;
+			console.log(error);
 			let errorDom = findWithClass(this.dom, "spine-player-error")[0];
 			errorDom.classList.remove("spine-player-hidden");
-			errorDom.innerHTML = `<p style="text-align: center; align-self: center;">${error}</p>`;
+			errorDom.innerHTML = '<p style="text-align: center; align-self: center;">' + error.replace("\n", "<br><br>") + '</p>';
 			this.config.error(this, error);
 		}
 
@@ -414,7 +421,7 @@ module spine {
 				this.config = this.validateConfig(config);
 			} catch (e) {
 				this.showError(e);
-				return dom
+				return dom;
 			}
 
 			try {
@@ -426,12 +433,12 @@ module spine {
 				this.sceneRenderer = new spine.webgl.SceneRenderer(this.canvas, this.context, true);
 				this.loadingScreen = new spine.webgl.LoadingScreen(this.sceneRenderer);
 			} catch (e) {
-				this.showError("Sorry, your browser does not support WebGL.<br><br>Please use the latest version of Firefox, Chrome, Edge, or Safari.");
+				this.showError("Sorry, your browser does not support WebGL.\nPlease use the latest version of Firefox, Chrome, Edge, or Safari.");
 				return dom;
 			}
 
 			// Load the assets
-			this.assetManager = new spine.webgl.AssetManager(this.context);
+			this.assetManager = new spine.webgl.AssetManager(this.context, "", config.downloader);
 			if (config.rawDataURIs) {
 				for (let path in config.rawDataURIs) {
 					let data = config.rawDataURIs[path];
@@ -439,11 +446,9 @@ module spine {
 				}
 			}
 			let jsonUrl = config.jsonUrl;
-			if (jsonUrl) {
-				let hash = jsonUrl.indexOf("#");
-				if (hash != -1) jsonUrl = jsonUrl.substr(0, hash);
-				this.assetManager.loadText(jsonUrl);
-			} else
+			if (jsonUrl)
+				this.assetManager.loadJson(jsonUrl);
+			else
 				this.assetManager.loadBinary(config.skelUrl);
 			this.assetManager.loadTextureAtlas(config.atlasUrl);
 			if (config.backgroundImage && config.backgroundImage.url)
@@ -597,9 +602,7 @@ module spine {
 			let rows = findWithClass(popup.dom, "spine-player-list")[0];
 			this.skeleton.data.animations.forEach((animation) => {
 				// skip animations not whitelisted if a whitelist is given
-				if (this.config.animations && this.config.animations.indexOf(animation.name) < 0) {
-					return;
-				}
+				if (this.config.animations && this.config.animations.indexOf(animation.name) < 0) return;
 
 				let row = createElement(/*html*/`
 					<li class="spine-player-list-item selectable">
@@ -727,125 +730,129 @@ module spine {
 		}
 
 		drawFrame (requestNextFrame = true) {
-			if (requestNextFrame && !this.stopRequestAnimationFrame) requestAnimationFrame(() => this.drawFrame());
-			let ctx = this.context;
-			let gl = ctx.gl;
-
-			// Clear the viewport
-			var doc = document as any;
-			var isFullscreen = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement;
-			let bg = new Color().setFromString(isFullscreen ? this.config.fullScreenBackgroundColor : this.config.backgroundColor);
-			gl.clearColor(bg.r, bg.g, bg.b, bg.a);
-			gl.clear(gl.COLOR_BUFFER_BIT);
-
-			// Display loading screen
-			this.loadingScreen.backgroundColor.setFromColor(bg);
-			this.loadingScreen.draw(this.assetManager.isLoadingComplete());
-
-			// Have we finished loading the asset? Then set things up
-			if (this.assetManager.isLoadingComplete() && !this.skeleton) this.loadSkeleton();
-
-			// Resize the canvas
-			this.sceneRenderer.resize(webgl.ResizeMode.Expand);
-
-			// Update and draw the skeleton
-			if (this.loaded) {
-				// Update animation and skeleton based on user selections
-				if (!this.paused && this.config.animation) {
-					this.time.update();
-					let delta = this.time.delta * this.speed;
-
-					let animationDuration = this.animationState.getCurrent(0).animation.duration;
-					this.playTime += delta;
-					while (this.playTime >= animationDuration && animationDuration != 0) {
-						this.playTime -= animationDuration;
-					}
-					this.playTime = Math.max(0, Math.min(this.playTime, animationDuration));
-					this.timelineSlider.setValue(this.playTime / animationDuration);
-
-					this.animationState.update(delta);
-					this.animationState.apply(this.skeleton);
-				}
+			try {
+				if (requestNextFrame && !this.stopRequestAnimationFrame && !this.error) requestAnimationFrame(() => this.drawFrame());
+				let ctx = this.context;
+				let gl = ctx.gl;
+
+				// Clear the viewport
+				var doc = document as any;
+				var isFullscreen = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement;
+				let bg = new Color().setFromString(isFullscreen ? this.config.fullScreenBackgroundColor : this.config.backgroundColor);
+				gl.clearColor(bg.r, bg.g, bg.b, bg.a);
+				gl.clear(gl.COLOR_BUFFER_BIT);
+
+				// Display loading screen
+				this.loadingScreen.backgroundColor.setFromColor(bg);
+				this.loadingScreen.draw(this.assetManager.isLoadingComplete());
+
+				// Have we finished loading the asset? Then set things up
+				if (this.assetManager.isLoadingComplete() && !this.skeleton) this.loadSkeleton();
+
+				// Resize the canvas
+				this.sceneRenderer.resize(webgl.ResizeMode.Expand);
+
+				// Update and draw the skeleton
+				if (this.loaded) {
+					// Update animation and skeleton based on user selections
+					if (!this.paused && this.config.animation) {
+						this.time.update();
+						let delta = this.time.delta * this.speed;
+
+						let animationDuration = this.animationState.getCurrent(0).animation.duration;
+						this.playTime += delta;
+						while (this.playTime >= animationDuration && animationDuration != 0) {
+							this.playTime -= animationDuration;
+						}
+						this.playTime = Math.max(0, Math.min(this.playTime, animationDuration));
+						this.timelineSlider.setValue(this.playTime / animationDuration);
 
-				this.skeleton.updateWorldTransform();
+						this.animationState.update(delta);
+						this.animationState.apply(this.skeleton);
+					}
 
-				let viewport = {
-					x: this.currentViewport.x - (this.currentViewport.padLeft as number),
-					y: this.currentViewport.y - (this.currentViewport.padBottom as number),
-					width: this.currentViewport.width + (this.currentViewport.padLeft as number) + (this.currentViewport.padRight as number),
-					height: this.currentViewport.height + (this.currentViewport.padBottom as number) + (this.currentViewport.padTop as number)
-				}
+					this.skeleton.updateWorldTransform();
 
-				let transitionAlpha = ((performance.now() - this.viewportTransitionStart) / 1000) / this.config.viewport.transitionTime;
-				if (this.previousViewport && transitionAlpha < 1) {
-					let oldViewport = {
-						x: this.previousViewport.x - (this.previousViewport.padLeft as number),
-						y: this.previousViewport.y - (this.previousViewport.padBottom as number),
-						width: this.previousViewport.width + (this.previousViewport.padLeft as number) + (this.previousViewport.padRight as number),
-						height: this.previousViewport.height + (this.previousViewport.padBottom as number) + (this.previousViewport.padTop as number)
+					let viewport = {
+						x: this.currentViewport.x - (this.currentViewport.padLeft as number),
+						y: this.currentViewport.y - (this.currentViewport.padBottom as number),
+						width: this.currentViewport.width + (this.currentViewport.padLeft as number) + (this.currentViewport.padRight as number),
+						height: this.currentViewport.height + (this.currentViewport.padBottom as number) + (this.currentViewport.padTop as number)
 					}
 
-					viewport = {
-						x: oldViewport.x + (viewport.x - oldViewport.x) * transitionAlpha,
-						y: oldViewport.y + (viewport.y - oldViewport.y) * transitionAlpha,
-						width: oldViewport.width + (viewport.width - oldViewport.width) * transitionAlpha,
-						height: oldViewport.height + (viewport.height - oldViewport.height) * transitionAlpha
+					let transitionAlpha = ((performance.now() - this.viewportTransitionStart) / 1000) / this.config.viewport.transitionTime;
+					if (this.previousViewport && transitionAlpha < 1) {
+						let oldViewport = {
+							x: this.previousViewport.x - (this.previousViewport.padLeft as number),
+							y: this.previousViewport.y - (this.previousViewport.padBottom as number),
+							width: this.previousViewport.width + (this.previousViewport.padLeft as number) + (this.previousViewport.padRight as number),
+							height: this.previousViewport.height + (this.previousViewport.padBottom as number) + (this.previousViewport.padTop as number)
+						}
+
+						viewport = {
+							x: oldViewport.x + (viewport.x - oldViewport.x) * transitionAlpha,
+							y: oldViewport.y + (viewport.y - oldViewport.y) * transitionAlpha,
+							width: oldViewport.width + (viewport.width - oldViewport.width) * transitionAlpha,
+							height: oldViewport.height + (viewport.height - oldViewport.height) * transitionAlpha
+						}
 					}
-				}
 
-				let viewportSize = this.scale(viewport.width, viewport.height, this.canvas.width, this.canvas.height);
+					let viewportSize = this.scale(viewport.width, viewport.height, this.canvas.width, this.canvas.height);
 
-				this.sceneRenderer.camera.zoom = viewport.width / viewportSize.x;
-				this.sceneRenderer.camera.position.x = viewport.x + viewport.width / 2;
-				this.sceneRenderer.camera.position.y = viewport.y + viewport.height / 2;
+					this.sceneRenderer.camera.zoom = viewport.width / viewportSize.x;
+					this.sceneRenderer.camera.position.x = viewport.x + viewport.width / 2;
+					this.sceneRenderer.camera.position.y = viewport.y + viewport.height / 2;
 
-				this.sceneRenderer.begin();
+					this.sceneRenderer.begin();
 
-				// Draw background image if given
-				if (this.config.backgroundImage && this.config.backgroundImage.url) {
-					let bgImage = this.assetManager.get(this.config.backgroundImage.url);
-					if (!(this.config.backgroundImage.hasOwnProperty("x") && this.config.backgroundImage.hasOwnProperty("y") && this.config.backgroundImage.hasOwnProperty("width") && this.config.backgroundImage.hasOwnProperty("height"))) {
-						this.sceneRenderer.drawTexture(bgImage, viewport.x, viewport.y, viewport.width, viewport.height);
-					} else {
-						this.sceneRenderer.drawTexture(bgImage, this.config.backgroundImage.x, this.config.backgroundImage.y, this.config.backgroundImage.width, this.config.backgroundImage.height);
+					// Draw background image if given
+					if (this.config.backgroundImage && this.config.backgroundImage.url) {
+						let bgImage = this.assetManager.get(this.config.backgroundImage.url);
+						if (!(this.config.backgroundImage.hasOwnProperty("x") && this.config.backgroundImage.hasOwnProperty("y") && this.config.backgroundImage.hasOwnProperty("width") && this.config.backgroundImage.hasOwnProperty("height"))) {
+							this.sceneRenderer.drawTexture(bgImage, viewport.x, viewport.y, viewport.width, viewport.height);
+						} else {
+							this.sceneRenderer.drawTexture(bgImage, this.config.backgroundImage.x, this.config.backgroundImage.y, this.config.backgroundImage.width, this.config.backgroundImage.height);
+						}
 					}
-				}
 
-				// Draw skeleton and debug output
-				this.sceneRenderer.drawSkeleton(this.skeleton, this.config.premultipliedAlpha);
-				this.sceneRenderer.skeletonDebugRenderer.drawBones = this.config.debug.bones;
-				this.sceneRenderer.skeletonDebugRenderer.drawBoundingBoxes = this.config.debug.bounds;
-				this.sceneRenderer.skeletonDebugRenderer.drawClipping = this.config.debug.clipping;
-				this.sceneRenderer.skeletonDebugRenderer.drawMeshHull = this.config.debug.hulls;
-				this.sceneRenderer.skeletonDebugRenderer.drawPaths = this.config.debug.paths;
-				this.sceneRenderer.skeletonDebugRenderer.drawRegionAttachments = this.config.debug.regions;
-				this.sceneRenderer.skeletonDebugRenderer.drawMeshTriangles = this.config.debug.meshes;
-				this.sceneRenderer.drawSkeletonDebug(this.skeleton, this.config.premultipliedAlpha);
-
-				// Render the selected bones
-				let controlBones = this.config.controlBones;
-				let selectedBones = this.selectedBones;
-				let skeleton = this.skeleton;
-				gl.lineWidth(2);
-				for (var i = 0; i < controlBones.length; i++) {
-					var bone = skeleton.findBone(controlBones[i]);
-					if (!bone) continue;
-					var colorInner = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_INNER : SpinePlayer.NON_HOVER_COLOR_INNER;
-					var colorOuter = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_OUTER : SpinePlayer.NON_HOVER_COLOR_OUTER;
-					this.sceneRenderer.circle(true, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorInner);
-					this.sceneRenderer.circle(false, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorOuter);
-				}
-				gl.lineWidth(1);
+					// Draw skeleton and debug output
+					this.sceneRenderer.drawSkeleton(this.skeleton, this.config.premultipliedAlpha);
+					this.sceneRenderer.skeletonDebugRenderer.drawBones = this.config.debug.bones;
+					this.sceneRenderer.skeletonDebugRenderer.drawBoundingBoxes = this.config.debug.bounds;
+					this.sceneRenderer.skeletonDebugRenderer.drawClipping = this.config.debug.clipping;
+					this.sceneRenderer.skeletonDebugRenderer.drawMeshHull = this.config.debug.hulls;
+					this.sceneRenderer.skeletonDebugRenderer.drawPaths = this.config.debug.paths;
+					this.sceneRenderer.skeletonDebugRenderer.drawRegionAttachments = this.config.debug.regions;
+					this.sceneRenderer.skeletonDebugRenderer.drawMeshTriangles = this.config.debug.meshes;
+					this.sceneRenderer.drawSkeletonDebug(this.skeleton, this.config.premultipliedAlpha);
+
+					// Render the selected bones
+					let controlBones = this.config.controlBones;
+					let selectedBones = this.selectedBones;
+					let skeleton = this.skeleton;
+					gl.lineWidth(2);
+					for (var i = 0; i < controlBones.length; i++) {
+						var bone = skeleton.findBone(controlBones[i]);
+						if (!bone) continue;
+						var colorInner = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_INNER : SpinePlayer.NON_HOVER_COLOR_INNER;
+						var colorOuter = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_OUTER : SpinePlayer.NON_HOVER_COLOR_OUTER;
+						this.sceneRenderer.circle(true, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorInner);
+						this.sceneRenderer.circle(false, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorOuter);
+					}
+					gl.lineWidth(1);
 
-				// Render the viewport bounds
-				if (this.config.viewport.debugRender) {
-					this.sceneRenderer.rect(false, this.currentViewport.x, this.currentViewport.y, this.currentViewport.width, this.currentViewport.height, Color.GREEN);
-					this.sceneRenderer.rect(false, viewport.x, viewport.y, viewport.width, viewport.height, Color.RED);
-				}
+					// Render the viewport bounds
+					if (this.config.viewport.debugRender) {
+						this.sceneRenderer.rect(false, this.currentViewport.x, this.currentViewport.y, this.currentViewport.width, this.currentViewport.height, Color.GREEN);
+						this.sceneRenderer.rect(false, viewport.x, viewport.y, viewport.width, viewport.height, Color.RED);
+					}
 
-				this.sceneRenderer.end();
+					this.sceneRenderer.end();
 
-				this.sceneRenderer.camera.zoom = 0;
+					this.sceneRenderer.camera.zoom = 0;
+				}
+			} catch (e) {
+				this.showError(`Error: Unable to render skeleton.\n${e.message}`);
 			}
 		}
 
@@ -861,9 +868,10 @@ module spine {
 
 		loadSkeleton () {
 			if (this.loaded) return;
+			if (this.error) return;
 
 			if (this.assetManager.hasErrors()) {
-				this.showError("Error: Assets could not be loaded.<br><br>" + escapeHtml(JSON.stringify(this.assetManager.getErrors())));
+				this.showError("Error: Assets could not be loaded.\n" + escapeHtml(JSON.stringify(this.assetManager.getErrors())));
 				return;
 			}
 
@@ -871,19 +879,17 @@ module spine {
 			let skeletonData: SkeletonData;
 			let jsonUrl = this.config.jsonUrl;
 			if (jsonUrl) {
-				let hash = jsonUrl.indexOf("#");
-				let field = null;
-				if (hash != -1) {
-					field = jsonUrl.substr(hash + 1);
-					jsonUrl = jsonUrl.substr(0, hash);
-				}
-				let jsonText = this.assetManager.get(jsonUrl);
-				if (field) jsonText = JSON.parse(jsonText)[field];
-				let json = new SkeletonJson(new AtlasAttachmentLoader(atlas));
 				try {
-					skeletonData = json.readSkeletonData(jsonText);
+					let jsonData = this.assetManager.get(jsonUrl);
+					if (!jsonData) throw new Error("Empty JSON data.");
+					if (this.config.jsonField) {
+						jsonData = jsonData[this.config.jsonField];
+						if (!jsonData) throw new Error("JSON field not found: " + this.config.jsonField);
+					}
+					let json = new SkeletonJson(new AtlasAttachmentLoader(atlas));
+					skeletonData = json.readSkeletonData(jsonData);
 				} catch (e) {
-					this.showError("Error: Could not load skeleton JSON.<br><br>" + e.toString());
+					this.showError(`Error: Could not load skeleton JSON.\n${e.message}`);
 					return;
 				}
 			} else {
@@ -892,7 +898,7 @@ module spine {
 				try {
 					skeletonData = binary.readSkeletonData(binaryData);
 				} catch (e) {
-					this.showError("Error: Could not load skeleton binary.<br><br>" + e.toString());
+					this.showError(`Error: Could not load skeleton binary.\n${e.message}`);
 					return;
 				}
 			}
@@ -905,22 +911,21 @@ module spine {
 			if (this.config.controlBones) {
 				this.config.controlBones.forEach(bone => {
 					if (!skeletonData.findBone(bone)) {
-						this.showError(`Error: control bone '${bone}' does not exist in skeleton.`);
+						this.showError(`Error: Control bone does not exist in skeleton: ${bone}`);
+						return;
 					}
 				})
 			}
 
 			// Setup skin
 			if (!this.config.skin) {
-				if (skeletonData.skins.length > 0) {
-					this.config.skin = skeletonData.skins[0].name;
-				}
+				if (skeletonData.skins.length > 0) this.config.skin = skeletonData.skins[0].name;
 			}
 
 			if (this.config.skins && this.config.skin.length > 0) {
 				this.config.skins.forEach(skin => {
 					if (!this.skeleton.data.findSkin(skin)) {
-						this.showError(`Error: skin '${skin}' in selectable skin list does not exist in skeleton.`);
+						this.showError(`Error: Skin in config list does not exist in skeleton: ${skin}`);
 						return;
 					}
 				});
@@ -928,16 +933,14 @@ module spine {
 
 			if (this.config.skin) {
 				if (!this.skeleton.data.findSkin(this.config.skin)) {
-					this.showError(`Error: skin '${this.config.skin}' does not exist in skeleton.`);
+					this.showError(`Error: Skin does not exist in skeleton: ${this.config.skin}`);
 					return;
 				}
 				this.skeleton.setSkinByName(this.config.skin);
 				this.skeleton.setSlotsToSetupPose();
 			}
 
-			// Setup empty viewport if none is given and check
-			// if all animations for which viewports where given
-			// exist.
+			// Setup empty viewport if none is given and check if all animations for which viewports where given exist.
 			if (!this.config.viewport) {
 				(this.config.viewport as any) = {
 					animations: {},
@@ -952,7 +955,7 @@ module spine {
 			} else {
 				Object.getOwnPropertyNames(this.config.viewport.animations).forEach((animation: string) => {
 					if (!skeletonData.findAnimation(animation)) {
-						this.showError(`Error: animation '${animation}' for which a viewport was specified does not exist in skeleton.`);
+						this.showError(`Error: Animation for which a viewport was specified does not exist in skeleton: ${animation}`);
 						return;
 					}
 				});
@@ -962,7 +965,7 @@ module spine {
 			if (this.config.animations && this.config.animations.length > 0) {
 				this.config.animations.forEach(animation => {
 					if (!this.skeleton.data.findAnimation(animation)) {
-						this.showError(`Error: animation '${animation}' in selectable animation list does not exist in skeleton.`);
+						this.showError(`Error: Animation in config list does not exist in skeleton: ${animation}`);
 						return;
 					}
 				});
@@ -980,7 +983,7 @@ module spine {
 
 			if(this.config.animation) {
 				if (!skeletonData.findAnimation(this.config.animation)) {
-					this.showError(`Error: animation '${this.config.animation}' does not exist in skeleton.`);
+					this.showError(`Error: Animation does not exist in skeleton: ${this.config.animation}`);
 					return;
 				}
 				this.play()
@@ -1127,11 +1130,8 @@ module spine {
 			this.playButton.classList.remove("spine-player-button-icon-play");
 			this.playButton.classList.add("spine-player-button-icon-pause");
 
-			if (this.config.animation) {
-				if (!this.animationState.getCurrent(0)) {
-					this.setAnimation(this.config.animation);
-				}
-			}
+			if (this.config.animation && !this.animationState.getCurrent(0))
+				this.setAnimation(this.config.animation);
 		}
 
 		private pause () {
@@ -1237,9 +1237,8 @@ module spine {
 					maxX = Math.max(offset.x + size.x, maxX);
 					minY = Math.min(offset.y, minY);
 					maxY = Math.max(offset.y + size.y, maxY);
-				} else {
-					console.log("Bounds of animation " + animationName + " are NaN");
-				}
+				} else
+					console.log("Animation bounds are NaN: " + animationName);
 			}
 
 			offset.x = minX;

+ 2 - 2
spine-ts/threejs/src/AssetManager.ts

@@ -29,10 +29,10 @@
 
 module spine.threejs {
 	export class AssetManager extends spine.AssetManager {
-		constructor (pathPrefix: string = "") {
+		constructor (pathPrefix: string = "", downloader: Downloader = null) {
 			super((image: HTMLImageElement) => {
 				return new ThreeJsTexture(image);
-			}, pathPrefix);
+			}, pathPrefix, downloader);
 		}
 	}
 }

+ 2 - 2
spine-ts/webgl/src/AssetManager.ts

@@ -29,10 +29,10 @@
 
 module spine.webgl {
 	export class AssetManager extends spine.AssetManager {
-		constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext, pathPrefix: string = "") {
+		constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext, pathPrefix: string = "", downloader: Downloader = null) {
 			super((image: HTMLImageElement | ImageBitmap) => {
 				return new spine.webgl.GLTexture(context, image);
-			}, pathPrefix);
+			}, pathPrefix, downloader);
 		}
 	}
 }