ditherkit
@ditherkit/core

Image adjustments

grayscale, applyBrightness, applyContrast — pre-processing helpers.

Three in-place helpers for conditioning a pixel buffer before you dither it. All of them take a Uint8ClampedArray of RGBA pixels and mutate it in place — no allocation, O(pixels) time.

grayscale

grayscale(pixels: Uint8ClampedArray): void

Converts every pixel to grayscale using the standard luminance weights:

gray = 0.299 * R + 0.587 * G + 0.114 * B

The three channels are all set to the same grey value; alpha is left untouched.

import { grayscale } from '@ditherkit/core'

grayscale(pixels)

When to use it

Call this before dithering when targeting a 2-colour palette (black-and-white style). The output is luminance-only anyway, so collapsing the channels first produces the cleanest error signal.

Skip it for any palette with more than 2 colours — Game Boy, CGA, NES, PICO-8, anything custom with hues. The dithering algorithms do nearest-colour matching in full RGB space, and flattening to luminance first would collapse every palette to brightness-only matching. See the note in Algorithms for the rule and the gory detail.

If your source image is already grayscale (e.g. you're reading from a single-channel Sharp buffer) and you're targeting a 2-colour palette, you still need to call this — the pixel buffer is RGBA even if the visual content is grey.

The <DitheredImage /> and <DitheredImageSSR /> wrappers auto-decide based on palette length, so this only matters when you're calling the core algorithms directly.

applyBrightness

applyBrightness(pixels: Uint8ClampedArray, brightness: number): void

Adjusts brightness by adding a constant offset to every RGB channel. brightness is in the range [-100, 100]:

  • 0 — no change
  • 100 — adds 255 to every channel (fully white)
  • -100 — subtracts 255 from every channel (fully black)
  • 10 — small lift, useful for shadow detail
  • -20 — small drop, useful before threshold dithering to push more pixels below the threshold line
import { applyBrightness } from '@ditherkit/core'

applyBrightness(pixels, 15)  // lighten by 15%

Values are clamped to [0, 255] on each channel, so pushing past the limits saturates cleanly without wrapping.

applyContrast

applyContrast(pixels: Uint8ClampedArray, contrast: number): void

Adjusts contrast by scaling every channel around the mid-grey point (128). contrast is in the range [-100, 100]:

  • 0 — no change (factor 1.0)
  • 100 — doubles contrast (factor 2.0, midtones pushed to extremes)
  • -100 — zero contrast (factor 0.0, every pixel becomes mid-grey)
import { applyContrast } from '@ditherkit/core'

applyContrast(pixels, 25)  // a modest contrast boost

Why contrast before dither?

Dithering with low-contrast input produces muddy output — the algorithm has very little tonal range to work with, so it spends a lot of error diffusion on pixels that were already close to mid-grey. A small contrast boost (15–30) before dithering often dramatically improves the perceived quality of the result, especially for photographs.

For a 2-colour palette (black-and-white):

applyBrightness(pixels, brightness)             // 1. Shift tonal range
applyContrast(pixels, contrast)                 // 2. Expand tonal range
grayscale(pixels)                               // 3. Collapse to luminance
floydSteinbergDither(pixels, w, h, palettes.bw) // 4. Dither

For a colour palette (Game Boy, CGA, PICO-8, NES, custom hues), skip the grayscale step:

applyBrightness(pixels, brightness)                  // 1. Shift tonal range
applyContrast(pixels, contrast)                      // 2. Expand tonal range
floydSteinbergDither(pixels, w, h, palettes.gameboy) // 3. Dither in RGB

The dithering algorithms do nearest-colour matching in RGB space, so the source colours need to survive into the algorithm.

Order matters:

  • Brightness before contrast — contrast scales around 128, so if you apply contrast first and then brightness, your contrast adjustment is centred on the wrong tonal midpoint.
  • Grayscale after both adjustments — doing it earlier wastes work (you'd adjust three identical channels) and also loses the tonal information that contrast wants to work with on color sources.
  • Dither last — it's the destructive step that snaps everything to the palette, and you can't un-dither to apply further adjustments.

Notes

  • All three are in-place. Clone the buffer if you need the original afterwards.
  • Alpha is untouched by all three.
  • No-op fast paths exist — applyBrightness(pixels, 0) and applyContrast(pixels, 0) still iterate the whole buffer. Check the value yourself if you want to skip the call entirely when there's nothing to do.

On this page