use tiny_skia::{Pixmap, PixmapPaint, Transform};
use zenith_scene::ShadowSpec;
type SceneColor = zenith_scene::Color;
pub(super) fn composite_shadows(
canvas: &mut Pixmap,
ink: &Pixmap,
shadows: &[ShadowSpec],
width: u32,
height: u32,
) {
let paint = PixmapPaint::default(); for spec in shadows.iter().rev() {
let Some(mut shadow) = Pixmap::new(width, height) else {
continue;
};
tint_coverage(&mut shadow, ink, spec.color);
gaussian_blur_premul(&mut shadow, spec.blur);
let dx = round_offset(spec.dx);
let dy = round_offset(spec.dy);
canvas.draw_pixmap(dx, dy, shadow.as_ref(), &paint, Transform::identity(), None);
}
canvas.draw_pixmap(0, 0, ink.as_ref(), &paint, Transform::identity(), None);
}
fn round_offset(v: f64) -> i32 {
if !v.is_finite() {
return 0;
}
let r = v.round();
if r >= i32::MAX as f64 {
i32::MAX
} else if r <= i32::MIN as f64 {
i32::MIN
} else {
r as i32
}
}
fn tint_coverage(shadow: &mut Pixmap, ink: &Pixmap, color: SceneColor) {
let ca = u32::from(color.a);
let cr = u32::from(color.r);
let cg = u32::from(color.g);
let cb = u32::from(color.b);
let dst = shadow.data_mut();
let src = ink.data();
for (out, inp) in dst.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
let ink_a = u32::from(inp[3]);
let a = ((ink_a * ca) + 127) / 255;
let pr = ((cr * a) + 127) / 255;
let pg = ((cg * a) + 127) / 255;
let pb = ((cb * a) + 127) / 255;
out[0] = pr.min(255) as u8;
out[1] = pg.min(255) as u8;
out[2] = pb.min(255) as u8;
out[3] = a.min(255) as u8;
}
}
fn boxes_for_gauss(sigma: f64) -> [u32; 3] {
const N: f64 = 3.0;
if sigma <= 0.0 {
return [1, 1, 1];
}
let w_ideal = ((12.0 * sigma * sigma / N) + 1.0).sqrt();
let mut wl = w_ideal.floor() as i64;
if wl % 2 == 0 {
wl -= 1;
}
if wl < 1 {
wl = 1;
}
let wu = wl + 2;
let wl_f = wl as f64;
let m_ideal =
(12.0 * sigma * sigma - N * wl_f * wl_f - 4.0 * N * wl_f - 3.0 * N) / (-4.0 * wl_f - 4.0);
let m = m_ideal.round() as i64;
let mut out = [0u32; 3];
for (i, slot) in out.iter_mut().enumerate() {
let w = if (i as i64) < m { wl } else { wu };
*slot = w.max(1) as u32;
}
out
}
pub(super) fn gaussian_blur_premul(pm: &mut Pixmap, sigma: f64) {
if !(sigma.is_finite() && sigma > 0.0) {
return;
}
let width = pm.width() as usize;
let height = pm.height() as usize;
if width == 0 || height == 0 {
return;
}
let boxes = boxes_for_gauss(sigma);
let data = pm.data_mut();
let expected = width * height * 4;
if data.len() != expected {
return; }
let mut scratch = vec![0u8; expected];
for w in boxes {
let radius = ((w.max(1) - 1) / 2) as usize;
if radius == 0 {
continue; }
box_blur_h(data, &mut scratch, width, height, radius);
box_blur_v(&scratch, data, width, height, radius);
}
}
fn box_blur_h(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
let window = (2 * radius + 1) as u32;
let last = width.saturating_sub(1);
for y in 0..height {
let row = y * width * 4;
for c in 0..4 {
let mut sum: u32 = 0;
for i in 0..=(2 * radius) {
let xx = (i.saturating_sub(radius)).min(last);
let v = src.get(row + xx * 4 + c).copied().unwrap_or(0);
sum += u32::from(v);
}
for x in 0..width {
if let Some(o) = dst.get_mut(row + x * 4 + c) {
*o = ((sum + window / 2) / window).min(255) as u8;
}
let add_x = (x + radius + 1).min(last);
let sub_x = x.saturating_sub(radius);
let add = src.get(row + add_x * 4 + c).copied().unwrap_or(0);
let sub = src.get(row + sub_x * 4 + c).copied().unwrap_or(0);
sum = sum + u32::from(add) - u32::from(sub);
}
}
}
}
fn box_blur_v(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
let window = (2 * radius + 1) as u32;
let stride = width * 4;
let last = height.saturating_sub(1);
for x in 0..width {
let col = x * 4;
for c in 0..4 {
let mut sum: u32 = 0;
for i in 0..=(2 * radius) {
let yy = (i.saturating_sub(radius)).min(last);
let v = src.get(col + yy * stride + c).copied().unwrap_or(0);
sum += u32::from(v);
}
for y in 0..height {
if let Some(o) = dst.get_mut(col + y * stride + c) {
*o = ((sum + window / 2) / window).min(255) as u8;
}
let add_y = (y + radius + 1).min(last);
let sub_y = y.saturating_sub(radius);
let add = src.get(col + add_y * stride + c).copied().unwrap_or(0);
let sub = src.get(col + sub_y * stride + c).copied().unwrap_or(0);
sum = sum + u32::from(add) - u32::from(sub);
}
}
}
}