From 05fdd15b45cfe0a0bd62c1d4c7dfc6841afc13fe Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Tue, 13 Oct 2026 01:52:47 +0200 Subject: [PATCH] Render: effect rasters drawn after a pattern fill keep their place and size fx.rs draws blur, shadow and glow rasters as image paints that map 1:1 onto their rect, but only set the geometry transform: the paint transform a pattern fill (or a radial or freeform gradient) left on the context still applied, so in supersampled renders (raster exports) the effect came out tiny and displaced. Both draws now reset the paint transform first. Fixes #1536 --- crates/render/src/fx.rs | 4 +++ crates/render/src/pattern.rs | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 248d9a34..7cefd6ee 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -274,6 +274,9 @@ fn draw_scaled(ctx: &mut RenderContext, image: std::sync::Arc // On whole pixels at full size the cheapest sampler is exact. let quality = if k < 1.0 { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low }; let sampler = peniko::ImageSampler { quality, ..Default::default() }; + // The raster maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern, + // radial or freeform gradient) would resample it (#1536). + ctx.reset_paint_transform(); ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler }); ctx.fill_rect(&Rect::new(0.0, 0.0, w, h)); ctx.set_transform(Affine::IDENTITY); @@ -617,6 +620,7 @@ impl Renderer { ctx.set_transform(Affine::translate((x, y))); ctx.set_blend_mode(blend_mode(mode)); let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Low, ..Default::default() }; + ctx.reset_paint_transform(); ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler }); ctx.fill_rect(&Rect::new(0.0, 0.0, w, h)); ctx.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal)); diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 001d2466..e0275aee 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -274,6 +274,58 @@ mod tests { p[0] > 200 && p[1] > 200 && p[2] > 200 } + /// #1536: a raster effect drawn after a pattern fill is the same as without the pattern (the + /// pattern's paint transform must not resample the effect's raster). + #[test] + fn effects_after_a_pattern_fill_keep_their_place_and_size() { + let fx = |id: &str, params: serde_json::Value| vectorcraft_doc::Effect { id: id.into(), params, visible: true }; + for effect in [ + fx("blur.gaussian", serde_json::json!({"radius": 4.0})), + fx("stylize.dropShadow", serde_json::json!({"x": 6, "y": 6, "blur": 3, "opacity": 100})), + ] { + let make = |with_pattern: bool| { + let mut d = doc_with_pattern(TileType::Grid); + let l = d.layers[0].id; + let filled = d.children(Some(l)).unwrap()[0].id; + d.remove(filled).unwrap(); + if with_pattern { + let id = d.alloc_id(); + let n = Node::path( + id, + shapes::rectangle(Rect::new(0.0, 0.0, 80.0, 80.0)), + Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0), + ); + d.insert(Some(l), 0, n).unwrap(); + } + let id = d.alloc_id(); + let mut n = Node::path( + id, + shapes::ellipse(Rect::new(120.0, 110.0, 180.0, 170.0)), + Appearance::basic(Paint::solid(Color::rgb(0.5, 0.5, 0.5)), Paint::None, 0.0), + ); + n.appearance.effects.push(effect.clone()); + let at = d.children(Some(l)).map_or(0, Vec::len); + d.insert(Some(l), at, n).unwrap(); + d + }; + // Supersampled, as raster exports draw. + let one = |d: &Document| { + let mut r = Renderer::new(); + r.render( + d, + 200, + 200, + Affine::IDENTITY, + &RenderOptions { background: Some([255, 255, 255, 255]), supersample: true, ..Default::default() }, + ) + }; + let (with, without) = (one(&make(true)), one(&make(false))); + for (x, y) in [(150, 140), (125, 140), (176, 160), (150, 172), (184, 176)] { + assert_eq!(with.pixel(x, y), without.pixel(x, y), "{} at ({x}, {y})", effect.id); + } + } + } + #[test] fn pattern_fill_is_periodic() { let d = doc_with_pattern(TileType::Grid);