Skip to main content
Technical preview

Redraw is currently in technical preview, available to wcandillon.dev subscribers. API is unstable.

Drawings

Two objects do most of the work in Redraw: a Library holds the compiled drawing vocabulary, and a Canvas records and renders frames against a GPU texture.

Library

A Library is the up-front half of the API. You declare your custom GPU functions once (geometries, colors, stroke widths, clips, feathers); the Library prepends the std base vocabulary and holds the device:

import { createLibrary } from "redraw";

const library = createLibrary(device, [PathGradient, HelloStroke], {
maxPerTile: 64,
});

The options:

OptionDefaultWhat it does
tileSize16Square tile edge in pixels for the binning grid (a GPU work-split tuning knob).
timestampsfalseWrap each render's GPU passes in timestamp queries so canvas.gpuTime() works.
maxPerTile32Upper bound on commands binned to a single tile. Raise it for dense scenes (many overlapping paths, self-intersecting strokes).
textureSlots1Distinct textures a frame can sample (texture bindings are fixed at compile time).

The std vocabulary is always included: Circle, RoundedRect, ColorFill, ImageShader, the path function, and the built-in feathers. You only declare what you author yourself (see Custom Effects). The shape operators (union, smoothUnion, ...) are opt-in: pass ...shapeOpFns to createLibrary (see Shape operators).

Canvas

library.makeCanvas(target) binds a GPUTexture and compiles the pipeline for that surface's resolution and format. Canvases are long-lived, like Skia's SkSurface: make one per surface and reuse it across frames.

const canvas = library.makeCanvas(texture);

The target must be a writable storage-texture format: rgba8unorm, bgra8unorm (with the bgra8unorm-storage device feature), rgba16float, or rgba32float.

A Canvas is an immediate-mode recorder. Each frame is an independent record → render cycle:

import { Circle, Paint } from "redraw";

canvas.fill(new Paint().setColor("#1a1a2e"));
canvas.draw(
new Circle([200, 200], 80),
new Paint().setColor("#3FCEBC"),
);
canvas.render(); // draws and resets the recorder

The most important canvas methods:

MethodWhat it records
fill(paint)Fill the current clip region (the background, when no clip is active).
draw(geometry, paint)A geometry (Circle, RoundedRect, custom SDF) or a shape-operator tree. See Shapes.
drawPath(path, paint, options?)A Path, filled or stroked depending on the paint. See Paths.
drawImage(texture, x?, y?, options?)A texture. See Images.
save() / restore()Save and restore the transform and clip stacks.
translate(x, y) / scale(sx, sy?) / concat(m)Mutate the current transform (an Affine).
clip(fn, props)Push a clip; subsequent draws are intersected with it on the GPU.
pushLayer(descriptor) / popLayer()Open and close a feathered, optionally tinted layer. See Vector Feathering.
render(target?)Upload the recording, run the GPU passes, reset the recorder.

Animation

There are no retained nodes and no handles: to animate, re-record the scene each frame with new values. Recording is cheap (the CPU side just stages structs and commands), and the React bindings call your render callback every frame with the elapsed time, in milliseconds:

export function render(canvas: Canvas, { width, height, time }: FrameInfo) {
canvas.fill(background);
// One orbit per 2π seconds.
const cx = width / 2 + Math.cos(time * 0.001) * 100;
const cy = height / 2 + Math.sin(time * 0.001) * 100;
canvas.draw(new Circle([cx, cy], 40), paint);
}
A scene re-recorded every frame: position, blend, and glow all driven by timeOpen in editor →

Transforms and clips

The GPU pipeline has no transform stack (tiling flattens the scene into independent commands), so the recorder holds one on the CPU. save() snapshots the current transform and clip depth, restore() rewinds to it:

canvas.save();
canvas.translate(cx, cy);
canvas.scale(zoom);
canvas.draw(shape, paint);
canvas.restore();

clip(fn, props) takes a clip-kind function. Author one in TypeScript with the clip() helper:

import { clip } from "redraw";
import { d, std } from "typegpu";

const CircleClip = clip(
(cctx, tctx, props) => {
"use gpu";
const delta = std.sub(tctx.pos, props.center);
const r = std.length(delta);
cctx.sdf = r - props.radius;
cctx.grad = std.select(d.vec2f(0), std.div(delta, r), r > 0);
},
{ center: [0, 0], radius: 0 },
);

// createLibrary(device, [CircleClip, ...])
canvas.clip(CircleClip, { center: [200, 200], radius: 150 });

Each subsequent draw bakes the active clips into its command, intersected per pixel on the GPU with a smooth antialiased edge. One caveat: the clip props are interpreted under each draw's transform (a command has one transform), so push clips under the same transform you draw with.

GPU timing

With a Library created with timestamps: true (and a device that has the timestamp-query feature), the canvas reports the GPU time of the most recent render():

const ns = await canvas.gpuTime(); // total, in nanoseconds
const { binning, draw } = await canvas.gpuTimes(); // per pass

The React bindings expose this through the onGPUTime and onStats props (see React).