1mod gpu;
2mod json;
3mod types;
4
5pub(crate) use gpu::GpuPaint;
6
7pub use types::{
8 GradientInterpolation, GradientPaint, GradientSpread, GradientStop, GradientUnits, Paint,
9 PaintDelegate, PaintKind,
10};
11
12impl Paint {
13 pub fn solid(color: [u8; 4]) -> Self {
14 Self::SolidColor(color[0], color[1], color[2], color[3])
15 }
16
17 #[cfg(feature = "json")]
18 pub fn from_json_value(value: &serde_json::Value) -> Option<Self> {
19 json::from_json_value(value)
20 }
21
22 pub(crate) fn to_gpu(&self, fallback: [u8; 4]) -> gpu::GpuPaint {
23 match self {
24 Self::SolidColor(r, g, b, a) => gpu::GpuPaint::solid([*r, *g, *b, *a]),
25 Self::Gradient(gradient) => gpu::gradient_to_gpu(gradient, fallback),
26 }
27 }
28
29 #[cfg(feature = "json")]
30 pub fn to_json_value(&self) -> serde_json::Value {
31 json::to_json_value(self)
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38 use bytemuck::Zeroable;
39 use gpu::{GpuPaint, test_gradient_to_gpu as gradient_to_gpu};
40 use types::{MAX_GRADIENT_STOPS, PaintDelegate};
41
42 #[test]
43 fn gpu_paint_size_matches_wgsl_uniform_stride() {
44 let size = std::mem::size_of::<GpuPaint>();
45 assert_eq!(size, 320);
46 }
47
48 #[test]
49 fn gpu_paint_solid_zeroed_is_well_formed() {
50 let paint = GpuPaint::solid([255, 0, 0, 255]);
51 assert_eq!(paint.colors[0], [1.0, 0.0, 0.0, 1.0]);
52 assert_eq!(paint.offsets[0][0], 0.0);
53 assert_eq!(paint.stop_count, 1);
54 let zeroed = GpuPaint::zeroed();
55 for i in 1..MAX_GRADIENT_STOPS {
56 assert_eq!(paint.colors[i], zeroed.colors[i]);
57 assert_eq!(paint.offsets[i], zeroed.offsets[i]);
58 }
59 assert_eq!(paint.kind, 0);
60 assert_eq!(paint.units, 0);
61 assert_eq!(paint.spread, 0);
62 assert_eq!(paint.interpolation, 0);
63 }
64
65 #[test]
66 fn gpu_paint_gradient_places_offsets_in_first_component() {
67 let gradient = GradientPaint {
68 kind: PaintKind::LinearGradient,
69 units: GradientUnits::UserSpace,
70 spread: GradientSpread::Repeat,
71 interpolation: GradientInterpolation::LinearSrgb,
72 start: [10.0, 20.0],
73 end: [100.0, 200.0],
74 center: [50.0, 60.0],
75 radius: [30.0, 40.0],
76 angle: 45.0,
77 stops: vec![
78 GradientStop {
79 offset: 0.25,
80 color: [255, 0, 0, 255],
81 },
82 GradientStop {
83 offset: 0.75,
84 color: [0, 0, 255, 255],
85 },
86 ],
87 };
88 let gpu = gradient_to_gpu(&gradient, [0, 0, 0, 255]);
89 assert_eq!(gpu.kind, 1);
90 assert_eq!(gpu.units, 1);
91 assert_eq!(gpu.spread, 1);
92 assert_eq!(gpu.interpolation, 1);
93 assert_eq!(gpu.start, [10.0, 20.0]);
94 assert_eq!(gpu.end, [100.0, 200.0]);
95 assert_eq!(gpu.center, [50.0, 60.0]);
96 assert_eq!(gpu.radius, [30.0, 40.0]);
97 assert_eq!(gpu.angle, 45.0);
98 assert_eq!(gpu.stop_count, 2);
99 assert!((gpu.offsets[0][0] - 0.25).abs() < 0.001);
100 assert!((gpu.offsets[1][0] - 0.75).abs() < 0.001);
101 assert!((gpu.colors[0][0] - 1.0).abs() < 0.01);
102 assert!((gpu.colors[0][2] - 0.0).abs() < 0.01);
103 assert!((gpu.colors[1][2] - 1.0).abs() < 0.01);
104 }
105
106 #[test]
107 fn gpu_paint_gradient_clamps_offset_to_0_1() {
108 let gradient = GradientPaint {
109 stops: vec![
110 GradientStop {
111 offset: -0.5,
112 color: [128, 128, 128, 255],
113 },
114 GradientStop {
115 offset: 1.5,
116 color: [64, 64, 64, 255],
117 },
118 ],
119 ..Default::default()
120 };
121 let gpu = gradient_to_gpu(&gradient, [0, 0, 0, 255]);
122 assert!((gpu.offsets[0][0] - 0.0).abs() < 0.001);
123 assert!((gpu.offsets[1][0] - 1.0).abs() < 0.001);
124 }
125
126 #[test]
127 fn gpu_paint_gradient_falls_back_on_empty_stops() {
128 let gradient = GradientPaint {
129 stops: Vec::new(),
130 ..Default::default()
131 };
132 let gpu = gradient_to_gpu(&gradient, [64, 128, 192, 255]);
133 assert_eq!(gpu.stop_count, 1);
134 assert!((gpu.colors[0][0] - 0.25).abs() < 0.02);
135 assert!((gpu.colors[0][1] - 0.5).abs() < 0.02);
136 assert!((gpu.colors[0][2] - 0.75).abs() < 0.02);
137 assert!((gpu.colors[0][3] - 1.0).abs() < 0.01);
138 }
139
140 #[cfg(feature = "json")]
141 #[test]
142 fn paint_json_roundtrips_solid_color() {
143 let solid = Paint::solid([64, 128, 192, 255]);
144 let json = solid.to_json_value();
145 let parsed = Paint::from_json_value(&json).unwrap();
146 assert_eq!(parsed, solid);
147 }
148
149 #[cfg(feature = "json")]
150 #[test]
151 fn paint_json_roundtrips_gradient() {
152 let gradient = Paint::Gradient(GradientPaint {
153 kind: PaintKind::RadialGradient,
154 units: GradientUnits::UserSpace,
155 spread: GradientSpread::Reflect,
156 interpolation: GradientInterpolation::LinearSrgb,
157 start: [0.1, 0.2],
158 end: [0.8, 0.9],
159 center: [0.4, 0.5],
160 radius: [0.3, 0.3],
161 angle: 90.0,
162 stops: vec![
163 GradientStop {
164 offset: 0.0,
165 color: [255, 0, 0, 255],
166 },
167 GradientStop {
168 offset: 1.0,
169 color: [0, 255, 0, 255],
170 },
171 ],
172 });
173 let json = gradient.to_json_value();
174 let parsed = Paint::from_json_value(&json).unwrap();
175 assert_eq!(parsed, gradient);
176 }
177
178 #[cfg(feature = "json")]
179 #[test]
180 fn paint_parses_solid_color_array() {
181 let json = serde_json::json!([100, 150, 200, 255]);
182 let paint = Paint::from_json_value(&json).unwrap();
183 assert_eq!(paint, Paint::solid([100, 150, 200, 255]));
184 }
185
186 #[cfg(feature = "json")]
187 #[test]
188 fn paint_parses_gradient_object() {
189 let json = serde_json::json!({
190 "type": "linear_gradient",
191 "units": "user_space",
192 "spread": "repeat",
193 "interpolation": "linear_srgb",
194 "start": [0.0, 0.0],
195 "end": [1.0, 0.0],
196 "stops": [
197 {"offset": 0.0, "color": [255, 0, 0, 255]},
198 {"offset": 1.0, "color": [0, 0, 255, 255]}
199 ]
200 });
201 let paint = Paint::from_json_value(&json).unwrap();
202 match paint {
203 Paint::Gradient(g) => {
204 assert_eq!(g.kind, PaintKind::LinearGradient);
205 assert_eq!(g.units, GradientUnits::UserSpace);
206 assert_eq!(g.spread, GradientSpread::Repeat);
207 assert_eq!(g.interpolation, GradientInterpolation::LinearSrgb);
208 assert_eq!(g.stops.len(), 2);
209 assert_eq!(g.stops[0].color, [255, 0, 0, 255]);
210 assert_eq!(g.stops[1].color, [0, 0, 255, 255]);
211 }
212 _ => panic!("expected Gradient"),
213 }
214 }
215
216 #[cfg(feature = "json")]
217 #[test]
218 fn paint_parses_gradient_with_shorthand_type_names() {
219 for (json, expected) in [
220 (
221 serde_json::json!({"type": "linear", "stops": [[0.0, [255, 0, 0, 255]], [1.0, [0, 0, 255, 255]]]}),
222 PaintKind::LinearGradient,
223 ),
224 (
225 serde_json::json!({"type": "radial", "stops": [[0.0, [255, 0, 0, 255]], [1.0, [0, 0, 255, 255]]]}),
226 PaintKind::RadialGradient,
227 ),
228 (
229 serde_json::json!({"type": "conic", "stops": [[0.0, [255, 0, 0, 255]], [1.0, [0, 0, 255, 255]]]}),
230 PaintKind::ConicGradient,
231 ),
232 ] {
233 let paint = Paint::from_json_value(&json).unwrap();
234 match paint {
235 Paint::Gradient(g) => assert_eq!(g.kind, expected),
236 _ => panic!("expected Gradient"),
237 }
238 }
239 }
240
241 #[cfg(feature = "json")]
242 #[test]
243 fn paint_parses_gradient_stops_as_flat_arrays() {
244 let json = serde_json::json!({
245 "type": "linear_gradient",
246 "stops": [
247 [0.0, [255, 0, 0, 255]],
248 [0.5, [0, 255, 0, 128]],
249 [1.0, [0, 0, 255, 64]]
250 ]
251 });
252 let paint = Paint::from_json_value(&json).unwrap();
253 match paint {
254 Paint::Gradient(g) => {
255 assert_eq!(g.stops.len(), 3);
256 assert_eq!(g.stops[0].offset, 0.0);
257 assert_eq!(g.stops[0].color, [255, 0, 0, 255]);
258 assert_eq!(g.stops[1].offset, 0.5);
259 assert_eq!(g.stops[1].color, [0, 255, 0, 128]);
260 assert_eq!(g.stops[2].offset, 1.0);
261 assert_eq!(g.stops[2].color, [0, 0, 255, 64]);
262 }
263 _ => panic!("expected Gradient"),
264 }
265 }
266
267 #[test]
268 fn gpu_paint_uses_wgsl_compatible_gradient_kind_mappings() {
269 assert_eq!(
270 gradient_to_gpu(
271 &GradientPaint {
272 kind: PaintKind::LinearGradient,
273 ..Default::default()
274 },
275 [0, 0, 0, 255],
276 )
277 .kind,
278 1
279 );
280 assert_eq!(
281 gradient_to_gpu(
282 &GradientPaint {
283 kind: PaintKind::RadialGradient,
284 ..Default::default()
285 },
286 [0, 0, 0, 255],
287 )
288 .kind,
289 2
290 );
291 assert_eq!(
292 gradient_to_gpu(
293 &GradientPaint {
294 kind: PaintKind::ConicGradient,
295 ..Default::default()
296 },
297 [0, 0, 0, 255],
298 )
299 .kind,
300 3
301 );
302 }
303
304 #[test]
305 fn gpu_paint_uses_wgsl_compatible_spread_mappings() {
306 assert_eq!(
307 gradient_to_gpu(
308 &GradientPaint {
309 spread: GradientSpread::Pad,
310 ..Default::default()
311 },
312 [0, 0, 0, 255],
313 )
314 .spread,
315 0
316 );
317 assert_eq!(
318 gradient_to_gpu(
319 &GradientPaint {
320 spread: GradientSpread::Repeat,
321 ..Default::default()
322 },
323 [0, 0, 0, 255],
324 )
325 .spread,
326 1
327 );
328 assert_eq!(
329 gradient_to_gpu(
330 &GradientPaint {
331 spread: GradientSpread::Reflect,
332 ..Default::default()
333 },
334 [0, 0, 0, 255],
335 )
336 .spread,
337 2
338 );
339 }
340
341 #[test]
342 fn gpu_paint_truncates_stops_beyond_max() {
343 let many_stops: Vec<GradientStop> = (0..(MAX_GRADIENT_STOPS + 3))
344 .map(|i| GradientStop {
345 offset: i as f32 / 10.0,
346 color: [i as u8, 0, 0, 255],
347 })
348 .collect();
349 let gradient = GradientPaint {
350 stops: many_stops,
351 ..Default::default()
352 };
353 let gpu = gradient_to_gpu(&gradient, [0, 0, 0, 255]);
354 assert_eq!(gpu.stop_count, MAX_GRADIENT_STOPS as u32);
355 assert!((gpu.colors[0][0] - 0.0).abs() < 0.01);
356 let last_idx = MAX_GRADIENT_STOPS - 1;
357 assert!((gpu.colors[last_idx][0] - (last_idx as f32 / 255.0)).abs() < 0.01);
358 }
359
360 #[test]
361 fn paint_to_gpu_solid_uses_color_from_enum() {
362 let paint = Paint::solid([50, 100, 150, 200]);
363 let gpu = paint.to_gpu([0, 0, 0, 255]);
364 assert_eq!(gpu.kind, 0);
365 assert_eq!(gpu.stop_count, 1);
366 assert!((gpu.colors[0][0] - (50.0 / 255.0)).abs() < 0.01);
367 assert!((gpu.colors[0][1] - (100.0 / 255.0)).abs() < 0.01);
368 assert!((gpu.colors[0][2] - (150.0 / 255.0)).abs() < 0.01);
369 assert!((gpu.colors[0][3] - (200.0 / 255.0)).abs() < 0.01);
370 }
371
372 #[test]
373 fn paint_to_gpu_gradient_uses_fallback_when_no_stops() {
374 let paint = Paint::Gradient(GradientPaint {
375 stops: Vec::new(),
376 ..Default::default()
377 });
378 let gpu = paint.to_gpu([10, 20, 30, 40]);
379 assert_eq!(gpu.stop_count, 1);
380 assert!((gpu.colors[0][0] - (10.0 / 255.0)).abs() < 0.01);
381 assert!((gpu.colors[0][1] - (20.0 / 255.0)).abs() < 0.01);
382 assert!((gpu.colors[0][2] - (30.0 / 255.0)).abs() < 0.01);
383 assert!((gpu.colors[0][3] - (40.0 / 255.0)).abs() < 0.01);
384 }
385
386 #[test]
387 fn paint_delegate_roundtrips_solid_color() {
388 let solid = Paint::SolidColor(10, 20, 30, 40);
389 let delegate = PaintDelegate::from(solid.clone());
390 let roundtripped = delegate.into_evaluated().unwrap();
391 assert_eq!(roundtripped, solid);
392 }
393
394 #[test]
395 fn paint_delegate_roundtrips_complete_gradient() {
396 let gradient = Paint::Gradient(GradientPaint {
397 kind: PaintKind::RadialGradient,
398 units: GradientUnits::UserSpace,
399 spread: GradientSpread::Reflect,
400 interpolation: GradientInterpolation::LinearSrgb,
401 start: [5.0, 10.0],
402 end: [15.0, 20.0],
403 center: [25.0, 30.0],
404 radius: [35.0, 40.0],
405 angle: 45.0,
406 stops: vec![
407 GradientStop {
408 offset: 0.2,
409 color: [10, 20, 30, 40],
410 },
411 GradientStop {
412 offset: 0.8,
413 color: [50, 60, 70, 80],
414 },
415 ],
416 });
417 let delegate = PaintDelegate::from(gradient.clone());
418 let roundtripped = delegate.into_evaluated().unwrap();
419 assert_eq!(roundtripped, gradient);
420 }
421
422 #[test]
423 fn paint_default_is_solid_black() {
424 assert_eq!(Paint::default(), Paint::solid([0, 0, 0, 255]));
425 }
426
427 #[test]
428 fn gradient_paint_default_is_linear_object_bounding_box() {
429 let g = GradientPaint::default();
430 assert_eq!(g.kind, PaintKind::LinearGradient);
431 assert_eq!(g.units, GradientUnits::ObjectBoundingBox);
432 assert_eq!(g.spread, GradientSpread::Pad);
433 assert_eq!(g.interpolation, GradientInterpolation::Srgb);
434 assert_eq!(g.start, [0.0, 0.0]);
435 assert_eq!(g.end, [1.0, 0.0]);
436 assert_eq!(g.center, [0.5, 0.5]);
437 assert_eq!(g.radius, [0.5, 0.5]);
438 assert_eq!(g.angle, 0.0);
439 assert!(g.stops.is_empty());
440 }
441}