API

Every attribute and every export. Each row says what it trades, not just what it sets, because almost nothing here is free and the costs are measured.

The value

import 'nocap-js';                          // registers <nocap-secret>
el.secret = await fetchAccountNumber();  // write-only. never enters the DOM
membertypewhat it does, and what it costs
.secretset onlyThe value. No getter, deliberately: one would put the plaintext back within reach of anything holding the element. The element keeps it internally so a restyle does not need it again, which is why scramble exists.
.planesPixels[]The frames a capture can land on. Plane k is exactly what one screenshot gets.
.measureLeak()numberWorst plane scored against the mean. ~0.2 at the defaults on text, 1.0 is fully readable.
.fittedobjectWhat the palette was moved to, or null if it already masked. See fit.
width / heightCSS pxAttributes, not styles. Do not size with CSS: letting the browser rescale the canvas resamples the noise toward its mean, which is the one transformation that makes a captured frame readable.

Strength

valuewhat it trades
weakThe block sits deliberately under the saturation point, so a blur has a radius worth trying. Calmest to look at.
mediumExactly saturated. The default, and the balanced point.
strongHeadroom. The most visually active.
These are tested points on the masking curve, not the comfort curve. All three read comfortably at the default size on a 60Hz panel, judged by one person on one display. Large text is unmeasured. Use a preset rather than tuning numbers; custom palettes and large-type deployments are a craft of their own, and the promo reel shows what a tuned deployment looks like.

The split

attributedefaultwhat it does, and what it costs
amplitude110How far each pixel is displaced, as a fraction of the headroom its colour allows. The biggest lever on raw leak. Capped by the palette, not by this number.
noise-scale2 × stroke, max 16Block size in device px. Its only job is blur resistance: raw leak barely moves with it. Derived rather than set, because the right block follows the stroke, then capped by noise-scale-max. Warns when it lands under twice the stroke.
hardness11 puts every pixel at full amplitude. Lower spreads the magnitudes and measures worse both raw and blurred, so 1 is right.
chroma00 shares one draw across R/G/B, putting the whole budget into luminance, which is what the eye and OCR key on. 1 is per channel: rainbow static, and measurably weaker.
noise-profilewhiteblue high-passes the lattice, which may read calmer. Security unchanged. Not true blue noise, which needs void-and-cluster.
frames2, or 6 for aperturePlanes per cycle. Chosen by the mode. More frames means a slower cycle, and cycle rate is refresh ÷ frames.
gamma2.4Display EOTF. Decides where plane centres are solved, so a wrong value shifts the perceived colour.
contrast1Pre-emphasis. Not needed under linear light, which does not compress.
ink-bias0Leans amplitude toward the content. The trap: where the noise is, is where the text is, and the measured leak climbs steadily across its range. Past 0.3, measure on your own content.
patternnonedots, hatch, grid or grain drawn into the element background, so a page texture continues across the boundary. Not free, unlike a page texture: the split carries it as content.
pattern-strength16In code levels. The measured leak cost rises with the strength. Judge it live: it survives into the mean at full strength, but a single frame spans far more levels and buries it, so a screenshot shows nothing at any setting.
noise-scale-max16Ceiling on the derived block. Chosen for looks, not security: measured, the leak does not care where the ceiling sits, while large blocks read as tiles rather than noise.
edge-fade0Tapers noise to nothing within N px of the element edge, so a lone block dissolves. Free, unlike ink-bias: it follows the canvas rectangle, which an attacker already sees. Not for adjacent elements -- two taperings meet at a seam and read as a border.

Colour

attributedefaultwhat it does, and what it costs
color#9ea6b4Text colour.
background#6b7280Background. Usually the one to move: the page has to meet the secret, because a near-black ground has no room to carry noise.
fitonMoves an unmaskable palette into one that masks, keeping hue and light-or-dark character. off keeps your exact hex and loses the protection. White on black goes from masking nothing to masking.
adaptiveoffExact colours, amplitude capped per pixel to their own headroom.
Masking and contrast are the same axis pointing opposite ways. Ratio is min(swing) / separation, so high contrast is a large separation, and past a point a single frame stays readable however the rest is set. The contrast page measures the trade live on any pair. Saturation destroys masking, not hue: a saturated colour pins a channel at an extreme, and a pinned channel cannot be displaced.

Type

attributedefaultwhat it does, and what it costs
font-familyui-monospaceThe stroke follows the font and the block follows the stroke, so this changes protection as well as looks.
font-weight600The block derivation assumes roughly this weight. A 300 stem is much thinner and wants a finer block.
font-sizeheight × 0.46In device px. On a 2× display a CSS-px number renders half-size -- use font-scale instead.
font-scale0.46Fraction of the element height. Density-independent, so prefer this.
letter-spacing0Needs a CSS length; a bare number gets px added. Inert under scramble.
text-aligncenterleft / center / right. Inert under scramble.
padding-x / padding-y0Inert under scramble.
Any value that is not usable falls back to the documented default and warns once, because an invalid ctx.font or a non-finite fillText coordinate is a silent no-op in canvas. Text wider than the element also warns rather than being quietly clipped. Stack elements with --nocap-radius: 0 or the 4px rounding notches every seam.

Modes

modeleakwhat it does, and what it costs
amplitudemasksEvery pixel present, every value displaced. Needs colour headroom. The default.
aperturemasksA band sweeps down; each frame carries one slice and genuinely lacks the rest. Needs no colour headroom, so it works at pure white. Costs a 6-frame cycle, which is 10Hz on a 60Hz panel -- near the peak of temporal sensitivity.
interleavereadableKept as a counterexample. Splits where pixels are, and subsampled text reads fine.
fakeoffauto / number / text / random. Each cycle carries a different plausible wrong value: a capture freezes one at full contrast, the viewer resolves none. Needs a maskable palette (ratio 1.0+). Draws the value centred -- alignment and spacing attributes are inert while on.
scramble -- Stores glyphs shuffled, so a heap search never finds the value in order. Obfuscation, not encryption.
chroma-decoy -- A decoy in chrominance at zero luminance contrast. Spatial, so it survives frame averaging -- the only thing here that does. Block 2-4px: 1px is annihilated by 4:2:0.
watermark -- An identifier baked into chrominance at zero luminance contrast, so it survives the averaging that recovers the value and names the capture that leaked. Attribution, not protection: a single greyscale conversion removes it. Casual leaks, not determined ones.
watermark-swing60Chroma excursion for the watermark. The direction is picked toward the side with headroom, so a blue-heavy ground moves toward yellow rather than quietly getting less swing than asked.
watermark-repeat3How many times the mark tiles, 1-8. More survives cropping; more is also easier to spot.
chroma-block2Block size in px for the chroma decoy and watermark. Keep it ≥ 2: 1px chroma is annihilated by the 4:2:0 subsampling every screenshot pipeline applies.
fake-share0.8Share of each ink pixel's excursion budget the decoy takes, 0-0.9. The re-solved centre keeps the perceived value exact at every setting, so the cost of raising it is noise where the decoy's ink falls, not ghosting. A quiet decoy reads under the truth and convinces nobody.
fake-size1Decoy glyph size as a ratio of the real type. Full size is the default because it is the measured requirement: a smaller decoy scored below the real value in a captured frame at every share.

Scratch

attributedefaultwhat it does, and what it costs
scratchoffUnmask only a trail under the pointer. Needs a pointer, so keyboard and screen-reader users need another route.
scratch-linger30Seconds for a stroke to fade to 1%. A long trail sits near full duty and gives up most of the capture benefit. 1-2s if capture is the threat.
scratch-radius34, or 52 coarseBrush radius in CSS px. Wider on touch, because a fingertip covers what it reveals.
scratch-hintScratch to revealThe affordance. Without it the element is a blank rectangle. It is real DOM text, so it is the one string the element contributes to innerText.
scratch-exclusiveonRevealing one clears the others. Does not slow extraction -- capture is 0.3s. It stops one frame containing two revealed values.

Texture

Draws the page's own pattern through the element, so the block reads as part of the surface instead of a patch on it. The pairing page is the live version of this table.

attributedefaultwhat it does, and what it costs
patternnonedots, hatch or grid -- the three the page CSS can mirror exactly (16px dot lattice, 3px/13px 45° hatch, 46px grid). Grain does not survive over noise and is deliberately absent.
pattern-strength16Levels the texture moves its ground, same meaning as the page's CSS alpha solved per ground. A level count is not a visibility: over noise spanning the full range the eye normalises it away, so matching the page's look takes a good deal more than the page's own number, and the mismatch in numbers is what makes them look equal.
pattern-layerbackback draws it into the canvas: the split carries it, so it is capped by the ground's headroom and vanishes in a still capture. front composites it over the canvas: free (added identically to every frame, the planes still average to target plus a constant), full-strength on any ground, and it survives a screenshot -- at the cost of competing with the glyphs. It adds no protection either way; an attacker who knows the pattern subtracts it.
pattern-offset-x / pattern-offset-y0Phase, in CSS px: where the element sits relative to the pattern's origin, so the lattice continues through the block instead of restarting at its edge. The front layer also reads the live custom properties --nocap-pattern-ox/oy, which land without a repaint.
pattern-enter -- left / right / up / down / center: the direction the front texture wipes in from, via clip-path keyframes. Without it the texture is simply shown -- the failure mode is no animation, never no texture.
pattern-playing -- Presence runs the wipe; remove and re-add to replay it. Honoured only alongside pattern-enter.

Playback

attributedefaultwhat it does, and what it costs
max-dpruncappedCeiling on the devicePixelRatio the canvas renders at. Split cost, bitmap memory and every per-frame draw scale with dpr squared, and the noise is deliberately chunky, so a dpr-3 phone does 2.25× the work of dpr-2 for a look that is indistinguishable at reel-sized type. The promo sets 2. Leave it uncapped for body-sized text, where glyph edges still buy something.
paused -- Stops the frame cycle. A paused element freezes on ONE plane, which is a full-amplitude noise frame -- exactly what a capture contains, which makes it the honest "what a screenshot gets" demo. Pause anything off screen: thirty-nine running canvases pulled a page to ~41Hz, putting the cycle at 21Hz, squarely in the discomfort band. Pause only after a fade-out, or the frozen plane is visible mid-fade.

Exports

exportkindwhat it is for
splitFramescoreOne image to N planes. Pure, DOM-free, runs in Node.
averageFramescoreArithmetic mean -- what a re-encoded recording produces.
perceivedMeancoreMean in light -- what a viewer resolves. Differs from the above by ~19 levels.
leakScoremetric|Pearson r| between a plane and the truth. Blind spot: a mode inserting a large content-independent pattern scores better for it.
boxBlur / gaussianBlur / medianFilterattackShipped so a claim can be run. Box wins against block noise; median loses because at a radius that keeps strokes it sits inside one block.
denoisedLeak / bestAttackattackWorst result across radii, or across all three denoisers.
checkPalettepaletteMasking ratio, grade and warnings for a pair.
fitToBandpaletteMove an unmaskable pair into one that masks.
isoluminantPairpaletteTwo colours of equal luminance whose mean is exactly the background.
contrastRatio / codeSwingpaletteWCAG contrast; how far a colour can travel before clipping.
auditPagecheckSearch every readable surface for a value. Takes plaintext, so development and tests only.
fakeLike / detectFormat / passesLuhngeneratePlausible values of a matching shape. Luhn-valid cards, real dates.
resolveOptions / resolveTextpureAttribute resolution, exported so it can be tested without a DOM.
luma / toRgb / toHexpalettePerceptual luma of a colour; hex↔[r,g,b]. Arrays, not objects -- a harness passing {r,g,b} once measured NaN for a session and shipped a wrong constant on it.
toLight / toCodepalettesRGB code value ↔ linear light. Averaging happens in light -- a display emits (v/255)γ -- which is why perceivedMean and averageFrames differ by ~19 levels.
planeRange / expandRange / maxAmplitudeForpaletteRange arithmetic: what a plane can span, and the largest amplitude a pair carries without clipping -- clipping silently breaks the zero-sum property.
suggestConfig / placeInBand / isoluminantPartnerpalettePick settings for a palette; move a colour into the maskable band; the equal-luminance partner a chroma effect needs.
FlickerruntimeThe presenter, if you want the split without the element.

How it actually works

Content is split into frames that alternate at your display's refresh rate. Each frame is noise, their mean is the content, and your visual system does the averaging. There is no noise layer. Each plane pixel is the target pixel displaced, so "covering" is the wrong picture: the noise moves values, it does not sit on top of them.

Two consequences follow, and both surprise people. A pixel can only be displaced as far as its colour allows, which is why white -- already at the ceiling -- cannot be displaced at all and why fit exists. And every pixel inside one block gets the same offset, so local edges survive intact and the block shifts brightness rather than destroying structure. That is why it can look faintly transparent while still masking.

The split is solved in light, not in code values. A display emits (v/255)^gamma and the eye integrates light, so averaging in sRGB reads far too bright: #ff0000 arrives as #be8c8c. Solving the centre so the pair averages in light is what makes color and background mean what they say.

The claims are narrow on purpose, and they are checkable rather than taken on trust: the security check and the scraping challenge run them live on this site.