2025-12-11 15:35:31 +01:00

223 lines
8.0 KiB
JavaScript

class App {
constructor() {
this.skeleton = null;
this.animationState = null;
this.canvas = null;
}
loadAssets(canvas) {
this.canvas = canvas;
// Load assets of Spineboy.
canvas.assetManager.loadBinary("/assets/spineboy-pro.skel");
canvas.assetManager.loadTextureAtlas("/assets/spineboy-pma.atlas");
}
initialize(canvas) {
// Load the Spineboy skeleton
this.loadSkeleton("/assets/spineboy-pro.skel", "/assets/spineboy-pma.atlas", "run");
// Setup listener for animation selection box
let animationSelectBox = document.body.querySelector("#animations");
animationSelectBox.onchange = () => {
this.animationState.setAnimation(0, animationSelectBox.value, true);
}
// Setup the drag and drop listener
new FileDragAndDrop(canvas.htmlCanvas, (files) => this.onDrop(files))
// Setup a camera controller for paning and zooming
new spine.CameraController(canvas.htmlCanvas, canvas.renderer.camera);
}
onDrop(files) {
let atlasFile;
let skeletonFile;
let pngs = [];
let assetManager = this.canvas.assetManager;
// We use data URIs to load the dropped files. Some file types
// are binary, so we have to encode them to base64 for loading
// through AssetManager.
let bufferToBase64 = (buffer) => {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
for (var file of files) {
if (file.name.endsWith(".atlas") || file.name.endsWith(".atlas.txt")) {
atlasFile = file;
assetManager.setRawDataURI(file.name, "data:text/plain;," + file.contentText);
} else if (file.name.endsWith(".skel")) {
skeletonFile = file;
assetManager.setRawDataURI(file.name, "data:application/octet-stream;base64," + bufferToBase64(file.contentBinary));
assetManager.loadBinary(file.name);
} else if (file.name.endsWith(".json")) {
skeletonFile = file;
assetManager.setRawDataURI(file.name, "data:text/plain;," + file.contentText);
assetManager.loadJson(file.name);
} else if (file.name.endsWith(".png")) {
pngs.push(file);
assetManager.setRawDataURI(file.name, "data:image/png;base64," + bufferToBase64(file.contentBinary));
}
}
if (!atlasFile) {
alert("Please provide a .atlas or .atlas.txt atlas file.");
return;
}
if (pngs.length == 0) {
alert("Please provide the atlas page .png file(s).");
}
if (!skeletonFile) {
alert("Please provide a .skel or .json skeleton file.");
return;
}
assetManager.loadTextureAtlas(atlasFile.name);
let waitForLoad = () => {
if (this.canvas.assetManager.isLoadingComplete()) {
this.loadSkeleton(skeletonFile.name, atlasFile.name);
} else {
requestAnimationFrame(waitForLoad);
}
}
waitForLoad();
}
loadSkeleton(skeletonFile, atlasFile, animationName) {
// Load the skeleton and setup the animation state
let assetManager = this.canvas.assetManager;
var atlas = assetManager.require(atlasFile);
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
var skeletonData;
var skeletonBinaryOrJson = skeletonFile.endsWith(".skel") ?
new spine.SkeletonBinary(atlasLoader) :
new spine.SkeletonJson(atlasLoader);
skeletonBinaryOrJson.scale = 1;
skeletonData = skeletonBinaryOrJson.readSkeletonData(assetManager.require(skeletonFile));
this.skeleton = new spine.Skeleton(skeletonData);
var animationStateData = new spine.AnimationStateData(skeletonData);
this.animationState = new spine.AnimationState(animationStateData);
// Fill the animation selection box.
let animationSelectBox = document.body.querySelector("#animations");
animationSelectBox.innerHTML = "";
for (var animation of this.skeleton.data.animations) {
if (!animationName) animationName = animation.name;
let option = document.createElement("option");
option.value = option.innerText = animation.name;
option.selected = animation.name == animationName;
animationSelectBox.appendChild(option);
}
this.animationState.setAnimation(0, animationName, true);
// Center the skeleton in the viewport
this.centerSkeleton();
}
centerSkeleton() {
// Calculate the bounds of the skeleton
this.animationState.update(0);
this.animationState.apply(this.skeleton);
this.skeleton.updateWorldTransform(spine.Physics.update);
let offset = new spine.Vector2(), size = new spine.Vector2();
this.skeleton.getBounds(offset, size);
// Make sure the canvas is sized properly and position and zoom
// the camera so the skeleton is centered in the viewport.
let renderer = this.canvas.renderer;
renderer.resize(spine.ResizeMode.Expand);
let camera = this.canvas.renderer.camera;
camera.position.x = offset.x + size.x / 2;
camera.position.y = offset.y + size.y / 2;
camera.zoom = size.x > size.y ? size.x / this.canvas.htmlCanvas.width * 3 : size.y / this.canvas.htmlCanvas.height * 3;
camera.update();
}
update(canvas, delta) {
this.animationState.update(delta);
this.animationState.apply(this.skeleton);
this.skeleton.updateWorldTransform(spine.Physics.update);
}
render(canvas) {
let renderer = canvas.renderer;
renderer.resize(spine.ResizeMode.Expand);
canvas.clear(0.2, 0.2, 0.2, 1);
renderer.begin();
renderer.line(-10000, 0, 10000, 0, spine.Color.RED);
renderer.line(0, -10000, 0, 10000, spine.Color.GREEN);
renderer.drawSkeleton(this.skeleton);
renderer.end();
}
}
new spine.SpineCanvas(document.getElementById("canvas"), {
app: new App(),
webglConfig: {
alpha: false
}
});
class FileDragAndDrop {
constructor(element, callback) {
this.callback = callback;
element.ondrop = (ev) => this.onDrop(ev);
element.ondragover = (ev) => ev.preventDefault();
}
async onDrop(event) {
event.preventDefault();
event.stopPropagation();
const items = Object.keys(event.dataTransfer.items);
let files = [];
await Promise.all(items.map(async (key) => {
var file = event.dataTransfer.items[key].getAsFile();
if (file.kind == "string") return;
let contentBinary = await file.arrayBuffer();
let contentText = await file.text();
files.push({ name: file.name, contentBinary: contentBinary, contentText: contentText });
}));
this.callback(files);
}
}
// Shim for older browsers for File/Blob.arrayBuffer() and .text()
(function () {
function arrayBuffer() {
return new Promise(function () {
let fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsArrayBuffer();
})
}
function text() {
return new Promise(function () {
let fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsText(this);
})
}
if ('File' in self) {
File.prototype.arrayBuffer = File.prototype.arrayBuffer || arrayBuffer;
File.prototype.text = File.prototype.text || text;
}
Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || arrayBuffer;
Blob.prototype.text = Blob.prototype.text || text;
})();