Brave perturbs getImageData by one, on a scattering of channels, by default, so that a canvas cannot be used as a fingerprint. That is invisible in a photograph and it is catastrophic in front of a threshold, which is the one thing this renderer does with every frame it composes. The scene is drawn in greyscale onto an offscreen canvas and read back whole, because error diffusion pays its error forward across the entire buffer. shade() then fast-pathed the two extremes by testing them for equality: alpha exactly 0 was ground nobody drew over, tone exactly 0 was ink, tone exactly 255 was paper. Under the perturbation a transparent backdrop pixel whose alpha reads 1 stops being ground and becomes ink; paper that reads 254 falls out of the fast path, lands in a tone bucket of its own and gets dithered. It printed as single white specks scattered over the whole picture, and because the read happens again on every repaint, they re-rolled whenever the atlas scrolled or a block moved. So nothing is compared for equality any more. The scene only ever composes 0, 64, 128, 192 and 255, and a band of eight is nowhere near any of them. Diffusion snaps to the extremes on the way in as well as banding on the way out, because it is the path that cares most: a flat field of true 255 pays no error forward at all, and one pixel of 254 in the middle of it starts a trail that then has to decay back out again. Sprites come through the same function, so they are covered by the same bands. Checked by shimming getImageData to nudge a twentieth of a percent of pixels by one and counting the colours on the board canvas: six when clean, twenty-one with the shim and the old code, six with the shim and this one.
- commit
- 8c12cb0f85cde116c0907f0b95a068d9855083ab
- parent
- d4072597f09131396c507224285e8ec10ab0a444
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 32 | +++++++++++++++++++++++++----- |
@@ -193,6 +193,21 @@ let diffusionBuffer = new Float32Array(0);
193193 /// this window out of it — the same thing a press does, and at no cost, because194194 /// the field never has to exist. Diffusion takes no offset and needs none: its195195 /// texture is aperiodic, so there is no phase for it to be out of.196+///197+/// The scene arrives here through `getImageData`, and a browser is allowed to198+/// lie about what it reads back: Brave perturbs a scattering of channel values199+/// by one, by default, so that a canvas cannot be used as a fingerprint. That is200+/// invisible in a photograph and catastrophic in front of a threshold — a201+/// transparent pixel whose alpha reads 1 stops being ground and becomes ink, and202+/// paper that reads 254 stops being paper and gets dithered. It printed as203+/// single white specks scattered over the whole picture, re-rolled on every204+/// repaint, which is what a scroll or a moving block made visible.205+///206+/// So nothing is compared for equality. The scene only ever composes 0, 64, 128,207+/// 192 and 255, and a band of eight is nowhere near any of them.208+const CLEAR = 8;209+const SOLID = 247;210+196211 function shade(src, dst, w, h, screen, ink32, paper32, phaseX = 0, phaseY = 0) {197212 if (screen.diffuse) return diffuse(src, dst, w, h, ink32, paper32);198213 @@ -208,16 +223,16 @@ function shade(src, dst, w, h, screen, ink32, paper32, phaseX = 0, phaseY = 0) {
208223 // they resolve without ever touching the screen.209224 for (const group of groups.values()) group.length = 0;210225 for (let i = 0; i < n; i++) {211- if (src[i] >>> 24 === 0) {226+ if (src[i] >>> 24 <= CLEAR) {212227 dst[i] = 0;213228 continue;214229 }215230 const value = src[i] & 0xff;216- if (value === 0) {231+ if (value <= CLEAR) {217232 dst[i] = ink32;218233 continue;219234 }220- if (value === 255) {235+ if (value >= SOLID) {221236 dst[i] = paper32;222237 continue;223238 }@@ -253,7 +268,14 @@ function diffuse(src, dst, w, h, ink32, paper32) {
253268 const n = w * h;254269 if (diffusionBuffer.length < n) diffusionBuffer = new Float32Array(n);255270 const buf = diffusionBuffer;256- for (let i = 0; i < n; i++) buf[i] = src[i] & 0xff;271+ // Snapped to the extremes on the way in, for the reason `shade` gives above.272+ // Diffusion is the path that cares most: a flat field of true 255 pays no273+ // error forward at all, and one pixel of 254 in the middle of it starts a274+ // trail of error that has to decay back out again.275+ for (let i = 0; i < n; i++) {276+ const value = src[i] & 0xff;277+ buf[i] = value >= SOLID ? 255 : value <= CLEAR ? 0 : value;278+ }257279 258280 for (let y = 0; y < h; y++) {259281 const row = y * w;@@ -261,7 +283,7 @@ function diffuse(src, dst, w, h, ink32, paper32) {
261283 const hasNext2 = y + 2 < h;262284 for (let x = 0; x < w; x++) {263285 const i = row + x;264- if (src[i] >>> 24 === 0) {286+ if (src[i] >>> 24 <= CLEAR) {265287 dst[i] = 0;266288 continue;267289 }