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

An animation sequence with frame indices and timing.

pub type Animation {
  Animation(
    name: String,
    frames: iv.Array(Int),
    frame_duration: Float,
    loop: LoopMode,
  )
}

Constructors

  • Animation(
      name: String,
      frames: iv.Array(Int),
      frame_duration: Float,
      loop: LoopMode,
    )

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

  • Once

    Play once and stop on the last frame

  • Repeat

    Loop continuously from start to finish

  • PingPong

    Play 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

  • InvalidColumns

    Columns must be at least 1

  • InvalidRows

    Rows must be at least 1

  • InvalidFrameCount

    Frame count must be at least 1

  • FrameCountExceedsGrid

    Frame 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 seconds
  • loop: 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 columns(sheet: Spritesheet) -> Int

Get the number of columns in the spritesheet.

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 frames
  • columns: Number of frames horizontally
  • rows: 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 pause(state: AnimationState) -> AnimationState

Pause the animation.

pub fn play(state: AnimationState) -> AnimationState

Play/resume the animation.

pub fn rows(sheet: Spritesheet) -> Int

Get the number of rows in the spritesheet.

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)
    }
  }
}
Search Document