use super::*; use std::mem; /// Produce shapes that together make up a rounded rectangle. pub fn rounded_rect( size: Size, radius: Corners, fill: Option, stroke: Sides>, ) -> Vec { let mut res = vec![]; if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { res.push(Shape { geometry: fill_geometry(size, radius), fill, stroke: if stroke.is_uniform() { stroke.top } else { None }, }); } if !stroke.is_uniform() { for (path, stroke) in stroke_segments(size, radius, stroke) { if stroke.is_some() { res.push(Shape { geometry: Geometry::Path(path), fill: None, stroke }); } } } res } /// Output the shape of the rectangle as a path or primitive rectangle, /// depending on whether it is rounded. fn fill_geometry(size: Size, radius: Corners) -> Geometry { if radius.iter().copied().all(Abs::is_zero) { Geometry::Rect(size) } else { let mut paths = stroke_segments(size, radius, Sides::splat(None)); assert_eq!(paths.len(), 1); Geometry::Path(paths.pop().unwrap().0) } } /// Output the minimum number of paths along the rectangles border. fn stroke_segments( size: Size, radius: Corners, stroke: Sides>, ) -> Vec<(Path, Option)> { let mut res = vec![]; let mut connection = Connection::default(); let mut path = Path::new(); let mut always_continuous = true; let max_radius = size.x.min(size.y) / 2.0; for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { let continuous = stroke.get(side) == stroke.get(side.next_cw()); connection = connection.advance(continuous && side != Side::Left); always_continuous &= continuous; draw_side( &mut path, side, size, radius.get(side.start_corner()).clamp(Abs::zero(), max_radius), radius.get(side.end_corner()).clamp(Abs::zero(), max_radius), connection, ); if !continuous { res.push((mem::take(&mut path), stroke.get(side))); } } if always_continuous { path.close_path(); } if !path.0.is_empty() { res.push((path, stroke.left)); } res } /// Draws one side of the rounded rectangle. Will always draw the left arc. The /// right arc will be drawn halfway if and only if there is no connection. fn draw_side( path: &mut Path, side: Side, size: Size, start_radius: Abs, end_radius: Abs, connection: Connection, ) { let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 }); let length = size.get(side.axis()); // The arcs for a border of the rectangle along the x-axis, starting at (0,0). let p1 = Point::with_x(start_radius); let mut arc1 = bezier_arc( p1 + Point::new( -angle_left.sin() * start_radius, (1.0 - angle_left.cos()) * start_radius, ), Point::new(start_radius, start_radius), p1, ); let p2 = Point::with_x(length - end_radius); let mut arc2 = bezier_arc( p2, Point::new(length - end_radius, end_radius), p2 + Point::new( angle_right.sin() * end_radius, (1.0 - angle_right.cos()) * end_radius, ), ); let transform = match side { Side::Left => Transform::rotate(Angle::deg(-90.0)) .post_concat(Transform::translate(Abs::zero(), size.y)), Side::Bottom => Transform::rotate(Angle::deg(180.0)) .post_concat(Transform::translate(size.x, size.y)), Side::Right => Transform::rotate(Angle::deg(90.0)) .post_concat(Transform::translate(size.x, Abs::zero())), _ => Transform::identity(), }; arc1 = arc1.map(|x| x.transform(transform)); arc2 = arc2.map(|x| x.transform(transform)); if !connection.prev { path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] }); } if !start_radius.is_zero() { path.cubic_to(arc1[1], arc1[2], arc1[3]); } path.line_to(arc2[0]); if !connection.next && !end_radius.is_zero() { path.cubic_to(arc2[1], arc2[2], arc2[3]); } } /// Get the control points for a bezier curve that describes a circular arc for /// a start point, an end point and a center of the circle whose arc connects /// the two. fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] { // https://stackoverflow.com/a/44829356/1567835 let a = start - center; let b = end - center; let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); [start, control_1, control_2, end] } /// Indicates which sides of the border strokes in a 2D polygon are connected to /// their neighboring sides. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] struct Connection { prev: bool, next: bool, } impl Connection { /// Advance to the next clockwise side of the polygon. The argument /// indicates whether the border is connected on the right side of the next /// edge. pub fn advance(self, next: bool) -> Self { Self { prev: self.next, next } } }