tiramisu/spritesheet
Spritesheet animation system for texture-based sprites.
This module provides a functional approach to spritesheet animation, allowing you to define sprite atlases and animate through frames over time.
Quick Start
import tiramisu/spritesheet
import tiramisu/asset
// 1. Define your spritesheet (8 frames in a horizontal strip)
let assert Ok(sheet) = spritesheet.from_grid(
texture: my_texture,
columns: 8,
rows: 1,
)
// 2. Define an animation sequence
let walk_animation = spritesheet.animation(
name: "walk",
frames: [0, 1, 2, 3, 4, 5, 6, 7],
frame_duration: 0.1, // 10 FPS
loop: spritesheet.Repeat,
)
// 3. Create initial animation state
let state = spritesheet.initial_state("walk")
// 4. Update in your game loop
fn update(model, msg, ctx) {
let new_state = spritesheet.update(
state: model.anim_state,
animation: walk_animation,
delta_time: ctx.delta_time,
)
Model(..model, anim_state: new_state)
}
Types
Runtime animation state tracked in your model.
This contains the current playback state that you update each frame.
pub type AnimationState {
AnimationState(
current_animation: String,
current_frame_index: Int,
elapsed_time: Float,
is_playing: Bool,
ping_pong_forward: Bool,
)
}
Constructors
-
AnimationState( current_animation: String, current_frame_index: Int, elapsed_time: Float, is_playing: Bool, ping_pong_forward: Bool, )
How the animation should loop.
pub type LoopMode {
Once
Repeat
PingPong
}
Constructors
-
OncePlay once and stop on the last frame
-
RepeatLoop continuously from start to finish
-
PingPongPlay forward then backward (ping-pong)
A spritesheet configuration defining a grid-based sprite atlas.
Spritesheets are textures containing multiple frames arranged in a grid. Each frame has equal dimensions determined by dividing the texture by the number of columns and rows.
pub opaque type Spritesheet
Errors that can occur when creating a spritesheet.
pub type SpritesheetError {
InvalidColumns
InvalidRows
InvalidFrameCount
FrameCountExceedsGrid
}
Constructors
-
InvalidColumnsColumns must be at least 1
-
InvalidRowsRows must be at least 1
-
InvalidFrameCountFrame count must be at least 1
-
FrameCountExceedsGridFrame count exceeds grid capacity (columns * rows)
Values
pub fn animation(
name name: String,
frames frames: List(Int),
frame_duration frame_duration: Float,
loop loop: LoopMode,
) -> Animation
Create an animation sequence.
Parameters
name: Identifier for this animation (e.g., “walk”, “jump”, “idle”)frames: Array of frame indices to play (0-indexed)frame_duration: How long to show each frame in secondsloop: How the animation should loop
Example
let walk = spritesheet.animation(
name: "walk",
frames: iv.from_list([0, 1, 2, 3]),
frame_duration: 0.1, // 10 FPS
loop: spritesheet.Repeat,
)
let jump = spritesheet.animation(
name: "jump",
frames: iv.from_list([8, 9, 10]),
frame_duration: 0.15,
loop: spritesheet.Once,
)
pub fn apply_frame(
sheet: Spritesheet,
tex: asset.Texture,
frame_index: Int,
) -> asset.Texture
Apply spritesheet frame to a texture.
This is a convenience function that sets up the texture with the correct UV offset, repeat, and wrap mode for the current frame.
Example
let assert Ok(frame) = spritesheet.current_frame(state, animation)
let animated_texture = spritesheet.apply_frame(sheet, my_texture, frame)
pub fn change_animation(
state: AnimationState,
animation_name: String,
) -> AnimationState
Change to a different animation.
This resets the state to the first frame of the new animation.
Example
PlayerJumped -> {
let new_state = spritesheet.change_animation(
model.player_anim_state,
"jump",
)
Model(..model, player_anim_state: new_state)
}
pub fn current_animation(state: AnimationState) -> String
Get the current animation name.
pub fn current_frame(
state: AnimationState,
animation: Animation,
) -> Result(Int, Nil)
Get the actual sprite frame number to display.
This looks up the frame index in the animation’s frame array to get the actual sprite frame number.
Example
let animation = spritesheet.animation(
name: "walk",
frames: iv.from_list([0, 1, 2, 3]), // Frames to play
frame_duration: 0.1,
loop: spritesheet.Repeat,
)
// If current_frame_index is 2, this returns 2 (the 3rd frame in the array)
let frame = spritesheet.current_frame(state, animation)
pub fn current_frame_index(state: AnimationState) -> Int
Get the current frame index from the animation state.
This returns the index into the animation’s frame list, not the actual sprite frame number.
pub fn frame_count(sheet: Spritesheet) -> Int
Get the total number of frames in the spritesheet.
pub fn frame_offset(
sheet: Spritesheet,
frame_index: Int,
) -> #(Float, Float)
Calculate UV offset for a specific frame.
Returns (offset_x, offset_y) for use with texture.set_offset().
Example
let assert Ok(frame) = spritesheet.current_frame(state, animation)
let #(offset_x, offset_y) = spritesheet.frame_offset(sheet, frame)
my_texture
|> texture.set_offset(offset_x, offset_y)
pub fn frame_repeat(sheet: Spritesheet) -> #(Float, Float)
Calculate UV repeat values for the spritesheet.
Returns (repeat_x, repeat_y) for use with texture.set_repeat().
Example
let #(repeat_x, repeat_y) = spritesheet.frame_repeat(sheet)
my_texture
|> texture.set_repeat(repeat_x, repeat_y)
pub fn from_grid(
texture texture: asset.Texture,
columns columns: Int,
rows rows: Int,
) -> Result(Spritesheet, SpritesheetError)
Create a spritesheet from a grid-based texture atlas.
The texture is divided into equal-sized frames based on columns and rows.
Parameters
texture: The loaded texture containing the sprite framescolumns: Number of frames horizontallyrows: Number of frames vertically
The frame_count defaults to columns * rows, but you can specify fewer
if your spritesheet has empty cells at the end.
Example
// 8 frames in a horizontal strip (8 columns, 1 row)
let assert Ok(sheet) = spritesheet.from_grid(
texture: player_texture,
columns: 8,
rows: 1,
)
pub fn from_grid_with_count(
texture texture: asset.Texture,
columns columns: Int,
rows rows: Int,
frame_count frame_count: Int,
) -> Result(Spritesheet, SpritesheetError)
Create a spritesheet with a specific frame count.
Use this when your sprite atlas has empty cells at the end.
Example
// 4x4 grid but only 12 frames used
let assert Ok(sheet) = spritesheet.from_grid_with_count(
texture: items_texture,
columns: 4,
rows: 4,
frame_count: 12,
)
pub fn initial_state(animation_name: String) -> AnimationState
Create initial animation state.
Use this in your init() function to set up the starting state.
Example
fn init(_ctx) {
let model = Model(
anim_state: spritesheet.initial_state("idle"),
// ...
)
#(model, effect.none(), option.None)
}
pub fn is_playing(state: AnimationState) -> Bool
Check if the animation is currently playing.
pub fn stop(state: AnimationState) -> AnimationState
Stop the animation and reset to first frame.
pub fn texture(sheet: Spritesheet) -> asset.Texture
Get the base texture from a spritesheet.
Note: This returns the original texture. For animated sprites, you should clone this texture so each sprite can animate independently.
pub fn update(
state state: AnimationState,
animation animation: Animation,
delta_time delta_time: Float,
) -> AnimationState
Update animation state based on delta time.
Call this in your update() function every frame to advance the animation.
Example
fn update(model: Model, msg: Msg, ctx: Context) {
case msg {
Tick -> {
let new_state = spritesheet.update(
state: model.player_anim_state,
animation: model.walk_animation,
delta_time: ctx.delta_time,
)
#(Model(..model, player_anim_state: new_state), effect.tick(Tick), option.None)
}
}
}