1use crate::display_list::ImageSampling;
2use incular_core::finite_or_zero;
3use incular_core::{Color, Offset};
4use std::sync::{
5 Arc,
6 atomic::{AtomicU64, Ordering},
7};
8
9static NEXT_GRADIENT_ID: AtomicU64 = AtomicU64::new(1);
10
11fn next_gradient_id() -> GradientId {
12 GradientId(
13 NEXT_GRADIENT_ID
14 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
15 .expect("gradient identity space exhausted"),
16 )
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub struct GradientId(u64);
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct GradientStop {
25 pub offset: f32,
26 pub color: Color,
27}
28#[derive(Clone, Debug)]
29pub struct GradientStops {
30 id: GradientId,
31 stops: Arc<[GradientStop]>,
32}
33impl PartialEq for GradientStops {
34 fn eq(&self, other: &Self) -> bool {
35 self.stops == other.stops
36 }
37}
38
39#[must_use]
43pub fn sample_gradient_stops(stops: &GradientStops, t: f32) -> [f32; 4] {
44 let stops = stops.as_slice();
45 let t = finite_or_zero(t);
46 if t <= stops[0].offset {
47 return premultiplied(stops[0].color);
48 }
49 let last = stops.last().expect("normalized stops are non-empty");
50 if t >= last.offset {
51 return premultiplied(last.color);
52 }
53 let mut left = 0;
54 for index in 1..stops.len() {
55 if stops[index].offset <= t {
56 left = index;
57 } else {
58 let a = stops[left];
59 let b = stops[index];
60 let amount = ((t - a.offset) / (b.offset - a.offset)).clamp(0., 1.);
61 let a = premultiplied(a.color);
62 let b = premultiplied(b.color);
63 return [
64 a[0] + (b[0] - a[0]) * amount,
65 a[1] + (b[1] - a[1]) * amount,
66 a[2] + (b[2] - a[2]) * amount,
67 a[3] + (b[3] - a[3]) * amount,
68 ];
69 }
70 }
71 premultiplied(last.color)
72}
73fn premultiplied(color: Color) -> [f32; 4] {
74 crate::premultiplied_working_rgba(color)
75}
76#[must_use]
79pub fn linear_gradient_t(start: Offset, end: Offset, point: Offset) -> f32 {
80 let dx = end.x - start.x;
81 let dy = end.y - start.y;
82 let length_squared = dx * dx + dy * dy;
83 if !length_squared.is_finite() || length_squared <= f32::EPSILON {
84 return 1.;
85 }
86 (((point.x - start.x) * dx + (point.y - start.y) * dy) / length_squared).clamp(0., 1.)
87}
88#[must_use]
91pub fn radial_gradient_t(center: Offset, radius: f32, point: Offset) -> f32 {
92 if !radius.is_finite() || radius <= 0. {
93 return 1.;
94 }
95 (((point.x - center.x).hypot(point.y - center.y)) / radius).clamp(0., 1.)
96}
97impl GradientStops {
98 #[must_use]
101 pub fn new(mut stops: Vec<GradientStop>) -> Self {
102 for s in &mut stops {
103 s.offset = if s.offset.is_finite() {
104 s.offset.clamp(0., 1.)
105 } else {
106 0.
107 };
108 }
109 stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
110 if stops.is_empty() {
111 stops.push(GradientStop {
112 offset: 0.,
113 color: Color::TRANSPARENT,
114 });
115 }
116 if stops.len() == 1 {
117 stops.push(GradientStop {
118 offset: 1.,
119 color: stops[0].color,
120 });
121 }
122 Self {
123 id: next_gradient_id(),
124 stops: stops.into(),
125 }
126 }
127 #[must_use]
128 pub fn as_slice(&self) -> &[GradientStop] {
129 &self.stops
130 }
131 #[must_use]
134 pub const fn id(&self) -> GradientId {
135 self.id
136 }
137}
138impl GradientId {
139 #[must_use]
140 pub const fn get(self) -> u64 {
141 self.0
142 }
143}
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
147pub enum TileMode {
148 #[default]
150 Clamp,
151 Repeated,
153 Mirror,
155 Decal,
157}
158
159impl TileMode {
160 #[must_use]
163 pub fn apply(self, t: f32) -> Option<f32> {
164 let t = finite_or_zero(t);
165 match self {
166 Self::Clamp => Some(t.clamp(0., 1.)),
167 Self::Repeated => Some(t - t.floor()),
168 Self::Mirror => {
169 let period = t * 0.5 - (t * 0.5).floor();
170 Some(1. - (period * 2. - 1.).abs())
171 }
172 Self::Decal => (0. ..=1.).contains(&t).then_some(t),
173 }
174 }
175
176 #[must_use]
178 pub const fn shader_code(self) -> f32 {
179 match self {
180 Self::Clamp => 0.,
181 Self::Repeated => 1.,
182 Self::Mirror => 2.,
183 Self::Decal => 3.,
184 }
185 }
186}
187
188#[derive(Clone, Debug, PartialEq)]
189pub struct LinearGradient {
190 pub start: Offset,
191 pub end: Offset,
192 pub stops: GradientStops,
193 pub tile_mode: TileMode,
195}
196#[derive(Clone, Debug, PartialEq)]
197pub struct RadialGradient {
198 pub center: Offset,
199 pub radius: f32,
200 pub stops: GradientStops,
201 pub tile_mode: TileMode,
203 pub focal: Option<Offset>,
206 pub focal_radius: f32,
209}
210#[derive(Clone, Debug, PartialEq)]
211pub struct SweepGradient {
212 pub center: Offset,
213 pub start_angle: f32,
214 pub stops: GradientStops,
215 pub tile_mode: TileMode,
217}
218
219impl LinearGradient {
220 #[must_use]
222 pub fn new(start: Offset, end: Offset, stops: GradientStops) -> Self {
223 Self {
224 start,
225 end,
226 stops,
227 tile_mode: TileMode::Clamp,
228 }
229 }
230}
231
232impl RadialGradient {
233 #[must_use]
235 pub fn new(center: Offset, radius: f32, stops: GradientStops) -> Self {
236 Self {
237 center,
238 radius,
239 stops,
240 tile_mode: TileMode::Clamp,
241 focal: None,
242 focal_radius: 0.,
243 }
244 }
245
246 #[must_use]
249 pub fn is_focal(&self) -> bool {
250 self.focal.is_some_and(|focal| focal != self.center) || self.focal_radius != 0.
251 }
252}
253
254impl SweepGradient {
255 #[must_use]
257 pub fn new(center: Offset, start_angle: f32, stops: GradientStops) -> Self {
258 Self {
259 center,
260 start_angle,
261 stops,
262 tile_mode: TileMode::Clamp,
263 }
264 }
265}
266
267#[must_use]
273pub fn two_point_conical_gradient_t(
274 focal: Offset,
275 focal_radius: f32,
276 center: Offset,
277 radius: f32,
278 point: Offset,
279) -> Option<f32> {
280 let cd = Offset::new(center.x - focal.x, center.y - focal.y);
281 let dr = radius - focal_radius;
282 let pd = Offset::new(point.x - focal.x, point.y - focal.y);
283 let a = cd.x * cd.x + cd.y * cd.y - dr * dr;
284 let b = pd.x * cd.x + pd.y * cd.y + focal_radius * dr;
285 let c = pd.x * pd.x + pd.y * pd.y - focal_radius * focal_radius;
286 let radius_at = |t: f32| focal_radius + t * dr;
287 if a.abs() <= f32::EPSILON {
288 if b == 0. {
290 return None;
291 }
292 let t = c / (2. * b);
293 return (radius_at(t) >= 0.).then_some(t);
294 }
295 let discriminant = b * b - a * c;
296 if discriminant < 0. {
297 return None;
298 }
299 let root = discriminant.sqrt();
300 let (high, low) = {
301 let first = (b + root) / a;
302 let second = (b - root) / a;
303 (first.max(second), first.min(second))
304 };
305 if radius_at(high) >= 0. {
306 Some(high)
307 } else if radius_at(low) >= 0. {
308 Some(low)
309 } else {
310 None
311 }
312}
313#[derive(Clone, Debug, PartialEq)]
314pub enum Brush {
315 Solid(Color),
316 LinearGradient(LinearGradient),
317 RadialGradient(RadialGradient),
318 SweepGradient(SweepGradient),
319}
320
321pub type Shader = Brush;
327
328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
335pub enum FilterQuality {
336 None,
337 #[default]
338 Low,
339 Medium,
340 High,
341}
342
343impl FilterQuality {
344 #[must_use]
345 pub const fn sampling(self) -> ImageSampling {
346 match self {
347 Self::None => ImageSampling::Nearest,
348 Self::Low | Self::Medium | Self::High => ImageSampling::Linear,
349 }
350 }
351}
352
353impl From<FilterQuality> for ImageSampling {
354 fn from(value: FilterQuality) -> Self {
355 value.sampling()
356 }
357}
358impl From<Color> for Brush {
359 fn from(color: Color) -> Self {
360 Self::Solid(color)
361 }
362}
363impl From<LinearGradient> for Brush {
364 fn from(gradient: LinearGradient) -> Self {
365 Self::LinearGradient(gradient)
366 }
367}
368impl From<RadialGradient> for Brush {
369 fn from(gradient: RadialGradient) -> Self {
370 Self::RadialGradient(gradient)
371 }
372}
373impl From<SweepGradient> for Brush {
374 fn from(gradient: SweepGradient) -> Self {
375 Self::SweepGradient(gradient)
376 }
377}
378#[must_use]
379pub fn sweep_gradient_t(center: Offset, start_angle: f32, point: Offset) -> f32 {
380 ((point.y - center.y).atan2(point.x - center.x) - start_angle).rem_euclid(std::f32::consts::TAU)
381 / std::f32::consts::TAU
382}