Give the image overlay an optional shadow or glow

A blurred copy of the overlay's own alpha is drawn in a colour beneath it:
moved by an offset for a shadow, centred for a glow, with a size, a spread
and an opacity. The mask is integer-only so a seed still reproduces it, and
reaches past the canvas so an image lying off it still casts onto it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-24 00:56:27 +02:00
co-authored by Claude Opus 5.5
parent 7d122ddd6c
commit 9fb4428a3b
3 changed files with 198 additions and 4 deletions
+43
View File
@@ -638,6 +638,49 @@ int main() {
solid(100, 100, 0, 0, 255)),
"zero opacity leaves the input untouched");
// Halo: none by default, a shadow moved by its offset, a glow all round.
check(sameBytes(renderOverlay(blue, 100, 100, at),
renderOverlay(blue, 100, 100, [&] {
ParamValues none = at;
none["overlay_halo"] = std::string("none");
return none;
}())),
"no halo is the default");
ParamValues shadowed = at;
shadowed["overlay_halo"] = std::string("shadow");
shadowed["overlay_halo_size"] = 0L;
shadowed["overlay_halo_opacity"] = 1.0;
shadowed["overlay_shadow_x"] = 5L;
shadowed["overlay_shadow_y"] = 5L;
const auto shadow = renderOverlay(blue, 100, 100, shadowed);
const auto dark = pixel(shadow, 72, 62); // past the logo's bottom right corner (70, 60)
check(dark[0] == 0 && dark[1] == 0 && dark[2] == 0,
"a hard shadow is cast down and right by its offset");
check(isBlue(pixel(shadow, 28, 50)) && isRed(pixel(shadow, 50, 50)),
"it leaves the other side alone and stays beneath the image");
shadowed["overlay_halo_size"] = 12L;
const auto soft = pixel(renderOverlay(blue, 100, 100, shadowed), 78, 50);
check(soft[2] > 0 && soft[2] < 255, "a soft shadow fades out past the edge");
ParamValues glowing = at;
glowing["overlay_halo"] = std::string("glow");
const auto glow = renderOverlay(blue, 100, 100, glowing);
const auto left = pixel(glow, 27, 50);
const auto right = pixel(glow, 72, 50);
check(left[0] > 0 && left[0] == right[0] && left[1] == right[1],
"a glow lightens both sides alike");
check(isBlue(pixel(glow, 2, 2)), "and fades out before the far corners");
glowing["overlay_halo_spread"] = 0.5;
check(pixel(renderOverlay(blue, 100, 100, glowing), 27, 50)[0] > left[0],
"spread strengthens the halo near the edge");
ParamValues offCanvas = shadowed;
offCanvas["overlay_halo_size"] = 0L;
offCanvas["overlay_offset_x"] = -70L; // the logo ends just left of the canvas
offCanvas["overlay_shadow_x"] = 20L;
const auto cast = pixel(renderOverlay(blue, 100, 100, offCanvas), 5, 50);
check(cast[2] == 0, "an image off the canvas still casts its shadow onto it");
// No image chosen is the state the interface starts in: a pass through.
check(sameBytes(renderOverlay(blue, 100, 100, {}), solid(100, 100, 0, 0, 255)),
"with no image chosen the stage passes its input through");