use kurbo::{
Affine, BezPath, CurveFitSample, ParamCurve, ParamCurveArclen, ParamCurveFit, Point, Rect,
RoundedRect, RoundedRectRadii, Shape, Stroke, Vec2, fit_to_bezpath,
};
use peniko::{Color, ColorStop, ColorStops, Fill, Gradient};
use vello::Scene;
use crate::element::PathData;
use crate::style::{Border, CornerRadius, Paint, Shadow, Style};
use crate::tokens::FOCUS_RING;
const BLUR_TO_STD_DEV: f32 = 0.5;
pub(crate) fn rounded_rect(rect: Rect, corners: CornerRadius) -> RoundedRect {
let clamp = |r: f32| -> f64 {
let max = 0.5 * rect.width().min(rect.height());
f64::from(r).clamp(0.0, max.max(0.0))
};
RoundedRect::from_rect(
rect,
RoundedRectRadii::new(
clamp(corners.tl),
clamp(corners.tr),
clamp(corners.br),
clamp(corners.bl),
),
)
}
const N_MAX: f64 = 5.0;
const SQUIRCLE_ACCURACY: f64 = 0.08;
const SQUIRCLE_MIN_RADIUS: f64 = 0.25;
pub(crate) enum BoxPath {
Arc(RoundedRect),
Squircle(BezPath),
}
impl Shape for BoxPath {
type PathElementsIter<'i> = Box<dyn Iterator<Item = kurbo::PathEl> + 'i>;
fn path_elements(&self, tol: f64) -> Self::PathElementsIter<'_> {
match self {
BoxPath::Arc(r) => Box::new(r.path_elements(tol)),
BoxPath::Squircle(p) => Box::new(p.path_elements(tol)),
}
}
fn area(&self) -> f64 {
match self {
BoxPath::Arc(r) => r.area(),
BoxPath::Squircle(p) => p.area(),
}
}
fn perimeter(&self, accuracy: f64) -> f64 {
match self {
BoxPath::Arc(r) => r.perimeter(accuracy),
BoxPath::Squircle(p) => p.perimeter(accuracy),
}
}
fn winding(&self, pt: Point) -> i32 {
match self {
BoxPath::Arc(r) => r.winding(pt),
BoxPath::Squircle(p) => p.winding(pt),
}
}
fn bounding_box(&self) -> Rect {
match self {
BoxPath::Arc(r) => r.bounding_box(),
BoxPath::Squircle(p) => p.bounding_box(),
}
}
}
pub(crate) fn corner_path(rect: Rect, corners: CornerRadius, smoothing: f32) -> BoxPath {
if smoothing <= 0.0 {
BoxPath::Arc(rounded_rect(rect, corners))
} else {
BoxPath::Squircle(build_squircle(rect, corners, smoothing))
}
}
fn squircle_exponent(smoothing: f32) -> f64 {
let s = f64::from(smoothing.clamp(0.0, 1.0));
2.0 + s * (N_MAX - 2.0)
}
fn superellipse_point(center: Point, u: Vec2, v: Vec2, r: f64, n: f64, theta: f64) -> Point {
let cx = theta.cos().max(0.0).powf(2.0 / n);
let sy = theta.sin().max(0.0).powf(2.0 / n);
center + u * (r * cx) + v * (r * sy)
}
struct SuperellipseQuadrant {
center: Point,
u: Vec2,
v: Vec2,
r: f64,
n: f64,
}
impl SuperellipseQuadrant {
const EPS: f64 = 1e-6;
fn point(&self, theta: f64) -> Point {
superellipse_point(self.center, self.u, self.v, self.r, self.n, theta)
}
fn tangent(&self, theta: f64) -> Vec2 {
if theta <= Self::EPS {
return self.v;
}
if theta >= std::f64::consts::FRAC_PI_2 - Self::EPS {
return -self.u;
}
let (s, c) = theta.sin_cos();
let du = -c.powf(2.0 / self.n - 1.0) * s;
let dv = s.powf(2.0 / self.n - 1.0) * c;
(self.u * du + self.v * dv).normalize()
}
}
impl ParamCurveFit for SuperellipseQuadrant {
fn sample_pt_tangent(&self, t: f64, _sign: f64) -> CurveFitSample {
let theta = t * std::f64::consts::FRAC_PI_2;
CurveFitSample {
p: self.point(theta),
tangent: self.tangent(theta),
}
}
fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) {
let theta = (t * std::f64::consts::FRAC_PI_2)
.clamp(Self::EPS, std::f64::consts::FRAC_PI_2 - Self::EPS);
let (s, c) = theta.sin_cos();
let du = -c.powf(2.0 / self.n - 1.0) * s;
let dv = s.powf(2.0 / self.n - 1.0) * c;
let scale = self.r * (2.0 / self.n) * std::f64::consts::FRAC_PI_2;
(self.point(theta), (self.u * du + self.v * dv) * scale)
}
fn break_cusp(&self, _range: std::ops::Range<f64>) -> Option<f64> {
None
}
}
fn build_squircle(rect: Rect, corners: CornerRadius, smoothing: f32) -> BezPath {
let n = squircle_exponent(smoothing);
let max = (0.5 * rect.width().min(rect.height())).max(0.0);
let clamp = |r: f32| f64::from(r).clamp(0.0, max);
let (tl, tr, br, bl) = (
clamp(corners.tl),
clamp(corners.tr),
clamp(corners.br),
clamp(corners.bl),
);
let (x0, y0, x1, y1) = (rect.x0, rect.y0, rect.x1, rect.y1);
let mut path = BezPath::new();
append_corner(
&mut path,
Point::new(x1 - tr, y0 + tr),
Vec2::new(0.0, -1.0),
Vec2::new(1.0, 0.0),
tr,
n,
true,
);
append_corner(
&mut path,
Point::new(x1 - br, y1 - br),
Vec2::new(1.0, 0.0),
Vec2::new(0.0, 1.0),
br,
n,
false,
);
append_corner(
&mut path,
Point::new(x0 + bl, y1 - bl),
Vec2::new(0.0, 1.0),
Vec2::new(-1.0, 0.0),
bl,
n,
false,
);
append_corner(
&mut path,
Point::new(x0 + tl, y0 + tl),
Vec2::new(-1.0, 0.0),
Vec2::new(0.0, -1.0),
tl,
n,
false,
);
path.close_path();
path
}
fn append_corner(path: &mut BezPath, center: Point, u: Vec2, v: Vec2, r: f64, n: f64, first: bool) {
let start = center + u * r;
if first {
path.move_to(start);
} else {
path.line_to(start);
}
if r <= SQUIRCLE_MIN_RADIUS {
return;
}
let quad = SuperellipseQuadrant { center, u, v, r, n };
let fitted = fit_to_bezpath(&quad, SQUIRCLE_ACCURACY);
for el in fitted.elements().iter().skip(1) {
path.push(*el);
}
}
fn uniform_radius(rect: Rect, corners: CornerRadius) -> f64 {
let max = (0.5 * rect.width().min(rect.height())).max(0.0);
let c = |r: f32| f64::from(r).clamp(0.0, max);
0.25 * (c(corners.tl) + c(corners.tr) + c(corners.br) + c(corners.bl))
}
fn shadow_layer(scene: &mut Scene, rect: Rect, corners: CornerRadius, shadow: &Shadow) {
if shadow.color.components[3] <= 0.0 {
return;
}
let spread = f64::from(shadow.spread);
let shadow_rect = rect.inflate(spread, spread).with_origin(Point::new(
rect.x0 - spread + f64::from(shadow.dx),
rect.y0 - spread + f64::from(shadow.dy),
));
let radius = (uniform_radius(rect, corners) + spread).max(0.0);
let std_dev = f64::from(shadow.blur * BLUR_TO_STD_DEV);
if std_dev <= 0.0 {
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
shadow.color,
None,
&RoundedRect::from_rect(shadow_rect, radius),
);
} else {
scene.draw_blurred_rounded_rect(
Affine::IDENTITY,
shadow_rect,
shadow.color,
radius,
std_dev,
);
}
}
fn brush_for(paint: &Paint, rect: Rect) -> peniko::Brush {
match paint {
Paint::Solid(color) => (*color).into(),
Paint::LinearGradient { angle_deg, stops } => {
let theta = f64::from(*angle_deg).to_radians();
let (sin, cos) = theta.sin_cos();
let half_len = 0.5 * (rect.width() * sin.abs() + rect.height() * cos.abs());
let center = rect.center();
let dir = Vec2::new(sin, -cos);
Gradient::new_linear(center - dir * half_len, center + dir * half_len)
.with_stops(color_stops(stops))
.into()
}
Paint::RadialGradient {
center,
radius,
stops,
} => {
let c = Point::new(
rect.x0 + f64::from(center.0) * rect.width(),
rect.y0 + f64::from(center.1) * rect.height(),
);
let r = f64::from(*radius) * 0.5 * rect.width().max(rect.height());
Gradient::new_radial(c, r as f32)
.with_stops(color_stops(stops))
.into()
}
Paint::ConicGradient { center, stops } => {
let c = Point::new(
rect.x0 + f64::from(center.0) * rect.width(),
rect.y0 + f64::from(center.1) * rect.height(),
);
Gradient::new_sweep(c, 0.0, std::f32::consts::TAU)
.with_stops(color_stops(stops))
.into()
}
}
}
fn color_stops(stops: &[crate::style::GradientStop]) -> ColorStops {
ColorStops(
stops
.iter()
.map(|s| ColorStop::from((s.offset, s.color)))
.collect(),
)
}
fn snap(v: f64, scale: f64) -> f64 {
(v * scale).round() / scale
}
fn snap_hairline_rect(rect: Rect, scale: f64) -> Rect {
let mut r = rect;
if rect.height() * scale < 1.75 {
let h = (rect.height() * scale).round().max(1.0) / scale;
r.y0 = snap(rect.y0, scale);
r.y1 = r.y0 + h;
}
if rect.width() * scale < 1.75 {
let w = (rect.width() * scale).round().max(1.0) / scale;
r.x0 = snap(rect.x0, scale);
r.x1 = r.x0 + w;
}
r
}
pub(crate) fn fill_rounded(scene: &mut Scene, rect: Rect, radius: f32, color: peniko::Color) {
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
color,
None,
&rounded_rect(rect, CornerRadius::all(radius)),
);
}
pub(crate) fn push_box(
scene: &mut Scene,
style: &Style,
rect: Rect,
canvas: Rect,
scale: f64,
backdrop: Option<&peniko::ImageData>,
) -> usize {
let mut layers = 0;
let smoothing = style.corner_smoothing.unwrap_or(0.0);
if style.opacity < 1.0 {
scene.push_layer(
Fill::NonZero,
peniko::Mix::Normal,
style.opacity.clamp(0.0, 1.0),
Affine::IDENTITY,
&rounded_rect(canvas, CornerRadius::default()),
);
layers += 1;
}
for shadow in &style.shadows {
shadow_layer(scene, rect, style.corner_radius, shadow);
}
let path = corner_path(rect, style.corner_radius, smoothing);
if let Some(image) = backdrop
&& image.width > 0
&& image.height > 0
{
let transform = Affine::translate((rect.x0, rect.y0))
* Affine::scale_non_uniform(
rect.width() / f64::from(image.width),
rect.height() / f64::from(image.height),
);
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path);
scene.draw_image(image, transform);
scene.pop_layer();
}
if let Some(paint) = &style.fill {
let fill_rect = snap_hairline_rect(rect, scale);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
&brush_for(paint, fill_rect),
None,
&corner_path(fill_rect, style.corner_radius, smoothing),
);
}
if let Some(border) = style.border
&& border.width > 0.0
{
let width = (f64::from(border.width) * scale).round().max(1.0) / scale;
let half = width * 0.5;
let snapped = Rect::new(
snap(rect.x0, scale),
snap(rect.y0, scale),
snap(rect.x1, scale),
snap(rect.y1, scale),
);
let inset_rect = snapped.inset(-half);
let mut corners = style.corner_radius;
for r in [
&mut corners.tl,
&mut corners.tr,
&mut corners.br,
&mut corners.bl,
] {
#[expect(clippy::cast_possible_truncation, reason = "logical px fit in f32")]
{
*r = (*r - half as f32).max(0.0);
}
}
scene.stroke(
&Stroke::new(width),
Affine::IDENTITY,
border.color,
None,
&corner_path(inset_rect, corners, smoothing),
);
}
let sb = style.side_borders;
if sb.top.or(sb.right).or(sb.bottom).or(sb.left).is_some() {
let r = Rect::new(
snap(rect.x0, scale),
snap(rect.y0, scale),
snap(rect.x1, scale),
snap(rect.y1, scale),
);
let mut stroke_edge = |edge: Option<Border>, p0: Point, p1: Point| {
if let Some(edge) = edge
&& edge.width > 0.0
{
let w = (f64::from(edge.width) * scale).round().max(1.0) / scale;
let mut line = BezPath::new();
line.move_to(p0);
line.line_to(p1);
scene.stroke(&Stroke::new(w), Affine::IDENTITY, edge.color, None, &line);
}
};
stroke_edge(sb.top, Point::new(r.x0, r.y0), Point::new(r.x1, r.y0));
stroke_edge(sb.bottom, Point::new(r.x0, r.y1), Point::new(r.x1, r.y1));
stroke_edge(sb.left, Point::new(r.x0, r.y0), Point::new(r.x0, r.y1));
stroke_edge(sb.right, Point::new(r.x1, r.y0), Point::new(r.x1, r.y1));
}
if let Some(highlight) = style.highlight_top
&& highlight.components[3] > 0.0
{
let h = 1.0 / scale;
let top = snap(rect.y0, scale);
let bar = Rect::new(rect.x0, top, rect.x1, top + h);
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path);
scene.fill(Fill::NonZero, Affine::IDENTITY, highlight, None, &bar);
scene.pop_layer();
}
if style.clip {
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path);
layers += 1;
}
layers
}
pub(crate) fn pop_box(scene: &mut Scene, layers: usize) {
for _ in 0..layers {
scene.pop_layer();
}
}
pub(crate) fn draw_image(
scene: &mut Scene,
image: &peniko::ImageData,
rect: Rect,
corners: CornerRadius,
smoothing: f32,
) {
if image.width == 0 || image.height == 0 || rect.width() <= 0.0 || rect.height() <= 0.0 {
return;
}
let transform = Affine::translate((rect.x0, rect.y0))
* Affine::scale_non_uniform(
rect.width() / f64::from(image.width),
rect.height() / f64::from(image.height),
);
scene.push_clip_layer(
Fill::NonZero,
Affine::IDENTITY,
&corner_path(rect, corners, smoothing),
);
scene.draw_image(image, transform);
scene.pop_layer();
}
pub(crate) fn focus_ring(
scene: &mut Scene,
rect: Rect,
corners: CornerRadius,
smoothing: f32,
color: Color,
) {
let offset = f64::from(FOCUS_RING.offset) + f64::from(FOCUS_RING.width) * 0.5;
let ring_rect = rect.inflate(offset, offset);
let mut ring_corners = corners;
for r in [
&mut ring_corners.tl,
&mut ring_corners.tr,
&mut ring_corners.br,
&mut ring_corners.bl,
] {
*r += FOCUS_RING.offset;
}
scene.stroke(
&Stroke::new(f64::from(FOCUS_RING.width)),
Affine::IDENTITY,
color,
None,
&corner_path(ring_rect, ring_corners, smoothing),
);
}
pub(crate) fn draw_path_rotated(
scene: &mut Scene,
data: &PathData,
trim: f32,
color: Color,
rect: Rect,
rotation: f64,
) {
if trim <= 0.0 {
return;
}
let sx = rect.width() / data.viewbox.0.max(1e-6);
let sy = rect.height() / data.viewbox.1.max(1e-6);
let rotate = if rotation == 0.0 {
Affine::IDENTITY
} else {
Affine::rotate_about(rotation, rect.center())
};
let transform = rotate
* Affine::translate((rect.x0, rect.y0))
* Affine::scale_non_uniform(sx, sy)
* optical_pretransform(data);
let trimmed;
let path: &BezPath = if trim >= 1.0 {
&data.path
} else {
trimmed = trim_path(&data.path, f64::from(trim));
&trimmed
};
match data.stroke {
Some(width) => {
let stroke = Stroke::new(width)
.with_caps(kurbo::Cap::Round)
.with_join(kurbo::Join::Round);
scene.stroke(&stroke, transform, color, None, path);
}
None => scene.fill(Fill::NonZero, transform, color, None, path),
}
}
fn optical_pretransform(data: &PathData) -> Affine {
let optical = data.optical;
if !optical.overshoot && !optical.center {
return Affine::IDENTITY;
}
let center = Point::new(data.viewbox.0 / 2.0, data.viewbox.1 / 2.0);
let mut pre = Affine::IDENTITY;
if optical.center
&& let Some((cx, cy)) = path_anchor_centroid(&data.path)
{
pre = Affine::translate((center.x - cx, center.y - cy));
}
if optical.overshoot {
pre = Affine::scale_about(f64::from(crate::optical::CIRCLE_OVERSHOOT), center) * pre;
}
pre
}
fn path_anchor_centroid(path: &BezPath) -> Option<(f64, f64)> {
use kurbo::PathEl;
let mut pts: Vec<(f32, f32)> = Vec::new();
for el in path.elements() {
let p = match el {
PathEl::MoveTo(p)
| PathEl::LineTo(p)
| PathEl::QuadTo(_, p)
| PathEl::CurveTo(_, _, p) => *p,
PathEl::ClosePath => continue,
};
#[expect(
clippy::cast_possible_truncation,
reason = "icon viewbox coords are small (≤ a few hundred); f32 is exact enough for a centroid"
)]
pts.push((p.x as f32, p.y as f32));
}
if pts.is_empty() {
return None;
}
let (cx, cy) = crate::optical::centroid(&pts);
Some((f64::from(cx), f64::from(cy)))
}
fn trim_path(path: &BezPath, t: f64) -> BezPath {
const ACCURACY: f64 = 0.1;
let segments: Vec<kurbo::PathSeg> = path.segments().collect();
let total: f64 = segments.iter().map(|s| s.arclen(ACCURACY)).sum();
let mut budget = total * t.clamp(0.0, 1.0);
let mut out = BezPath::new();
for seg in segments {
let len = seg.arclen(ACCURACY);
if budget <= 0.0 {
break;
}
let piece = if len <= budget {
seg
} else {
seg.subsegment(0.0..(budget / len))
};
let needs_move =
out.elements().is_empty() || piece.start().distance(last_point(&out)) > 1e-6;
if needs_move {
out.move_to(piece.start());
}
match piece {
kurbo::PathSeg::Line(l) => out.line_to(l.p1),
kurbo::PathSeg::Quad(q) => out.quad_to(q.p1, q.p2),
kurbo::PathSeg::Cubic(c) => out.curve_to(c.p1, c.p2, c.p3),
}
budget -= len;
}
out
}
fn last_point(path: &BezPath) -> Point {
match path.elements().last() {
Some(kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p)) => *p,
Some(kurbo::PathEl::QuadTo(_, p) | kurbo::PathEl::CurveTo(_, _, p)) => *p,
_ => Point::ORIGIN,
}
}
#[cfg(test)]
mod optical_tests {
use super::*;
use crate::element::{OpticalCorrection, PathData};
fn play_tri() -> PathData {
let mut p = BezPath::new();
p.move_to((14.0, 12.0));
p.line_to((14.0, 36.0));
p.line_to((34.0, 24.0));
p.close_path();
PathData {
path: std::sync::Arc::new(p),
viewbox: (48.0, 48.0),
stroke: None,
optical: OpticalCorrection::default(),
}
}
#[test]
fn no_correction_is_identity() {
assert_eq!(optical_pretransform(&play_tri()), Affine::IDENTITY);
}
#[test]
fn anchor_centroid_is_the_vertex_mean() {
let (cx, cy) = path_anchor_centroid(&play_tri().path).expect("non-empty");
assert!((cx - 62.0 / 3.0).abs() < 1e-3, "cx {cx}");
assert!((cy - 24.0).abs() < 1e-3, "cy {cy}");
}
#[test]
fn center_moves_centroid_to_viewbox_center() {
let mut d = play_tri();
d.optical.center = true;
let pre = optical_pretransform(&d);
let moved = pre * Point::new(62.0 / 3.0, 24.0);
assert!(
(moved.x - 24.0).abs() < 1e-3 && (moved.y - 24.0).abs() < 1e-3,
"{moved:?}"
);
}
#[test]
fn overshoot_scales_about_the_viewbox_center() {
let mut d = play_tri();
d.optical.overshoot = true;
let pre = optical_pretransform(&d);
let center = Point::new(24.0, 24.0);
let fixed = pre * center;
assert!(
(fixed.x - 24.0).abs() < 1e-6 && (fixed.y - 24.0).abs() < 1e-6,
"{fixed:?}"
);
let out = pre * Point::new(34.0, 24.0);
let expected = 24.0 + 10.0 * f64::from(crate::optical::CIRCLE_OVERSHOOT);
assert!((out.x - expected).abs() < 1e-4, "{out:?} vs {expected}");
}
}
#[cfg(test)]
mod squircle_tests {
use super::*;
#[test]
fn squircle_exponent_zero_is_circle() {
assert_eq!(squircle_exponent(0.0), 2.0);
assert_eq!(squircle_exponent(1.0), N_MAX);
let mut prev = squircle_exponent(0.0);
for s in [0.1_f32, 0.25, 0.5, 0.75, 1.0] {
let n = squircle_exponent(s);
assert!(
n > prev,
"exponent must increase with smoothing: {prev} -> {n}"
);
prev = n;
}
}
#[test]
fn superellipse_reduces_to_circle_at_n2() {
let c = Point::new(10.0, 10.0);
let (u, v) = (Vec2::new(0.0, -1.0), Vec2::new(1.0, 0.0));
let r = 8.0;
for i in 0..=16 {
let theta = (f64::from(i) / 16.0) * std::f64::consts::FRAC_PI_2;
let p = superellipse_point(c, u, v, r, 2.0, theta);
let circle = c + u * (r * theta.cos()) + v * (r * theta.sin());
assert!(
(p - circle).hypot() < 1e-9,
"n=2 must trace the circle at theta={theta}"
);
}
}
#[test]
fn corner_path_zero_smoothing_is_exact_arc() {
let rect = Rect::new(0.0, 0.0, 100.0, 60.0);
let corners = CornerRadius {
tl: 4.0,
tr: 8.0,
br: 12.0,
bl: 16.0,
};
match corner_path(rect, corners, 0.0) {
BoxPath::Arc(r) => assert_eq!(r, rounded_rect(rect, corners)),
BoxPath::Squircle(_) => panic!("zero smoothing must take the exact arc path"),
}
}
#[test]
fn squircle_corner_is_fuller_than_circle() {
let c = Point::ORIGIN;
let (u, v) = (Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0));
let r = 10.0;
let theta = std::f64::consts::FRAC_PI_4;
let circle = superellipse_point(c, u, v, r, squircle_exponent(0.0), theta);
assert!(
((circle - c).hypot() - r).abs() < 1e-9,
"circle bisector sits exactly r from center"
);
let squircle = superellipse_point(c, u, v, r, squircle_exponent(0.6), theta);
assert!(
(squircle - c).hypot() > r + 1e-6,
"squircle bisector must push past r toward the geometric corner"
);
}
#[test]
fn squircle_path_is_cubic_beziers() {
let rect = Rect::new(0.0, 0.0, 120.0, 120.0);
let path = build_squircle(rect, CornerRadius::all(32.0), 0.6);
let curves = path
.elements()
.iter()
.filter(|e| matches!(e, kurbo::PathEl::CurveTo(..)))
.count();
assert!(curves >= 4, "every corner fits to cubics, got {curves}");
}
#[test]
fn squircle_bbox_matches_rect() {
let rect = Rect::new(10.0, 20.0, 130.0, 100.0);
let bb = build_squircle(rect, CornerRadius::all(24.0), 0.6).bounding_box();
assert!(
(bb.x0 - rect.x0).abs() < 0.2 && (bb.y0 - rect.y0).abs() < 0.2,
"{bb:?}"
);
assert!(
(bb.x1 - rect.x1).abs() < 0.2 && (bb.y1 - rect.y1).abs() < 0.2,
"{bb:?}"
);
}
#[test]
fn squircle_opens_on_the_top_edge_join() {
let rect = Rect::new(0.0, 0.0, 100.0, 80.0);
let r = 20.0_f32;
let path = build_squircle(rect, CornerRadius::all(r), 0.7);
match path.elements()[0] {
kurbo::PathEl::MoveTo(p) => assert!(
(p.x - (rect.x1 - f64::from(r))).abs() < 1e-9 && (p.y - rect.y0).abs() < 1e-9,
"{p:?}"
),
other => panic!("path must open with MoveTo, got {other:?}"),
}
}
#[test]
fn squircle_fit_tracks_the_superellipse() {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let r = 30.0_f32;
let rf = f64::from(r);
let n = squircle_exponent(0.6);
let c = Point::new(rect.x1 - rf, rect.y0 + rf);
let (u, v) = (Vec2::new(0.0, -1.0), Vec2::new(1.0, 0.0));
let dense: Vec<Point> = (0..=4000)
.map(|i| {
let theta = (f64::from(i) / 4000.0) * std::f64::consts::FRAC_PI_2;
superellipse_point(c, u, v, rf, n, theta)
})
.collect();
for el in build_squircle(rect, CornerRadius::all(r), 0.6).elements() {
let p = match el {
kurbo::PathEl::MoveTo(p)
| kurbo::PathEl::LineTo(p)
| kurbo::PathEl::CurveTo(_, _, p) => *p,
_ => continue,
};
if p.x >= rect.x1 - rf - 1e-6 && p.y <= rect.y0 + rf + 1e-6 {
let nearest = dense
.iter()
.map(|q| (p - *q).hypot())
.fold(f64::INFINITY, f64::min);
assert!(nearest < 0.05, "anchor {p:?} off the curve by {nearest}");
}
}
}
#[test]
fn squircle_zero_radius_is_a_rectangle() {
let rect = Rect::new(0.0, 0.0, 50.0, 40.0);
let path = build_squircle(rect, CornerRadius::all(0.0), 0.8);
let bb = path.bounding_box();
assert!(
(bb.x0 - rect.x0).abs() < 1e-9 && (bb.x1 - rect.x1).abs() < 1e-9,
"{bb:?}"
);
assert!(
(bb.y0 - rect.y0).abs() < 1e-9 && (bb.y1 - rect.y1).abs() < 1e-9,
"{bb:?}"
);
let curves = path
.elements()
.iter()
.filter(|e| matches!(e, kurbo::PathEl::CurveTo(..)))
.count();
assert_eq!(curves, 0, "a zero-radius squircle is a plain rectangle");
}
}