Skip to main content

kurbo/
ellipse.rs

1// Copyright 2020 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Implementation of ellipse shape.
5
6use core::f64::consts::PI;
7use core::{
8    iter,
9    ops::{Add, Mul, Sub},
10};
11
12use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2};
13
14#[cfg(not(feature = "std"))]
15use 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))]
21pub 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
29impl 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(always)]
63    pub const 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: impl Into<Point>) -> Ellipse {
71        let Point { x: cx, y: cy } = new_center.into();
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    #[inline]
79    #[must_use]
80    pub fn with_radii(self, new_radii: Vec2) -> Ellipse {
81        let rotation = self.inner.svd().1;
82        let translation = self.inner.translation();
83        Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation)
84    }
85
86    /// Create a new `Ellipse`, with the rotation replaced by `rotation`
87    /// radians.
88    ///
89    /// The rotation is clockwise, for a y-down coordinate system. For more
90    /// on rotation, See [`Affine::rotate`].
91    #[inline]
92    #[must_use]
93    pub fn with_rotation(self, rotation: f64) -> Ellipse {
94        let scale = self.inner.svd().0;
95        let translation = self.inner.translation();
96        Ellipse::private_new(translation, scale.x, scale.y, rotation)
97    }
98
99    /// This gives us an internal method without any type conversions.
100    #[inline]
101    fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse {
102        // Since the circle is symmetric about the x and y axes, using absolute values for the
103        // radii results in the same ellipse. For simplicity we make this change here.
104        Ellipse {
105            inner: Affine::translate(center)
106                * Affine::rotate(x_rotation)
107                * Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()),
108        }
109    }
110
111    // Getters and setters.
112
113    /// Returns the center of this ellipse.
114    #[inline(always)]
115    pub fn center(&self) -> Point {
116        self.inner.translation().to_point()
117    }
118
119    /// Returns the two radii of this ellipse.
120    ///
121    /// The first number is the horizontal radius and the second is the vertical
122    /// radius, before rotation.
123    ///
124    /// If you are only interested in the value of the greatest or smallest radius of this ellipse,
125    /// consider using [`Ellipse::major_radius`] or [`Ellipse::minor_radius`] instead.
126    #[inline]
127    pub fn radii(&self) -> Vec2 {
128        self.inner.svd().0
129    }
130
131    /// Returns the major radius of this ellipse.
132    ///
133    /// This metric is also known as the semi-major axis.
134    #[inline]
135    pub fn major_radius(&self) -> f64 {
136        self.inner.svd().0.x
137    }
138
139    /// Returns the minor radius of this ellipse.
140    ///
141    /// This metric is also known as the semi-minor axis.
142    #[inline]
143    pub fn minor_radius(&self) -> f64 {
144        self.inner.svd().0.y
145    }
146
147    /// The ellipse's rotation, in radians.
148    ///
149    /// This allows all possible ellipses to be drawn by always starting with
150    /// an ellipse with the two radii on the x and y axes.
151    #[inline]
152    pub fn rotation(&self) -> f64 {
153        self.inner.svd().1
154    }
155
156    /// Returns the radii and the rotation of this ellipse.
157    ///
158    /// Equivalent to `(self.radii(), self.rotation())` but more efficient.
159    #[inline]
160    pub fn radii_and_rotation(&self) -> (Vec2, f64) {
161        self.inner.svd()
162    }
163
164    /// Is this ellipse [finite]?
165    ///
166    /// [finite]: f64::is_finite
167    #[inline]
168    pub const fn is_finite(&self) -> bool {
169        self.inner.is_finite()
170    }
171
172    /// Is this ellipse [NaN]?
173    ///
174    /// [NaN]: f64::is_nan
175    #[inline]
176    pub const fn is_nan(&self) -> bool {
177        self.inner.is_nan()
178    }
179}
180
181impl Add<Vec2> for Ellipse {
182    type Output = Ellipse;
183
184    /// In this context adding a `Vec2` applies the corresponding translation to the ellipse.
185    #[inline]
186    #[allow(clippy::suspicious_arithmetic_impl)]
187    fn add(self, v: Vec2) -> Ellipse {
188        Ellipse {
189            inner: Affine::translate(v) * self.inner,
190        }
191    }
192}
193
194impl Sub<Vec2> for Ellipse {
195    type Output = Ellipse;
196
197    /// In this context subtracting a `Vec2` applies the corresponding translation to the ellipse.
198    #[inline]
199    fn sub(self, v: Vec2) -> Ellipse {
200        Ellipse {
201            inner: Affine::translate(-v) * self.inner,
202        }
203    }
204}
205
206impl Mul<Ellipse> for Affine {
207    type Output = Ellipse;
208    #[inline]
209    fn mul(self, other: Ellipse) -> Self::Output {
210        Ellipse {
211            inner: self * other.inner,
212        }
213    }
214}
215
216impl From<Circle> for Ellipse {
217    #[inline]
218    fn from(circle: Circle) -> Self {
219        Ellipse::new(circle.center, Vec2::splat(circle.radius), 0.0)
220    }
221}
222
223impl Shape for Ellipse {
224    type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
225
226    fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
227        let (radii, x_rotation) = self.inner.svd();
228        Arc {
229            center: self.center(),
230            radii,
231            start_angle: 0.0,
232            sweep_angle: 2.0 * PI,
233            x_rotation,
234        }
235        .path_elements(tolerance)
236    }
237
238    /// The area of the ellipse.
239    ///
240    /// This is always positive if the result is finite.
241    ///
242    /// If non-finite, this will be [`f64::INFINITY`] or [`f64::NAN`] depending on whether the
243    /// inner affine's [determinant](Affine::determinant) is [`f64::INFINITY`] (either positive or
244    /// negative) or [`f64::NAN`].
245    #[inline]
246    fn area(&self) -> f64 {
247        // `Ellipse` is represented as a unit circle transformed by `Affine`. The transformed area
248        // of a region is `area * |det(affine)|`, see
249        // <https://en.wikipedia.org/w/index.php?title=Determinant&oldid=1344268205#Geometric_meaning>.
250        //
251        // A unit circle has area `PI`. Therefore, the area of this ellipse is PI multiplied by the
252        // affine's determinant.
253        PI * self.inner.determinant().abs()
254    }
255
256    /// Approximate the ellipse perimeter.
257    ///
258    /// This uses a numerical approximation. The absolute error between the calculated perimeter
259    /// and the true perimeter is bounded by `accuracy` (modulo floating point rounding errors).
260    ///
261    /// For circular ellipses (equal horizontal and vertical radii), the calculated perimeter is
262    /// exact.
263    #[inline]
264    fn perimeter(&self, accuracy: f64) -> f64 {
265        let radii = self.radii();
266
267        // Note: the radii are calculated from an inner affine map (`self.inner`), and may be NaN.
268        // Currently, constructing an ellipse with infinite radii will produce an ellipse whose
269        // calculated radii are NaN.
270        if !radii.is_finite() {
271            return f64::NAN;
272        }
273
274        // Check for the trivial case where the ellipse has one of its radii equal to 0, i.e.,
275        // where it describes a line, as the numerical method used breaks down with this extreme.
276        if radii.x == 0. || radii.y == 0. {
277            return 4. * f64::max(radii.x, radii.y);
278        }
279
280        // Evaluate an approximation based on a truncated infinite series. If it returns a good
281        // enough value, we do not need to iterate.
282        if kummer_elliptic_perimeter_range(radii) <= accuracy {
283            return kummer_elliptic_perimeter(radii);
284        }
285
286        agm_elliptic_perimeter(accuracy, radii)
287    }
288
289    #[inline]
290    fn winding(&self, pt: Point) -> i32 {
291        // Strategy here is to apply the inverse map to the point and see if it is in the unit
292        // circle.
293        let inv = self.inner.inverse();
294        if (inv * pt).to_vec2().hypot2() < 1.0 {
295            1
296        } else {
297            0
298        }
299    }
300
301    // Compute a tight bounding box of the ellipse.
302    //
303    // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
304    // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
305    // (a, b) and (c, d) if the affine map is
306    //
307    //  a | c | e
308    // -----------
309    //  b | d | f
310    //
311    //  We can then use the method in the link with the translation to get the bounding box.
312    #[inline]
313    fn bounding_box(&self) -> Rect {
314        let aff = self.inner.as_coeffs();
315        let a2 = aff[0] * aff[0];
316        let b2 = aff[1] * aff[1];
317        let c2 = aff[2] * aff[2];
318        let d2 = aff[3] * aff[3];
319        let cx = aff[4];
320        let cy = aff[5];
321        let range_x = (a2 + c2).sqrt();
322        let range_y = (b2 + d2).sqrt();
323        Rect {
324            x0: cx - range_x,
325            y0: cy - range_y,
326            x1: cx + range_x,
327            y1: cy + range_y,
328        }
329    }
330}
331
332/// Calculates circumference C of an ellipse with radii (x, y) as the infinite series
333///
334/// C = pi (x+y) * ∑ binom(1/2, n)^2 * h^n from n = 0 to inf
335/// with h = (x - y)^2 / (x + y)^2
336/// and binom(.,.) the binomial coefficient
337///
338/// as described by Kummer ("Über die Hypergeometrische Reihe", 1837) and rediscovered by
339/// Linderholm and Segal ("An Overlooked Series for the Elliptic Perimeter", 1995).
340///
341/// The series converges very quickly for ellipses with only moderate eccentricity (`h` not close
342/// to 1).
343///
344/// The series is truncated to the sixth power, meaning a lower bound on the true value is
345/// returned. Adding the value of [`kummer_elliptic_perimeter_range`] to the value returned by this
346/// function calculates an upper bound on the true value.
347#[inline]
348fn kummer_elliptic_perimeter(radii: Vec2) -> f64 {
349    let Vec2 { x, y } = radii;
350    let h = ((x - y) / (x + y)).powi(2);
351    let h2 = h * h;
352    let h3 = h2 * h;
353    let h4 = h3 * h;
354    let h5 = h4 * h;
355    let h6 = h5 * h;
356
357    let lower = PI
358        + h * (PI / 4.)
359        + h2 * (PI / 64.)
360        + h3 * (PI / 256.)
361        + h4 * (PI * 25. / 16384.)
362        + h5 * (PI * 49. / 65536.)
363        + h6 * (PI * 441. / 1048576.);
364
365    (x + y) * lower
366}
367
368/// This calculates the error range of [`kummer_elliptic_perimeter`]. That function returns a lower
369/// bound on the true value, and though we do not know what the remainder of the infinite series
370/// sums to, we can calculate an upper bound:
371///
372/// ∑ binom(1/2, n)^2 for n = 0 to inf
373///   = 1 + (1 / 2!!)^2 + (1!! / 4!!)^2 + (3!! / 6!!)^2 + (5!! / 8!!)^2 + ..
374///   = 4 / pi
375///   with !! the double factorial
376/// (equation 274 in "Summation of Series", L. B. W. Jolley, 1961).
377///
378/// This means the remainder of the infinite series for C, assuming the series was truncated to the
379/// mth term and h = 1, sums to
380/// 4 / pi - ∑ binom(1/2, n)^2 for n = 0 to m-1
381///
382/// As 0 ≤ h ≤ 1, this is an upper bound.
383#[inline]
384fn kummer_elliptic_perimeter_range(radii: Vec2) -> f64 {
385    let Vec2 { x, y } = radii;
386    let h = ((x - y) / (x + y)).powi(2);
387
388    const BINOM_SQUARED_REMAINDER: f64 = 0.00101416479131503;
389    // = 4. / PI - (1. + 1. / 4. + 1. / 64. + 1. / 256. + 25. / 16384. + 49. / 65536. + 441. / 1048576.)
390
391    PI * BINOM_SQUARED_REMAINDER * h.powi(7) * (x + y)
392}
393
394/// Calculates circumference C of an ellipse with radii (x, y) using the arithmetic-geometric mean,
395/// as described in equation 19.8.6 of
396/// <https://web.archive.org/web/20240926233336/https://dlmf.nist.gov/19.8#i>.
397fn agm_elliptic_perimeter(accuracy: f64, radii: Vec2) -> f64 {
398    let Vec2 { x, y } = if radii.x >= radii.y {
399        radii
400    } else {
401        Vec2::new(radii.y, radii.x)
402    };
403
404    let accuracy = accuracy / (2. * PI * x);
405
406    let mut sum = 1.;
407    let mut a = 1.;
408    let mut g = y / x;
409    let mut c = (1. - g.powi(2)).sqrt();
410    let mut mul = 0.5;
411
412    loop {
413        let c2 = c.powi(2);
414        // term = 2^(n-1) c_n^2
415        let term = mul * c2;
416        sum -= term;
417
418        // We have c_(n+1) ≤ 1/2 c_n
419        // (for a derivation, see e.g. section 2.1 of  "Elliptic integrals, the
420        // arithmetic-geometric mean and the Brent-Salamin algorithm for π" by G.J.O. Jameson:
421        // <https://web.archive.org/web/20241002140956/https://www.maths.lancs.ac.uk/jameson/ellagm.pdf>)
422        //
423        // Therefore
424        // ∑ 2^(i-1) c_i^2 from i = 1 ≤ ∑ 2^(i-1) ((1/2)^i c_0)^2 from i = 1
425        //                            = ∑ 2^-(i+1) c_0^2          from i = 1
426        //                            = 1/2 c_0^2
427        //
428        // or, for arbitrary starting point i = n+1:
429        // ∑ 2^(i-1) c_i^2 from i = n+1 ≤ ∑ 2^(i-1) ((1/2)^(i-n) c_n)^2 from i = n+1
430        //                              = ∑ 2^(2n - i - 1) c_n^2        from i = n+1
431        //                              = 2^(2n) ∑ 2^(-(i+1)) c_n^2     from i = n+1
432        //                              = 2^(2n) 2^(-(n+1)) c_n^2
433        //                              = 2^(n-1) c_n^2
434        //
435        // Therefore, the remainder of the series sums to less than or equal to 2^(n-1) c_n^2,
436        // which is exactly the value of the nth term.
437        //
438        // Furthermore, a_m ≥ g_n, and g_n ≤ 1, for all m, n.
439        if term <= accuracy * g {
440            // `sum` currently overestimates the true value - subtract the upper bound of the
441            // remaining series. We will then underestimate the true value, but by no more than
442            // `accuracy`.
443            sum -= term;
444            break;
445        }
446
447        mul *= 2.;
448        // This is equal to c_next = c^2 / (4 * a_next)
449        c = (a - g) / 2.;
450        let a_next = (a + g) / 2.;
451        g = (a * g).sqrt();
452        a = a_next;
453    }
454
455    2. * PI * x / a * sum
456}
457
458#[cfg(test)]
459mod tests {
460    use crate::{Circle, Ellipse, Point, Shape};
461    use std::f64::consts::PI;
462
463    fn assert_approx_eq(x: f64, y: f64) {
464        // Note: we might want to be more rigorous in testing the accuracy
465        // of the conversion into Béziers. But this seems good enough.
466        assert!((x - y).abs() < 1e-7, "{x} != {y}");
467    }
468
469    #[test]
470    fn circular_perimeter() {
471        for radius in [1.0, 0., 1.5, PI, 10.0, -1.0, 1_234_567_890.1] {
472            let circle = Circle::new((0., 0.), radius);
473            let ellipse = Ellipse::new((0., 0.), (radius, radius), 0.);
474
475            let circle_p = circle.perimeter(0.);
476            let ellipse_p = ellipse.perimeter(0.1);
477
478            // We expect the results to be identical, modulo potential roundoff errors. Compare
479            // with a very small relative epsilon.
480            let epsilon = f64::EPSILON * 8. * circle_p.max(ellipse_p);
481            assert!(
482                (circle_p - ellipse_p).abs() <= epsilon,
483                "Expected circular ellipse radius {ellipse_p} to be equal to circle radius {circle_p} for radius {radius}"
484            );
485        }
486    }
487
488    #[test]
489    fn compare_perimeter_with_bez() {
490        const EPSILON: f64 = 0.000_002;
491
492        for radii in [
493            (0.5, 1.),
494            (2., 1.),
495            (0.000_000_1, 1.),
496            (0., 1.),
497            (1., 0.),
498            (-0.5, 1.),
499            (-0.000_000_1, -1.),
500        ] {
501            let ellipse = Ellipse::new((0., 0.), radii, 0.);
502
503            let ellipse_p = ellipse.perimeter(0.000_001);
504            let bez_p = ellipse.path_segments(0.000_000_25).perimeter(0.000_000_25);
505
506            assert!(
507                (ellipse_p - bez_p).abs() < EPSILON,
508                "Numerically approximated ellipse perimeter ({ellipse_p}) does not match bezier segment perimeter length ({bez_p}) for radii {radii:?}"
509            );
510        }
511    }
512
513    #[test]
514    fn known_perimeter() {
515        const ACCURACY: f64 = 0.000_000_000_001;
516
517        for (radii, perimeter) in [
518            ((0.5, 1.), 4.844_224_110_273_838),
519            ((0.001, 1.), 4.000_015_588_104_688),
520        ] {
521            let ellipse = Ellipse::new((0., 0.), radii, 0.);
522
523            let ellipse_p = ellipse.perimeter(ACCURACY);
524            assert!(
525                (ellipse_p - perimeter).abs() <= ACCURACY,
526                "Numerically approximated ellipse perimeter ({ellipse_p}) does not match known perimeter ({perimeter}) radii {radii:?}"
527            );
528        }
529    }
530
531    #[test]
532    fn area_sign() {
533        let center = Point::new(5.0, 5.0);
534        let e = Ellipse::new(center, (5.0, 5.0), 1.0);
535        assert_approx_eq(e.area(), 25.0 * PI);
536        let e = Ellipse::new(center, (5.0, 10.0), 1.0);
537        assert_approx_eq(e.area(), 50.0 * PI);
538
539        assert_eq!(e.winding(center), 1);
540
541        let p = e.to_path(1e-9);
542        assert_approx_eq(e.area(), p.area());
543        assert_eq!(e.winding(center), p.winding(center));
544
545        let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
546        assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
547
548        assert_eq!(e_neg_radius.winding(center), 1);
549
550        let p_neg_radius = e_neg_radius.to_path(1e-9);
551        assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
552        assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
553    }
554}