1 | //! The ellipse primitive |
2 | |
3 | use crate::{ |
4 | geometry::{Dimensions, Point, Size}, |
5 | primitives::{circle, ContainsPoint, OffsetOutline, PointsIter, Primitive, Rectangle}, |
6 | transform::Transform, |
7 | }; |
8 | |
9 | mod points; |
10 | mod styled; |
11 | |
12 | pub use points::Points; |
13 | pub use styled::StyledPixelsIterator; |
14 | |
15 | /// Ellipse primitive |
16 | /// |
17 | /// # Examples |
18 | /// |
19 | /// ## Create some ellipses with different styles |
20 | /// |
21 | /// ```rust |
22 | /// use embedded_graphics::{ |
23 | /// pixelcolor::Rgb565, |
24 | /// prelude::*, |
25 | /// primitives::{Ellipse, PrimitiveStyle, PrimitiveStyleBuilder}, |
26 | /// }; |
27 | /// # use embedded_graphics::mock_display::MockDisplay; |
28 | /// |
29 | /// // Ellipse with 1 pixel wide white stroke with top-left point at (10, 20) with a size of (30, 40) |
30 | /// # let mut display = MockDisplay::default(); |
31 | /// Ellipse::new(Point::new(10, 20), Size::new(30, 40)) |
32 | /// .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1)) |
33 | /// .draw(&mut display)?; |
34 | /// |
35 | /// // Ellipse with styled stroke and fill with top-left point at (20, 30) with a size of (40, 30) |
36 | /// let style = PrimitiveStyleBuilder::new() |
37 | /// .stroke_color(Rgb565::RED) |
38 | /// .stroke_width(3) |
39 | /// .fill_color(Rgb565::GREEN) |
40 | /// .build(); |
41 | /// |
42 | /// # let mut display = MockDisplay::default(); |
43 | /// Ellipse::new(Point::new(20, 30), Size::new(40, 30)) |
44 | /// .into_styled(style) |
45 | /// .draw(&mut display)?; |
46 | /// |
47 | /// // Ellipse with blue fill and no stroke with a translation applied |
48 | /// # let mut display = MockDisplay::default(); |
49 | /// Ellipse::new(Point::new(10, 20), Size::new(20, 40)) |
50 | /// .translate(Point::new(10, -15)) |
51 | /// .into_styled(PrimitiveStyle::with_fill(Rgb565::BLUE)) |
52 | /// .draw(&mut display)?; |
53 | /// # Ok::<(), core::convert::Infallible>(()) |
54 | /// ``` |
55 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] |
56 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
57 | pub struct Ellipse { |
58 | /// Top-left point of ellipse's bounding box |
59 | pub top_left: Point, |
60 | |
61 | /// Size of the ellipse |
62 | pub size: Size, |
63 | } |
64 | |
65 | impl Ellipse { |
66 | /// Create a new ellipse delimited with a top-left point with a specific size |
67 | pub const fn new(top_left: Point, size: Size) -> Self { |
68 | Ellipse { top_left, size } |
69 | } |
70 | |
71 | /// Create a new ellipse centered around a given point with a specific size |
72 | pub const fn with_center(center: Point, size: Size) -> Self { |
73 | let top_left = Rectangle::with_center(center, size).top_left; |
74 | |
75 | Ellipse { top_left, size } |
76 | } |
77 | |
78 | /// Return the center point of the ellipse |
79 | pub fn center(&self) -> Point { |
80 | self.bounding_box().center() |
81 | } |
82 | |
83 | /// Return the center point of the ellipse scaled by a factor of 2 |
84 | /// |
85 | /// This method is used to accurately calculate the outside edge of the ellipse. |
86 | /// The result is not equivalent to `self.center() * 2` because of rounding. |
87 | fn center_2x(&self) -> Point { |
88 | center_2x(self.top_left, self.size) |
89 | } |
90 | } |
91 | |
92 | impl OffsetOutline for Ellipse { |
93 | fn offset(&self, offset: i32) -> Self { |
94 | let size: Size = if offset >= 0 { |
95 | self.size.saturating_add(Size::new_equal(2 * offset as u32)) |
96 | } else { |
97 | self.size |
98 | .saturating_sub(Size::new_equal(2 * (-offset) as u32)) |
99 | }; |
100 | |
101 | Self::with_center(self.center(), size) |
102 | } |
103 | } |
104 | |
105 | /// Return the center point of the ellipse scaled by a factor of 2 |
106 | /// |
107 | /// This method is used to accurately calculate the outside edge of the ellipse. |
108 | /// The result is not equivalent to `Ellipse::center() * 2` because of rounding. |
109 | pub(in crate::primitives) fn center_2x(top_left: Point, size: Size) -> Point { |
110 | let radius: Size = size.saturating_sub(Size::new(width:1, height:1)); |
111 | |
112 | top_left * 2 + radius |
113 | } |
114 | |
115 | impl Primitive for Ellipse {} |
116 | |
117 | impl PointsIter for Ellipse { |
118 | type Iter = Points; |
119 | |
120 | fn points(&self) -> Self::Iter { |
121 | Points::new(self) |
122 | } |
123 | } |
124 | |
125 | impl ContainsPoint for Ellipse { |
126 | fn contains(&self, point: Point) -> bool { |
127 | let ellipse_contains: EllipseContains = EllipseContains::new(self.size); |
128 | ellipse_contains.contains(point:point * 2 - self.center_2x()) |
129 | } |
130 | } |
131 | |
132 | impl Dimensions for Ellipse { |
133 | fn bounding_box(&self) -> Rectangle { |
134 | Rectangle::new(self.top_left, self.size) |
135 | } |
136 | } |
137 | |
138 | impl Transform for Ellipse { |
139 | /// Translate the ellipse from its current position to a new position by (x, y) pixels, |
140 | /// returning a new `Ellipse`. For a mutating transform, see `translate_mut`. |
141 | /// |
142 | /// ``` |
143 | /// # use embedded_graphics::primitives::Ellipse; |
144 | /// # use embedded_graphics::prelude::*; |
145 | /// let ellipse = Ellipse::new(Point::new(5, 10), Size::new(10, 15)); |
146 | /// let moved = ellipse.translate(Point::new(10, 10)); |
147 | /// |
148 | /// assert_eq!(moved.top_left, Point::new(15, 20)); |
149 | /// ``` |
150 | fn translate(&self, by: Point) -> Self { |
151 | Self { |
152 | top_left: self.top_left + by, |
153 | ..*self |
154 | } |
155 | } |
156 | |
157 | /// Translate the ellipse from its current position to a new position by (x, y) pixels. |
158 | /// |
159 | /// ``` |
160 | /// # use embedded_graphics::primitives::Ellipse; |
161 | /// # use embedded_graphics::prelude::*; |
162 | /// let mut ellipse = Ellipse::new(Point::new(5, 10), Size::new(10, 15)); |
163 | /// ellipse.translate_mut(Point::new(10, 10)); |
164 | /// |
165 | /// assert_eq!(ellipse.top_left, Point::new(15, 20)); |
166 | /// ``` |
167 | fn translate_mut(&mut self, by: Point) -> &mut Self { |
168 | self.top_left += by; |
169 | |
170 | self |
171 | } |
172 | } |
173 | |
174 | /// Determines if a point is inside an ellipse. |
175 | // TODO: Make this available to the user as part of #343 |
176 | #[derive (Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Debug)] |
177 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
178 | pub(in crate::primitives) struct EllipseContains { |
179 | a: u32, |
180 | b: u32, |
181 | threshold: u32, |
182 | } |
183 | |
184 | impl EllipseContains { |
185 | /// Creates an object to determine if a point is inside an ellipse. |
186 | /// |
187 | /// The ellipse is always located in the origin. |
188 | pub const fn new(size: Size) -> Self { |
189 | let Size { width, height } = size; |
190 | |
191 | let a = width.pow(2); |
192 | let b = height.pow(2); |
193 | |
194 | // Special case for circles, where width and height are equal |
195 | let threshold = if width == height { |
196 | circle::diameter_to_threshold(width) |
197 | } else { |
198 | b * a |
199 | }; |
200 | |
201 | Self { a, b, threshold } |
202 | } |
203 | |
204 | /// Returns `true` if the point is inside the ellipse. |
205 | pub const fn contains(&self, point: Point) -> bool { |
206 | let x = point.x.pow(2) as u32; |
207 | let y = point.y.pow(2) as u32; |
208 | |
209 | // Special case for circles, where width and height are equal |
210 | if self.a == self.b { |
211 | x + y < self.threshold |
212 | } else { |
213 | self.b * x + self.a * y < self.threshold |
214 | } |
215 | } |
216 | } |
217 | |
218 | #[cfg (test)] |
219 | mod tests { |
220 | use super::*; |
221 | use crate::{ |
222 | geometry::{Point, Size}, |
223 | mock_display::MockDisplay, |
224 | pixelcolor::BinaryColor, |
225 | primitives::ContainsPoint, |
226 | }; |
227 | |
228 | #[test ] |
229 | fn contains() { |
230 | let ellipse = Ellipse::new(Point::zero(), Size::new(40, 20)); |
231 | |
232 | let display = MockDisplay::from_points( |
233 | ellipse |
234 | .bounding_box() |
235 | .points() |
236 | .filter(|p| ellipse.contains(*p)), |
237 | BinaryColor::On, |
238 | ); |
239 | |
240 | let expected = MockDisplay::from_points(ellipse.points(), BinaryColor::On); |
241 | |
242 | display.assert_eq(&expected); |
243 | } |
244 | |
245 | #[test ] |
246 | fn translate() { |
247 | let moved = Ellipse::new(Point::new(4, 6), Size::new(5, 8)).translate(Point::new(3, 5)); |
248 | |
249 | assert_eq!(moved, Ellipse::new(Point::new(7, 11), Size::new(5, 8))); |
250 | } |
251 | |
252 | #[test ] |
253 | fn offset() { |
254 | let center = Point::new(5, 6); |
255 | let ellipse = Ellipse::with_center(center, Size::new(3, 4)); |
256 | |
257 | assert_eq!(ellipse.offset(0), ellipse); |
258 | |
259 | assert_eq!( |
260 | ellipse.offset(1), |
261 | Ellipse::with_center(center, Size::new(5, 6)) |
262 | ); |
263 | assert_eq!( |
264 | ellipse.offset(2), |
265 | Ellipse::with_center(center, Size::new(7, 8)) |
266 | ); |
267 | |
268 | assert_eq!( |
269 | ellipse.offset(-1), |
270 | Ellipse::with_center(center, Size::new(1, 2)) |
271 | ); |
272 | assert_eq!( |
273 | ellipse.offset(-2), |
274 | Ellipse::with_center(center, Size::new(0, 0)) |
275 | ); |
276 | assert_eq!( |
277 | ellipse.offset(-3), |
278 | Ellipse::with_center(center, Size::new(0, 0)) |
279 | ); |
280 | } |
281 | } |
282 | |