use kurbo::Point;
use crate::{Color, Lerp};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GradientKind {
Linear {
from: Point,
to: Point,
},
Radial {
center: Point,
radius: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Gradient {
pub kind: GradientKind,
pub stops: Vec<(f64, Color)>,
}
impl Gradient {
pub fn linear(from: impl Into<Point>, to: impl Into<Point>) -> Self {
Self {
kind: GradientKind::Linear {
from: from.into(),
to: to.into(),
},
stops: Vec::new(),
}
}
pub fn radial(center: impl Into<Point>, radius: f64) -> Self {
Self {
kind: GradientKind::Radial {
center: center.into(),
radius,
},
stops: Vec::new(),
}
}
pub fn stop(mut self, offset: f64, color: Color) -> Self {
self.stops.push((offset.clamp(0.0, 1.0), color));
self.stops.sort_by(|a, b| a.0.total_cmp(&b.0));
self
}
pub fn stops(mut self, colors: impl IntoIterator<Item = Color>) -> Self {
let colors: Vec<Color> = colors.into_iter().collect();
let n = colors.len().max(2) - 1;
self.stops = colors
.into_iter()
.enumerate()
.map(|(i, c)| (i as f64 / n as f64, c))
.collect();
self
}
pub fn with_opacity(&self, opacity: f64) -> Self {
Self {
kind: self.kind,
stops: self
.stops
.iter()
.map(|(o, c)| (*o, c.with_alpha((f64::from(c.a) * opacity) as f32)))
.collect(),
}
}
pub fn is_visible(&self) -> bool {
self.stops.iter().any(|(_, c)| c.a > 0.0)
}
}
impl Lerp for Gradient {
fn lerp(&self, to: &Self, t: f64) -> Self {
let kind = match (self.kind, to.kind) {
(GradientKind::Linear { from: a, to: b }, GradientKind::Linear { from: c, to: d }) => {
GradientKind::Linear {
from: Lerp::lerp(&a, &c, t),
to: Lerp::lerp(&b, &d, t),
}
}
(
GradientKind::Radial {
center: a,
radius: r1,
},
GradientKind::Radial {
center: b,
radius: r2,
},
) => GradientKind::Radial {
center: Lerp::lerp(&a, &b, t),
radius: r1.lerp(&r2, t),
},
_ => return if t < 0.5 { self.clone() } else { to.clone() },
};
if self.stops.len() != to.stops.len() {
return if t < 0.5 { self.clone() } else { to.clone() };
}
Self {
kind,
stops: self
.stops
.iter()
.zip(&to.stops)
.map(|((o1, c1), (o2, c2))| (o1.lerp(o2, t), c1.lerp(c2, t)))
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BlendMode {
#[default]
Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gradient_lerp() {
let a = Gradient::linear((0.0, 0.0), (10.0, 0.0)).stops([Color::BLACK, Color::WHITE]);
let b = Gradient::linear((0.0, 0.0), (20.0, 0.0)).stops([Color::WHITE, Color::BLACK]);
let m = a.lerp(&b, 0.5);
assert_eq!(
m.kind,
GradientKind::Linear {
from: Point::ZERO,
to: Point::new(15.0, 0.0)
}
);
assert_eq!(m.stops.len(), 2);
let c = Gradient::radial((0.0, 0.0), 5.0).stop(0.0, Color::BLACK);
assert_eq!(a.lerp(&c, 0.4), a);
assert_eq!(a.lerp(&c, 0.6), c);
}
#[test]
fn evenly_spaced_stops() {
let g = Gradient::linear((0.0, 0.0), (1.0, 0.0)).stops([
Color::BLACK,
Color::WHITE,
Color::BLACK,
]);
assert_eq!(
g.stops.iter().map(|s| s.0).collect::<Vec<_>>(),
vec![0.0, 0.5, 1.0]
);
}
}