Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions sparse_strips/vello_common/src/blurred_rounded_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RoundedRectRadii> 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<f64> for CornerRadii {
fn from(radius: f64) -> Self {
RoundedRectRadii::from(radius).into()
}
}

/// A blurred, rounded rectangle.
#[derive(Debug)]
Expand All @@ -14,9 +59,8 @@ pub struct BlurredRoundedRectangle {
pub color: AlphaColor<Srgb>,
/// 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.
Expand Down
47 changes: 36 additions & 11 deletions sparse_strips/vello_common/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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::<f32>();
let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * mean_radius));

let encoded = EncodedBlurredRoundedRectangle {
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion sparse_strips/vello_cpu/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 36 additions & 10 deletions sparse_strips/vello_cpu/src/fine/common/rounded_blurred_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,20 +183,37 @@ impl<S: Simd> Iterator for AlphaCalculator<S> {
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));
Expand All @@ -218,7 +235,10 @@ struct SimdRoundedBlurredRect<S: Simd> {
pub h: f32x8<S>,
pub width: f32x8<S>,
pub height: f32x8<S>,
pub r1: [f32x8<S>; 4],
pub r1_x: [f32x8<S>; 4],
pub r1_y: [f32x8<S>; 4],
pub w_x: [f32x8<S>; 4],
pub w_y: [f32x8<S>; 4],
pub v0: f32x8<S>,
pub v1: f32x8<S>,
}
Expand All @@ -232,7 +252,10 @@ impl<S: Simd> SimdRoundedBlurredRect<S> {
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);
Expand All @@ -253,7 +276,10 @@ impl<S: Simd> SimdRoundedBlurredRect<S> {
h,
width,
height,
r1,
r1_x,
r1_y,
w_x,
w_y,
}
},
)
Expand Down
1 change: 1 addition & 0 deletions sparse_strips/vello_cpu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
9 changes: 4 additions & 5 deletions sparse_strips/vello_cpu/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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,
) {
Expand Down
22 changes: 19 additions & 3 deletions sparse_strips/vello_example_scenes/src/blurred_rounded_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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,
);

Expand All @@ -111,7 +127,7 @@ fn draw_blurred_rect(
transform: Affine,
rect: Rect,
color: AlphaColor<Srgb>,
radii: RoundedRectRadii,
radii: CornerRadii,
std_dev: f32,
) {
ctx.set_transform(transform);
Expand Down
9 changes: 5 additions & 4 deletions sparse_strips/vello_example_scenes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion sparse_strips/vello_hybrid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions sparse_strips/vello_hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading