367 lines
15 KiB
TypeScript

/******************************************************************************
* Spine Runtimes Software License v2.5
*
* Copyright (c) 2013-2016, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable, and
* non-transferable license to use, install, execute, and perform the Spine
* Runtimes software and derivative works solely for personal or internal
* use. Without the written permission of Esoteric Software (see Section 2 of
* the Spine Software License Agreement), you may not (a) modify, translate,
* adapt, or develop new applications using the Spine Runtimes or otherwise
* create derivative works or improvements of the Spine Runtimes or (b) remove,
* delete, alter, or obscure any trademarks or any copyright, trademark, patent,
* or other intellectual property or proprietary rights notices on or in the
* Software, including any copy thereof. Redistributions in binary or source
* form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
* USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
module spine {
export class SpineWidget {
skeleton: Skeleton;
state: AnimationState;
context: spine.webgl.ManagedWebGLRenderingContext;
canvas: HTMLCanvasElement;
debugRenderer: spine.webgl.SkeletonDebugRenderer;
private config: SpineWidgetConfig;
private assetManager: spine.webgl.AssetManager;
private shader: spine.webgl.Shader;
private batcher: spine.webgl.PolygonBatcher;
private shapes: spine.webgl.ShapeRenderer;
private debugShader: spine.webgl.Shader;
private mvp = new spine.webgl.Matrix4();
private skeletonRenderer: spine.webgl.SkeletonRenderer;
private paused = false;
private lastFrameTime = Date.now() / 1000.0;
private backgroundColor = new Color();
private loaded = false;
private bounds = { offset: new Vector2(), size: new Vector2() };
constructor (element: HTMLElement | string, config: SpineWidgetConfig) {
if (!element) throw new Error("Please provide a DOM element, e.g. document.getElementById('myelement')");
if (!config) throw new Error("Please provide a configuration, specifying at least the json file, atlas file and animation name");
let elementId = element as string;
if (typeof(element) === "string") element = document.getElementById(element as string);
if (element == null) throw new Error(`Element ${elementId} does not exist`);
this.validateConfig(config);
let existingCanvas = <HTMLCanvasElement>element.children[0];
let canvas = this.canvas = existingCanvas || document.createElement("canvas");
canvas.style.width = "100%";
canvas.style.height = "100%";
if (!existingCanvas) {
(<HTMLElement> element).appendChild(canvas);
}
canvas.width = (<HTMLElement>element).clientWidth;
canvas.height = (<HTMLElement>element).clientHeight;
var webglConfig = { alpha: config.alpha };
this.context = new spine.webgl.ManagedWebGLRenderingContext(canvas, webglConfig);
this.shader = spine.webgl.Shader.newTwoColoredTextured(this.context);
this.batcher = new spine.webgl.PolygonBatcher(this.context);
this.mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1);
this.skeletonRenderer = new spine.webgl.SkeletonRenderer(this.context);
this.debugShader = spine.webgl.Shader.newColored(this.context);
this.debugRenderer = new spine.webgl.SkeletonDebugRenderer(this.context);
this.shapes = new spine.webgl.ShapeRenderer(this.context);
let assets = this.assetManager = new spine.webgl.AssetManager(this.context, config.imagesPath ? config.imagesPath : "");
if (!config.atlasContent) {
assets.loadText(config.atlas);
}
if (!config.jsonContent) {
assets.loadText(config.json);
}
if (config.atlasPages == null) {
if (config.atlas) {
var atlasPage = config.atlas.replace(".atlas", ".png");
if (atlasPage.lastIndexOf(config.imagesPath) == 0) {
atlasPage = atlasPage.substr(config.imagesPath.length);
}
assets.loadTexture(atlasPage);
} else {
let firstLine = config.atlasContent.trim().split("\n")[0];
assets.loadTexture(firstLine);
}
} else {
for (let i = 0; i < config.atlasPages.length; i++) {
if (config.atlasPagesContent && config.atlasPagesContent[i]) {
assets.loadTextureData(config.atlasPages[i], config.atlasPagesContent[0]);
} else {
assets.loadTexture(config.atlasPages[i]);
}
}
}
requestAnimationFrame(() => { this.load(); });
}
private validateConfig (config: SpineWidgetConfig) {
if (!config.atlas && !config.atlasContent) throw new Error("Please specify config.atlas or config.atlasContent");
if (!config.json && !config.jsonContent) throw new Error("Please specify config.json or config.jsonContent");
if (!config.animation) throw new Error("Please specify config.animationName");
if (!config.scale) config.scale = 1.0;
if (!config.skin) config.skin = "default";
if (config.loop === undefined) config.loop = true;
if (!config.x) config.x = 0;
if (!config.y) config.y = 0;
if (config.fitToCanvas === undefined) config.fitToCanvas = true;
if (!config.backgroundColor) config.backgroundColor = "#555555";
if (!config.imagesPath) {
if (config.atlas) {
let index = config.atlas.lastIndexOf("/");
if (index != -1) {
config.imagesPath = config.atlas.substr(0, index) + "/";
} else {
config.imagesPath = "";
}
} else {
config.imagesPath = "";
}
}
if (config.json && config.json.lastIndexOf(config.imagesPath) == 0) {
config.json = config.json.substr(config.imagesPath.length);
}
if (config.atlas && config.atlas.lastIndexOf(config.imagesPath) == 0) {
config.atlas = config.atlas.substr(config.imagesPath.length);
}
if (!config.premultipliedAlpha === undefined) config.premultipliedAlpha = false;
if (!config.debug === undefined) config.debug = false;
if (!config.alpha === undefined) config.alpha = true;
this.backgroundColor.setFromString(config.backgroundColor);
this.config = config;
}
private load () {
let assetManager = this.assetManager;
let imagesPath = this.config.imagesPath;
let config = this.config;
if (assetManager.isLoadingComplete()) {
if (assetManager.hasErrors()) {
if (config.error) config.error(this, "Failed to load assets: " + JSON.stringify(assetManager.getErrors()));
else throw new Error("Failed to load assets: " + JSON.stringify(assetManager.getErrors()));
}
let atlasContent = config.atlasContent === undefined ? this.assetManager.get(this.config.atlas) as string : config.atlasContent;
let atlas = new spine.TextureAtlas(atlasContent, (path: string) => {
let texture = assetManager.get(path) as spine.webgl.GLTexture;
return texture;
});
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
var skeletonJson = new spine.SkeletonJson(atlasLoader);
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
skeletonJson.scale = config.scale;
let jsonContent = config.jsonContent === undefined ? assetManager.get(config.json) as string : config.jsonContent;
var skeletonData = skeletonJson.readSkeletonData(jsonContent);
var skeleton = this.skeleton = new spine.Skeleton(skeletonData);
var bounds = this.bounds;
skeleton.setSkinByName(config.skin);
skeleton.setToSetupPose();
skeleton.updateWorldTransform();
skeleton.getBounds(bounds.offset, bounds.size, []);
if (!config.fitToCanvas) {
skeleton.x = config.x;
skeleton.y = config.y;
}
var animationState = this.state = new spine.AnimationState(new spine.AnimationStateData(skeleton.data));
animationState.setAnimation(0, config.animation, config.loop);
this.loaded = true;
if (config.success) config.success(this);
requestAnimationFrame(() => { this.render(); });
} else
requestAnimationFrame(() => { this.load(); });
}
private render () {
var now = Date.now() / 1000;
var delta = now - this.lastFrameTime;
if (delta > 0.1) delta = 0;
this.lastFrameTime = now;
let gl = this.context.gl;
let color = this.backgroundColor;
this.resize();
gl.clearColor(color.r, color.g, color.b, color.a);
gl.clear(gl.COLOR_BUFFER_BIT);
// Apply the animation state based on the delta time.
var state = this.state;
var skeleton = this.skeleton;
var premultipliedAlpha = this.config.premultipliedAlpha;
state.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform();
// Draw the skeleton
let shader = this.shader;
let batcher = this.batcher;
let skeletonRenderer = this.skeletonRenderer;
shader.bind();
shader.setUniformi(spine.webgl.Shader.SAMPLER, 0);
shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, this.mvp.values);
batcher.begin(shader);
skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
skeletonRenderer.draw(batcher, skeleton);
batcher.end();
shader.unbind();
// Draw debug information if requested via config
if (this.config.debug) {
let shader = this.debugShader;
let shapes = this.shapes;
let renderer = this.debugRenderer;
shader.bind();
shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, this.mvp.values);
renderer.premultipliedAlpha = premultipliedAlpha;
shapes.begin(shader);
renderer.draw(shapes, skeleton);
shapes.end();
shader.unbind();
}
if (!this.paused) requestAnimationFrame(() => { this.render(); });
}
private resize () {
let canvas = this.canvas;
let w = canvas.clientWidth;
let h = canvas.clientHeight;
let bounds = this.bounds;
var devicePixelRatio = window.devicePixelRatio || 1;
if (canvas.width != Math.floor(w * devicePixelRatio) || canvas.height != Math.floor(h * devicePixelRatio)) {
canvas.width = Math.floor(w * devicePixelRatio);
canvas.height = Math.floor(h * devicePixelRatio);
}
// magic
if (this.config.fitToCanvas) {
var centerX = bounds.offset.x + bounds.size.x / 2;
var centerY = bounds.offset.y + bounds.size.y / 2;
var scaleX = bounds.size.x / w;
var scaleY = bounds.size.y / h;
var scale = Math.max(scaleX, scaleY) * 1.2;
if (scale < 1) scale = 1;
var width = w * scale;
var height = h * scale;
this.skeleton.x = this.skeleton.y = 0;
this.mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height);
} else {
this.mvp.ortho2d(0, 0, w - 1, h - 1);
}
this.context.gl.viewport(0, 0, canvas.width, canvas.height);
}
pause () {
this.paused = true;
}
play () {
this.paused = false;
requestAnimationFrame(() => { this.render(); });
}
isPlaying () {
return !this.paused;
}
setAnimation (animationName: string, animationStateListener: AnimationStateListener2 = null) {
if (!this.loaded) throw new Error("Widget isn't loaded yet");
this.skeleton.setToSetupPose();
let entry = this.state.setAnimation(0, animationName, this.config.loop);
entry.listener = animationStateListener
}
static loadWidgets() {
let widgets = document.getElementsByClassName("spine-widget");
for (var i = 0; i < widgets.length; i++) {
SpineWidget.loadWidget(<HTMLElement>widgets[i]);
}
}
static loadWidget(widget: HTMLElement) {
let config = new SpineWidgetConfig();
config.atlas = widget.getAttribute("data-atlas");
config.json = widget.getAttribute("data-json");
config.animation = widget.getAttribute("data-animation");
if (widget.getAttribute("data-images-path")) config.imagesPath = widget.getAttribute("data-images-path");
if (widget.getAttribute("data-atlas-pages")) config.atlasPages = widget.getAttribute("data-atlas-pages").split(",");
if (widget.getAttribute("data-skin")) config.skin = widget.getAttribute("data-skin");
if (widget.getAttribute("data-loop")) config.loop = widget.getAttribute("data-loop") === "true";
if (widget.getAttribute("data-scale")) config.scale = parseFloat(widget.getAttribute("data-scale"));
if (widget.getAttribute("data-x")) config.x = parseFloat(widget.getAttribute("data-x"));
if (widget.getAttribute("data-y")) config.y = parseFloat(widget.getAttribute("data-y"));
if (widget.getAttribute("data-fit-to-canvas")) config.fitToCanvas = widget.getAttribute("data-fit-to-canvas") === "true";
if (widget.getAttribute("data-background-color")) config.backgroundColor = widget.getAttribute("data-background-color");
if (widget.getAttribute("data-premultiplied-alpha")) config.premultipliedAlpha = widget.getAttribute("data-premultiplied-alpha") === "true";
if (widget.getAttribute("data-debug")) config.debug = widget.getAttribute("data-debug") === "true";
if (widget.getAttribute("data-alpha")) config.alpha = widget.getAttribute("data-alpha") === "true";
new spine.SpineWidget(widget, config);
}
static pageLoaded = false;
private static ready () {
if (SpineWidget.pageLoaded) return;
SpineWidget.pageLoaded = true;
SpineWidget.loadWidgets();
}
static setupDOMListener() {
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", SpineWidget.ready, false);
window.addEventListener("load", SpineWidget.ready, false);
} else {
(<any>document).attachEvent("onreadystatechange", function readyStateChange() {
if (document.readyState === "complete" ) SpineWidget.ready();
});
(<any>window).attachEvent("onload", SpineWidget.ready);
}
}
}
export class SpineWidgetConfig {
json: string;
jsonContent: any;
atlas: string;
atlasContent: string;
animation: string;
imagesPath: string;
atlasPages: string[];
atlasPagesContent: string[];
skin = "default";
loop = true;
scale = 1.0;
x = 0;
y = 0;
alpha = true;
fitToCanvas = true;
backgroundColor = "#555555";
premultipliedAlpha = false;
debug = false;
success: (widget: SpineWidget) => void;
error: (widget: SpineWidget, msg: string) => void;
}
}
spine.SpineWidget.setupDOMListener();