TakumiTakumi

Keyframe Animation

Animate scenes with CSS keyframes and render them as GIFs, WebP, APNG, or video frames.

Takumi supports two animation workflows:

  • Use renderAnimation() when you want a simple animated webp, apng, or gif.
  • Use render() with timeMs when you want full control over frame rendering, structured keyframes, or external encoding tools like ffmpeg.

This is an example of a keyframe animation rendered with ffmpeg + shiki for syntax highlighting:

How to define animations

Structured keyframes objects

Use structured keyframes when you render a specific animation frame with render() and timeMs. This is the API that accepts the keyframes option.

render.tsx
import {  } from "@takumi-rs/core";
import {  } from "@takumi-rs/helpers/jsx";

const  = new ();

const {  } = await (
  < ="animate-[move_1s_ease-in-out_infinite_alternate]" />,
);

const  = await .(, {
  : 100,
  : 100,
  : "png",
  : 500,
  : {
    : {
      : {
        : "translateX(0)",
      },
      "50%": {
        : "translateX(60px)",
      },
      : {
        : "translateX(120px)",
      },
    },
  },
});

CSS Stylesheets

Use stylesheet @keyframes when you want the animation to travel with the JSX tree and then render it with either renderAnimation() or render() + stylesheets.

render.tsx
import {  } from "@takumi-rs/core";
import {  } from "@takumi-rs/helpers/jsx";

const  = new ();

const { ,  } = await (
  < ="w-full h-full items-center justify-center">
    <>{`
      @keyframes move {
        from {
          transform: translateX(0);
        }

        to {
          transform: translateX(60px);
        }
      }
    `}</>
    < ="w-10 h-10 bg-red-500 animate-[move_1s_ease-in-out_infinite_alternate]" />
  </>,
);

const  = await .({
  : 100,
  : 100,
  : 30,
  : "webp",
  ,
  : [
    {
      : 1000,
      ,
    },
  ],
});

Tailwind animation utilities

Takumi supports these Tailwind animation forms:

  • Presets: animate-none, animate-spin, animate-ping, animate-pulse, animate-bounce
  • Arbitrary shorthand values: animate-[move_1s_ease-in-out_infinite_alternate]

For arbitrary values, _ is converted to a space, so:

animate-[move_1s_ease-in-out_infinite_alternate]

becomes:

animation: move 1s ease-in-out infinite alternate;

Takumi does not currently support Tailwind's animate-(--custom-property) form because CSS custom property resolution for animation is not implemented.

Ways to render

The next two examples share this file.

scene.tsx
import type {  } from "@takumi-rs/core";

export const :  = {
  : {
    : {
      : "translateX(0)",
    },
    : {
      : "translateX(60px)",
    },
  },
};

export function () {
  return (
    < ="w-full h-full items-center justify-center">
      < ="w-10 h-10 bg-red-500 animate-[move_1s_ease-in-out_infinite_alternate]" />
    </>
  );
}

render()

Use render() when you want a single frame at a specific animation time.

render.tsx
import {  } from "@takumi-rs/core";
import {  } from "@takumi-rs/helpers/jsx";
import { ,  } from "./scene";

const  = new ();

const {  } = await (< />);

const  = await .(, {
  : 100,
  : 100,
  : "png",
  ,
  : 500,
});

renderAnimation()

This is the minimal API for animated image output. In most cases, you should keep a single scene and animate it with stylesheet keyframes or Tailwind's built-in animation presets. The scenes field is still an array because you may want to compose multiple scenes into a single animated image, but Takumi does not generate transitions between them.

render.tsx
import {  } from "@takumi-rs/core";
import {  } from "@takumi-rs/helpers/jsx";

const  = new ();

const { ,  } = await (
  < ="w-full h-full items-center justify-center">
    <>{`
      @keyframes move {
        from {
          transform: translateX(0);
        }

        to {
          transform: translateX(60px);
        }
      }
    `}</>
    < ="w-10 h-10 bg-red-500 animate-[move_1s_ease-in-out_infinite_alternate]" />
  </>,
);

const  = await .({
  : 100,
  : 100,
  : 30,
  : "webp",
  ,
  : [
    {
      : 1000,
      ,
    },
  ],
});

render() + ffmpeg

The ffmpeg-keyframe-animation example renders raw frames with render() and streams them into ffmpeg. This is the better route when you want video output or tighter control over the pipeline.

The core idea is:

  1. Build the scene once.
  2. Render each frame at a specific timeMs.
  3. Pipe the raw RGBA frames into ffmpeg.
render.tsx
import {  } from "@takumi-rs/core";
import {  } from "@takumi-rs/helpers/jsx";
import {  } from "bun";
import { ,  } from "./scene";

const  = new ();
const  = 30;
const  = 4;
const  = 1200;
const  = 630;
const  =  * ;

const {  } = await (< />);

const  = (
  [
    "ffmpeg",
    "-y",
    "-f",
    "rawvideo",
    "-pixel_format",
    "rgba",
    "-video_size",
    `${}x${}`,
    "-framerate",
    `${}`,
    "-i",
    "pipe:0",
    "output.mp4",
  ],
  { : "pipe", : "ignore", : "ignore" },
);

for (let  = 0;  < ; ++) {
  const  = ( / ) * 1000;
  const  = await .(, {
    ,
    ,
    : "raw",
    ,
    ,
  });

  ..();
}

..();
await .;
Edit on GitHub

Last updated on

On this page