Render: gradients with rotWithShape=0 keep to the slide's axes

A gradient fill marked not to rotate with its shape turned and flipped
with it anyway. PowerPoint lays such a gradient out upright, over the box
the rotated or flipped shape covers on the slide (a 90 degree gradient on
a quarter-turned triangle still runs top to bottom). Linear and radial
gradients now do; rect and outline path gradients still follow the shape.

Wild synthetic set: gradients on rotated/flipped shapes 5.48 -> 1.34,
gradient angles slide 6.75 -> 1.95.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 07:13:34 -04:00
parent fa1db7809a
commit bbba07cb39
2 changed files with 42 additions and 0 deletions

View File

@ -35,6 +35,18 @@ pub fn fill_path(
if g.stops.is_empty() {
return;
}
// `rotWithShape="0"`: the gradient keeps to the slide's axes when the shape is
// rotated or flipped, laid out over the box the shape covers on the slide.
let c = m.as_coeffs();
let turned = c[1].abs() > 1e-9 || c[2].abs() > 1e-9 || c[0] < 0.0 || c[3] < 0.0;
let path_kind = matches!(&g.shape, GradientShape::Path { path: k, .. } if matches!(k.as_str(), "rect" | "shape"));
if !g.rotate_with_shape && turned && !path_kind && m.determinant().abs() > 1e-12 {
let on_slide = m.transform_rect_bbox(bounds);
let upright = deckcraft_model::style::Gradient { rotate_with_shape: true, ..g.clone() };
let paint_path = m * path.clone();
fill_path(ctx, rctx, &Fill::Gradient(upright), ph, &paint_path, on_slide, Affine::IDENTITY, mode, alpha, pres);
return;
}
// A linear gradient spans its tile rectangle (the shape's box unless `tileRect` moves
// its edges); a radial one ends on the circle around it, from a focus placed in the
// shape's own box (PowerPoint's "from corner" presets centre the tile on the corner).

View File

@ -632,3 +632,33 @@ fn a_large_pictures_shadow_is_whole() {
}
assert!(counts[1] > 3000 && (counts[0] as i64 - counts[1] as i64).abs() < 300, "{counts:?}");
}
#[test]
fn a_gradient_not_rotating_with_its_shape_keeps_to_the_slide() {
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
let blue = Rgba::rgb(0, 0, 255);
for rotate_with_shape in [true, false] {
let g = Gradient {
stops: vec![GradientStop { pos: 0.0, color: ColorRef::rgb(RED) }, GradientStop { pos: 1.0, color: ColorRef::rgb(blue) }],
shape: GradientShape::Linear { angle: 0.0, scaled: false },
rotate_with_shape,
tile: [0.0; 4],
};
// 200 × 100 turned a quarter: it covers 150..250 across and 50..250 down.
let sh = Shape {
id: ShapeId(28),
xfrm: Some(Xfrm { rot: 90.0, ..Xfrm::new(100.0, 100.0, 200.0, 100.0) }),
fill: Some(Fill::Gradient(g)),
line: no_line(),
..Default::default()
};
let img = render(vec![sh]);
let (left, top) = (img.pixel(152, 150), img.pixel(200, 52));
if rotate_with_shape {
assert!(top[0] > 200 && top[2] < 60, "turned with the shape: red at the top {top:?}");
} else {
assert!(left[0] > 200 && left[2] < 60, "upright: red at the left {left:?}");
assert!(img.pixel(248, 150)[2] > 200, "blue at the right");
}
}
}