1use crate::{Alignment, AlignmentGeometry, Color, TextDirection};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum TileMode {
16 #[default]
18 Clamp,
19 Repeated,
21 Mirror,
23 Decal,
25}
26
27#[derive(Clone, Debug, PartialEq)]
30pub struct LinearGradient {
31 pub begin: AlignmentGeometry,
33 pub end: AlignmentGeometry,
35 pub colors: Vec<Color>,
37 pub stops: Option<Vec<f32>>,
40 pub tile_mode: TileMode,
42 pub rotation: f32,
45}
46
47#[derive(Clone, Debug, PartialEq)]
52pub struct RadialGradient {
53 pub center: AlignmentGeometry,
55 pub radius: f32,
57 pub colors: Vec<Color>,
59 pub stops: Option<Vec<f32>>,
61 pub tile_mode: TileMode,
63 pub focal: Option<AlignmentGeometry>,
65 pub focal_radius: f32,
67 pub rotation: f32,
69}
70
71#[derive(Clone, Debug, PartialEq)]
74pub struct SweepGradient {
75 pub center: AlignmentGeometry,
77 pub start_angle: f32,
79 pub end_angle: f32,
81 pub colors: Vec<Color>,
83 pub stops: Option<Vec<f32>>,
85 pub tile_mode: TileMode,
87 pub rotation: f32,
89}
90
91#[derive(Clone, Debug, PartialEq)]
93pub enum Gradient {
94 Linear(LinearGradient),
96 Radial(RadialGradient),
98 Sweep(SweepGradient),
100}
101
102fn physical(a: Alignment) -> AlignmentGeometry {
103 AlignmentGeometry::Physical(a)
104}
105
106impl LinearGradient {
107 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
109 Self {
110 begin: physical(Alignment::CENTER_LEFT),
111 end: physical(Alignment::CENTER_RIGHT),
112 colors: colors.into(),
113 stops: None,
114 tile_mode: TileMode::Clamp,
115 rotation: 0.0,
116 }
117 }
118
119 #[must_use]
121 pub fn between(
122 mut self,
123 begin: impl Into<AlignmentGeometry>,
124 end: impl Into<AlignmentGeometry>,
125 ) -> Self {
126 self.begin = begin.into();
127 self.end = end.into();
128 self
129 }
130}
131
132impl RadialGradient {
133 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
135 Self {
136 center: physical(Alignment::CENTER),
137 radius: 0.5,
138 colors: colors.into(),
139 stops: None,
140 tile_mode: TileMode::Clamp,
141 focal: None,
142 focal_radius: 0.0,
143 rotation: 0.0,
144 }
145 }
146}
147
148impl SweepGradient {
149 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
151 Self {
152 center: physical(Alignment::CENTER),
153 start_angle: 0.0,
154 end_angle: std::f32::consts::TAU,
155 colors: colors.into(),
156 stops: None,
157 tile_mode: TileMode::Clamp,
158 rotation: 0.0,
159 }
160 }
161}
162
163impl From<LinearGradient> for Gradient {
164 fn from(g: LinearGradient) -> Self {
165 Gradient::Linear(g)
166 }
167}
168
169impl From<RadialGradient> for Gradient {
170 fn from(g: RadialGradient) -> Self {
171 Gradient::Radial(g)
172 }
173}
174
175impl From<SweepGradient> for Gradient {
176 fn from(g: SweepGradient) -> Self {
177 Gradient::Sweep(g)
178 }
179}
180
181impl Gradient {
182 pub fn colors(&self) -> &[Color] {
184 match self {
185 Gradient::Linear(g) => &g.colors,
186 Gradient::Radial(g) => &g.colors,
187 Gradient::Sweep(g) => &g.colors,
188 }
189 }
190
191 fn colors_mut(&mut self) -> &mut Vec<Color> {
192 match self {
193 Gradient::Linear(g) => &mut g.colors,
194 Gradient::Radial(g) => &mut g.colors,
195 Gradient::Sweep(g) => &mut g.colors,
196 }
197 }
198
199 pub fn stops(&self) -> Option<&[f32]> {
201 match self {
202 Gradient::Linear(g) => g.stops.as_deref(),
203 Gradient::Radial(g) => g.stops.as_deref(),
204 Gradient::Sweep(g) => g.stops.as_deref(),
205 }
206 }
207
208 pub fn tile_mode(&self) -> TileMode {
210 match self {
211 Gradient::Linear(g) => g.tile_mode,
212 Gradient::Radial(g) => g.tile_mode,
213 Gradient::Sweep(g) => g.tile_mode,
214 }
215 }
216
217 pub fn rotation(&self) -> f32 {
219 match self {
220 Gradient::Linear(g) => g.rotation,
221 Gradient::Radial(g) => g.rotation,
222 Gradient::Sweep(g) => g.rotation,
223 }
224 }
225
226 pub fn resolved_stops(&self) -> Vec<f32> {
229 let n = self.colors().len();
230 match self.stops() {
231 Some(stops) if stops.len() == n => stops.to_vec(),
232 _ if n <= 1 => vec![0.0; n],
233 _ => (0..n).map(|i| i as f32 / (n - 1) as f32).collect(),
234 }
235 }
236
237 pub fn sample(&self, t: f32) -> Color {
240 let colors = self.colors();
241 match colors {
242 [] => Color::TRANSPARENT,
243 [only] => *only,
244 _ => {
245 let stops = self.resolved_stops();
246 if t <= stops[0] {
247 return colors[0];
248 }
249 for i in 1..colors.len() {
250 if t <= stops[i] {
251 let span = stops[i] - stops[i - 1];
252 let k = if span > 0.0 {
253 (t - stops[i - 1]) / span
254 } else {
255 1.0
256 };
257 return colors[i - 1].lerp(colors[i], k);
258 }
259 }
260 colors[colors.len() - 1]
261 }
262 }
263 }
264
265 #[must_use]
267 pub fn resolve(&self, direction: TextDirection) -> Gradient {
268 let fix = |a: AlignmentGeometry| physical(a.resolve(direction));
269 match self {
270 Gradient::Linear(g) => Gradient::Linear(LinearGradient {
271 begin: fix(g.begin),
272 end: fix(g.end),
273 ..g.clone()
274 }),
275 Gradient::Radial(g) => Gradient::Radial(RadialGradient {
276 center: fix(g.center),
277 focal: g.focal.map(fix),
278 ..g.clone()
279 }),
280 Gradient::Sweep(g) => Gradient::Sweep(SweepGradient {
281 center: fix(g.center),
282 ..g.clone()
283 }),
284 }
285 }
286
287 #[must_use]
290 pub fn scale(&self, factor: f32) -> Gradient {
291 let mut g = self.clone();
292 for c in g.colors_mut() {
293 *c = c.fade(factor);
294 }
295 g
296 }
297
298 #[must_use]
302 pub fn lerp(a: Option<&Gradient>, b: Option<&Gradient>, t: f32) -> Option<Gradient> {
303 let mix = |x: f32, y: f32| x + (y - x) * t;
304 let mix_a = |x: AlignmentGeometry, y: AlignmentGeometry| {
305 let (x, y) = (x.resolve(TextDirection::Ltr), y.resolve(TextDirection::Ltr));
306 physical(Alignment::new(mix(x.x, y.x), mix(x.y, y.y)))
307 };
308 let colors = |x: &[Color], y: &[Color]| -> Vec<Color> {
309 x.iter().zip(y).map(|(p, q)| p.lerp(*q, t)).collect()
310 };
311 let stops = |x: &Gradient, y: &Gradient| -> Option<Vec<f32>> {
312 if x.stops().is_none() && y.stops().is_none() {
313 return None;
314 }
315 Some(
316 x.resolved_stops()
317 .iter()
318 .zip(y.resolved_stops())
319 .map(|(p, q)| mix(*p, q))
320 .collect(),
321 )
322 };
323 match (a, b) {
324 (None, None) => None,
325 (Some(a), None) => Some(a.scale(1.0 - t)),
326 (None, Some(b)) => Some(b.scale(t)),
327 (Some(a), Some(b)) if a.colors().len() == b.colors().len() => {
328 let tile = if t < 0.5 {
329 a.tile_mode()
330 } else {
331 b.tile_mode()
332 };
333 match (a, b) {
334 (Gradient::Linear(x), Gradient::Linear(y)) => {
335 Some(Gradient::Linear(LinearGradient {
336 begin: mix_a(x.begin, y.begin),
337 end: mix_a(x.end, y.end),
338 colors: colors(&x.colors, &y.colors),
339 stops: stops(a, b),
340 tile_mode: tile,
341 rotation: mix(x.rotation, y.rotation),
342 }))
343 }
344 (Gradient::Radial(x), Gradient::Radial(y)) => {
345 Some(Gradient::Radial(RadialGradient {
346 center: mix_a(x.center, y.center),
347 radius: mix(x.radius, y.radius),
348 colors: colors(&x.colors, &y.colors),
349 stops: stops(a, b),
350 tile_mode: tile,
351 focal: match (x.focal, y.focal) {
352 (None, None) => None,
353 (p, q) => Some(mix_a(p.unwrap_or(x.center), q.unwrap_or(y.center))),
354 },
355 focal_radius: mix(x.focal_radius, y.focal_radius),
356 rotation: mix(x.rotation, y.rotation),
357 }))
358 }
359 (Gradient::Sweep(x), Gradient::Sweep(y)) => {
360 Some(Gradient::Sweep(SweepGradient {
361 center: mix_a(x.center, y.center),
362 start_angle: mix(x.start_angle, y.start_angle),
363 end_angle: mix(x.end_angle, y.end_angle),
364 colors: colors(&x.colors, &y.colors),
365 stops: stops(a, b),
366 tile_mode: tile,
367 rotation: mix(x.rotation, y.rotation),
368 }))
369 }
370 _ => Some(if t < 0.5 {
371 a.scale(1.0 - 2.0 * t)
372 } else {
373 b.scale(2.0 * t - 1.0)
374 }),
375 }
376 }
377 (Some(a), Some(b)) => Some(if t < 0.5 {
378 a.scale(1.0 - 2.0 * t)
379 } else {
380 b.scale(2.0 * t - 1.0)
381 }),
382 }
383 }
384}
385
386pub fn tile(t: f32, mode: TileMode) -> Option<f32> {
389 match mode {
390 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
391 TileMode::Repeated => Some(t - t.floor()),
392 TileMode::Mirror => {
393 let m = t.rem_euclid(2.0);
394 Some(if m <= 1.0 { m } else { 2.0 - m })
395 }
396 TileMode::Decal => (0.0..=1.0).contains(&t).then_some(t),
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 const RED: Color = Color::rgb(1.0, 0.0, 0.0);
405 const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
406 const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
407
408 #[test]
411 fn colours_are_sampled_between_their_stops() {
412 let even = Gradient::from(LinearGradient::new(vec![RED, GREEN, BLUE]));
413 assert_eq!(even.resolved_stops(), vec![0.0, 0.5, 1.0]);
414 assert_eq!(even.sample(0.0), RED);
415 assert_eq!(even.sample(0.5), GREEN);
416 assert_eq!(even.sample(0.25), RED.lerp(GREEN, 0.5));
417 assert_eq!(even.sample(1.0), BLUE);
418
419 let mut stopped = LinearGradient::new(vec![RED, BLUE]);
420 stopped.stops = Some(vec![0.2, 0.6]);
421 let stopped = Gradient::from(stopped);
422 assert_eq!(stopped.sample(0.1), RED, "held before the first stop");
423 let mid = stopped.sample(0.4);
424 assert!(
425 (mid.r - 0.5).abs() < 1e-6 && (mid.b - 0.5).abs() < 1e-6,
426 "half way between the stops: {mid:?}"
427 );
428 assert_eq!(stopped.sample(0.9), BLUE, "and after the last");
429
430 let mut wrong = LinearGradient::new(vec![RED, GREEN, BLUE]);
432 wrong.stops = Some(vec![0.0, 1.0]);
433 assert_eq!(Gradient::from(wrong).resolved_stops(), vec![0.0, 0.5, 1.0]);
434 }
435
436 #[test]
438 fn the_tile_modes() {
439 assert_eq!(tile(1.4, TileMode::Clamp), Some(1.0));
440 assert_eq!(tile(-0.3, TileMode::Clamp), Some(0.0));
441 assert!((tile(1.25, TileMode::Repeated).unwrap() - 0.25).abs() < 1e-6);
442 assert!((tile(1.25, TileMode::Mirror).unwrap() - 0.75).abs() < 1e-6);
443 assert!((tile(-0.25, TileMode::Mirror).unwrap() - 0.25).abs() < 1e-6);
444 assert_eq!(tile(1.25, TileMode::Decal), None);
445 assert_eq!(tile(0.5, TileMode::Decal), Some(0.5));
446 }
447
448 #[test]
450 fn the_defaults_are_the_reference_s() {
451 let l = LinearGradient::new(vec![RED, BLUE]);
452 assert_eq!(l.begin, physical(Alignment::CENTER_LEFT));
453 assert_eq!(l.end, physical(Alignment::CENTER_RIGHT));
454 let r = RadialGradient::new(vec![RED, BLUE]);
455 assert_eq!((r.radius, r.focal, r.focal_radius), (0.5, None, 0.0));
456 let s = SweepGradient::new(vec![RED, BLUE]);
457 assert_eq!((s.start_angle, s.end_angle), (0.0, std::f32::consts::TAU));
458 assert_eq!(l.tile_mode, TileMode::Clamp);
459 }
460
461 #[test]
464 fn gradients_lerp_as_the_reference_s() {
465 let a = Gradient::from(LinearGradient::new(vec![RED, RED]));
466 let b = Gradient::from(
467 LinearGradient::new(vec![BLUE, BLUE])
468 .between(Alignment::TOP_CENTER, Alignment::BOTTOM_CENTER),
469 );
470 let mid = Gradient::lerp(Some(&a), Some(&b), 0.5).unwrap();
471 match &mid {
472 Gradient::Linear(g) => {
473 assert_eq!(g.colors[0], RED.lerp(BLUE, 0.5));
474 assert_eq!(g.begin, physical(Alignment::new(-0.5, -0.5)));
475 }
476 other => panic!("{other:?}"),
477 }
478 let fading = Gradient::lerp(Some(&a), None, 0.25).unwrap();
479 assert!((fading.colors()[0].a - 0.75).abs() < 1e-6);
480 let sweep = Gradient::from(SweepGradient::new(vec![GREEN, GREEN]));
482 assert!(matches!(
483 Gradient::lerp(Some(&a), Some(&sweep), 0.25),
484 Some(Gradient::Linear(_))
485 ));
486 assert!(matches!(
487 Gradient::lerp(Some(&a), Some(&sweep), 0.75),
488 Some(Gradient::Sweep(_))
489 ));
490 }
491
492 #[test]
495 fn a_faded_gradient_rectangle_carries_the_fade() {
496 let mut scene = crate::Scene::new();
497 scene.shaded_rect(
498 crate::Rect::new(0.0, 0.0, 10.0, 10.0),
499 Gradient::from(LinearGradient::new(vec![RED, BLUE])),
500 0.5,
501 0.0,
502 0.0,
503 Color::TRANSPARENT,
504 );
505 match &scene.primitives()[0] {
506 crate::Primitive::Rect {
507 shader: Some(g),
508 color,
509 ..
510 } => {
511 assert_eq!(g.colors()[1].a, 0.5);
512 assert_eq!(
513 *color,
514 RED.fade(0.5),
515 "the first colour, for what reads a colour"
516 );
517 }
518 other => panic!("{other:?}"),
519 }
520 }
521}