Render your first animation
Follow this tutorial to load a Lottie animation in C, render it into a pixel buffer with the software renderer, and save frames to an image file.
In this tutorial you will write a complete C program that loads a Lottie animation, renders it into a pixel buffer, and saves a frame to an image file you can open. It needs no window system and no graphics library — just the runtime and a C compiler.
Prerequisites
A built library and
dotlottie_player.hfor your machine. If you don't have them yet, run through Build for your target architecture withmake nativeWorking include and link flags
A
.lottieor Lottie.jsonfile to play
Step 1: Create a player
Every operation goes through a player handle. Create one with dotlottie_new_player(), and release it with dotlottie_destroy() when you're done.
#include "dotlottie_player.h"
DotLottiePlayer *player = dotlottie_new_player(0);
if (!player) {
return 1;
}The argument is the number of rendering worker threads. Pass 0 to render on the calling thread.
The renderer's thread pool is created once, when the first player is constructed, and later players reuse it. Passing
a different number to a second dotlottie_new_player() call has no effect. Threading also requires the tvg-threads
feature to be compiled in.
Step 2: Attach a render target
The player does not allocate a canvas for you. With the software renderer, you supply a buffer of width × height 32-bit pixels and the player draws into it. The buffer belongs to you, so it has to stay alive for as long as the player uses it.
#include <stdlib.h>
#define WIDTH 512
#define HEIGHT 512
uint32_t *buffer = malloc(WIDTH * HEIGHT * sizeof(uint32_t));
if (dotlottie_set_sw_target(player, buffer, WIDTH, HEIGHT, ABGR8888) != DOTLOTTIE_SUCCESS) {
return 1;
}ABGR8888 packs each pixel as 0xAABBGGRR — alpha in the high byte, red in the low byte — with colors alpha-premultiplied.
Step 3: Configure and load
Set your playback configuration before loading, then load the animation from a path:
dotlottie_set_background(player, 255, 255, 255, 255); /* opaque white */
dotlottie_set_loop(player, true);
dotlottie_set_autoplay(player, true);
if (dotlottie_load_animation_path(player, "animation.lottie") != DOTLOTTIE_SUCCESS) {
return 1;
}With autoplay enabled the animation begins playing as soon as it loads, so you don't need to call dotlottie_play().
Every fallible function returns a dotlottieDotLottieResult, where DOTLOTTIE_SUCCESS (0) means success. Checking it on load is important: a missing file, an unsupported feature, or malformed animation data all surface here.
Step 4: Render a frame and save it
Jump to a specific frame and render it. dotlottie_render() draws the current frame without advancing time, which is exactly what you want for a still image:
float total = 0.0f;
dotlottie_get_total_frames(player, &total);
dotlottie_set_frame(player, total / 2.0f); /* halfway through */
dotlottie_render(player);The buffer now holds the rendered frame. Write it out as a PPM↗ file, a format simple enough to produce in a few lines and openable by most image viewers:
#include <stdio.h>
static void save_ppm(const char *path, const uint32_t *pixels, int width, int height) {
FILE *f = fopen(path, "wb");
fprintf(f, "P6\n%d %d\n255\n", width, height);
for (int i = 0; i < width * height; i++) {
uint32_t px = pixels[i];
fputc(px & 0xFF, f); /* red */
fputc((px >> 8) & 0xFF, f); /* green */
fputc((px >> 16) & 0xFF, f); /* blue */
}
fclose(f);
}Step 5: Clean up
Destroy the player before freeing the buffer. The player holds the pointer you handed it in step 2, so releasing the memory first would leave it with a dangling reference:
dotlottie_destroy(player);
free(buffer);The complete program
#include "dotlottie_player.h"
#include <stdio.h>
#include <stdlib.h>
#define WIDTH 512
#define HEIGHT 512
static void save_ppm(const char *path, const uint32_t *pixels, int width, int height) {
FILE *f = fopen(path, "wb");
if (!f) return;
fprintf(f, "P6\n%d %d\n255\n", width, height);
for (int i = 0; i < width * height; i++) {
uint32_t px = pixels[i];
fputc(px & 0xFF, f);
fputc((px >> 8) & 0xFF, f);
fputc((px >> 16) & 0xFF, f);
}
fclose(f);
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s <animation.lottie|animation.json>\n", argv[0]);
return 1;
}
DotLottiePlayer *player = dotlottie_new_player(0);
if (!player) {
fprintf(stderr, "could not create player\n");
return 1;
}
uint32_t *buffer = malloc(WIDTH * HEIGHT * sizeof(uint32_t));
if (!buffer) {
dotlottie_destroy(player);
return 1;
}
int status = 1;
if (dotlottie_set_sw_target(player, buffer, WIDTH, HEIGHT, ABGR8888) != DOTLOTTIE_SUCCESS) {
fprintf(stderr, "could not set render target\n");
goto cleanup;
}
dotlottie_set_background(player, 255, 255, 255, 255);
dotlottie_set_loop(player, true);
dotlottie_set_autoplay(player, true);
if (dotlottie_load_animation_path(player, argv[1]) != DOTLOTTIE_SUCCESS) {
fprintf(stderr, "could not load %s\n", argv[1]);
goto cleanup;
}
float total = 0.0f;
float duration = 0.0f;
dotlottie_get_total_frames(player, &total);
dotlottie_get_duration(player, &duration);
printf("loaded %.0f frames, %.2f seconds\n", total, duration);
dotlottie_set_frame(player, total / 2.0f);
if (dotlottie_render(player) != DOTLOTTIE_SUCCESS) {
fprintf(stderr, "render failed\n");
goto cleanup;
}
save_ppm("frame.ppm", buffer, WIDTH, HEIGHT);
printf("wrote frame.ppm\n");
status = 0;
cleanup:
dotlottie_destroy(player);
free(buffer);
return status;
}Build and run it:
DOTLOTTIE_DIR=release/native/dotlottie-player
cc main.c -I$DOTLOTTIE_DIR/include -L$DOTLOTTIE_DIR/lib -ldotlottie_player -o player
LD_LIBRARY_PATH=$DOTLOTTIE_DIR/lib ./player animation.lottieOpen frame.ppm to confirm it rendered.
Step 6: Animate over time
For continuous playback, replace the single render with a loop driven by dotlottie_tick(). It advances the animation by an elapsed time and renders only when the displayed frame actually changed, so you can skip presenting a frame that would be identical to the last one:
#include <time.h>
static double now_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
double last = now_seconds();
while (!dotlottie_is_complete(player)) {
double current = now_seconds();
float dt = (float)(current - last);
last = current;
bool rendered = false;
dotlottie_tick(player, dt, &rendered);
if (rendered) {
present(buffer, WIDTH, HEIGHT); /* blit to your window or texture */
}
}dotlottie_tick() expects the elapsed time in seconds. The example inside the generated dotlottie_player.h
computes dt from a millisecond clock without converting, which advances the animation 1000× too fast — don't copy
it. If your timer reports milliseconds, divide by 1000 before passing the value.
Because looping is enabled in this program, dotlottie_is_complete() never returns true and the loop runs until you break out of it. Set dotlottie_set_loop(player, false) to have the animation finish on its own.
Next steps
How the C API works — the design behind handles, results, and features
Choose a render target — render with OpenGL or WebGPU instead of software
Control playback — speed, direction, segments, and markers
C API reference — every function and type