<canvas>

<canvas> is a pixel surface you draw on with a subset of the HTML CanvasRenderingContext2D path API. It is a styled node like <node> (layout, background, border, pointer handlers) whose texture the element owns. Drawing calls are recorded in JS and rasterized on the Bevy side, on the CPU and anti-aliased, at the node's laid-out size times the display's scale factor. Like an HTML canvas, the surface is retained: paint accumulates until something clears it.

Usage

<canvas
  style={{ width: 300, height: 150 }}
  draw={(ctx) => {
    ctx.strokeStyle = "#7aa2f7";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(0, 150);
    ctx.bezierCurveTo(100, 0, 200, 150, 300, 20);
    ctx.stroke();
  }}
/>

Coordinates are logical pixels from the node's top-left corner, the same units as style. The canvas has no intrinsic size: give it a width and a height, or let flex layout size it.

<canvas> comes from the canvas cargo feature, which is on by default and adds CanvasPlugin to ReactPlugins. Without it, a <canvas> mounts as a plain node and reports a featureMissing warning (see Cargo features).

There are two ways to draw, and one canvas can use both:

  • Declarative: a draw painter that describes the whole picture. The canvas is cleared and the painter replayed whenever draw changes.
  • Imperative: a ref handle whose context draws at any time, outside React rendering. Each batch paints on top of what is already there.

Declarative drawing

draw takes a painter function, called with a fresh CanvasContext that records every call into a display list. The list crosses to Bevy, which clears the surface, resets the drawing state and replays it.

  • The painter runs whenever the draw prop changes. A function compares by identity, so an inline arrow re-records on every render of the component. Wrap the painter in useCallback, with the data it reads as dependencies, to skip repaints when nothing changed.
  • draw also accepts a prebuilt DrawCmd[] list (the DrawCmd type is exported from bevy-react). A list compares by value.
  • After a resize, the runtime replays the latest painter for you.
  • The painter does not receive the canvas size. Draw at a size you set in style, or keep the size from onResize in state.
  • draw={[]} clears the canvas. Removing the draw prop keeps the last picture but stops the replays on resize.

Imperative drawing

A ref on a <canvas> resolves to a BevyCanvasElement handle. getContext() returns its long-lived context, with the same drawing API:

import { useRef } from "react";
import type { BevyCanvasElement } from "bevy-react";

function Sketchpad() {
  const ref = useRef<BevyCanvasElement>(null);
  const last = useRef<{ x: number; y: number } | null>(null);
  // Pointer x/y are normalized 0..1 within the element.
  const at = (e: { x: number; y: number }) => ({
    x: e.x * ref.current!.width,
    y: e.y * ref.current!.height,
  });

  return (
    <canvas
      ref={ref}
      style={{ width: 400, height: 300 }}
      onResize={({ width, height }) => {
        const ctx = ref.current!.getContext();
        ctx.fillStyle = "#1a1b26";
        ctx.beginPath();
        ctx.rect(0, 0, width, height);
        ctx.fill();
      }}
      onPointerDown={(e) => (last.current = at(e))}
      onPointerMove={(e) => {
        const from = last.current;
        if (!from) return;
        const to = at(e);
        const ctx = ref.current!.getContext();
        ctx.strokeStyle = "#7aa2f7";
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(from.x, from.y);
        ctx.lineTo(to.x, to.y);
        ctx.stroke();
        last.current = to;
      }}
      onPointerUp={() => (last.current = null)}
    />
  );
}
  • Calls made in one synchronous burst are batched and sent to Bevy as one update on a microtask. No React render is involved, so drawing at pointer rate costs no re-renders.
  • width and height are the laid-out size in logical pixels, 0 before the first layout.
  • The handle survives re-renders and Fast Refresh. Drawing through a handle whose canvas has unmounted does nothing.
  • Draw content that must survive a resize from onResize, which also fires on the first layout.

Retained pixels and resizing

Paint accumulates on the canvas until one of these clears it:

  • clear() or clearRect(x, y, w, h);
  • a change of the draw prop (clear, then replay);
  • a resize.

The drawing state (fillStyle, strokeStyle, lineWidth and the current path) also persists between batches, as on the web. A draw replay or a resize resets it to the defaults.

A resize is any change of the canvas's laid-out size in physical pixels, including its first layout and a display scale-factor change at the same logical size. It clears the surface (HTML canvas width/height semantics), then replays the declarative painter if there is one, then calls onResize with the new { width, height } in logical pixels.

Imperative drawing is erased by the next resize and, on a canvas that also has a draw painter, by the next draw change.

Drawing API

Member Effect
fillStyle, strokeStyle Fill and stroke color, any CSS color string
lineWidth Stroke width in logical pixels; 0, negative or non-finite values are ignored
beginPath() Start a new, empty path
moveTo(x, y) Start a subpath at a point
lineTo(x, y) Add a line; without a current point it starts the subpath there
quadraticCurveTo(cx, cy, x, y) Add a quadratic Bézier; ignored without a current point
bezierCurveTo(c1x, c1y, c2x, c2y, x, y) Add a cubic Bézier; ignored without a current point
arc(x, y, r, start, end) Add a circular arc, angles in radians, drawn clockwise; joins the current point by line
rect(x, y, w, h) Add a rectangle subpath
closePath() Close the current subpath
fill() Fill the current path (nonzero winding rule)
stroke() Stroke the current path (butt caps, miter joins)
clearRect(x, y, w, h) Erase a rectangle to transparent; path and styles are kept
clear() Erase the whole surface (not in the web API); path and styles are kept
  • Colors take the same strings as styles: hex, named colors, rgb(), hsl() and the other functional forms, alpha included. A color that doesn't parse paints opaque black, without a warning.
  • The starting fill color is white and the starting stroke color black, with a 1px line. On the web the fill starts black, so set fillStyle before the first fill().
  • Every method returns the context, so calls chain.

Styles and events

  • <canvas> takes everything <node> does: style, hoverStyle, pressStyle, focusStyle, pointer handlers, onWheel and the scroll props.
  • Pointer events report x/y normalized to 0..1. Multiply by the handle's width/height to get canvas coordinates, as above.
  • backgroundColor shows through transparent pixels. backgroundImage is ignored (the element owns its image) with a styleIgnored warning.

Limits

  • Only the path API above. There are no transforms (translate, rotate, setTransform), no text, images, gradients or patterns, no line caps, joins or dashes, no globalAlpha, no save/restore, no ellipse, arcTo, fillRect or strokeRect, no counter-clockwise arcs, and no isPointInPath.
  • Rasterization runs on the CPU, and every painted batch re-uploads the whole texture. A large canvas redrawn every frame costs CPU time and upload bandwidth.
  • The backing texture is capped at 4096 pixels per side.
  • imageRendering modes other than "auto" are not available on a canvas and report an imageRendering warning.

See <canvas> in the element reference.