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): voidConverts every pixel to grayscale using the standard luminance weights:
gray = 0.299 * R + 0.587 * G + 0.114 * BThe 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): voidAdjusts brightness by adding a constant offset to every RGB channel.
brightness is in the range [-100, 100]:
0— no change100— 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): voidAdjusts 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 boostWhy 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.
Recommended pipeline order
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. DitherFor 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 RGBThe 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)andapplyContrast(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.