Redraw is currently in technical preview, available to wcandillon.dev subscribers. API is unstable.
Gradients
Redraw ships four gradient bindings: LinearGradient, RadialGradient,
GradientAlongPath, and BilinearGradient. They are all part of std, so
they need no Library registration; construct one and
add it to a paint:
import { LinearGradient, Paint } from "redraw";
const paint = new Paint().addShader(
new LinearGradient(["#8D38AA", "#004AA9"], { to: [width, 0] }),
);
They all share the same stop model:
- Colors accept anything a paint does: CSS-style strings (
"#8D38AA","rgb(36,43,56)", named colors) or[r, g, b, a]arrays of 0..1 floats. - Positions are optional:
positionsplaces each color on the gradient axis, in[0, 1]and never decreasing, one per color (evenly spaced when omitted). Two colors sharing a position read as a hard cut. - Everything stays live after construction.
setColors,setPositions, and the geometry accessors (from/to,center/radius,shift, ...) can be reassigned every frame: the stops are re-staged on each draw that uses the binding, so one instance built at module scope serves an animated scene.
LinearGradient
Interpolates the colors between two points in the draw's local coordinate
space. The position is projected onto the from to to axis and clamped, so
the end colors extend past the endpoints.
const gradient = new LinearGradient(["#FFFFFF", "#E8EEFF"], {
from: [0, 0],
to: [width, height],
});
| Option | What it does |
|---|---|
from | Where the first stop sits (default [0, 0]). Animatable. |
to | Where the last stop sits (default [0, 0]; set it, or the gradient degenerates to the first color). Animatable. |
positions | Relative position of each color on the axis (default: evenly spaced). |
RadialGradient
Interpolates the colors over the distance from a center point: the first stop
at the center, the last at radius, clamped past it.
const background = new RadialGradient(["#2C2C2C", "#080808"]);
// In render(), the accessors follow the canvas. `fill` accepts a color
// binding directly (sugar for a paint with that single shader):
background.center = [width / 2, height / 2];
background.radius = width / 2;
canvas.fill(background);
| Option | What it does |
|---|---|
center | Center of the gradient (default [0, 0]). Animatable. |
radius | Distance from the center where the last stop sits (default 1). Animatable. |
positions | Relative position of each color between center and radius (default: evenly spaced). |
GradientAlongPath
Interpolates the colors over the along-path coordinate ctx.t: the palette
rides the arc length of the drawn path, which is the natural gradient for
strokes.
const gradient = new GradientAlongPath(
["#3FCEBC", "#3CBCEB", "#5F96E7", "#816FE3", "#9F5EE2", "#DE589F"],
{ period: 5000 },
);
// In render(), one full palette cycle every `period` ms:
gradient.time = time;
const paint = new Paint().addShader(gradient).setStroke(25);
By default the palette is cyclic: the last stop blends back into the
first, so the gradient tiles seamlessly and an animated shift never shows a
seam. Set cyclic: false to lay the stops end to end over t in [0, 1]
instead, for gradients whose two ends differ (e.g. a fade).
| Option | What it does |
|---|---|
shift | Along-path offset in palette cycles (default 0), a dimensionless phase: 0.25 rotates the palette a quarter turn. Animatable. |
period | Milliseconds for one full palette cycle; unlocks the time accessor (gradient.time = frame.time). |
cyclic | Tile the palette seamlessly (default true). |
positions | Relative position of each color along the path. In cyclic mode the positions live on the cycle: the last stop blends back into the first over the remaining span. |
anchor | What t = 0 and t = 1 mean when drawing a Path.segment cut (default "source", see below). |
colorSpace | "oklab" interpolates the stops perceptually (no muddy gray midpoints); the result is encoded back to sRGB, so filters and blending compose unchanged. |
Write-on animations
When the drawn path is a Path.segment cut, the default anchor: "source"
reads the gradient over the cut's source range: the colors stay anchored to
the full path while a sub-range is drawn, so a write-on reveals them in place
instead of squeezing the whole palette into the drawn portion. Set
anchor: "segment" for the opposite: the gradient spans the drawn segment
itself and stretches with it (e.g. a comet trail that always shows the full
palette).
BilinearGradient
A two-edge gradient over a stroked path's (u, v) chart: one stop row per
stroke edge, each read as a gradient along the path, blended across the
stroke's width. Only meaningful on stroked draws.
const gradient = new BilinearGradient(
["red", "lime"], // one edge
["blue", "yellow"], // the other edge
);
const paint = new Paint().addShader(gradient).setStroke(96);
// Or a four-corner gradient, one color per corner:
BilinearGradient.fromCorners({
topLeft: "red",
topRight: "lime",
bottomLeft: "blue",
bottomRight: "yellow",
});
| Option | What it does |
|---|---|
uWrap, vWrap | How a chart coordinate pushed past [0, 1] reads (default "clamp"): "repeat" tiles seamlessly, "mirror" folds back and forth. |
uRepeat, vRepeat | How many times the rows tile along and across the stroke (default 1); values past 1 need a repeat or mirror wrap to show. Animatable. |
shift, period | Along-path slide, same contract as GradientAlongPath. |
positions | Stop positions along u, shared by both rows. |
The rows are structured, so replace them with setRows(top, bottom) (both
rows must keep the same number of stops).
Gradients under a bloom layer
A BloomFilter({ glow }) layer leaves its content untouched and fills the
transparent surround with glow, so the background is drawn outside the layer.
For the halo to read as light, that backdrop still needs to stay close to
black. The dim(color, factor) helper scales a color's RGB toward black
(keeping its alpha) for exactly this:
import { RadialGradient, dim } from "redraw";
const background = new RadialGradient([
dim("#2C2C2C", 0.12),
dim("#080808", 0.12),
]);
Beyond the built-ins
Anything these bindings cannot express (a gradient driven by the SDF, noise,
per-contour math, ...) is a few lines with the
createColor() helper: ctx.t, ctx.sdf,
ctx.d, and tctx.pos give you the same coordinates the built-ins read.