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);
| Geometry | Parameters |
|---|---|
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.
| Factory | Result |
|---|---|
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.