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]
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
171impl 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
184impl 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
196impl 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
205impl 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
211impl 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)]
286mod 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