API reference
Every function, option and type in dither-art, with the exact errors each one throws. Pictures on this
page are drawn in your browser by the same build the package ships.
Install
ESM only. Node 18 or later, or any modern browser, Bun, Deno or edge runtime.
npm install dither-art
Quick start
Draw a grid of pixels from a seed, then turn it into whatever your target needs. The same seed and size
always give the same pixels.
import { ditherPixels, toSVG } from "dither-art";
const pixels = ditherPixels(320, 180, "my-seed"); // Uint8Array, 1 = ink, 0 = paper
const svg = toSVG(pixels, 320, 180); // ink is currentColor, paper is transparent
document.querySelector("#cover").innerHTML = svg;
Ink defaults to currentColor, so the picture takes the colour of the text around it and
follows dark mode with no extra work.
#cover { color: var(--accent); }
#cover svg { width: 100%; height: auto; }
ditherPixels
The main entry point: seed in, pixels out.
ditherPixels(width: number, height: number, seed: string, options?: DitherOptions): Uint8Array
Draws a picture and dithers it to one bit per pixel. Sizes are floored to whole numbers and clamped to
at least 1, so ditherPixels(10.9, 5.2, s) gives a 10 × 5 picture.
width, height number
- Size in pixels. Throws
RangeError for NaN or Infinity.
seed string
- Any string. The same seed gives the same picture, on every runtime.
options.scene DitherScene | Scene
- A built-in name or your own function. Default: chosen from the seed.
options.method DitherMethod
- How grey becomes 1-bit. Default
"bayer".
options.density number
-
Ink multiplier. Default
1. Zero, negative or NaN gives a blank picture
rather than throwing.
Returns a Uint8Array of width * height values, row by row:
1 is ink, 0 is paper.
Throws RangeError for a non-finite size, and TypeError for an unknown
scene or method name — the message lists the valid ones.
toSVG
toSVG(pixels: ArrayLike<number>, width: number, height: number, options?: SvgOptions): string
Returns an SVG string: one path per row tracing each run of ink as a one-pixel stroke, in
relative steps, with shape-rendering="crispEdges". It scales to any size without blurring
and can go straight into HTML, an image route, or a data URL.
options.ink string
- Any CSS colour. Default
"currentColor".
options.paper string
- Background colour. Default: none, so the paper is transparent.
options.scale number
- Displayed pixels per picture pixel, written into the width and height attributes. Default
1.
options.title string
- An accessible name. Without one the SVG is marked
aria-hidden="true" as decoration.
Size. Dithered pictures are busy: expect roughly 40–100 KB for 320 × 180, which compresses to
3–6 KB with the gzip or Brotli your server or CDN already applies.
toRGBA
toRGBA(pixels: ArrayLike<number>, options?: RgbaOptions): Uint8ClampedArray
Turns the pixels into RGBA bytes, ready for a canvas.
options.ink Colour
-
"#rgb", "#rgba", "#rrggbb", "#rrggbbaa" or
[r, g, b, a?] with channels 0–255. Default "#000".
options.paper Colour
- Same formats. Default: transparent.
Returns a Uint8ClampedArray of 4 bytes per pixel.
const rgba = toRGBA(pixels, { ink: "#c41e3a", paper: "#fdf6e3" });
ctx.putImageData(new ImageData(rgba, width, height), 0, 0);
dither
dither(ink: ArrayLike<number>, width: number, height: number, method?: DitherMethod): Uint8Array
Dithers your own grid of ink amounts, so you can run a photo, a gradient or any greyscale field
through the same methods. Values outside 0–1 are clamped, and NaN counts as paper.
Throws RangeError when ink.length is not
width * height — the message states both numbers.
Scenes
Six built-ins. Leave scene out and the seed picks one; sceneForSeed(seed)
tells you which it would choose. Every picture below is the seed "docs".
Dithering methods
One bit per pixel means every pixel is ink or paper. The greys are an illusion made of dots, and these
are four ways to arrange them. Same seed and scene throughout.
| Method | Look | Behaviour |
"bayer" |
Regular crosshatch, retro |
The default. The grain stays put when the size changes. |
"blue-noise" |
Fine, even grain with no pattern |
Same stability as Bayer. The 64 × 64 map is built on first use, in under 100 ms. |
"floyd-steinberg" |
Smooth tones, most detail |
Error diffusion: the grain reshuffles when the size changes. |
"atkinson" |
Crisp and contrasty, like the original Macintosh |
Error diffusion. Very light and very dark areas go solid. |
Your own scene
A scene receives seeded randomness and the picture's shape, and returns the ink amount at every point.
x runs from 0 to aspect (width / height) and y from 0 at the top
to 1 at the bottom, so a scene is resolution-independent.
import { ditherPixels, type Scene } from "dither-art";
const halo: Scene = (rand, { aspect, pixel }) => {
const cx = aspect * (0.3 + rand() * 0.4);
const r = 0.25 + rand() * 0.1;
return (x, y) => {
const d = Math.hypot(x - cx, y - 0.5);
if (Math.abs(d - r) < Math.max(0.01, pixel)) return 1; // a ring at least a pixel wide
return d < r ? 0.15 : Math.min(1, (d - r) * 1.5);
};
};
const pixels = ditherPixels(320, 180, "my-seed", { scene: halo });
rand () => number
- Seeded generator in
[0, 1). Call it the same number of times for the same picture.
context.aspect number
- Width / height. The field's
x runs from 0 to this.
context.pixel number
-
The size of one output pixel in field units (
1 / height). Use it to keep lines and dots
at least a pixel wide so small sizes stay readable.
Other exports
sceneForSeed(seed)
- The scene a seed gets when none is given.
ridges, orbs, contours, sea, dunes, planet
- The built-in scenes as
Scene functions, to wrap or combine.
hashSeed(string)
- FNV-1a 32-bit hash.
hashSeed("abc") is 440920331.
seededRandom(seed)
- A mulberry32 generator of numbers in
[0, 1), from a 32-bit integer seed.
blueNoiseMap()
- The 64 × 64 blue-noise threshold map: a
Float32Array of 4096 values in (0, 1).
BAYER
- The 8 × 8 Bayer thresholds: 64 numbers in (0, 1), row by row.
DITHER_SCENES, DITHER_METHODS
- The scene and method names, as readonly arrays.
Types
TypeScript 5.7 or later. All types are exported from the package root.
type DitherScene = "ridges" | "orbs" | "contours" | "sea" | "dunes" | "planet";
type DitherMethod = "bayer" | "blue-noise" | "floyd-steinberg" | "atkinson";
type Field = (x: number, y: number) => number;
type Scene = (rand: () => number, context: SceneContext) => Field;
interface SceneContext { aspect: number; pixel: number }
interface DitherOptions { scene?: DitherScene | Scene; density?: number; method?: DitherMethod }
interface SvgOptions { ink?: string; paper?: string; scale?: number; title?: string }
interface RgbaOptions { ink?: Colour; paper?: Colour }
type Colour = string | readonly [number, number, number, number?];
Recipes
Canvas
const [w, h] = [320, 180];
canvas.width = w;
canvas.height = h;
canvas.style.imageRendering = "pixelated";
const pixels = ditherPixels(w, h, "my-seed", { method: "atkinson" });
const rgba = toRGBA(pixels, { ink: "#c41e3a", paper: "#fdf6e3" });
canvas.getContext("2d").putImageData(new ImageData(rgba, w, h), 0, 0);
Server-rendered image route
const pixels = ditherPixels(600, 315, `post-${id}`);
return new Response(toSVG(pixels, 600, 315, { ink: "#111", paper: "#fff", title: "Cover art" }), {
headers: {
"content-type": "image/svg+xml",
"cache-control": "public, max-age=31536000, immutable",
},
});
The picture is a pure function of the seed, so it is safe to cache forever.
Dither your own image
const { data, width, height } = ctx.getImageData(0, 0, canvas.width, canvas.height);
const ink = new Float32Array(width * height);
for (let i = 0; i < ink.length; i++) {
const [r, g, b] = [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]];
ink[i] = 1 - (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; // brightness to ink
}
const pixels = dither(ink, width, height, "floyd-steinberg");
ctx.putImageData(new ImageData(toRGBA(pixels), width, height), 0, 0);
Stability
Pictures are part of the API. Within a major version, a seed, size, scene and method always draw exactly
the same pixels. The test suite pins a hash of every scene and method, and CI checks them on Node 18–22,
Bun and Deno. Any change to how pictures look is a major release.
Leaving scene out lets the seed choose from all scenes, so adding a scene in a later major
version can change which scene a seed gets. Pass scene explicitly if a picture must never
change across major versions.