1use core::fmt;
8use std::sync::Arc;
9
10use crate::color::Color;
11use crate::geometry::{Point, Transform};
12
13use super::Shader;
14
15const LUT_SIZE: usize = 256;
17
18#[derive(Clone)]
27struct ColorRamp {
28 lut: Arc<[Color; LUT_SIZE]>,
29}
30
31impl ColorRamp {
32 fn new(stops: &[GradientStop]) -> ColorRamp {
35 let mut lut = [Color::TRANSPARENT; LUT_SIZE];
36 for (i, slot) in lut.iter_mut().enumerate() {
37 let t = i as f32 / (LUT_SIZE - 1) as f32;
38 *slot = sample_stops(stops, t);
39 }
40 ColorRamp { lut: Arc::new(lut) }
41 }
42
43 #[inline]
46 fn sample(&self, t: f32) -> Color {
47 let x = t.clamp(0.0, 1.0) * (LUT_SIZE - 1) as f32;
48 let i = x as usize; if i >= LUT_SIZE - 1 {
50 return self.lut[LUT_SIZE - 1];
51 }
52 let frac = x - i as f32;
53 let a = self.lut[i];
54 let b = self.lut[i + 1];
55 Color::from_rgba(
56 a.red() + (b.red() - a.red()) * frac,
57 a.green() + (b.green() - a.green()) * frac,
58 a.blue() + (b.blue() - a.blue()) * frac,
59 a.alpha() + (b.alpha() - a.alpha()) * frac,
60 )
61 }
62}
63
64impl fmt::Debug for ColorRamp {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "ColorRamp({LUT_SIZE} entries)")
67 }
68}
69
70#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
72pub enum SpreadMode {
73 #[default]
75 Pad,
76 Repeat,
78 Reflect,
80}
81
82#[derive(Copy, Clone, Debug, PartialEq)]
84pub struct GradientStop {
85 pub position: f32,
86 pub color: Color,
87}
88
89impl GradientStop {
90 pub fn new(position: f32, color: Color) -> Self {
91 GradientStop { position: position.clamp(0.0, 1.0), color }
92 }
93}
94
95#[derive(Clone, Debug)]
97pub struct LinearGradient {
98 start: Point,
99 end: Point,
100 ramp: ColorRamp,
101 spread: SpreadMode,
102 inv_transform: Transform,
103}
104
105impl LinearGradient {
106 #[allow(clippy::new_ret_no_self)] pub fn new(
110 start: Point,
111 end: Point,
112 stops: Vec<GradientStop>,
113 spread: SpreadMode,
114 transform: Transform,
115 ) -> Option<Shader> {
116 if stops.len() < 2 {
117 return None;
118 }
119 let inv_transform = transform.invert()?;
120 let mut stops = stops;
121 stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(std::cmp::Ordering::Equal));
122 let ramp = ColorRamp::new(&stops);
123 Some(Shader::Linear(LinearGradient { start, end, ramp, spread, inv_transform }))
124 }
125
126 #[inline]
128 fn param(&self, p: Point) -> f32 {
129 let dir = self.end - self.start;
130 let len_sq = dir.dot(dir);
131 if len_sq <= 1e-12 {
132 0.0
133 } else {
134 (p - self.start).dot(dir) / len_sq
135 }
136 }
137
138 pub(super) fn color_at(&self, p: Point) -> Color {
139 let p = self.inv_transform.map_point(p);
140 self.ramp.sample(apply_spread(self.param(p), self.spread))
141 }
142
143 pub(super) fn shade_span(&self, x: usize, y: usize, len: usize, out: &mut Vec<Color>) {
148 let mut p = self.inv_transform.map_point(Point::new(x as f32 + 0.5, y as f32 + 0.5));
149 let step = column_step(&self.inv_transform);
150 for _ in 0..len {
151 out.push(self.ramp.sample(apply_spread(self.param(p), self.spread)));
152 p = p + step;
153 }
154 }
155}
156
157#[derive(Clone, Debug)]
159pub struct RadialGradient {
160 center: Point,
161 radius: f32,
162 ramp: ColorRamp,
163 spread: SpreadMode,
164 inv_transform: Transform,
165}
166
167impl RadialGradient {
168 #[allow(clippy::new_ret_no_self)] pub fn new(
170 center: Point,
171 radius: f32,
172 stops: Vec<GradientStop>,
173 spread: SpreadMode,
174 transform: Transform,
175 ) -> Option<Shader> {
176 if stops.len() < 2 || !radius.is_finite() || radius <= 0.0 {
177 return None;
178 }
179 let inv_transform = transform.invert()?;
180 let mut stops = stops;
181 stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(std::cmp::Ordering::Equal));
182 let ramp = ColorRamp::new(&stops);
183 Some(Shader::Radial(RadialGradient { center, radius, ramp, spread, inv_transform }))
184 }
185
186 pub(super) fn color_at(&self, p: Point) -> Color {
187 let p = self.inv_transform.map_point(p);
188 let t = (p - self.center).length() / self.radius;
189 self.ramp.sample(apply_spread(t, self.spread))
190 }
191
192 pub(super) fn shade_span(&self, x: usize, y: usize, len: usize, out: &mut Vec<Color>) {
194 let mut p = self.inv_transform.map_point(Point::new(x as f32 + 0.5, y as f32 + 0.5));
195 let step = column_step(&self.inv_transform);
196 for _ in 0..len {
197 let t = (p - self.center).length() / self.radius;
198 out.push(self.ramp.sample(apply_spread(t, self.spread)));
199 p = p + step;
200 }
201 }
202}
203
204#[derive(Clone, Debug)]
212pub struct ConicGradient {
213 center: Point,
214 start_angle: f32,
216 ramp: ColorRamp,
217 spread: SpreadMode,
218 inv_transform: Transform,
219}
220
221impl ConicGradient {
222 #[allow(clippy::new_ret_no_self)] pub fn new(
226 center: Point,
227 start_angle: f32,
228 stops: Vec<GradientStop>,
229 spread: SpreadMode,
230 transform: Transform,
231 ) -> Option<Shader> {
232 if stops.len() < 2 {
233 return None;
234 }
235 let inv_transform = transform.invert()?;
236 let mut stops = stops;
237 stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(std::cmp::Ordering::Equal));
238 let ramp = ColorRamp::new(&stops);
239 Some(Shader::Conic(ConicGradient {
240 center,
241 start_angle: start_angle.to_radians(),
242 ramp,
243 spread,
244 inv_transform,
245 }))
246 }
247
248 #[inline]
250 fn param(&self, p: Point) -> f32 {
251 let d = p - self.center;
252 let mut a = (d.y.atan2(d.x) - self.start_angle) / (2.0 * std::f32::consts::PI);
254 a -= a.floor();
255 a
256 }
257
258 pub(super) fn color_at(&self, p: Point) -> Color {
259 let p = self.inv_transform.map_point(p);
260 self.ramp.sample(apply_spread(self.param(p), self.spread))
261 }
262
263 pub(super) fn shade_span(&self, x: usize, y: usize, len: usize, out: &mut Vec<Color>) {
265 let mut p = self.inv_transform.map_point(Point::new(x as f32 + 0.5, y as f32 + 0.5));
266 let step = column_step(&self.inv_transform);
267 for _ in 0..len {
268 out.push(self.ramp.sample(apply_spread(self.param(p), self.spread)));
269 p = p + step;
270 }
271 }
272}
273
274#[inline]
279fn column_step(inv_transform: &Transform) -> Point {
280 Point::new(inv_transform.sx, inv_transform.ky)
281}
282
283#[inline]
284pub(super) fn apply_spread(t: f32, spread: SpreadMode) -> f32 {
285 match spread {
286 SpreadMode::Pad => t.clamp(0.0, 1.0),
287 SpreadMode::Repeat => t - t.floor(),
288 SpreadMode::Reflect => {
289 let u = (t.abs()) % 2.0;
290 if u > 1.0 {
291 2.0 - u
292 } else {
293 u
294 }
295 }
296 }
297}
298
299fn sample_stops(stops: &[GradientStop], t: f32) -> Color {
306 if t <= stops[0].position {
307 return stops[0].color;
308 }
309 let last = &stops[stops.len() - 1];
310 if t >= last.position {
311 return last.color;
312 }
313 for w in stops.windows(2) {
314 let (a, b) = (&w[0], &w[1]);
315 if t >= a.position && t <= b.position {
316 let span = b.position - a.position;
317 let local = if span <= 1e-6 { 0.0 } else { (t - a.position) / span };
318 return Color::from_rgba(
319 a.color.red() + (b.color.red() - a.color.red()) * local,
320 a.color.green() + (b.color.green() - a.color.green()) * local,
321 a.color.blue() + (b.color.blue() - a.color.blue()) * local,
322 a.color.alpha() + (b.color.alpha() - a.color.alpha()) * local,
323 );
324 }
325 }
326 last.color
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn conic_gradient_sweeps_by_angle() {
335 let shader = ConicGradient::new(
336 Point::new(0.0, 0.0),
337 0.0,
338 vec![
339 GradientStop::new(0.0, Color::from_rgba8(0, 0, 0, 255)),
340 GradientStop::new(1.0, Color::from_rgba8(255, 255, 255, 255)),
341 ],
342 SpreadMode::Repeat,
343 Transform::identity(),
344 )
345 .unwrap();
346 assert!(shader.color_at(10.0, 0.0).red() < 0.05);
348 assert!((shader.color_at(-10.0, 0.0).red() - 0.5).abs() < 0.1);
350 }
351
352 #[test]
356 fn shade_span_matches_color_at() {
357 let stops = || {
358 vec![
359 GradientStop::new(0.0, Color::from_rgba8(255, 0, 0, 255)),
360 GradientStop::new(0.5, Color::from_rgba8(0, 255, 0, 255)),
361 GradientStop::new(1.0, Color::from_rgba8(0, 0, 255, 255)),
362 ]
363 };
364 let transform = Transform::from_translate(3.0, -2.0)
365 .pre_concat(Transform::from_rotate(25.0))
366 .pre_concat(Transform::from_scale(1.7, 0.8));
367 let shaders = [
368 LinearGradient::new(
369 Point::new(1.0, 2.0),
370 Point::new(30.0, 12.0),
371 stops(),
372 SpreadMode::Repeat,
373 transform,
374 )
375 .unwrap(),
376 RadialGradient::new(Point::new(10.0, 8.0), 14.0, stops(), SpreadMode::Reflect, transform)
377 .unwrap(),
378 ConicGradient::new(Point::new(9.0, 7.0), 30.0, stops(), SpreadMode::Repeat, transform)
379 .unwrap(),
380 ];
381
382 let (x0, y, len) = (4usize, 11usize, 24usize);
383 for shader in &shaders {
384 let mut span = Vec::new();
385 shader.shade_span(x0, y, len, &mut span);
386 assert_eq!(span.len(), len);
387 for (i, c) in span.iter().enumerate() {
388 let want = shader.color_at((x0 + i) as f32 + 0.5, y as f32 + 0.5);
389 assert!((c.red() - want.red()).abs() < 1e-4, "red @{i}: {c:?} vs {want:?}");
390 assert!((c.green() - want.green()).abs() < 1e-4, "green @{i}");
391 assert!((c.blue() - want.blue()).abs() < 1e-4, "blue @{i}");
392 assert!((c.alpha() - want.alpha()).abs() < 1e-4, "alpha @{i}");
393 }
394 }
395 }
396
397 #[test]
398 fn linear_gradient_midpoint() {
399 let shader = LinearGradient::new(
400 Point::new(0.0, 0.0),
401 Point::new(10.0, 0.0),
402 vec![
403 GradientStop::new(0.0, Color::from_rgba8(0, 0, 0, 255)),
404 GradientStop::new(1.0, Color::from_rgba8(255, 255, 255, 255)),
405 ],
406 SpreadMode::Pad,
407 Transform::identity(),
408 )
409 .unwrap();
410 let mid = shader.color_at(5.0, 0.0);
411 assert!((mid.red() - 0.5).abs() < 0.05, "{}", mid.red());
412 }
413}