- JavaScript 89.1%
- CSS 6.9%
- HTML 4%
| atkinson | ||
| bayer2 | ||
| bayer4 | ||
| bayer8 | ||
| blue-noise | ||
| burkes | ||
| floyd-steinberg | ||
| halftone | ||
| jarvis-judice-ninke | ||
| pattern | ||
| random | ||
| scripts | ||
| sierra | ||
| stucki | ||
| threshold | ||
| .gitignore | ||
| index.css | ||
| index.html | ||
| index.js | ||
| library.js | ||
| LICENSE.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| source-sample.svg | ||
Basic Dither Library
This folder contains a browser-friendly dither plug-in library for general image export.
It is designed to be hosted as static JavaScript files on any public web server, CDN, Git repo raw URL, or local asset pipeline. Nothing in this library requires a build step.
What It Provides
The entry point is library.js. When loaded in a page that already exposes window.TPP.registerDitherLibrary, it registers a catalog of available dithers without loading every dither implementation immediately.
Each dither lives in its own self-contained plugin.js file and is loaded only when needed.
Included dithers:
thresholdbayer2bayer4bayer8floyd-steinbergjarvis-judice-ninkestuckiburkessierraatkinsonhalftoneblue-noiserandompattern
To regenerate the gallery samples after changing any plug-in:
npm run dither
Demo Page
Open index.html in a browser served from this folder to try the library interactively. The page:
- uploads an image and displays it on a canvas
- loads
library.jsand the selected plug-in throughwindow.TPP - lets you switch between monochrome and a bundled CMYK overprint palette
- exposes a threshold slider for mono dithering
- renders the processed result to a second canvas
Dither Gallery
The gallery shows the original source image, three monochrome threshold settings, and the same image reduced to a CMYK overprint palette built from paper white plus cyan, magenta, yellow, black, and their layered combinations. That gives you a print-flavored comparison point alongside the mono output and also shows how threshold changes the balance of light and dark detail.
Threshold
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
The simplest possible dither: each pixel is pushed to black or white based on a single cutoff. It is fast and predictable, but it tends to produce abrupt contouring because no neighboring pixels influence each other.
Bayer 2x2
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This is ordered dithering with a tiny repeating matrix. It keeps edges crisp and is inexpensive to compute, but the small tile can create a visibly repetitive texture.
Bayer 4x4
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This uses a larger ordered threshold matrix than bayer2, which smooths gradients a bit more while still preserving a deliberate pixel-art feel. It is a good middle ground when you want a stable, structured pattern.
Bayer 8x8
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
The larger Bayer matrix reduces coarse banding and gives smoother tonal transitions than the smaller Bayer variants. The tradeoff is that the regular grid becomes part of the look.
Floyd-Steinberg
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This is classic error diffusion: each quantization error is pushed into neighboring pixels. It often gives natural-looking gradients and detail retention, but it can produce worm-like texture in flat regions.
Jarvis, Judice & Ninke
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This spreads error over a wider neighborhood than Floyd-Steinberg, which usually softens harsh artifacts and produces smoother transitions. It is a solid choice when quality matters more than speed.
Stucki
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
Stucki is another wide-kernel error diffusion method with a balanced distribution of error. It tends to preserve detail well while staying a bit cleaner than some harsher diffusion patterns.
Burkes
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
Burkes keeps the spirit of larger diffusion kernels while trimming the neighborhood to stay lighter-weight. It can be a nice compromise between performance and smooth tonal reproduction.
Sierra
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
Sierra diffuses error across multiple rows like other higher-quality diffusion methods, but with its own weighting pattern. It often produces pleasing gradients with less obvious structure than ordered dithers.
Atkinson
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
Atkinson diffusion intentionally discards some error instead of spreading all of it. That gives it a punchy, airy look that works well for retro graphics and high-contrast imagery.
Halftone
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This uses a clustered-dot threshold pattern inspired by print halftoning. It is useful when you want the image to feel printed or mechanical rather than purely digital.
Blue-noise
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This variant uses a generated blue-noise-style mask instead of a regular Bayer grid. The goal is to break up visible repetition and hide patterning in smoother, more organic-looking noise.
Random
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
Random dithering adds noise before thresholding instead of using a fixed matrix or diffusion kernel. It avoids regular tiling, but the grain can feel rough and less controlled than blue-noise or error diffusion.
Pattern
| Original | Mono 25% | Mono 50% | Mono 75% | CMYK overprint palette |
|---|---|---|---|---|
This is another ordered dither built around a repeating threshold pattern. It is useful when you want a stylized, clearly synthetic texture instead of photorealistic shading.
How To Host It
Host this folder so the files keep their relative paths intact. For example:
https://git.lewismoten.com/tiny-pockets-press/dither-lib/library.js
https://git.lewismoten.com/tiny-pockets-press/dither-lib/threshold/plugin.js
https://git.lewismoten.com/tiny-pockets-press/dither-lib/bayer4/plugin.js
The relative URLs inside library.js are resolved from the URL used to load the library.
How To Use It
Your application should provide:
window.TPP.registerDitherLibrary(library)window.TPP.registerDither(dither)
Then load the library:
<script src="https://git.lewismoten.com/tiny-pockets-press/dither-lib/library.js"></script>
When library.js runs, it calls window.TPP.registerDitherLibrary(...) with metadata such as:
idnamekindurl
Your app can then lazy-load the individual plugin.js files later.
Using Outside TPP
The plug-ins are not tied to Tiny Pockets Press image export. The TPP hooks are just a lightweight registration mechanism. Any application can load a plug-in, capture the registered object, and call its handlers directly.
Minimal browser example:
<script>
let loadedDither = null;
window.TPP = {
registerDither(dither) {
loadedDither = dither;
},
};
</script>
<script src="./floyd-steinberg/plugin.js"></script>
<script>
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
loadedDither.applyMono(imageData.data, canvas.width, canvas.height, {
threshold: 128,
});
context.putImageData(imageData, 0, 0);
</script>
For palette dithering, pass a palette like:
const palette = [
[0, 0, 0],
[255, 255, 255],
[214, 116, 62],
[53, 92, 125],
];
loadedDither.applyPalette(
imageData.data,
canvas.width,
canvas.height,
palette,
{},
);
Expected Plug-in Shape
Each plug-in registers itself like this:
window.TPP.registerDither({
id: "threshold",
name: "Threshold",
kind: "both",
applyMono: function (data, width, height, options) {
// ...
},
applyPalette: function (data, width, height, palette, options) {
// ...
},
});
API Reference
Library Metadata
id: Stable machine-readable identifier such asfloyd-steinberg.name: Human-readable label for UI display.url: Relative path to the plug-in file fromlibrary.js.kind: Declares which output modes the plug-in supports.
Current and recommended kind values:
mono: The plug-in implementsapplyMono(...)only.palette: The plug-in implementsapplyPalette(...)and/orapplyPaletteAsync(...)only.both: The plug-in supports monochrome and palette workflows.
All plug-ins in this repository currently use both.
Handler Parameters
data: A flat RGBA pixel buffer, usually aUint8ClampedArrayfromImageData.data. The handlers mutate this buffer in place.width: Image width in pixels.height: Image height in pixels.options: Extra algorithm-specific settings. In this library, the common option isthreshold, a number from0to255. For bundled palette dithers,128is neutral, lower values bias the result lighter, and higher values bias it darker before palette reduction.palette: An array of RGB swatches such as[[0, 0, 0], [255, 255, 255]]. Each swatch is[r, g, b], with channel values from0to255.
Supported handlers:
applyMono(data, width, height, options)applyPalette(data, width, height, palette, options)applyPaletteAsync(data, width, height, palette, options)
Handler Behavior
applyMono(...): Converts the image to a black-and-white dither in place.applyPalette(...): Reduces the image to the supplied palette in place.applyPaletteAsync(...): Async version of palette reduction for implementations that need to yield, stream, or offload work. If present, it should produce the same kind of in-place result asapplyPalette(...).
Practical Notes
- The alpha channel is passed through in the same buffer. These plug-ins operate on RGB values and generally leave alpha untouched.
- The library assumes row-major RGBA ordering, which matches browser
ImageData. - If your app works with another pixel layout, convert into RGBA first, run the dither, then convert back.
optionsis intentionally open-ended so custom plug-ins can add their own knobs without changing the registration format.
Authoring Your Own Plug-in
The easiest way to add a new dither is to copy one of the existing
plugin.js files, rename the id and name, and then replace the algorithm
inside applyMono(...) and applyPalette(...).
Recommended guidelines:
- Prefer supporting
options.threshold. For mono dithers it acts as the cutoff point; for palette dithers in this library it is also used as a simple brightness bias around128, which gives host applications a shared control that works in both output modes. - Use
kind: "mono"if you only supportapplyMono(...),kind: "palette"if you only support palette reduction, orkind: "both"if you implement both paths. - Mutate the provided
databuffer in place instead of allocating a brand new output buffer unless your algorithm genuinely requires working storage. - Keep palette swatches in
[r, g, b]form with0to255channel values. - For long-running palette or diffusion passes, lean toward
applyPaletteAsync(...)so the host application can keep the UI responsive while work is in progress. - If your async implementation yields control, it should still produce the same final image shape as the synchronous version and should document any progress or chunking behavior expected by the host app.
- If the algorithm is standard or historical, add a short comment explaining whether the important part is a known kernel, threshold matrix, or sampling pattern. That makes future maintenance easier and helps with provenance.
Minimal template:
window.TPP.registerDither({
id: "my-dither",
name: "My Dither",
kind: "both",
applyMono: function (data, width, height, options) {
const threshold = Math.max(
0,
Math.min(255, Number((options || {}).threshold) || 128),
);
// mutate data in place
},
applyPalette: function (data, width, height, palette, options) {
if (!Array.isArray(palette) || !palette.length) return;
// mutate data in place
},
});
Notes
- The library is intentionally quiet for end users.
- If registration fails, errors are written to
console.error. - Because each dither is self-contained, you can also load a single plug-in directly without loading
library.jsfirst, as long aswindow.TPP.registerDitheralready exists.
Provenance And Licensing
- The named dithers in this package are standard image-processing methods implemented locally in this repository.
- The underlying algorithms, kernels, and threshold matrices are historical method definitions, not proprietary to this package.
- Copyright in this repo applies to this specific code and documentation, not to the underlying dithering methods themselves.
- If any future plug-in is adapted from another project, keep that source's attribution and license notice with the file.