/// Drop Shadow effect generator. /// /// Renders a drop shadow by offsetting the layer alpha, applying spread, /// blurring, and filling with the shadow color. Supports knock-out mode. use crate::helpers::{apply_spread, box_blur_f32}; /// Generate a drop shadow buffer. /// /// **Purpose:** /// Renders a drop shadow effect on the transparent background by shifting the layer alpha, /// applying spread scaling, blurring, and coloring. /// /// **Logic & Workflow:** /// 1. Offset layer alpha by the specified angle and distance. /// 2. Apply spread scaling to the offset alpha (multiplied by `spread_scale = 0.00` based on Krita tuning). /// 3. Apply box blur for Gaussian approximation using the tuned parameters (`blur_factor = 1.5`, `passes = 1`). /// 4. Populate the final RGBA buffer with the shadow color and the computed alpha values. /// 5. Optionally knock out the shadow under the original opaque layer pixels. /// /// **Arguments:** /// * `alpha` — `&[u8]`: Layer alpha channel. /// * `w` — `u32`: Canvas width. /// * `h` — `u32`: Canvas height. /// * `angle` — `f32`: Shadow angle in degrees (PSD convention: 0=down, 90=left). /// * `distance` — `f32`: Shadow offset distance in pixels. /// * `spread` — `f32`: Spread percentage (0.0–100.0). /// * `size` — `f32`: Blur radius. /// * `color` — `[u8; 4]`: Shadow RGBA color. /// * `knock_out` — `bool`: If true, shadow is hidden behind the layer. /// /// **Returns:** /// `Vec`: RGBA buffer with the rendered drop shadow. /// /// **Side Effects / Dependencies:** /// Relies on `box_blur_f32` and `apply_spread` helpers from the `helpers` module. pub(crate) fn generate_shadow( alpha: &[u8], w: u32, h: u32, angle: f32, distance: f32, spread: f32, size: f32, color: [u8; 4], knock_out: bool, ) -> Vec { let px = (w * h) as usize; let mut buf = vec![0u8; px * 4]; let rad = angle.to_radians(); let dx_raw = (-rad.cos() * distance).round() as i32; let dy_raw = (rad.sin() * distance).round() as i32; let mut offset_alpha = vec![0u8; px]; for y in 0..h as i32 { for x in 0..w as i32 { let a = alpha[(y as u32 * w + x as u32) as usize]; if a == 0 { continue; } let dx = x + dx_raw; let dy = y + dy_raw; if dx >= 0 && dx < w as i32 && dy >= 0 && dy < h as i32 { let di = (dy as u32 * w + dx as u32) as usize; offset_alpha[di] = offset_alpha[di].max(a); } } } // MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800) // Best: blur_factor=5.0, passes=5, spread_scale=1.00 let radius = size.max(0.0).round() as i32; if radius > 0 { let gaussian_r = (radius as f32 / 5.0).round().max(1.0) as i32; let mut f32_buf: Vec = offset_alpha.iter().map(|&a| a as f32).collect(); for _ in 0..5 { f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r); } offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect(); } // Actually use the spread parameter (0.0 to 100.0) from the user AFTER the blur! apply_spread(&mut offset_alpha, spread); for i in 0..px { let a = offset_alpha[i]; if a > 0 { buf[i * 4] = color[0]; buf[i * 4 + 1] = color[1]; buf[i * 4 + 2] = color[2]; buf[i * 4 + 3] = a; } } if knock_out { for i in 0..px { if alpha[i] > 0 { buf[i * 4 + 3] = 0; } } } buf }