Skip to content

Animate sprites using spritesheets and frame-based animation.

What you’ll learn

  • How to load a spritesheet (TexturePacker / Aseprite JSON) and define an animationSet
  • How to drive playback with play / pause / resume / stop plus onFrame and onComplete callbacks
  • How update(deltaMs) fits into your render loop and how speed scales it

three-flatland supports frame-based animation using spritesheets. The AnimatedSprite2D class works with SpriteSheetLoader to load and play animations.

If you’ve ever wired up sprite animation by hand — frame timers, wraparound math, swap textures on each tick — this is the part you don’t have to write anymore.

The animation data pathatlas frames are grouped into named clips, then a timeline advances the active clip over delta timeSpriteSheet (atlas)SpriteSheetLoader (Aseprite JSON)idle_0idle_1run_0run_1every cell is a named frame:getFrame('idle_0') returns itsatlas rect { x, y, w, h }.The sheet is just storage —it has no notion of clips,timing, or playback. Thoselive one level up.animationSet (clips)named clips reference frames by nameidleframes: idle_0..3 fps: 8 loop: truerunframes: run_0..3 fps: 12 loop: truejumpframes: jump_0..2 fps: 10 loop: falsegroup bynameplayback timelineplay('run') selects the active cliprun_0 run_1 run_2 run_3each step = 1000 / fps ms (12 fps -> 83 ms)update(deltaMs) per frameaccumulates time, advances the frameindex, scaled by speedloop: true -> wraps to frame 0loop: false -> holds last frame, fires onCompleteonFrame(i) fires on every frame advanceplay(name)select clip
The animation data path — atlas frames are grouped into named clips, then play() picks the active clip and update(deltaMs) advances its frames over time.
import { AnimatedSprite2D, SpriteSheetLoader, Layers } from 'three-flatland';
// Load spritesheet (texture presets automatically applied)
const spriteSheet = await SpriteSheetLoader.load('/sprites/character.json');
// Create animated sprite
const sprite = new AnimatedSprite2D({
spriteSheet,
animationSet: {
fps: 10, // Default fps for all animations
animations: {
idle: {
frames: ['idle_0', 'idle_1', 'idle_2', 'idle_3'],
fps: 8,
loop: true,
},
run: {
frames: ['run_0', 'run_1', 'run_2', 'run_3'],
fps: 12,
loop: true,
},
jump: {
frames: ['jump_0', 'jump_1', 'jump_2'],
fps: 10,
loop: false,
},
},
},
animation: 'idle', // Start with idle animation
layer: Layers.ENTITIES,
});
sprite.scale.set(64, 64, 1);
scene.add(sprite);
// In your animation loop
function animate() {
const deltaMs = /* time since last frame */;
sprite.update(deltaMs);
renderer.render(scene, camera);
}

The animationSet option provides a structured way to define multiple animations:

animationSet: {
fps: 12, // Default fps (optional)
animations: {
idle: {
frames: ['frame_0', 'frame_1', 'frame_2'], // Frame names from spritesheet
fps: 8, // Override default fps (optional)
loop: true, // Loop animation (default: true)
pingPong: false, // Play forward then backward (optional)
},
attack: {
frames: ['attack_0', 'attack_1', 'attack_2'],
fps: 15,
loop: false,
},
},
}
sprite.play('run'); // Play animation by name
sprite.pause(); // Pause at current frame
sprite.resume(); // Resume paused animation
sprite.stop(); // Stop and reset
sprite.gotoFrame(3); // Jump to specific frame
// Check animation state
sprite.isPlaying(); // Is any animation playing?
sprite.isPlaying('run'); // Is 'run' animation playing?
sprite.currentAnimation; // Get current animation name
sprite.speed = 1.5; // 1.5x speed
sprite.speed = 0.5; // Half speed
sprite.speed = 2; // Double speed

Use the play() method’s options to handle animation events:

sprite.play('attack', {
onFrame: (frameIndex) => {
console.log('Frame:', frameIndex);
if (frameIndex === 2) {
// Trigger damage on specific frame
dealDamage();
}
},
onComplete: () => {
// Animation finished (non-looping only)
sprite.play('idle');
},
});

The SpriteSheetLoader supports Aseprite JSON format. Texture presets (like NearestFilter for pixel art) are automatically applied:

import { SpriteSheetLoader } from 'three-flatland';
// Load spritesheet (presets automatically applied)
const spriteSheet = await SpriteSheetLoader.load('/sprites/character.json');
// Access frame data
const frame = spriteSheet.getFrame('idle_0');
console.log(frame); // { x, y, width, height, sourceWidth, sourceHeight, ... }

The update(deltaMs) method must be called each frame to advance the animation:

let lastTime = performance.now();
function animate() {
const now = performance.now();
const deltaMs = now - lastTime;
lastTime = now;
// Update all animated sprites
sprite.update(deltaMs);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}