What you’ll learn
- How to load a spritesheet (TexturePacker / Aseprite JSON) and define an
animationSet - How to drive playback with
play / pause / resume / stopplusonFrameandonCompletecallbacks - How
update(deltaMs)fits into your render loop and howspeedscales it - How to advance a large
SpriteGrouponce while keeping time in your application
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.
import { AnimatedSprite2D, SpriteSheetLoader, SortLayers } from 'three-flatland';
// Load spritesheet (texture presets automatically applied)const spriteSheet = await SpriteSheetLoader.load('/sprites/character.json');
// Create animated spriteconst 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 sortLayer: SortLayers.ENTITIES,});
sprite.scale.set(64, 64, 1);scene.add(sprite);
// In your animation loopfunction animate() { const deltaMs = /* time since last frame */; sprite.update(deltaMs); renderer.render(scene, camera);}import { Suspense, useRef } from 'react';import { extend, useFrame, useLoader } from '@react-three/fiber/webgpu';import { AnimatedSprite2D, SpriteSheetLoader, SortLayers } from 'three-flatland/react';
extend({ AnimatedSprite2D });
function Character() { // useLoader suspends while loading, presets applied automatically const spriteSheet = useLoader(SpriteSheetLoader, '/sprites/character.json'); const spriteRef = useRef<AnimatedSprite2D>(null);
useFrame((_, delta) => { spriteRef.current?.update(delta * 1000); });
return ( <animatedSprite2D ref={spriteRef} spriteSheet={spriteSheet} animationSet={{ fps: 10, 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 }, }, }} animation="idle" sortLayer={SortLayers.ENTITIES} scale={[64, 64, 1]} /> );}
// Wrap with Suspense in parentfunction App() { return ( <Suspense fallback={null}> <Character /> </Suspense> );}Animation Set Definition
Section titled “Animation Set Definition”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, }, },}Playback Control
Section titled “Playback Control”sprite.play('run') // Play animation by namesprite.pause() // Pause at current framesprite.resume() // Resume paused animationsprite.stop() // Stop and resetsprite.gotoFrame(3) // Jump to specific frame
// Check animation statesprite.isPlaying() // Is any animation playing?sprite.isPlaying('run') // Is 'run' animation playing?sprite.currentAnimation // Get current animation namePlayback Speed
Section titled “Playback Speed”sprite.speed = 1.5 // 1.5x speedsprite.speed = 0.5 // Half speedsprite.speed = 2 // Double speedAnimation Callbacks
Section titled “Animation Callbacks”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') },})Loading Spritesheets
Section titled “Loading Spritesheets”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 dataconst frame = spriteSheet.getFrame('idle_0')console.log(frame) // { x, y, width, height, sourceWidth, sourceHeight, ... }Update Loop
Section titled “Update Loop”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)}Advance a SpriteGroup once
Section titled “Advance a SpriteGroup once”When many animated sprites already belong to one SpriteGroup, advance the group instead of keeping
an application-side loop over every sprite. Time remains explicit and caller-owned; rendering never
advances animation implicitly. Sprites that share the same definition, phase, speed, and loop behavior
can share timeline work. Callbacks, events, divergent playback, custom subclasses, and large catch-up
steps automatically keep the exact per-controller path.
import { SpriteGroup } from 'three-flatland'
const spriteGroup = new SpriteGroup({ expectedSprites: 16_384 })scene.add(spriteGroup)spriteGroup.addSprites(...animatedSprites)
function animate() { const deltaMs = clock.getDelta() * 1000 spriteGroup.advanceAnimations(deltaMs) renderer.render(scene, camera)}import { useRef } from 'react'import { extend, useFrame } from '@react-three/fiber/webgpu'import { AnimatedSprite2D, SpriteGroup } from 'three-flatland/react'
extend({ SpriteGroup, AnimatedSprite2D })
function AnimatedCrowd() { const groupRef = useRef<SpriteGroup>(null)
useFrame((_, delta) => { groupRef.current?.advanceAnimations(delta * 1000) })
return ( <spriteGroup ref={groupRef}> {/* AnimatedSprite2D children */} </spriteGroup> )}Next steps
Section titled “Next steps”- Animation example — a spritesheet-driven
AnimatedSprite2Din a runnable scene. AnimatedSprite2DAPI reference — the full animation-set and playback surface.- Sprites for the underlying
Sprite2D, and Loaders for loading spritesheets.