npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opendisplay/epaper-dithering

v5.0.6

Published

Dithering algorithms for e-paper/e-ink displays (JavaScript/TypeScript)

Readme

@opendisplay/epaper-dithering

npm

High-quality dithering algorithms for e-paper/e-ink displays, powered by a Rust/WASM core. Works in both browser and Node.js environments.

Features

  • Rust/WASM Core: Compiled Rust logic bundled inline — no async init, no external files, works everywhere
  • 9 Dithering Algorithms: From fast ordered dithering to high-quality error diffusion
  • 8 Color Schemes: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE_4/8/16
  • Measured Palettes: Use real display-calibrated colors for accurate dithering (SPECTRA_7_3_6COLOR_V2, BWRY_3_97, and more)
  • OKLab Color Matching: Weighted Cartesian OKLab — preserves hue without the achromatic-attractor bug that plagues LCH-weighted approaches
  • Pre-dither Adjustments: Per-image exposure, saturation, shadows, highlights, dynamic-range compression, and gamut compression — all orthogonal knobs
  • Serpentine Scanning: Alternates row direction to eliminate directional artifacts
  • Universal: Works in browser (Canvas API) and Node.js (≥18)
  • Zero Dependencies: WASM binary bundled inline, no image library required

Installation

npm install @opendisplay/epaper-dithering
# or
bun add @opendisplay/epaper-dithering

Quick Start

Browser (Canvas API)

import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';

const img = new Image();
img.src = 'photo.jpg';
await img.decode();

const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);

const dithered = ditherImage(
  { width: imageData.width, height: imageData.height, data: imageData.data },
  ColorScheme.BWR,
  { mode: DitherMode.FLOYD_STEINBERG },
);

// Render result
const out = ctx.createImageData(dithered.width, dithered.height);
for (let i = 0; i < dithered.indices.length; i++) {
  const c = dithered.palette[dithered.indices[i]];
  out.data[i * 4] = c.r; out.data[i * 4 + 1] = c.g;
  out.data[i * 4 + 2] = c.b; out.data[i * 4 + 3] = 255;
}
ctx.putImageData(out, 0, 0);

Measured Palettes

Standard ColorScheme values use ideal sRGB colors (e.g. white = 255,255,255). Real e-paper displays reflect significantly less light. Use a measured ColorPalette for accurate dithering:

import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';

const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, { mode: DitherMode.BURKES });

// Opt in when you want automatic tone/gamut compression for photos
const compressed = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, {
  mode: DitherMode.BURKES,
  tone: 'auto',
  gamut: 'auto',
});

Available measured palettes: SPECTRA_7_3_6COLOR_V2, SPECTRA_7_3_6COLOR, BWRY_3_97, MONO_4_26, BWRY_4_2, SOLUM_BWR, HANSHOW_BWR, HANSHOW_BWY.

Node.js (with sharp)

import sharp from 'sharp';
import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';

const { data, info } = await sharp('photo.jpg')
  .ensureAlpha()
  .raw()
  .toBuffer({ resolveWithObject: true });

const dithered = ditherImage(
  { width: info.width, height: info.height, data: new Uint8ClampedArray(data) },
  ColorScheme.BWR,
  { mode: DitherMode.BURKES },
);

const rgbaBuffer = Buffer.alloc(dithered.width * dithered.height * 4);
for (let i = 0; i < dithered.indices.length; i++) {
  const c = dithered.palette[dithered.indices[i]];
  rgbaBuffer[i * 4] = c.r; rgbaBuffer[i * 4 + 1] = c.g;
  rgbaBuffer[i * 4 + 2] = c.b; rgbaBuffer[i * 4 + 3] = 255;
}

await sharp(rgbaBuffer, { raw: { width: dithered.width, height: dithered.height, channels: 4 } })
  .png()
  .toFile('dithered.png');

API Reference

ditherImage(image, colorScheme, options?)

ditherImage(image: ImageBuffer, palette: ColorScheme | ColorPalette, options?: DitherOptions): PaletteImageBuffer

| options field | Type | Default | Description | |---|---|---|---| | mode | DitherMode | BURKES | Dithering algorithm | | serpentine | boolean | true | Alternate row direction to reduce artifacts | | exposure | number | 1.0 | Linear-RGB exposure multiplier. 2.0 = +1 stop, 0.5 = −1 stop | | saturation | number | 1.0 | OKLab saturation multiplier. 0.0 = grayscale, >1 = boost. Hue-preserving | | shadows | number | 0.0 | Shadow lift strength (S-curve lower half). 0.0 = off, 1.0 = strong | | highlights | number | 0.0 | Highlight compression strength (S-curve upper half). 0.0 = off, 1.0 = strong | | tone | number \| 'auto' \| 'off' | 0.0 | Dynamic range compression. 0.0/'off' = disabled; 'auto' = histogram-based; numeric = fixed strength. Ignored for ColorScheme | | gamut | number \| 'auto' \| 'off' | 0.0 | Pre-dither gamut compression. 0.0/'off' = disabled; 'auto' = activate when image exceeds palette gamut; numeric = fixed. Ignored for ColorScheme |

Pre-processing pipeline: exposure → saturation → shadows/highlights → tone → gamut → dither. Each step is a no-op at its identity value.

DitherMode.NONE performs direct nearest-color mapping without error diffusion or ordered dithering. Built-in measured palettes carry their canonical firmware scheme, so pure display colors map to the corresponding firmware palette index even when measured RGB values are used for matching.

For built-in measured palettes, exact canonical display colors are also protected in ordered and error-diffusion modes when pre-processing is off: an image made entirely of display colors is returned as a direct palette-index map, and exact display-color pixels inside a mixed image keep their canonical index instead of being rematched to the measured RGB palette. Pre-processing runs before that exact-pixel check, so explicit tone: 'auto', gamut: 'auto', or other adjustments may intentionally alter those pixels first.

Returns PaletteImageBuffer.

Color Schemes

enum ColorScheme {
  MONO         = 0,  // Black & White (2 colors)
  BWR          = 1,  // Black, White, Red (3 colors)
  BWY          = 2,  // Black, White, Yellow (3 colors)
  BWRY         = 3,  // Black, White, Red, Yellow (4 colors)
  BWGBRY       = 4,  // Black, White, Green, Blue, Red, Yellow (6 colors)
  GRAYSCALE_4  = 5,  // 4-level grayscale
  GRAYSCALE_8  = 6,  // 8-level grayscale
  GRAYSCALE_16 = 7,  // 16-level grayscale
}

Dither Modes

| Mode | Quality | Speed | Notes | |---|---|---|---| | NONE | — | Fastest | Direct palette mapping | | ORDERED | Low | Very fast | 4×4 Bayer matrix | | SIERRA_LITE | Medium | Fast | 3-neighbor kernel | | FLOYD_STEINBERG | Good | Medium | Most popular | | BURKES | Good | Medium | Default | | ATKINSON | Good | Medium | Classic Mac aesthetic | | SIERRA | High | Medium | — | | STUCKI | Very high | Slow | — | | JARVIS_JUDICE_NINKE | Highest | Slowest | — |

Types

interface ImageBuffer {
  width: number;
  height: number;
  data: Uint8ClampedArray; // RGBA, row-major
}

interface PaletteImageBuffer {
  width: number;
  height: number;
  indices: Uint8Array; // palette index per pixel
  palette: RGB[];      // sRGB colors
}

interface ColorPalette {
  readonly colors: Record<string, RGB>;
  readonly accent: string;
  readonly scheme?: number;
}

Preview Tool

An interactive browser tool for comparing dithering modes and palettes:

Hosted (always latest release): https://opendisplay.github.io/epaper-dithering/

Local (against your working branch):

cd packages/javascript
bun run dev
# → http://localhost:3456/dev.html

Features: drag & drop or paste from clipboard, live re-render on every setting change, timing display, palette swatch preview.

Development

bun install

# When Rust source changes, rebuild the WASM (from repo root):
wasm-pack build packages/rust/wasm --target bundler --out-dir ../../javascript/src/wasm-core

bun run test        # vitest
bun run build       # tsup → dist/
bun run type-check

Related Projects