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 a 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.
“A gradient”, not “every gradient ever”: this is the behaviour of the
platforms Cranpose targets, and it has a floor. Captured on one emulator
host from one APK, a Compose radialGradient over black comes back as
round(255*v + m/16 − 15/32) of the analytic ramp on an android-34 Wear
image at both 454x454 and 384x384, and as bare round(255*v) — no spatial
structure, residual variance exactly the 1/12 of a plain rounding — on an
android-30 one. Only the system image moves between those. So an
android-30 capture is not a reference for this function and never was; a
build compared against one reads as half a level wrong over most of every
gradient it draws, which is exactly what it should read as.
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.
Confirmed since without cranpose in the loop at all, which is the reading that matters — the difference of two builds cannot say which one carries the pattern. Against the gradient’s own analytic ramp the Compose frame’s residual, binned the same way, IS this table: rms 0.005 of a level per cell over 109k pixels, against 0.285 for the flat table an undithered build gives.
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.