Every Shade Is a Lie Made of Dots
The pyrography pen has two settings: burning and not burning. There is no “burn a little” option. No half-char. You’re either leaving carbon behind or you aren’t, and yet somehow skilled wood-burners produce work with soft gradients, subtle shading, shadows that fade into nothing.
The trick is stippling—thousands of tiny burns, varying in density. Pack them close and the eye sees dark. Space them apart and it reads as light. The material is binary; the perception is continuous.
Robert Floyd and Louis Steinberg published exactly this insight in 1976, though they were thinking about laser printers rather than heated nibs. Their algorithm takes a grayscale image and converts it to pure black and white—one bit per pixel—while preserving the appearance of continuous tone. The secret is error diffusion. When you round a grey pixel to black or white, you’ve introduced an error. That error doesn’t vanish; it gets distributed to neighbouring pixels, nudging them toward correction. Mistakes cascade forward, and the cascade averages out to truth.
// Floyd-Steinberg dithering: spread the error to neighbours
func diffuse(img [][]float64, x, y int, err float64) {
w, h := len(img[0]), len(img)
if x+1 < w { img[y][x+1] += err * 7 / 16 }
if y+1 < h {
if x > 0 { img[y+1][x-1] += err * 3 / 16 }
img[y+1][x] += err * 5 / 16
if x+1 < w { img[y+1][x+1] += err * 1 / 16 }
}
}
Those magic fractions—7/16, 3/16, 5/16, 1/16—sum to exactly one. No error is lost, just redistributed. The pattern favours rightward and downward neighbours because scanlines move left-to-right, top-to-bottom. By the time you’ve crossed the image, every rounding error has been absorbed into the collective texture.
-- Threshold and spread the difference
function dither_pixel(grid, x, y)
local old = grid[y][x]
local new = old < 0.5 and 0 or 1
grid[y][x] = new
local err = old - new
if grid[y][x+1] then grid[y][x+1] = grid[y][x+1] + err * 0.4375 end
if grid[y+1] then
if grid[y+1][x-1] then grid[y+1][x-1] = grid[y+1][x-1] + err * 0.1875 end
if grid[y+1][x] then grid[y+1][x] = grid[y+1][x] + err * 0.3125 end
if grid[y+1][x+1] then grid[y+1][x+1] = grid[y+1][x+1] + err * 0.0625 end
end
end
What I like about Floyd-Steinberg is how honest it is about its own limitations. It doesn’t pretend to represent grey; it admits it can only do black and white, then works backward from that constraint. The shading you see is an optical illusion, a consensual hallucination between algorithm and retina.
Same thing happens on the basswood. The pen tip can only char or not-char, and everything else is negotiation—spacing, overlap, the hand’s micro-tremors averaging out to something that looks intentional. The grain cooperates or it doesn’t. Earlywood burns lighter; latewood burns darker. You diffuse your errors by adjusting the next stroke.
Floyd and Steinberg were writing about printers, but they were really writing about every medium that has to fake continuity from discrete marks. Pyrography. Pointillism. Newsprint halftones. The math is older than the paper it was printed on.