diff --git a/crates/effects/src/pixel/edges.rs b/crates/effects/src/pixel/edges.rs index f1c170600..afed97da2 100644 --- a/crates/effects/src/pixel/edges.rs +++ b/crates/effects/src/pixel/edges.rs @@ -10,10 +10,15 @@ pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightnes } let src = px.to_vec(); let signal = |x: isize, y: isize| -> f32 { - if x < 0 || y < 0 { return 0.0; } - let Some(p) = src.get(y as usize * w + x as usize) else { return 0.0 }; + // 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 }; + if x >= w || y >= h { + return 0.0; + } + let Some(p) = src.get(y * w + x) else { return 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] }; + 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 }; let mut edges = vec![0.0f32; px.len()]; @@ -21,18 +26,39 @@ pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightnes for x in 0..w { let x = x as isize; let y = y as isize; - let gx = -signal(x - 1, y - 1) + signal(x + 1, y - 1) - 2.0 * signal(x - 1, y) + 2.0 * signal(x + 1, y) - signal(x - 1, y + 1) + signal(x + 1, y + 1); - let gy = -signal(x - 1, y - 1) - 2.0 * signal(x, y - 1) - signal(x + 1, y - 1) + 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 gx = -signal(x - 1, y - 1) + signal(x + 1, y - 1) - 2.0 * signal(x - 1, y) + 2.0 * signal(x + 1, y) - signal(x - 1, y + 1) + + signal(x + 1, y + 1); + let gy = -signal(x - 1, y - 1) - 2.0 * signal(x, y - 1) - signal(x + 1, y - 1) + + 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); + } } } blur_plane(&mut edges, w, h, smoothness.max(0.0)); - if width > 0.5 { blur_plane(&mut edges, w, h, width * 0.5); } + 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 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 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 a = alpha.max(glow); - *out = [(straight[0] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (straight[1] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (straight[2] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (a * 255.0).round() as u8]; + *out = [ + (straight[0] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, + (straight[1] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, + (straight[2] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, + (a * 255.0).round() as u8, + ]; } } diff --git a/crates/effects/src/pixel/tests.rs b/crates/effects/src/pixel/tests.rs index 9a0ebe46d..120e7a1c4 100644 --- a/crates/effects/src/pixel/tests.rs +++ b/crates/effects/src/pixel/tests.rs @@ -128,7 +128,19 @@ fn glowing_edges_draw_a_bright_outline_and_preserve_premultiplication() { let mut out = src.clone(); fx("stylize.glowingEdges", json!({"edgeWidth": 1, "smoothness": 1, "edgeBrightness": 20})).apply(&mut out, w, h, &space(w, h)); assert!(at(&out, w, 3, 4)[0] > 0 || at(&out, w, 4, 4)[0] > 0); - assert!(out.chunks_exact(4).all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3])); + assert!(out.as_chunks::<4>().0.iter().all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3])); +} + +/// Glowing Edges sees transparency beyond every side of the raster: a solid square glows alike at +/// its left and right edges (the right one doesn't read the next row's first pixel). +#[test] +fn glowing_edges_glow_alike_on_every_side() { + let (w, h) = (9, 9); + let mut out = image(w, h, |_, _| [180, 180, 180, 255]); + fx("stylize.glowingEdges", json!({"edgeWidth": 1, "smoothness": 1, "edgeBrightness": 20})).apply(&mut out, w, h, &space(w, h)); + let (left, right, top, bottom) = (at(&out, w, 0, 4), at(&out, w, 8, 4), at(&out, w, 4, 0), at(&out, w, 4, 8)); + assert!(left[0] > 0 && left == right && top == bottom, "{left:?} {right:?} {top:?} {bottom:?}"); + assert!(at(&out, w, 4, 4)[0] < left[0], "the inside stays darker than the edges"); } #[test]