astrelis_geometry/
paint.rs1use astrelis_render::Color;
6use glam::Vec2;
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum Paint {
11 Solid(Color),
13 LinearGradient(LinearGradient),
15 RadialGradient(RadialGradient),
17}
18
19impl Paint {
20 pub fn solid(color: Color) -> Self {
22 Self::Solid(color)
23 }
24
25 pub fn linear_gradient(start: Vec2, end: Vec2, stops: Vec<GradientStop>) -> Self {
27 Self::LinearGradient(LinearGradient { start, end, stops })
28 }
29
30 pub fn radial_gradient(center: Vec2, radius: f32, stops: Vec<GradientStop>) -> Self {
32 Self::RadialGradient(RadialGradient {
33 center,
34 radius,
35 stops,
36 })
37 }
38
39 pub fn is_solid(&self) -> bool {
41 matches!(self, Self::Solid(_))
42 }
43
44 pub fn as_solid(&self) -> Option<Color> {
46 match self {
47 Self::Solid(color) => Some(*color),
48 _ => None,
49 }
50 }
51}
52
53impl Default for Paint {
54 fn default() -> Self {
55 Self::Solid(Color::BLACK)
56 }
57}
58
59impl From<Color> for Paint {
60 fn from(color: Color) -> Self {
61 Self::Solid(color)
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub struct LinearGradient {
68 pub start: Vec2,
70 pub end: Vec2,
72 pub stops: Vec<GradientStop>,
74}
75
76impl LinearGradient {
77 pub fn new(start: Vec2, end: Vec2, stops: Vec<GradientStop>) -> Self {
79 Self { start, end, stops }
80 }
81
82 pub fn horizontal(width: f32, stops: Vec<GradientStop>) -> Self {
84 Self {
85 start: Vec2::ZERO,
86 end: Vec2::new(width, 0.0),
87 stops,
88 }
89 }
90
91 pub fn vertical(height: f32, stops: Vec<GradientStop>) -> Self {
93 Self {
94 start: Vec2::ZERO,
95 end: Vec2::new(0.0, height),
96 stops,
97 }
98 }
99
100 pub fn direction(&self) -> Vec2 {
102 (self.end - self.start).normalize_or_zero()
103 }
104
105 pub fn sample(&self, position: Vec2) -> Color {
107 if self.stops.is_empty() {
108 return Color::TRANSPARENT;
109 }
110 if self.stops.len() == 1 {
111 return self.stops[0].color;
112 }
113
114 let dir = self.end - self.start;
115 let len_sq = dir.length_squared();
116 if len_sq < f32::EPSILON {
117 return self.stops[0].color;
118 }
119
120 let t = ((position - self.start).dot(dir) / len_sq).clamp(0.0, 1.0);
122
123 interpolate_gradient(&self.stops, t)
124 }
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct RadialGradient {
130 pub center: Vec2,
132 pub radius: f32,
134 pub stops: Vec<GradientStop>,
136}
137
138impl RadialGradient {
139 pub fn new(center: Vec2, radius: f32, stops: Vec<GradientStop>) -> Self {
141 Self {
142 center,
143 radius,
144 stops,
145 }
146 }
147
148 pub fn sample(&self, position: Vec2) -> Color {
150 if self.stops.is_empty() {
151 return Color::TRANSPARENT;
152 }
153 if self.stops.len() == 1 {
154 return self.stops[0].color;
155 }
156
157 let dist = (position - self.center).length();
158 let t = (dist / self.radius).clamp(0.0, 1.0);
159
160 interpolate_gradient(&self.stops, t)
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq)]
166pub struct GradientStop {
167 pub offset: f32,
169 pub color: Color,
171}
172
173impl GradientStop {
174 pub fn new(offset: f32, color: Color) -> Self {
176 Self {
177 offset: offset.clamp(0.0, 1.0),
178 color,
179 }
180 }
181}
182
183fn interpolate_gradient(stops: &[GradientStop], t: f32) -> Color {
185 if stops.is_empty() {
186 return Color::TRANSPARENT;
187 }
188 if stops.len() == 1 {
189 return stops[0].color;
190 }
191
192 let mut prev = &stops[0];
194 for stop in &stops[1..] {
195 if t <= stop.offset {
196 let range = stop.offset - prev.offset;
198 if range < f32::EPSILON {
199 return stop.color;
200 }
201 let local_t = (t - prev.offset) / range;
202 return lerp_color(prev.color, stop.color, local_t);
203 }
204 prev = stop;
205 }
206
207 stops.last().map(|s| s.color).unwrap_or(Color::TRANSPARENT)
209}
210
211fn lerp_color(a: Color, b: Color, t: f32) -> Color {
213 Color::rgba(
214 a.r + (b.r - a.r) * t,
215 a.g + (b.g - a.g) * t,
216 a.b + (b.b - a.b) * t,
217 a.a + (b.a - a.a) * t,
218 )
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_solid_paint() {
227 let paint = Paint::solid(Color::RED);
228 assert!(paint.is_solid());
229 assert_eq!(paint.as_solid(), Some(Color::RED));
230 }
231
232 #[test]
233 fn test_linear_gradient_sample() {
234 let gradient = LinearGradient::horizontal(
235 100.0,
236 vec![
237 GradientStop::new(0.0, Color::RED),
238 GradientStop::new(1.0, Color::BLUE),
239 ],
240 );
241
242 let at_start = gradient.sample(Vec2::new(0.0, 0.0));
243 let at_end = gradient.sample(Vec2::new(100.0, 0.0));
244 let at_mid = gradient.sample(Vec2::new(50.0, 0.0));
245
246 assert!((at_start.r - 1.0).abs() < 0.01);
247 assert!((at_end.b - 1.0).abs() < 0.01);
248 assert!((at_mid.r - 0.5).abs() < 0.01);
250 assert!((at_mid.b - 0.5).abs() < 0.01);
251 }
252
253 #[test]
254 fn test_gradient_stop() {
255 let stop = GradientStop::new(0.5, Color::GREEN);
256 assert_eq!(stop.offset, 0.5);
257 assert_eq!(stop.color, Color::GREEN);
258 }
259}