use core::marker::PhantomData;
use crate::arith::lerp_q0_16;
use crate::color::Color;
use crate::encoding::{Encoded, Linear};
use crate::fixed::Q0_16;
use crate::space::{ColorSpace, Perceptual};
const fn valid_lut_len(n: usize) -> bool {
n == 17 || n == 33 || n == 65 || n == 257
}
pub struct InterpLut<const N: usize> {
knots: [Q0_16; N],
max_err_lsb: u16,
}
impl<const N: usize> Copy for InterpLut<N> {}
impl<const N: usize> Clone for InterpLut<N> {
fn clone(&self) -> Self {
*self
}
}
impl<const N: usize> InterpLut<N> {
#[must_use]
pub const fn from_knots(knots: [Q0_16; N], max_err_lsb: u16) -> Self {
const { assert!(valid_lut_len(N), "InterpLut N must be 17, 33, 65, or 257") };
Self { knots, max_err_lsb }
}
#[must_use]
pub const fn max_err_lsb(&self) -> u16 {
self.max_err_lsb
}
#[must_use]
#[allow(clippy::indexing_slicing)] pub const fn lookup(&self, x: Q0_16) -> Q0_16 {
let last = N.saturating_sub(1);
let segs = last as u32;
let pos = (x.to_raw() as u32).saturating_mul(segs);
let hi = last.saturating_sub(1) as u32;
let raw = crate::arith::div_65535(pos);
let idx_u = if raw < hi { raw } else { hi };
let rem = crate::arith::rem_65535(pos, idx_u);
let idx = idx_u as usize;
lerp_q0_16(
self.knots[idx],
self.knots[idx.saturating_add(1)],
Q0_16::from_raw(rem),
)
}
#[must_use]
pub const fn encode<S: ColorSpace>(&self, color: Color<S, Linear>) -> Color<S, Encoded> {
let [c0, c1, c2] = color.ch;
Color::new([self.lookup(c0), self.lookup(c1), self.lookup(c2)])
}
#[must_use]
pub const fn decode<S: ColorSpace>(&self, color: Color<S, Encoded>) -> Color<S, Linear> {
let [c0, c1, c2] = color.ch;
Color::new([self.lookup(c0), self.lookup(c1), self.lookup(c2)])
}
#[cfg(feature = "f32")]
#[must_use]
pub fn lookup_f32(&self, x: f32) -> f32 {
let x = crate::color_f32::sat_unit(x);
let last = N.saturating_sub(1);
let Some(&last_knot) = self.knots.get(last) else {
return 0.0;
};
if last == 0 || x >= 1.0 {
return last_knot.to_f32();
}
if x <= 0.0 {
return match self.knots.first() {
Some(&k) => k.to_f32(),
None => 0.0,
};
}
let segs = last as f32;
let pos = x * segs;
let mut idx = pos as usize;
if idx >= last {
idx = last.saturating_sub(1);
}
let Some(&a_u) = self.knots.get(idx) else {
return last_knot.to_f32();
};
let b_u = match self.knots.get(idx.saturating_add(1)) {
Some(&k) => k,
None => a_u,
};
let frac = crate::color_f32::sat_unit(pos - (idx as f32));
crate::color_f32::lerp_unit(a_u.to_f32(), b_u.to_f32(), frac)
}
#[cfg(feature = "f32")]
#[must_use]
pub fn encode_f32<S: ColorSpace>(
&self,
color: crate::ColorF32<S, Linear>,
) -> crate::ColorF32<S, Encoded> {
let [c0, c1, c2] = color.ch;
crate::ColorF32::new([
self.lookup_f32(c0),
self.lookup_f32(c1),
self.lookup_f32(c2),
])
}
#[cfg(feature = "f32")]
#[must_use]
pub fn decode_f32<S: ColorSpace>(
&self,
color: crate::ColorF32<S, Encoded>,
) -> crate::ColorF32<S, Linear> {
let [c0, c1, c2] = color.ch;
crate::ColorF32::new([
self.lookup_f32(c0),
self.lookup_f32(c1),
self.lookup_f32(c2),
])
}
}
pub struct Gradient<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> {
stops: [Color<S, E>; N],
_pd: PhantomData<fn() -> (S, E)>,
}
impl<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> Copy for Gradient<S, E, N> {}
impl<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> Clone for Gradient<S, E, N> {
fn clone(&self) -> Self {
*self
}
}
impl<S: ColorSpace, const N: usize> Gradient<S, Linear, N> {
#[must_use]
pub const fn from_stops(stops: [Color<S, Linear>; N]) -> Self {
const { assert!(N >= 2, "Gradient requires at least two stops") };
Self {
stops,
_pd: PhantomData,
}
}
#[must_use]
pub const fn sample(&self, t: Q0_16) -> Color<S, Linear> {
sample_stops(&self.stops, t)
}
}
impl<S: ColorSpace + Perceptual, const N: usize> Gradient<S, Encoded, N> {
#[must_use]
pub const fn from_stops(stops: [Color<S, Encoded>; N]) -> Self {
const { assert!(N >= 2, "Gradient requires at least two stops") };
Self {
stops,
_pd: PhantomData,
}
}
#[must_use]
pub const fn sample(&self, t: Q0_16) -> Color<S, Encoded> {
sample_stops(&self.stops, t)
}
}
#[allow(clippy::indexing_slicing)] const fn sample_stops<S: ColorSpace, E: crate::encoding::Encoding, const N: usize>(
stops: &[Color<S, E>; N],
t: Q0_16,
) -> Color<S, E> {
let last = N.saturating_sub(1);
let segs = if last > u32::MAX as usize {
u32::MAX
} else {
last as u32
};
let pos = match (t.to_raw() as u32).saturating_mul(segs) {
p if p > 65535 * 65535 => 65535 * 65535,
p => p,
};
let hi = last.saturating_sub(1) as u32;
let raw = crate::arith::div_65535(pos);
let idx_u = if raw < hi { raw } else { hi };
let rem = crate::arith::rem_65535(pos, idx_u);
let idx = idx_u as usize;
lerp_color(
stops[idx],
stops[idx.saturating_add(1)],
Q0_16::from_raw(rem),
)
}
const fn lerp_color<S: ColorSpace, E: crate::encoding::Encoding>(
a: Color<S, E>,
b: Color<S, E>,
t: Q0_16,
) -> Color<S, E> {
let [a0, a1, a2] = a.ch;
let [b0, b1, b2] = b.ch;
Color::new([
lerp_q0_16(a0, b0, t),
lerp_q0_16(a1, b1, t),
lerp_q0_16(a2, b2, t),
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::space::Srgb;
fn ramp17() -> InterpLut<17> {
let mut knots = [0u16; 17];
for (i, slot) in knots.iter_mut().enumerate() {
let n = i as u32;
*slot = match n.saturating_mul(65535).checked_div(16) {
Some(v) if v <= u32::from(u16::MAX) => v as u16,
_ => 0,
};
}
InterpLut::from_knots(Q0_16::array_from_raw(knots), 0)
}
#[test]
fn endpoints_match_knots() {
let lut = ramp17();
assert_eq!(lut.lookup(Q0_16::ZERO), Q0_16::ZERO);
assert_eq!(lut.lookup(Q0_16::ONE), Q0_16::ONE);
}
#[test]
fn midpoint_between_first_knots() {
let lut = InterpLut::<17>::from_knots(
{
let mut k = [0u16; 17];
if let Some(slot) = k.get_mut(1) {
*slot = 1000;
}
Q0_16::array_from_raw(k)
},
0,
);
let rem = match (2048u64)
.saturating_mul(16)
.checked_rem(u64::from(u16::MAX))
{
Some(r) if r <= u64::from(u16::MAX) => r as u16,
_ => 0,
};
let y = lut.lookup(Q0_16::from_raw(2048));
assert_eq!(
y,
lerp_q0_16(Q0_16::ZERO, Q0_16::from_raw(1000), Q0_16::from_raw(rem))
);
}
#[test]
fn increasing_table_is_monotonic() {
let lut = ramp17();
let mut prev = lut.lookup(Q0_16::ZERO);
for x in 1..=u16::MAX {
let y = lut.lookup(Q0_16::from_raw(x));
assert!(y >= prev, "x={x} y={y:?} prev={prev:?}");
prev = y;
}
}
#[test]
fn decreasing_table_is_monotonic() {
let mut knots = [0u16; 17];
for (i, slot) in knots.iter_mut().enumerate() {
let n = 16u32.saturating_sub(i as u32);
*slot = match n.saturating_mul(65535).checked_div(16) {
Some(v) if v <= u32::from(u16::MAX) => v as u16,
_ => 0,
};
}
let lut = InterpLut::from_knots(Q0_16::array_from_raw(knots), 0);
let mut prev = lut.lookup(Q0_16::ZERO);
for x in 1..=u16::MAX {
let y = lut.lookup(Q0_16::from_raw(x));
assert!(y <= prev, "x={x} y={y:?} prev={prev:?}");
prev = y;
}
}
#[test]
fn lerp_endpoints() {
let a = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([0, 0, 0]));
let b = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([100, 200, 300]));
assert_eq!(a.lerp(b, Q0_16::ZERO), a);
assert_eq!(a.lerp(b, Q0_16::ONE), b);
}
#[test]
fn gradient_endpoints() {
let g = Gradient::<Srgb, Linear, 2>::from_stops([
Color::new(Q0_16::array_from_raw([0, 0, 0])),
Color::new(Q0_16::array_from_raw([10, 20, 30])),
]);
assert_eq!(g.sample(Q0_16::ZERO).ch, Q0_16::array_from_raw([0, 0, 0]));
assert_eq!(g.sample(Q0_16::ONE).ch, Q0_16::array_from_raw([10, 20, 30]));
}
}