Takumi

Keyframe Animation

Render CSS keyframe animations as GIF, animated WebP, APNG, or raw frames for ffmpeg.

Takumi samples CSS animations at a point in time. Pick the call by the output you need:

OutputCall
Animated WebP, APNG, or GIFrenderAnimation()
One frame at a given timerender() with timeMs
Video through ffmpegrender() with timeMs and format: "raw"

This video was rendered frame by frame and encoded with ffmpeg. Its code is highlighted with shiki.

Define an animation

With a stylesheet

Write @keyframes in a <style> tag inside the tree. render() and renderAnimation() both pick it up, so the animation travels with the component.

scene.tsx
export function () {
  return (
    < ="flex 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]" />
    </>
  );
}

The next examples render this Scene.

As an object

When the values come from application data, pass the animation as a css entry. Takumi checks every value before parsing, so nothing is concatenated into a CSS string.

render.tsx
import {  } from "takumi-js";

const  = await (< ="animate-[move_1s_ease-in-out_infinite_alternate]" />, {
  : 100,
  : 100,
  : 500,
  : {
    : "move",
    : [
      { : "from", : { : "translateX(0)" } },
      { : "50%", : { : "translateX(60px)" } },
      { : "to", : { : "translateX(120px)" } },
    ],
  },
});

Each step takes an offset:

OffsetPosition
fromStart of the animation
toEnd of the animation
"<n>%"A percentage offset, e.g. "50%"

The keyframes render option takes the same animation as a nested record. It is deprecated and will be removed in v3.

With Tailwind utilities

Takumi supports these Tailwind presets:

PresetEffect
animate-noneNo animation
animate-spinContinuous rotation
animate-pingScale and fade out, like a radar ping
animate-pulseFade opacity in and out
animate-bounceBounce up and down

An arbitrary value turns each _ into a space. animate-[move_1s_ease-in-out_infinite_alternate] becomes:

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

A named utility reads its theme variable, as in Tailwind. animate-wiggle reads var(--animate-wiggle), so declare that variable and its @keyframes. See Design tokens.

Takumi does not parse the animate-(--my-animation) form. Use a named utility with its --animate-* variable instead.

Render the animation

One frame with render()

Pass timeMs to render the frame at that point of the timeline.

render.tsx
import {  } from "takumi-js";
import {  } from "./scene";

const  = await (< />, {
  : 100,
  : 100,
  : 500,
});

An animated image with renderAnimation()

renderAnimation() samples each scene at fps and encodes the frames. It converts each scene's JSX, HTML string, or node tree, extracts its <style> tags, and fetches images once across every scene.

render.tsx
import {  } from "takumi-js";
import {  } from "./scene";

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

scenes plays several scenes in a row. Each scene starts its own timeline at 0, and Takumi does not tween between scenes.

Video with ffmpeg

Render each frame with format: "raw" and pipe the RGBA bytes into ffmpeg. The ffmpeg-keyframe-animation example is a complete version.

render.tsx
import {  } from "takumi-js";
import {  } from "bun";
import {  } from "./scene";

const  = 30;
const  = 4;
const  = 1200;
const  = 630;
const  =  * ;

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

const  = < />;

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

  ..();
}

..();
await .;

Last updated on