Back to Remotion

Using `` in Remotion

packages/skills/skills/remotion-markup/html-in-canvas.md

4.0.5133.6 KB
Original Source

Using <HtmlInCanvas> in Remotion

Renders children into a <canvas> so you can post-process them with the Canvas 2D API or WebGL.

Only works in Chrome 149+ with the chrome://flags/#canvas-draw-element flag enabled.
Give the user a notice.

Nesting

<HtmlInCanvas> components may be nested in Chrome 152.0.7944.0 and later. Older Chrome versions support a single <HtmlInCanvas>, but do not correctly paint nested HTML-in-canvas subtrees.

Enabling WebGL during renders

If you make use of WebGL during renders, you need to enable it:

From the CLI:

bash
npx remotion render --gl=angle

Set it as the default for Studio and CLI (advised):

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

Config.setChromiumOpenGlRenderer("angle");

Basic usage

By default, draws to canvas with no effect applied:

tsx
import { HtmlInCanvas } from "remotion";

export const MyComp = () => {
  return (
    <HtmlInCanvas width={1280} height={720}>
      <div style={{ fontSize: 80 }}>
        Hello
      </div>
    </HtmlInCanvas>
  );
};

2D effect with onPaint

onPaint runs whenever the content updates. Call ctx.drawElementImage(elementImage, 0, 0) to draw the captured DOM, and assign the returned transform to element.style.transform so DOM selection still aligns with the painted output.

tsx
import {
  AbsoluteFill,
  HtmlInCanvas,
  type HtmlInCanvasOnPaint,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import { useCallback } from "react";

export const Blur = () => {
  const frame = useCurrentFrame();
  const { width, height, fps } = useVideoConfig();

  const onPaint: HtmlInCanvasOnPaint = useCallback(
    ({ canvas, element, elementImage }) => {
      const ctx = canvas.getContext("2d");
      if (!ctx) throw new Error("Failed to acquire 2D context");

      const blurPx = 4 + 18 * (0.5 + 0.5 * Math.sin((frame / fps) * Math.PI));

      ctx.reset();
      ctx.filter = `blur(${blurPx}px)`;
      const transform = ctx.drawElementImage(elementImage, 0, 0);
      element.style.transform = transform.toString();
    },
    [frame, fps],
  );

  return (
    <HtmlInCanvas width={width} height={height} onPaint={onPaint}>
      <AbsoluteFill
        style={{
          justifyContent: "center",
          alignItems: "center",
          fontSize: 120,
        }}
      >
        <h1>
          Hello
        </h1>
      </AbsoluteFill>
    </HtmlInCanvas>
  );
};

WebGL effects

For WebGL, set up the context, program, and texture in onInit and return a cleanup function. Inside onPaint, upload the captured DOM with gl.texElementImage2D(...) and draw.

tsx
const onInit: HtmlInCanvasOnInit = useCallback(({ canvas }) => {
  const gl = canvas.getContext("webgl2", { alpha: true, premultipliedAlpha: true });
  if (!gl) {
    throw new Error(
      "WebGL2 unavailable. Try rendering with the --gl=angle option. See https://remotion.dev/docs/gl-options.",
    );
  }
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  // compile program, create texture, set up VAO...
  return () => {
    // delete program, texture, buffers...
  };
}, []);

const onPaint: HtmlInCanvasOnPaint = useCallback(({ elementImage }) => {
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, elementImage);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}, []);

For a fully working minimal example, see https://github.com/remotion-dev/remotion/blob/main/packages/docs/components/demos/HtmlInCanvasDocsDemoWebGL.tsx.

Async onPaint

onPaint may be async. Remotion holds the frame open via delayRender() until the promise resolves. Useful for multi-pass effects with createImageBitmap.