Skip to main content

lumen_engine/node/vector/paint/
mod.rs

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}