The 5% step is not "too faint to see", it is touched=0
A white watermark over a light base is the most common pairing in design files. Push the opacity slider to 5% and the exported PNG is 4,782 bytes; the same image size exported without any watermark is also 4,782 bytes, not one byte different. The measuring script records touched=0: no pixel in the frame was changed at all. The 6% step is touched=0 as well.
Scan all 96 opacity steps and only 15 distinct integers actually land on a light base; the remaining steps collapse onto one another.
The template is not what does the blending
Hunting through the code for a blend function is a disappointment. The whole template holds just two assignments, ctx.globalAlpha = opacity on line 293 and on line 461, with no getImageData and no hand-written dst*(1-a)+src*a. The pixel-by-pixel arithmetic is carried out by the browser compositor; the template only hands the alpha over.
That sweep across all 96 steps is 1,152 measured points. Compared with an ideal round(), a deviation of 0 hits 828 times, or 71.875%; a deviation of -1 hits 240 times and +1 hits 80 times, and the largest absolute deviation is 1 level.
The same alpha routed through fillText and through drawImage matches on 96/96 platform values one by one, so the text path and the logo path do not run on a second set of rules. The three channels of a coloured logo such as #ffd700 land independently, and the B channel spans only 18.
Contrast is something you measure
A white watermark pressed onto a dark base (RGB 20) and onto a light base (RGB 240) does not differ by whether it is visible, but by how high the WCAG ratio can climb. To re-check these steps, upload a solid-colour base image on the Image Watermark page and compare the exported byte counts of the two lowest steps.
| Measured item | Dark side, RGB 20 | Light side, RGB 240 |
|---|---|---|
| WCAG contrast ratio, first step scanned | 1.1177 | 1.0000 (5% step, touched=0) |
| WCAG contrast ratio at 80% | 11.9441 | 1.1108 |
| Pixels changed at the 5% and 6% steps | — | 0 |
| Exported PNG size in bytes | — | 4,782, the same as the base image |
| Distinct integers left after the 96 steps collapse | — | 15 |
A single watermark that straddles the hard boundary between dark and light says the most: at 80% the dark side reaches 11.9441 while the light side only gets 1.1108. Same alpha, same call, and the two sides sit an order of magnitude apart.
Fig. 1: the measured contrast ratio of one white watermark on the dark side and the light side.
Tiling spacing is not clearance
The tiling step is sx = measureText width + gapx and sy = fontSize + gapy. At font size 48 the box is 48 tall, while the ink of Watermark is only 35 tall, so a nominal vertical spacing of 120 px on the slider leaves 133 px of real clearance.
Pull gap up to 400, a legal slider value, and coverage is 0.0000%. The step arithmetic is not wrong: the loop origin is hard-coded as -wW and -wH, so origin and step grow together and the first cell lands outside the picture.
Rotation turns the drawn item only, never the step. Under font size 48 with gap 40, rot0 leaves 254 empty rows; rot15, 30, 45 and 135 at that same gap40 already have 0 empty rows and 0 empty columns. Coverage rises from 16.1331% to 17.9763% (45°) and 19.4256% (135°).
Can the original be solved back? No, but the distance is measurable
Consider only the 2,963 pixels the watermark covers completely. For one frame the error amplification factor is exactly 1/(1-a): an average error of 0.270 at 5%, 1.310 at 80%, and a maximum of 5.
Hoping to wash the watermark out in post-processing hurts on every count. Mean per-channel error, measured on that 80% frame:
- raw frame 78.461
- after contrast stretch 79.839
- after inversion 109.599
- after thresholding 111.953
- joint least squares over 6 steps 0.467, with the maximum error falling from 153 to 1
Only the joint multi-step fit drives the error down, and that is a fit across 6 samples taken at different alphas, not a restoration of the original image.
One more result: taking the per-channel minimum over 6 frames as an envelope gives an average error of 4.480, exactly the 4.480 of the single 5% frame. The envelope has no extrapolation power; it equals nothing but the observation at the lowest step.
Fig. 2: all three post-processing steps are worse than the raw frame, and only the joint fit over 6 steps pulls the error down.
Nothing gates the export path
Export is a single line, toBlob(..., 'image/png'); the quality argument is undefined, the file name looks like watermarked_400x400.png, and the tool has no JPEG branch at all. file.size, img.width and any size clamp never appear in the template, so a 4,000 × 4,000 base image still runs all the way out to a 1,580,930 byte PNG.
Broken images are not stopped either. Lines 207 to 215 bind only onload and never bind onerror; a 72 byte corrupt.png clears the MIME gate and then fails silently, with baseImage still null 4,000 ms later.
The MIME gate is one unanchored substring search: lines 204 and 232 both run file.type.match(/image\//). The very same PNG bytes, declared as x-image/png and declared as image/png, yield exactly the same 400x400 / 4,659 pixels / 10,676 bytes; writing application/image+x is what gets rejected.
Batch, corner drag and the scale slider: none of the three holds
The file control has no multiple, and both onchange and drop take only files[0], so feeding in 3 files loads just the first image.
Dragging the lower-right corner in logo mode moves wmScale from 1 to 1.4951, yet the logo box that can be exported is still 60x60@170,170, still 3,600 pixels and 114,858 bytes. Text mode is blunter: the images exported at the 5, 30 and 100 readings of the watermark scale slider are byte-for-byte identical, all of them 4,659 pixels and 121,019 bytes, while the slider keeps printing 60% — wmScale is never written back anywhere in the template.
Beyond these numbers
This article reports measured pixel-level steps only. Where a watermark should sit and how to trade off its density belong to the older piece on deterring image theft, and no "right opacity" is offered here: on a light base 5% and 6% are both touched=0, while on a dark base the same step measures 1.1177. The difference is set by the base colour, not by some universal value.
Q: What is actually different between 5% and 6%?
On a light base RGB 240 both steps report touched=0 and export 4,782 bytes, matching the base image, so only the slider reading differs.
Q: Can a watermarked image be computed back into the original?
No. The single-frame amplification factor is 1/(1-a), and contrast stretch 79.839, inversion 109.599 and thresholding 111.953 are all worse than the raw 78.461; the least squares fit over 6 steps reaches 0.467, which is a fit across repeated samples.
Q: Why does export set no size limit and no broken-image guard?
There is a single encoding path, image/png, quality is undefined, and there is no file.size ceiling; <img> never binds onerror, so a broken image clears the MIME gate and fails silently.