1use crate::{Color, MediaId, Point, Rect, Transform};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct GradientStop {
8 pub offset: f64,
9 pub color: Color,
10}
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum Paint {
15 Solid(Color),
16 Linear {
17 start: Point,
18 end: Point,
19 stops: Vec<GradientStop>,
20 extend: (bool, bool),
21 },
22 Radial {
23 center: Point,
24 radius: f64,
25 focal: Point,
26 stops: Vec<GradientStop>,
27 extend: (bool, bool),
28 },
29 Grid {
32 bounds: Rect,
33 columns: u16,
34 rows: u16,
35 colors: Vec<Color>,
36 },
37 Tile {
38 image: MediaId,
39 tile: Rect,
40 transform: Transform,
41 },
42}
43
44impl Paint {
45 pub fn grid(bounds: Rect, columns: u16, rows: u16, colors: Vec<Color>) -> Option<Self> {
47 let count = usize::from(columns) * usize::from(rows);
48 (columns > 0
49 && rows > 0
50 && count <= 65_536
51 && colors.len() == count
52 && [bounds.x, bounds.y, bounds.width, bounds.height]
53 .into_iter()
54 .all(f64::is_finite)
55 && bounds.width > 0.0
56 && bounds.height > 0.0
57 && colors.iter().all(|c| {
58 [c.r, c.g, c.b, c.a]
59 .into_iter()
60 .all(|v| v.is_finite() && (0.0..=1.0).contains(&v))
61 }))
62 .then_some(Self::Grid {
63 bounds,
64 columns,
65 rows,
66 colors,
67 })
68 }
69
70 pub fn linear(
72 start: Point,
73 end: Point,
74 stops: Vec<GradientStop>,
75 extend: (bool, bool),
76 ) -> Self {
77 if let [stop] = stops.as_slice() {
78 Self::Solid(stop.color)
79 } else {
80 Self::Linear {
81 start,
82 end,
83 stops,
84 extend,
85 }
86 }
87 }
88
89 pub fn radial(
91 center: Point,
92 radius: f64,
93 focal: Point,
94 stops: Vec<GradientStop>,
95 extend: (bool, bool),
96 ) -> Self {
97 if let [stop] = stops.as_slice() {
98 Self::Solid(stop.color)
99 } else {
100 Self::Radial {
101 center,
102 radius,
103 focal,
104 stops,
105 extend,
106 }
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub enum LineCap {
114 #[default]
115 Butt,
116 Round,
117 Square,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum LineJoin {
123 #[default]
124 Miter,
125 Round,
126 Bevel,
127}
128
129#[derive(Debug, Clone, PartialEq)]
131pub struct Stroke {
132 pub paint: Paint,
133 pub width: f64,
134 pub cap: LineCap,
135 pub join: LineJoin,
136 pub dash: Option<Vec<f64>>,
137}
138
139impl Stroke {
140 pub fn new(paint: Paint, width: f64) -> Self {
142 Self {
143 paint,
144 width,
145 cap: LineCap::Butt,
146 join: LineJoin::Miter,
147 dash: None,
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::{GradientStop, LineCap, LineJoin, Paint, Stroke};
155 use crate::{Color, MediaId, Point, Rect, Transform};
156
157 fn red() -> Color {
158 Color {
159 r: 1.0,
160 g: 0.0,
161 b: 0.0,
162 a: 1.0,
163 }
164 }
165
166 #[test]
167 fn single_stop_gradients_degrade_to_solid_at_construction() {
168 let stops = vec![GradientStop {
169 offset: 0.25,
170 color: red(),
171 }];
172 assert_eq!(
173 Paint::linear(
174 Point { x: 0.0, y: 0.0 },
175 Point { x: 1.0, y: 1.0 },
176 stops.clone(),
177 (false, false),
178 ),
179 Paint::Solid(red())
180 );
181 assert_eq!(
182 Paint::radial(
183 Point { x: 0.5, y: 0.5 },
184 1.0,
185 Point { x: 0.5, y: 0.5 },
186 stops,
187 (false, false),
188 ),
189 Paint::Solid(red())
190 );
191 }
192
193 #[test]
194 fn multiple_stop_gradients_preserve_their_geometry_and_stops() {
195 let stops = vec![
196 GradientStop {
197 offset: 0.0,
198 color: Color::BLACK,
199 },
200 GradientStop {
201 offset: 1.0,
202 color: Color::WHITE,
203 },
204 ];
205 let paint = Paint::linear(
206 Point { x: 0.0, y: 0.0 },
207 Point { x: 1.0, y: 1.0 },
208 stops.clone(),
209 (true, false),
210 );
211 assert!(matches!(
212 paint,
213 Paint::Linear {
214 stops: actual,
215 extend: (true, false),
216 ..
217 } if actual == stops
218 ));
219 }
220
221 #[test]
222 fn stroke_new_uses_pdf_defaults() {
223 let stroke = Stroke::new(Paint::Solid(red()), 2.0);
224 assert_eq!(stroke.cap, LineCap::Butt);
225 assert_eq!(stroke.join, LineJoin::Miter);
226 assert_eq!(stroke.dash, None);
227 }
228
229 #[test]
230 fn tile_paint_uses_a_media_id() {
231 let media_id = MediaId::from_bytes(b"tile");
232 let paint = Paint::Tile {
233 image: media_id,
234 tile: Rect {
235 x: 0.0,
236 y: 0.0,
237 width: 10.0,
238 height: 20.0,
239 },
240 transform: Transform::IDENTITY,
241 };
242 assert!(matches!(paint, Paint::Tile { image, .. } if image == media_id));
243 }
244}