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);