What it measures
Canvas: we draw a scene and read back pixels; engine-bound.
Engine differences in anti-aliasing, sub-pixel rendering, font hinting, emoji rasterisation, gradient interpolation, and colour-space math cause each browser engine to produce a distinct byte sequence even on the same GPU with the same fonts installed. We apply per-call noise mitigation by reading back pixels across multiple runs and folding them together before hashing, so per-call random noise injection (used by some browsers and anti-detect tools) does not destabilise the hash.
The collector prefers OffscreenCanvas (off-main-thread, no DOM required) and falls back to a hidden DOM canvas when OffscreenCanvas is unavailable. Both paths read pixels synchronously via getImageData.
How it's collected
The collector draws a fixed deterministic scene onto a small fixed surface, reads back the RGBA pixel bytes across multiple runs via getImageData, and folds the runs together with a per-byte noise-mitigation rule before hashing the merged bytes with xxHash64. If the OffscreenCanvas path fails or every run returns null, the result is `absent`.
The exact drawing recipe (text string, coordinates, gradient colours, geometry parameters), the readback byte count, the run count, and the per-byte fold rule are deliberately withheld from the public docs. The hash output is the stable interface; the recipe evolves between releases.
// Public-facing value shape (canvas signal)
interface CanvasValue {
divergence: {
runs: number; // number of readback runs folded together
byteDifferences: number; // positions where not all runs agreed
byteCount: number; // total bytes per run
magnitudeHistogram: number[]; // count of divergent bytes by noise-magnitude bucket
edgeRegionCount: number; // divergent bytes inside the edge-region predicate
};
bytesPreview: string; // short hex preview of the merged bytes for debug display
}
// Reading divergence diagnostics
const div = result.signals.canvas.value?.divergence;
if (div && div.byteDifferences > 0) {
// divergence diagnostics feed the consistency layer.
}Treat the field shape as the stable interface. The drawing recipe, canvas geometry, histogram bin layout, and the edge-region heuristics that consume these fields are not part of the public contract.
Confidence rules
| Confidence | Trigger |
|---|---|
| normal | Either OffscreenCanvas or DOM canvas produced image data and the per-byte fold ran |
| absent | Both paths returned null on every run, or the top-level catch fired |
| stabilized | stabilize: ['private'] was set and canvas was dropped on Firefox / Safari 17+ / Brave (hash is the canonical absent placeholder, value is null). Distinct from absent so consumers can tell the signal was excluded on purpose rather than failing |
Why engine-bound
Canvas 2D rendering is implemented in each browser engine's graphics backend: Skia in Chrome/Edge, Cairo/Skia in Firefox, CoreGraphics in Safari. Differences in sub-pixel anti-aliasing, font rasterisation (especially emoji), gradient interpolation, and colour-space handling produce pixel-distinct readbacks even on the same GPU with the same font files installed.
Because the variation is driven by the rendering engine rather than the underlying hardware, the hash is stable within a browser but differs across engines on the same machine. The signal contributes to fingerprint but not to hardwareFingerprint.
Divergence fields
`value.divergence` carries two diagnostic fields. `magnitudeHistogram` is a fixed-length array where each index holds the count of divergent byte positions whose max pairwise delta across the readback runs falls into that bin. `edgeRegionCount` is the count of divergent byte positions that fall inside an edge-region predicate aligned with the drawn scene. The exact bin layout and edge-region predicate are part of the internal recipe and not part of the public contract.
Both fields are zero-filled when no divergence is present or when the failure path is taken. `byteDifferences`, `magnitudeHistogram`, and `edgeRegionCount` together feed a canvas noise-uniformity consistency flag without requiring an additional canvas render.
Things worth knowing
- Both OffscreenCanvas and DOM canvas paths use getImageData synchronously. No async await on the readback path; the prior async-conversion timeout is gone.
- Typical collection time is 50–150 ms.
- There is no degraded state. The signal is `normal`, `absent`, or `stabilized`.
- All drawing within a single run is wrapped in try/catch so partial drawing is still hashed if one drawing step fails. A whole run returning null is fine as long as at least one run produced data; the fold falls back to whatever runs are available.
- `value.divergence.byteDifferences` is an integer count of byte positions where not all readback runs agreed. Zero means the canvas is deterministic across calls; positive means per-call noise is being injected.
- `value.divergence.magnitudeHistogram` and `value.divergence.edgeRegionCount` provide finer-grained divergence diagnostics consumed by the consistency layer. The exact thresholds and the edge-region predicate are not part of the public contract.
- In v1.3.0 the canvas hash input changed from PNG bytes to the per-byte-merged RGBA bytes. The hash differs from prior versions even on stable browsers. Treat as a one-time fingerprint migration.
Last reviewed 2026-06-04

