Scratch to reveal experimental
Instead of unmasking the whole value, unmask only a trail under the pointer. Move across the element below.
The trail length is the whole trade
A pixel carries the value only while the trail sits on it. With a short trail, a
long capture averages to roughly duty × value while the noise keeps
its full amplitude. Measured over 30 frames:
| Duty | Leak | Recording needed for a clean 0.9 |
|---|---|---|
| 1.0, always visible | 0.70 | 4.3s |
| 0.1, a brief trail | 0.25 | 34s |
Nobody leaves an account number open for 34 seconds, which is the point.
Why it is timed in seconds
It was not, at first. The trail faded by a constant factor per frame, so a stroke cleared in about a second at 60Hz and about half that at 120Hz. The same setting meant different things on different displays, and no value of it could be stated in seconds at all.
scratch-linger is a time: how long a stroke takes to fade to 1%.
The decay is exponential, so the product of the per-frame factors depends only
on elapsed time and not on how that time was cut into frames. Measured in Chrome
against a manual clock, with a 30s setting:
| Elapsed | Trail remaining |
|---|---|
| 0s, under the pointer | 1.000 |
| 15s | 0.098 |
| 30s | 0.0078 |
The last is a little under 1% because the mask is 8-bit and the rounding accumulates.
The hint is on by default
With scratch enabled and nothing scratched yet, the element is a flat rectangle. Nothing about it says it can be rubbed, so a first-time user sees an empty box and moves on. The hint is the affordance, and it is on unless you turn it off.
It fades out the moment scratching starts and comes back only once the trail
has faded, which ties it to scratch-linger rather than to a
constant. That way it reappears exactly when the element has gone blank again
and needs explaining a second time. Drop the trail to a second or two above and
watch it return quickly.
It sits over the canvas rather than being painted into it. Painted in, it would
be split into frames and masked along with the value, and the one thing an
affordance must never be is hidden. It is ordinary text in the shadow root, so
it is also the one string the element contributes to innerText and
to a screen reader. That is on purpose: it is a fixed label, not a secret, and
announcing that something interactive is here beats silence.
On a phone
Touch works, and it takes three things rather than one. A drag over an element
scrolls the page unless the element sets touch-action: none, so
without that pointermove never fires and the whole mode silently
does nothing. The pointer is captured on the way down, because a fingertip is
imprecise and a stroke that wanders off the element would otherwise stop dead.
And the brush defaults wider on a coarse pointer, since a finger sits on top of
the thing it is revealing and the readable part of the trail has to reach past
it.
Lifting a finger fires pointerup, not pointerleave.
Listening only for the latter left the last position live, so every frame
repainted the trail there and it never faded. Both are handled now, along with
pointercancel.
Before you ship this
- It needs a pointer of some kind. Keyboard and screen reader users cannot scrub at all, so an integration has to offer them another route. This is a hard blocker, not a rough edge.
- Reading a long value by sweeping is slow. Opt in, never a default.
Using it
<nocap-secret scratch scratch-linger="30" <!-- seconds for a stroke to fade to 1% --> scratch-radius="34" <!-- brush radius in CSS px --> scratch-hint="Scratch to reveal" <!-- or "off" --> ></nocap-secret>