1use crate::clip::ClipRect;
4use crate::framebuffer::{pack_rgba, Framebuffer};
5use oxiui_core::Color;
6
7#[derive(Clone, Copy, Debug)]
9pub struct GradientStop {
10 pub offset: f32,
12 pub color: Color,
14}
15
16impl GradientStop {
17 pub fn new(offset: f32, color: Color) -> Self {
19 Self {
20 offset: offset.clamp(0.0, 1.0),
21 color,
22 }
23 }
24}
25
26#[derive(Clone, Debug)]
28pub struct LinearGradient {
29 pub start: (f32, f32),
31 pub end: (f32, f32),
33 pub stops: Vec<GradientStop>,
35}
36
37impl LinearGradient {
38 pub fn two_stop(start: (f32, f32), end: (f32, f32), from: Color, to: Color) -> Self {
40 Self {
41 start,
42 end,
43 stops: vec![GradientStop::new(0.0, from), GradientStop::new(1.0, to)],
44 }
45 }
46
47 pub fn new(start: (f32, f32), end: (f32, f32), mut stops: Vec<GradientStop>) -> Self {
49 stops.sort_by(|a, b| {
50 a.offset
51 .partial_cmp(&b.offset)
52 .unwrap_or(core::cmp::Ordering::Equal)
53 });
54 Self { start, end, stops }
55 }
56
57 pub fn sample(&self, t: f32) -> Color {
59 if self.stops.is_empty() {
60 return Color(0, 0, 0, 0);
61 }
62 let t = t.clamp(0.0, 1.0);
63 if t <= self.stops[0].offset {
65 return self.stops[0].color;
66 }
67 let last = self.stops.len() - 1;
68 if t >= self.stops[last].offset {
69 return self.stops[last].color;
70 }
71 for w in self.stops.windows(2) {
73 let a = &w[0];
74 let b = &w[1];
75 if t >= a.offset && t <= b.offset {
76 let span = (b.offset - a.offset).max(f32::EPSILON);
77 let local = (t - a.offset) / span;
78 return lerp_color(&a.color, &b.color, local);
79 }
80 }
81 self.stops[last].color
82 }
83
84 pub fn fill_rect(&self, fb: &mut Framebuffer, clip: &ClipRect, x: f32, y: f32, w: f32, h: f32) {
87 if w <= 0.0 || h <= 0.0 {
88 return;
89 }
90 let (sx, sy) = self.start;
91 let (ex, ey) = self.end;
92 let axis_x = ex - sx;
93 let axis_y = ey - sy;
94 let len_sq = axis_x * axis_x + axis_y * axis_y;
95 let x0 = (x.floor() as i64).max(clip.x0).max(0);
96 let y0 = (y.floor() as i64).max(clip.y0).max(0);
97 let x1 = ((x + w).ceil() as i64).min(clip.x1);
98 let y1 = ((y + h).ceil() as i64).min(clip.y1);
99 for py in y0..y1 {
100 for px in x0..x1 {
101 let t = if len_sq <= f32::EPSILON {
102 0.0
103 } else {
104 let rx = px as f32 + 0.5 - sx;
105 let ry = py as f32 + 0.5 - sy;
106 (rx * axis_x + ry * axis_y) / len_sq
107 };
108 let c = self.sample(t);
109 let Color(r, g, b, a) = c;
110 fb.blend(px as u32, py as u32, pack_rgba(r, g, b, a));
111 }
112 }
113 }
114}
115
116pub fn lerp_color(a: &Color, b: &Color, t: f32) -> Color {
118 let t = t.clamp(0.0, 1.0);
119 let mix = |x: u8, y: u8| -> u8 {
120 let v = x as f32 + (y as f32 - x as f32) * t;
121 v.round().clamp(0.0, 255.0) as u8
122 };
123 Color(mix(a.0, b.0), mix(a.1, b.1), mix(a.2, b.2), mix(a.3, b.3))
124}
125
126#[derive(Clone, Debug)]
135pub struct RadialGradient {
136 pub center: (f32, f32),
138 pub radius: f32,
140 pub stops: Vec<GradientStop>,
142}
143
144impl RadialGradient {
145 pub fn two_stop(center: (f32, f32), radius: f32, inner: Color, outer: Color) -> Self {
148 Self {
149 center,
150 radius,
151 stops: vec![GradientStop::new(0.0, inner), GradientStop::new(1.0, outer)],
152 }
153 }
154
155 pub fn new(center: (f32, f32), radius: f32, mut stops: Vec<GradientStop>) -> Self {
157 stops.sort_by(|a, b| {
158 a.offset
159 .partial_cmp(&b.offset)
160 .unwrap_or(core::cmp::Ordering::Equal)
161 });
162 Self {
163 center,
164 radius,
165 stops,
166 }
167 }
168
169 pub fn color_at(&self, px: f32, py: f32) -> Color {
174 if self.stops.is_empty() {
175 return Color(0, 0, 0, 0);
176 }
177 let r = self.radius.max(f32::EPSILON);
178 let dx = px - self.center.0;
179 let dy = py - self.center.1;
180 let dist = (dx * dx + dy * dy).sqrt();
181 let t = (dist / r).clamp(0.0, 1.0);
182 self.sample(t)
183 }
184
185 pub fn sample(&self, t: f32) -> Color {
187 if self.stops.is_empty() {
188 return Color(0, 0, 0, 0);
189 }
190 let t = t.clamp(0.0, 1.0);
191 if t <= self.stops[0].offset {
192 return self.stops[0].color;
193 }
194 let last = self.stops.len() - 1;
195 if t >= self.stops[last].offset {
196 return self.stops[last].color;
197 }
198 for w in self.stops.windows(2) {
199 let a = &w[0];
200 let b = &w[1];
201 if t >= a.offset && t <= b.offset {
202 let span = (b.offset - a.offset).max(f32::EPSILON);
203 let local = (t - a.offset) / span;
204 return lerp_color(&a.color, &b.color, local);
205 }
206 }
207 self.stops[last].color
208 }
209
210 pub fn fill_rect(&self, fb: &mut Framebuffer, clip: &ClipRect, x: f32, y: f32, w: f32, h: f32) {
213 if w <= 0.0 || h <= 0.0 {
214 return;
215 }
216 let x0 = (x.floor() as i64).max(clip.x0).max(0);
217 let y0 = (y.floor() as i64).max(clip.y0).max(0);
218 let x1 = ((x + w).ceil() as i64).min(clip.x1);
219 let y1 = ((y + h).ceil() as i64).min(clip.y1);
220 for py in y0..y1 {
221 for px in x0..x1 {
222 let c = self.color_at(px as f32 + 0.5, py as f32 + 0.5);
223 let Color(r, g, b, a) = c;
224 fb.blend(px as u32, py as u32, pack_rgba(r, g, b, a));
225 }
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn lerp_endpoints_and_midpoint() {
236 let red = Color(255, 0, 0, 255);
237 let blue = Color(0, 0, 255, 255);
238 assert_eq!(lerp_color(&red, &blue, 0.0), red);
239 assert_eq!(lerp_color(&red, &blue, 1.0), blue);
240 let mid = lerp_color(&red, &blue, 0.5);
241 assert!((120..=135).contains(&mid.0));
243 assert!((120..=135).contains(&mid.2));
244 assert_eq!(mid.1, 0);
245 }
246
247 #[test]
248 fn sample_two_stop() {
249 let g = LinearGradient::two_stop(
250 (0.0, 0.0),
251 (10.0, 0.0),
252 Color(255, 0, 0, 255),
253 Color(0, 0, 255, 255),
254 );
255 assert_eq!(g.sample(0.0), Color(255, 0, 0, 255));
256 assert_eq!(g.sample(1.0), Color(0, 0, 255, 255));
257 let m = g.sample(0.5);
258 assert!((120..=135).contains(&m.0));
259 }
260
261 #[test]
262 fn fill_rect_horizontal_midpoint_is_purple() {
263 let mut fb = Framebuffer::with_fill(10, 1, Color(0, 0, 0, 255));
264 let clip = ClipRect::full(10, 1);
265 let g = LinearGradient::two_stop(
266 (0.0, 0.0),
267 (10.0, 0.0),
268 Color(255, 0, 0, 255),
269 Color(0, 0, 255, 255),
270 );
271 g.fill_rect(&mut fb, &clip, 0.0, 0.0, 10.0, 1.0);
272 let (lr, _, _, _) = fb.get_rgba(0, 0).expect("left");
274 assert!(lr > 200);
275 let (_, _, rb, _) = fb.get_rgba(9, 0).expect("right");
277 assert!(rb > 200);
278 let (mr, _, mb, _) = fb.get_rgba(5, 0).expect("mid");
280 assert!(mr > 0 && mb > 0);
281 }
282
283 #[test]
284 fn multi_stop_sorted() {
285 let g = LinearGradient::new(
286 (0.0, 0.0),
287 (1.0, 0.0),
288 vec![
289 GradientStop::new(1.0, Color(0, 0, 255, 255)),
290 GradientStop::new(0.0, Color(255, 0, 0, 255)),
291 GradientStop::new(0.5, Color(0, 255, 0, 255)),
292 ],
293 );
294 assert_eq!(g.sample(0.5), Color(0, 255, 0, 255));
296 }
297
298 #[test]
299 fn radial_gradient_midpoint() {
300 let g = RadialGradient::two_stop(
302 (5.0, 5.0),
303 10.0,
304 Color(255, 0, 0, 255),
305 Color(0, 0, 255, 255),
306 );
307 let c_center = g.color_at(5.0, 5.0);
309 assert_eq!(c_center, Color(255, 0, 0, 255));
310 let c_edge = g.color_at(15.0, 5.0); assert_eq!(c_edge, Color(0, 0, 255, 255));
313 let c_mid = g.color_at(10.0, 5.0); assert!((100..=160).contains(&c_mid.0), "r={}", c_mid.0);
316 assert!((100..=160).contains(&c_mid.2), "b={}", c_mid.2);
317 }
318
319 #[test]
320 fn radial_gradient_fill_rect() {
321 let mut fb = Framebuffer::with_fill(20, 20, Color(0, 0, 0, 255));
322 let clip = ClipRect::full(20, 20);
323 let g = RadialGradient::two_stop(
324 (10.0, 10.0),
325 10.0,
326 Color(255, 0, 0, 255),
327 Color(0, 0, 255, 255),
328 );
329 g.fill_rect(&mut fb, &clip, 0.0, 0.0, 20.0, 20.0);
330 let (r, _, b, _) = fb.get_rgba(10, 10).expect("centre");
332 assert!(r > b, "centre should be red-dominant (r={r}, b={b})");
333 let (r2, _, b2, _) = fb.get_rgba(0, 0).expect("corner");
335 assert!(b2 >= r2, "corner should be blue-dominant (r={r2}, b={b2})");
336 }
337}