Skip to main content
Technical preview

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: positions places 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],
});
OptionWhat it does
fromWhere the first stop sits (default [0, 0]). Animatable.
toWhere the last stop sits (default [0, 0]; set it, or the gradient degenerates to the first color). Animatable.
positionsRelative position of each color on the axis (default: evenly spaced).
Two LinearGradients in sticker-local space: a paper sheen and a logo sweepOpen in editor →

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);
OptionWhat it does
centerCenter of the gradient (default [0, 0]). Animatable.
radiusDistance from the center where the last stop sits (default 1). Animatable.
positionsRelative position of each color between center and radius (default: evenly spaced).
A dark RadialGradient background, and one LinearGradient per glyphOpen in editor →

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).

OptionWhat it does
shiftAlong-path offset in palette cycles (default 0), a dimensionless phase: 0.25 rotates the palette a quarter turn. Animatable.
periodMilliseconds for one full palette cycle; unlocks the time accessor (gradient.time = frame.time).
cyclicTile the palette seamlessly (default true).
positionsRelative 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.
anchorWhat 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.
A GradientAlongPath riding a stroked path's arc lengthOpen in editor →

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",
});
OptionWhat it does
uWrap, vWrapHow a chart coordinate pushed past [0, 1] reads (default "clamp"): "repeat" tiles seamlessly, "mirror" folds back and forth.
uRepeat, vRepeatHow 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, periodAlong-path slide, same contract as GradientAlongPath.
positionsStop 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),
]);
A pre-dimmed RadialGradient background under a bloom layerOpen in editor →

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.