mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 14:24:53 +08:00
Previously, we'd export to global objects called spine.canvas, spine.webgl, spine.threejs. Going forward, all Spine APIs will be hosted by the global spine object when the runtime is used straight from the bundled .js files in the build/ folder. This is a minor change with a simple fix on the user side, i.e. replace spine.canvas. with spine.
405 lines
14 KiB
HTML
405 lines
14 KiB
HTML
<html>
|
|
<script src="../../build/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>
|
|
<canvas id="canvas"></canvas>
|
|
<center>
|
|
<div style="color: #fff; position: fixed; top: 0; width: 100%">
|
|
<span>Format:</span><select id="formatList"></select>
|
|
<span>Skeleton:</span><select id="skeletonList"></select>
|
|
<span>Animation:</span><select id="animationList"></select>
|
|
<span>Skin:</span><select id="skinList"></select>
|
|
<span>Vertex Effect:</span><select id="effectList"></select>
|
|
<span>Debug:</span><input type="checkbox" id="debug">
|
|
</div>
|
|
</center>
|
|
<script>
|
|
|
|
var canvas;
|
|
var gl;
|
|
var shader;
|
|
var batcher;
|
|
var mvp = new spine.Matrix4();
|
|
var skeletonRenderer;
|
|
var assetManager;
|
|
|
|
var debugRenderer;
|
|
var shapes;
|
|
|
|
var lastFrameTime;
|
|
var skeletons = {};
|
|
var format = "JSON";
|
|
var activeSkeleton = "spineboy";
|
|
var pow2 = new spine.Pow(2);
|
|
var swirlEffect = new spine.SwirlEffect(0);
|
|
var jitterEffect = new spine.JitterEffect(20, 20);
|
|
var swirlTime = 0;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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, "assets/");
|
|
|
|
// Create a debug renderer and the ShapeRenderer it needs to render lines.
|
|
debugRenderer = new spine.SkeletonDebugRenderer(gl);
|
|
debugRenderer.drawRegionAttachments = true;
|
|
debugRenderer.drawBoundingBoxes = true;
|
|
debugRenderer.drawMeshHull = true;
|
|
debugRenderer.drawMeshTriangles = true;
|
|
debugRenderer.drawPaths = true;
|
|
debugShader = spine.Shader.newColored(gl);
|
|
shapes = new spine.ShapeRenderer(gl);
|
|
|
|
// Tell AssetManager to load the resources for each skeleton, including the exported data 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("spineboy-pro.skel");
|
|
assetManager.loadText("spineboy-pro.json");
|
|
assetManager.loadTextureAtlas("spineboy-pma.atlas");
|
|
assetManager.loadBinary("raptor-pro.skel");
|
|
assetManager.loadText("raptor-pro.json");
|
|
assetManager.loadTextureAtlas("raptor-pma.atlas");
|
|
assetManager.loadBinary("tank-pro.skel");
|
|
assetManager.loadText("tank-pro.json");
|
|
assetManager.loadTextureAtlas("tank-pma.atlas");
|
|
assetManager.loadBinary("goblins-pro.skel");
|
|
assetManager.loadText("goblins-pro.json");
|
|
assetManager.loadTextureAtlas("goblins-pma.atlas");
|
|
assetManager.loadBinary("vine-pro.skel");
|
|
assetManager.loadText("vine-pro.json");
|
|
assetManager.loadTextureAtlas("vine-pma.atlas");
|
|
assetManager.loadBinary("stretchyman-pro.skel");
|
|
assetManager.loadText("stretchyman-pro.json");
|
|
assetManager.loadTextureAtlas("stretchyman-pma.atlas");
|
|
assetManager.loadBinary("coin-pro.skel");
|
|
assetManager.loadText("coin-pro.json");
|
|
assetManager.loadTextureAtlas("coin-pma.atlas");
|
|
assetManager.loadBinary("mix-and-match-pro.skel");
|
|
assetManager.loadText("mix-and-match-pro.json");
|
|
assetManager.loadTextureAtlas("mix-and-match-pma.atlas");
|
|
requestAnimationFrame(load);
|
|
}
|
|
|
|
function load() {
|
|
// Wait until the AssetManager has loaded all resources, then load the skeletons.
|
|
if (assetManager.isLoadingComplete()) {
|
|
skeletons = {
|
|
coin: {
|
|
Binary: loadSkeleton("coin-pro.skel", "animation", true),
|
|
JSON: loadSkeleton("coin-pro.json", "animation", true)
|
|
},
|
|
goblins: {
|
|
Binary: loadSkeleton("goblins-pro.skel", "walk", true, "goblin"),
|
|
JSON: loadSkeleton("goblins-pro.json", "walk", true, "goblin")
|
|
},
|
|
"mix-and-match-pro": {
|
|
Binary: loadSkeleton("mix-and-match-pro.skel", "dance", true, "full-skins/girl-blue-cape"),
|
|
JSON: loadSkeleton("mix-and-match-pro.json", "dance", true, "full-skins/girl-blue-cape")
|
|
},
|
|
raptor: {
|
|
Binary: loadSkeleton("raptor-pro.skel", "walk", true),
|
|
JSON: loadSkeleton("raptor-pro.json", "walk", true)
|
|
},
|
|
spineboy: {
|
|
Binary: loadSkeleton("spineboy-pro.skel", "run", true),
|
|
JSON: loadSkeleton("spineboy-pro.json", "run", true)
|
|
},
|
|
stretchyman: {
|
|
Binary: loadSkeleton("stretchyman-pro.skel", "sneak", true),
|
|
JSON: loadSkeleton("stretchyman-pro.json", "sneak", true)
|
|
},
|
|
tank: {
|
|
Binary: loadSkeleton("tank-pro.skel", "drive", true),
|
|
JSON: loadSkeleton("tank-pro.json", "drive", true)
|
|
},
|
|
vine: {
|
|
Binary: loadSkeleton("vine-pro.skel", "grow", true),
|
|
JSON: loadSkeleton("vine-pro.json", "grow", true)
|
|
}
|
|
};
|
|
setupUI();
|
|
lastFrameTime = Date.now() / 1000;
|
|
requestAnimationFrame(render); // Loading is done, call render every frame.
|
|
} else
|
|
requestAnimationFrame(load);
|
|
}
|
|
|
|
function loadSkeleton(name, initialAnimation, premultipliedAlpha, skin) {
|
|
if (skin === undefined) skin = "default";
|
|
|
|
// Load the texture atlas using name.atlas from the AssetManager.
|
|
var atlas = assetManager.require(name.replace(/(?:-ess|-pro)\.(skel|json)/, "") + (premultipliedAlpha ? "-pma" : "") + ".atlas");
|
|
|
|
// Create an AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
|
|
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
|
|
|
// Create a skeleton loader instance for parsing the skeleton data file.
|
|
var skeletonLoader = name.endsWith(".skel") ? new spine.SkeletonBinary(atlasLoader) : new spine.SkeletonJson(atlasLoader);
|
|
|
|
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
|
|
skeletonLoader.scale = 1;
|
|
var skeletonData = skeletonLoader.readSkeletonData(assetManager.require(name));
|
|
var skeleton = new spine.Skeleton(skeletonData);
|
|
skeleton.setSkinByName(skin);
|
|
var bounds = calculateSetupPoseBounds(skeleton);
|
|
|
|
// Create an AnimationState, and set the initial animation in looping mode.
|
|
var animationStateData = new spine.AnimationStateData(skeleton.data);
|
|
var animationState = new spine.AnimationState(animationStateData);
|
|
if (name == "spineboy-pro.skel" || name == "spineboy-pro.json") {
|
|
animationStateData.setMix("walk", "run", 1.5)
|
|
animationStateData.setMix("run", "jump", 0.2)
|
|
animationStateData.setMix("jump", "run", 0.4);
|
|
animationState.setEmptyAnimation(0, 0);
|
|
var entry = animationState.addAnimation(0, "walk", true, 0);
|
|
entry.mixDuration = 1;
|
|
animationState.addAnimation(0, "run", true, 1.5);
|
|
animationState.addAnimation(0, "jump", false, 2);
|
|
animationState.addAnimation(0, "run", true, 0);
|
|
animationState.addEmptyAnimation(0, 1, 1);
|
|
entry = animationState.addAnimation(0, "walk", true, 1.5);
|
|
entry.mixDuration = 1;
|
|
} else
|
|
animationState.setAnimation(0, initialAnimation, true);
|
|
|
|
function log(message) {
|
|
if ($('#debug').is(':checked')) console.log(message);
|
|
}
|
|
animationState.addListener({
|
|
start: function (track) {
|
|
log("Animation on track " + track.trackIndex + " started");
|
|
},
|
|
interrupt: function (track) {
|
|
log("Animation on track " + track.trackIndex + " interrupted");
|
|
},
|
|
end: function (track) {
|
|
log("Animation on track " + track.trackIndex + " ended");
|
|
},
|
|
disposed: function (track) {
|
|
log("Animation on track " + track.trackIndex + " disposed");
|
|
},
|
|
complete: function (track) {
|
|
log("Animation on track " + track.trackIndex + " completed");
|
|
},
|
|
event: function (track, event) {
|
|
log("Event on track " + track.trackIndex + ": " + JSON.stringify(event));
|
|
}
|
|
})
|
|
|
|
// Pack everything up and return to caller.
|
|
return { skeleton: skeleton, state: animationState, bounds: bounds, premultipliedAlpha: premultipliedAlpha };
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
function setupUI() {
|
|
var formatList = $("#formatList");
|
|
formatList.append($("<option>Binary</option>"));
|
|
formatList.append($("<option>JSON</option>"));
|
|
var skeletonList = $("#skeletonList");
|
|
for (var skeletonName in skeletons) {
|
|
var option = $("<option></option>");
|
|
option.attr("value", skeletonName).text(skeletonName);
|
|
if (skeletonName === activeSkeleton) option.attr("selected", "selected");
|
|
skeletonList.append(option);
|
|
}
|
|
var effectList = $("#effectList");
|
|
var effects = ["None", "Swirl", "Jitter"];
|
|
for (var effect in effects) {
|
|
var effectName = effects[effect];
|
|
var option = $("<option></option>");
|
|
option.attr("value", effectName).text(effectName);
|
|
effectList.append(option);
|
|
}
|
|
var setupAnimationUI = function () {
|
|
var animationList = $("#animationList");
|
|
animationList.empty();
|
|
var skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
var state = skeletons[activeSkeleton][format].state;
|
|
var activeAnimation = state.tracks[0].animation.name;
|
|
for (var i = 0; i < skeleton.data.animations.length; i++) {
|
|
var name = skeleton.data.animations[i].name;
|
|
var option = $("<option></option>");
|
|
option.attr("value", name).text(name);
|
|
if (name === activeAnimation) option.attr("selected", "selected");
|
|
animationList.append(option);
|
|
}
|
|
|
|
animationList.change(function () {
|
|
var state = skeletons[activeSkeleton][format].state;
|
|
var skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
var animationName = $("#animationList option:selected").text();
|
|
skeleton.setToSetupPose();
|
|
state.setAnimation(0, animationName, true);
|
|
})
|
|
}
|
|
|
|
var setupSkinUI = function () {
|
|
var skinList = $("#skinList");
|
|
skinList.empty();
|
|
var skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
var activeSkin = skeleton.skin == null ? "default" : skeleton.skin.name;
|
|
for (var i = 0; i < skeleton.data.skins.length; i++) {
|
|
var name = skeleton.data.skins[i].name;
|
|
var option = $("<option></option>");
|
|
option.attr("value", name).text(name);
|
|
if (name === activeSkin) option.attr("selected", "selected");
|
|
skinList.append(option);
|
|
}
|
|
|
|
skinList.change(function () {
|
|
var skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
var skinName = $("#skinList option:selected").text();
|
|
skeleton.setSkinByName(skinName);
|
|
skeleton.setSlotsToSetupPose();
|
|
})
|
|
}
|
|
|
|
skeletonList.change(function () {
|
|
activeSkeleton = $("#skeletonList option:selected").text();
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
})
|
|
|
|
formatList.change(function () {
|
|
format = $("#formatList option:selected").text();
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
})
|
|
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
}
|
|
|
|
function render() {
|
|
var now = Date.now() / 1000;
|
|
var delta = now - lastFrameTime;
|
|
lastFrameTime = now;
|
|
|
|
// Update the MVP matrix to adjust for canvas size changes
|
|
resize();
|
|
|
|
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 = skeletons[activeSkeleton][format].skeleton;
|
|
var state = skeletons[activeSkeleton][format].state;
|
|
var bounds = skeletons[activeSkeleton][format].bounds;
|
|
var premultipliedAlpha = skeletons[activeSkeleton][format].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);
|
|
|
|
var effect = $("#effectList option:selected").text();
|
|
if (effect == "None") {
|
|
skeletonRenderer.vertexEffect = null;
|
|
} else if (effect == "Swirl") {
|
|
swirlTime += delta;
|
|
var percent = swirlTime % 2;
|
|
if (percent > 1) percent = 1 - (percent - 1);
|
|
swirlEffect.angle = pow2.apply(-60, 60, percent);
|
|
swirlEffect.centerX = bounds.offset.x + bounds.size.x / 2;
|
|
swirlEffect.centerY = bounds.offset.y + bounds.size.y / 2;
|
|
swirlEffect.radius = Math.sqrt(bounds.size.x * bounds.size.x + bounds.size.y * bounds.size.y) * 0.75;
|
|
skeletonRenderer.vertexEffect = swirlEffect;
|
|
} else if (effect == "Jitter")
|
|
skeletonRenderer.vertexEffect = jitterEffect;
|
|
|
|
skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
|
|
skeletonRenderer.draw(batcher, skeleton);
|
|
batcher.end();
|
|
|
|
shader.unbind();
|
|
|
|
// Draw debug information.
|
|
var debug = $('#debug').is(':checked');
|
|
if (debug) {
|
|
debugShader.bind();
|
|
debugShader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values);
|
|
debugRenderer.premultipliedAlpha = premultipliedAlpha;
|
|
shapes.begin(debugShader);
|
|
debugRenderer.draw(shapes, skeleton);
|
|
shapes.end();
|
|
debugShader.unbind();
|
|
}
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
function resize() {
|
|
var w = canvas.clientWidth;
|
|
var h = canvas.clientHeight;
|
|
if (canvas.width != w || canvas.height != h) {
|
|
canvas.width = w;
|
|
canvas.height = h;
|
|
}
|
|
|
|
// Calculations to center the skeleton in the canvas.
|
|
var bounds = skeletons[activeSkeleton][format].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) * 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();
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |