element.version === currentVersion)) { versionNodes.unshift({ version: currentVersion, url: "#" }); } document.querySelector("#project-version").innerHTML = versionNodes.reduce( (acc, element) => { const status = currentVersion === element.version ? "selected disabled" : ""; return ` ${acc} `; }, `
statemachine · v1.0.0

StateMachine

A generic, type-safe state machine library for Gleam with easing function support and transition weights.

Features

  • 🎯 Fully Generic: Works with any state type (enums, strings, custom types)
  • 🎨 Easing Support: Compatible with easings_gleam for smooth transitions
  • ⚖️ Weighted Transitions: Prioritize transitions when multiple conditions are true
  • 🔄 Automatic Blending: Smooth interpolation between states with configurable durations
  • ⚡ Context-Aware Conditions: Pass game/app context to transition condition functions
  • 🛡️ Type-Safe: Leverage Gleam’s type system for correctness
  • 🔧 Immutable: Pure functional updates, no hidden state
  • ⏱️ Duration-Based: Uses gleam_time’s Duration type for precise timing

Installation

gleam add statemachine

Quick Start

import statemachine
import gleam/option.{None}
import gleam/time/duration

// Define your state type
type PlayerState {
  Idle
  Walking
  Running
}

// Define your context
type GameContext {
  GameContext(velocity: Float)
}

// Create state machine
let machine =
  statemachine.new(initial_state: Idle)
  |> statemachine.with_state(state: Walking)
  |> statemachine.with_state(state: Running)
  |> statemachine.with_transition(
    from: Idle,
    to: Walking,
    condition: statemachine.Custom(fn(ctx: GameContext) { ctx.velocity >. 0.1 }),
    blend_duration: duration.milliseconds(200),
    easing: None,
    weight: 0,
  )
  |> statemachine.with_transition(
    from: Walking,
    to: Running,
    condition: statemachine.Custom(fn(ctx: GameContext) { ctx.velocity >. 5.0 }),
    blend_duration: duration.milliseconds(300),
    easing: None,
    weight: 0,
  )

// Update in game loop
fn update(model: Model, delta_time: duration.Duration) {
  let ctx = GameContext(velocity: model.velocity)
  let #(new_machine, transitioned) = statemachine.update(machine, ctx, delta_time)
  
  case statemachine.state_data(new_machine) {
    statemachine.Single(state) -> // Use single state
    statemachine.BlendingData(from, to, factor) -> // Interpolate
  }
}

Using with easings_gleam

The library works seamlessly with easings_gleam for beautiful transitions:

import statemachine
import easings
import gleam/option.{Some}
import gleam/time/duration

statemachine.with_transition(
  from: Idle,
  to: Running,
  condition: statemachine.Always,
  blend_duration: duration.milliseconds(500),
  easing: Some(easings.cubic_in_out),
  weight: 0,
)

Transition Conditions

Three types of conditions control when transitions occur:

Always

Transition immediately:

import gleam/time/duration

statemachine.with_transition(
  from: Idle,
  to: Jump,
  condition: statemachine.Always,
  blend_duration: duration.milliseconds(100),
  easing: None,
  weight: 0,
)

AfterDuration

Transition after spending time in the current state:

import gleam/time/duration
import easings
import gleam/option.{Some}

// Automatically transition after 5 seconds
statemachine.with_transition(
  from: Idle,
  to: Sleeping,
  condition: statemachine.AfterDuration(duration.seconds(5)),
  blend_duration: duration.seconds(1),
  easing: Some(easings.ease_in_out_sine),
  weight: 0,
)

Custom

Transition based on custom game/app context:

// Velocity-based
statemachine.Custom(fn(ctx) { ctx.velocity >. 5.0 })

// Input-based
statemachine.Custom(fn(ctx) { ctx.jump_pressed && ctx.is_grounded })

Transition Weights

When multiple transitions from the same state have their conditions met, the transition with the highest weight is chosen:

import statemachine
import gleam/time/duration

statemachine.new(initial_state: Idle)
|> statemachine.with_state(state: Idle)
|> statemachine.with_state(state: Walking)
|> statemachine.with_state(state: Running)
// Lower priority transition
|> statemachine.with_transition(
  from: Idle,
  to: Walking,
  condition: statemachine.Always,
  blend_duration: duration.milliseconds(200),
  easing: None,
  weight: 5,
)
// Higher priority transition - this one will be chosen!
|> statemachine.with_transition(
  from: Idle,
  to: Running,
  condition: statemachine.Always,
  blend_duration: duration.milliseconds(200),
  easing: None,
  weight: 10,
)

This is useful for creating layered logic where more specific conditions take precedence.

Manual Transitions

Force transitions programmatically (useful for events like damage, death, cutscenes):

import easings
import gleam/option.{None, Some}
import gleam/time/duration

// Force transition with custom blend and easing
let machine = statemachine.transition_to(
  machine,
  HitReaction,
  blend_duration: Some(duration.milliseconds(100)),
  easing: Some(easings.ease_out_back),
)

// Force transition with defaults
let machine = statemachine.transition_to(
  machine,
  Dead,
  blend_duration: None,
  easing: None,
)

API Overview

Building State Machines

  • new(initial_state:) - Create a new state machine
  • with_state(machine, state:) - Register a state
  • with_transition(...) - Define conditional transitions with easing and weight
  • with_default_blend(machine, duration:) - Set default blend duration
  • with_default_easing(machine, easing:) - Set default easing function

Runtime

  • update(machine, context, delta_time) - Evaluate conditions and update
  • state_data(machine) - Get current or blended state data (returns StateData)
  • transition_to(...) - Manually force a transition

Querying

  • is_blending(machine) - Check if transitioning
  • blend_progress(machine) - Get linear blend progress (0.0 to 1.0)
  • state_ids(machine) - Get Set of all state IDs
  • state_count(machine) - Count states
  • transition_count(machine) - Count transitions

Types

  • StateData(state) - Result of state_data(): Single(state) or BlendingData(from, to, factor)
  • Condition(ctx) - Transition conditions: Always, AfterDuration(Duration), or Custom(fn(ctx) -> Bool)

Philosophy

StateMachine is designed to be:

  • Generic: Works with any state type, not just enums
  • Composable: Build complex state graphs declaratively
  • Type-safe: Leverage Gleam’s type system
  • Functional: Immutable updates, pure functions
  • Flexible: Compatible with existing easing libraries
  • Precise: Uses Duration for accurate timing

Use Cases

  • Game Animations: Character controllers, NPC behaviors
  • UI Transitions: Menu states, loading screens, modal animations
  • Application States: Workflow management, process orchestration
  • Audio: Music/sound state management with crossfading

License

MIT

Contributing

Contributions welcome! This library was extracted from Tiramisu to benefit the broader Gleam ecosystem.

Search Document