Scratch to reveal experimental

Instead of unmasking the whole value, unmask only a trail under the pointer. Move across the element below.

Drag across it. The trail fades behind you.
duty at this trail … estimated leak over a long capture …

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:

DutyLeakRecording needed for a clean 0.9
1.0, always visible0.704.3s
0.1, a brief trail0.2534s

Nobody leaves an account number open for 34 seconds, which is the point.

The 30s default gives most of that back, deliberately. A trail that fades in a second is close to unreadable, and a trail that outlasts your reading sits near full duty. So treat the default as a gate on when the value appears rather than as a defence against capture. Set it to a second or two if capture is the threat you actually care about, and accept reading a few glyphs at a time.

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:

ElapsedTrail remaining
0s, under the pointer1.000
15s0.098
30s0.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

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>