diff --git a/research/CHANGELOG.md b/research/CHANGELOG.md index 9e1dd3d1bb..c0505927bf 100644 --- a/research/CHANGELOG.md +++ b/research/CHANGELOG.md @@ -12,6 +12,10 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe This release has an [MSRV][] of 1.89. +### Changed + +- Breaking change: `Scene::draw_blurred_rounded_rect_in` now has an `invert` parameter for painting the inverse (`1 - alpha`) of the blur coverage, enabling inset box shadows. Use `false` to preserve the previous behavior. `vello_encoding::Encoding::encode_blurred_rounded_rect` has the same new parameter. ([#1718][] by [@nicoburns][]) + ## [0.11.0][] - 2026-10-02 This release has an [MSRV][] of 1.89. @@ -478,6 +482,7 @@ This release has an [MSRV][] of 1.75. [#1638]: https://github.com/linebender/vello/pull/1638 [#1643]: https://github.com/linebender/vello/pull/1643 [#1700]: https://github.com/linebender/vello/pull/1700 +[#1718]: https://github.com/linebender/vello/pull/1718 [#1774]: https://github.com/linebender/vello/pull/1774 [#1777]: https://github.com/linebender/vello/pull/1777 [#1847]: https://github.com/linebender/vello/pull/1847 diff --git a/research/examples/scenes/src/test_scenes.rs b/research/examples/scenes/src/test_scenes.rs index 6dfa1fecfe..ac83adf8c3 100644 --- a/research/examples/scenes/src/test_scenes.rs +++ b/research/examples/scenes/src/test_scenes.rs @@ -2048,6 +2048,7 @@ mod impls { palette::css::BLACK, radius, std_dev, + false, ); } diff --git a/research/vello_encoding/src/draw.rs b/research/vello_encoding/src/draw.rs index 024324f332..82fb751069 100644 --- a/research/vello_encoding/src/draw.rs +++ b/research/vello_encoding/src/draw.rs @@ -182,6 +182,14 @@ pub struct DrawBlurRoundedRect { /// Rectangle corner radius. pub radius: f32, /// Standard deviation of gaussian filter. + /// + /// The magnitude is the standard deviation; the sign bit encodes whether to paint the + /// inverse (`1 - alpha`) of the blur coverage (sign bit clear = normal, set = inverse). + /// Note that this means that an inverse blur with a standard deviation of zero is + /// encoded as `-0.0`. + /// + /// [`Encoding::encode_blurred_rounded_rect`](crate::Encoding::encode_blurred_rounded_rect) + /// produces this encoding from a standard deviation and an `invert` flag. pub std_dev: f32, } diff --git a/research/vello_encoding/src/encoding.rs b/research/vello_encoding/src/encoding.rs index df1b00db59..9cce0c9fdb 100644 --- a/research/vello_encoding/src/encoding.rs +++ b/research/vello_encoding/src/encoding.rs @@ -456,7 +456,11 @@ impl Encoding { }))); } - // Encodes a blurred rounded rectangle brush. + /// Encodes a blurred rounded rectangle brush. + /// + /// Negative values of `std_dev` are treated as zero. + /// + /// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted. pub fn encode_blurred_rounded_rect( &mut self, color: impl Into, @@ -464,7 +468,17 @@ impl Encoding { height: f32, radius: f32, std_dev: f32, + invert: bool, ) { + // A standard deviation is a magnitude, so negative (and NaN) values are clamped to + // zero. That leaves the sign bit of the encoded `std_dev` free to carry the `invert` + // flag (see `DrawBlurRoundedRect::std_dev`). This also works for a standard deviation + // of zero, as `-0.0` has its sign bit set. + let std_dev = if invert { + -std_dev.max(0.0) + } else { + std_dev.max(0.0) + }; self.draw_tags.push(DrawTag::BLUR_RECT); self.draw_data .extend_from_slice(bytemuck::cast_slice(bytemuck::bytes_of( diff --git a/research/vello_research/src/scene.rs b/research/vello_research/src/scene.rs index a6a91e543d..e864a08b55 100644 --- a/research/vello_research/src/scene.rs +++ b/research/vello_research/src/scene.rs @@ -254,19 +254,34 @@ impl Scene { let kernel_size = 2.5 * std_dev; let shape: Rect = rect.inflate(kernel_size, kernel_size); - self.draw_blurred_rounded_rect_in(&shape, transform, rect, brush, radius, std_dev); + self.draw_blurred_rounded_rect_in(&shape, transform, rect, brush, radius, std_dev, false); } /// Draw a rounded rectangle blurred with a gaussian filter in `shape`. /// - /// For performance reasons, `shape` should not extend more than approximately 2.5 times - /// `std_dev` away from the edges of `rect` (as any such points will not be perceptably painted to, - /// but calculations will still be performed for them). - /// /// This method effectively draws the blurred rounded rectangle clipped to the given shape. /// If just the blurred rounded rectangle is desired without clipping, /// use the simpler [`Self::draw_blurred_rounded_rect`]. /// For many users, that method will be easier to use. + /// + /// The blurred rounded rectangle is `rect` with its corners rounded by `radius`, blurred by + /// a gaussian filter with a standard deviation of `std_dev`. Negative values of `std_dev` + /// are treated as zero. + /// + /// When `invert` is `false`, `brush` is painted with the coverage (`alpha`) of the blurred + /// rounded rectangle. Points which are more than approximately 2.5 times `std_dev` outside + /// of the edges of `rect` will not be perceptibly painted to, but calculations will still + /// be performed for them. So for performance reasons, `shape` should not extend further + /// than that. + /// + /// When `invert` is `true`, the inverse (`1 - alpha`) of that coverage is painted instead. + /// The coverage then falls off towards the inside of `rect`, and is effectively full at + /// all points of `shape` which are more than approximately 2.5 times `std_dev` outside of + /// the edges of `rect`. This means that the extent of the painted area is determined only + /// by `shape`, which can extend as far beyond `rect` as is needed. + /// This can be used to implement inset box shadows, by using the box as `shape`, the box + /// shrunk by the spread of the shadow and translated by its offset as `rect`, and the + /// corner radius of the box reduced by the spread as `radius`. pub fn draw_blurred_rounded_rect_in( &mut self, shape: &impl Shape, @@ -275,6 +290,7 @@ impl Scene { brush: Color, radius: f64, std_dev: f64, + invert: bool, ) { let t = Transform::from_kurbo(&transform); self.encoding.encode_transform(t); @@ -292,6 +308,7 @@ impl Scene { rect.height() as _, radius as _, std_dev as _, + invert, ); } } diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle.png new file mode 100644 index 0000000000..73dd18a72a --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a937a5dead83bab82c853a9be00a75ca5bbf179ea1df080a1f75dc02ac51427 +size 2607 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png new file mode 100644 index 0000000000..893ea8826b --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df22126db85f500dade38161084c5f8b5e08b4ba474d686f375c18a517587a00 +size 2768 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_in_even_odd.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_even_odd.png new file mode 100644 index 0000000000..fca104fa5e --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_in_even_odd.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06f7b951db2950df1154f9b7587cf80f75bea91b38446c29e71f7358118f079 +size 3441 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_large_std_dev.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_large_std_dev.png new file mode 100644 index 0000000000..e13d466b06 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_large_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec43e82de3f38889a01ab6eaec3fdb152e950892bf462b7264a99331f9b4eea5 +size 2820 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_medium_std_dev.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_medium_std_dev.png new file mode 100644 index 0000000000..d4903bc380 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_medium_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bec461fc08f8174db2b2417b562386e65de73aeed0ba6dcb48600455d10929f9 +size 2117 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_none.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_none.png new file mode 100644 index 0000000000..a37827625c --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:265e6e70abc4c1cd42c726e1ad7233d3ef4ff5a0fb0e7cf8404a3532959a1477 +size 155 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_small_std_dev.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_small_std_dev.png new file mode 100644 index 0000000000..92f9bb7ebd --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_small_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf38af05fedb9b9c8027b04e4b23bd2b50f4d6329ee68bb0fdf6b9a3fbc819a7 +size 1037 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_with_large_radius.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_large_radius.png new file mode 100644 index 0000000000..54380b0eff --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_large_radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2f4e55fa1de3649f2cf5718c2be5d0a39c2317b4a6030d78668be0bc82a40c9 +size 2930 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_with_radius.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_radius.png new file mode 100644 index 0000000000..bbf1dd59c5 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb2545018b02f33744ca90230022fc07953a821ad3ce5cf0760842d9f0318fe8 +size 2303 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_with_transform.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_transform.png new file mode 100644 index 0000000000..b70ab7b521 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7273708ee0b981fbe3b3ec5d655cc6293d36d75d4f194549cf7e2d4a06543479 +size 2420 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_zero.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_zero.png new file mode 100644 index 0000000000..dcbd9aece8 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_zero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c08e4d48bc0ebe9b571c8700acbf10161428276e2c883321660acf96187ec261 +size 123 diff --git a/research/vello_research_tests/snapshots/blurred_rounded_rect_zero_with_radius.png b/research/vello_research_tests/snapshots/blurred_rounded_rect_zero_with_radius.png new file mode 100644 index 0000000000..9381e7a6a0 --- /dev/null +++ b/research/vello_research_tests/snapshots/blurred_rounded_rect_zero_with_radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:523ee2b3ed460f3260788b221bc1b2131e76460508595c385686df8cefe3c7ee +size 167 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_offset_down_right.png b/research/vello_research_tests/snapshots/inset_box_shadow_offset_down_right.png new file mode 100644 index 0000000000..95a49864f2 --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_offset_down_right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:702ef507cec76a64ddeb0c45217d0928a5889f7a695cd8b33ba6e4e8d8c50c54 +size 1283 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_offset_horizontal.png b/research/vello_research_tests/snapshots/inset_box_shadow_offset_horizontal.png new file mode 100644 index 0000000000..7f270fd653 --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_offset_horizontal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54cfe5bf76dff3d6b7f094654d44851d07e97d2145e673982ebfce875dd88064 +size 1033 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_offset_up_left.png b/research/vello_research_tests/snapshots/inset_box_shadow_offset_up_left.png new file mode 100644 index 0000000000..c3dbb48832 --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_offset_up_left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e76b77c35fc6415db8f76eb9c5644a5f70d5e41243f9f0194ca4103830f249 +size 1289 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_offset_with_transform.png b/research/vello_research_tests/snapshots/inset_box_shadow_offset_with_transform.png new file mode 100644 index 0000000000..7ea88b4f8c --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_offset_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a00d28406b5183ce67da578766158ecb447809cb2c6630a30e6cfd21d33991b6 +size 2970 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_offset_without_blur.png b/research/vello_research_tests/snapshots/inset_box_shadow_offset_without_blur.png new file mode 100644 index 0000000000..93a731ea3f --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_offset_without_blur.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec9559f02b17a5c98f929467856c405c9f03281920cfe7a5a542ac4a9ba45ca1 +size 141 diff --git a/research/vello_research_tests/snapshots/inset_box_shadow_with_spread.png b/research/vello_research_tests/snapshots/inset_box_shadow_with_spread.png new file mode 100644 index 0000000000..384597a7dc --- /dev/null +++ b/research/vello_research_tests/snapshots/inset_box_shadow_with_spread.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5a9faf37975bbe0e915876a1911235b0c3b2b786740fb88366c6b182c450f75 +size 951 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png new file mode 100644 index 0000000000..cf273a9665 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60e33de409cfc0c1ac477d48ba3121d6a20befb1a5200d60c2a02f21f331680 +size 1557 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_large_std_dev.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_large_std_dev.png new file mode 100644 index 0000000000..2484a7fd83 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_large_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b0968af49e483c5e5b08d4a85c32b1bd1deb34b52f0a803530c83d12ea193d1 +size 2863 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_medium_std_dev.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_medium_std_dev.png new file mode 100644 index 0000000000..c7dcd0a1d5 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_medium_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47c632deaa89fbd58bf67040a43c7d8571cde3dc5c93f551c00fa7ca712ea835 +size 2126 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_small_std_dev.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_small_std_dev.png new file mode 100644 index 0000000000..b674c64438 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_small_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e84be21fa52a0ae0bb1d4d330640564f4ae76eeb8870619a7bce4ff7256e169 +size 1070 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_large_radius.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_large_radius.png new file mode 100644 index 0000000000..7e7ba89da3 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_large_radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286b017c9b93f2f5ea151b76c3389c04244a680214d5f17bcaf7cc183f04c97f +size 2936 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_radius.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_radius.png new file mode 100644 index 0000000000..041e54d341 --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:562c32f6dd4d4c971ed32f3cfb072abefc9fae464d5cd5f58205a3dceba801b6 +size 2310 diff --git a/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_transform.png b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_transform.png new file mode 100644 index 0000000000..6870e6072b --- /dev/null +++ b/research/vello_research_tests/snapshots/inverse_blurred_rounded_rect_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f32a3b850b7f9d6a7225bdb1b750c6702f31ff36c84fb1c9b1ec4147ad07a52 +size 2586 diff --git a/research/vello_research_tests/tests/blurred_rounded_rect.rs b/research/vello_research_tests/tests/blurred_rounded_rect.rs new file mode 100644 index 0000000000..d0aa7f4848 --- /dev/null +++ b/research/vello_research_tests/tests/blurred_rounded_rect.rs @@ -0,0 +1,803 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Snapshot tests for the blurred rounded rectangle paint. +//! +//! When `invert` is `true`, the complement (`1 - alpha`) of the blur coverage is painted: the +//! brush is fully opaque outside the blurred rounded rectangle and fades to transparent inside it. +//! This is used to implement inset box shadows. +//! +//! These cases mirror the blurred rounded rectangle tests in the `vello_sparse_tests` crate. + +use vello::Scene; +use vello::kurbo::{Affine, BezPath, Circle, Point, Rect, RoundedRect, Shape, Vec2}; +use vello::peniko::Fill; +use vello::peniko::color::palette; +use vello_research_tests::{TestParams, snapshot_test_sync}; + +fn snapshot_blurred_rounded_rect( + name: &str, + radius: f64, + std_dev: f64, + affine: Affine, + use_cpu: bool, +) { + let mut scene = Scene::new(); + let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + scene.draw_blurred_rounded_rect(affine, rect, palette::css::REBECCA_PURPLE, radius, std_dev); + let params = TestParams { + use_cpu, + base_color: Some(palette::css::WHITE), + ..TestParams::new(name, 100, 100) + }; + snapshot_test_sync(scene, ¶ms) + .unwrap() + .assert_mean_less_than(0.01); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_zero_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_zero", + 0.0, + 0.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_zero_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_zero", + 0.0, + 0.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_zero_with_radius_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_zero_with_radius", + 10.0, + 0.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_zero_with_radius_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_zero_with_radius", + 10.0, + 0.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_none_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_none", + 0.0, + 0.1, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_none_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_none", + 0.0, + 0.1, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_small_std_dev_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_small_std_dev", + 0.0, + 5.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_small_std_dev_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_small_std_dev", + 0.0, + 5.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_medium_std_dev_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_medium_std_dev", + 0.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_medium_std_dev_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_medium_std_dev", + 0.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_large_std_dev_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_large_std_dev", + 0.0, + 20.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_large_std_dev_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_large_std_dev", + 0.0, + 20.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_radius_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_radius", + 10.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_radius_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_radius", + 10.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_large_radius_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_large_radius", + 30.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_large_radius_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_large_radius", + 30.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_transform_gpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_transform", + 10.0, + 10.0, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_with_transform_cpu() { + snapshot_blurred_rounded_rect( + "blurred_rounded_rect_with_transform", + 10.0, + 10.0, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + true, + ); +} + +fn snapshot_inverse_blurred_rounded_rect( + name: &str, + radius: f64, + std_dev: f64, + affine: Affine, + use_cpu: bool, +) { + let mut scene = Scene::new(); + let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + // Extend the shape to the blur kernel, so the fully covered area outside of `rect` is painted. + let kernel_size = 2.5 * std_dev; + scene.draw_blurred_rounded_rect_in( + &rect.inflate(kernel_size, kernel_size), + affine, + rect, + palette::css::REBECCA_PURPLE, + radius, + std_dev, + true, + ); + let params = TestParams { + use_cpu, + base_color: Some(palette::css::WHITE), + ..TestParams::new(name, 100, 100) + }; + snapshot_test_sync(scene, ¶ms) + .unwrap() + .assert_mean_less_than(0.01); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_small_std_dev_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_small_std_dev", + 0.0, + 5.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_small_std_dev_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_small_std_dev", + 0.0, + 5.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_medium_std_dev_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_medium_std_dev", + 0.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_medium_std_dev_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_medium_std_dev", + 0.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_large_std_dev_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_large_std_dev", + 0.0, + 20.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_large_std_dev_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_large_std_dev", + 0.0, + 20.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_radius_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_radius", + 10.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_radius_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_radius", + 10.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_large_radius_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_large_radius", + 30.0, + 10.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_large_radius_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_large_radius", + 30.0, + 10.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_transform_gpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_transform", + 10.0, + 10.0, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_with_transform_cpu() { + snapshot_inverse_blurred_rounded_rect( + "inverse_blurred_rounded_rect_with_transform", + 10.0, + 10.0, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + true, + ); +} + +fn snapshot_blurred_rounded_rect_in( + name: &str, + path: &impl Shape, + radius: f64, + std_dev: f64, + invert: bool, + affine: Affine, + use_cpu: bool, +) { + let mut scene = Scene::new(); + let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + scene.draw_blurred_rounded_rect_in( + path, + affine, + rect, + palette::css::REBECCA_PURPLE, + radius, + std_dev, + invert, + ); + let params = TestParams { + use_cpu, + base_color: Some(palette::css::WHITE), + ..TestParams::new(name, 100, 100) + }; + snapshot_test_sync(scene, ¶ms) + .unwrap() + .assert_mean_less_than(0.01); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_circle_gpu() { + let path = Circle::new(Point::new(50.0, 50.0), 45.0); + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_circle", + &path, + 10.0, + 10.0, + false, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_circle_cpu() { + let path = Circle::new(Point::new(50.0, 50.0), 45.0); + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_circle", + &path, + 10.0, + 10.0, + false, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_circle_with_transform_gpu() { + let path = Circle::new(Point::new(50.0, 50.0), 45.0); + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_circle_with_transform", + &path, + 10.0, + 10.0, + false, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_circle_with_transform_cpu() { + let path = Circle::new(Point::new(50.0, 50.0), 45.0); + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_circle_with_transform", + &path, + 10.0, + 10.0, + false, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_in_rect_gpu() { + let path = Rect::new(20.0, 20.0, 80.0, 80.0); + snapshot_blurred_rounded_rect_in( + "inverse_blurred_rounded_rect_in_rect", + &path, + 10.0, + 10.0, + true, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inverse_blurred_rounded_rect_in_rect_cpu() { + let path = Rect::new(20.0, 20.0, 80.0, 80.0); + snapshot_blurred_rounded_rect_in( + "inverse_blurred_rounded_rect_in_rect", + &path, + 10.0, + 10.0, + true, + Affine::IDENTITY, + true, + ); +} + +/// Two overlapping circles with opposite winding directions. +/// +/// `draw_blurred_rounded_rect_in` always fills `shape` with the non-zero fill rule, so the +/// overlapping region of the two circles (which has a winding number of zero) is not painted. +/// This gives the same coverage as the even-odd case in the `vello_tests` crate. +fn opposing_circles() -> BezPath { + let mut path = Circle::new(Point::new(35.0, 50.0), 40.0).to_path(0.1); + path.extend( + Circle::new(Point::new(65.0, 50.0), 40.0) + .to_path(0.1) + .reverse_subpaths(), + ); + path +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_even_odd_gpu() { + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_even_odd", + &opposing_circles(), + 10.0, + 10.0, + false, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_in_even_odd_cpu() { + snapshot_blurred_rounded_rect_in( + "blurred_rounded_rect_in_even_odd", + &opposing_circles(), + 10.0, + 10.0, + false, + Affine::IDENTITY, + true, + ); +} + +/// Emulate a CSS inset box shadow: the inverse blurred rounded rectangle is shrunk by the spread +/// and offset relative to the border box it is painted into, and clipped to that border box. +fn snapshot_inset_box_shadow( + name: &str, + offset: Vec2, + spread: f64, + radius: f64, + std_dev: f64, + affine: Affine, + use_cpu: bool, +) { + let mut scene = Scene::new(); + let border_box = Rect::new(20.0, 20.0, 80.0, 80.0); + let shape = RoundedRect::from_rect(border_box, radius); + + scene.fill( + Fill::NonZero, + affine, + palette::css::PALE_GOLDENROD, + None, + &shape, + ); + scene.draw_blurred_rounded_rect_in( + &shape, + affine, + border_box.inflate(-spread, -spread) + offset, + palette::css::REBECCA_PURPLE, + (radius - spread).max(0.0), + std_dev, + true, + ); + + let params = TestParams { + use_cpu, + base_color: Some(palette::css::WHITE), + ..TestParams::new(name, 100, 100) + }; + snapshot_test_sync(scene, ¶ms) + .unwrap() + .assert_mean_less_than(0.01); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_down_right_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_down_right", + Vec2::new(8.0, 8.0), + 0.0, + 10.0, + 6.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_down_right_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_down_right", + Vec2::new(8.0, 8.0), + 0.0, + 10.0, + 6.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_up_left_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_up_left", + Vec2::new(-8.0, -8.0), + 0.0, + 10.0, + 6.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_up_left_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_up_left", + Vec2::new(-8.0, -8.0), + 0.0, + 10.0, + 6.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_horizontal_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_horizontal", + Vec2::new(12.0, 0.0), + 0.0, + 10.0, + 4.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_horizontal_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_horizontal", + Vec2::new(12.0, 0.0), + 0.0, + 10.0, + 4.0, + Affine::IDENTITY, + true, + ); +} + +/// An offset shadow larger than the blur, so the shadow has a hard edge and only covers the +/// top and left of the border box. +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_without_blur_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_without_blur", + Vec2::new(15.0, 15.0), + 0.0, + 0.0, + 0.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_without_blur_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_without_blur", + Vec2::new(15.0, 15.0), + 0.0, + 0.0, + 0.0, + Affine::IDENTITY, + true, + ); +} + +/// A spread shrinks the shadow rectangle (and its corner radius), so the shadow also covers the +/// bottom and right of the border box despite the offset. +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_with_spread_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_with_spread", + Vec2::new(4.0, 4.0), + 8.0, + 10.0, + 4.0, + Affine::IDENTITY, + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_with_spread_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_with_spread", + Vec2::new(4.0, 4.0), + 8.0, + 10.0, + 4.0, + Affine::IDENTITY, + true, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_with_transform_gpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_with_transform", + Vec2::new(8.0, 8.0), + 0.0, + 10.0, + 6.0, + Affine::rotate_about(20.0_f64.to_radians(), Point::new(50.0, 50.0)), + false, + ); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn inset_box_shadow_offset_with_transform_cpu() { + snapshot_inset_box_shadow( + "inset_box_shadow_offset_with_transform", + Vec2::new(8.0, 8.0), + 0.0, + 10.0, + 6.0, + Affine::rotate_about(20.0_f64.to_radians(), Point::new(50.0, 50.0)), + true, + ); +} diff --git a/research/vello_research_tests/tests/property.rs b/research/vello_research_tests/tests/property.rs index 0cb6205750..8d8cdf78e7 100644 --- a/research/vello_research_tests/tests/property.rs +++ b/research/vello_research_tests/tests/property.rs @@ -104,6 +104,84 @@ fn empty_scene_cpu() { empty_scene(true); } +/// Render a blurred rounded rectangle in both the normal and invert modes and check that the +/// invert mode paints the complementary (`1 - alpha`) coverage. +fn blurred_rounded_rect_invert(use_cpu: bool) { + const SIZE: u32 = 100; + let rect = Rect::from_center_size((50., 50.), (40., 40.)); + let radius = 0.0; + let std_dev = 5.0; + + // Paint the whole canvas, so that the coverage can be compared at any point. + let shape = Rect::new(0., 0., f64::from(SIZE), f64::from(SIZE)); + + let render = |invert: bool| { + let mut scene = Scene::new(); + scene.draw_blurred_rounded_rect_in( + &shape, + Affine::IDENTITY, + rect, + palette::css::BLACK, + radius, + std_dev, + invert, + ); + let params = TestParams { + use_cpu, + base_color: Some(TRANSPARENT), + ..TestParams::new("blurred_rounded_rect_invert", SIZE, SIZE) + }; + vello_research_tests::render_then_debug_sync(&scene, ¶ms).unwrap() + }; + + let normal = render(false); + let invert = render(true); + assert_eq!(normal.format, ImageFormat::Rgba8); + assert_eq!(invert.format, ImageFormat::Rgba8); + + // The brush is opaque black on a transparent background, so the rendered alpha channel is + // exactly the blur coverage. + let alpha_at = |image: &ImageData, x: u32, y: u32| -> u8 { + let idx = ((y * SIZE + x) * 4 + 3) as usize; + image.data.data()[idx] + }; + + // Deep inside the rectangle the (non-invert) blur is essentially fully covered, while the + // invert blur is essentially fully transparent. + assert!( + alpha_at(&normal, 50, 50) > 250, + "expected near-opaque center, got {}", + alpha_at(&normal, 50, 50) + ); + assert!( + alpha_at(&invert, 50, 50) < 5, + "expected near-transparent center, got {}", + alpha_at(&invert, 50, 50) + ); + + // At every point within the painted shape, the invert coverage is the complement of the + // normal coverage, including at the partially-covered edges of the rectangle. + for &(x, y) in &[(50, 50), (30, 50), (50, 30), (25, 50), (50, 70), (35, 65)] { + let sum = u16::from(alpha_at(&normal, x, y)) + u16::from(alpha_at(&invert, x, y)); + assert!( + sum.abs_diff(255) <= 4, + "normal + invert coverage should sum to ~255 at ({x}, {y}), got {sum}" + ); + } +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_invert_gpu() { + blurred_rounded_rect_invert(false); +} + +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn blurred_rounded_rect_invert_cpu() { + blurred_rounded_rect_invert(true); +} + #[test] #[cfg_attr(skip_gpu_tests, ignore)] fn bgra_image() { diff --git a/research/vello_shaders/shader/fine.wgsl b/research/vello_shaders/shader/fine.wgsl index 55f615de25..1ed4ce0a48 100644 --- a/research/vello_shaders/shader/fine.wgsl +++ b/research/vello_shaders/shader/fine.wgsl @@ -750,9 +750,15 @@ fn read_blur_rect(cmd_ix: u32) -> CmdBlurRect { let width = bitcast(info[info_offset + 6u]); let height = bitcast(info[info_offset + 7u]); let radius = bitcast(info[info_offset + 8u]); - let std_dev = bitcast(info[info_offset + 9u]); - - return CmdBlurRect(rgba_color, matrx, xlat, width, height, radius, std_dev); + // The encoded `std_dev` carries two values: its magnitude is the standard deviation, and + // its sign bit is the invert flag (see `DrawBlurRoundedRect` in `vello_encoding`). + // The flag is read from the raw bits rather than by comparing against zero, as an inverse + // blur with a standard deviation of zero is encoded as `-0.0`. + let std_dev_bits = info[info_offset + 9u]; + let std_dev = abs(bitcast(std_dev_bits)); + let invert = std_dev_bits >> 31u; + + return CmdBlurRect(rgba_color, matrx, xlat, width, height, radius, std_dev, invert); } fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { @@ -1214,7 +1220,10 @@ fn main( let d_pos = pow(pow(x1, exponent) + pow(y1, exponent), inv_exponent); let d_neg = min(max(x0, y0), 0.0); let d = d_pos + d_neg - r1; - let alpha = scale * (erf7(inv_std_dev * (min_edge + d)) - erf7(inv_std_dev * d)); + let blur_coverage = scale * (erf7(inv_std_dev * (min_edge + d)) - erf7(inv_std_dev * d)); + // When `invert` is set, paint the inverse (1 - alpha) coverage, used to + // implement inset box shadows. + let alpha = select(blur_coverage, 1.0 - blur_coverage, blur.invert != 0u); let fg_rgba = blur_rgba * alpha; let fg_i = fg_rgba * area[i]; diff --git a/research/vello_shaders/shader/shared/ptcl.wgsl b/research/vello_shaders/shader/shared/ptcl.wgsl index 487c3496dd..d3c60b417b 100644 --- a/research/vello_shaders/shader/shared/ptcl.wgsl +++ b/research/vello_shaders/shader/shared/ptcl.wgsl @@ -62,6 +62,10 @@ struct CmdBlurRect { // Gaussian filter standard deviation std_dev: f32, + + // Whether to paint the inverse (1 - alpha) of the blur coverage. + // Unpacked from the sign bit of the encoded standard deviation. + invert: u32, } struct CmdLinGrad { diff --git a/vello_cpu/CHANGELOG.md b/vello_cpu/CHANGELOG.md index 3bf66033c1..f2b1155c92 100644 --- a/vello_cpu/CHANGELOG.md +++ b/vello_cpu/CHANGELOG.md @@ -12,6 +12,14 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe This release has an [MSRV][] of 1.89. +### Added + +- `RenderContext::fill_blurred_rounded_rect_in` for painting a blurred rounded rectangle inside an arbitrary path, with an `invert` parameter for painting the inverse (`1 - alpha`) of the blur coverage, enabling inset box shadows. ([#1718][] by [@nicoburns][]) + +### Changed + +- Breaking change: The `invert` parameter of `RenderContext::fill_blurred_rounded_rect` has been removed. Use `RenderContext::fill_blurred_rounded_rect_in` with `invert` set to `true` instead. ([#1718][] by [@nicoburns][]) + ## [0.3.0][] - 2026-10-02 This release has an [MSRV][] of 1.89. @@ -305,6 +313,7 @@ See also the [vello_common 0.0.1](../vello_common/CHANGELOG.md#001---2025-05-10) [#1703]: https://github.com/linebender/vello/pull/1703 [#1705]: https://github.com/linebender/vello/pull/1705 [#1715]: https://github.com/linebender/vello/pull/1715 +[#1718]: https://github.com/linebender/vello/pull/1718 [#1720]: https://github.com/linebender/vello/pull/1720 [#1723]: https://github.com/linebender/vello/pull/1723 [#1726]: https://github.com/linebender/vello/pull/1726 diff --git a/vello_cpu/src/render.rs b/vello_cpu/src/render.rs index ef6f07a289..c837fe2b09 100644 --- a/vello_cpu/src/render.rs +++ b/vello_cpu/src/render.rs @@ -414,32 +414,12 @@ impl RenderContext { /// Fill a blurred rectangle with the given corner radius and standard deviation. /// - /// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted: the - /// paint is fully opaque outside the blurred rectangle and fades to transparent inside it. This - /// can be used to implement inset box shadows. - /// /// Note that this only works properly if the current paint is set to a solid color. /// If not, it will fall back to using black as the fill color. - pub fn fill_blurred_rounded_rect( - &mut self, - rect: &Rect, - radius: f32, - std_dev: f32, - invert: bool, - ) { + pub fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { let rect = rect.abs(); - let color = match self.state.paint { - PaintType::Solid(s) => s, - // Fallback to black when attempting to blur a rectangle with an image/gradient paint - _ => BLACK, - }; - - let blurred_rect = BlurredRoundedRectangle { - rect, - color, - radius, - std_dev, - invert, + let Some(paint) = self.encode_blurred_rounded_rect(rect, radius, std_dev, false) else { + return; }; // The actual rectangle we paint needs to be larger so that the blurring effect @@ -451,19 +431,69 @@ impl RenderContext { let transform = self .root_transforms .effective_path_transform(self.transforms()); - let paint_transform = self - .root_transforms - .effective_paint_transform(self.transforms()); - let Some(paint) = blurred_rect.encode_into(&mut self.encoded_paints, paint_transform, None) + self.rect_to_temp_path(&inflated_rect); + + self.dispatcher.fill_path( + &self.temp_path, + Fill::NonZero, + transform, + paint, + self.state.blend_mode, + self.aliasing_threshold, + self.mask.clone(), + ); + } + + /// Fill `path` with a blurred rounded rectangle with the given corner radius and standard + /// deviation. + /// + /// This effectively draws the blurred rounded rectangle clipped to `path`. If just the blurred + /// rounded rectangle is desired without clipping, use the simpler + /// [`Self::fill_blurred_rounded_rect`], which will be easier to use for many users. + /// + /// `path` is filled using the current fill rule. + /// + /// The blurred rounded rectangle is `rect` with its corners rounded by `radius`, blurred by + /// a gaussian filter with a standard deviation of `std_dev`. Negative values of `std_dev` + /// are treated as zero. + /// + /// When `invert` is `false`, the paint is applied with the coverage (`alpha`) of the blurred + /// rounded rectangle. Points which are more than approximately 2.5 times `std_dev` outside + /// of the edges of `rect` will not be perceptibly painted to, but calculations will still + /// be performed for them. So for performance reasons, `path` should not extend further + /// than that. + /// + /// When `invert` is `true`, the inverse (`1 - alpha`) of that coverage is applied instead. + /// The coverage then falls off towards the inside of `rect`, and is effectively full at + /// all points of `path` which are more than approximately 2.5 times `std_dev` outside of + /// the edges of `rect`. This means that the extent of the painted area is determined only + /// by `path`, which can extend as far beyond `rect` as is needed. + /// This can be used to implement inset box shadows, by using the box as `path`, the box + /// shrunk by the spread of the shadow and translated by its offset as `rect`, and the + /// corner radius of the box reduced by the spread as `radius`. + /// + /// Note that this only works properly if the current paint is set to a solid color. + /// If not, it will fall back to using black as the fill color. + pub fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) { + let Some(paint) = self.encode_blurred_rounded_rect(rect.abs(), radius, std_dev, invert) else { return; }; + let transform = self + .root_transforms + .effective_path_transform(self.transforms()); - self.rect_to_temp_path(&inflated_rect); self.dispatcher.fill_path( - &self.temp_path, - Fill::NonZero, + path, + self.state.fill_rule, transform, paint, self.state.blend_mode, @@ -472,6 +502,33 @@ impl RenderContext { ); } + fn encode_blurred_rounded_rect( + &mut self, + rect: Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) -> Option { + let color = match self.state.paint { + PaintType::Solid(s) => s, + // Fallback to black when attempting to blur a rectangle with an image/gradient paint + _ => BLACK, + }; + + let blurred_rect = BlurredRoundedRectangle { + rect, + color, + radius, + std_dev, + invert, + }; + + let paint_transform = self + .root_transforms + .effective_paint_transform(self.transforms()); + blurred_rect.encode_into(&mut self.encoded_paints, paint_transform, None) + } + /// Creates a builder for drawing a run of glyphs that have the same attributes. #[cfg(feature = "text")] pub fn glyph_run<'a>( diff --git a/vello_gpu/CHANGELOG.md b/vello_gpu/CHANGELOG.md index dd553b2fb9..e888b501d0 100644 --- a/vello_gpu/CHANGELOG.md +++ b/vello_gpu/CHANGELOG.md @@ -12,6 +12,14 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe This release has an [MSRV][] of 1.89. +### Added + +- `Scene::fill_blurred_rounded_rect_in` for painting a blurred rounded rectangle inside an arbitrary path, with an `invert` parameter for painting the inverse (`1 - alpha`) of the blur coverage, enabling inset box shadows. ([#1718][] by [@nicoburns][]) + +### Changed + +- Breaking change: The `invert` parameter of `Scene::fill_blurred_rounded_rect` has been removed. Use `Scene::fill_blurred_rounded_rect_in` with `invert` set to `true` instead. ([#1718][] by [@nicoburns][]) + ## [0.3.0][] - 2026-10-02 This release has an [MSRV][] of 1.89. @@ -338,6 +346,7 @@ See also the [vello_cpu 0.0.4](../vello_cpu/CHANGELOG.md#004---2025-10-17) and [ [#1691]: https://github.com/linebender/vello/pull/1691 [#1697]: https://github.com/linebender/vello/pull/1697 [#1715]: https://github.com/linebender/vello/pull/1715 +[#1718]: https://github.com/linebender/vello/pull/1718 [#1720]: https://github.com/linebender/vello/pull/1720 [#1726]: https://github.com/linebender/vello/pull/1726 [#1734]: https://github.com/linebender/vello/pull/1734 diff --git a/vello_gpu/src/scene.rs b/vello_gpu/src/scene.rs index 10108b1216..907308893c 100644 --- a/vello_gpu/src/scene.rs +++ b/vello_gpu/src/scene.rs @@ -541,44 +541,21 @@ impl Scene { /// Fill a blurred rectangle with the given corner radius and standard deviation. /// - /// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted: the - /// paint is fully opaque outside the blurred rectangle and fades to transparent inside it. This - /// can be used to implement inset box shadows. - /// /// This operation uses the current transform and paint transform. Like Vello CPU, it only /// uses solid paints; non-solid paints fall back to black. - pub fn fill_blurred_rounded_rect( - &mut self, - rect: &Rect, - radius: f32, - std_dev: f32, - invert: bool, - ) { + pub fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { if !self.paint_transform_has_area() { return; } self.with_optional_filter_or_blend_layer(|ctx| { let rect = rect.abs(); - let color = match ctx.render_state.paint { - PaintType::Solid(s) => s, - _ => BLACK, - }; - let blurred_rect = BlurredRoundedRectangle { - rect, - color, - radius, - std_dev, - invert, + let Some(paint) = ctx.encode_blurred_rounded_rect(rect, radius, std_dev, false) else { + return; }; let kernel_size = 2.5 * std_dev; let inflated_rect = rect.inflate(f64::from(kernel_size), f64::from(kernel_size)); - let transform = ctx.effective_paint_transform(); - let Some(paint) = blurred_rect.encode_into(&mut ctx.encoded_paints, transform, None) - else { - return; - }; if let Some(bounds) = ctx.fast_rect_bounds(&inflated_rect) { ctx.recorder @@ -609,6 +586,86 @@ impl Scene { }); } + /// Fill `path` with a blurred rounded rectangle with the given corner radius and standard + /// deviation. + /// + /// This effectively draws the blurred rounded rectangle clipped to `path`. If just the blurred + /// rounded rectangle is desired without clipping, use the simpler + /// [`Self::fill_blurred_rounded_rect`], which will be easier to use for many users. + /// + /// `path` is filled using the current fill rule. + /// + /// The blurred rounded rectangle is `rect` with its corners rounded by `radius`, blurred by + /// a gaussian filter with a standard deviation of `std_dev`. Negative values of `std_dev` + /// are treated as zero. + /// + /// When `invert` is `false`, the paint is applied with the coverage (`alpha`) of the blurred + /// rounded rectangle. Points which are more than approximately 2.5 times `std_dev` outside + /// of the edges of `rect` will not be perceptibly painted to, but calculations will still + /// be performed for them. So for performance reasons, `path` should not extend further + /// than that. + /// + /// When `invert` is `true`, the inverse (`1 - alpha`) of that coverage is applied instead. + /// The coverage then falls off towards the inside of `rect`, and is effectively full at + /// all points of `path` which are more than approximately 2.5 times `std_dev` outside of + /// the edges of `rect`. This means that the extent of the painted area is determined only + /// by `path`, which can extend as far beyond `rect` as is needed. + /// This can be used to implement inset box shadows, by using the box as `path`, the box + /// shrunk by the spread of the shadow and translated by its offset as `rect`, and the + /// corner radius of the box reduced by the spread as `radius`. + /// + /// This operation uses the current transform and paint transform. Like Vello CPU, it only + /// uses solid paints; non-solid paints fall back to black. + pub fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) { + if !self.paint_transform_has_area() { + return; + } + + self.with_optional_filter_or_blend_layer(|ctx| { + let Some(paint) = ctx.encode_blurred_rounded_rect(rect.abs(), radius, std_dev, invert) + else { + return; + }; + ctx.fill_path_with( + path, + ctx.effective_path_transform(), + ctx.render_state.fill_rule, + paint, + ctx.aliasing_threshold, + ); + }); + } + + fn encode_blurred_rounded_rect( + &mut self, + rect: Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) -> Option { + let color = match self.render_state.paint { + PaintType::Solid(s) => s, + _ => BLACK, + }; + let blurred_rect = BlurredRoundedRectangle { + rect, + color, + radius, + std_dev, + invert, + }; + + let transform = self.effective_paint_transform(); + blurred_rect.encode_into(&mut self.encoded_paints, transform, None) + } + /// Creates a builder for drawing a run of glyphs that have the same attributes. #[cfg(feature = "text")] pub fn glyph_run<'a>( diff --git a/vello_tests/snapshots/blurred_rounded_rect_in_circle.png b/vello_tests/snapshots/blurred_rounded_rect_in_circle.png new file mode 100644 index 0000000000..0663e44b2b --- /dev/null +++ b/vello_tests/snapshots/blurred_rounded_rect_in_circle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a2c4c64f90b1890b7e3b0fd134a3f0eb573d17fcfdd27e1fec8437d7225b85d +size 2273 diff --git a/vello_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png b/vello_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png new file mode 100644 index 0000000000..3625823b8a --- /dev/null +++ b/vello_tests/snapshots/blurred_rounded_rect_in_circle_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf8d61e6156f1bd809a81fe8bc457f29ec22b43ad34be037cd2929164aa9ff18 +size 2290 diff --git a/vello_tests/snapshots/blurred_rounded_rect_in_even_odd.png b/vello_tests/snapshots/blurred_rounded_rect_in_even_odd.png new file mode 100644 index 0000000000..e4ccb68869 --- /dev/null +++ b/vello_tests/snapshots/blurred_rounded_rect_in_even_odd.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccde7b922784c568e60e93d9960e91bbdb0aaede60de14e98ff068289ca0926d +size 2643 diff --git a/vello_tests/snapshots/inset_box_shadow_offset_down_right.png b/vello_tests/snapshots/inset_box_shadow_offset_down_right.png new file mode 100644 index 0000000000..01bc1d2d36 --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_offset_down_right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde45215207bef5094e9e865cb16618156472d2f6496c2ac976aa0b6885aa43d +size 1270 diff --git a/vello_tests/snapshots/inset_box_shadow_offset_horizontal.png b/vello_tests/snapshots/inset_box_shadow_offset_horizontal.png new file mode 100644 index 0000000000..30423aad5a --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_offset_horizontal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:918faf282c9bd7b57c9b173692f53a6bfc5741d4fe8ddbeca6352b25a17eeba5 +size 914 diff --git a/vello_tests/snapshots/inset_box_shadow_offset_up_left.png b/vello_tests/snapshots/inset_box_shadow_offset_up_left.png new file mode 100644 index 0000000000..5935357460 --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_offset_up_left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a462ad0ccc48e761978807680f09c40ce7fc6a23134b8ecb307d7ff9d72593 +size 1265 diff --git a/vello_tests/snapshots/inset_box_shadow_offset_with_transform.png b/vello_tests/snapshots/inset_box_shadow_offset_with_transform.png new file mode 100644 index 0000000000..72367cb362 --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_offset_with_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6af8ee3ec0d67f2f3f5ce56dd987381bf3dc448b12dbe0458758d8a63ed58c1 +size 2909 diff --git a/vello_tests/snapshots/inset_box_shadow_offset_without_blur.png b/vello_tests/snapshots/inset_box_shadow_offset_without_blur.png new file mode 100644 index 0000000000..60c6be1eb5 --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_offset_without_blur.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a13302591d11d4379d33da329ac5bfe9dee898055996b2b28353a97d5edf628 +size 119 diff --git a/vello_tests/snapshots/inset_box_shadow_with_spread.png b/vello_tests/snapshots/inset_box_shadow_with_spread.png new file mode 100644 index 0000000000..47a64b4b17 --- /dev/null +++ b/vello_tests/snapshots/inset_box_shadow_with_spread.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12898acfb96f64d932cd91a50c9cf0aa356ccd57348aaa62196a79e80b17f14c +size 977 diff --git a/vello_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png b/vello_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png new file mode 100644 index 0000000000..14c7e4eae4 --- /dev/null +++ b/vello_tests/snapshots/inverse_blurred_rounded_rect_in_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abff848d5562e5aa96c63858fe10dc2c0a222701ceee4cb4a4007de142882e7e +size 1239 diff --git a/vello_tests/src/renderer.rs b/vello_tests/src/renderer.rs index 2c6c10bfa3..d46d2624bc 100644 --- a/vello_tests/src/renderer.rs +++ b/vello_tests/src/renderer.rs @@ -53,7 +53,15 @@ pub trait Renderer: Sized { fn fill_path(&mut self, path: &BezPath); fn stroke_path(&mut self, path: &BezPath); fn fill_rect(&mut self, rect: &Rect); - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool); + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32); + fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ); fn stroke_rect(&mut self, rect: &Rect); fn glyph_run( &mut self, @@ -137,9 +145,20 @@ impl Renderer for CpuRenderer { self.ctx.fill_rect(rect); } - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { + self.ctx.fill_blurred_rounded_rect(rect, radius, std_dev); + } + + fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) { self.ctx - .fill_blurred_rounded_rect(rect, radius, std_dev, invert); + .fill_blurred_rounded_rect_in(path, rect, radius, std_dev, invert); } fn stroke_rect(&mut self, rect: &Rect) { @@ -477,9 +496,20 @@ impl Renderer for GpuRenderer { self.scene.fill_rect(rect); } - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { + self.scene.fill_blurred_rounded_rect(rect, radius, std_dev); + } + + fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) { self.scene - .fill_blurred_rounded_rect(rect, radius, std_dev, invert); + .fill_blurred_rounded_rect_in(path, rect, radius, std_dev, invert); } fn stroke_rect(&mut self, rect: &Rect) { @@ -870,9 +900,20 @@ impl Renderer for GpuRenderer { self.scene.fill_rect(rect); } - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { + self.scene.fill_blurred_rounded_rect(rect, radius, std_dev); + } + + fn fill_blurred_rounded_rect_in( + &mut self, + path: &BezPath, + rect: &Rect, + radius: f32, + std_dev: f32, + invert: bool, + ) { self.scene - .fill_blurred_rounded_rect(rect, radius, std_dev, invert); + .fill_blurred_rounded_rect_in(path, rect, radius, std_dev, invert); } fn stroke_rect(&mut self, rect: &Rect) { diff --git a/vello_tests/tests/basic.rs b/vello_tests/tests/basic.rs index b6bab163ed..33b465da5a 100644 --- a/vello_tests/tests/basic.rs +++ b/vello_tests/tests/basic.rs @@ -353,7 +353,7 @@ fn blurred_rounded_rect_inverted(ctx: &mut impl Renderer) { let rect = Rect::new(80.0, 80.0, 20.0, 20.0); ctx.set_paint(REBECCA_PURPLE); - ctx.fill_blurred_rounded_rect(&rect, 10.0, 2.0, false); + ctx.fill_blurred_rounded_rect(&rect, 10.0, 2.0); } #[vello_test(width = 30, height = 30)] diff --git a/vello_tests/tests/blurred_rounded_rect.rs b/vello_tests/tests/blurred_rounded_rect.rs index 68b08b2bcb..7b47f748eb 100644 --- a/vello_tests/tests/blurred_rounded_rect.rs +++ b/vello_tests/tests/blurred_rounded_rect.rs @@ -2,15 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::renderer::Renderer; -use vello_common::color::palette::css::REBECCA_PURPLE; -use vello_common::kurbo::{Affine, Point, Rect}; +use vello_common::color::palette::css::{PALE_GOLDENROD, REBECCA_PURPLE}; +use vello_common::kurbo::{Affine, BezPath, Circle, Point, Rect, RoundedRect, Shape, Vec2}; +use vello_common::peniko::Fill; use vello_dev_macros::vello_test; fn rect_with(ctx: &mut impl Renderer, radius: f32, std_dev: f32, affine: Affine) { let rect = Rect::new(20.0, 20.0, 80.0, 80.0); ctx.set_paint(REBECCA_PURPLE); ctx.set_transform(affine); - ctx.fill_blurred_rounded_rect(&rect, radius, std_dev, false); + ctx.fill_blurred_rounded_rect(&rect, radius, std_dev); } #[vello_test] @@ -65,9 +66,12 @@ fn blurred_rounded_rect_with_transform(ctx: &mut impl Renderer) { fn inverse_rect_with(ctx: &mut impl Renderer, radius: f32, std_dev: f32, affine: Affine) { let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + // Extend the path to the blur kernel, so the fully covered area outside of `rect` is painted. + let kernel_size = 2.5 * f64::from(std_dev); + let path = rect.inflate(kernel_size, kernel_size).to_path(0.1); ctx.set_paint(REBECCA_PURPLE); ctx.set_transform(affine); - ctx.fill_blurred_rounded_rect(&rect, radius, std_dev, true); + ctx.fill_blurred_rounded_rect_in(&path, &rect, radius, std_dev, true); } #[vello_test] @@ -104,3 +108,120 @@ fn inverse_blurred_rounded_rect_with_transform(ctx: &mut impl Renderer) { Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), ); } + +fn rect_in_with( + ctx: &mut impl Renderer, + path: &BezPath, + radius: f32, + std_dev: f32, + invert: bool, + affine: Affine, +) { + let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + ctx.set_paint(REBECCA_PURPLE); + ctx.set_transform(affine); + ctx.fill_blurred_rounded_rect_in(path, &rect, radius, std_dev, invert); +} + +#[vello_test] +fn blurred_rounded_rect_in_circle(ctx: &mut impl Renderer) { + let path = Circle::new(Point::new(50.0, 50.0), 45.0).to_path(0.1); + rect_in_with(ctx, &path, 10.0, 10.0, false, Affine::IDENTITY); +} + +#[vello_test] +fn blurred_rounded_rect_in_circle_with_transform(ctx: &mut impl Renderer) { + let path = Circle::new(Point::new(50.0, 50.0), 45.0).to_path(0.1); + rect_in_with( + ctx, + &path, + 10.0, + 10.0, + false, + Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), + ); +} + +/// The typical inset box shadow use case: the inverse blur is clipped to the shape it is +/// inset into. +#[vello_test] +fn inverse_blurred_rounded_rect_in_rect(ctx: &mut impl Renderer) { + let path = Rect::new(20.0, 20.0, 80.0, 80.0).to_path(0.1); + rect_in_with(ctx, &path, 10.0, 10.0, true, Affine::IDENTITY); +} + +/// Emulate a CSS inset box shadow: the inverse blurred rounded rectangle is shrunk by the spread +/// and offset relative to the border box it is painted into, and clipped to that border box. +fn inset_box_shadow( + ctx: &mut impl Renderer, + offset: Vec2, + spread: f32, + radius: f32, + std_dev: f32, + affine: Affine, +) { + let border_box = Rect::new(20.0, 20.0, 80.0, 80.0); + let path = RoundedRect::from_rect(border_box, f64::from(radius)).to_path(0.1); + let spread = f64::from(spread); + let shadow_rect = border_box.inflate(-spread, -spread) + offset; + let shadow_radius = (radius - spread as f32).max(0.0); + + ctx.set_transform(affine); + ctx.set_paint(PALE_GOLDENROD); + ctx.fill_path(&path); + + ctx.set_paint(REBECCA_PURPLE); + ctx.fill_blurred_rounded_rect_in(&path, &shadow_rect, shadow_radius, std_dev, true); +} + +#[vello_test] +fn inset_box_shadow_offset_down_right(ctx: &mut impl Renderer) { + inset_box_shadow(ctx, Vec2::new(8.0, 8.0), 0.0, 10.0, 6.0, Affine::IDENTITY); +} + +#[vello_test] +fn inset_box_shadow_offset_up_left(ctx: &mut impl Renderer) { + inset_box_shadow(ctx, Vec2::new(-8.0, -8.0), 0.0, 10.0, 6.0, Affine::IDENTITY); +} + +#[vello_test] +fn inset_box_shadow_offset_horizontal(ctx: &mut impl Renderer) { + inset_box_shadow(ctx, Vec2::new(12.0, 0.0), 0.0, 10.0, 4.0, Affine::IDENTITY); +} + +/// An offset shadow larger than the blur, so the shadow has a hard edge and only covers the +/// top and left of the border box. +#[vello_test] +fn inset_box_shadow_offset_without_blur(ctx: &mut impl Renderer) { + inset_box_shadow(ctx, Vec2::new(15.0, 15.0), 0.0, 0.0, 0.0, Affine::IDENTITY); +} + +/// A spread shrinks the shadow rectangle (and its corner radius), so the shadow also covers the +/// bottom and right of the border box despite the offset. +#[vello_test] +fn inset_box_shadow_with_spread(ctx: &mut impl Renderer) { + inset_box_shadow(ctx, Vec2::new(4.0, 4.0), 8.0, 10.0, 4.0, Affine::IDENTITY); +} + +#[vello_test] +fn inset_box_shadow_offset_with_transform(ctx: &mut impl Renderer) { + inset_box_shadow( + ctx, + Vec2::new(8.0, 8.0), + 0.0, + 10.0, + 6.0, + Affine::rotate_about(20.0_f64.to_radians(), Point::new(50.0, 50.0)), + ); +} + +/// The shape is filled with the current fill rule, so the overlapping region of the two +/// circles is not painted. +#[vello_test] +fn blurred_rounded_rect_in_even_odd(ctx: &mut impl Renderer) { + let mut path = Circle::new(Point::new(35.0, 50.0), 40.0).to_path(0.1); + path.extend(Circle::new(Point::new(65.0, 50.0), 40.0).to_path(0.1)); + + ctx.set_fill_rule(Fill::EvenOdd); + rect_in_with(ctx, &path, 10.0, 10.0, false, Affine::IDENTITY); +} diff --git a/vello_tests/vello_example_scenes/src/lib.rs b/vello_tests/vello_example_scenes/src/lib.rs index 136d17aea6..568e25fabc 100644 --- a/vello_tests/vello_example_scenes/src/lib.rs +++ b/vello_tests/vello_example_scenes/src/lib.rs @@ -168,7 +168,7 @@ impl RenderingContext for RenderContext { } fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { - self.fill_blurred_rounded_rect(rect, radius, std_dev, false); + self.fill_blurred_rounded_rect(rect, radius, std_dev); } fn glyph_run<'a>( @@ -268,7 +268,7 @@ impl RenderingContext for Scene { } fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) { - self.fill_blurred_rounded_rect(rect, radius, std_dev, false); + self.fill_blurred_rounded_rect(rect, radius, std_dev); } fn glyph_run<'a>(