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 | #[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 | |
167 | impl 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 | |
180 | impl 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 | |
192 | impl 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 | |
201 | impl 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 | |
207 | impl 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)] |
282 | mod 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 | |