| Component name | canvas |
|---|---|
| In the visitorId | yes |
| structuredId group | gpu (graphics) |
| Hashing | always a digest (first 128 bits of SHA-256) |
| Excluded under | Brave (brave_farbling); Firefox resistFingerprinting and Tor Browser (firefox_rfp) |
| Inner timeout | 1000 ms |
| Added in | signalsVersion 1 |
WhiskerJS draws a small, fixed picture in an invisible canvas: two lines of text with a cat emoji, three overlapping coloured circles and a ring. Every device draws text, emoji and blended colours very slightly differently, so the exact pixels of the picture differ between systems. The picture is never shown and never sent: WhiskerJS turns it into a short code (a digest) in the browser.
What it reads
The canvas component creates a detached <canvas> element, takes its 2D context with getContext('2d'), sets it to 260 by 80 pixels and draws one fixed scene:
- an orange filled rectangle;
- the text “Whiskers twitch, jumpy fox” followed by a cat face emoji, once in 14 px “Times New Roman” and once in 19 px Arial with a semi-transparent green fill;
- three circles drawn with the
multiplycomposite operation, so their overlaps blend; - a ring made of two arcs filled with the
evenoddrule.
The canvas is then exported with toDataURL(), which returns the image as a PNG data URL. That string is what gets hashed.
How the technique works
- Draw the scene once and export it.
- Draw exactly the same scene again on the same canvas and export it again.
- If the first export does not start with
data:image/or is suspiciously short, the browser gave back an empty or fake image: the component is reportedblockedwith ruleIdempty_image. - If the two exports differ, something added random noise between the two reads: the component is reported
noisywith ruleIdrender_mismatchand carries no value. - Otherwise the export is passed to the runner, which hashes it with SHA-256 (canvas is one of the “large” components that are always hashed) and keeps the first 128 bits.
The element is never attached to the page, so nothing appears on screen.
Why it helps
Turning text and shapes into pixels involves many layers that differ between systems: the font files installed and their versions, the font rasteriser and its anti-aliasing and hinting, the emoji font (each operating system ships its own emoji images), the graphics library that blends colours, and sometimes the GPU. Two devices with the same browser can still produce different pixels, while the same device usually produces the same pixels every time. That makes the digest useful for telling setups apart. It sits in the gpu group of the structuredId together with the WebGL and WebGPU components.
Stability
The digest stays the same as long as the drawing pipeline stays the same. It can change after:
- a browser update that changes text rendering or the graphics back end;
- an operating system update that changes fonts or the emoji set;
- a GPU driver update, or switching hardware acceleration on or off;
- installing or removing the fonts the scene uses;
- a private window or an extension that adds noise (the component turns
noisy, which also changes the visitorId compared with anokrun).
When only the gpu part of two structuredIds differs, a graphics change like these is a likely cause. See What changes the id.
Browser support and what blocks it
- All current browsers offer the 2D canvas, so
canvasis usuallyokon Chrome, Edge, Safari, Firefox and mobile browsers. - Brave:
excludedwith ruleIdbrave_farbling, because Brave randomises canvas reads per site and session. - Firefox with resistFingerprinting and Tor Browser:
excludedwith ruleIdfirefox_rfp, because that mode blocks or fakes canvas readback. - Browsers or extensions that add noise only sometimes (for example in private windows) give
noisy/render_mismatch.
Privacy
The image itself contains only the fixed test scene, nothing from the page or the user. It reveals details about the rendering stack (fonts, emoji set, graphics), not personal content. It leaves the library only as a 32-character digest. Drawing on a canvas needs no permission, shows no prompt and makes no network request.
How WhiskerJS handles noise and failures
| Status | ruleId | When |
|---|---|---|
ok |
none | the scene was drawn twice with identical output; the value is a digest (hashed: true) |
noisy |
render_mismatch |
the two exports differed |
blocked |
empty_image |
the export was empty or not an image |
blocked |
security_error, threw |
the browser threw while drawing or exporting |
unavailable |
no_document, no_api, no_context |
no document, no getContext, or no 2D context |
unavailable |
no_digest |
Web Crypto is missing, so the value cannot be hashed and is not sent raw |
excluded |
brave_farbling, firefox_rfp, caller_excluded |
the exclusion table or your exclude list |
timed_out |
timeout, aborted |
the 1000 ms inner timeout, the overall budget or your abort signal |
Any status other than ok adds the same placeholder line to the visitorId, so blocked in one run and timed_out in the next give the same id. The separate canvas_readback probe tells you whether canvas reads are being altered at all.
Example value
From a headless Chromium test browser in a container:
"canvas": { "status": "ok", "value": "ddf67a6b33259ceea15174ae31a4ac73", "hashed": true }
Questions and answers
Why does canvas show as excluded in Brave?
Brave adds per-site, per-session noise to canvas reads (it calls this farbling). Reading it would only add noise to the id, so the exclusion table skips canvas for the brave browser class and reports excluded with ruleId brave_farbling.
What does noisy with render_mismatch mean?
WhiskerJS drew the same scene twice and the two images differed. That only happens when the browser or an extension adds random noise to canvas reads, so the value is left out and the component counts as missing in the visitorId.
Is the image sent to my server?
No. The exported image is hashed in the browser and only the first 128 bits of its SHA-256 digest leave the library, as 32 hex characters.