embedded_graphics/primitives/ellipse/mod.rs
1//! The ellipse primitive
2
3use crate::{
4 geometry::{Dimensions, Point, Size},
5 primitives::{circle, ContainsPoint, OffsetOutline, PointsIter, Primitive, Rectangle},
6 transform::Transform,
7};
8
9mod points;
10mod styled;
11
12pub use points::Points;
13pub 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))]
57pub 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
65impl 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
92impl OffsetOutline for Ellipse {
93 fn offset(&self, offset: i32) -> Self {
94 let 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.
109pub(in crate::primitives) fn center_2x(top_left: Point, size: Size) -> Point {
110 let radius = size.saturating_sub(Size::new(1, 1));
111
112 top_left * 2 + radius
113}
114
115impl Primitive for Ellipse {}
116
117impl PointsIter for Ellipse {
118 type Iter = Points;
119
120 fn points(&self) -> Self::Iter {
121 Points::new(self)
122 }
123}
124
125impl ContainsPoint for Ellipse {
126 fn contains(&self, point: Point) -> bool {
127 let ellipse_contains = EllipseContains::new(self.size);
128 ellipse_contains.contains(point * 2 - self.center_2x())
129 }
130}
131
132impl Dimensions for Ellipse {
133 fn bounding_box(&self) -> Rectangle {
134 Rectangle::new(self.top_left, self.size)
135 }
136}
137
138impl 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))]
178pub(in crate::primitives) struct EllipseContains {
179 a: u32,
180 b: u32,
181 threshold: u32,
182}
183
184impl 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)]
219mod 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}