use embedded_graphics_core::pixelcolor::{Rgb565, Rgb888, RgbColor};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GradientStop<C> {
pub location: f32,
pub color: C,
}
impl<C> GradientStop<C> {
pub const fn new(location: f32, color: C) -> Self {
Self { location, color }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColorGradient<'a, C> {
stops: &'a [GradientStop<C>],
}
impl<'a, C> ColorGradient<'a, C> {
pub const fn new(stops: &'a [GradientStop<C>]) -> Self {
Self { stops }
}
#[inline]
pub const fn len(&self) -> usize {
self.stops.len()
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.stops.is_empty()
}
}
impl<'a> ColorGradient<'a, Rgb565> {
pub fn sample(&self, t: f32) -> Rgb565 {
if self.stops.is_empty() {
return Rgb565::BLACK;
}
if self.stops.len() == 1 {
return self.stops[0].color;
}
let t = t.clamp(0.0, 1.0);
if t <= self.stops[0].location {
return self.stops[0].color;
}
if t >= self.stops[self.stops.len() - 1].location {
return self.stops[self.stops.len() - 1].color;
}
for i in 0..self.stops.len() - 1 {
let left = &self.stops[i];
let right = &self.stops[i + 1];
if t >= left.location && t <= right.location {
let span = right.location - left.location;
let alpha = if span > 1e-6 {
(t - left.location) / span
} else {
0.0
};
let r = lerp_u8(left.color.r(), right.color.r(), alpha, 31);
let g = lerp_u8(left.color.g(), right.color.g(), alpha, 63);
let b = lerp_u8(left.color.b(), right.color.b(), alpha, 31);
return Rgb565::new(r, g, b);
}
}
self.stops[self.stops.len() - 1].color
}
pub fn sample_hsv(&self, t: f32) -> Rgb565 {
if self.stops.is_empty() {
return Rgb565::new(0, 0, 0);
}
if self.stops.len() == 1 {
return self.stops[0].color;
}
let t = t.clamp(0.0, 1.0);
if t <= self.stops[0].location {
return self.stops[0].color;
}
if t >= self.stops[self.stops.len() - 1].location {
return self.stops[self.stops.len() - 1].color;
}
for i in 0..self.stops.len() - 1 {
let left = &self.stops[i];
let right = &self.stops[i + 1];
if t >= left.location && t <= right.location {
let span = right.location - left.location;
let alpha = if span > 1e-6 {
(t - left.location) / span
} else {
0.0
};
let hsv_left = crate::color::Hsv::from_rgb565(left.color);
let hsv_right = crate::color::Hsv::from_rgb565(right.color);
return crate::color::Hsv::lerp(hsv_left, hsv_right, alpha).to_rgb565();
}
}
self.stops[self.stops.len() - 1].color
}
}
impl<'a> ColorGradient<'a, Rgb888> {
pub fn sample(&self, t: f32) -> Rgb888 {
if self.stops.is_empty() {
return Rgb888::BLACK;
}
if self.stops.len() == 1 {
return self.stops[0].color;
}
let t = t.clamp(0.0, 1.0);
if t <= self.stops[0].location {
return self.stops[0].color;
}
if t >= self.stops[self.stops.len() - 1].location {
return self.stops[self.stops.len() - 1].color;
}
for i in 0..self.stops.len() - 1 {
let left = &self.stops[i];
let right = &self.stops[i + 1];
if t >= left.location && t <= right.location {
let span = right.location - left.location;
let alpha = if span > 1e-6 {
(t - left.location) / span
} else {
0.0
};
let r = lerp_u8(left.color.r(), right.color.r(), alpha, 255);
let g = lerp_u8(left.color.g(), right.color.g(), alpha, 255);
let b = lerp_u8(left.color.b(), right.color.b(), alpha, 255);
return Rgb888::new(r, g, b);
}
}
self.stops[self.stops.len() - 1].color
}
}
#[inline]
fn lerp_u8(start: u8, end: u8, t: f32, max: u8) -> u8 {
let val = start as f32 + t * (end as f32 - start as f32);
val.clamp(0.0, max as f32) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gradient_rgb565_sampling() {
let stops = [
GradientStop::new(0.0, Rgb565::BLACK),
GradientStop::new(1.0, Rgb565::WHITE),
];
let grad = ColorGradient::new(&stops);
assert_eq!(grad.sample(0.0), Rgb565::BLACK);
assert_eq!(grad.sample(1.0), Rgb565::WHITE);
let mid = grad.sample(0.5);
assert_eq!(mid.r(), 15);
assert_eq!(mid.g(), 31);
assert_eq!(mid.b(), 15);
}
#[test]
fn test_multi_stop_gradient() {
let stops = [
GradientStop::new(0.0, Rgb565::new(31, 0, 0)), GradientStop::new(0.5, Rgb565::new(0, 63, 0)), GradientStop::new(1.0, Rgb565::new(0, 0, 31)), ];
let grad = ColorGradient::new(&stops);
assert_eq!(grad.sample(0.0), Rgb565::new(31, 0, 0));
assert_eq!(grad.sample(0.5), Rgb565::new(0, 63, 0));
assert_eq!(grad.sample(1.0), Rgb565::new(0, 0, 31));
let sample_quarter = grad.sample(0.25);
assert!(sample_quarter.r() > 10 && sample_quarter.r() < 31);
assert!(sample_quarter.g() > 10 && sample_quarter.g() < 63);
assert_eq!(sample_quarter.b(), 0);
}
#[test]
fn test_hsv_gradient() {
let stops = [
GradientStop::new(0.0, Rgb565::new(31, 0, 0)), GradientStop::new(1.0, Rgb565::new(31, 63, 0)), ];
let grad = ColorGradient::new(&stops);
let mid = grad.sample_hsv(0.5);
assert!(mid.r() > 25);
assert!(mid.g() > 20 && mid.g() < 45);
assert_eq!(mid.b(), 0);
}
#[test]
fn test_empty_single_rgb888_and_clamped() {
let empty = ColorGradient::<Rgb565>::new(&[]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert_eq!(empty.sample(0.5), Rgb565::BLACK);
assert_eq!(empty.sample_hsv(0.5), Rgb565::new(0, 0, 0));
let single = [GradientStop::new(0.3, Rgb565::new(1, 2, 3))];
let one = ColorGradient::new(&single);
assert_eq!(one.len(), 1);
assert!(!one.is_empty());
assert_eq!(one.sample(0.0), Rgb565::new(1, 2, 3));
assert_eq!(one.sample_hsv(1.0), Rgb565::new(1, 2, 3));
let stops = [
GradientStop::new(0.2, Rgb565::new(0, 0, 0)),
GradientStop::new(0.8, Rgb565::new(31, 63, 31)),
];
let grad = ColorGradient::new(&stops);
assert_eq!(grad.sample(-1.0), Rgb565::new(0, 0, 0));
assert_eq!(grad.sample(2.0), Rgb565::new(31, 63, 31));
let rgb888_stops = [
GradientStop::new(0.0, Rgb888::BLACK),
GradientStop::new(1.0, Rgb888::WHITE),
];
let rgb888 = ColorGradient::new(&rgb888_stops);
assert_eq!(rgb888.sample(0.5), Rgb888::new(127, 127, 127));
}
}