Your code does many things that can not work. Let's start with:
function setPosition(pos) {
container.style.left = pos.x + "px";
container.style.top = pos.y + "px";
}
function playAnimation(animation) {
if (player) {
player.animationState?.clearTracks();
let loop = false;
if (animation.includes("idle")) {
loop = true;
} else {
loop = false;
}
player.setAnimation(animation, loop);
player.play();
}
}
function handlePosAction(pos, animation) {
setTimeout(() => {
setPosition(coords[pos]);
playAnimation(animation);
}, 0);
}
function handleMove() {
remainingSteps = 1;
handlePosAction(curPos, "right_move");
}
window.onload = () => {
const animation = coords[curPos].direction + "_idle";
handlePosAction(curPos, animation);
};
When window.onload()
is called, you call handlePosAction()
which in turn calls playAnimation()
in a setTimeout()
handler. Depending on various server/browser related factors, the player and skeleton may not have been loaded at that time, resulting in an error, e.g.
Uncaught TypeError: Cannot read properties of null (reading 'data')
at SpinePlayer.setViewport (Player.ts:724:40)
at SpinePlayer.setAnimation (Player.ts:711:20)
at playAnimation ((index):167:16)
at (index):175:9
On my machine, I do not see the skeleton when I load the page because of that.
You need to move the logic from window.onload()
into the success
handler of the Spine player. This also allows us to remove the setTimeout()
call in handlePosAction()
.
You also queue an empty animation. The complete listener will be called for that empty animation as well. Empty animations do not have their animation
field set, so the handler code in complete fails. I added a check.
The default mix duration is set to something != 0. This means that when switching from one animation to the next, there will be interpolation. Your skeleton is extremely complicated and when previewing it in Spine in the preview pane, switching between animations, one can see that the transitions between animations are broken. This is not something the Spine player can fix. That is something you need to fix in your Spine project in the Spine Editor. @Misaki can possibly help you with that. In any case, I've set the default mix duration to 0.
Finally, in handlePosAction()
you call setPosition()
before playAnimation()
. setPosition()
will immediately move the <div>
containing the <canvas>
, while the player hasn't rendered a new frame with the new animation yet. This results in a noticable flicker. It can be fixed by first playing the new animation, and then moving the <div>
in the next frame.
Here's the entire fixed code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>spine</title>
<script src="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/spine-player.css" />
<style>
body {
margin: 0;
padding: 0;
background-color: bisque;
}
.grid {
position: relative;
left: 150px;
top: 400px;
display: flex;
flex-wrap: wrap;
width: 600px;
height: 60px;
}
.item {
width: 80px;
height: 60px;
box-sizing: border-box;
border: 1px solid green;
}
.container {
position: absolute;
left: -30px;
top: -10px;
width: 300px;
height: 200px;
}
.btn {
position: absolute;
left: 200px;
top: 600px;
width: 100px;
height: 40px;
}
</style>
</head>
<body>
<div class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div id="container" class="container"></div>
</div>
<div>
<button type="button" class="btn" onclick="handleMove()">Move</button>
</div>
<script>
const coords = [
{
x: -30,
y: -10,
direction: "right",
},
{
x: 50,
y: -10,
direction: "front",
},
{
x: 130,
y: -10,
direction: "right",
},
{
x: 210,
y: -10,
direction: "right",
},
{
x: 290,
y: -10,
direction: "right",
},
{
x: 370,
y: -10,
direction: "right",
},
{
x: 370,
y: -70,
direction: "front",
},
];
const player = new spine.SpinePlayer("container", {
skeleton: "./spine/yuyan.json",
atlas: "./spine/yuyan.atlas",
alpha: true,
backgroundColor: "#00000000",
showControls: false,
viewport: {
debugRender: true,
padTop: 0,
padLeft: 0,
padBottom: 0,
padRight: 0,
x: -300,
y: -200,
width: 600,
height: 400,
},
success: (player) => {
const animation = coords[curPos].direction + "_idle";
handlePosAction(curPos, animation);
player.animationState.data.defaultMix = 0;
player.animationState?.addListener({
complete: function (entry) {
if (!entry.animation) return; // empty animation, do nothing
if (entry.animation.name.includes("move")) {
remainingSteps--;
curPos++;
if (remainingSteps > 0) {
const animation = coords[curPos].direction + "_move";
handlePosAction(curPos, animation);
} else {
const animation = coords[curPos].direction + "_idle";
handlePosAction(curPos, animation);
}
}
},
});
},
});
let curPos = 0;
let remainingSteps = 0;
function setPosition(pos) {
container.style.left = pos.x + "px";
container.style.top = pos.y + "px";
}
function playAnimation(animation) {
if (player) {
if (animation.includes("idle")) {
loop = true;
} else {
loop = false;
}
player.setAnimation(animation, loop);
player.play();
}
}
function handlePosAction(pos, animation) {
playAnimation(animation);
requestAnimationFrame(() => setPosition(coords[pos]));
}
function handleMove() {
remainingSteps = 1;
handlePosAction(curPos, "right_move");
}
</script>
</body>
</html>
Now, this is clearly going to be some kind of game. I would STRONGLY suggest to not use Spine player for games. Please use spine-phaser or spine-pixi.