Repository navigation
feat(camera): round and feather the spotlight - #49
Conversation
The hole was a clip-path polygon, which can express exactly one shape: a hard-edged rectangle. Rounding its corners means emitting arcs by hand, and softening its edge is not possible at all. On a pill button the square cutout reads as a crop rather than as a highlight, and the one-pixel step from clear to full scrim is the harshest part of the effect. An SVG mask says both in two numbers. A white full-viewport rect keeps the scrim, a black rounded rect punches through it, and blurring that black rect is what feathers the hole. `radius` and `feather` are new options, both defaulted so existing callers get a softer effect without changing anything, and `feather: 0, radius: 0` restores the old look. The fades move to a cubic-bezier ease rather than ease-out, which is what makes the arrival read as a transition instead of a switch. Two details the mask needs and would fail quietly without. `color-interpolation-filters="sRGB"` because SVG defaults to linearRGB, which ramps the scrim far too fast right at the edge. And a filter region widened to 200%, because the default object bounding box clips the blur partway through its ramp on a small target. The four cutout defaults live in SPOTLIGHT_DEFAULTS and are passed through, since the page function is serialized and cannot close over module scope. Written twice, once in the signature and once as a fallback inside the page function, the two could disagree about what a default is. Measured against Chromium by reading pixels rather than markup. At the shipped defaults the scrim ramps 76, 80, 88, 98, 114, 134, 157, 180, 202, 221, 235, 245, 251 across roughly 26px, where before it stepped from 76 to 255 in a single pixel.
…adding
Two regressions from the clip-path div to the SVG mask, both silent.
Page CSS beats SVG presentation attributes. A reset of the kind icon
libraries ship, `svg rect { fill: currentColor }`, repainted both mask rects
the same colour, so on those sites the spotlight dimmed nothing at all. The
old div was immune because it was styled inline. Fills, the mask reference
and the feather filter now go in inline `style`, which a stylesheet cannot
override.
Negative padding was clamped to 0. It shrinks the cutout inside the target,
e.g. to leave out a transparent margin, and rendered correctly before the
rewrite, so existing demos would have got a wider hole with no warning. The
clamp moves to the resulting width and height, which is what actually needs
to stay non-negative.
Both cases have e2e tests that fail without this change. The first test's
fixture needed a dark text colour to fail honestly: with white text the
repainted scrim is white, and white over a white target looks untouched.
Verified in chromium, firefox and webkit.
|
Thanks @Joilence, this is a big visual step up. A rounded, feathered cutout reads as a highlight, where the old hard rectangle read as a crop, and the JSDoc on I pushed one commit to your branch (1d70caa) with two fixes, both for regressions from the clip-path div to SVG that fail silently: Page CSS can override SVG presentation attributes. A reset like Negative Both have e2e tests that fail without the change. One detail: the stylesheet test needed a dark text color to fail. With white text the repainted scrim is white, and white over a white target looks untouched, so my first version of that test passed on the broken code. Merging. Thanks again! |
Why
The cutout was a
clip-path: polygon: a hard-edged rectangle, and nothing else. Rounding the corners means emitting arcs by hand; softening the edge is not possible at all. On a pill button that reads as a crop rather than a highlight, and the one-pixel step to full scrim is the harshest part of the effect.Same page, target and options, both sides calling argo's exported
spotlight(). Bottom row crops the cutout's corner, upscaled nearest-neighbour so the interpolator cannot invent the soft edge in question.Scrim luma across the cutout's left edge, at the shipped defaults:
What
radius: 0, feather: 0restores the old look exactly.color-interpolation-filters="sRGB": SVG defaults to linearRGB, which ramps the scrim far too fast at the hole's edge.featherstays under about a third of the cutout's shorter side, which the option documents.paddingis clamped for the first time here: onmaina large negative value still leaves a hole, under the mask it removes one.Known limitation:
svg.innerHTMLis a Trusted Types sink. On a page servingrequire-trusted-types-for 'script'the assignment throws,runCameraEffectswallows it, and no spotlight renders wheremain'sstyle.cssTextwould have. Left as is because building the nodes costs the mask markup's readability. Happy to convert.Test
main