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

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 thread count is fixed for the whole process

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.lottie

Open 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 */
  }
}
dt is in seconds, not milliseconds

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

Last updated: August 13, 2026 at 9:17 AMEdit this page