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 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 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 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 #[inline]
239 fn area(&self) -> f64 {
240 let Vec2 { x, y } = self.radii();
241 PI * x * y
242 }
243
244 /// Approximate the ellipse perimeter.
245 ///
246 /// This uses a numerical approximation. The absolute error between the calculated perimeter
247 /// and the true perimeter is bounded by `accuracy` (modulo floating point rounding errors).
248 ///
249 /// For circular ellipses (equal horizontal and vertical radii), the calculated perimeter is
250 /// exact.
251 #[inline]
252 fn perimeter(&self, accuracy: f64) -> f64 {
253 let radii = self.radii();
254
255 // Note: the radii are calculated from an inner affine map (`self.inner`), and may be NaN.
256 // Currently, constructing an ellipse with infinite radii will produce an ellipse whose
257 // calculated radii are NaN.
258 if !self.radii().is_finite() {
259 return f64::NAN;
260 }
261
262 // Check for the trivial case where the ellipse has one of its radii equal to 0, i.e.,
263 // where it describes a line, as the numerical method used breaks down with this extreme.
264 if radii.x == 0. || radii.y == 0. {
265 return 4. * f64::max(radii.x, radii.y);
266 }
267
268 // Evaluate an approximation based on a truncated infinite series. If it returns a good
269 // enough value, we do not need to iterate.
270 if kummer_elliptic_perimeter_range(radii) <= accuracy {
271 return kummer_elliptic_perimeter(radii);
272 }
273
274 agm_elliptic_perimeter(accuracy, radii)
275 }
276
277 #[inline]
278 fn winding(&self, pt: Point) -> i32 {
279 // Strategy here is to apply the inverse map to the point and see if it is in the unit
280 // circle.
281 let inv = self.inner.inverse();
282 if (inv * pt).to_vec2().hypot2() < 1.0 {
283 1
284 } else {
285 0
286 }
287 }
288
289 // Compute a tight bounding box of the ellipse.
290 //
291 // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
292 // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
293 // (a, b) and (c, d) if the affine map is
294 //
295 // a | c | e
296 // -----------
297 // b | d | f
298 //
299 // We can then use the method in the link with the translation to get the bounding box.
300 #[inline]
301 fn bounding_box(&self) -> Rect {
302 let aff = self.inner.as_coeffs();
303 let a2 = aff[0] * aff[0];
304 let b2 = aff[1] * aff[1];
305 let c2 = aff[2] * aff[2];
306 let d2 = aff[3] * aff[3];
307 let cx = aff[4];
308 let cy = aff[5];
309 let range_x = (a2 + c2).sqrt();
310 let range_y = (b2 + d2).sqrt();
311 Rect {
312 x0: cx - range_x,
313 y0: cy - range_y,
314 x1: cx + range_x,
315 y1: cy + range_y,
316 }
317 }
318}
319
320/// Calculates circumference C of an ellipse with radii (x, y) as the infinite series
321///
322/// C = pi (x+y) * ∑ binom(1/2, n)^2 * h^n from n = 0 to inf
323/// with h = (x - y)^2 / (x + y)^2
324/// and binom(.,.) the binomial coefficient
325///
326/// as described by Kummer ("Über die Hypergeometrische Reihe", 1837) and rediscovered by
327/// Linderholm and Segal ("An Overlooked Series for the Elliptic Perimeter", 1995).
328///
329/// The series converges very quickly for ellipses with only moderate eccentricity (`h` not close
330/// to 1).
331///
332/// The series is truncated to the sixth power, meaning a lower bound on the true value is
333/// returned. Adding the value of [`kummer_elliptic_perimeter_range`] to the value returned by this
334/// function calculates an upper bound on the true value.
335#[inline]
336fn kummer_elliptic_perimeter(radii: Vec2) -> f64 {
337 let Vec2 { x, y } = radii;
338 let h = ((x - y) / (x + y)).powi(2);
339 let h2 = h * h;
340 let h3 = h2 * h;
341 let h4 = h3 * h;
342 let h5 = h4 * h;
343 let h6 = h5 * h;
344
345 let lower = PI
346 + h * (PI / 4.)
347 + h2 * (PI / 64.)
348 + h3 * (PI / 256.)
349 + h4 * (PI * 25. / 16384.)
350 + h5 * (PI * 49. / 65536.)
351 + h6 * (PI * 441. / 1048576.);
352
353 (x + y) * lower
354}
355
356/// This calculates the error range of [`kummer_elliptic_perimeter`]. That function returns a lower
357/// bound on the true value, and though we do not know what the remainder of the infinite series
358/// sums to, we can calculate an upper bound:
359///
360/// ∑ binom(1/2, n)^2 for n = 0 to inf
361/// = 1 + (1 / 2!!)^2 + (1!! / 4!!)^2 + (3!! / 6!!)^2 + (5!! / 8!!)^2 + ..
362/// = 4 / pi
363/// with !! the double factorial
364/// (equation 274 in "Summation of Series", L. B. W. Jolley, 1961).
365///
366/// This means the remainder of the infinite series for C, assuming the series was truncated to the
367/// mth term and h = 1, sums to
368/// 4 / pi - ∑ binom(1/2, n)^2 for n = 0 to m-1
369///
370/// As 0 ≤ h ≤ 1, this is an upper bound.
371#[inline]
372fn kummer_elliptic_perimeter_range(radii: Vec2) -> f64 {
373 let Vec2 { x, y } = radii;
374 let h = ((x - y) / (x + y)).powi(2);
375
376 const BINOM_SQUARED_REMAINDER: f64 = 0.00101416479131503;
377 // = 4. / PI - (1. + 1. / 4. + 1. / 64. + 1. / 256. + 25. / 16384. + 49. / 65536. + 441. / 1048576.)
378
379 PI * BINOM_SQUARED_REMAINDER * h.powi(7) * (x + y)
380}
381
382/// Calculates circumference C of an ellipse with radii (x, y) using the arithmetic-geometric mean,
383/// as described in equation 19.8.6 of
384/// <https://web.archive.org/web/20240926233336/https://dlmf.nist.gov/19.8#i>.
385fn agm_elliptic_perimeter(accuracy: f64, radii: Vec2) -> f64 {
386 let Vec2 { x, y } = if radii.x >= radii.y {
387 radii
388 } else {
389 Vec2::new(radii.y, radii.x)
390 };
391
392 let accuracy = accuracy / (2. * PI * x);
393
394 let mut sum = 1.;
395 let mut a = 1.;
396 let mut g = y / x;
397 let mut c = (1. - g.powi(2)).sqrt();
398 let mut mul = 0.5;
399
400 loop {
401 let c2 = c.powi(2);
402 // term = 2^(n-1) c_n^2
403 let term = mul * c2;
404 sum -= term;
405
406 // We have c_(n+1) ≤ 1/2 c_n
407 // (for a derivation, see e.g. section 2.1 of "Elliptic integrals, the
408 // arithmetic-geometric mean and the Brent-Salamin algorithm for π" by G.J.O. Jameson:
409 // <https://web.archive.org/web/20241002140956/https://www.maths.lancs.ac.uk/jameson/ellagm.pdf>)
410 //
411 // Therefore
412 // ∑ 2^(i-1) c_i^2 from i = 1 ≤ ∑ 2^(i-1) ((1/2)^i c_0)^2 from i = 1
413 // = ∑ 2^-(i+1) c_0^2 from i = 1
414 // = 1/2 c_0^2
415 //
416 // or, for arbitrary starting point i = n+1:
417 // ∑ 2^(i-1) c_i^2 from i = n+1 ≤ ∑ 2^(i-1) ((1/2)^(i-n) c_n)^2 from i = n+1
418 // = ∑ 2^(2n - i - 1) c_n^2 from i = n+1
419 // = 2^(2n) ∑ 2^(-(i+1)) c_n^2 from i = n+1
420 // = 2^(2n) 2^(-(n+1)) c_n^2
421 // = 2^(n-1) c_n^2
422 //
423 // Therefore, the remainder of the series sums to less than or equal to 2^(n-1) c_n^2,
424 // which is exactly the value of the nth term.
425 //
426 // Furthermore, a_m ≥ g_n, and g_n ≤ 1, for all m, n.
427 if term <= accuracy * g {
428 // `sum` currently overestimates the true value - subtract the upper bound of the
429 // remaining series. We will then underestimate the true value, but by no more than
430 // `accuracy`.
431 sum -= term;
432 break;
433 }
434
435 mul *= 2.;
436 // This is equal to c_next = c^2 / (4 * a_next)
437 c = (a - g) / 2.;
438 let a_next = (a + g) / 2.;
439 g = (a * g).sqrt();
440 a = a_next;
441 }
442
443 2. * PI * x / a * sum
444}
445
446#[cfg(test)]
447mod tests {
448 use crate::{Circle, Ellipse, Point, Shape};
449 use std::f64::consts::PI;
450
451 fn assert_approx_eq(x: f64, y: f64) {
452 // Note: we might want to be more rigorous in testing the accuracy
453 // of the conversion into Béziers. But this seems good enough.
454 assert!((x - y).abs() < 1e-7, "{x} != {y}");
455 }
456
457 #[test]
458 fn circular_perimeter() {
459 for radius in [1.0, 0., 1.5, PI, 10.0, -1.0, 1_234_567_890.1] {
460 let circle = Circle::new((0., 0.), radius);
461 let ellipse = Ellipse::new((0., 0.), (radius, radius), 0.);
462
463 let circle_p = circle.perimeter(0.);
464 let ellipse_p = ellipse.perimeter(0.1);
465
466 // We expect the results to be identical, modulo potential roundoff errors. Compare
467 // with a very small relative epsilon.
468 let epsilon = f64::EPSILON * 8. * circle_p.max(ellipse_p);
469 assert!(
470 (circle_p - ellipse_p).abs() <= epsilon,
471 "Expected circular ellipse radius {ellipse_p} to be equal to circle radius {circle_p} for radius {radius}"
472 );
473 }
474 }
475
476 #[test]
477 fn compare_perimeter_with_bez() {
478 const EPSILON: f64 = 0.000_002;
479
480 for radii in [
481 (0.5, 1.),
482 (2., 1.),
483 (0.000_000_1, 1.),
484 (0., 1.),
485 (1., 0.),
486 (-0.5, 1.),
487 (-0.000_000_1, -1.),
488 ] {
489 let ellipse = Ellipse::new((0., 0.), radii, 0.);
490
491 let ellipse_p = ellipse.perimeter(0.000_001);
492 let bez_p = ellipse.path_segments(0.000_000_25).perimeter(0.000_000_25);
493
494 assert!(
495 (ellipse_p - bez_p).abs() < EPSILON,
496 "Numerically approximated ellipse perimeter ({ellipse_p}) does not match bezier segment perimeter length ({bez_p}) for radii {radii:?}"
497 );
498 }
499 }
500
501 #[test]
502 fn known_perimeter() {
503 const ACCURACY: f64 = 0.000_000_000_001;
504
505 for (radii, perimeter) in [
506 ((0.5, 1.), 4.844_224_110_273_838),
507 ((0.001, 1.), 4.000_015_588_104_688),
508 ] {
509 let ellipse = Ellipse::new((0., 0.), radii, 0.);
510
511 let ellipse_p = ellipse.perimeter(ACCURACY);
512 assert!(
513 (ellipse_p - perimeter).abs() <= ACCURACY,
514 "Numerically approximated ellipse perimeter ({ellipse_p}) does not match known perimeter ({perimeter}) radii {radii:?}"
515 );
516 }
517 }
518
519 #[test]
520 fn area_sign() {
521 let center = Point::new(5.0, 5.0);
522 let e = Ellipse::new(center, (5.0, 5.0), 1.0);
523 assert_approx_eq(e.area(), 25.0 * PI);
524 let e = Ellipse::new(center, (5.0, 10.0), 1.0);
525 assert_approx_eq(e.area(), 50.0 * PI);
526
527 assert_eq!(e.winding(center), 1);
528
529 let p = e.to_path(1e-9);
530 assert_approx_eq(e.area(), p.area());
531 assert_eq!(e.winding(center), p.winding(center));
532
533 let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
534 assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
535
536 assert_eq!(e_neg_radius.winding(center), 1);
537
538 let p_neg_radius = e_neg_radius.to_path(1e-9);
539 assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
540 assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
541 }
542}