BitmapListAnimationLib — Sprite Sheet Animation

Animates through a sequence of images in a sprite sheet — frame-by-frame animation for sprites, loading spinners, character walk cycles, explosions, and any visual effect built from sequential frames. The animation cycles through equally-sized cells in a grid-based sprite sheet image, attached to an Image control. 47 functions.

CategoryCountDescription
Error Handling4bmplistani_error, errormsg$, strerror$, clearerror
Creation & Destruction3bmplistani# (2 overloads), bmplistani_free
Playback Control3start, stop, stopatcurrent
Sprite Sheet Config7animationbitmap (set/get), loadspritesheet#, animationcount, animationrowcount (get/set)
Timing4duration, delay (get/set)
Behavior — Easing4animationtype, interpolation (get/set)
Behavior — Flags8autoreverse, inverse, loop, enabled (get/set)
State Queries3running, normalizedtime, name$
Property Name2propertyname (get/set)
Triggers4trigger, triggerinverse (get/set)
Events5onfinish, onprocess (get/set), clearcallbacks#

Cross-Platform Support

PlatformStatusNotes
Windows✅ Full SupportWin32/Win64
Linux✅ Full SupportGTK-based
Android✅ Full SupportUse local file paths for assets

Error Handling

FunctionSignatureDescription
bmplistani_error()bmplistani_error@Last error code (0 = no error)
bmplistani_errormsg$()bmplistani_errormsg$@Last error message as string
bmplistani_strerror$(code)bmplistani_strerror$@nDescription for a given error code
bmplistani_clearerror()bmplistani_clearerror@Clear the error state

Error Codes

CodeConstantDescription
0ERR_NONENo error
1ERR_NIL_ANIMATIONAnimation pointer is nil
2ERR_INVALID_BITMAPInvalid bitmap or source image
3ERR_INVALID_VALUEInvalid parameter value
4ERR_ANIMATION_RUNNINGCannot modify while animation is running
5ERR_FILE_NOT_FOUNDSprite sheet file not found or load error

How It Works

A bitmap list animation cycles through frames in a sprite sheet — a single image file containing a grid of equally-sized frames. The animation is attached to an Image control and swaps the displayed frame at the rate determined by the duration and frame count:

╯ basic pattern
' 1. Create an Image control to display the sprite
let sprite# = image#(frm#, 100, 100, 64, 64)

' 2. Create the animation on the Image
let ani# = bmplistani#(sprite#)

' 3. Load sprite sheet from file
bmplistani_loadspritesheet#(ani#, "https://plan9basic.com/assets/img_examples/spritesheet.png")

' 4. Configure frame grid
bmplistani_animationcount#(ani#, 8)      ' 8 total frames
bmplistani_animationrowcount#(ani#, 2)   ' arranged in 2 rows

' 5. Set timing (all 8 frames in 1 second = 8 FPS)
bmplistani_duration#(ani#, 1.0)
bmplistani_interpolation#(ani#, "Linear")

' 6. Loop and start
bmplistani_loop#(ani#, 1)
bmplistani_start(ani#)
⚠ Important: Always check bmplistani_error() after calling loadspritesheet#. If the file is not found or cannot be loaded, error code 5 is set and the animation will not display anything.

Sprite Sheet Layout

Sprite sheets are organized as grids of equally-sized frames. Frames are numbered left-to-right, top-to-bottom, starting from 0:

Frame 0
Frame 1
Frame 2
Frame 3
Frame 4
Frame 5
Frame 6
Frame 7

animationcount = 8, animationrowcount = 2 → 4 columns × 2 rows

The engine automatically calculates frame size: frame width = sheet width ÷ columns, frame height = sheet height ÷ rows, where columns = animationcount ÷ animationrowcount.

ⓘ Frame rate formula: FPS = animationcount ÷ duration. For example, 8 frames with 1.0s duration = 8 FPS. 8 frames with 0.5s duration = 16 FPS.

Creating Sprite Sheets

Free Resources

  • OpenGameArt.org — CC0 licensed game assets (coins, explosions, characters)
  • Kenney.nl — Free CC0 game assets with characters, effects, and UI
  • itch.io — Many free sprite packs (search “free sprite sheet”)

Tools for Creating Your Own

  • Piskel (piskelapp.com) — Free online pixel art editor with sprite sheet export
  • TexturePacker — Combine individual frames into optimized sprite sheets
  • Any image editor — Create a grid manually with uniform frame sizes

Requirements

  • All frames must have identical dimensions
  • Frames arranged in a grid: left-to-right, top-to-bottom
  • No gaps or spacing between frames
  • PNG format recommended (supports transparency)
  • Power-of-2 dimensions recommended for performance (64, 128, 256)

Interpolation & Easing

For sprite animations, interpolation controls how frame timing is distributed across the duration. "Linear" is almost always the right choice — it gives each frame equal screen time:

InterpolationEffect on Frame TimingBest For
"Linear"Each frame shown for equal timeAlmost all sprite animations (recommended)
"Quadratic"Slow start, fast endAcceleration effects (wheels spinning up)
"Cubic"More pronounced accelerationDramatic speed-up effects

Easing Types (AnimationType)

TypeEffectDescription
"In"Slow start → fast endFrame rate accelerates
"Out"Fast start → slow endFrame rate decelerates
"InOut"Slow → fast → slowSpeed up then slow down

Creation & Destruction

FunctionSignatureDescription
bmplistani#(parent#)bmplistani#@#Create animation attached to parent Image control
bmplistani#(parent#, name$)bmplistani#@#$Create named animation attached to parent Image
bmplistani_free(ani#)bmplistani_free@#Destroy animation object
ⓘ Parent must be an Image control. Unlike other animation libraries that work on any visual control, BitmapListAnimation requires an image# control as its parent — it swaps the displayed bitmap frame on that Image.

Playback Control

FunctionSignatureDescription
bmplistani_start(ani#)bmplistani_start@#Start the animation
bmplistani_stop(ani#)bmplistani_stop@#Stop and reset to first frame
bmplistani_stopatcurrent(ani#)bmplistani_stopatcurrent@#Stop at current frame

Sprite Sheet Configuration

Loading

FunctionSignatureDescription
bmplistani_loadspritesheet#(ani#, filename$)bmplistani_loadspritesheet#@#$Load sprite sheet from local file and assign bitmap
bmplistani_animationbitmap#(ani#, sourceImage#)bmplistani_animationbitmap#@##Set bitmap from another Image control’s bitmap
bmplistani_animationbitmap#(ani#)bmplistani_animationbitmap#@#Get the current sprite sheet bitmap pointer

Grid Configuration

FunctionSignatureDescription
bmplistani_animationcount#(ani#, count)bmplistani_animationcount#@#nSet total number of frames
bmplistani_animationcount(ani#)bmplistani_animationcount@#Get frame count
bmplistani_animationrowcount#(ani#, rows)bmplistani_animationrowcount#@#nSet number of rows in sprite sheet grid
bmplistani_animationrowcount(ani#)bmplistani_animationrowcount@#Get row count

Timing

FunctionSignatureDescription
bmplistani_duration#(ani#, seconds)bmplistani_duration#@#nSet total animation duration in seconds
bmplistani_duration(ani#)bmplistani_duration@#Get duration
bmplistani_delay#(ani#, seconds)bmplistani_delay#@#nSet delay before animation starts
bmplistani_delay(ani#)bmplistani_delay@#Get delay

Behavior Flags

Easing & Interpolation

FunctionSignatureDescription
bmplistani_animationtype#(ani#, type$)bmplistani_animationtype#@#$Set easing: "In", "Out", "InOut"
bmplistani_animationtype$(ani#)bmplistani_animationtype$@#Get easing type
bmplistani_interpolation#(ani#, type$)bmplistani_interpolation#@#$Set interpolation curve
bmplistani_interpolation$(ani#)bmplistani_interpolation$@#Get interpolation type

Boolean Flags

FunctionSignatureDescription
bmplistani_autoreverse#(ani#, flag)bmplistani_autoreverse#@#nIf 1, plays forward then backward (ping-pong)
bmplistani_autoreverse(ani#)bmplistani_autoreverse@#Get autoreverse flag
bmplistani_inverse#(ani#, flag)bmplistani_inverse#@#nIf 1, plays frames in reverse order
bmplistani_inverse(ani#)bmplistani_inverse@#Get inverse flag
bmplistani_loop#(ani#, flag)bmplistani_loop#@#nIf 1, loops indefinitely
bmplistani_loop(ani#)bmplistani_loop@#Get loop flag
bmplistani_enabled#(ani#, flag)bmplistani_enabled#@#nEnable or disable the animation
bmplistani_enabled(ani#)bmplistani_enabled@#Get enabled state

State Queries

FunctionSignatureDescription
bmplistani_running(ani#)bmplistani_running@#Returns 1 if currently animating
bmplistani_normalizedtime(ani#)bmplistani_normalizedtime@#Returns progress from 0.0 to 1.0
bmplistani_name$(ani#)bmplistani_name$@#Get the animation’s name
ⓘ Calculating current frame: Use normalizedtime to determine which frame is showing: currentFrame = int(normalizedtime * animationcount). This is useful in onprocess callbacks for triggering events at specific frames.

Property Name

FunctionSignatureDescription
bmplistani_propertyname#(ani#, name$)bmplistani_propertyname#@#$Set property name for the animation
bmplistani_propertyname$(ani#)bmplistani_propertyname$@#Get property name

Triggers

FunctionSignatureDescription
bmplistani_trigger#(ani#, expr$)bmplistani_trigger#@#$Set trigger expression (starts animation)
bmplistani_trigger$(ani#)bmplistani_trigger$@#Get trigger expression
bmplistani_triggerinverse#(ani#, expr$)bmplistani_triggerinverse#@#$Set inverse trigger (reverses animation)
bmplistani_triggerinverse$(ani#)bmplistani_triggerinverse$@#Get inverse trigger expression

Events

Event SetterGetterCallback SignatureDescription
bmplistani_onfinish#(ani#, func$)bmplistani_onfinish$(ani#)function name(sender#)Fired when animation completes (not in loop mode)
bmplistani_onprocess#(ani#, func$)bmplistani_onprocess$(ani#)function name(sender#)Fired on every animation frame
bmplistani_clearcallbacks#(ani#)Disconnect all callbacks

Complete Examples

Simple Sprite Animation

╯ sprite.bas
' Basic sprite animation from a local file
let frm# = form#("Sprite Demo", 400, 300)
form_position#(frm#, 4)

let sprite# = image#(frm#, 150, 100, 100, 100)

let spriteAni# = bmplistani#(sprite#)
bmplistani_loadspritesheet#(spriteAni#, "https://plan9basic.com/assets/img_examples/spritesheet.png")
bmplistani_animationcount#(spriteAni#, 4)
bmplistani_animationrowcount#(spriteAni#, 1)
bmplistani_duration#(spriteAni#, 0.5)
bmplistani_interpolation#(spriteAni#, "Linear")
bmplistani_loop#(spriteAni#, 1)

' Always check for load errors
if bmplistani_error() <> 0 then
    print "Error: " + bmplistani_errormsg$()
else
    bmplistani_start(spriteAni#)
end if

form_show(frm#)

while form_visible(frm#) = 1
    processmessages()
end while

Character Walk Cycle (2-Row Grid)

╯ walkcycle.bas
let frm# = form#("Walk Cycle Demo", 400, 300)
form_position#(frm#, 4)

let bg# = rectangle#(frm#, 0, 0, 400, 300)
rectangle_fill#(bg#, "#87CEEB")

let character# = image#(frm#, 160, 150, 80, 100)

' 8 frames, 2 rows × 4 columns
let walkAni# = bmplistani#(character#)
bmplistani_loadspritesheet#(walkAni#, "https://plan9basic.com/assets/img_examples/character_walk.png")
bmplistani_animationcount#(walkAni#, 8)
bmplistani_animationrowcount#(walkAni#, 2)
bmplistani_duration#(walkAni#, 0.8)
bmplistani_interpolation#(walkAni#, "Linear")
bmplistani_loop#(walkAni#, 1)
bmplistani_start(walkAni#)

form_show(frm#)

while form_visible(frm#) = 1
    processmessages()
end while

Explosion (Play Once with Callback)

╯ explosion.bas
let frm# = form#("Explosion Demo", 400, 300)
form_position#(frm#, 4)

let bg# = rectangle#(frm#, 0, 0, 400, 300)
rectangle_fill#(bg#, "#1a1a2e")

let explosion# = image#(frm#, 100, 30, 200, 200)

let explodeAni# = bmplistani#(explosion#)
bmplistani_loadspritesheet#(explodeAni#, "https://plan9basic.com/assets/img_examples/explosion.png")
bmplistani_animationcount#(explodeAni#, 12)
bmplistani_animationrowcount#(explodeAni#, 3)
bmplistani_duration#(explodeAni#, 0.8)
bmplistani_interpolation#(explodeAni#, "Linear")

' Play once, then hide when done
bmplistani_loop#(explodeAni#, 0)
bmplistani_onfinish#(explodeAni#, "onexplosiondone")

let btn# = button#(frm#, "Explode!", 150, 250, 100, 40)
button_onclick#(btn#, "doexplode")

form_show(frm#)

function doexplode(sender#)
    image_visible#(explosion#, 1)
    bmplistani_start(explodeAni#)
endfunction

function onexplosiondone(sender#)
    image_visible#(explosion#, 0)
endfunction

while form_visible(frm#) = 1
    processmessages()
end while

Speed Control

╯ speed-control.bas
let frm# = form#("Speed Control", 400, 380)
form_position#(frm#, 4)

let sprite# = image#(frm#, 150, 40, 100, 100)

let spriteAni# = bmplistani#(sprite#)
bmplistani_loadspritesheet#(spriteAni#, "https://plan9basic.com/assets/img_examples/spritesheet.png")
bmplistani_animationcount#(spriteAni#, 8)
bmplistani_animationrowcount#(spriteAni#, 1)
bmplistani_duration#(spriteAni#, 1.0)
bmplistani_interpolation#(spriteAni#, "Linear")
bmplistani_loop#(spriteAni#, 1)
bmplistani_start(spriteAni#)

let speedLbl# = label#(frm#, "Duration: 1.00s")
label_move#(speedLbl#, 130, 195)
let fpsLbl# = label#(frm#, "(8.0 FPS)")
label_move#(fpsLbl#, 165, 225)

let btnSlower# = button#(frm#, "Slower", 80, 270, 100, 40)
button_onclick#(btnSlower#, "slower")
let btnFaster# = button#(frm#, "Faster", 220, 270, 100, 40)
button_onclick#(btnFaster#, "faster")

let currentSpeed = 1.0

form_show(frm#)

function updatespeedlabels() local fps
    fps = 8 / currentSpeed
    label_text#(speedLbl#, "Duration: " + stri$(currentSpeed, 2) + "s")
    label_text#(fpsLbl#, "(" + stri$(fps, 1) + " FPS)")
endfunction

function slower(sender#)
    currentSpeed = currentSpeed + 0.25
    if currentSpeed > 4.0 then currentSpeed = 4.0
    bmplistani_stop(spriteAni#)
    bmplistani_duration#(spriteAni#, currentSpeed)
    bmplistani_start(spriteAni#)
    updatespeedlabels()
endfunction

function faster(sender#)
    currentSpeed = currentSpeed - 0.25
    if currentSpeed < 0.25 then currentSpeed = 0.25
    bmplistani_stop(spriteAni#)
    bmplistani_duration#(spriteAni#, currentSpeed)
    bmplistani_start(spriteAni#)
    updatespeedlabels()
endfunction

while form_visible(frm#) = 1
    processmessages()
end while

Ping-Pong (Auto-Reverse)

╯ pingpong.bas
let frm# = form#("Ping-Pong Demo", 350, 300)
form_position#(frm#, 4)

let sprite# = image#(frm#, 125, 50, 100, 100)

let spriteAni# = bmplistani#(sprite#)
bmplistani_loadspritesheet#(spriteAni#, "https://plan9basic.com/assets/img_examples/character.png")
bmplistani_animationcount#(spriteAni#, 6)
bmplistani_animationrowcount#(spriteAni#, 1)
bmplistani_duration#(spriteAni#, 1.5)
bmplistani_interpolation#(spriteAni#, "Linear")
bmplistani_autoreverse#(spriteAni#, 1)
bmplistani_loop#(spriteAni#, 1)
bmplistani_start(spriteAni#)

let lbl# = label#(frm#, "Frames play forward then backward")
label_move#(lbl#, 40, 180)

let btnToggle# = button#(frm#, "Toggle Auto-Reverse", 95, 240, 160, 35)
button_onclick#(btnToggle#, "toggle")

form_show(frm#)

function toggle(sender#) local current
    bmplistani_stop(spriteAni#)
    current = bmplistani_autoreverse(spriteAni#)
    if current = 1 then
        bmplistani_autoreverse#(spriteAni#, 0)
        label_text#(lbl#, "Forward only, then restarts")
    else
        bmplistani_autoreverse#(spriteAni#, 1)
        label_text#(lbl#, "Frames play forward then backward")
    end if
    bmplistani_start(spriteAni#)
endfunction

while form_visible(frm#) = 1
    processmessages()
end while

Troubleshooting

ProblemCauseSolution
“File not found” error (code 5)Wrong file pathUse absolute path or ensure file is in working directory. Check spelling and extension.
Animation not visibleImage control hidden or zero-sizedVerify Image has proper dimensions and is visible. Check that sprite sheet loaded successfully.
Frames appear scrambledWrong count/row valuesEnsure animationcount matches actual frame count. Ensure animationrowcount matches grid rows.
Animation too fast/slowWrong durationFPS = frames ÷ duration. Adjust duration for desired speed.
Frames show partial imagesNon-uniform frame sizesAll frames must be identical size. No gaps between frames in the sprite sheet.
Nothing happens after startMissing bmplistani_startLoading the sprite sheet does not auto-start. Call start explicitly.

Best Practices

PracticeWhy
Always use "Linear" interpolationGives each frame equal display time — correct for almost all sprite animations
Check bmplistani_error() after loadingCatch file-not-found errors before attempting to start
Use PNG format with transparencyAlpha channel lets sprites display over any background
Use power-of-2 frame dimensions64×64, 128×128, 256×256 give best GPU performance
Calculate FPS: frames ÷ duration8 frames at 0.5s = 16 FPS; 8 frames at 1.0s = 8 FPS
Use onfinish for one-shot effectsExplosions, damage flashes, and pick-ups should play once then hide
Use autoreverse + loop for ping-pongBreathing, idle bobbing, or pendulum-style animations
Stop before changing durationCall stop, change duration, then start again
Parent must be an image# controlBitmapListAnimation swaps frames on the Image bitmap
No gaps between frames in sheetEngine divides sheet evenly — gaps cause misaligned frames

Quick Reference

FunctionSignatureDescription
bmplistani_error / errormsg$ / strerror$ / clearerrorvariousError handling (4)
bmplistani#(parent#[, name$])bmplistani#@# / bmplistani#@#$Create (2 overloads)
bmplistani_free(ani#)bmplistani_free@#Destroy
bmplistani_start / stop / stopatcurrentvariousPlayback control (3)
bmplistani_loadspritesheet# / animationbitmap#variousSprite loading (3)
bmplistani_animationcount / animationrowcountvariousGrid config (4)
bmplistani_duration / delayvariousTiming (4)
bmplistani_animationtype / interpolationvariousEasing & curves (4)
bmplistani_autoreverse / inverse / loop / enabledvariousBehavior flags (8)
bmplistani_running / normalizedtime / name$variousState queries (3)
bmplistani_propertyname# / propertyname$variousProperty name (2)
bmplistani_trigger / triggerinversevariousAutomatic triggers (4)
bmplistani_onfinish / onprocess / clearcallbacks#variousEvents (5)

47 functions. Part of the Plan9Basic Animation library system.