[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.
This commit is contained in:
Nathan Sweet 2021-06-18 02:31:10 -04:00
parent 214c778bac
commit 43cb6dfe22
5 changed files with 287 additions and 277 deletions

View File

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

View File

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

View File

@ -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;
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);
// 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());
// 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();
// 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);
// 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;
// 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);
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);
}
this.skeleton.updateWorldTransform();
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)
}
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)
this.animationState.update(delta);
this.animationState.apply(this.skeleton);
}
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
this.skeleton.updateWorldTransform();
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)
}
}
let viewportSize = this.scale(viewport.width, viewport.height, this.canvas.width, this.canvas.height);
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)
}
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();
// 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);
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);
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();
// 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);
// 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.camera.zoom = 0;
}
// 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);
}
this.sceneRenderer.end();
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;

View File

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

View File

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