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 createSdf() helper: the callback writes the signed distance into ctx.sdf (and optionally the gradient into ctx.grad for exact antialiasing):

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

const Disc = createSdf(
(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.

Groups of drifting discs fused by smooth unions, each 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.