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 asix 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 #[inline]
150 pub fn is_finite(&self) -> bool {
151 self.inner.is_finite()
152 }
153
154 /// Is this ellipse NaN?
155 #[inline]
156 pub fn is_nan(&self) -> bool {
157 self.inner.is_nan()
158 }
159
160 #[doc(hidden)]
161 #[deprecated(since = "0.7.0", note = "use rotation() instead")]
162 pub fn x_rotation(&self) -> f64 {
163 self.rotation()
164 }
165}
166
167impl Add<Vec2> for Ellipse {
168 type Output = Ellipse;
169
170 /// In this context adding a `Vec2` applies the corresponding translation to the ellipse.
171 #[inline]
172 #[allow(clippy::suspicious_arithmetic_impl)]
173 fn add(self, v: Vec2) -> Ellipse {
174 Ellipse {
175 inner: Affine::translate(v) * self.inner,
176 }
177 }
178}
179
180impl Sub<Vec2> for Ellipse {
181 type Output = Ellipse;
182
183 /// In this context subtracting a `Vec2` applies the corresponding translation to the ellipse.
184 #[inline]
185 fn sub(self, v: Vec2) -> Ellipse {
186 Ellipse {
187 inner: Affine::translate(-v) * self.inner,
188 }
189 }
190}
191
192impl Mul<Ellipse> for Affine {
193 type Output = Ellipse;
194 fn mul(self, other: Ellipse) -> Self::Output {
195 Ellipse {
196 inner: self * other.inner,
197 }
198 }
199}
200
201impl From<Circle> for Ellipse {
202 fn from(circle: Circle) -> Self {
203 Ellipse::new(circle.center, radii:Vec2::splat(circle.radius), x_rotation:0.0)
204 }
205}
206
207impl Shape for Ellipse {
208 type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
209
210 fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
211 let (radii, x_rotation) = self.inner.svd();
212 Arc {
213 center: self.center(),
214 radii,
215 start_angle: 0.0,
216 sweep_angle: 2.0 * PI,
217 x_rotation,
218 }
219 .path_elements(tolerance)
220 }
221
222 #[inline]
223 fn area(&self) -> f64 {
224 let Vec2 { x, y } = self.radii();
225 PI * x * y
226 }
227
228 #[inline]
229 fn perimeter(&self, accuracy: f64) -> f64 {
230 // TODO rather than delegate to the bezier path, it is possible to use various series
231 // expansions to compute the perimeter to any accuracy. I believe Ramanujan authored the
232 // quickest to converge. See
233 // https://www.mathematica-journal.com/2009/11/23/on-the-perimeter-of-an-ellipse/
234 // and https://en.wikipedia.org/wiki/Ellipse#Circumference
235 //
236 self.path_segments(0.1).perimeter(accuracy)
237 }
238
239 fn winding(&self, pt: Point) -> i32 {
240 // Strategy here is to apply the inverse map to the point and see if it is in the unit
241 // circle.
242 let inv = self.inner.inverse();
243 if (inv * pt).to_vec2().hypot2() < 1.0 {
244 1
245 } else {
246 0
247 }
248 }
249
250 // Compute a tight bounding box of the ellipse.
251 //
252 // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
253 // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
254 // (a, b) and (c, d) if the affine map is
255 //
256 // a | c | e
257 // -----------
258 // b | d | f
259 //
260 // We can then use the method in the link with the translation to get the bounding box.
261 #[inline]
262 fn bounding_box(&self) -> Rect {
263 let aff = self.inner.as_coeffs();
264 let a2 = aff[0] * aff[0];
265 let b2 = aff[1] * aff[1];
266 let c2 = aff[2] * aff[2];
267 let d2 = aff[3] * aff[3];
268 let cx = aff[4];
269 let cy = aff[5];
270 let range_x = (a2 + c2).sqrt();
271 let range_y = (b2 + d2).sqrt();
272 Rect {
273 x0: cx - range_x,
274 y0: cy - range_y,
275 x1: cx + range_x,
276 y1: cy + range_y,
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use crate::{Ellipse, Point, Shape};
284 use std::f64::consts::PI;
285
286 fn assert_approx_eq(x: f64, y: f64) {
287 // Note: we might want to be more rigorous in testing the accuracy
288 // of the conversion into Béziers. But this seems good enough.
289 assert!((x - y).abs() < 1e-7, "{x} != {y}");
290 }
291
292 #[test]
293 fn area_sign() {
294 let center = Point::new(5.0, 5.0);
295 let e = Ellipse::new(center, (5.0, 5.0), 1.0);
296 assert_approx_eq(e.area(), 25.0 * PI);
297 let e = Ellipse::new(center, (5.0, 10.0), 1.0);
298 assert_approx_eq(e.area(), 50.0 * PI);
299
300 assert_eq!(e.winding(center), 1);
301
302 let p = e.to_path(1e-9);
303 assert_approx_eq(e.area(), p.area());
304 assert_eq!(e.winding(center), p.winding(center));
305
306 let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
307 assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
308
309 assert_eq!(e_neg_radius.winding(center), 1);
310
311 let p_neg_radius = e_neg_radius.to_path(1e-9);
312 assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
313 assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
314 }
315}
316