1use crate::MaterialId;
2use crate::geometry::{Point, Size};
3
4use super::Color;
5
6pub const MAX_STOPS: usize = 8;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum PaintEvalSpaceV1 {
10 #[default]
12 LocalPx,
13 ViewportPx,
15 StrokeS01,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct PaintBindingV1 {
21 pub paint: Paint,
22 pub eval_space: PaintEvalSpaceV1,
23}
24
25impl PaintBindingV1 {
26 pub const fn new(paint: Paint) -> Self {
27 Self {
28 paint,
29 eval_space: PaintEvalSpaceV1::LocalPx,
30 }
31 }
32
33 pub const fn with_eval_space(paint: Paint, eval_space: PaintEvalSpaceV1) -> Self {
34 Self { paint, eval_space }
35 }
36
37 pub fn sanitize(self) -> Self {
38 Self {
39 paint: self.paint.sanitize(),
40 eval_space: self.eval_space,
41 }
42 }
43}
44
45impl From<Paint> for PaintBindingV1 {
46 fn from(value: Paint) -> Self {
47 Self::new(value)
48 }
49}
50
51impl From<Color> for PaintBindingV1 {
52 fn from(value: Color) -> Self {
53 Self::new(Paint::Solid(value))
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum TileMode {
59 Clamp,
60 Repeat,
61 Mirror,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ColorSpace {
66 Srgb,
67 Oklab,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct GradientStop {
72 pub offset: f32,
73 pub color: Color,
74}
75
76impl GradientStop {
77 pub const fn new(offset: f32, color: Color) -> Self {
78 Self { offset, color }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct LinearGradient {
84 pub start: Point,
85 pub end: Point,
86 pub tile_mode: TileMode,
87 pub color_space: ColorSpace,
88 pub stop_count: u8,
89 pub stops: [GradientStop; MAX_STOPS],
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct RadialGradient {
94 pub center: Point,
95 pub radius: Size,
96 pub tile_mode: TileMode,
97 pub color_space: ColorSpace,
98 pub stop_count: u8,
99 pub stops: [GradientStop; MAX_STOPS],
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct SweepGradient {
104 pub center: Point,
105 pub start_angle_turns: f32,
107 pub end_angle_turns: f32,
109 pub tile_mode: TileMode,
110 pub color_space: ColorSpace,
111 pub stop_count: u8,
112 pub stops: [GradientStop; MAX_STOPS],
113}
114
115#[repr(C)]
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct MaterialParams {
118 pub vec4s: [[f32; 4]; 4],
119}
120
121impl MaterialParams {
122 pub const ZERO: Self = Self {
123 vec4s: [[0.0; 4]; 4],
124 };
125
126 pub fn sanitize(self) -> Self {
127 let mut out = self;
128 for v in &mut out.vec4s {
129 for x in v {
130 if !x.is_finite() {
131 *x = 0.0;
132 }
133 }
134 }
135 out
136 }
137
138 pub fn is_finite(self) -> bool {
139 self.vec4s.iter().flatten().all(|&x| x.is_finite())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
144pub enum Paint {
145 Solid(Color),
146 LinearGradient(LinearGradient),
147 RadialGradient(RadialGradient),
148 SweepGradient(SweepGradient),
149 Material {
150 id: MaterialId,
151 params: MaterialParams,
152 },
153}
154
155impl From<Color> for Paint {
156 fn from(value: Color) -> Self {
157 Paint::Solid(value)
158 }
159}
160
161impl Paint {
162 pub const TRANSPARENT: Self = Self::Solid(Color::TRANSPARENT);
163
164 pub fn sanitize(self) -> Self {
165 fn color_is_finite(c: Color) -> bool {
166 c.r.is_finite() && c.g.is_finite() && c.b.is_finite() && c.a.is_finite()
167 }
168
169 fn point_is_finite(p: Point) -> bool {
170 p.x.0.is_finite() && p.y.0.is_finite()
171 }
172
173 fn size_is_finite(s: Size) -> bool {
174 s.width.0.is_finite() && s.height.0.is_finite()
175 }
176
177 fn stops_all_finite(count: u8, stops: &[GradientStop; MAX_STOPS]) -> bool {
178 let n = usize::from(count).min(MAX_STOPS);
179 for s in stops.iter().take(n) {
180 if !s.offset.is_finite() || !color_is_finite(s.color) {
181 return false;
182 }
183 }
184 true
185 }
186
187 fn clamp01(x: f32) -> f32 {
188 x.clamp(0.0, 1.0)
189 }
190
191 fn sort_stops(
192 count: u8,
193 mut stops: [GradientStop; MAX_STOPS],
194 ) -> [GradientStop; MAX_STOPS] {
195 let n = usize::from(count).min(MAX_STOPS);
196
197 for stop in stops.iter_mut().take(n) {
198 stop.offset = clamp01(stop.offset);
199 }
200
201 for i in 1..n {
203 let key = stops[i];
204 let mut j = i;
205 while j > 0 && stops[j - 1].offset > key.offset {
206 stops[j] = stops[j - 1];
207 j -= 1;
208 }
209 stops[j] = key;
210 }
211
212 stops
213 }
214
215 fn normalize_stop_count(count: u8) -> u8 {
216 count.min(MAX_STOPS as u8)
217 }
218
219 fn degrade_tile_mode(tile_mode: TileMode) -> TileMode {
220 tile_mode
221 }
222
223 fn degrade_color_space(color_space: ColorSpace) -> ColorSpace {
224 color_space
225 }
226
227 fn maybe_solid_from_degenerate(
228 count: u8,
229 stops: &[GradientStop; MAX_STOPS],
230 ) -> Option<Paint> {
231 let n = usize::from(count).min(MAX_STOPS);
232 if n == 0 {
233 return Some(Paint::TRANSPARENT);
234 }
235 if n == 1 {
236 return Some(Paint::Solid(stops[0].color));
237 }
238 let first = stops[0].offset;
239 let all_same = (1..n).all(|i| stops[i].offset == first);
240 if all_same {
241 return Some(Paint::Solid(stops[n - 1].color));
242 }
243 None
244 }
245
246 match self {
247 Paint::Solid(c) => {
248 if !color_is_finite(c) {
249 Paint::TRANSPARENT
250 } else {
251 Paint::Solid(c)
252 }
253 }
254 Paint::LinearGradient(mut g) => {
255 g.stop_count = normalize_stop_count(g.stop_count);
256 g.tile_mode = degrade_tile_mode(g.tile_mode);
257 g.color_space = degrade_color_space(g.color_space);
258
259 if !point_is_finite(g.start)
260 || !point_is_finite(g.end)
261 || !stops_all_finite(g.stop_count, &g.stops)
262 {
263 return Paint::TRANSPARENT;
264 }
265
266 g.stops = sort_stops(g.stop_count, g.stops);
267 if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
268 return solid;
269 }
270
271 Paint::LinearGradient(g)
272 }
273 Paint::RadialGradient(mut g) => {
274 g.stop_count = normalize_stop_count(g.stop_count);
275 g.tile_mode = degrade_tile_mode(g.tile_mode);
276 g.color_space = degrade_color_space(g.color_space);
277
278 if !point_is_finite(g.center)
279 || !size_is_finite(g.radius)
280 || !stops_all_finite(g.stop_count, &g.stops)
281 {
282 return Paint::TRANSPARENT;
283 }
284
285 g.stops = sort_stops(g.stop_count, g.stops);
286 if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
287 return solid;
288 }
289
290 Paint::RadialGradient(g)
291 }
292 Paint::SweepGradient(mut g) => {
293 g.stop_count = normalize_stop_count(g.stop_count);
294 g.tile_mode = degrade_tile_mode(g.tile_mode);
295 g.color_space = degrade_color_space(g.color_space);
296
297 if !point_is_finite(g.center)
298 || !g.start_angle_turns.is_finite()
299 || !g.end_angle_turns.is_finite()
300 || !stops_all_finite(g.stop_count, &g.stops)
301 {
302 return Paint::TRANSPARENT;
303 }
304
305 g.stops = sort_stops(g.stop_count, g.stops);
306 if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
307 return solid;
308 }
309
310 let start = g.start_angle_turns.rem_euclid(1.0);
311 let span_raw = g.end_angle_turns - g.start_angle_turns;
312 let span_mod = span_raw.rem_euclid(1.0);
313 let span = if span_mod <= 1e-6 && span_raw.abs() >= 1.0 - 1e-6 {
314 1.0
315 } else {
316 span_mod
317 };
318
319 if span <= 1e-6 {
320 let n = usize::from(g.stop_count).min(MAX_STOPS);
321 let c = g.stops[n.saturating_sub(1)].color;
322 return Paint::Solid(c);
323 }
324
325 g.start_angle_turns = start;
326 g.end_angle_turns = start + span;
327
328 Paint::SweepGradient(g)
329 }
330 Paint::Material { id, params } => Paint::Material {
331 id,
332 params: params.sanitize(),
333 },
334 }
335 }
336}