1use kurbo::Point;
2
3use crate::{Color, Lerp};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum GradientKind {
8 Linear {
10 from: Point,
12 to: Point,
14 },
15 Radial {
17 center: Point,
19 radius: f64,
21 },
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct Gradient {
27 pub kind: GradientKind,
29 pub stops: Vec<(f64, Color)>,
31}
32
33impl Gradient {
34 pub fn linear(from: impl Into<Point>, to: impl Into<Point>) -> Self {
36 Self {
37 kind: GradientKind::Linear {
38 from: from.into(),
39 to: to.into(),
40 },
41 stops: Vec::new(),
42 }
43 }
44
45 pub fn radial(center: impl Into<Point>, radius: f64) -> Self {
47 Self {
48 kind: GradientKind::Radial {
49 center: center.into(),
50 radius,
51 },
52 stops: Vec::new(),
53 }
54 }
55
56 pub fn stop(mut self, offset: f64, color: Color) -> Self {
58 self.stops.push((offset.clamp(0.0, 1.0), color));
59 self.stops.sort_by(|a, b| a.0.total_cmp(&b.0));
60 self
61 }
62
63 pub fn stops(mut self, colors: impl IntoIterator<Item = Color>) -> Self {
65 let colors: Vec<Color> = colors.into_iter().collect();
66 let n = colors.len().max(2) - 1;
67 self.stops = colors
68 .into_iter()
69 .enumerate()
70 .map(|(i, c)| (i as f64 / n as f64, c))
71 .collect();
72 self
73 }
74
75 pub fn with_opacity(&self, opacity: f64) -> Self {
77 Self {
78 kind: self.kind,
79 stops: self
80 .stops
81 .iter()
82 .map(|(o, c)| (*o, c.with_alpha((f64::from(c.a) * opacity) as f32)))
83 .collect(),
84 }
85 }
86
87 pub fn is_visible(&self) -> bool {
89 self.stops.iter().any(|(_, c)| c.a > 0.0)
90 }
91}
92
93impl Lerp for Gradient {
96 fn lerp(&self, to: &Self, t: f64) -> Self {
97 let kind = match (self.kind, to.kind) {
98 (GradientKind::Linear { from: a, to: b }, GradientKind::Linear { from: c, to: d }) => {
99 GradientKind::Linear {
100 from: Lerp::lerp(&a, &c, t),
101 to: Lerp::lerp(&b, &d, t),
102 }
103 }
104 (
105 GradientKind::Radial {
106 center: a,
107 radius: r1,
108 },
109 GradientKind::Radial {
110 center: b,
111 radius: r2,
112 },
113 ) => GradientKind::Radial {
114 center: Lerp::lerp(&a, &b, t),
115 radius: r1.lerp(&r2, t),
116 },
117 _ => return if t < 0.5 { self.clone() } else { to.clone() },
118 };
119 if self.stops.len() != to.stops.len() {
120 return if t < 0.5 { self.clone() } else { to.clone() };
121 }
122 Self {
123 kind,
124 stops: self
125 .stops
126 .iter()
127 .zip(&to.stops)
128 .map(|((o1, c1), (o2, c2))| (o1.lerp(o2, t), c1.lerp(c2, t)))
129 .collect(),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
136pub enum BlendMode {
137 #[default]
138 Normal,
140 Multiply,
142 Screen,
144 Overlay,
146 Darken,
148 Lighten,
150 ColorDodge,
152 ColorBurn,
154 HardLight,
156 SoftLight,
158 Difference,
160 Exclusion,
162 Hue,
164 Saturation,
166 Color,
168 Luminosity,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn gradient_lerp() {
178 let a = Gradient::linear((0.0, 0.0), (10.0, 0.0)).stops([Color::BLACK, Color::WHITE]);
179 let b = Gradient::linear((0.0, 0.0), (20.0, 0.0)).stops([Color::WHITE, Color::BLACK]);
180 let m = a.lerp(&b, 0.5);
181 assert_eq!(
182 m.kind,
183 GradientKind::Linear {
184 from: Point::ZERO,
185 to: Point::new(15.0, 0.0)
186 }
187 );
188 assert_eq!(m.stops.len(), 2);
189 let c = Gradient::radial((0.0, 0.0), 5.0).stop(0.0, Color::BLACK);
190 assert_eq!(a.lerp(&c, 0.4), a);
191 assert_eq!(a.lerp(&c, 0.6), c);
192 }
193
194 #[test]
195 fn evenly_spaced_stops() {
196 let g = Gradient::linear((0.0, 0.0), (1.0, 0.0)).stops([
197 Color::BLACK,
198 Color::WHITE,
199 Color::BLACK,
200 ]);
201 assert_eq!(
202 g.stops.iter().map(|s| s.0).collect::<Vec<_>>(),
203 vec![0.0, 0.5, 1.0]
204 );
205 }
206}