Skip to content

Remotion

Tegaki integrates with Remotion for programmatic video rendering. Because Remotion drives time via useCurrentFrame(), the trick is to put Tegaki in controlled progress mode and map frame / durationInFrames to a 0–1 progress value.

npm install tegaki remotion @remotion/cli react react-dom
import { Composition, useCurrentFrame, useVideoConfig } from 'remotion';
import { TegakiRenderer } from 'tegaki';
import caveat from 'tegaki/fonts/caveat';

const Handwriting: React.FC = () => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();
  const progress = frame / durationInFrames;

  return (
    <div style={{ flex: 1, backgroundColor: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <TegakiRenderer
        font={caveat}
        text="What a wonderful world"
        style={{ fontSize: 120 }}
        time={{ mode: 'controlled', value: progress, unit: 'progress' }}
      />
    </div>
  );
};

export const RemotionRoot: React.FC = () => {
  return (
    <Composition
      id="Handwriting"
      component={Handwriting}
      durationInFrames={180}
      fps={60}
      width={1920}
      height={1080}
    />
  );
};

The unit: 'progress' option interprets value as a 0–1 ratio of the total animation duration, so you don’t need to know Tegaki’s computed timeline length — Remotion’s composition length drives the mapping directly.

Tegaki’s font bundles (tegaki/fonts/caveat, tegaki/fonts/italianno, etc.) handle font loading automatically: the engine creates a FontFace and registers it with document.fonts. Remotion waits on document.fonts.ready before capturing each frame, so no loadFont call is required — just pass the bundle to TegakiRenderer.

If you render directly with remotion render, everything works out of the box. However, Remotion Studio’s dev server serves the TTF URL incorrectly because the bundled path escapes the studio’s root (webpack emits ../../node_modules/tegaki/fonts/caveat/caveat.ttf, which the SPA catch-all intercepts and returns HTML).

The fix is to tell webpack to inline .ttf files as data URLs. Add a remotion.config.ts at the root of your Remotion project:

// remotion.config.ts
import { Config } from '@remotion/cli/config';

Config.overrideWebpackConfig((config) => {
  return {
    ...config,
    module: {
      ...config.module,
      rules: [
        ...(config.module?.rules ?? []).filter((rule) => {
          if (typeof rule !== 'object' || !rule || !rule.test) return true;
          const test = rule.test;
          return !(test instanceof RegExp && test.test('a.ttf'));
        }),
        {
          test: /\.ttf$/,
          type: 'asset/inline',
        },
      ],
    },
  };
});

This works in both the studio and in render/build mode. The bundled font is ~60–250 kB depending on the family, which is negligible for a video bundle.

By default, progress = frame / durationInFrames spreads the handwriting animation across the entire composition. Two common variations:

Finish drawing before the end of the composition — e.g. hold the finished text for the last second:

const { fps, durationInFrames } = useVideoConfig();
const drawFrames = durationInFrames - fps; // leave 1s of hold
const progress = Math.min(1, frame / drawFrames);

Start drawing after a delay:

const { fps } = useVideoConfig();
const delayFrames = fps; // 1s delay
const drawFrames = 120; // 2s to draw
const progress = Math.max(0, Math.min(1, (frame - delayFrames) / drawFrames));

The pre-built bundles (tegaki/fonts/*) are convenient but you can use any font generated by the Tegaki generator. See Generating Font Data.