diff --git a/sparse_strips/vello_common/src/blurred_rounded_rect.rs b/sparse_strips/vello_common/src/blurred_rounded_rect.rs index 1d1b6bf29..98262f750 100644 --- a/sparse_strips/vello_common/src/blurred_rounded_rect.rs +++ b/sparse_strips/vello_common/src/blurred_rounded_rect.rs @@ -3,7 +3,52 @@ //! Blurred, rounded rectangles. use crate::color::{AlphaColor, Srgb}; -use crate::kurbo::{Rect, RoundedRectRadii}; +use crate::kurbo::{Rect, RoundedRectRadii, Vec2}; + +/// Per-corner, possibly elliptical, radii of a rounded rectangle. +/// +/// Each corner has an x radius and a y radius (the `x`/`y` components of the +/// corresponding [`Vec2`]), matching the semantics of CSS `border-radius`. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct CornerRadii { + /// The radii of the top-left corner. + pub top_left: Vec2, + /// The radii of the top-right corner. + pub top_right: Vec2, + /// The radii of the bottom-left corner. + pub bottom_left: Vec2, + /// The radii of the bottom-right corner. + pub bottom_right: Vec2, +} + +impl CornerRadii { + /// Create a new `CornerRadii` from per-corner elliptical radii. + pub fn new(top_left: Vec2, top_right: Vec2, bottom_left: Vec2, bottom_right: Vec2) -> Self { + Self { + top_left, + top_right, + bottom_left, + bottom_right, + } + } +} + +impl From for CornerRadii { + fn from(radii: RoundedRectRadii) -> Self { + Self { + top_left: Vec2::new(radii.top_left, radii.top_left), + top_right: Vec2::new(radii.top_right, radii.top_right), + bottom_left: Vec2::new(radii.bottom_left, radii.bottom_left), + bottom_right: Vec2::new(radii.bottom_right, radii.bottom_right), + } + } +} + +impl From for CornerRadii { + fn from(radius: f64) -> Self { + RoundedRectRadii::from(radius).into() + } +} /// A blurred, rounded rectangle. #[derive(Debug)] @@ -14,9 +59,8 @@ pub struct BlurredRoundedRectangle { pub color: AlphaColor, /// The radii of the rounded rectangle's corners. /// - /// Each corner may have a different radius. Note that only scalar (circular) corner - /// radii are supported; elliptical radii are not. - pub radii: RoundedRectRadii, + /// Each corner may have a different, possibly elliptical, radius. + pub radii: CornerRadii, /// The standard deviation of the blur effect. pub std_dev: f32, /// Whether to paint the inverse (`1 - alpha`) of the blur coverage. diff --git a/sparse_strips/vello_common/src/encode.rs b/sparse_strips/vello_common/src/encode.rs index 866306cae..8991a943e 100644 --- a/sparse_strips/vello_common/src/encode.rs +++ b/sparse_strips/vello_common/src/encode.rs @@ -860,9 +860,9 @@ pub struct GradientRange { /// Per-corner values are stored in the order `[top-left, top-right, bottom-left, bottom-right]`. #[derive(Debug)] pub struct EncodedBlurredRoundedRectangle { - /// A per-corner component for computing the blur effect. + /// The per-corner superellipse exponent. pub exponent: [f32; 4], - /// A per-corner component for computing the blur effect. + /// The reciprocal of the per-corner superellipse exponent. pub recip_exponent: [f32; 4], /// An component for computing the blur effect. pub scale: f32, @@ -878,8 +878,14 @@ pub struct EncodedBlurredRoundedRectangle { pub width: f32, /// An component for computing the blur effect. pub height: f32, - /// A per-corner component for computing the blur effect. - pub r1: [f32; 4], + /// The per-corner outer radius along the x axis. + pub r1_x: [f32; 4], + /// The per-corner outer radius along the y axis. + pub r1_y: [f32; 4], + /// `r1_x^-exponent`, per corner. + pub w_x: [f32; 4], + /// `r1_y^-exponent`, per corner. + pub w_y: [f32; 4], /// Whether to paint the inverse (`1 - alpha`) of the blur coverage. /// /// When `true`, the paint is fully opaque outside the blurred rectangle and fades to @@ -932,22 +938,38 @@ impl EncodeExt for BlurredRoundedRectangle { let min_edge = width.min(height); let rmax = 0.5 * min_edge; - // Per-corner radii, in the order [top-left, top-right, bottom-left, bottom-right]. + // Per-corner elliptical radii, in the order + // [top-left, top-right, bottom-left, bottom-right]. The x radius is clamped to half + // the rectangle's width and the y radius to half its height. let radii = [ self.radii.top_left, self.radii.top_right, self.radii.bottom_left, self.radii.bottom_right, ] - .map(|r| (r as f32).clamp(0.0, rmax)); + .map(|r| { + ( + (r.x as f32).clamp(0.0, 0.5 * width), + (r.y as f32).clamp(0.0, 0.5 * height), + ) + }); - let r1 = radii.map(|radius| radius.hypot(std_dev * 2.0).min(rmax)); + let r1_x = radii.map(|(rx, _)| rx.hypot(std_dev * 2.0).min(0.5 * width)); + let r1_y = radii.map(|(_, ry)| ry.hypot(std_dev * 2.0).min(0.5 * height)); + // The superellipse exponent of each corner is derived from the corner's geometric + // mean radius, which for circular corners matches the uniform-radius formulation. let mut exponent = [0.0; 4]; let mut recip_exponent = [0.0; 4]; + let mut w_x = [0.0; 4]; + let mut w_y = [0.0; 4]; for i in 0..4 { - let r0 = radii[i].hypot(std_dev * 1.15).min(rmax); - exponent[i] = 2.0 * r1[i] / r0; + let rg = (radii[i].0 * radii[i].1).sqrt(); + let r0 = rg.hypot(std_dev * 1.15).min(rmax); + let r1 = rg.hypot(std_dev * 2.0).min(rmax); + exponent[i] = 2.0 * r1 / r0; recip_exponent[i] = exponent[i].recip(); + w_x[i] = r1_x[i].powf(-exponent[i]); + w_y[i] = r1_y[i].powf(-exponent[i]); } let std_dev_inv = std_dev.recip(); @@ -960,7 +982,7 @@ impl EncodeExt for BlurredRoundedRectangle { let w = width + delta.min(0.0); let h = height - delta.max(0.0); - let mean_radius = 0.25 * (radii[0] + radii[1] + radii[2] + radii[3]); + let mean_radius = 0.25 * radii.iter().map(|&(rx, ry)| (rx * ry).sqrt()).sum::(); let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * mean_radius)); let encoded = EncodedBlurredRoundedRectangle { @@ -969,7 +991,10 @@ impl EncodeExt for BlurredRoundedRectangle { width, height, scale, - r1, + r1_x, + r1_y, + w_x, + w_y, std_dev_inv, min_edge, invert: self.invert, diff --git a/sparse_strips/vello_cpu/CHANGELOG.md b/sparse_strips/vello_cpu/CHANGELOG.md index 07f009428..2a83aea2c 100644 --- a/sparse_strips/vello_cpu/CHANGELOG.md +++ b/sparse_strips/vello_cpu/CHANGELOG.md @@ -14,7 +14,7 @@ This release has an [MSRV][] of 1.88. ### Changed -- Breaking change: `RenderContext::fill_blurred_rounded_rect` now takes a `RoundedRectRadii`, allowing a different radius per corner. A uniform radius can be passed as `radius.into()`. +- Breaking change: `RenderContext::fill_blurred_rounded_rect` now takes a `CornerRadii`, allowing a different, possibly elliptical, radius per corner. A uniform radius or a `RoundedRectRadii` can be passed as `radii.into()`. ## [0.1.0][] - 2026-07-29 diff --git a/sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs b/sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs index 7f887cb15..4b2ffd5cd 100644 --- a/sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs +++ b/sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs @@ -183,20 +183,37 @@ impl Iterator for AlphaCalculator { simd.select_f32x8(right, v[1], v[0]), ) }; - let r1 = select_corner(&r.r1); + let r1_x = select_corner(&r.r1_x); + let r1_y = select_corner(&r.r1_y); + let w_x = select_corner(&r.w_x); + let w_y = select_corner(&r.w_y); let exponent = select_corner(&r.exponent); let recip_exponent = select_corner(&r.recip_exponent); - // Equivalent to r1 + y.abs() - (r.h * r.v1) - let y0 = r1 - r.h.mul_sub(r.v1, y.abs()); + // Equivalent to r1_y + y.abs() - (r.h * r.v1) + let y0 = r1_y - r.h.mul_sub(r.v1, y.abs()); let y1 = y0.max(r.v0); - // Equivalent to r1 + x.abs() - (r.w * r.v1) - let x0 = r1 - r.w.mul_sub(r.v1, x.abs()); + // Equivalent to r1_x + x.abs() - (r.w * r.v1) + let x0 = r1_x - r.w.mul_sub(r.v1, x.abs()); let x1 = x0.max(r.v0); - let d_pos = (x1.powf(exponent) + y1.powf(exponent)).powf(recip_exponent); + let a = x1.powf(exponent); + let b = y1.powf(exponent); + let d_pos = (a + b).powf(recip_exponent); + // The direction-dependent effective corner radius: the Euclidean-style radius of + // the superellipse in the direction of the current point, which reduces to the + // corner radius itself for circular corners. The epsilon terms keep the ratio + // well-defined when both `a` and `b` are zero. + let eps = f32x8::splat(simd, 1e-18); + let r_eff_num = a + b + eps; + let r_eff_den = a.mul_add(w_x, b.mul_add(w_y, eps * (r.v1 * (w_x + w_y)))); + let r_eff = simd.select_f32x8( + simd.simd_eq_f32x8(r1_x, r1_y), + r1_x, + simd.div_f32x8(r_eff_num, r_eff_den).powf(recip_exponent), + ); let d_neg = x0.max(y0).min(r.v0); - let d = d_pos + d_neg - r1; + let d = d_pos + d_neg - r_eff; let z = r.scale * (f32x8::compute_erf7(self.simd, r.std_dev_inv * (r.min_edge + d)) - f32x8::compute_erf7(self.simd, r.std_dev_inv * d)); @@ -218,7 +235,10 @@ struct SimdRoundedBlurredRect { pub h: f32x8, pub width: f32x8, pub height: f32x8, - pub r1: [f32x8; 4], + pub r1_x: [f32x8; 4], + pub r1_y: [f32x8; 4], + pub w_x: [f32x8; 4], + pub w_y: [f32x8; 4], pub v0: f32x8, pub v1: f32x8, } @@ -232,7 +252,10 @@ impl SimdRoundedBlurredRect { let w = f32x8::splat(s, encoded.w); let width = f32x8::splat(s, encoded.width); let height = f32x8::splat(s, encoded.height); - let r1 = encoded.r1.map(|v| f32x8::splat(s, v)); + let r1_x = encoded.r1_x.map(|v| f32x8::splat(s, v)); + let r1_y = encoded.r1_y.map(|v| f32x8::splat(s, v)); + let w_x = encoded.w_x.map(|v| f32x8::splat(s, v)); + let w_y = encoded.w_y.map(|v| f32x8::splat(s, v)); let exponent = encoded.exponent.map(|v| f32x8::splat(s, v)); let recip_exponent = encoded.recip_exponent.map(|v| f32x8::splat(s, v)); let scale = f32x8::splat(s, encoded.scale); @@ -253,7 +276,10 @@ impl SimdRoundedBlurredRect { h, width, height, - r1, + r1_x, + r1_y, + w_x, + w_y, } }, ) diff --git a/sparse_strips/vello_cpu/src/lib.rs b/sparse_strips/vello_cpu/src/lib.rs index a0cd312d8..2f8585b8c 100644 --- a/sparse_strips/vello_cpu/src/lib.rs +++ b/sparse_strips/vello_cpu/src/lib.rs @@ -176,6 +176,7 @@ pub use render::{ pub use glifo::Glyph; #[cfg(feature = "text")] pub use text::{CpuGlyphRunBackend, GlyphRunBuilder}; +pub use vello_common::blurred_rounded_rect::CornerRadii; pub use vello_common::fearless_simd::Level; pub use vello_common::mask::Mask; pub use vello_common::paint::{Image, ImageSource, Paint, PaintType}; diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 524399d02..294e8fab5 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -19,12 +19,12 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use hashbrown::HashMap; -use vello_common::blurred_rounded_rect::BlurredRoundedRectangle; +use vello_common::blurred_rounded_rect::{BlurredRoundedRectangle, CornerRadii}; use vello_common::encode::{EncodeExt, EncodedPaint}; use vello_common::fearless_simd::Level; use vello_common::filter::FilterData; use vello_common::filter_effects::Filter; -use vello_common::kurbo::{Affine, BezPath, Rect, RoundedRectRadii, Stroke}; +use vello_common::kurbo::{Affine, BezPath, Rect, Stroke}; use vello_common::mask::Mask; use vello_common::paint::{ImageId, ImageResolver, Paint, PaintType, Tint}; use vello_common::peniko::color::palette::css::BLACK; @@ -385,8 +385,7 @@ impl RenderContext { /// Fill a blurred rectangle with the given corner radii and standard deviation. /// - /// Each corner may have a different (scalar) radius; elliptical corner radii are not - /// supported. + /// Each corner may have a different, possibly elliptical, radius. /// /// 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 @@ -397,7 +396,7 @@ impl RenderContext { pub fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ) { diff --git a/sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs b/sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs index efcf41499..096189a47 100644 --- a/sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs +++ b/sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs @@ -3,7 +3,8 @@ //! Scene demonstrating blurred rounded rectangle rendering. -use vello_common::kurbo::{Affine, Rect, RoundedRectRadii}; +use vello_common::blurred_rounded_rect::CornerRadii; +use vello_common::kurbo::{Affine, Rect, RoundedRectRadii, Vec2}; use vello_common::peniko::color::palette; use vello_common::peniko::color::{AlphaColor, Srgb}; @@ -92,7 +93,22 @@ pub fn render(ctx: &mut impl RenderingContext, root_transform: Affine) { scene_transform * Affine::translate((900.0, 1000.0)), rect, palette::css::BLACK, - RoundedRectRadii::new(120.0, 0.0, 60.0, 20.0), + RoundedRectRadii::new(120.0, 0.0, 60.0, 20.0).into(), + 45.0, + ); + + // Elliptical corner radii. + draw_blurred_rect( + ctx, + scene_transform * Affine::translate((300.0, 1000.0)), + rect, + palette::css::BLACK, + CornerRadii::new( + Vec2::new(150.0, 60.0), + Vec2::new(30.0, 120.0), + Vec2::new(0.0, 0.0), + Vec2::new(80.0, 20.0), + ), 45.0, ); @@ -111,7 +127,7 @@ fn draw_blurred_rect( transform: Affine, rect: Rect, color: AlphaColor, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, ) { ctx.set_transform(transform); diff --git a/sparse_strips/vello_example_scenes/src/lib.rs b/sparse_strips/vello_example_scenes/src/lib.rs index f10f60812..4dcd9514d 100644 --- a/sparse_strips/vello_example_scenes/src/lib.rs +++ b/sparse_strips/vello_example_scenes/src/lib.rs @@ -21,9 +21,10 @@ pub mod svg; pub mod text; use glifo::GlyphRunBackend; +pub use vello_common::blurred_rounded_rect::CornerRadii; use vello_common::filter_effects::Filter; use vello_common::kurbo::Affine; -pub use vello_common::kurbo::{BezPath, Rect, RoundedRectRadii, Shape, Stroke}; +pub use vello_common::kurbo::{BezPath, Rect, Shape, Stroke}; pub use vello_common::mask::Mask; use vello_common::paint::ImageSource; pub use vello_common::paint::{Paint, PaintType}; @@ -81,7 +82,7 @@ pub trait RenderingContext: Sized { /// Fill a rectangle with the current paint. fn fill_rect(&mut self, rect: &Rect); /// Fill a blurred rounded rectangle with the current solid paint. - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32); + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: CornerRadii, std_dev: f32); /// Create a glyph run builder for text rendering. fn glyph_run<'a>( &'a mut self, @@ -172,7 +173,7 @@ impl RenderingContext for RenderContext { self.fill_rect(rect); } - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32) { + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: CornerRadii, std_dev: f32) { self.fill_blurred_rounded_rect(rect, radii, std_dev, false); } @@ -277,7 +278,7 @@ impl RenderingContext for Scene { self.fill_rect(rect); } - fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: RoundedRectRadii, std_dev: f32) { + fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radii: CornerRadii, std_dev: f32) { self.fill_blurred_rounded_rect(rect, radii, std_dev, false); } diff --git a/sparse_strips/vello_hybrid/CHANGELOG.md b/sparse_strips/vello_hybrid/CHANGELOG.md index 52bb121b7..eb8a866ac 100644 --- a/sparse_strips/vello_hybrid/CHANGELOG.md +++ b/sparse_strips/vello_hybrid/CHANGELOG.md @@ -19,7 +19,7 @@ This release has an [MSRV][] of 1.88. ### Changed - Breaking change: `WebGlRenderer::render` and `WebGlRenderer::render_to_atlas` now take a `WebGlTextureBindings`, providing the external textures referenced by the scene. Pass `&WebGlTextureBindings::new()` if the scene does not use any. -- Breaking change: `Scene::fill_blurred_rounded_rect` now takes a `RoundedRectRadii`, allowing a different radius per corner. A uniform radius can be passed as `radius.into()`. +- Breaking change: `Scene::fill_blurred_rounded_rect` now takes a `CornerRadii`, allowing a different, possibly elliptical, radius per corner. A uniform radius or a `RoundedRectRadii` can be passed as `radii.into()`. ## [0.1.0][] - 2026-07-29 diff --git a/sparse_strips/vello_hybrid/src/lib.rs b/sparse_strips/vello_hybrid/src/lib.rs index 0d60e1b72..86cb01cbe 100644 --- a/sparse_strips/vello_hybrid/src/lib.rs +++ b/sparse_strips/vello_hybrid/src/lib.rs @@ -107,6 +107,7 @@ pub use scene::{LayersConfig, MemorySettings, RenderSettings, Scene}; pub use text::{GlyphRunBuilder, HybridGlyphRunBackend}; pub use util::DimensionConstraints; pub use vello_common::TextureId; +pub use vello_common::blurred_rounded_rect::CornerRadii; pub use vello_common::geometry::SizeU16; pub use vello_common::multi_atlas::{AllocationStrategy, AtlasConfig, AtlasId}; pub use vello_common::pixmap::Pixmap; diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs index 721da78d5..b5ca73cec 100644 --- a/sparse_strips/vello_hybrid/src/render/common.rs +++ b/sparse_strips/vello_hybrid/src/render/common.rs @@ -466,10 +466,16 @@ pub(crate) struct GpuBlurredRoundedRect { pub params0: [f32; 4], /// Blur parameters: adjusted width, adjusted height, width, and height. pub params1: [f32; 4], - /// Per-corner outer radii, in the order [top-left, top-right, bottom-left, bottom-right]. - pub r1: [f32; 4], + /// Per-corner outer x radii, in the order [top-left, top-right, bottom-left, bottom-right]. + pub r1_x: [f32; 4], + /// Per-corner outer y radii, in the order [top-left, top-right, bottom-left, bottom-right]. + pub r1_y: [f32; 4], /// Per-corner exponents, in the order [top-left, top-right, bottom-left, bottom-right]. pub exponent: [f32; 4], + /// Per-corner `r1_x^-exponent`, in the order [top-left, top-right, bottom-left, bottom-right]. + pub w_x: [f32; 4], + /// Per-corner `r1_y^-exponent`, in the order [top-left, top-right, bottom-left, bottom-right]. + pub w_y: [f32; 4], } /// GPU encoded linear gradient data. diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index c3c1b9b11..e4e1d773f 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs @@ -898,8 +898,11 @@ impl WebGlRenderer { invert: u32::from(rect.invert), params0: [rect.scale, rect.std_dev_inv, rect.min_edge, 0.0], params1: [rect.w, rect.h, rect.width, rect.height], - r1: rect.r1, + r1_x: rect.r1_x, + r1_y: rect.r1_y, exponent: rect.exponent, + w_x: rect.w_x, + w_y: rect.w_y, }) } } diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs index 1acc5a0f2..7f6e8649b 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs @@ -861,8 +861,11 @@ impl Renderer { invert: u32::from(rect.invert), params0: [rect.scale, rect.std_dev_inv, rect.min_edge, 0.0], params1: [rect.w, rect.h, rect.width, rect.height], - r1: rect.r1, + r1_x: rect.r1_x, + r1_y: rect.r1_y, exponent: rect.exponent, + w_x: rect.w_x, + w_y: rect.w_y, }) } } diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index 6cb2b5614..022bc6bff 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -13,14 +13,14 @@ use alloc::vec::Vec; use core::cell::RefCell; use core::ops::Range; use vello_common::TextureId; -use vello_common::blurred_rounded_rect::BlurredRoundedRectangle; +use vello_common::blurred_rounded_rect::{BlurredRoundedRectangle, CornerRadii}; use vello_common::clip::PathDataRef; use vello_common::encode::{EncodeExt, EncodedExternalTexture, EncodedPaint}; use vello_common::fearless_simd::Level; use vello_common::filter::FilterData; use vello_common::filter_effects::Filter; use vello_common::geometry::{RectU16, SizeU16}; -use vello_common::kurbo::{Affine, BezPath, Rect, RoundedRectRadii, Shape, Stroke}; +use vello_common::kurbo::{Affine, BezPath, Rect, Shape, Stroke}; use vello_common::mask::Mask; use vello_common::multi_atlas::AtlasConfig; use vello_common::paint::{Paint, PaintType, Tint}; @@ -632,8 +632,7 @@ impl Scene { /// Fill a blurred rectangle with the given corner radii and standard deviation. /// - /// Each corner may have a different (scalar) radius; elliptical corner radii are not - /// supported. + /// Each corner may have a different, possibly elliptical, radius. /// /// 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 @@ -644,7 +643,7 @@ impl Scene { pub fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ) { diff --git a/sparse_strips/vello_sparse_shaders/shaders/render.wesl b/sparse_strips/vello_sparse_shaders/shaders/render.wesl index 0b06709d7..379bcf51a 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render.wesl +++ b/sparse_strips/vello_sparse_shaders/shaders/render.wesl @@ -521,6 +521,9 @@ fn fs_main( let blurred_texel3 = load_encoded_paint_texel(paint_tex_idx, 3u); let blurred_texel4 = load_encoded_paint_texel(paint_tex_idx, 4u); let blurred_texel5 = load_encoded_paint_texel(paint_tex_idx, 5u); + let blurred_texel6 = load_encoded_paint_texel(paint_tex_idx, 6u); + let blurred_texel7 = load_encoded_paint_texel(paint_tex_idx, 7u); + let blurred_texel8 = load_encoded_paint_texel(paint_tex_idx, 8u); final_color = alpha * calculate_blurred_rounded_rect( sample_xy, blurred_texel0, @@ -529,6 +532,9 @@ fn fs_main( blurred_texel3, blurred_texel4, blurred_texel5, + blurred_texel6, + blurred_texel7, + blurred_texel8, ); } } else if color_source == COLOR_SOURCE_LAYER { @@ -998,11 +1004,20 @@ fn get_blurred_rounded_rect_width(texel3: vec4) -> f32 { return bitcast) -> f32 { return bitcast(texel3.w); } -/// Per-corner outer radii, in the order [top-left, top-right, bottom-left, bottom-right]. -fn get_blurred_rounded_rect_r1(texel4: vec4) -> vec4 { return bitcast>(texel4); } +/// Per-corner outer x radii, in the order [top-left, top-right, bottom-left, bottom-right]. +fn get_blurred_rounded_rect_r1_x(texel4: vec4) -> vec4 { return bitcast>(texel4); } + +/// Per-corner outer y radii, in the order [top-left, top-right, bottom-left, bottom-right]. +fn get_blurred_rounded_rect_r1_y(texel5: vec4) -> vec4 { return bitcast>(texel5); } /// Per-corner exponents, in the order [top-left, top-right, bottom-left, bottom-right]. -fn get_blurred_rounded_rect_exponent(texel5: vec4) -> vec4 { return bitcast>(texel5); } +fn get_blurred_rounded_rect_exponent(texel6: vec4) -> vec4 { return bitcast>(texel6); } + +/// Per-corner `r1_x^-exponent`, in the order [top-left, top-right, bottom-left, bottom-right]. +fn get_blurred_rounded_rect_w_x(texel7: vec4) -> vec4 { return bitcast>(texel7); } + +/// Per-corner `r1_y^-exponent`, in the order [top-left, top-right, bottom-left, bottom-right]. +fn get_blurred_rounded_rect_w_y(texel8: vec4) -> vec4 { return bitcast>(texel8); } // Calculate a radial gradient; matches vello_cpu implementation. fn calculate_radial_gradient( @@ -1116,6 +1131,9 @@ fn calculate_blurred_rounded_rect( texel3: vec4, texel4: vec4, texel5: vec4, + texel6: vec4, + texel7: vec4, + texel8: vec4, ) -> vec4 { let transform = get_blurred_rounded_rect_transform(texel0); let translate = get_blurred_rounded_rect_translate(texel1); @@ -1128,8 +1146,11 @@ fn calculate_blurred_rounded_rect( let h = get_blurred_rounded_rect_h(texel3); let width = get_blurred_rounded_rect_width(texel3); let height = get_blurred_rounded_rect_height(texel3); - let corner_r1 = get_blurred_rounded_rect_r1(texel4); - let corner_exponent = get_blurred_rounded_rect_exponent(texel5); + let corner_r1_x = get_blurred_rounded_rect_r1_x(texel4); + let corner_r1_y = get_blurred_rounded_rect_r1_y(texel5); + let corner_exponent = get_blurred_rounded_rect_exponent(texel6); + let corner_w_x = get_blurred_rounded_rect_w_x(texel7); + let corner_w_y = get_blurred_rounded_rect_w_y(texel8); let local_xy = transform * fragment_pos + translate; // The 0.5 and 0.0 constants correspond to vello_cpu's v1 and v0 respectively. @@ -1139,22 +1160,34 @@ fn calculate_blurred_rounded_rect( // Select the parameters of the corner in whose quadrant the fragment lies. // Per-corner values are stored as [top-left, top-right, bottom-left, bottom-right]. let corner = select(0u, 1u, x >= 0.0) + select(0u, 2u, y >= 0.0); - let r1 = corner_r1[corner]; + let r1_x = corner_r1_x[corner]; + let r1_y = corner_r1_y[corner]; + let w_x = corner_w_x[corner]; + let w_y = corner_w_y[corner]; let exponent = corner_exponent[corner]; let recip_exponent = 1.0 / exponent; - let y0 = r1 + abs(y) - 0.5 * h; + let y0 = r1_y + abs(y) - 0.5 * h; let y1 = max(y0, 0.0); - let x0 = r1 + abs(x) - 0.5 * w; + let x0 = r1_x + abs(x) - 0.5 * w; let x1 = max(x0, 0.0); - let d_pos = pow( - pow(x1, exponent) + pow(y1, exponent), + let a = pow(x1, exponent); + let b = pow(y1, exponent); + let d_pos = pow(a + b, recip_exponent); + // The direction-dependent effective corner radius: the Euclidean-style radius of the + // superellipse in the direction of the current point, which reduces to the corner + // radius itself for circular corners. The epsilon terms keep the ratio well-defined + // when both `a` and `b` are zero. + const EPS: f32 = 1e-18; + let r_eff_computed = pow( + (a + b + EPS) / (a * w_x + b * w_y + EPS * 0.5 * (w_x + w_y)), recip_exponent, ); + let r_eff = select(r_eff_computed, r1_x, r1_x == r1_y); let d_neg = min(max(x0, y0), 0.0); - let d = d_pos + d_neg - r1; + let d = d_pos + d_neg - r_eff; let blur_coverage = scale * ( erf7(std_dev_inv * (min_edge + d)) - erf7(std_dev_inv * d) diff --git a/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_pill.png b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_pill.png new file mode 100644 index 000000000..3976ddc40 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_pill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01785ddce19d3c2b6b3230d23e767432fca5c59a75a79680bd445252a5497dc6 +size 2201 diff --git a/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii.png b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii.png new file mode 100644 index 000000000..cfab27bf4 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22cc09826517e15bd82714336400968cad33c051167d7b15b8b50d38b07d2f04 +size 1547 diff --git a/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii_std_dev.png b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii_std_dev.png new file mode 100644 index 000000000..7f0fa4534 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/blurred_rounded_rect_elliptical_radii_std_dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5e0776d7fc9dda34f5ccc91a83a6248a51f487da0736cacf79961c86acb2462 +size 2757 diff --git a/sparse_strips/vello_sparse_tests/snapshots/inverse_blurred_rounded_rect_elliptical_radii.png b/sparse_strips/vello_sparse_tests/snapshots/inverse_blurred_rounded_rect_elliptical_radii.png new file mode 100644 index 000000000..2b721c3a6 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/inverse_blurred_rounded_rect_elliptical_radii.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d62c8fcfc55a79383e9614130ba060605815ec8aa67ab59d75035b1fe1accb0 +size 1972 diff --git a/sparse_strips/vello_sparse_tests/tests/blurred_rounded_rect.rs b/sparse_strips/vello_sparse_tests/tests/blurred_rounded_rect.rs index d28491b85..1da673e94 100644 --- a/sparse_strips/vello_sparse_tests/tests/blurred_rounded_rect.rs +++ b/sparse_strips/vello_sparse_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::blurred_rounded_rect::CornerRadii; use vello_common::color::palette::css::REBECCA_PURPLE; -use vello_common::kurbo::{Affine, Point, Rect, RoundedRectRadii}; +use vello_common::kurbo::{Affine, Point, Rect, RoundedRectRadii, Vec2}; use vello_dev_macros::vello_test; fn rect_with(ctx: &mut impl Renderer, radius: f32, std_dev: f32, affine: Affine) { rect_with_radii(ctx, f64::from(radius).into(), std_dev, affine); } -fn rect_with_radii(ctx: &mut impl Renderer, radii: RoundedRectRadii, std_dev: f32, affine: Affine) { +fn rect_with_radii(ctx: &mut impl Renderer, radii: CornerRadii, 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); @@ -71,7 +72,7 @@ fn blurred_rounded_rect_with_transform(ctx: &mut impl Renderer) { fn blurred_rounded_rect_non_uniform_radii(ctx: &mut impl Renderer) { rect_with_radii( ctx, - RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0), + RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0).into(), 3.0, Affine::IDENTITY, ); @@ -81,7 +82,7 @@ fn blurred_rounded_rect_non_uniform_radii(ctx: &mut impl Renderer) { fn blurred_rounded_rect_non_uniform_radii_large_std_dev(ctx: &mut impl Renderer) { rect_with_radii( ctx, - RoundedRectRadii::new(30.0, 0.0, 15.0, 5.0), + RoundedRectRadii::new(30.0, 0.0, 15.0, 5.0).into(), 10.0, Affine::IDENTITY, ); @@ -91,12 +92,57 @@ fn blurred_rounded_rect_non_uniform_radii_large_std_dev(ctx: &mut impl Renderer) fn blurred_rounded_rect_non_uniform_radii_with_transform(ctx: &mut impl Renderer) { rect_with_radii( ctx, - RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0), + RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0).into(), 3.0, Affine::rotate_about(45.0_f64.to_radians(), Point::new(50.0, 50.0)), ); } +#[vello_test] +fn blurred_rounded_rect_elliptical_radii(ctx: &mut impl Renderer) { + rect_with_radii( + ctx, + CornerRadii::new( + Vec2::new(25.0, 10.0), + Vec2::new(5.0, 20.0), + Vec2::new(0.0, 0.0), + Vec2::new(15.0, 30.0), + ), + 2.0, + Affine::IDENTITY, + ); +} + +#[vello_test] +fn blurred_rounded_rect_elliptical_radii_std_dev(ctx: &mut impl Renderer) { + rect_with_radii( + ctx, + CornerRadii::new( + Vec2::new(25.0, 10.0), + Vec2::new(5.0, 20.0), + Vec2::new(0.0, 0.0), + Vec2::new(15.0, 30.0), + ), + 6.0, + Affine::IDENTITY, + ); +} + +#[vello_test] +fn blurred_rounded_rect_elliptical_pill(ctx: &mut impl Renderer) { + // A fully elliptical box: all radii at half the rectangle's size. + ctx.set_paint(REBECCA_PURPLE); + ctx.set_transform(Affine::IDENTITY); + let rect = Rect::new(10.0, 25.0, 90.0, 75.0); + let radii = CornerRadii::new( + Vec2::new(40.0, 25.0), + Vec2::new(40.0, 25.0), + Vec2::new(40.0, 25.0), + Vec2::new(40.0, 25.0), + ); + ctx.fill_blurred_rounded_rect(&rect, radii, 3.0, false); +} + 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); ctx.set_paint(REBECCA_PURPLE); @@ -146,8 +192,22 @@ fn inverse_blurred_rounded_rect_non_uniform_radii(ctx: &mut impl Renderer) { ctx.set_transform(Affine::IDENTITY); ctx.fill_blurred_rounded_rect( &rect, - RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0), + RoundedRectRadii::new(20.0, 0.0, 10.0, 5.0).into(), 3.0, true, ); } + +#[vello_test] +fn inverse_blurred_rounded_rect_elliptical_radii(ctx: &mut impl Renderer) { + let rect = Rect::new(20.0, 20.0, 80.0, 80.0); + ctx.set_paint(REBECCA_PURPLE); + ctx.set_transform(Affine::IDENTITY); + let radii = CornerRadii::new( + Vec2::new(25.0, 10.0), + Vec2::new(5.0, 20.0), + Vec2::new(0.0, 0.0), + Vec2::new(15.0, 30.0), + ); + ctx.fill_blurred_rounded_rect(&rect, radii, 3.0, true); +} diff --git a/sparse_strips/vello_sparse_tests/tests/renderer.rs b/sparse_strips/vello_sparse_tests/tests/renderer.rs index c2fbb4350..552f119be 100644 --- a/sparse_strips/vello_sparse_tests/tests/renderer.rs +++ b/sparse_strips/vello_sparse_tests/tests/renderer.rs @@ -6,8 +6,9 @@ use std::collections::HashMap; use std::sync::Arc; use glifo::GlyphRunBackend; +use vello_common::blurred_rounded_rect::CornerRadii; use vello_common::filter_effects::Filter; -use vello_common::kurbo::{Affine, BezPath, Rect, RoundedRectRadii, Stroke}; +use vello_common::kurbo::{Affine, BezPath, Rect, Stroke}; use vello_common::mask::Mask; use vello_common::paint::{ImageId, ImageSource, PaintType, Tint}; use vello_common::peniko::{BlendMode, Fill, FontData, ImageQuality}; @@ -38,7 +39,7 @@ pub(crate) trait Renderer: Sized { fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ); @@ -129,7 +130,7 @@ impl Renderer for CpuRenderer { fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ) { @@ -415,7 +416,7 @@ impl Renderer for HybridRenderer { fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ) { @@ -794,7 +795,7 @@ impl Renderer for HybridRenderer { fn fill_blurred_rounded_rect( &mut self, rect: &Rect, - radii: RoundedRectRadii, + radii: CornerRadii, std_dev: f32, invert: bool, ) {