Render: perspective shadows scale and skew about their alignment point
A shadow's sx/sy/kx/ky were applied about the shape's centre with the skew unscaled, so a perspective shadow (algn=b, sy 23%, kx -20 deg) floated in the middle of the shape instead of lying along its bottom edge. As PowerPoint draws it, the shadow is skewed then scaled about the point of the box its algn names (b: middle of the bottom edge, tl: top-left...). Wild synthetic set: shadow alignment/scale/skew slide 5.57 -> 0.41, picture frames 13.71 -> 12.71. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0796325a1f
commit
b3a980dcb5
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Fill>, line: Option<Line>, 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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue