Skip to main content
Animation brings your pixel art to life by displaying a sequence of frames over time. Aseprite provides powerful tools for creating frame-by-frame animations with precise timing control.

Animation Fundamentals

Animations in Aseprite are created by:
  1. Frames - Individual images in the sequence
  2. Cels - The actual image content on each layer at each frame
  3. Duration - How long each frame displays
  4. Tags - Named animation sequences
  5. Animation Direction - Forward, reverse, or ping-pong playback
Think of frames as the timeline, cels as the drawings, and tags as named clips within that timeline.

Creating Simple Animations

Basic Frame Animation

-- Create a sprite
local sprite = Sprite(32, 32)
local layer = sprite.layers[1]

-- Add more frames
for i = 1, 7 do
  sprite:newFrame()
end

print(#sprite.frames)  -- 8 frames

-- Set timing (10 FPS = 0.1 seconds per frame)
sprite:setDurationForAllFrames(0.1)

-- Now draw different content in each frame
for i, frame in ipairs(sprite.frames) do
  local cel = layer:cel(frame.frameNumber)
  if cel then
    -- Draw on cel.image for each frame
  end
end

Frame Timing

Control the playback speed by adjusting frame durations:
local sprite = Sprite(32, 32)

-- Create 8-frame animation
for i = 1, 7 do
  sprite:newFrame()
end

-- Fast animation (20 FPS)
sprite:setDurationForAllFrames(0.05)  -- 50ms per frame

-- Slow animation (5 FPS)  
sprite:setDurationForAllFrames(0.2)   -- 200ms per frame

-- Custom timing per frame
sprite.frames[1].duration = 0.5   -- Hold first frame longer
sprite.frames[5].duration = 0.15  -- Slow down middle frame

Animation Tags

Tags organize frames into named animation sequences. A sprite can have multiple tags for different animations.

Creating Tags

local sprite = Sprite(32, 32)

-- Create enough frames for multiple animations
for i = 1, 16 do
  sprite:newFrame()
end

-- Create walk animation (frames 1-8)
local walkTag = sprite:newTag(1, 8)
walkTag.name = "walk"
walkTag.aniDir = AniDir.FORWARD

-- Create jump animation (frames 9-14)  
local jumpTag = sprite:newTag(9, 14)
jumpTag.name = "jump"
jumpTag.aniDir = AniDir.FORWARD

-- Create idle animation (frames 15-17)
local idleTag = sprite:newTag(15, 17)
idleTag.name = "idle"
idleTag.aniDir = AniDir.PING_PONG

Tag Properties

local sprite = Sprite(32, 32)
for i = 1, 8 do
  sprite:newFrame()
end

local tag = sprite:newTag(1, 8)

-- Name
print(tag.name)  -- "Tag" (default)
tag.name = "walk"

-- Frame range
print(tag.fromFrame.frameNumber)  -- 1
print(tag.toFrame.frameNumber)    -- 8
print(tag.frames)                 -- 8

-- Change frame range
tag.fromFrame = 2
tag.toFrame = 5
print(tag.frames)  -- 4

-- Color (for visual organization)
tag.color = Color(255, 0, 0)  -- Red tag
print(tag.color)

-- Repeat count (0 = infinite)
print(tag.repeats)  -- 0
tag.repeats = 3     -- Loop 3 times

-- Custom data
tag.data = "Animation metadata"

Animation Direction

Control how animations play using AniDir (animation direction):
local tag = sprite:newTag(1, 8)

-- Forward: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 (repeat)
tag.aniDir = AniDir.FORWARD

-- Reverse: 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1 (repeat)
tag.aniDir = AniDir.REVERSE

-- Ping-Pong: 1 → 2 → ... → 8 → 7 → ... → 1 (repeat)
tag.aniDir = AniDir.PING_PONG

-- Ping-Pong Reverse: 8 → 7 → ... → 1 → 2 → ... → 8 (repeat)
tag.aniDir = AniDir.PING_PONG_REVERSE
Best for animations that loop naturally, like:
  • Walking cycles
  • Idle animations
  • Rotating objects
Perfect for animations that need to reverse smoothly:
  • Breathing effects
  • Bobbing objects
  • Blinking
  • Anything that needs to return to start state
Useful for:
  • Rewinding effects
  • Backward walking/movement
  • Countdown animations

Working with Tags

Accessing Tags

local sprite = Sprite(32, 32)

-- Get all tags
local tags = sprite.tags
print(#tags)  -- Number of tags

-- Iterate tags
for i, tag in ipairs(sprite.tags) do
  print(tag.name, tag.fromFrame, tag.toFrame)
end

-- Find tag by name
function findTag(sprite, name)
  for i, tag in ipairs(sprite.tags) do
    if tag.name == name then
      return tag
    end
  end
  return nil
end

local walkTag = findTag(sprite, "walk")

Deleting Tags

local sprite = Sprite(32, 32)
for i = 1, 8 do
  sprite:newFrame()
end

local tag = sprite:newTag(1, 4)
print(#sprite.tags)  -- 1

sprite:deleteTag(tag)
print(#sprite.tags)  -- 0

Animation Techniques

Frame-by-Frame Animation

Draw each frame individually for maximum control:
local sprite = Sprite(32, 32)
local layer = sprite.layers[1]

-- Create 8 frames
for i = 1, 7 do
  sprite:newFrame()
end

-- Draw on each frame
for frameNum = 1, 8 do
  app.activeFrame = sprite.frames[frameNum]
  local cel = layer:cel(frameNum)
  if cel then
    local img = cel.image
    -- Draw different content on each frame
    -- img:drawPixel(x, y, color)
  end
end

Onion Skinning

While Aseprite provides onion skinning in the UI, you can simulate it programmatically:
-- Access previous and next frames while drawing
local currentFrame = app.activeFrame
if currentFrame.previous then
  local prevCel = layer:cel(currentFrame.previous.frameNumber)
  -- Use prevCel as reference
end

Cel Reuse and Linking

Save time by reusing or linking cels:
local sprite = Sprite(32, 32)
local layer = sprite.layers[1]

-- Create frames
for i = 1, 8 do
  sprite:newFrame()
end

-- Link cels for static parts
local baseCel = layer:cel(1)
for i = 2, 8 do
  -- Create linked cel (shares same image)
  sprite:newCel(layer, i, baseCel.image)
end

-- Now frame 1-8 all show the same image
-- Editing one affects all

Layer-Based Animation

Animate different parts independently using layers:
local sprite = Sprite(32, 32)

-- Create layers for different parts
local bodyLayer = sprite.layers[1]
bodyLayer.name = "Body"
local eyesLayer = sprite:newLayer()
eyesLayer.name = "Eyes"

-- Create frames
for i = 1, 8 do
  sprite:newFrame()
end

-- Body can have a 4-frame cycle
-- Eyes can have a 2-frame blink
-- They animate independently

Animation Playback

Duration Calculations

local sprite = Sprite(32, 32)

-- Create animation
for i = 1, 7 do
  sprite:newFrame()
end
sprite:setDurationForAllFrames(0.1)

-- Calculate total duration
local totalMs = sprite:totalAnimationDuration()
print("Animation duration:", totalMs, "seconds")

-- Calculate FPS
local avgDuration = totalMs / #sprite.frames
local fps = 1.0 / avgDuration
print("Average FPS:", fps)

Tag-Specific Duration

function getTagDuration(tag)
  local sprite = tag.sprite
  local duration = 0
  
  for i = tag.fromFrame.frameNumber, tag.toFrame.frameNumber do
    duration = duration + sprite.frames[i].duration
  end
  
  return duration
end

local walkTag = sprite:newTag(1, 8)
sprite:setFrameRangeDuration(1, 8, 0.1)
print("Walk duration:", getTagDuration(walkTag), "seconds")  -- 0.8s

Best Practices

Before starting:
  1. Sketch key poses on paper
  2. Decide frame count (more = smoother, but more work)
  3. Determine timing (fast action vs slow idle)
  4. Plan which parts animate vs stay static
Always use tags to organize animations:
  • Easier to find and edit specific animations
  • Better for exporting (can export tags separately)
  • Clearer for team collaboration
  • Essential for game engines that expect named animations
For parts that don’t change:
  • Background elements
  • Static body parts
  • Repeated frames
Use linked cels to save memory and ensure consistency.
Common frame rates:
  • 24 FPS (0.042s): Film standard, cinematic
  • 12 FPS (0.083s): Common for pixel art, “on twos”
  • 10 FPS (0.1s): Retro games, simple animations
  • 8 FPS (0.125s): Very stylized, low frame count
  • 6 FPS (0.167s): Minimal animation

Common Animation Types

Walk Cycle

-- Typical walk cycle: 6-8 frames
local sprite = Sprite(32, 32)

for i = 1, 7 do
  sprite:newFrame()
end

local walk = sprite:newTag(1, 8)
walk.name = "walk"
walk.aniDir = AniDir.FORWARD
sprite:setDurationForAllFrames(0.1)  -- 10 FPS

-- Key poses:
-- Frame 1: Contact (foot down)
-- Frame 2-3: Passing (weight shifts)
-- Frame 4: High point (opposite foot up)
-- Frame 5: Contact (opposite foot down)
-- Frame 6-7: Passing (weight shifts)
-- Frame 8: High point (first foot up)

Idle Animation

-- Subtle breathing/bobbing: 4-6 frames
local sprite = Sprite(32, 32)

for i = 1, 3 do
  sprite:newFrame()
end

local idle = sprite:newTag(1, 4)
idle.name = "idle"
idle.aniDir = AniDir.PING_PONG  -- Smooth loop
sprite:setDurationForAllFrames(0.2)  -- Slow, subtle

Attack Animation

-- Attack with anticipation and recovery
local sprite = Sprite(32, 32)

for i = 1, 9 do
  sprite:newFrame()
end

local attack = sprite:newTag(1, 10)
attack.name = "attack"
attack.aniDir = AniDir.FORWARD

-- Variable timing for impact
sprite.frames[1].duration = 0.15  -- Anticipation (slow)
sprite.frames[2].duration = 0.1
sprite.frames[3].duration = 0.05  -- Wind up (fast)
sprite.frames[4].duration = 0.03  -- Attack! (very fast)
sprite.frames[5].duration = 0.2   -- Impact hold
sprite.frames[6].duration = 0.1   -- Recovery
sprite.frames[7].duration = 0.1
sprite.frames[8].duration = 0.12
sprite.frames[9].duration = 0.12
sprite.frames[10].duration = 0.15

Exporting Animations

-- Save as animated GIF
sprite:saveAs("animation.gif")

-- Save as sprite sheet
app.command.ExportSpriteSheet{
  ui = false,
  type = SpriteSheetType.HORIZONTAL,
  textureFilename = "spritesheet.png",
  dataFilename = "spritesheet.json"
}

Frames

Deep dive into frame management

Layers

Learn about layer organization

Sprites

Understand the sprite container

API Reference

For complete API documentation, see: