| 1 | // Copyright 2020 the Kurbo Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | |
| 4 | //! Implementation of ellipse shape. |
| 5 | |
| 6 | use core::f64::consts::PI; |
| 7 | use core::{ |
| 8 | iter, |
| 9 | ops::{Add, Mul, Sub}, |
| 10 | }; |
| 11 | |
| 12 | use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2}; |
| 13 | |
| 14 | #[cfg (not(feature = "std" ))] |
| 15 | use crate::common::FloatFuncs; |
| 16 | |
| 17 | /// An ellipse. |
| 18 | #[derive (Clone, Copy, Default, Debug, PartialEq)] |
| 19 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
| 20 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
| 21 | pub struct Ellipse { |
| 22 | /// All ellipses can be represented as an affine map of the unit circle, |
| 23 | /// centred at (0, 0). Therefore we can store the ellipse as an affine map, |
| 24 | /// with the implication that it be applied to the unit circle to recover the |
| 25 | /// actual shape. |
| 26 | inner: Affine, |
| 27 | } |
| 28 | |
| 29 | impl Ellipse { |
| 30 | /// Create A new ellipse with a given center, radii, and rotation. |
| 31 | /// |
| 32 | /// The returned ellipse will be the result of taking a circle, stretching |
| 33 | /// it by the `radii` along the x and y axes, then rotating it from the |
| 34 | /// x axis by `rotation` radians, before finally translating the center |
| 35 | /// to `center`. |
| 36 | /// |
| 37 | /// Rotation is clockwise in a y-down coordinate system. For more on |
| 38 | /// rotation, see [`Affine::rotate`]. |
| 39 | #[inline ] |
| 40 | pub fn new(center: impl Into<Point>, radii: impl Into<Vec2>, x_rotation: f64) -> Ellipse { |
| 41 | let Point { x: cx, y: cy } = center.into(); |
| 42 | let Vec2 { x: rx, y: ry } = radii.into(); |
| 43 | Ellipse::private_new(Vec2 { x: cx, y: cy }, rx, ry, x_rotation) |
| 44 | } |
| 45 | |
| 46 | /// Returns the largest ellipse that can be bounded by this [`Rect`]. |
| 47 | /// |
| 48 | /// This uses the absolute width and height of the rectangle. |
| 49 | /// |
| 50 | /// This ellipse is always axis-aligned; to apply rotation you can call |
| 51 | /// [`with_rotation`] with the result. |
| 52 | /// |
| 53 | /// [`with_rotation`]: Ellipse::with_rotation |
| 54 | #[inline ] |
| 55 | pub fn from_rect(rect: Rect) -> Self { |
| 56 | let center = rect.center().to_vec2(); |
| 57 | let Size { width, height } = rect.size() / 2.0; |
| 58 | Ellipse::private_new(center, width, height, 0.0) |
| 59 | } |
| 60 | |
| 61 | /// Create an ellipse from an affine transformation of the unit circle. |
| 62 | #[inline ] |
| 63 | pub fn from_affine(affine: Affine) -> Self { |
| 64 | Ellipse { inner: affine } |
| 65 | } |
| 66 | |
| 67 | /// Create a new `Ellipse` centered on the provided point. |
| 68 | #[inline ] |
| 69 | #[must_use ] |
| 70 | pub fn with_center(self, new_center: Point) -> Ellipse { |
| 71 | let Point { x: cx, y: cy } = new_center; |
| 72 | Ellipse { |
| 73 | inner: self.inner.with_translation(Vec2 { x: cx, y: cy }), |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /// Create a new `Ellipse` with the provided radii. |
| 78 | #[must_use ] |
| 79 | pub fn with_radii(self, new_radii: Vec2) -> Ellipse { |
| 80 | let rotation = self.inner.svd().1; |
| 81 | let translation = self.inner.translation(); |
| 82 | Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation) |
| 83 | } |
| 84 | |
| 85 | /// Create a new `Ellipse`, with the rotation replaced by `rotation` |
| 86 | /// radians. |
| 87 | /// |
| 88 | /// The rotation is clockwise, for a y-down coordinate system. For more |
| 89 | /// on rotation, See [`Affine::rotate`]. |
| 90 | #[must_use ] |
| 91 | pub fn with_rotation(self, rotation: f64) -> Ellipse { |
| 92 | let scale = self.inner.svd().0; |
| 93 | let translation = self.inner.translation(); |
| 94 | Ellipse::private_new(translation, scale.x, scale.y, rotation) |
| 95 | } |
| 96 | |
| 97 | #[deprecated (since = "0.7.0" , note = "use with_rotation instead" )] |
| 98 | #[must_use ] |
| 99 | #[doc (hidden)] |
| 100 | pub fn with_x_rotation(self, rotation_radians: f64) -> Ellipse { |
| 101 | self.with_rotation(rotation_radians) |
| 102 | } |
| 103 | |
| 104 | /// This gives us an internal method without any type conversions. |
| 105 | #[inline ] |
| 106 | fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse { |
| 107 | // Since the circle is symmetric about the x and y axes, using absolute values for the |
| 108 | // radii results in the same ellipse. For simplicity we make this change here. |
| 109 | Ellipse { |
| 110 | inner: Affine::translate(center) |
| 111 | * Affine::rotate(x_rotation) |
| 112 | * Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()), |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | // Getters and setters. |
| 117 | |
| 118 | /// Returns the center of this ellipse. |
| 119 | #[inline ] |
| 120 | pub fn center(&self) -> Point { |
| 121 | let Vec2 { x: cx, y: cy } = self.inner.translation(); |
| 122 | Point { x: cx, y: cy } |
| 123 | } |
| 124 | |
| 125 | /// Returns the two radii of this ellipse. |
| 126 | /// |
| 127 | /// The first number is the horizontal radius and the second is the vertical |
| 128 | /// radius, before rotation. |
| 129 | pub fn radii(&self) -> Vec2 { |
| 130 | self.inner.svd().0 |
| 131 | } |
| 132 | |
| 133 | /// The ellipse's rotation, in radians. |
| 134 | /// |
| 135 | /// This allows all possible ellipses to be drawn by always starting with |
| 136 | /// an ellipse with the two radii on the x and y axes. |
| 137 | pub fn rotation(&self) -> f64 { |
| 138 | self.inner.svd().1 |
| 139 | } |
| 140 | |
| 141 | /// Returns the radii and the rotation of this ellipse. |
| 142 | /// |
| 143 | /// Equivalent to `(self.radii(), self.rotation())` but more efficient. |
| 144 | pub fn radii_and_rotation(&self) -> (Vec2, f64) { |
| 145 | self.inner.svd() |
| 146 | } |
| 147 | |
| 148 | /// Is this ellipse [finite]? |
| 149 | /// |
| 150 | /// [finite]: f64::is_finite |
| 151 | #[inline ] |
| 152 | pub fn is_finite(&self) -> bool { |
| 153 | self.inner.is_finite() |
| 154 | } |
| 155 | |
| 156 | /// Is this ellipse [NaN]? |
| 157 | /// |
| 158 | /// [NaN]: f64::is_nan |
| 159 | #[inline ] |
| 160 | pub fn is_nan(&self) -> bool { |
| 161 | self.inner.is_nan() |
| 162 | } |
| 163 | |
| 164 | #[doc (hidden)] |
| 165 | #[deprecated (since = "0.7.0" , note = "use rotation() instead" )] |
| 166 | pub fn x_rotation(&self) -> f64 { |
| 167 | self.rotation() |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | impl Add<Vec2> for Ellipse { |
| 172 | type Output = Ellipse; |
| 173 | |
| 174 | /// In this context adding a `Vec2` applies the corresponding translation to the ellipse. |
| 175 | #[inline ] |
| 176 | #[allow (clippy::suspicious_arithmetic_impl)] |
| 177 | fn add(self, v: Vec2) -> Ellipse { |
| 178 | Ellipse { |
| 179 | inner: Affine::translate(v) * self.inner, |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | impl Sub<Vec2> for Ellipse { |
| 185 | type Output = Ellipse; |
| 186 | |
| 187 | /// In this context subtracting a `Vec2` applies the corresponding translation to the ellipse. |
| 188 | #[inline ] |
| 189 | fn sub(self, v: Vec2) -> Ellipse { |
| 190 | Ellipse { |
| 191 | inner: Affine::translate(-v) * self.inner, |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | impl Mul<Ellipse> for Affine { |
| 197 | type Output = Ellipse; |
| 198 | fn mul(self, other: Ellipse) -> Self::Output { |
| 199 | Ellipse { |
| 200 | inner: self * other.inner, |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | impl From<Circle> for Ellipse { |
| 206 | fn from(circle: Circle) -> Self { |
| 207 | Ellipse::new(circle.center, radii:Vec2::splat(circle.radius), x_rotation:0.0) |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | impl Shape for Ellipse { |
| 212 | type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>; |
| 213 | |
| 214 | fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> { |
| 215 | let (radii, x_rotation) = self.inner.svd(); |
| 216 | Arc { |
| 217 | center: self.center(), |
| 218 | radii, |
| 219 | start_angle: 0.0, |
| 220 | sweep_angle: 2.0 * PI, |
| 221 | x_rotation, |
| 222 | } |
| 223 | .path_elements(tolerance) |
| 224 | } |
| 225 | |
| 226 | #[inline ] |
| 227 | fn area(&self) -> f64 { |
| 228 | let Vec2 { x, y } = self.radii(); |
| 229 | PI * x * y |
| 230 | } |
| 231 | |
| 232 | #[inline ] |
| 233 | fn perimeter(&self, accuracy: f64) -> f64 { |
| 234 | // TODO rather than delegate to the bezier path, it is possible to use various series |
| 235 | // expansions to compute the perimeter to any accuracy. I believe Ramanujan authored the |
| 236 | // quickest to converge. See |
| 237 | // https://www.mathematica-journal.com/2009/11/23/on-the-perimeter-of-an-ellipse/ |
| 238 | // and https://en.wikipedia.org/wiki/Ellipse#Circumference |
| 239 | // |
| 240 | self.path_segments(0.1).perimeter(accuracy) |
| 241 | } |
| 242 | |
| 243 | fn winding(&self, pt: Point) -> i32 { |
| 244 | // Strategy here is to apply the inverse map to the point and see if it is in the unit |
| 245 | // circle. |
| 246 | let inv = self.inner.inverse(); |
| 247 | if (inv * pt).to_vec2().hypot2() < 1.0 { |
| 248 | 1 |
| 249 | } else { |
| 250 | 0 |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | // Compute a tight bounding box of the ellipse. |
| 255 | // |
| 256 | // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two |
| 257 | // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives |
| 258 | // (a, b) and (c, d) if the affine map is |
| 259 | // |
| 260 | // a | c | e |
| 261 | // ----------- |
| 262 | // b | d | f |
| 263 | // |
| 264 | // We can then use the method in the link with the translation to get the bounding box. |
| 265 | #[inline ] |
| 266 | fn bounding_box(&self) -> Rect { |
| 267 | let aff = self.inner.as_coeffs(); |
| 268 | let a2 = aff[0] * aff[0]; |
| 269 | let b2 = aff[1] * aff[1]; |
| 270 | let c2 = aff[2] * aff[2]; |
| 271 | let d2 = aff[3] * aff[3]; |
| 272 | let cx = aff[4]; |
| 273 | let cy = aff[5]; |
| 274 | let range_x = (a2 + c2).sqrt(); |
| 275 | let range_y = (b2 + d2).sqrt(); |
| 276 | Rect { |
| 277 | x0: cx - range_x, |
| 278 | y0: cy - range_y, |
| 279 | x1: cx + range_x, |
| 280 | y1: cy + range_y, |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | #[cfg (test)] |
| 286 | mod tests { |
| 287 | use crate::{Ellipse, Point, Shape}; |
| 288 | use std::f64::consts::PI; |
| 289 | |
| 290 | fn assert_approx_eq(x: f64, y: f64) { |
| 291 | // Note: we might want to be more rigorous in testing the accuracy |
| 292 | // of the conversion into Béziers. But this seems good enough. |
| 293 | assert!((x - y).abs() < 1e-7, "{x} != {y}" ); |
| 294 | } |
| 295 | |
| 296 | #[test ] |
| 297 | fn area_sign() { |
| 298 | let center = Point::new(5.0, 5.0); |
| 299 | let e = Ellipse::new(center, (5.0, 5.0), 1.0); |
| 300 | assert_approx_eq(e.area(), 25.0 * PI); |
| 301 | let e = Ellipse::new(center, (5.0, 10.0), 1.0); |
| 302 | assert_approx_eq(e.area(), 50.0 * PI); |
| 303 | |
| 304 | assert_eq!(e.winding(center), 1); |
| 305 | |
| 306 | let p = e.to_path(1e-9); |
| 307 | assert_approx_eq(e.area(), p.area()); |
| 308 | assert_eq!(e.winding(center), p.winding(center)); |
| 309 | |
| 310 | let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0); |
| 311 | assert_approx_eq(e_neg_radius.area(), 50.0 * PI); |
| 312 | |
| 313 | assert_eq!(e_neg_radius.winding(center), 1); |
| 314 | |
| 315 | let p_neg_radius = e_neg_radius.to_path(1e-9); |
| 316 | assert_approx_eq(e_neg_radius.area(), p_neg_radius.area()); |
| 317 | assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center)); |
| 318 | } |
| 319 | } |
| 320 | |