1use crate::color::Color;
21use crate::geom::Vec2;
22use crate::value::Value;
23
24#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Stop {
30 pub color: Color,
31 pub at: Option<f32>,
32}
33
34impl From<Color> for Stop {
35 fn from(color: Color) -> Self {
36 Stop { color, at: None }
37 }
38}
39
40impl From<(Color, f32)> for Stop {
41 fn from((color, at): (Color, f32)) -> Self {
42 Stop {
43 color,
44 at: Some(at),
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Side {
52 Right,
53 BottomRight,
54 Bottom,
55 BottomLeft,
56 Left,
57 TopLeft,
58 Top,
59 TopRight,
60}
61
62impl Side {
63 pub const ALL: [(Side, &'static str); 8] = [
66 (Side::Right, "right"),
67 (Side::BottomRight, "bottom right"),
68 (Side::Bottom, "bottom"),
69 (Side::BottomLeft, "bottom left"),
70 (Side::Left, "left"),
71 (Side::TopLeft, "top left"),
72 (Side::Top, "top"),
73 (Side::TopRight, "top right"),
74 ];
75
76 pub fn turns(self) -> f32 {
78 self as u8 as f32 / 8.0
79 }
80
81 pub fn parse(s: &str) -> Option<Side> {
84 let mut words: Vec<&str> = s.split_whitespace().collect();
85 words.sort_unstable();
86 Side::ALL
87 .iter()
88 .find(|(_, name)| {
89 let mut want: Vec<&str> = name.split(' ').collect();
90 want.sort_unstable();
91 want == words
92 })
93 .map(|&(side, _)| side)
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
99enum Shape {
100 Linear { dx: f32, dy: f32 },
104 Radial { at: Vec2 },
107}
108
109#[derive(Clone, Debug, PartialEq)]
113pub struct Gradient {
114 shape: Shape,
115 stops: Stops,
117 key: u64,
119}
120
121#[derive(Clone, Debug)]
125enum Stops {
126 Few([(Color, f32); 4], u8),
127 Many(Vec<(Color, f32)>),
128}
129
130impl Stops {
131 fn push(&mut self, stop: (Color, f32)) {
132 match self {
133 Stops::Few(held, n) if (*n as usize) < held.len() => {
134 held[*n as usize] = stop;
135 *n += 1;
136 }
137 Stops::Few(held, _) => {
138 let mut list = Vec::with_capacity(16);
139 list.extend_from_slice(held);
140 list.push(stop);
141 *self = Stops::Many(list);
142 }
143 Stops::Many(list) => list.push(stop),
144 }
145 }
146}
147
148impl std::ops::Deref for Stops {
149 type Target = [(Color, f32)];
150 fn deref(&self) -> &Self::Target {
151 match self {
152 Stops::Few(held, n) => &held[..*n as usize],
153 Stops::Many(list) => list,
154 }
155 }
156}
157
158impl std::ops::DerefMut for Stops {
159 fn deref_mut(&mut self) -> &mut Self::Target {
160 match self {
161 Stops::Few(held, n) => &mut held[..*n as usize],
162 Stops::Many(list) => list,
163 }
164 }
165}
166
167impl PartialEq for Stops {
168 fn eq(&self, other: &Self) -> bool {
169 **self == **other
170 }
171}
172
173pub const STRIP: u32 = 256;
175pub const SQUARE: u32 = 128;
177
178impl Gradient {
179 pub fn to<S: Into<Stop>>(side: Side, stops: impl IntoIterator<Item = S>) -> Self {
183 Self::angle(side.turns(), stops)
184 }
185
186 pub fn angle<S: Into<Stop>>(turns: f32, stops: impl IntoIterator<Item = S>) -> Self {
191 let (dx, dy) = direction(turns);
192 Self::new(Shape::Linear { dx, dy }, stops)
193 }
194
195 pub fn radial<S: Into<Stop>>(stops: impl IntoIterator<Item = S>) -> Self {
197 Self::radial_at(Vec2::new(0.5, 0.5), stops)
198 }
199
200 pub fn radial_at<S: Into<Stop>>(at: Vec2, stops: impl IntoIterator<Item = S>) -> Self {
203 Self::new(Shape::Radial { at }, stops)
204 }
205
206 fn new<S: Into<Stop>>(shape: Shape, stops: impl IntoIterator<Item = S>) -> Self {
207 let mut list = Stops::Few([(Color::TRANSPARENT, 0.0); 4], 0);
210 for s in stops {
211 let s: Stop = s.into();
212 let at = s.at.filter(|a| a.is_finite()).map(|a| a.clamp(0.0, 1.0));
213 list.push((s.color, at.unwrap_or(f32::NAN)));
214 }
215 let mut stops = list;
216 resolve(&mut stops);
217 let mut g = Gradient {
218 shape,
219 stops,
220 key: 0,
221 };
222 g.key = g.hash();
223 g
224 }
225
226 pub fn is_drawable(&self) -> bool {
229 let shape = match self.shape {
230 Shape::Linear { dx, dy } => dx.is_finite() && dy.is_finite(),
231 Shape::Radial { at } => at.x.is_finite() && at.y.is_finite(),
232 };
233 shape && self.stops.len() >= 2
234 }
235
236 pub fn key(&self) -> u64 {
239 self.key
240 }
241
242 pub fn stops(&self) -> &[(Color, f32)] {
244 &self.stops
245 }
246
247 fn hash(&self) -> u64 {
248 const PRIME: u64 = 0x0000_0100_0000_01b3;
249 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
250 let mut mix = |v: u32| {
251 h ^= u64::from(v);
252 h = h.wrapping_mul(PRIME);
253 };
254 match self.shape {
255 Shape::Linear { dx, dy } => {
258 mix(1);
259 mix(dx.to_bits());
260 mix(dy.to_bits());
261 }
262 Shape::Radial { at } => {
263 mix(2);
264 mix(at.x.to_bits());
265 mix(at.y.to_bits());
266 }
267 }
268 for (c, at) in self.stops.iter() {
269 for lane in c.lanes() {
270 mix(lane.to_bits());
271 }
272 mix(at.to_bits());
273 }
274 h
275 }
276
277 pub fn raster_size(&self) -> (u32, u32) {
280 match self.shape {
281 Shape::Linear { dy: 0.0, .. } => (STRIP, 1),
282 Shape::Linear { dx: 0.0, .. } => (1, STRIP),
283 Shape::Linear { .. } => (SQUARE, SQUARE),
284 Shape::Radial { .. } => (SQUARE, SQUARE),
285 }
286 }
287
288 pub fn slot_size(&self) -> (u32, u32) {
291 let (w, h) = self.raster_size();
292 (w + 2, h + 2)
293 }
294
295 pub fn rasterize(&self) -> Vec<u8> {
303 let (w, h) = self.raster_size();
304 let mut out = Vec::with_capacity(((w + 2) * (h + 2) * 4) as usize);
305 if w.min(h) == 1 {
306 for row in 0..h + 2 {
308 let y = (row as f32 - 0.5) / h as f32;
309 for col in 0..w + 2 {
310 let x = (col as f32 - 0.5) / w as f32;
311 out.extend_from_slice(&bytes(self.color_at(self.along(x, y))));
312 }
313 }
314 return out;
315 }
316 const RAMP: usize = 1024;
320 let ramp: Vec<[u8; 4]> = (0..=RAMP)
321 .map(|i| bytes(self.color_at(i as f32 / RAMP as f32)))
322 .collect();
323 for row in 0..h + 2 {
324 let y = (row as f32 - 0.5) / h as f32;
325 for col in 0..w + 2 {
326 let x = (col as f32 - 0.5) / w as f32;
327 let t = self.along(x, y).clamp(0.0, 1.0);
328 out.extend_from_slice(&ramp[(t * RAMP as f32 + 0.5) as usize]);
329 }
330 }
331 out
332 }
333
334 pub fn color_in(&self, x: f32, y: f32) -> Color {
338 self.color_at(self.along(x, y))
339 }
340
341 #[inline]
343 fn along(&self, x: f32, y: f32) -> f32 {
344 match self.shape {
345 Shape::Linear { dx, dy } => {
346 let len = dx.abs() + dy.abs();
349 ((x - 0.5) * dx + (y - 0.5) * dy) / len + 0.5
350 }
351 Shape::Radial { at } => {
352 let far = (at.x.max(1.0 - at.x)).hypot(at.y.max(1.0 - at.y));
353 let (rx, ry) = (x - at.x, y - at.y);
354 (rx * rx + ry * ry).sqrt() / if far > 0.0 { far } else { 1.0 }
355 }
356 }
357 }
358
359 pub fn color_at(&self, t: f32) -> Color {
364 let stops = &self.stops;
365 let Some(&(first, first_at)) = stops.first() else {
366 return Color::TRANSPARENT;
367 };
368 if t.is_nan() || t <= first_at {
369 return first;
370 }
371 for pair in stops.windows(2) {
372 let ((a, a_at), (b, b_at)) = (pair[0], pair[1]);
373 if t <= b_at {
374 let span = b_at - a_at;
375 let u = if span > 0.0 { (t - a_at) / span } else { 1.0 };
376 let alpha = a.a + (b.a - a.a) * u;
377 if alpha <= 0.0 {
378 return Color {
381 a: 0.0,
382 ..a.lerp(b, u)
383 };
384 }
385 let lane = |a_c: f32, b_c: f32| (a_c * a.a + (b_c * b.a - a_c * a.a) * u) / alpha;
386 return Color {
387 r: lane(a.r, b.r),
388 g: lane(a.g, b.g),
389 b: lane(a.b, b.b),
390 a: alpha,
391 };
392 }
393 }
394 stops[stops.len() - 1].0
395 }
396}
397
398fn bytes(c: Color) -> [u8; 4] {
399 [c.r, c.g, c.b, c.a].map(|lane| (lane.clamp(0.0, 1.0) * 255.0).round() as u8)
400}
401
402fn direction(turns: f32) -> (f32, f32) {
407 const D: f32 = std::f32::consts::FRAC_1_SQRT_2;
408 const EIGHTHS: [(f32, f32); 8] = [
409 (1.0, 0.0),
410 (D, D),
411 (0.0, 1.0),
412 (-D, D),
413 (-1.0, 0.0),
414 (-D, -D),
415 (0.0, -1.0),
416 (D, -D),
417 ];
418 let eighths = turns.rem_euclid(1.0) * 8.0;
419 if eighths == eighths.round() {
420 return EIGHTHS[eighths as usize & 7];
421 }
422 let a = eighths * (std::f32::consts::TAU / 8.0);
423 (a.cos(), a.sin())
424}
425
426fn resolve(stops: &mut [(Color, f32)]) {
430 let n = stops.len();
431 if n == 0 {
432 return;
433 }
434 if stops[0].1.is_nan() {
435 stops[0].1 = 0.0;
436 }
437 if stops[n - 1].1.is_nan() {
438 stops[n - 1].1 = 1.0;
439 }
440 let mut floor = 0.0f32;
441 for (_, at) in stops.iter_mut().filter(|s| !s.1.is_nan()) {
442 floor = floor.max(*at);
443 *at = floor;
444 }
445 let mut i = 0;
446 while i + 1 < n {
447 let j = (i + 1..n).find(|&j| !stops[j].1.is_nan()).unwrap_or(n - 1);
450 let (here, there) = (stops[i].1, stops[j].1);
451 for (n, stop) in stops[i + 1..j].iter_mut().enumerate() {
452 stop.1 = here + (there - here) * ((n + 1) as f32 / (j - i) as f32);
453 }
454 i = j;
455 }
456}
457
458pub fn parse_with(
469 v: &Value,
470 mut refs: Option<&mut crate::tokens::NameRefs<'_>>,
471) -> Result<Gradient, String> {
472 let Value::Map(fields) = v else {
473 return Err("gradient must be an object with stops".into());
474 };
475 let num = |v: &Value, what: &str| {
476 v.as_float()
477 .map(|n| n as f32)
478 .filter(|n| n.is_finite())
479 .ok_or_else(|| format!("gradient: {what} must be a number"))
480 };
481 let (mut to, mut angle, mut radial, mut at, mut stops) = (None, None, false, None, None);
482 for (k, v) in fields {
483 match k.as_str() {
484 "to" => {
485 let side = v.as_str().and_then(Side::parse).ok_or_else(|| {
486 "gradient: `to` is a side or a corner (\"bottom\", \"top right\", …)"
487 .to_string()
488 })?;
489 to = Some(side);
490 }
491 "angle" => angle = Some(num(v, "`angle`")?),
492 "radial" => {
493 radial = v
494 .as_bool()
495 .ok_or("gradient: `radial` must be true or false")?
496 }
497 "at" => match v.as_list() {
498 Some([x, y]) => at = Some(Vec2::new(num(x, "`at`")?, num(y, "`at`")?)),
499 _ => return Err("gradient: `at` is an [x, y] pair".into()),
500 },
501 "stops" => stops = v.as_list(),
502 other => return Err(format!("gradient: unknown field `{other}`")),
503 }
504 }
505 let Some(list) = stops else {
506 return Err("gradient needs `stops`, a list of colours".into());
507 };
508 let mut color = |v: &Value, i: usize| -> Result<Option<Color>, String> {
509 if let Some(hit) = refs.as_deref_mut().and_then(|r| r.color_ref(v)) {
510 return Ok(hit);
511 }
512 crate::slots::color_value(v)
513 .map(Some)
514 .map_err(|e| format!("gradient stop {i}: {e}"))
515 };
516 let mut out = Vec::with_capacity(list.len());
517 for (i, stop) in list.iter().enumerate() {
518 let (c, at) = match stop {
519 Value::List(pair) => match pair.as_slice() {
520 [c, at] => (
521 color(c, i)?,
522 Some(num(at, &format!("stop {i}'s position"))?),
523 ),
524 _ => {
525 return Err(format!(
526 "gradient stop {i}: a colour or a [colour, at] pair"
527 ));
528 }
529 },
530 c => (color(c, i)?, None),
531 };
532 if let Some(color) = c {
533 out.push(Stop { color, at });
534 }
535 }
536 if list.len() < 2 {
537 return Err("gradient needs two stops or more".into());
538 }
539 let linear = |turns: f32| {
540 let (dx, dy) = direction(turns);
541 Shape::Linear { dx, dy }
542 };
543 let shape = match (radial, to, angle) {
544 (true, None, None) => Shape::Radial {
545 at: at.unwrap_or(Vec2::new(0.5, 0.5)),
546 },
547 (false, Some(side), None) => linear(side.turns()),
548 (false, None, Some(turns)) => linear(turns),
549 (false, None, None) => linear(Side::Bottom.turns()),
550 _ => return Err("gradient: one of `to`, `angle` and `radial`".into()),
551 };
552 if at.is_some() && !radial {
553 return Err("gradient: `at` is a radial gradient's centre".into());
554 }
555 Ok(Gradient::new(shape, out))
556}
557
558pub fn parse(v: &Value) -> Result<Gradient, String> {
560 parse_with(v, None)
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 const RED: Color = Color {
568 r: 1.0,
569 g: 0.0,
570 b: 0.0,
571 a: 1.0,
572 };
573 const BLUE: Color = Color {
574 r: 0.0,
575 g: 0.0,
576 b: 1.0,
577 a: 1.0,
578 };
579
580 fn texel(g: &Gradient, x: u32, y: u32) -> [u8; 4] {
581 let (w, _) = g.slot_size();
583 let px = g.rasterize();
584 let i = (((y + 1) * w + x + 1) * 4) as usize;
585 [px[i], px[i + 1], px[i + 2], px[i + 3]]
586 }
587
588 #[test]
589 fn a_side_is_a_strip_and_runs_the_way_it_says() {
590 let right = Gradient::to(Side::Right, [RED, BLUE]);
591 assert_eq!(right.raster_size(), (STRIP, 1));
592 assert_eq!(texel(&right, 0, 0), [255, 0, 0, 255]);
593 assert_eq!(texel(&right, 255, 0), [0, 0, 255, 255]);
594 assert_eq!(texel(&right, 128, 0), [127, 0, 128, 255]);
595 let left = Gradient::to(Side::Left, [RED, BLUE]);
596 assert_eq!(left.raster_size(), (STRIP, 1));
597 assert_eq!(texel(&left, 0, 0), [0, 0, 255, 255]);
598 let up = Gradient::to(Side::Top, [RED, BLUE]);
599 assert_eq!(up.raster_size(), (1, STRIP));
600 assert_eq!(texel(&up, 0, 255), [255, 0, 0, 255]);
601 assert_eq!(
603 Gradient::angle(1.25, [RED, BLUE]).key(),
604 Gradient::to(Side::Bottom, [RED, BLUE]).key()
605 );
606 }
607
608 #[test]
611 fn a_corner_runs_corner_to_corner_in_the_unit_square() {
612 let g = Gradient::to(Side::BottomRight, [RED, BLUE]);
613 assert_eq!(g.raster_size(), (SQUARE, SQUARE));
614 let n = SQUARE - 1;
615 let [r, _, b, _] = texel(&g, 0, 0);
616 assert!(r > 250 && b < 5, "{r} {b}");
617 let [r, _, b, _] = texel(&g, n, n);
618 assert!(r < 5 && b > 250, "{r} {b}");
619 for (x, y) in [(n, 0), (0, n)] {
620 let [r, _, b, _] = texel(&g, x, y);
621 assert!(r.abs_diff(b) <= 1, "{r} {b}");
622 }
623 }
624
625 #[test]
626 fn a_radial_reaches_its_farthest_corner() {
627 let g = Gradient::radial([RED, BLUE]);
628 let mid = SQUARE / 2;
629 let [r, _, b, _] = texel(&g, mid, mid);
630 assert!(r > 250 && b < 5);
631 let [r, _, b, _] = texel(&g, 0, 0);
632 assert!(r < 5 && b > 250);
633 let top = Gradient::radial_at(Vec2::new(0.5, 0.0), [RED, BLUE]);
635 let [r, _, b, _] = texel(&top, 0, SQUARE - 1);
636 assert!(r < 5 && b > 250);
637 assert_ne!(top.key(), g.key());
638 }
639
640 #[test]
641 fn stops_without_a_position_are_spaced_between_those_with() {
642 let g = Gradient::to(
643 Side::Right,
644 [
645 Stop::from(RED),
646 Stop::from(BLUE),
647 Stop::from((RED, 0.5)),
648 Stop::from(BLUE),
649 Stop::from(RED),
650 ],
651 );
652 let at: Vec<f32> = g.stops().iter().map(|s| s.1).collect();
653 assert_eq!(at, [0.0, 0.25, 0.5, 0.75, 1.0]);
654 let g = Gradient::to(Side::Right, [(RED, 0.6), (BLUE, 0.4)]);
656 let at: Vec<f32> = g.stops().iter().map(|s| s.1).collect();
657 assert_eq!(at, [0.6, 0.6]);
658 assert_eq!(g.color_at(0.5), RED);
659 assert_eq!(g.color_at(0.7), BLUE);
660 }
661
662 #[test]
665 fn a_fade_to_transparent_keeps_its_colour() {
666 let g = Gradient::to(Side::Right, [RED, Color::TRANSPARENT]);
667 let c = g.color_at(0.5);
668 assert_eq!((c.r, c.g, c.b, c.a), (1.0, 0.0, 0.0, 0.5));
669 }
670
671 #[test]
672 fn one_stop_or_a_number_that_is_not_one_draws_nothing() {
673 assert!(!Gradient::to(Side::Right, [RED]).is_drawable());
674 assert!(!Gradient::angle(f32::NAN, [RED, BLUE]).is_drawable());
675 assert!(Gradient::to(Side::Right, [RED, BLUE]).is_drawable());
676 }
677
678 #[test]
679 fn plain_data_reads_as_the_builders_do() {
680 let stops = || Value::list([Value::str("#ff0000"), Value::Int(0x0000ffff)]);
681 let g = parse(&Value::map([
682 ("to", Value::str("right bottom")),
683 ("stops", stops()),
684 ]));
685 assert_eq!(g, Ok(Gradient::to(Side::BottomRight, [RED, BLUE])));
686 let g = parse(&Value::map([("stops", stops())]));
687 assert_eq!(g, Ok(Gradient::to(Side::Bottom, [RED, BLUE])));
688 let g = parse(&Value::map([
689 ("radial", Value::Bool(true)),
690 ("at", Value::floats(&[0.5, 0.0])),
691 (
692 "stops",
693 Value::list([
694 Value::str("#ff0000"),
695 Value::list([Value::str("#0000ff"), Value::float(0.8)]),
696 ]),
697 ),
698 ]));
699 assert_eq!(
700 g,
701 Ok(Gradient::radial_at(
702 Vec2::new(0.5, 0.0),
703 [Stop::from(RED), Stop::from((BLUE, 0.8))]
704 ))
705 );
706 let bad = |v: Value| parse(&v).unwrap_err();
707 assert!(bad(Value::map([("to", Value::str("up")), ("stops", stops())])).contains("side"));
708 assert!(
709 bad(Value::map([("stops", Value::list([Value::str("#fff")]))])).contains("two stops")
710 );
711 assert!(bad(Value::map([("angle", Value::float(0.1))])).contains("needs `stops`"));
712 assert!(
714 bad(Value::map([
715 ("radial", Value::str("yes")),
716 ("stops", stops())
717 ]))
718 .contains("true or false")
719 );
720 assert!(
721 bad(Value::map([
722 ("angle", Value::float(0.1)),
723 ("radial", Value::Bool(true)),
724 ("stops", stops())
725 ]))
726 .contains("one of")
727 );
728 }
729}