mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-13 23:08:23 +00:00
Render: a group's outer shadow is cast by its children
An outer shadow set on a group (grpSpPr effectLst) was ignored. PowerPoint casts it from everything in the group: the children are now drawn once more under the shadow's offset, scale and skew (anchored on the group's box), recoloured to the shadow colour and blurred, below the group. Wild synthetic set: combined-effects slide (shadowed group) 2.00 -> 0.98. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
aafce0d2ef
commit
84e6c12543
2 files changed
+51
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user