Skip to main content
Technical preview

Redraw is currently in technical preview, available to start-react-native.dev subscribers. API is unstable.

Custom Colors

A color function is the same fn(...) you saw in Stroke, but it writes to canvas.color (a vec4f) instead of canvas.field. The same geometry context is available (ctx.t, ctx.sdf, ctx.tan, ctx.grad), so colors can react to where on the path they're being painted.

Anatomy

import { fn, Color, interpolateColors } from "redraw";
import { d, std } from "typegpu";

const PathGradient = fn(
(canvas, ctx, props) => {
"use gpu";
const a = Color("#3FCEBC");
const b = Color("#DE589F");
canvas.color = d.vec4f(std.mix(a, b, ctx.t), 1.0);
},
{ /* default props */ },
);

Pass an instance to brush.addStroke(colorFn, width) or brush.addFill(colorFn). With SingleStrokeBrush (see Brushes) the full geometry context is populated; with Brush you have ctx.sdf, ctx.pos, and shape bounds.

What you write

canvas.color = d.vec4f(r, g, b, a); // pre-multiplied RGBA

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 string into a vec3f for use inside a fn:

const palette = [Color("#3FCEBC"), Color("#DE589F"), Color("#FAEC54")];

interpolateColors(t, colors) walks a palette and returns a struct with .rgb:

const rgb = interpolateColors(ctx.t, palette).rgb;
canvas.color = d.vec4f(rgb, 1.0);

Both are exported from redraw. They 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. A colorShift prop animates the offset:

const ColorNode = fn(
(canvas, ctx, props) => {
"use gpu";
const palette = [
Color("#3FCEBC"), Color("#3CBCEB"), Color("#5F96E7"),
Color("#816FE3"), Color("#9F5EE2"), Color("#DE589F"),
Color("#FF645E"), Color("#FDA859"), Color("#FAEC54"),
Color("#9EE671"), Color("#41E08D"),
];
const rgb = interpolateColors(ctx.t + props.colorShift, palette).rgb;
canvas.color = d.vec4f(rgb, 1.0);
},
{ colorShift: 0 },
);

// Animate every frame:
colorNode.props.colorShift = ctx.time * 0.2;
Eleven-color palette interpolated along ctx.t, drifting over timeOpen in editor →

Centerline highlight from ctx.sdf

ctx.sdf is negative inside the stroke and zero at the centerline. Combined with canvas.field (the local half-width), you can compute distance from the center as a normalized [0, 1]:

const distFromCenter = ctx.sdf + canvas.field * 0.5;
const centerFactor = std.saturate(1.0 - distFromCenter / (canvas.field * 0.5));
const finalRgb = std.mix(rgb, d.vec3f(1), centerFactor * props.progress);

This produces a white "ridge" along the spine of the stroke that fades to the underlying color at the edges.

Tangent sidedness

The cross product ctx.tan.x * ctx.grad.y - ctx.tan.y * ctx.grad.x is positive on one side of the centerline and negative on the other: direction-of-travel "left vs right". Use it for two-tone strokes or gradients that span the stroke perpendicular to the path:

const RainbowColor = fn(
(canvas, ctx, props) => {
"use gpu";
const distFromCenter = ctx.sdf + canvas.field * 0.5;
const side = std.sign(ctx.tan.x * ctx.grad.y - ctx.tan.y * ctx.grad.x);
const signedDist = distFromCenter * side;
const normalized = (signedDist / (canvas.field * 0.5)) * 0.5 + 0.5;
const rgb = interpolateColors(normalized + props.colorShift, palette).rgb;
canvas.color = d.vec4f(rgb, 1.0);
},
{ colorShift: 0 },
);
Cross product of tan × grad gives a signed distance across the strokeOpen in editor →

Multi-palette + cel shading

Conditional palette selection plus smoothstep posterization gives a crisp, illustrative look:

Watercolor body, blue shadow, sketchy noise-perturbed outline, all in one fnOpen in editor →