mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
[ts] Added SpineCanvas, a simpler way to render via spine-webgl
Rewrote mix-and-match example as well as barebones example to illustrate usage.
This commit is contained in:
parent
386f847667
commit
91a8b6a100
@ -216,6 +216,7 @@
|
||||
* `AssetManager` constructor now takes an option `Downloader` instance. Used to download assets only once and share them between `AssetManager` instances.
|
||||
* Added web worker support to `AssetManager`.
|
||||
* Added various default parameters to `AnimationState` methods for ease of use.
|
||||
* Added `SpineCanvas`, a simpler way to render a scene via spine-webgl. See `spine-ts/spine-webgl/examples/barebones.html` and `spine-ts/spine-webgl/examples/mix-and-match.html`.
|
||||
|
||||
### WebGL backend
|
||||
* **Breaking change:** removed `SharedAssetManager`. Use `AssetManager` with a shared `Downloader` instance instead.
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
<ul>
|
||||
<li><a href="/spine-webgl/example">Example</a></li>
|
||||
<li><a href="/spine-webgl/example/barebones.html">Barebones</a></li>
|
||||
<li><a href="/spine-webgl/example/mix-and-match.html">Mix & match</a></li>
|
||||
<li><a href="/spine-webgl/demos/additiveblending.html">Additive blending</a></li>
|
||||
<li><a href="/spine-webgl/demos/clipping.html">Clipping</a></li>
|
||||
<li><a href="/spine-webgl/demos/hoverboard.html">Hoverboard</a></li>
|
||||
|
||||
32
spine-ts/package-lock.json
generated
32
spine-ts/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@esotericsoftware/spine-ts",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@esotericsoftware/spine-ts",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.4",
|
||||
"license": "LicenseRef-LICENSE",
|
||||
"workspaces": [
|
||||
"spine-core",
|
||||
@ -8251,6 +8251,11 @@
|
||||
"@esotericsoftware/spine-core": "4.0.2"
|
||||
}
|
||||
},
|
||||
"spine-canvas/node_modules/@esotericsoftware/spine-core": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.0.2.tgz",
|
||||
"integrity": "sha512-tkHGEt0NyC/UuHPJtPSmD2YK5D3Z8Yp0OqKhKeH298aRJTIYfsmlgpupgraUqxaacEv9IatIC79pLATtlEsz7A=="
|
||||
},
|
||||
"spine-core": {
|
||||
"name": "@esotericsoftware/spine-core",
|
||||
"version": "4.0.4",
|
||||
@ -8270,6 +8275,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.0.2.tgz",
|
||||
"integrity": "sha512-tkHGEt0NyC/UuHPJtPSmD2YK5D3Z8Yp0OqKhKeH298aRJTIYfsmlgpupgraUqxaacEv9IatIC79pLATtlEsz7A=="
|
||||
},
|
||||
"spine-player/node_modules/@esotericsoftware/spine-webgl": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-webgl/-/spine-webgl-4.0.2.tgz",
|
||||
"integrity": "sha512-hSiMZ62g73td5qS5whRb6XtHlTCPYSsVCKHPmrTG4PSxIj0VYWh4ByOZvTcA9mTXZuV8kAZhdqNVc2AXmAe++A==",
|
||||
"dependencies": {
|
||||
"@esotericsoftware/spine-core": "4.0.2"
|
||||
}
|
||||
},
|
||||
"spine-threejs": {
|
||||
"name": "@esotericsoftware/spine-threejs",
|
||||
"version": "4.0.4",
|
||||
@ -8330,6 +8343,13 @@
|
||||
"version": "file:spine-canvas",
|
||||
"requires": {
|
||||
"@esotericsoftware/spine-core": "4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esotericsoftware/spine-core": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.0.2.tgz",
|
||||
"integrity": "sha512-tkHGEt0NyC/UuHPJtPSmD2YK5D3Z8Yp0OqKhKeH298aRJTIYfsmlgpupgraUqxaacEv9IatIC79pLATtlEsz7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@esotericsoftware/spine-core": {
|
||||
@ -8346,6 +8366,14 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.0.2.tgz",
|
||||
"integrity": "sha512-tkHGEt0NyC/UuHPJtPSmD2YK5D3Z8Yp0OqKhKeH298aRJTIYfsmlgpupgraUqxaacEv9IatIC79pLATtlEsz7A=="
|
||||
},
|
||||
"@esotericsoftware/spine-webgl": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-webgl/-/spine-webgl-4.0.2.tgz",
|
||||
"integrity": "sha512-hSiMZ62g73td5qS5whRb6XtHlTCPYSsVCKHPmrTG4PSxIj0VYWh4ByOZvTcA9mTXZuV8kAZhdqNVc2AXmAe++A==",
|
||||
"requires": {
|
||||
"@esotericsoftware/spine-core": "4.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -40,36 +40,36 @@ export class AssetManagerBase implements Disposable {
|
||||
private toLoad = 0;
|
||||
private loaded = 0;
|
||||
|
||||
constructor (textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader: Downloader = null) {
|
||||
constructor(textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader: Downloader = null) {
|
||||
this.textureLoader = textureLoader;
|
||||
this.pathPrefix = pathPrefix;
|
||||
this.downloader = downloader || new Downloader();
|
||||
}
|
||||
|
||||
private start (path: string): string {
|
||||
private start(path: string): string {
|
||||
this.toLoad++;
|
||||
return this.pathPrefix + path;
|
||||
}
|
||||
|
||||
private success (callback: (path: string, data: any) => void, path: string, asset: any) {
|
||||
private success(callback: (path: string, data: any) => void, path: string, asset: any) {
|
||||
this.toLoad--;
|
||||
this.loaded++;
|
||||
this.assets[path] = asset;
|
||||
if (callback) callback(path, asset);
|
||||
}
|
||||
|
||||
private error (callback: (path: string, message: string) => void, path: string, message: string) {
|
||||
private error(callback: (path: string, message: string) => void, path: string, message: string) {
|
||||
this.toLoad--;
|
||||
this.loaded++;
|
||||
this.errors[path] = message;
|
||||
if (callback) callback(path, message);
|
||||
}
|
||||
|
||||
setRawDataURI (path: string, data: string) {
|
||||
setRawDataURI(path: string, data: string) {
|
||||
this.downloader.rawDataUris[this.pathPrefix + path] = data;
|
||||
}
|
||||
|
||||
loadBinary (path: string,
|
||||
loadBinary(path: string,
|
||||
success: (path: string, binary: Uint8Array) => void = null,
|
||||
error: (path: string, message: string) => void = null) {
|
||||
path = this.start(path);
|
||||
@ -81,7 +81,7 @@ export class AssetManagerBase implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
loadText (path: string,
|
||||
loadText(path: string,
|
||||
success: (path: string, text: string) => void = null,
|
||||
error: (path: string, message: string) => void = null) {
|
||||
path = this.start(path);
|
||||
@ -93,7 +93,7 @@ export class AssetManagerBase implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
loadJson (path: string,
|
||||
loadJson(path: string,
|
||||
success: (path: string, object: object) => void = null,
|
||||
error: (path: string, message: string) => void = null) {
|
||||
path = this.start(path);
|
||||
@ -105,7 +105,7 @@ export class AssetManagerBase implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
loadTexture (path: string,
|
||||
loadTexture(path: string,
|
||||
success: (path: string, texture: Texture) => void = null,
|
||||
error: (path: string, message: string) => void = null) {
|
||||
path = this.start(path);
|
||||
@ -136,7 +136,7 @@ export class AssetManagerBase implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
loadTextureAtlas (path: string,
|
||||
loadTextureAtlas(path: string,
|
||||
success: (path: string, atlas: TextureAtlas) => void = null,
|
||||
error: (path: string, message: string) => void = null
|
||||
) {
|
||||
@ -170,11 +170,11 @@ export class AssetManagerBase implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
get (path: string) {
|
||||
get(path: string) {
|
||||
return this.assets[this.pathPrefix + path];
|
||||
}
|
||||
|
||||
require (path: string) {
|
||||
require(path: string) {
|
||||
path = this.pathPrefix + path;
|
||||
let asset = this.assets[path];
|
||||
if (asset) return asset;
|
||||
@ -182,7 +182,7 @@ export class AssetManagerBase implements Disposable {
|
||||
throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
|
||||
}
|
||||
|
||||
remove (path: string) {
|
||||
remove(path: string) {
|
||||
path = this.pathPrefix + path;
|
||||
let asset = this.assets[path];
|
||||
if ((<any>asset).dispose) (<any>asset).dispose();
|
||||
@ -190,7 +190,7 @@ export class AssetManagerBase implements Disposable {
|
||||
return asset;
|
||||
}
|
||||
|
||||
removeAll () {
|
||||
removeAll() {
|
||||
for (let key in this.assets) {
|
||||
let asset = this.assets[key];
|
||||
if ((<any>asset).dispose) (<any>asset).dispose();
|
||||
@ -198,27 +198,27 @@ export class AssetManagerBase implements Disposable {
|
||||
this.assets = {};
|
||||
}
|
||||
|
||||
isLoadingComplete (): boolean {
|
||||
isLoadingComplete(): boolean {
|
||||
return this.toLoad == 0;
|
||||
}
|
||||
|
||||
getToLoad (): number {
|
||||
getToLoad(): number {
|
||||
return this.toLoad;
|
||||
}
|
||||
|
||||
getLoaded (): number {
|
||||
getLoaded(): number {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
dispose () {
|
||||
dispose() {
|
||||
this.removeAll();
|
||||
}
|
||||
|
||||
hasErrors () {
|
||||
hasErrors() {
|
||||
return Object.keys(this.errors).length > 0;
|
||||
}
|
||||
|
||||
getErrors () {
|
||||
getErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
}
|
||||
@ -227,7 +227,7 @@ export class Downloader {
|
||||
private callbacks: StringMap<Array<Function>> = {};
|
||||
rawDataUris: StringMap<string> = {};
|
||||
|
||||
downloadText (url: string, success: (data: string) => void, error: (status: number, responseText: string) => void) {
|
||||
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();
|
||||
@ -241,20 +241,20 @@ export class Downloader {
|
||||
request.send();
|
||||
}
|
||||
|
||||
downloadJson (url: string, success: (data: object) => void, error: (status: number, responseText: string) => void) {
|
||||
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) {
|
||||
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);
|
||||
this.finish(url, request.status, request.response);
|
||||
};
|
||||
request.onload = () => {
|
||||
if (request.status == 200)
|
||||
@ -266,7 +266,7 @@ export class Downloader {
|
||||
request.send();
|
||||
}
|
||||
|
||||
private start (url: string, success: any, error: any) {
|
||||
private start(url: string, success: any, error: any) {
|
||||
let callbacks = this.callbacks[url];
|
||||
try {
|
||||
if (callbacks) return true;
|
||||
@ -276,7 +276,7 @@ export class Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
private finish (url: string, status: number, data: any) {
|
||||
private finish(url: string, status: number, data: any) {
|
||||
let callbacks = this.callbacks[url];
|
||||
delete this.callbacks[url];
|
||||
let args = status == 200 ? [data] : [status, data];
|
||||
|
||||
@ -5,74 +5,26 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
|
||||
<script>
|
||||
class App {
|
||||
skeleton;
|
||||
animationState;
|
||||
|
||||
var canvas;
|
||||
var gl;
|
||||
var shader;
|
||||
var batcher;
|
||||
var mvp = new spine.Matrix4();
|
||||
var assetManager;
|
||||
var skeletonRenderer;
|
||||
|
||||
var lastFrameTime;
|
||||
var spineboy;
|
||||
|
||||
function init() {
|
||||
// Setup canvas and WebGL context. We pass alpha: false to canvas.getContext() so we don't use premultiplied alpha when
|
||||
// loading textures. That is handled separately by PolygonBatcher.
|
||||
canvas = document.getElementById("canvas");
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
var config = { alpha: false };
|
||||
gl = canvas.getContext("webgl", config) || canvas.getContext("experimental-webgl", config);
|
||||
if (!gl) {
|
||||
alert('WebGL is unavailable.');
|
||||
return;
|
||||
loadAssets(canvas) {
|
||||
// Load the skeleton file.
|
||||
canvas.assetManager.loadBinary("assets/spineboy-pro.skel");
|
||||
// Load the atlas and its pages.
|
||||
canvas.assetManager.loadTextureAtlas("assets/spineboy-pma.atlas");
|
||||
}
|
||||
|
||||
// Create a simple shader, mesh, model-view-projection matrix, SkeletonRenderer, and AssetManager.
|
||||
shader = spine.Shader.newTwoColoredTextured(gl);
|
||||
batcher = new spine.PolygonBatcher(gl);
|
||||
mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1);
|
||||
skeletonRenderer = new spine.SkeletonRenderer(gl);
|
||||
assetManager = new spine.AssetManager(gl);
|
||||
initialize(canvas) {
|
||||
let assetManager = canvas.assetManager;
|
||||
|
||||
// Tell AssetManager to load the resources for each skeleton, including the exported .skel file, the .atlas file and the .png
|
||||
// file for the atlas. We then wait until all resources are loaded in the load() method.
|
||||
assetManager.loadBinary("assets/spineboy-pro.skel");
|
||||
assetManager.loadTextureAtlas("assets/spineboy-pma.atlas");
|
||||
requestAnimationFrame(load);
|
||||
}
|
||||
|
||||
function load() {
|
||||
// Wait until the AssetManager has loaded all resources, then load the skeletons.
|
||||
if (assetManager.isLoadingComplete()) {
|
||||
spineboy = loadSpineboy("run", true);
|
||||
lastFrameTime = Date.now() / 1000;
|
||||
requestAnimationFrame(render); // Loading is done, call render every frame.
|
||||
} else {
|
||||
requestAnimationFrame(load);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSpineboy(initialAnimation, premultipliedAlpha) {
|
||||
// Load the texture atlas from the AssetManager.
|
||||
// Create the texture atlas.
|
||||
var atlas = assetManager.require("assets/spineboy-pma.atlas");
|
||||
|
||||
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
|
||||
@ -84,87 +36,43 @@
|
||||
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
|
||||
skeletonBinary.scale = 1;
|
||||
var skeletonData = skeletonBinary.readSkeletonData(assetManager.require("assets/spineboy-pro.skel"));
|
||||
var skeleton = new spine.Skeleton(skeletonData);
|
||||
var bounds = calculateSetupPoseBounds(skeleton);
|
||||
this.skeleton = new spine.Skeleton(skeletonData);
|
||||
|
||||
// Create an AnimationState, and set the initial animation in looping mode.
|
||||
var animationStateData = new spine.AnimationStateData(skeleton.data);
|
||||
var animationState = new spine.AnimationState(animationStateData);
|
||||
animationState.setAnimation(0, initialAnimation, true);
|
||||
|
||||
// Pack everything up and return to caller.
|
||||
return { skeleton: skeleton, state: animationState, bounds: bounds, premultipliedAlpha: premultipliedAlpha };
|
||||
// Create an AnimationState, and set the "run" animation in looping mode.
|
||||
var animationStateData = new spine.AnimationStateData(skeletonData);
|
||||
this.animationState = new spine.AnimationState(animationStateData);
|
||||
this.animationState.setAnimation(0, "run", true);
|
||||
}
|
||||
|
||||
function calculateSetupPoseBounds(skeleton) {
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.updateWorldTransform();
|
||||
var offset = new spine.Vector2();
|
||||
var size = new spine.Vector2();
|
||||
skeleton.getBounds(offset, size, []);
|
||||
return { offset: offset, size: size };
|
||||
update(canvas, delta) {
|
||||
// Update the animation state using the delta time.
|
||||
this.animationState.update(delta);
|
||||
// Apply the animation state to the skeleton.
|
||||
this.animationState.apply(this.skeleton);
|
||||
// Let the skeleton update the transforms of its bones.
|
||||
this.skeleton.updateWorldTransform();
|
||||
}
|
||||
|
||||
function render() {
|
||||
var now = Date.now() / 1000;
|
||||
var delta = now - lastFrameTime;
|
||||
lastFrameTime = now;
|
||||
render(canvas) {
|
||||
let renderer = canvas.renderer;
|
||||
// Resize the viewport to the full canvas.
|
||||
renderer.resize(spine.ResizeMode.Expand);
|
||||
|
||||
// Update the MVP matrix to adjust for canvas size changes
|
||||
resize();
|
||||
// Clear the canvas with a light gray color.
|
||||
canvas.clear(0.2, 0.2, 0.2, 1);
|
||||
|
||||
gl.clearColor(0.3, 0.3, 0.3, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// Apply the animation state based on the delta time.
|
||||
var skeleton = spineboy.skeleton;
|
||||
var state = spineboy.state;
|
||||
var premultipliedAlpha = spineboy.premultipliedAlpha;
|
||||
state.update(delta);
|
||||
state.apply(skeleton);
|
||||
skeleton.updateWorldTransform();
|
||||
|
||||
// Bind the shader and set the texture and model-view-projection matrix.
|
||||
shader.bind();
|
||||
shader.setUniformi(spine.Shader.SAMPLER, 0);
|
||||
shader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values);
|
||||
|
||||
// Start the batch and tell the SkeletonRenderer to render the active skeleton.
|
||||
batcher.begin(shader);
|
||||
skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
skeletonRenderer.draw(batcher, skeleton);
|
||||
batcher.end();
|
||||
|
||||
shader.unbind();
|
||||
|
||||
requestAnimationFrame(render);
|
||||
// Begin rendering.
|
||||
renderer.begin();
|
||||
// Draw the skeleton
|
||||
renderer.drawSkeleton(this.skeleton, true);
|
||||
// Complete rendering.
|
||||
renderer.end();
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
var w = canvas.clientWidth;
|
||||
var h = canvas.clientHeight;
|
||||
if (canvas.width != w || canvas.height != h) {
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
}
|
||||
|
||||
// Calculations to center the skeleton in the canvas.
|
||||
var bounds = spineboy.bounds;
|
||||
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;
|
||||
|
||||
mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
new spine.SpineCanvas(document.getElementById("canvas"), {
|
||||
app: new App()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
84
spine-ts/spine-webgl/example/mix-and-match.html
Normal file
84
spine-ts/spine-webgl/example/mix-and-match.html
Normal file
@ -0,0 +1,84 @@
|
||||
<html>
|
||||
<script src="../dist/iife/spine-webgl.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
|
||||
</body>
|
||||
<script>
|
||||
// Define the class running in the Spine canvas
|
||||
class App {
|
||||
skeleton;
|
||||
state;
|
||||
|
||||
loadAssets(canvas) {
|
||||
canvas.assetManager.AnimationState
|
||||
canvas.assetManager.loadTextureAtlas("mix-and-match-pma.atlas");
|
||||
canvas.assetManager.loadBinary("mix-and-match-pro.skel");
|
||||
}
|
||||
|
||||
initialize(canvas) {
|
||||
let assetManager = canvas.assetManager;
|
||||
|
||||
// Create the atlas
|
||||
let atlas = canvas.assetManager.get("mix-and-match-pma.atlas");
|
||||
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||
|
||||
// Create the skeleton
|
||||
let skeletonBinary = new spine.SkeletonBinary(atlasLoader);
|
||||
skeletonBinary.scale = 0.5;
|
||||
let skeletonData = skeletonBinary.readSkeletonData(assetManager.get("mix-and-match-pro.skel"));
|
||||
this.skeleton = new spine.Skeleton(skeletonData);
|
||||
|
||||
// Create the animation state
|
||||
let stateData = new spine.AnimationStateData(skeletonData);
|
||||
this.state = new spine.AnimationState(stateData);
|
||||
this.state.setAnimation(0, "dance", true);
|
||||
|
||||
// Create a new skin, by mixing and matching other skins
|
||||
// that fit together. Items making up the girl are individual
|
||||
// skins. Using the skin API, a new skin is created which is
|
||||
// a combination of all these individual item skins.
|
||||
let mixAndMatchSkin = new spine.Skin("custom-girl");
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("skin-base"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("nose/short"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyelids/girly"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyes/violet"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("hair/brown"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("clothes/hoodie-orange"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("legs/pants-jeans"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/bag"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
|
||||
this.skeleton.setSkin(mixAndMatchSkin);
|
||||
}
|
||||
|
||||
update(canvas, delta) {
|
||||
this.state.update(delta);
|
||||
this.state.apply(this.skeleton);
|
||||
this.skeleton.updateWorldTransform();
|
||||
}
|
||||
|
||||
render(canvas) {
|
||||
let renderer = canvas.renderer;
|
||||
renderer.resize(spine.ResizeMode.Expand);
|
||||
canvas.clear(0.2, 0.2, 0.2, 1);
|
||||
renderer.begin();
|
||||
renderer.drawSkeleton(this.skeleton, true);
|
||||
renderer.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Create the Spine canvas which runs the app
|
||||
new spine.SpineCanvas(document.getElementById("canvas"), {
|
||||
pathPrefix: "assets/",
|
||||
app: new App()
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
||||
121
spine-ts/spine-webgl/src/SpineCanvas.ts
Normal file
121
spine-ts/spine-webgl/src/SpineCanvas.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated January 1, 2020. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2020, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { TimeKeeper, AssetManager, ManagedWebGLRenderingContext, SceneRenderer, Input, StringMap } from "./";
|
||||
|
||||
/** An app running inside a {@link SpineCanvas}. The app life cycle
|
||||
* is as follows:
|
||||
*
|
||||
* 1. `loadAssets()` is called. The app can queue assets for loading via {@link SpineCanvas#assetManager}.
|
||||
* 2. `initialize()` is called when all assets are loaded. The app can setup anything it needs to enter the main application logic.
|
||||
* 3. `update()` is called periodically at screen refresh rate. The app can update its state.
|
||||
* 4. `render()` is called periodically at screen refresh rate. The app can render its state via {@link SpineCanvas#renderer} or directly via the WebGL context in {@link SpineCanvas.gl}`
|
||||
*/
|
||||
export interface SpineCanvasApp {
|
||||
loadAssets?(canvas: SpineCanvas): void;
|
||||
initialize?(canvas: SpineCanvas): void;
|
||||
update?(canvas: SpineCanvas, delta: number): void;
|
||||
render?(canvas: SpineCanvas): void;
|
||||
error?(canvas: SpineCanvas, errors: StringMap<string>): void;
|
||||
}
|
||||
|
||||
/** Configuration passed to the {@link SpineCanvas} constructor */
|
||||
export interface SpineCanvasConfig {
|
||||
/* The {@link SpineCanvasApp} to be run in the canvas. */
|
||||
app: SpineCanvasApp;
|
||||
/* The path prefix to be used by the {@link AssetManager}. */
|
||||
pathPrefix: string;
|
||||
}
|
||||
|
||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasApp}. The app loads
|
||||
* assets and initializes itself, then updates and renders its state at the screen refresh rate. */
|
||||
export class SpineCanvas {
|
||||
readonly context: ManagedWebGLRenderingContext;
|
||||
|
||||
/** Tracks the current time, delta, and other time related statistics. */
|
||||
readonly time = new TimeKeeper();
|
||||
/** The HTML canvas to render to. */
|
||||
readonly htmlCanvas: HTMLCanvasElement;
|
||||
/** The WebGL rendering context. */
|
||||
readonly gl: WebGLRenderingContext;
|
||||
/** The scene renderer for easy drawing of skeletons, shapes, and images. */
|
||||
readonly renderer: SceneRenderer;
|
||||
/** The asset manager to load assets with. */
|
||||
readonly assetManager: AssetManager;
|
||||
/** The input processor used to listen to mouse, touch, and keyboard events. */
|
||||
readonly input: Input;
|
||||
|
||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
||||
constructor(canvas: HTMLCanvasElement, config: SpineCanvasConfig) {
|
||||
if (config.pathPrefix === undefined) config.pathPrefix = "";
|
||||
if (config.app === undefined) config.app = {
|
||||
loadAssets: () => { },
|
||||
initialize: () => { },
|
||||
update: () => { },
|
||||
render: () => { },
|
||||
error: () => { },
|
||||
}
|
||||
|
||||
this.htmlCanvas = canvas;
|
||||
this.context = new ManagedWebGLRenderingContext(canvas, { alpha: true });
|
||||
this.renderer = new SceneRenderer(canvas, this.context);
|
||||
this.gl = this.context.gl;
|
||||
this.assetManager = new AssetManager(this.context, config.pathPrefix);
|
||||
this.input = new Input(canvas);
|
||||
|
||||
config.app.loadAssets?.(this);
|
||||
|
||||
let loop = () => {
|
||||
requestAnimationFrame(loop);
|
||||
this.time.update();
|
||||
config.app.update?.(this, this.time.delta);
|
||||
config.app.render?.(this);
|
||||
}
|
||||
|
||||
let waitForAssets = () => {
|
||||
if (this.assetManager.isLoadingComplete()) {
|
||||
if (this.assetManager.hasErrors()) {
|
||||
config.app.error?.(this, this.assetManager.getErrors());
|
||||
} else {
|
||||
config.app.initialize?.(this);
|
||||
loop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(waitForAssets);
|
||||
}
|
||||
requestAnimationFrame(waitForAssets);
|
||||
}
|
||||
|
||||
/** Clears the canvas with the given color. The color values are given in the range [0,1]. */
|
||||
clear(r: number, g: number, b: number, a: number) {
|
||||
this.gl.clearColor(r, g, b, a);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ export class ManagedWebGLRenderingContext {
|
||||
public gl: WebGLRenderingContext;
|
||||
private restorables = new Array<Restorable>();
|
||||
|
||||
constructor (canvasOrContext: HTMLCanvasElement | WebGLRenderingContext | EventTarget, contextConfig: any = { alpha: "true" }) {
|
||||
constructor(canvasOrContext: HTMLCanvasElement | WebGLRenderingContext | EventTarget, contextConfig: any = { alpha: "true" }) {
|
||||
if (!((canvasOrContext instanceof WebGLRenderingContext) || (typeof WebGL2RenderingContext !== 'undefined' && canvasOrContext instanceof WebGL2RenderingContext)))
|
||||
this.setupCanvas(canvasOrContext, contextConfig);
|
||||
else {
|
||||
@ -43,7 +43,7 @@ export class ManagedWebGLRenderingContext {
|
||||
}
|
||||
}
|
||||
|
||||
private setupCanvas (canvas: any, contextConfig: any) {
|
||||
private setupCanvas(canvas: any, contextConfig: any) {
|
||||
this.gl = <WebGLRenderingContext>(canvas.getContext("webgl2", contextConfig) || canvas.getContext("webgl", contextConfig));
|
||||
this.canvas = canvas;
|
||||
canvas.addEventListener("webglcontextlost", (e: any) => {
|
||||
@ -57,11 +57,11 @@ export class ManagedWebGLRenderingContext {
|
||||
});
|
||||
}
|
||||
|
||||
addRestorable (restorable: Restorable) {
|
||||
addRestorable(restorable: Restorable) {
|
||||
this.restorables.push(restorable);
|
||||
}
|
||||
|
||||
removeRestorable (restorable: Restorable) {
|
||||
removeRestorable(restorable: Restorable) {
|
||||
let index = this.restorables.indexOf(restorable);
|
||||
if (index > -1) this.restorables.splice(index, 1);
|
||||
}
|
||||
@ -75,7 +75,7 @@ const ONE_MINUS_DST_ALPHA = 0x0305;
|
||||
const DST_COLOR = 0x0306;
|
||||
|
||||
export class WebGLBlendModeConverter {
|
||||
static getDestGLBlendMode (blendMode: BlendMode) {
|
||||
static getDestGLBlendMode(blendMode: BlendMode) {
|
||||
switch (blendMode) {
|
||||
case BlendMode.Normal: return ONE_MINUS_SRC_ALPHA;
|
||||
case BlendMode.Additive: return ONE;
|
||||
@ -85,7 +85,7 @@ export class WebGLBlendModeConverter {
|
||||
}
|
||||
}
|
||||
|
||||
static getSourceColorGLBlendMode (blendMode: BlendMode, premultipliedAlpha: boolean = false) {
|
||||
static getSourceColorGLBlendMode(blendMode: BlendMode, premultipliedAlpha: boolean = false) {
|
||||
switch (blendMode) {
|
||||
case BlendMode.Normal: return premultipliedAlpha ? ONE : SRC_ALPHA;
|
||||
case BlendMode.Additive: return premultipliedAlpha ? ONE : SRC_ALPHA;
|
||||
@ -95,7 +95,7 @@ export class WebGLBlendModeConverter {
|
||||
}
|
||||
}
|
||||
|
||||
static getSourceAlphaGLBlendMode (blendMode: BlendMode) {
|
||||
static getSourceAlphaGLBlendMode(blendMode: BlendMode) {
|
||||
switch (blendMode) {
|
||||
case BlendMode.Normal: return ONE;
|
||||
case BlendMode.Additive: return ONE;
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
export * from './AssetManager';
|
||||
export * from './Camera';
|
||||
export * from './GLTexture';
|
||||
export * from './Input';
|
||||
export * from './LoadingScreen';
|
||||
export * from './Matrix4';
|
||||
export * from './Mesh';
|
||||
export * from './PolygonBatcher';
|
||||
export * from './SceneRenderer';
|
||||
export * from './Shader';
|
||||
export * from './ShapeRenderer';
|
||||
export * from './SkeletonDebugRenderer';
|
||||
export * from './SkeletonRenderer';
|
||||
export * from './Vector3';
|
||||
export * from './WebGL';
|
||||
export * from "./AssetManager";
|
||||
export * from "./Camera";
|
||||
export * from "./GLTexture";
|
||||
export * from "./Input";
|
||||
export * from "./LoadingScreen";
|
||||
export * from "./Matrix4";
|
||||
export * from "./Mesh";
|
||||
export * from "./PolygonBatcher";
|
||||
export * from "./SceneRenderer";
|
||||
export * from "./Shader";
|
||||
export * from "./ShapeRenderer";
|
||||
export * from "./SkeletonDebugRenderer";
|
||||
export * from "./SkeletonRenderer";
|
||||
export * from "./SpineCanvas";
|
||||
export * from "./Vector3";
|
||||
export * from "./WebGL";
|
||||
export * from "@esotericsoftware/spine-core";
|
||||
@ -1,132 +0,0 @@
|
||||
<html>
|
||||
<script src="../dist/iife/spine-webgl.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>
|
||||
|
||||
<body>
|
||||
<div id="label" style="position: absolute; top: 0; left: 0; color: #fff; z-index: 10"></div>
|
||||
<canvas id="canvas" style="background: red;"></canvas>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
var FILE = "mix-and-match-pro";
|
||||
var ANIMATION = "dance";
|
||||
var SCALE = 0.5;
|
||||
|
||||
var canvas, context, gl, renderer, input, assetManager;
|
||||
var skeletons = [];
|
||||
var timeKeeper;
|
||||
var label = document.getElementById("label");
|
||||
var updateMean = new spine.WindowedMean();
|
||||
var renderMean = new spine.WindowedMean();
|
||||
|
||||
function init() {
|
||||
canvas = document.getElementById("canvas");
|
||||
canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
|
||||
context = new spine.ManagedWebGLRenderingContext(canvas, { alpha: false });
|
||||
gl = context.gl;
|
||||
|
||||
renderer = new spine.SceneRenderer(canvas, context);
|
||||
assetManager = new spine.AssetManager(context, "../example/assets/");
|
||||
input = new spine.Input(canvas);
|
||||
|
||||
assetManager.loadTextureAtlas(FILE.replace("-pro", "").replace("-ess", "") + "-pma.atlas");
|
||||
assetManager.loadBinary(FILE + ".skel");
|
||||
|
||||
timeKeeper = new spine.TimeKeeper();
|
||||
requestAnimationFrame(load);
|
||||
}
|
||||
|
||||
var run = true;
|
||||
|
||||
function load() {
|
||||
timeKeeper.update();
|
||||
if (assetManager.isLoadingComplete()) {
|
||||
var atlas = assetManager.get(FILE.replace("-pro", "").replace("-ess", "") + "-pma.atlas");
|
||||
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||
var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
|
||||
skeletonBinary.scale = SCALE;
|
||||
var skeletonData = skeletonBinary.readSkeletonData(assetManager.get(FILE + ".skel"));
|
||||
|
||||
skeleton = new spine.Skeleton(skeletonData);
|
||||
var stateData = new spine.AnimationStateData(skeleton.data);
|
||||
state = new spine.AnimationState(stateData);
|
||||
stateData.defaultMix = 0;
|
||||
|
||||
// Create a new skin, by mixing and matching other skins
|
||||
// that fit together. Items making up the girl are individual
|
||||
// skins. Using the skin API, a new skin is created which is
|
||||
// a combination of all these individual item skins.
|
||||
var mixAndMatchSkin = new spine.Skin("custom-girl");
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("skin-base"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("nose/short"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyelids/girly"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyes/violet"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("hair/brown"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("clothes/hoodie-orange"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("legs/pants-jeans"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/bag"));
|
||||
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
|
||||
skeleton.setSkin(mixAndMatchSkin);
|
||||
|
||||
state.setAnimation(0, ANIMATION, true);
|
||||
skeletons.push({ skeleton: skeleton, state: state });
|
||||
|
||||
requestAnimationFrame(render);
|
||||
} else {
|
||||
requestAnimationFrame(load);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
var start = Date.now()
|
||||
timeKeeper.update();
|
||||
var delta = timeKeeper.delta;
|
||||
|
||||
for (var i = 0; i < skeletons.length; i++) {
|
||||
var state = skeletons[i].state;
|
||||
var skeleton = skeletons[i].skeleton;
|
||||
state.update(delta);
|
||||
state.apply(skeleton);
|
||||
skeleton.updateWorldTransform();
|
||||
}
|
||||
updateMean.addValue(Date.now() - start);
|
||||
start = Date.now();
|
||||
|
||||
gl.clearColor(0.2, 0.2, 0.2, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
renderer.resize(spine.ResizeMode.Fit);
|
||||
renderer.begin();
|
||||
for (var i = 0; i < skeletons.length; i++) {
|
||||
var skeleton = skeletons[i].skeleton;
|
||||
renderer.drawSkeleton(skeleton, true);
|
||||
}
|
||||
renderer.end();
|
||||
|
||||
requestAnimationFrame(render)
|
||||
renderMean.addValue(Date.now() - start);
|
||||
label.innerHTML = ("Update time: " + Number(updateMean.getMean()).toFixed(2) + " ms\n" +
|
||||
"Render time: " + Number(renderMean.getMean()).toFixed(2) + " ms\n");
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user