mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
Merge branch '4.0' into 4.1-beta
# Conflicts: # spine-ts/package-lock.json # spine-ts/package.json # spine-ts/spine-canvas/package.json # spine-ts/spine-core/package.json # spine-ts/spine-player/package.json # spine-ts/spine-threejs/package.json # spine-ts/spine-webgl/package.json
This commit is contained in:
commit
3b1f7617df
@ -732,6 +732,8 @@
|
|||||||
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
|
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
|
||||||
* Added IK softness.
|
* Added IK softness.
|
||||||
* Added `AssetManager.setRawDataURI(path, data)`. Allows to embed data URIs for skeletons, atlases and atlas page images directly in the HTML/JS without needing to load it from a separate file.
|
* Added `AssetManager.setRawDataURI(path, data)`. Allows to embed data URIs for skeletons, atlases and atlas page images directly in the HTML/JS without needing to load it from a separate file.
|
||||||
|
* Added `AssetManager.loadAll()` to allow Promise/async/await based waiting for completion of asset load. See the `spine-canvas` examples.
|
||||||
|
* Added `Skeleton.getBoundRect()` helper method to calculate the bouding rectangle of the current pose, returning the result as `{ x, y, width, height }`. Note that this method will create temporary objects which can add to garbage collection pressure.
|
||||||
|
|
||||||
### WebGL backend
|
### WebGL backend
|
||||||
* `Input` can now take a partially defined implementation of `InputListener`.
|
* `Input` can now take a partially defined implementation of `InputListener`.
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<li>Canvas</li>
|
<li>Canvas</li>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/spine-canvas/example">Example</a></li>
|
<li><a href="/spine-canvas/example">Example</a></li>
|
||||||
|
<li><a href="/spine-canvas/example/mouse-click.html">Mouse click</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<li>Player</li>
|
<li>Player</li>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@ -1,181 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<script src="../dist/iife/spine-canvas.js"></script>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
<head>
|
||||||
html {
|
<!--<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.0.*/dist/iife/spine-canvas.js"></script>-->
|
||||||
height: 100%
|
<script src="../dist/iife/spine-canvas.js"></script>
|
||||||
}
|
</head>
|
||||||
|
|
||||||
canvas {
|
<body style="margin: 0; padding: 0;">
|
||||||
position: absolute;
|
<canvas id="canvas" style="width: 100%; height: 100vh;"></canvas>
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let lastFrameTime = Date.now() / 1000;
|
||||||
|
let canvas, context;
|
||||||
|
let assetManager;
|
||||||
|
let skeleton, animationState, bounds;
|
||||||
|
let skeletonRenderer;
|
||||||
|
|
||||||
var lastFrameTime = Date.now() / 1000;
|
async function load() {
|
||||||
var canvas, context;
|
|
||||||
var assetManager;
|
|
||||||
var skeleton, state, bounds;
|
|
||||||
var skeletonRenderer;
|
|
||||||
|
|
||||||
var skelName = "spineboy-ess";
|
|
||||||
var animName = "walk";
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
canvas = document.getElementById("canvas");
|
canvas = document.getElementById("canvas");
|
||||||
canvas.width = window.innerWidth;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
context = canvas.getContext("2d");
|
context = canvas.getContext("2d");
|
||||||
|
|
||||||
skeletonRenderer = new spine.SkeletonRenderer(context);
|
skeletonRenderer = new spine.SkeletonRenderer(context);
|
||||||
// enable debug rendering
|
|
||||||
skeletonRenderer.debugRendering = true;
|
|
||||||
// enable the triangle renderer, supports meshes, but may produce artifacts in some browsers
|
|
||||||
skeletonRenderer.triangleRendering = false;
|
|
||||||
|
|
||||||
assetManager = new spine.AssetManager("assets/");
|
// Load the assets.
|
||||||
|
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
|
||||||
|
assetManager.loadText("spineboy-ess.json");
|
||||||
|
assetManager.loadTextureAtlas("spineboy.atlas");
|
||||||
|
await assetManager.loadAll();
|
||||||
|
|
||||||
assetManager.loadText(skelName + ".json");
|
// Create the texture atlas and skeleton data.
|
||||||
assetManager.loadText(skelName.replace("-pro", "").replace("-ess", "") + ".atlas");
|
let atlas = assetManager.require("spineboy.atlas");
|
||||||
assetManager.loadTexture(skelName.replace("-pro", "").replace("-ess", "") + ".png");
|
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||||
|
let skeletonJson = new spine.SkeletonJson(atlasLoader);
|
||||||
|
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
|
||||||
|
|
||||||
requestAnimationFrame(load);
|
// Instantiate a new skeleton based on the atlas and skeleton data.
|
||||||
}
|
skeleton = new spine.Skeleton(skeletonData);
|
||||||
|
|
||||||
function load() {
|
|
||||||
if (assetManager.isLoadingComplete()) {
|
|
||||||
var data = loadSkeleton(skelName, animName, "default");
|
|
||||||
skeleton = data.skeleton;
|
|
||||||
state = data.state;
|
|
||||||
bounds = data.bounds;
|
|
||||||
requestAnimationFrame(render);
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(load);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSkeleton(name, initialAnimation, skin) {
|
|
||||||
if (skin === undefined) skin = "default";
|
|
||||||
|
|
||||||
// Load the texture atlas using name.atlas and name.png from the AssetManager.
|
|
||||||
// The function passed to TextureAtlas is used to resolve relative paths.
|
|
||||||
atlas = new spine.TextureAtlas(assetManager.require(name.replace("-pro", "").replace("-ess", "") + ".atlas"));
|
|
||||||
atlas.setTextures(assetManager);
|
|
||||||
|
|
||||||
// Create a AtlasAttachmentLoader, which is specific to the WebGL backend.
|
|
||||||
atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
|
||||||
|
|
||||||
// Create a SkeletonJson instance for parsing the .json file.
|
|
||||||
var skeletonJson = new spine.SkeletonJson(atlasLoader);
|
|
||||||
|
|
||||||
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
|
|
||||||
var skeletonData = skeletonJson.readSkeletonData(assetManager.require(name + ".json"));
|
|
||||||
var skeleton = new spine.Skeleton(skeletonData);
|
|
||||||
skeleton.scaleY = -1;
|
|
||||||
var bounds = calculateBounds(skeleton);
|
|
||||||
skeleton.setSkinByName(skin);
|
|
||||||
|
|
||||||
// Create an AnimationState, and set the initial animation in looping mode.
|
|
||||||
var animationState = new spine.AnimationState(new spine.AnimationStateData(skeleton.data));
|
|
||||||
animationState.setAnimation(0, initialAnimation, true);
|
|
||||||
animationState.addListener({
|
|
||||||
event: function (trackIndex, event) {
|
|
||||||
// console.log("Event on track " + trackIndex + ": " + JSON.stringify(event));
|
|
||||||
},
|
|
||||||
complete: function (trackIndex, loopCount) {
|
|
||||||
// console.log("Animation on track " + trackIndex + " completed, loop count: " + loopCount);
|
|
||||||
},
|
|
||||||
start: function (trackIndex) {
|
|
||||||
// console.log("Animation on track " + trackIndex + " started");
|
|
||||||
},
|
|
||||||
end: function (trackIndex) {
|
|
||||||
// console.log("Animation on track " + trackIndex + " ended");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Pack everything up and return to caller.
|
|
||||||
return { skeleton: skeleton, state: animationState, bounds: bounds };
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateBounds(skeleton) {
|
|
||||||
var data = skeleton.data;
|
|
||||||
skeleton.setToSetupPose();
|
skeleton.setToSetupPose();
|
||||||
skeleton.updateWorldTransform();
|
skeleton.updateWorldTransform();
|
||||||
var offset = new spine.Vector2();
|
bounds = skeleton.getBoundsRect();
|
||||||
var size = new spine.Vector2();
|
|
||||||
skeleton.getBounds(offset, size, []);
|
// Setup an animation state with a default mix of 0.2 seconds.
|
||||||
return { offset: offset, size: size };
|
var animationStateData = new spine.AnimationStateData(skeleton.data);
|
||||||
|
animationStateData.defaultMix = 0.2;
|
||||||
|
animationState = new spine.AnimationState(animationStateData);
|
||||||
|
|
||||||
|
// Start rendering.
|
||||||
|
requestAnimationFrame(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
// Calculate the delta time between this and the last frame in seconds.
|
||||||
var now = Date.now() / 1000;
|
var now = Date.now() / 1000;
|
||||||
var delta = now - lastFrameTime;
|
var delta = now - lastFrameTime;
|
||||||
lastFrameTime = now;
|
lastFrameTime = now;
|
||||||
|
|
||||||
resize();
|
// Resize the canvas drawing buffer if the canvas CSS width and height changed
|
||||||
|
// and clear the canvas.
|
||||||
|
if (canvas.width != canvas.clientWidth || canvas.height != canvas.clientHeight) {
|
||||||
|
canvas.width = canvas.clientWidth;
|
||||||
|
canvas.height = canvas.clientHeight;
|
||||||
|
}
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
context.save();
|
// Center the skeleton and resize it so it fits inside the canvas.
|
||||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
skeleton.x = canvas.width / 2;
|
||||||
context.fillStyle = "#cccccc";
|
skeleton.y = canvas.height - canvas.height * 0.1;
|
||||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
let scale = canvas.height / bounds.height * 0.8;
|
||||||
context.restore();
|
skeleton.scaleX = scale;
|
||||||
|
skeleton.scaleY = -scale;
|
||||||
|
|
||||||
state.update(delta);
|
// Update and apply the animation state, update the skeleton's
|
||||||
state.apply(skeleton);
|
// world transforms and render the skeleton.
|
||||||
|
animationState.update(delta);
|
||||||
|
animationState.apply(skeleton);
|
||||||
skeleton.updateWorldTransform();
|
skeleton.updateWorldTransform();
|
||||||
skeletonRenderer.draw(skeleton);
|
skeletonRenderer.draw(skeleton);
|
||||||
|
|
||||||
context.strokeStyle = "green";
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(-1000, 0);
|
|
||||||
context.lineTo(1000, 0);
|
|
||||||
context.moveTo(0, -1000);
|
|
||||||
context.lineTo(0, 1000);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
requestAnimationFrame(render);
|
requestAnimationFrame(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resize() {
|
load();
|
||||||
var w = canvas.clientWidth;
|
|
||||||
var h = canvas.clientHeight;
|
|
||||||
if (canvas.width != w || canvas.height != h) {
|
|
||||||
canvas.width = w;
|
|
||||||
canvas.height = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// magic
|
|
||||||
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;
|
|
||||||
|
|
||||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
||||||
context.scale(1 / scale, 1 / scale);
|
|
||||||
context.translate(-centerX, -centerY);
|
|
||||||
context.translate(width / 2, height / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
init();
|
|
||||||
}());
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
111
spine-ts/spine-canvas/example/mouse-click.html
Normal file
111
spine-ts/spine-canvas/example/mouse-click.html
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!--<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.0.*/dist/iife/spine-canvas.js"></script>-->
|
||||||
|
<script src="../dist/iife/spine-canvas.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="margin: 0; padding: 0;">
|
||||||
|
<canvas id="canvas" style="width: 100%; height: 100vh;"></canvas>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let lastFrameTime = Date.now() / 1000;
|
||||||
|
let canvas, context;
|
||||||
|
let assetManager;
|
||||||
|
let skeleton, animationState, bounds;
|
||||||
|
let skeletonRenderer;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
canvas = document.getElementById("canvas");
|
||||||
|
context = canvas.getContext("2d");
|
||||||
|
skeletonRenderer = new spine.SkeletonRenderer(context);
|
||||||
|
|
||||||
|
// Load the assets.
|
||||||
|
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
|
||||||
|
assetManager.loadText("spineboy-ess.json");
|
||||||
|
assetManager.loadTextureAtlas("spineboy.atlas");
|
||||||
|
await assetManager.loadAll();
|
||||||
|
|
||||||
|
// Create the texture atlas and skeleton data.
|
||||||
|
let atlas = assetManager.require("spineboy.atlas");
|
||||||
|
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||||
|
let skeletonJson = new spine.SkeletonJson(atlasLoader);
|
||||||
|
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
|
||||||
|
|
||||||
|
// Instantiate a new skeleton based on the atlas and skeleton data.
|
||||||
|
skeleton = new spine.Skeleton(skeletonData);
|
||||||
|
skeleton.setToSetupPose();
|
||||||
|
skeleton.updateWorldTransform();
|
||||||
|
bounds = skeleton.getBoundsRect();
|
||||||
|
|
||||||
|
// Setup an animation state with a default mix of 0.2 seconds.
|
||||||
|
var animationStateData = new spine.AnimationStateData(skeleton.data);
|
||||||
|
animationStateData.defaultMix = 0.2;
|
||||||
|
animationState = new spine.AnimationState(animationStateData);
|
||||||
|
|
||||||
|
// Add a click listener to the canvas which checks if Spineboy's head
|
||||||
|
// was clicked.
|
||||||
|
canvas.addEventListener('click', event => {
|
||||||
|
// Make the mouse click coordinates relative to the canvas.
|
||||||
|
let canvasRect = canvas.getBoundingClientRect();
|
||||||
|
var mouseX = event.x - canvasRect.x;
|
||||||
|
var mouseY = event.y - canvasRect.y;
|
||||||
|
|
||||||
|
// Find the "head" bone.
|
||||||
|
var headBone = skeleton.findBone("head");
|
||||||
|
|
||||||
|
// If the mouse pointer is within 100 pixels of the head bone, fire the jump animation event.
|
||||||
|
// Afterwards, loop the run animation.
|
||||||
|
if (pointInCircle(mouseX, mouseY, headBone.worldX, headBone.worldY, 100)) {
|
||||||
|
var jumpEntry = animationState.setAnimation(0, "jump", false);
|
||||||
|
var walkEntry = animationState.addAnimation(0, "run", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
// Calculate the delta time between this and the last frame in seconds.
|
||||||
|
var now = Date.now() / 1000;
|
||||||
|
var delta = now - lastFrameTime;
|
||||||
|
lastFrameTime = now;
|
||||||
|
|
||||||
|
// Resize the canvas drawing buffer if the canvas CSS width and height changed
|
||||||
|
// and clear the canvas.
|
||||||
|
if (canvas.width != canvas.clientWidth || canvas.height != canvas.clientHeight) {
|
||||||
|
canvas.width = canvas.clientWidth;
|
||||||
|
canvas.height = canvas.clientHeight;
|
||||||
|
}
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Center the skeleton and resize it so it fits inside the canvas.
|
||||||
|
skeleton.x = canvas.width / 2;
|
||||||
|
skeleton.y = canvas.height - canvas.height * 0.1;
|
||||||
|
let scale = canvas.height / bounds.height * 0.8;
|
||||||
|
skeleton.scaleX = scale;
|
||||||
|
skeleton.scaleY = -scale;
|
||||||
|
|
||||||
|
// Update and apply the animation state, update the skeleton's
|
||||||
|
// world transforms and render the skeleton.
|
||||||
|
animationState.update(delta);
|
||||||
|
animationState.apply(skeleton);
|
||||||
|
skeleton.updateWorldTransform();
|
||||||
|
skeletonRenderer.draw(skeleton);
|
||||||
|
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the point given by x/y are within the circle.
|
||||||
|
function pointInCircle(x, y, circleX, circleY, circleRadius) {
|
||||||
|
var distX = x - circleX;
|
||||||
|
var distY = y - circleY;
|
||||||
|
return distX * distX + distY * distY <= circleRadius * circleRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -65,6 +65,21 @@ export class AssetManagerBase implements Disposable {
|
|||||||
if (callback) callback(path, message);
|
if (callback) callback(path, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadAll () {
|
||||||
|
let promise = new Promise((resolve: (assetManager: AssetManagerBase) => void, reject: (errors: StringMap<string>) => void) => {
|
||||||
|
let check = () => {
|
||||||
|
if (this.isLoadingComplete()) {
|
||||||
|
if (this.hasErrors()) reject(this.errors);
|
||||||
|
else resolve(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
setRawDataURI (path: string, data: string) {
|
setRawDataURI (path: string, data: string) {
|
||||||
this.downloader.rawDataUris[this.pathPrefix + path] = data;
|
this.downloader.rawDataUris[this.pathPrefix + path] = data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -580,6 +580,15 @@ export class Skeleton {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose as `{ x: number, y: number, width: number, height: number }`.
|
||||||
|
* Note that this method will create temporary objects which can add to garbage collection pressure. Use `getBounds()` if garbage collection is a concern. */
|
||||||
|
getBoundsRect () {
|
||||||
|
let offset = new Vector2();
|
||||||
|
let size = new Vector2();
|
||||||
|
this.getBounds(offset, size);
|
||||||
|
return { x: offset.x, y: offset.y, width: size.x, height: size.y };
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
|
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
|
||||||
* @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
|
* @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
|
||||||
* @param size An output value, the width and height of the AABB.
|
* @param size An output value, the width and height of the AABB.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user