Hey everyone, I’m working on a tiny pixel-art runner in canvas, and on my laptop it looks fine at first but after a minute the walk cycle and movement feel slightly out of sync on a 120hz display. I tried keeping movement frame-rate independent, but if I round positions for crisp pixels the motion gets uneven.
let last = performance.now();
let acc = 0;
const step = 1000 / 12;
let frame = 0;
let x = 0;
function loop(now) {
const dt = now - last;
last = now;
acc += dt;
x += 90 * (dt / 1000);
while (acc >= step) {
frame = (frame + 1) % 6;
acc -= step;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSprite(Math.round(x), 20, frame);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Should I be separating sprite frame timing from world movement in a different way so pixel art stays crisp without the animation slowly drifting?
@BayMax yep, the drift is basically from running two clocks: x moves from dt, but the walk cycle advances from acc. Once you round draw positions, Math.round(x) can visually sit on the same pixel for a frame or two while the sprite frame keeps changing, and 120Hz makes that mismatch easier to spot.
For a walk cycle, tie the frame to distance traveled and keep x as a float the whole time. Only round when you draw:
js
let last = performance.now();
let x = 0;
const speed = 90;
const stridePx = 8; // tune this to match one animation step
function loop(now) {
const dt = now - last;
last = now;
x += speed * (dt / 1000);
const frame = Math.floor(x / stridePx) % 6;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSprite(Math.round(x), 20, frame);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
That way the feet only advance when the character has actually moved far enough, so snapping stays crisp without the walk cycle wandering off on its own. I’d only keep separate timers for stuff like idle, blinking, or jump/apex animations.
@ArthurDent your stridePx = 8 example is the right fix, and I’d also make sure that value matches the actual foot-contact spacing in the sprite sheet because if the art says 7px but the code says 8, the desync comes back as a slow phase error instead of a timer bug.