Send a photograph to a thermal receipt printer and you meet the problem in its purest form. The ESC/POS raster image command takes one bit per dot: a bit set to 1 is printed, a bit set to 0 is not, and the row width is counted in whole bytes. There is no grey in that data stream. Something upstream has to decide, for every single dot, black or nothing, and the quality of that decision is the whole picture.
The same shape of problem is everywhere once you look. Colour e-paper does not mix inks: E Ink describes its ACeP platform as a four particle system that reaches eight primary colours, so your photo has to be rewritten in a handful of fixed inks before the panel will show it. A laser engraver either fires at a position or it does not. A screen print has one ink per screen. A Game Boy screen was two bits per pixel, four shades, and nothing in between.
Dithering is how you cheat that limit: scatter the available colours so the eye averages them back into the tone you wanted. Every method does that, and every method leaves a signature you can recognise across a room. The Floyd-Steinberg vs Atkinson comparison is the classic one, because Atkinson throws away a quarter of its error on purpose and the result has the hard, sooty contrast that people read as “old Macintosh” without knowing why.
Eight methods, one picture
Comparisons of dithering algorithms are usually dishonest by accident: different pictures, different palettes, different scaling, screenshots from different decades. The lab below runs all eight methods on the picture you give it, with the same palette, the same options and the same output size, so the only variable left is the algorithm.
Dithering Lab
Knock a picture down to a handful of colours and watch the method decide everything. The same picture, the same palette, eight algorithms side by side, with a loupe to compare two of them on the same pixels. The picture is read and drawn in this browser tab, nothing is uploaded and nothing leaves the page.
Click anywhere on this picture to move the loupe below.
or press Enter to pick one. It is read on your own machine and never sent anywhere.
Every tile is the picture you loaded, dithered with the palette and the options below, so the comparison is honest. The tiles are small on purpose: a dither that only looks good at full size is not a dither, it is a texture.
Serpentine turns every second row around, which breaks up the diagonal streaks a one-way scan leaves behind. Linear light is the one almost every implementation gets wrong: an sRGB byte is not a quantity of light, so adding an error to it darkens or lightens the wrong amount, and large dark areas come out visibly too pale. Both switches only touch the error diffusion methods, because the ordered ones have no error to carry.
Both halves show the same rectangle of the same picture, so the only difference you see is the method.
It earns its keep wherever the output really has few inks: e-ink panels, thermal receipt printers, laser engraving, screen printing, badge and label printers, and indexed PNG or GIF where a gradient would otherwise band. It does not earn its keep on a photograph headed for a normal screen, where twenty-four bit colour needs no help, and it actively hurts in front of a lossy encoder: JPEG, WebP and AVIF spend their whole bit budget describing the noise you just added, so a dithered source is both bigger and worse. It also does not survive resampling. Dither last, at the size and for the device that will actually show it.
Your picture is read with FileReader and drawn on a canvas in your own tab. There is no upload, no fetch, no external script or font: the file never leaves the browser, which matters when the thing you are testing is a client logo or an unreleased product shot.
One loop, five different tables
Error diffusion is a single idea. Walk the pixels in order. For each one, pick the nearest colour the output can actually produce, then measure what you got wrong: the difference between the value you wanted and the value you emitted. Push that error into the neighbours you have not visited yet, in fixed proportions. A pixel that came out too dark makes the next few pixels a little brighter, and the average survives even though no individual pixel is right.
Everything that distinguishes one error diffusion method from another lives in the table of proportions. X marks the pixel being processed, the numbers are the shares, and the divisor underneath is what you divide by.
Floyd-Steinberg / 16 sum = 16
X 7
3 5 1
Jarvis, Judice, Ninke / 48 sum = 48
X 7 5
3 5 7 5 3
1 3 5 3 1
Stucki / 42 sum = 42
X 8 4
2 4 8 4 2
1 2 4 2 1
Sierra Lite / 4 sum = 4
X 2
1 1
Atkinson / 8 sum = 6
X 1 1
1 1 1
1
Read those tables and you can predict the output. Floyd-Steinberg spreads over four neighbours and one extra row, which is cheap and keeps detail tight, but the error travels mostly rightward and downward and can pull into faint diagonal worms in flat areas. Jarvis, Judice and Ninke spread the error over twelve cells and two further rows, so the grain looks softer and more photographic, at roughly three times the work per pixel. Stucki uses the same footprint with weights that are all powers of two over a divisor of 42, a speed decision from the era of integer arithmetic, and comes out slightly crisper than Jarvis. Sierra Lite is three cells over a divisor of 4, the smallest table that still behaves.
Atkinson keeps only three quarters of the error
Look at the Atkinson table again. Six cells, each worth one eighth, against a divisor of eight. The weights add up to six, not eight, so two eighths of every error are simply dropped on the floor. That is not a typo in the table and not a bug in the implementation. Bill Atkinson wrote it at Apple for the original Macintosh, and only three quarters of the error is diffused outward on purpose.
The consequence is easy to reason about. Error diffusion normally conserves tone: whatever a pixel loses, its neighbours owe back, so a flat area comes out at the right average brightness however far it is from a palette colour. Atkinson breaks that contract. Near white, the small debts that would have accumulated into an occasional black dot keep evaporating, so the region goes clean white; near black it goes solid black. The midtones, where the errors are large, still dither normally. Local detail with high contrast and crushed ends is the look people recognise from a 1984 screen, and it is why Atkinson is the right pick for a logo headed for one-bit output and the wrong pick for a photograph where the sky has to hold its tone.
Ordered dithering moves the threshold, not the error
Bayer and blue noise work on a completely different principle. Nothing is carried anywhere. Each pixel is compared against a threshold that depends only on its position, read out of a small tile that repeats across the image. Because no pixel depends on any other, ordered dithering parallelises perfectly, survives being computed on tiles, and produces no jitter between frames of an animation, which is why it is still used where error diffusion cannot be.
The Bayer matrix is not a magic table to be copied off a forum. It is built by recursion from a single zero, each step quadrupling the previous matrix and adding a fixed offset per quadrant.
M(1) = [ 0 ]
[ 4*M(n) 4*M(n) + 2 ]
M(2n) = [ 4*M(n) + 3 4*M(n) + 1 ]
M(2) = 0 2 M(4) = 0 8 2 10
3 1 12 4 14 6
3 11 1 9
15 7 13 5
threshold(x, y) = ( M(x mod n, y mod n) + 0.5 ) / n^2
The offsets 0, 2, 3, 1 are what makes it work: each quadrant is filled in an order maximally out of step with its neighbours, so at every scale the dots that turn on sit as far apart as the grid allows. The + 0.5 in the normalisation matters more than it looks. Without it the smallest threshold is exactly 0, one pixel in every tile flips a step too early, and flat tones pick up a visible bias. Tile sizes of 2, 4, 8 and 16 differ in scale rather than subtlety: a 2 by 2 tile is a coarse checker, a 16 by 16 tile a fine plaid that reads almost as texture.
The cost is the crosshatch. Bayer’s regularity is exactly what the eye is best at detecting, and in a gradient it shows as a grid laid over the picture. That is the “looks like a game” signature: consoles and early PCs used ordered dithering because a fixed matrix costs no memory and no second pass, and the pattern became the period’s accent.
Blue noise, computed the same way every time
Blue noise is the answer to Bayer’s grid. The idea is to keep the position dependent threshold, but fill the tile so that the pattern has no repeating structure at any scale while still keeping every dot as far from its neighbours as possible: energy concentrated at high spatial frequencies, almost none at low ones. Your visual system is poor at high frequencies, so the pattern disappears into texture instead of announcing itself as a grid.
The lab builds its 32 by 32 tile with void-and-cluster, the method Robert Ulichney published in 1993: find the tightest cluster of dots, remove one, find the largest void, put it there, repeat until the arrangement is stable, then rank every position by that process. The starting pattern is an R2 low discrepancy sequence rather than a random seed, so no random number enters the construction. The matrix is byte identical on every machine and after every reload, which is what you want when a client asks why yesterday’s export looks different.
Serpentine scanning
Error diffusion has a direction, and direction leaves tracks. Scanning every row left to right means every error is pushed the same way, and in areas of slowly changing tone the residue lines up into diagonal streaks that all lean the same direction. Serpentine scanning mirrors the horizontal offsets on every second row, so odd rows push their error left and even rows push it right. The bias cancels row by row instead of accumulating down the frame.
It costs one sign flip in the inner loop and it is the cheapest quality improvement in the subject. It does nothing at all for Bayer or blue noise, because there is no error being carried, which is why that switch goes visibly dead when you select an ordered method rather than silently pretending to work.
The error belongs in linear light
Here is the mistake almost every quick implementation makes, including a lot of published ones. The byte in your image buffer is not an amount of light. It is an sRGB encoded value, and the encoding is deliberately non linear so that the 256 available steps are spread evenly across what the eye can distinguish rather than evenly across physical intensity.
c = byte / 255
L = c / 12.92 when c <= 0.04045
L = ( (c + 0.055) / 1.055 ) ^ 2.4 when c > 0.04045
Run byte 128 through it. As a fraction that is 0.502, halfway up the scale, but the light it stands for is 0.216, just under 22 per cent. Meanwhile an even checkerboard of black and white pixels emits exactly 50 per cent of full light, because dots average in light, not in bytes. So a dithering algorithm that averages a checkerboard against byte 128 is comparing two quantities that are not the same kind of number, and the whole error term inherits the mistake.
The visible result is dark areas coming out too pale and the tone curve of the whole picture bending. The fix is to decode to linear light, do the quantisation and the error arithmetic there, and encode back on the way out. Switch it on and off on a photograph with a large shadow area and the difference is not subtle. This is the same family of problem as images that come out washed out after WordPress processes them: an assumption about what the numbers in a pixel mean, made once and then carried through everything downstream.
Ordered dithering is the exception, and the lab treats it as one. The Bayer and blue noise thresholds perturb in sRGB, not in linear light, because the threshold offset is a perceptual step rather than a quantity of light. Its amplitude comes from the mean nearest neighbour distance in the palette, which gives the right spread for two levels, for n grey steps and for the web safe grid alike. The tests assert that Bayer output is byte identical with both switches flipped.
The palette decides more than the method
People argue about algorithms and then hand them an unsuitable palette. The available colours set the ceiling; the method only decides how gracefully you approach it. The lab offers black and white, n grey steps from 2 to 64, the web safe 216 (six values per channel: 00, 33, 66, 99, CC, FF, a 1990s artefact of 256 colour displays), the 16 colour CGA and EGA set built from three RGB bits plus an intensity bit, four shade Game Boy, a seven ink e-paper set, and median cut.
Median cut is Paul Heckbert’s 1982 algorithm: put every pixel in one box, repeatedly split the box with the greatest range along its longest axis at the median, and average each final box to get a colour. It adapts to the picture, which is why a sunset in sixteen median cut colours beats the same sunset in the sixteen colours of CGA. It stops early when no box can be split, so a two colour picture gives back two colours however many you asked for, and the status line reports how many palette entries were actually used rather than how many were offered. If you are picking a fixed set by hand instead, the reasoning in how to choose a colour palette without learning colour theory applies here too.
Finding the nearest palette colour for every pixel is the inner loop, and a linear scan over the palette for each pixel is the naive version. The lab buckets the RGB cube into 16 by 16 by 16 cells and keeps, per cell, only the palette entries that can still win somewhere inside it. That is exact rather than approximate, and the tests verify it against a full scan.
What the lab deliberately will not do
The preview and every thumbnail are computed at the exact pixel width of the box they are shown in, capped at 640, because a dithered image rescaled by the browser shows moire that is not in the picture and you would be comparing scaling artefacts instead of algorithms. There is no resize listener, so those sizes hold until you load a new picture. The full size export is capped at 1600 pixels wide. The 1-bit PNG re-runs the current method against a black and white palette rather than thresholding the coloured result, because thresholding a dithered colour image gives noise, not a bilevel version of it. Two instances on one page are not supported, the same limit the rest of the plugin has.
Two more limits are about your workflow rather than the tool. Dithering does not belong in front of a lossy encoder: a dithered image is maximum spatial frequency by construction, which is precisely what a JPEG or WebP quantiser discards first, so you get mush and a larger file at once. The tradeoffs in choosing between WebP, AVIF, JPEG and PNG and in what actually costs the bytes in an image both point at PNG or a raw bit buffer. And dithering does not survive resampling: scale a dithered image and the pattern beats against the new pixel grid. Dither last, at final size.
If the destination is a press rather than a panel, the printer’s own halftone screen is already doing something like this, and dithering on top of it fights the RIP. The differences between screen and press are covered in what actually changes when a design gets printed. Dither for devices that take your bits literally: receipt printers, e-paper, engravers, LED matrices.
Nothing here is exclusive. ImageMagick has had these kernels for decades, every graphics library ships Floyd-Steinberg, and there are other browser based dither tools. What the lab is for is the comparison: the same picture, the same palette, the same options, eight methods at once, with the numbers that produce each result written down rather than hidden behind a preset name.
Once the tables are in front of you the choice stops being a matter of taste. Line art and logos to one bit: Atkinson, for the contrast its missing quarter buys. Photographs where tone has to hold: Jarvis or Stucki, in linear light, serpentine on. Anything animated, tiled or computed in parallel: blue noise, because it has no memory and no seam. Bayer when you want the grid to show, which is a design decision and not an accident.
The reason to understand the mechanism rather than cycle through presets is that the failures become diagnosable. Diagonal worms in a flat sky mean directional bias, so turn serpentine on. Chalky shadows mean the error is being diffused in sRGB bytes. A gradient that bands in steps rather than dissolving means the palette is too small for the tonal range, not that the algorithm is wrong. Naming the cause is faster than trying all eight again.
WunderPaint exists for the ordinary image work inside WordPress, the cropping and resizing and library tidying that fills a normal week. This lab sits at the other end of the scale, for the day something has to come out of a device that only understands on and off. Load a picture, put the eight tiles side by side, and let the picture settle the argument.