Skip to main content
Technical preview

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

Shapes

canvas.draw(geometry, paint) draws a geometry: a signed distance function (SDF) evaluated per pixel. Geometries come in three flavors: the built-in primitives, your own SDFs authored in TypeScript, and operator trees that compose shapes into one.

Primitives

std ships two geometry classes. Constructing one assembles the GPU props and the local bounding box for you:

import { Circle, RoundedRect } from "redraw";

canvas.draw(new Circle([200, 200], 80), paint);
canvas.draw(new RoundedRect([200, 200], [120, 80], 16), paint);
GeometryParameters
new Circle(center, radius, pad?)Center and radius. pad widens the bounding box for a wide stroke band or feather fringe.
new RoundedRect(center, halfSize, radius?)A box center ± halfSize with a corner radius.

Custom SDFs

A geometry is any object with { fn, props, bounds }, where fn is an sdf-kind function. Author one in TypeScript with the sdf() helper: the callback writes the signed distance into ctx.sdf (and optionally the gradient into ctx.grad for exact antialiasing):

import { sdf, createLibrary } from "redraw";
import { d, std } from "typegpu";

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

const library = createLibrary(device, [Disc]);

// At draw time, provide the props and a local-space bounding box:
canvas.draw(
{
fn: Disc,
props: { center: [200, 200], radius: 80 },
bounds: [120, 120, 280, 280],
},
paint,
);

The defaults object is the single source of truth: it types the callback's props and becomes the GPU-side props struct. A number maps to f32, a fixed-length tuple to the matching vecNf.

Shape operators

Operators compose shapes into a new shape: union, intersect, subtract, xor, and their smooth variants smoothUnion, smoothIntersect, smoothSubtract (which take a blend radius k). Operators nest, and the result is drawn like any geometry. They are opt-in: they compile a small per-pixel save stack into the canvas shader, so std leaves them out and you register shapeOpFns with your library:

import { Circle, createLibrary, shapeOpFns, smoothUnion } from "redraw";

const library = createLibrary(device, [...shapeOpFns]);

const a = new Circle([cx - 60, cy], 80);
const b = new Circle([cx + 60, cy], 96);
canvas.draw(smoothUnion(a, b, 40), paint);

The whole tree flattens into a single draw command, so the composite rasterizes as one shape: a translucent paint never double-blends where the operands overlap, and a stroke follows the combined outline.

Two orbiting discs joined by a smooth union, rasterized as one shapeOpen in editor →
FactoryResult
union(a, b)a ∪ b (min of the two SDFs).
intersect(a, b)a ∩ b (max).
subtract(a, b)a minus b.
xor(a, b)Covered by exactly one of a, b.
smoothUnion(a, b, k)Union with a polynomial smooth blend of radius k (the metaball effect).
smoothIntersect(a, b, k)Intersection with the crease rounded inward over k.
smoothSubtract(a, b, k)Subtraction with the cut edge rounded over k.

One evaluation detail: only an operator's right operand consumes a slot on the GPU's context stack, so nest deeper subtrees on the left. A left-leaning chain of any length needs a single slot; the constructor throws if a tree exceeds the pipeline's stack depth.