use crate::paint::GradientStop;
pub const RAMP_WIDTH: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ramp {
pub texels: Vec<u8>,
}
impl Ramp {
pub fn bake(stops: &[GradientStop]) -> Self {
let mut texels = vec![0u8; RAMP_WIDTH * 8];
for (i, texel) in texels.chunks_exact_mut(8).enumerate() {
let t = (i as f32 + 0.5) / RAMP_WIDTH as f32;
let color = sample_at(stops, t);
for (channel, out) in color.to_array().iter().zip(texel.chunks_exact_mut(2)) {
out.copy_from_slice(&half::f16::from_f32(*channel).to_le_bytes());
}
}
Self { texels }
}
}
fn sample_at(stops: &[GradientStop], t: f32) -> crate::Color {
let Some(first) = stops.first() else {
return crate::Color::TRANSPARENT;
};
let mut result = first.color;
for pair in stops.windows(2) {
let (lower, upper) = (&pair[0], &pair[1]);
let span = (upper.offset - lower.offset).max(1e-6);
let local = ((t - lower.offset) / span).clamp(0.0, 1.0);
if t >= lower.offset {
result = mix(lower.color, upper.color, local);
}
}
result
}
fn mix(a: crate::Color, b: crate::Color, t: f32) -> crate::Color {
let [ar, ag, ab, aa] = a.to_array();
let [br, bg, bb, ba] = b.to_array();
crate::Color::srgb(
ar + (br - ar) * t,
ag + (bg - ag) * t,
ab + (bb - ab) * t,
aa + (ba - aa) * t,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Color;
fn stops(n: usize) -> Vec<GradientStop> {
(0..n)
.map(|i| {
let t = i as f32 / (n - 1) as f32;
GradientStop::new(Color::srgb(t, 0.0, 1.0 - t, 1.0), t)
})
.collect()
}
fn texel(ramp: &Ramp, i: usize) -> [f32; 4] {
let at = i * 8;
let mut out = [0.0; 4];
for (channel, slot) in out.iter_mut().enumerate() {
let byte = at + channel * 2;
*slot = half::f16::from_le_bytes([ramp.texels[byte], ramp.texels[byte + 1]]).to_f32();
}
out
}
#[test]
fn a_ramp_is_the_full_width_and_fully_written() {
let ramp = Ramp::bake(&stops(6));
assert_eq!(ramp.texels.len(), RAMP_WIDTH * 8);
assert!(
(0..RAMP_WIDTH).all(|i| texel(&ramp, i)[3] == 1.0),
"a texel was left unwritten"
);
}
#[test]
fn the_ends_hold_the_first_and_last_stop() {
let ramp = Ramp::bake(&stops(6));
let first = texel(&ramp, 0);
let last = texel(&ramp, RAMP_WIDTH - 1);
assert!(first[2] > 0.94 && first[0] < 0.06, "first texel {first:?}");
assert!(last[0] > 0.94 && last[2] < 0.06, "last texel {last:?}");
}
#[test]
fn every_stop_appears_where_it_was_placed() {
let stops = vec![
GradientStop::new(Color::linear(1.0, 0.0, 0.0, 1.0), 0.25),
GradientStop::new(Color::linear(0.0, 1.0, 0.0, 1.0), 0.5),
GradientStop::new(Color::linear(0.0, 0.0, 1.0, 1.0), 0.75),
];
let ramp = Ramp::bake(&stops);
let at = |t: f32| {
texel(
&ramp,
((t * RAMP_WIDTH as f32) as usize).min(RAMP_WIDTH - 1),
)
};
assert!(at(0.25)[0] > 0.78, "the red stop is not at a quarter");
assert!(at(0.5)[1] > 0.78, "the green stop is not at the half");
assert!(at(0.75)[2] > 0.78, "the blue stop is not at three quarters");
assert!(
at(0.05)[0] > 0.78,
"the ramp faded in before its first stop"
);
}
#[test]
fn the_ramp_is_monotonic_where_the_stops_are() {
let ramp = Ramp::bake(&[
GradientStop::new(Color::linear(0.0, 0.0, 0.0, 1.0), 0.0),
GradientStop::new(Color::linear(1.0, 1.0, 1.0, 1.0), 1.0),
]);
let mut previous = 0.0;
for i in 0..RAMP_WIDTH {
let value = texel(&ramp, i)[0];
assert!(
value >= previous,
"texel {i} is {value} after {previous}, so the ramp goes backwards"
);
previous = value;
}
assert!(previous > 0.98, "the ramp never reached white");
}
#[test]
fn the_table_holds_what_the_walk_produced() {
let ramp = Ramp::bake(&[
GradientStop::new(Color::srgb(0.5, 0.5, 0.5, 0.5), 0.0),
GradientStop::new(Color::srgb(0.5, 0.5, 0.5, 0.5), 1.0),
]);
let t = texel(&ramp, RAMP_WIDTH / 2);
assert!((t[0] - 0.5).abs() < 1e-3, "color came back {}", t[0]);
assert!((t[3] - 0.5).abs() < 1e-3, "alpha came back {}", t[3]);
}
#[test]
fn a_stop_outside_the_srgb_primaries_survives_being_tabulated() {
let ramp = Ramp::bake(&[
GradientStop::new(Color::srgb(1.2, -0.3, 0.0, 1.0), 0.0),
GradientStop::new(Color::srgb(1.2, -0.3, 0.0, 1.0), 1.0),
]);
let t = texel(&ramp, RAMP_WIDTH / 2);
assert!((t[0] - 1.2).abs() < 1e-2, "above one became {}", t[0]);
assert!((t[1] + 0.3).abs() < 1e-2, "below zero became {}", t[1]);
}
#[test]
fn no_stops_bakes_to_nothing_rather_than_to_a_panic() {
let ramp = Ramp::bake(&[]);
assert_eq!(ramp.texels.len(), RAMP_WIDTH * 8);
assert!(ramp.texels.iter().all(|b| *b == 0));
}
}