#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Stop {
pub offset: f32,
pub color: u32,
}
impl Stop {
pub fn new(offset: f32, color: u32) -> Self {
Stop {
offset: offset.clamp(0.0, 1.0),
color,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum Gradient {
Linear { angle_deg: f32, stops: Vec<Stop> },
Radial {
cx: f32,
cy: f32,
radius: f32,
stops: Vec<Stop>,
},
}
impl Gradient {
pub fn linear(angle_deg: f32, stops: impl IntoIterator<Item = Stop>) -> Self {
let mut s: Vec<Stop> = stops.into_iter().collect();
s.sort_by(|a, b| {
a.offset
.partial_cmp(&b.offset)
.unwrap_or(std::cmp::Ordering::Equal)
});
Gradient::Linear {
angle_deg,
stops: s,
}
}
pub fn radial(cx: f32, cy: f32, radius: f32, stops: impl IntoIterator<Item = Stop>) -> Self {
let mut s: Vec<Stop> = stops.into_iter().collect();
s.sort_by(|a, b| {
a.offset
.partial_cmp(&b.offset)
.unwrap_or(std::cmp::Ordering::Equal)
});
Gradient::Radial {
cx,
cy,
radius,
stops: s,
}
}
pub fn stops(&self) -> &[Stop] {
match self {
Gradient::Linear { stops, .. } => stops,
Gradient::Radial { stops, .. } => stops,
}
}
pub fn sample(&self, t: f32) -> Option<u32> {
let stops = self.stops();
if stops.is_empty() {
return None;
}
let t = t.clamp(0.0, 1.0);
if t <= stops[0].offset {
return Some(stops[0].color);
}
if t >= stops[stops.len() - 1].offset {
return Some(stops[stops.len() - 1].color);
}
let mut idx = 0;
for (i, s) in stops.iter().enumerate() {
if s.offset <= t {
idx = i;
} else {
break;
}
}
let a = &stops[idx];
let b = &stops[idx + 1];
let span = b.offset - a.offset;
let f = if span <= 0.0 {
0.0
} else {
(t - a.offset) / span
};
Some(lerp_color(a.color, b.color, f))
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum Paint {
Solid(u32),
Gradient(Gradient),
}
impl Paint {
pub fn solid(rgba: u32) -> Self {
Paint::Solid(rgba)
}
pub fn gradient(g: Gradient) -> Self {
Paint::Gradient(g)
}
pub fn sample(&self, t: f32) -> u32 {
match self {
Paint::Solid(rgba) => *rgba,
Paint::Gradient(g) => g.sample(t).unwrap_or(0),
}
}
}
fn lerp_color(a: u32, b: u32, t: f32) -> u32 {
let ac = [
((a >> 24) & 0xff) as f32,
((a >> 16) & 0xff) as f32,
((a >> 8) & 0xff) as f32,
(a & 0xff) as f32,
];
let bc = [
((b >> 24) & 0xff) as f32,
((b >> 16) & 0xff) as f32,
((b >> 8) & 0xff) as f32,
(b & 0xff) as f32,
];
let mut out = 0u32;
for i in 0..4 {
let v = (ac[i] + (bc[i] - ac[i]) * t).clamp(0.0, 255.0) as u32;
out |= v << ((3 - i) * 8);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stop_clamps_offset() {
assert_eq!(Stop::new(-0.5, 0).offset, 0.0);
assert_eq!(Stop::new(2.0, 0).offset, 1.0);
assert_eq!(Stop::new(0.25, 0).offset, 0.25);
}
#[test]
fn linear_constructor_sorts_stops() {
let g = Gradient::linear(
45.0,
vec![
Stop::new(1.0, 0xFFFFFFFF),
Stop::new(0.0, 0x000000FF),
Stop::new(0.5, 0x808080FF),
],
);
let s = g.stops();
assert_eq!(s.len(), 3);
assert!(s[0].offset <= s[1].offset);
assert!(s[1].offset <= s[2].offset);
}
#[test]
fn sample_clamps_to_endpoints() {
let g = Gradient::linear(
0.0,
vec![Stop::new(0.0, 0xFF000000), Stop::new(1.0, 0x00FF0000)],
);
assert_eq!(g.sample(-1.0), Some(0xFF000000));
assert_eq!(g.sample(2.0), Some(0x00FF0000));
}
#[test]
fn sample_midpoint_lerps_per_channel() {
let g = Gradient::linear(
0.0,
vec![Stop::new(0.0, 0x000000FF), Stop::new(1.0, 0xFFFFFFFF)],
);
let mid = g.sample(0.5).unwrap();
let r = (mid >> 24) & 0xff;
let g_ = (mid >> 16) & 0xff;
let b = (mid >> 8) & 0xff;
let a = mid & 0xff;
assert!((100..=155).contains(&r), "r={r}");
assert!((100..=155).contains(&g_), "g={g_}");
assert!((100..=155).contains(&b), "b={b}");
assert_eq!(a, 0xff);
}
#[test]
fn sample_picks_correct_segment_with_three_stops() {
let g = Gradient::linear(
0.0,
vec![
Stop::new(0.0, 0xFF0000FF), Stop::new(0.5, 0x00FF00FF), Stop::new(1.0, 0x0000FFFF), ],
);
let v = g.sample(0.25).unwrap();
let r = (v >> 24) & 0xff;
let g_ = (v >> 16) & 0xff;
let b = (v >> 8) & 0xff;
assert!(r > 100 && r < 155, "r={r}");
assert!(g_ > 100 && g_ < 155, "g={g_}");
assert_eq!(b, 0);
let v = g.sample(0.75).unwrap();
let r = (v >> 24) & 0xff;
let g_ = (v >> 16) & 0xff;
let b = (v >> 8) & 0xff;
assert_eq!(r, 0);
assert!(g_ > 100 && g_ < 155, "g={g_}");
assert!(b > 100 && b < 155, "b={b}");
}
#[test]
fn empty_gradient_returns_none() {
let g = Gradient::linear(0.0, Vec::<Stop>::new());
assert!(g.sample(0.5).is_none());
}
#[test]
fn radial_constructor_sorts_stops() {
let g = Gradient::radial(
0.5,
0.5,
0.5,
vec![Stop::new(1.0, 0x000000FF), Stop::new(0.0, 0xFFFFFFFF)],
);
let s = g.stops();
assert_eq!(s.len(), 2);
assert_eq!(s[0].offset, 0.0);
assert_eq!(s[1].offset, 1.0);
if let Gradient::Radial {
cx,
cy,
radius,
stops,
} = &g
{
assert_eq!(*cx, 0.5);
assert_eq!(*cy, 0.5);
assert_eq!(*radius, 0.5);
assert_eq!(stops.len(), 2);
} else {
panic!("expected Radial");
}
}
#[test]
fn paint_solid_sample_is_constant() {
let p = Paint::solid(0x123456FF);
assert_eq!(p.sample(0.0), 0x123456FF);
assert_eq!(p.sample(0.5), 0x123456FF);
assert_eq!(p.sample(1.0), 0x123456FF);
}
#[test]
fn paint_gradient_sample_delegates() {
let p = Paint::gradient(Gradient::linear(
0.0,
vec![Stop::new(0.0, 0x000000FF), Stop::new(1.0, 0xFFFFFFFF)],
));
let v = p.sample(0.5);
let r = (v >> 24) & 0xff;
assert!((100..=155).contains(&r));
}
#[test]
fn paint_empty_gradient_samples_zero() {
let p = Paint::gradient(Gradient::linear(0.0, Vec::<Stop>::new()));
assert_eq!(p.sample(0.5), 0);
}
}