Gradients
Linear, radial, conic and along-the-path gradients in OKLab, with smooth easing. Half-float ramps and dithering keep dark gradients free of banding.
Updated pixi-silk 0.1.0
Source src/demos/gradients.demo.ts
import { Text } from 'pixi.js';
import { conic, linear, radial, SilkGraphics } from 'pixi-silk';
import { defineDemo } from './runtime';
export default defineDemo({
size: [480, 240],
controls: {
space: { type: 'select', label: 'space', options: ['oklab', 'linear', 'srgb'], value: 'oklab' },
easing: { type: 'select', label: 'easing', options: ['linear', 'smooth'], value: 'linear' },
},
setup({ stage, params, tick }) {
const g = new SilkGraphics();
const label = new Text({ text: '', style: { fill: 0x8e8e93, fontSize: 12, fontFamily: 'system-ui' } });
label.position.set(20, 214);
stage.addChild(g, label);
tick(() => {
const options = { space: params.space as 'oklab', easing: params.easing as 'linear' };
g.clear();
// gradient objects are cheap: ramps with the same stops share one atlas row
g.roundRect(20, 20, 440, 50, 14).fill(linear([0x0a84ff, 0xffd60a], options));
g.roundRect(20, 84, 440, 50, 14).fill(linear([0xff375f, 0x30d158, 0x5e5ce6], options));
g.circle(80, 180, 26).fill(radial([0xffffff, 0xff9f0a, [1, 0xff375f, 0]], options));
g.circle(160, 180, 26).fill(conic([0xff375f, 0xffd60a, 0x30d158, 0x64d2ff, 0xbf5af2, 0xff375f], options));
g.arcSweep(240, 180, 22, 0, Math.PI * 1.6).stroke({
width: 10,
cap: 'round',
gradient: conic([0x30d158, 0xff453a], options),
});
label.text = `interpolated in ${params.space}, ${params.easing} easing`;
});
},
});Kinds#
import { linear, vertical, horizontal, radial, conic, along } from 'pixi-silk';
linear(stops, { from: [0, 0], to: [1, 1] }); // diagonal across the shape's box
vertical(stops); // top to bottom of the shape
horizontal(stops); // left to right
radial(stops, { center: [0.3, 0.3], radius: 0.8, innerRadius: 0.2 });
conic(stops, { startAngle, sweep }); // around a centre (on an arc, it spans the arc)
along(stops); // along a stroke, first point to last
Use a gradient anywhere a colour goes: fill(gradient), stroke({ gradient }), or
fill({ gradient, color }), where color tints the ramp.
Stops#
linear([0xff375f, 0x0a84ff]); // spread evenly
linear([[0, 0xff375f], [0.3, 0xffd60a], [1, 0x0a84ff]]);
linear([[0, 0xffffff, 1], [1, 0xffffff, 0]]); // [offset, color, alpha]
linear([{ offset: 0.5, color: '#30d158', alpha: 0.5 }]);
linear([[0, RED], [0.5, RED], [0.5, BLUE], [1, BLUE]]); // hard stop
Silk clamps offsets to 0..1 and makes them non-decreasing, like CSS.
Colour space#
By default, stops interpolate in OKLab, a perceptual space. A red-to-green ramp passes through
a bright yellow, not a muddy brown. Lightness also changes evenly. Pass space: 'linear' for
physically linear light (glows, light falloff) or space: 'srgb' for CSS-like results.
Interpolation is premultiplied, so fading a colour to transparent never darkens it.
Easing#
easing: 'smooth' runs a monotone cubic spline through the stops, with flat ends. It removes the
Mach bands at every stop of a linear ramp. It never overshoots. A function (t) => t remaps the
ramp parameter for custom falloffs.
Units and extend#
- With
units: 'shape'(default), coordinates are fractions of the shape’s bounding box. One gradient object then fits every shape it paints. - With
units: 'local', coordinates are in the graphics’ local space. One continuous ramp then spans many shapes (a chart’s bars, a row of pills). extend: 'pad' | 'repeat' | 'reflect'controls what happens outside 0..1.
No banding#
Dark, wide gradients stay smooth on 8-bit displays. Silk bakes each ramp once into a shared 256-wide half-float atlas. The shader samples it per pixel with ±½ LSB dither (interleaved gradient noise). Try both toggles below. The reveal toggle multiplies the contrast by 8 to show the steps.
Source src/demos/dither.demo.ts
import { ColorMatrixFilter, Container } from 'pixi.js';
import { SilkGraphics, vertical } from 'pixi-silk';
import { defineDemo } from './runtime';
// A dark, wide gradient has only a few 8-bit steps. Half-float ramps + ±½ LSB dither hide them.
const NIGHT = vertical([0x0b1026, 0x1d1a3a, 0x10141f]);
export default defineDemo({
size: [480, 220],
background: 0x000000,
controls: {
dither: { type: 'toggle', label: 'dither', value: true },
reveal: { type: 'toggle', label: 'reveal x8', value: true },
},
setup({ stage, params, tick }) {
const view = new Container();
const g = new SilkGraphics();
// multiply the contrast by 8 around black so every banding step shows
const reveal = new ColorMatrixFilter();
reveal.brightness(8, false);
view.addChild(g);
stage.addChild(view);
g.rect(0, 0, 480, 220).fill(NIGHT);
g.circle(360, 70, 60).fill({ color: 0xffd9a0, alpha: 0.12, blur: 40 });
tick(() => {
g.dither = params.dither ? 1 : 0;
view.filters = params.reveal ? [reveal] : [];
});
},
});API reference: linear, vertical, horizontal, radial, conic, along, Gradient