Redraw is currently in technical preview, available to wcandillon.dev subscribers. API is unstable.
Custom Colors
A color function is authored like the stroke functions in
Stroke, with the color() helper. The callback writes a
straight-alpha RGBA into paint.color, and the same geometry context is
available (ctx.t, ctx.sdf, ctx.tan, ctx.grad, tctx.pos), so
colors can react to where on the shape they're being painted.
Anatomy
import { color, Color } from "redraw";
import { std } from "typegpu";
const PathGradient = color(
(paint, ctx, tctx, props) => {
"use gpu";
const a = Color("#3FCEBC");
const b = Color("#DE589F");
paint.color = std.mix(a, b, std.fract(ctx.t + props.shift));
},
{ shift: 0 },
);
The callback receives (paint, ctx, tctx, props):
| Parameter | What it is |
|---|---|
paint | Mutable: write paint.color (a vec4f); read paint.strokeWidth. |
ctx | The geometry context: ctx.t, ctx.sdf, ctx.tan, ctx.grad. |
tctx | The transform context: tctx.pos, tctx.worldPos. |
props | The per-draw uniforms, typed from the defaults object. |
Add it with addShader: the first color shader is the paint's base
color, and later ones act as filters (each reads and overwrites the color
written by the steps before it):
// 0.2 palette cycles per second (time is in milliseconds):
const paint = new Paint().addShader(PathGradient, { shift: time * 0.0002 });
// or, appended after a base color:
paint.addShader(MyFilter, { amount: 0.5 });
As with every custom function, declare it in the canvas's Library.
What you write
paint.color = d.vec4f(r, g, b, a); // straight (un-premultiplied) alpha
Anything from (0, 0, 0, 0) (transparent) to (1, 1, 1, 1) (opaque
white). Values outside [0, 1] are valid for HDR-style blending but get
clamped on output.
Helpers
Color("#hex") parses a color at compile time into a vec4f for use
inside a GPU callback:
const palette = [Color("#3FCEBC"), Color("#DE589F"), Color("#FAEC54")];
interpolateColors(t, colors) walks a palette cyclically (the last
color wraps back to the first):
paint.color = interpolateColors(ctx.t + props.shift, palette);
There are also scalar and vector interpolators over explicit stops:
interpolate(t, stops, values), interpolate2, interpolate3,
interpolate4. All are exported from redraw and generate WGSL inline;
no runtime overhead beyond the actual color math.
Recipes
Palette along the path
The Hello example walks an 11-color palette using ctx.t, with a
shift prop animating the offset each frame:
const PathGradient = color(
(paint, ctx, _tctx, props) => {
"use gpu";
const colors = [
Color("#3FCEBC"), Color("#3CBCEB"), Color("#5F96E7"),
Color("#816FE3"), Color("#9F5EE2"), Color("#DE589F"),
Color("#FF645E"), Color("#FDA859"), Color("#FAEC54"),
Color("#9EE671"), Color("#41E08D"),
];
const pos = std.fract(ctx.t + props.shift) * 10;
const i = d.u32(std.floor(pos));
const f = std.fract(pos);
const rgb = std.mix(colors[std.min(i, 10)], colors[std.min(i + 1, 10)], f);
paint.color = rgb.rgba;
},
{ shift: 0 },
);
// Every frame, 0.2 palette cycles per second (time is in milliseconds):
const paint = new Paint().addShader(PathGradient, { shift: time * 0.0002 });
Gradients from the position
tctx.pos is the position in drawing space, so a linear gradient is a
projection onto a direction:
const LinearGradient = color(
(paint, _ctx, tctx, props) => {
"use gpu";
const dir = std.sub(props.p2, props.p1);
const t = std.clamp(
std.dot(std.sub(tctx.pos, props.p1), dir) /
std.max(std.dot(dir, dir), 0.000001),
0,
1,
);
const c0 = d.vec3f(0.553, 0.22, 0.667);
const c1 = d.vec3f(0.0, 0.29, 0.663);
paint.color = d.vec4f(std.mix(c0, c1, t), 1);
},
{ p1: [0, 0], p2: [0, 0] },
);
Shading by distance
ctx.sdf is the signed distance to the shape's edge (negative inside),
and for strokes paint.strokeWidth carries the local width, so you can
build centerline highlights or two-tone strokes: compute the distance
from the centerline as ctx.sdf + paint.strokeWidth * 0.5 and mix
toward a highlight color where it approaches zero.