Preserve edge color and brightness through glow blur

This commit is contained in:
Vikram Bhaskaran
2026-10-10 09:50:05 +07:00
committed by bflatastic
parent 800baacbba
commit 60831b87d9
2 changed files with 56 additions and 20 deletions
+41 -20
View File
@@ -2,26 +2,31 @@
use super::blur_plane;
/// Find Sobel edges using alpha-weighted straight colour, smooth and expand them, then draw
/// coloured outlines on black. Samples outside the raster are transparent.
/// Find Sobel edges, smooth and expand them, then draw coloured outlines on black. Samples outside
/// the raster are transparent.
pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightness: f64, smoothness: f64) {
if w == 0 || h == 0 || w.checked_mul(h) != Some(px.len()) {
return;
}
let src = px.to_vec();
let signal = |x: isize, y: isize| -> f32 {
let pixel = |x: isize, y: isize| -> ([f32; 3], f32) {
// Beyond the raster's edges is transparency (not the next row's first pixel).
let (Ok(x), Ok(y)) = (usize::try_from(x), usize::try_from(y)) else { return 0.0 };
let (Ok(x), Ok(y)) = (usize::try_from(x), usize::try_from(y)) else { return ([0.0; 3], 0.0) };
if x >= w || y >= h {
return 0.0;
return ([0.0; 3], 0.0);
}
let Some(p) = src.get(y * w + x) else { return 0.0 };
let Some(p) = src.get(y * w + x) else { return ([0.0; 3], 0.0) };
let alpha = f32::from(p[3]) / 255.0;
let straight =
if alpha > 0.0 { [f32::from(p[0]) / 255.0 / alpha, f32::from(p[1]) / 255.0 / alpha, f32::from(p[2]) / 255.0 / alpha] } else { [0.0; 3] };
(0.2126 * straight[0] + 0.7152 * straight[1] + 0.0722 * straight[2]) * alpha
(straight, alpha)
};
let signal = |x: isize, y: isize| {
let (rgb, alpha) = pixel(x, y);
(0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) * alpha
};
let mut edges = vec![0.0f32; px.len()];
let mut colours: [Vec<f32>; 3] = std::array::from_fn(|_| vec![0.0; px.len()]);
for y in 0..h {
for x in 0..w {
let x = x as isize;
@@ -32,8 +37,22 @@ pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightnes
+ signal(x - 1, y + 1)
+ 2.0 * signal(x, y + 1)
+ signal(x + 1, y + 1);
if let Some(edge) = edges.get_mut(y as usize * w + x as usize) {
*edge = gx.hypot(gy).clamp(0.0, 1.0);
let alpha_signal = |x, y| pixel(x, y).1;
let agx = -alpha_signal(x - 1, y - 1) + alpha_signal(x + 1, y - 1) - 2.0 * alpha_signal(x - 1, y) + 2.0 * alpha_signal(x + 1, y)
- alpha_signal(x - 1, y + 1)
+ alpha_signal(x + 1, y + 1);
let agy = -alpha_signal(x - 1, y - 1) - 2.0 * alpha_signal(x, y - 1) - alpha_signal(x + 1, y - 1)
+ alpha_signal(x - 1, y + 1)
+ 2.0 * alpha_signal(x, y + 1)
+ alpha_signal(x + 1, y + 1);
let index = y as usize * w + x as usize;
let edge = gx.hypot(gy).max(agx.hypot(agy)).clamp(0.0, 1.0);
if let Some(value) = edges.get_mut(index) {
*value = edge;
let (rgb, alpha) = pixel(x, y);
for channel in 0..3 {
colours[channel][index] = rgb[channel] * alpha * edge;
}
}
}
}
@@ -41,18 +60,20 @@ pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightnes
if width > 0.5 {
blur_plane(&mut edges, w, h, width * 0.5);
}
for (out, (original, edge)) in px.iter_mut().zip(src.iter().zip(edges)) {
let peak = edges.iter().copied().fold(0.0f32, f32::max);
if peak > 0.0 {
for colour in &mut colours {
blur_plane(colour, w, h, smoothness.max(0.0));
if width > 0.5 {
blur_plane(colour, w, h, width * 0.5);
}
}
}
for (index, (out, (original, edge))) in px.iter_mut().zip(src.iter().zip(edges)).enumerate() {
let alpha = f32::from(original[3]) / 255.0;
let glow = (edge * brightness as f32 / 6.0).clamp(0.0, 1.0);
let straight = if original[3] > 0 {
[
f32::from(original[0]) / 255.0 / alpha.max(1e-6),
f32::from(original[1]) / 255.0 / alpha.max(1e-6),
f32::from(original[2]) / 255.0 / alpha.max(1e-6),
]
} else {
[1.0, 1.0, 1.0]
};
let glow = if peak > 0.0 { (edge / peak * brightness as f32 / 6.0).clamp(0.0, 1.0) } else { 0.0 };
let colour_weight = colours[0][index].max(colours[1][index]).max(colours[2][index]);
let straight = if colour_weight > 1e-6 { std::array::from_fn(|channel| colours[channel][index] / colour_weight) } else { [0.0; 3] };
let a = alpha.max(glow);
*out = [
(straight[0] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8,
+15
View File
@@ -143,6 +143,21 @@ fn glowing_edges_glow_alike_on_every_side() {
assert!(at(&out, w, 4, 4)[0] < left[0], "the inside stays darker than the edges");
}
#[test]
fn glowing_edges_defaults_keep_the_outline_bright_and_use_source_colour() {
let (w, h) = (17, 17);
let src = image(w, h, |x, y| if (4..13).contains(&x) && (4..13).contains(&y) { [180, 0, 0, 255] } else { [0; 4] });
let mut out = src.clone();
fx("stylize.glowingEdges", json!({})).apply(&mut out, w, h, &space(w, h));
let outline = at(&out, w, 4, 8);
assert!(outline[0] >= 170, "default outline should retain a near-full-color peak: {outline:?}");
let outside = at(&out, w, 3, 8);
assert_eq!(outside[1], 0, "transparent halo must not be grey: {outside:?}");
assert_eq!(outside[2], 0, "transparent halo must retain source hue: {outside:?}");
assert!(out.as_chunks::<4>().0.iter().all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3]));
}
#[test]
fn spin_blur_smears_along_arcs_and_keeps_the_centre() {
let (w, h) = (41, 41);