State Machines
Load and run dotLottie state machines from C, forward pointer input, drive transitions with typed inputs, and poll transition events.
A state machine turns an animation into something interactive. Instead of playing start to finish, the animation sits in a named state and moves between states in response to input — a pointer entering the canvas, a click, a numeric value crossing a threshold.
The machine is authored alongside the animation and packaged in the .lottie file, so behavior lives with the artwork rather than in your code. Your job in C is to load it, forward input, and tick it.
For the concepts behind state machines, see the state machine explanation — the model is identical across every dotLottie player.
State machines require the state-machines feature, which is enabled by default.
Load and start
A state machine is a second handle with its own lifetime, borrowed from the player.
dotlottieDotLottieStateMachine *machine =
dotlottie_state_machine_load(player, "button-states");
if (!machine) {
/* no machine with that ID, or the state-machines feature is off */
}Load a definition from your own JSON instead with dotlottie_state_machine_load_data().
Starting it takes a security policy for any OpenUrl actions the machine might perform:
dotlottie_state_machine_start(machine, "https://lottiefiles.com", true);| Parameter | Meaning |
whitelist | Comma-separated list of permitted URL prefixes. NULL or "" permits none |
require_user_interaction | When true, a URL only opens as a result of genuine user input |
A .lottie file is data, and a state machine inside one can ask to open a URL. If you load files you did not author,
pass a restrictive whitelist and keep require_user_interaction set to true. Passing NULL denies all URL opening,
which is the safest default.
Starting resets playback: the machine stops the player, rewinds to the first frame, and sets speed to 1.0, forward mode, no looping, and no autoplay. Configure playback after starting, not before.
Tick the machine, not the player
Once a machine is running, drive it with dotlottie_state_machine_tick() instead of dotlottie_tick(). It advances the animation and also evaluates transitions and guards.
bool rendered = false;
dotlottie_state_machine_tick(machine, dt, &rendered);
if (rendered) {
present(buffer, width, height);
}As with dotlottie_tick(), dt is the elapsed time in seconds.
Forward input
The runtime has no window system, so it cannot observe input itself. Translate your platform's events into canvas coordinates and post them.
dotlottie_state_machine_post_pointer_move(machine, x, y);
dotlottie_state_machine_post_pointer_down(machine, x, y);
dotlottie_state_machine_post_pointer_up(machine, x, y);
dotlottie_state_machine_post_click(machine, x, y);
dotlottie_state_machine_post_pointer_enter(machine, x, y);
dotlottie_state_machine_post_pointer_exit(machine, x, y);Coordinates are in pixels relative to the render target's top-left corner. If you scaled or offset the animation with layout or a viewport, convert from window coordinates before posting, or hit tests will be measured against the wrong area.
Only post what the machine uses
dotlottie_state_machine_get_framework_setup() reports which interactions the loaded machine actually listens for, as a bitmask. Use it to skip the work of tracking input nothing responds to — pointer move especially, which fires constantly.
uint16_t interactions = 0;
dotlottie_state_machine_get_framework_setup(machine, &interactions);
bool needs_move = (interactions & (1 << 4)) != 0;| Bit | Value | Interaction |
1 << 0 | 1 | Pointer up |
1 << 1 | 2 | Pointer down |
1 << 2 | 4 | Pointer enter |
1 << 3 | 8 | Pointer exit |
1 << 4 | 16 | Pointer move |
1 << 5 | 32 | Click |
1 << 6 | 64 | On complete |
1 << 7 | 128 | On loop complete |
A value of 0 means the machine is driven entirely by inputs and events rather than pointer interaction.
The bitmask type is internal to the runtime and cbindgen does not emit it, so the values above are the reference.
Define your own named constants for readability.
Inputs
Inputs are named variables the machine's guards read. Setting one can trigger a transition immediately.
dotlottie_state_machine_set_numeric_input(machine, "progress", 0.75f);
dotlottie_state_machine_set_string_input(machine, "status", "loading");
dotlottie_state_machine_set_boolean_input(machine, "enabled", true);Read them back with the matching getters:
float progress = 0.0f;
dotlottie_state_machine_get_numeric_input(machine, "progress", &progress);
bool enabled = false;
dotlottie_state_machine_get_boolean_input(machine, "enabled", &enabled);String inputs use the two-call pattern, since they return text:
size_t size = 0;
if (dotlottie_state_machine_get_string_input(machine, "status", NULL, &size) == DOTLOTTIE_SUCCESS) {
char *status = malloc(size);
dotlottie_state_machine_get_string_input(machine, "status", status, NULL);
/* ... */
free(status);
}Custom events
Where inputs represent state, events represent a moment. Fire one by name:
dotlottie_state_machine_fire_event(machine, "submit");The machine must define a transition listening for that name; unrecognized names are ignored.
Inspect the machine
size_t size = 0;
if (dotlottie_state_machine_get_current_state(machine, NULL, &size) == DOTLOTTIE_SUCCESS) {
char *state = malloc(size);
dotlottie_state_machine_get_current_state(machine, state, NULL);
printf("current state: %s\n", state);
free(state);
}dotlottie_state_machine_get_status() reports the engine's status the same way.
For reproducible behavior in machines that make random choices, fix the seed:
dotlottie_state_machine_set_seed(machine, 42);Observe transitions
State machines have their own event queue, drained like the player's:
dotlottieStateMachineEvent event;
while (dotlottie_state_machine_poll_event(machine, &event) == 1) {
switch (event.event_type) {
case StateMachineTransition:
printf("%s -> %s\n",
event.data.transition.previous_state,
event.data.transition.new_state);
break;
case StateMachineStateEntered:
printf("entered %s\n", event.data.state.state);
break;
case StateMachineNumericInputChange:
printf("%s: %.2f -> %.2f\n",
event.data.numeric_input.name,
event.data.numeric_input.old_value,
event.data.numeric_input.new_value);
break;
default:
break;
}
}| Event type | Numeric | Payload member |
StateMachineStart | 0 | — |
StateMachineStop | 1 | — |
StateMachineTransition | 2 | transition |
StateMachineStateEntered | 3 | state |
StateMachineStateExit | 4 | state |
StateMachineCustomEvent | 5 | message |
StateMachineError | 6 | message |
StateMachineStringInputChange | 7 | string_input |
StateMachineNumericInputChange | 8 | numeric_input |
StateMachineBooleanInputChange | 9 | boolean_input |
StateMachineInputFired | 10 | input_fired |
Every string pointer in a state machine event points into library-owned memory that the next poll call reuses. Use
strdup() or copy into your own buffer for anything you keep beyond the current iteration.
A second queue, drained with dotlottie_state_machine_poll_internal_event(), carries messages intended for platform integrations. Most applications can ignore it.
Stop and release
dotlottie_state_machine_stop() halts the machine but keeps the handle usable. dotlottie_state_machine_release() destroys it.
dotlottie_state_machine_stop(machine);
dotlottie_state_machine_release(machine);
dotlottie_destroy(player);The machine holds an internal borrow of the player, so the player must outlive it. Destroying the player first leaves the machine handle dangling.
Putting it together
dotlottieDotLottieStateMachine *machine =
dotlottie_state_machine_load(player, "button-states");
if (!machine) {
return 1;
}
dotlottie_state_machine_start(machine, NULL, true);
uint16_t interactions = 0;
dotlottie_state_machine_get_framework_setup(machine, &interactions);
bool needs_move = (interactions & (1 << 4)) != 0;
double last = now_seconds();
for (;;) {
double current = now_seconds();
float dt = (float)(current - last);
last = current;
input_event in;
while (next_input_event(&in)) {
switch (in.kind) {
case INPUT_MOVE:
if (needs_move) {
dotlottie_state_machine_post_pointer_move(machine, in.x, in.y);
}
break;
case INPUT_DOWN:
dotlottie_state_machine_post_pointer_down(machine, in.x, in.y);
break;
case INPUT_UP:
dotlottie_state_machine_post_pointer_up(machine, in.x, in.y);
dotlottie_state_machine_post_click(machine, in.x, in.y);
break;
}
}
bool rendered = false;
dotlottie_state_machine_tick(machine, dt, &rendered);
dotlottieStateMachineEvent event;
while (dotlottie_state_machine_poll_event(machine, &event) == 1) {
handle_machine_event(&event);
}
if (rendered) {
present(buffer, width, height);
}
}
dotlottie_state_machine_release(machine);
dotlottie_destroy(player);Learn more
Handle events — the player's own event queue
Memory and safety rules — handle lifetimes and destruction order
C API reference — full signatures