From bbba07cb394a1f0d26b84e2318a7734369eb096a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 07:13:34 -0400 Subject: [PATCH] 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) --- crates/render/src/paint.rs | 12 ++++++++++++ crates/render/src/tests_wild.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/crates/render/src/paint.rs b/crates/render/src/paint.rs index 5ea7e3d..82976bc 100644 --- a/crates/render/src/paint.rs +++ b/crates/render/src/paint.rs @@ -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). diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 846bce6..6d09dd4 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -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"); + } + } +}