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:
| Option | Default | What it does |
|---|---|---|
tileSize | 16 | Square tile edge in pixels for the binning grid (a GPU work-split tuning knob). |
timestamps | false | Wrap each render's GPU passes in timestamp queries so canvas.gpuTime() works. |
maxPerTile | 32 | Upper bound on commands binned to a single tile. Raise it for dense scenes (many overlapping paths, self-intersecting strokes). |
textureSlots | 1 | Distinct 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:
| Method | What 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);
}
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).