diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c9fe024..fe7ca39 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1105,10 +1105,7 @@ impl Renderer { if let Some(sh) = &e.outer_shadow { let c = rctx.color(&sh.color, ph); let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist; - let c0 = Point::new(w / 2.0, h / 2.0); - let local = Affine::translate(c0.to_vec2()) - * Affine::new([sh.sx, sh.ky.to_radians().tan(), sh.kx.to_radians().tan(), sh.sy, 0.0, 0.0]) - * Affine::translate(-c0.to_vec2()); + let local = shadow_local(sh, w, h); // Shadow offset is in slide space (doesn't rotate with the shape unless asked). let sm = if sh.rotate_with_shape { m * Affine::translate(d) * local } else { pre_translate(m, d * k) * local }; ctx.set_transform(Affine::IDENTITY); @@ -1169,6 +1166,32 @@ impl Renderer { } } +/// A shadow's own scale and skew (`sx`, `sy`, `kx`, `ky`) in the shape's space, about the point +/// of the shape's box its alignment names (`algn`: `b` is the middle of the bottom edge, `tl` the +/// top-left corner…), as PowerPoint applies them: skewed, then scaled. A perspective shadow +/// (`algn="b"`, `sy` 23%, `kx` -20°) so lies flat along the shape's bottom edge, not around its +/// middle. +fn shadow_local(sh: &deckcraft_model::style::Shadow, w: f64, h: f64) -> Affine { + let (ax, ay) = match sh.align.as_str() { + "tl" => (0.0, 0.0), + "t" => (0.5, 0.0), + "tr" => (1.0, 0.0), + "l" => (0.0, 0.5), + "r" => (1.0, 0.5), + "bl" => (0.0, 1.0), + "b" => (0.5, 1.0), + "br" => (1.0, 1.0), + _ => (0.5, 0.5), + }; + let a = Vec2::new(w * ax, h * ay); + let (tx, ty) = (sh.kx.to_radians().tan(), sh.ky.to_radians().tan()); + let k = Affine::new([sh.sx, sh.sy * ty, sh.sx * tx, sh.sy, 0.0, 0.0]); + if !k.is_finite() { + return Affine::IDENTITY; + } + Affine::translate(a) * k * Affine::translate(-a) +} + fn pre_translate(m: Affine, d: Vec2) -> Affine { Affine::translate(d) * m } diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 5f87c7a..c1ec2ef 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -189,3 +189,39 @@ fn a_radial_gradient_off_centre_ends_on_the_circle_around_the_box() { assert!(img.pixel(300, 200)[0] > 240 && img.pixel(101, 101)[0] < 60); assert!((img.pixel(110, 200)[0] as i32 - img.pixel(490, 200)[0] as i32).abs() <= 2); } + +fn shadow(sx: f64, sy: f64, kx: f64, align: &str) -> deckcraft_model::style::Shadow { + deckcraft_model::style::Shadow { + color: ColorRef::rgb(Rgba::BLACK), + blur: 0.0, + dist: 0.0, + dir: 0.0, + inner: false, + sx, + sy, + kx, + ky: 0.0, + align: align.into(), + rotate_with_shape: false, + } +} + +fn with_effects(fill: Option, line: Option, effects: deckcraft_model::Effects) -> Shape { + Shape { id: ShapeId(14), xfrm: Some(Xfrm::new(100.0, 100.0, 100.0, 100.0)), fill, line, effects: Some(effects), ..Default::default() } +} + +#[test] +fn a_perspective_shadow_lies_along_the_edge_its_alignment_names() { + // Half height, skewed 45° so its top leans right, anchored at the bottom middle: the shadow's + // bottom edge is the shape's (y = 200) and its top (y = 150) is 50 pt to the right. + let fx = deckcraft_model::Effects { outer_shadow: Some(shadow(1.0, 0.5, -45.0, "b")), ..Default::default() }; + let img = render(vec![with_effects(solid(RED), no_line(), fx)]); + assert!(close(img.pixel(210, 175), Rgba::BLACK, 30), "{:?}", img.pixel(210, 175)); + assert!(close(img.pixel(240, 155), Rgba::BLACK, 30), "{:?}", img.pixel(240, 155)); + assert!(close(img.pixel(240, 180), WHITE, 10), "{:?}", img.pixel(240, 180)); + assert!(close(img.pixel(205, 140), WHITE, 10), "nothing above the half height {:?}", img.pixel(205, 140)); + // Anchored bottom-right and half as big again: it grows up and to the left only. + let fx = deckcraft_model::Effects { outer_shadow: Some(shadow(1.5, 1.5, 0.0, "br")), ..Default::default() }; + let img = render(vec![with_effects(solid(RED), no_line(), fx)]); + assert!(close(img.pixel(70, 70), Rgba::BLACK, 30) && close(img.pixel(210, 210), WHITE, 10)); +}