diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 415ec36..8a00a19 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -788,6 +788,33 @@ impl Renderer { Some(fl) => Some((fl.clone(), m, Rect::new(0.0, 0.0, x.w, x.h))), }; let outer = std::mem::replace(&mut self.group_fill, own); + // A group's outer shadow is cast by everything in it, as PowerPoint draws it: the + // children drawn once more under the shadow's offset, scale and skew, recoloured to + // the shadow colour and blurred, below the group. + if let Some(sh) = s.effects.as_ref().and_then(|e| e.outer_shadow.as_ref()) { + let c = rctx.color(&sh.color, None); + let k = scale_of(m); + let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist; + let local = shadow_local(sh, x.w, x.h); + let (own, pre) = + if sh.rotate_with_shape { (Affine::translate(d) * local, Affine::IDENTITY) } else { (local, Affine::translate(d * k)) }; + let det = m.determinant(); + if det.is_finite() && det.abs() > 1e-12 { + let shift = pre * m * own * m.inverse(); + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, None, Some((c.a as f32) / 255.0), None, Some(blur(sh.blur / 3.0 * k))); + for ch in children { + self.shape(ctx, f, rctx, ch, shift * gm, depth + 1); + } + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn)), None, None, None); + ctx.set_paint(color(c.with_alpha(255), 1.0)); + let (w, h) = (f.opts.scale * f.pres.slide_size.width, f.opts.scale * f.pres.slide_size.height); + ctx.fill_rect(&Rect::new(-w, -h, 3.0 * w, 3.0 * h).inflate(1e4, 1e4)); + ctx.pop_layer(); + ctx.pop_layer(); + } + } for c in children { self.shape(ctx, f, rctx, c, gm, depth + 1); } diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index b87fd45..1611302 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -511,3 +511,27 @@ fn gradient_lines_and_their_heads_take_the_gradient() { let (top, bottom) = (img.pixel(200, 91), img.pixel(200, 109)); assert!(top[0] > 200 && top[2] < 60 && bottom[2] > 200 && bottom[0] < 60, "{top:?} {bottom:?}"); } + +#[test] +fn a_groups_shadow_is_cast_by_its_children() { + let child = |id: u32, x: f64| Shape { + id: ShapeId(id), + xfrm: Some(Xfrm::new(x, 0.0, 80.0, 100.0)), + fill: solid(RED), + line: no_line(), + ..Default::default() + }; + let g = Shape { + id: ShapeId(22), + xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), + kind: ShapeKind::Group { children: vec![child(23, 0.0), child(24, 120.0)], child: Xfrm::new(0.0, 0.0, 200.0, 100.0) }, + effects: Some(offset_shadow()), + ..Default::default() + }; + let img = render(vec![g]); + // 20 pt to the right of each child: the shadow; in the gap only the left child's shadow shows. + assert!(close(img.pixel(190, 150), Rgba::BLACK, 30), "{:?}", img.pixel(190, 150)); + assert!(close(img.pixel(210, 150), WHITE, 10), "{:?}", img.pixel(210, 150)); + assert!(close(img.pixel(310, 150), Rgba::BLACK, 30), "{:?}", img.pixel(310, 150)); + assert!(close(img.pixel(150, 150), RED, 10), "children drawn over it"); +}