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