<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
drawpainter that describes the whole picture. The canvas is cleared and the painter replayed wheneverdrawchanges. - Imperative: a
refhandle 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
drawprop changes. A function compares by identity, so an inline arrow re-records on every render of the component. Wrap the painter inuseCallback, with the data it reads as dependencies, to skip repaints when nothing changed. drawalso accepts a prebuiltDrawCmd[]list (theDrawCmdtype is exported frombevy-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 fromonResizein state. draw={[]}clears the canvas. Removing thedrawprop 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.
widthandheightare the laid-out size in logical pixels,0before 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()orclearRect(x, y, w, h);- a change of the
drawprop (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
fillStylebefore the firstfill(). - Every method returns the context, so calls chain.
Styles and events
<canvas>takes everything<node>does:style,hoverStyle,pressStyle,focusStyle, pointer handlers,onWheeland the scroll props.- Pointer events report
x/ynormalized to0..1. Multiply by the handle'swidth/heightto get canvas coordinates, as above. backgroundColorshows through transparent pixels.backgroundImageis ignored (the element owns its image) with astyleIgnoredwarning.
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, noglobalAlpha, nosave/restore, noellipse,arcTo,fillRectorstrokeRect, no counter-clockwise arcs, and noisPointInPath. - 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.
imageRenderingmodes other than"auto"are not available on a canvas and report animageRenderingwarning.
See <canvas> in the element reference.
bevy-react