pub fn gradient_dither_offset(x: f32, y: f32) -> f32Expand description
The ordered-dither offset a gradient gets at device pixel (x, y), in
output levels — the same value Skia adds, so a Cranpose gradient lands on
the same bytes as the Jetpack Compose gradient it is standing in for.
Skia dithers every gradient it draws to an 8-bit target. The pattern is not
noise: it is a 4x4 Bayer matrix built by striping the low two bits of the
device coordinate — (X:a1a2, Y:b1b2) becomes b1 a1 b2 a2 — and mapped
onto [-15/32, +15/32], half a level either way. Undithered, a slow ramp
quantises into visible bands; dithered, the band edges break into the
checkerboard every Android gradient has.
x→ 0 1 2 3
y 0 0 4 1 5
1 8 12 9 13
2 2 6 3 7
3 10 14 11 15Recovered from device captures rather than from memory: binning
compose − cranpose over a radial gradient by (x % 4, y % 4) reproduces
this matrix, and the per-cell means track m / 16 − 15 / 32 to within the
estimator’s own bias.
The pattern is anchored one pixel on from the coordinate handed in, and
that is measured too. Evaluated at the fragment’s own coordinate the
dither came out as the mirror of Skia’s — two dithers disagreeing is worse
than one, and a captured frame went from 39.5% identical pixels against
the Compose build to 23.6%. A probe shader that painted floor(position)
straight into the frame said why: the fragment that lands on captured
column N reports column N-1. The pattern is a phase as much as a matrix,
so the phase is part of what has to match.