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 existapplyBrightness(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