1use core::marker::PhantomData;
7
8use crate::arith::lerp_q0_16;
9use crate::color::Color;
10use crate::encoding::{Encoded, Linear};
11use crate::fixed::Q0_16;
12use crate::space::{ColorSpace, Perceptual};
13
14const fn valid_lut_len(n: usize) -> bool {
15 n == 17 || n == 33 || n == 65 || n == 257
16}
17
18pub struct InterpLut<const N: usize> {
20 knots: [Q0_16; N],
21 max_err_lsb: u16,
22}
23
24impl<const N: usize> Copy for InterpLut<N> {}
25
26impl<const N: usize> Clone for InterpLut<N> {
27 fn clone(&self) -> Self {
28 *self
29 }
30}
31
32impl<const N: usize> InterpLut<N> {
33 #[must_use]
38 pub const fn from_knots(knots: [Q0_16; N], max_err_lsb: u16) -> Self {
39 const { assert!(valid_lut_len(N), "InterpLut N must be 17, 33, 65, or 257") };
40 Self { knots, max_err_lsb }
41 }
42
43 #[must_use]
45 pub const fn max_err_lsb(&self) -> u16 {
46 self.max_err_lsb
47 }
48
49 #[must_use]
61 #[allow(clippy::indexing_slicing)] pub const fn lookup(&self, x: Q0_16) -> Q0_16 {
63 let last = N.saturating_sub(1);
64 let segs = last as u32;
65 let pos = (x.to_raw() as u32).saturating_mul(segs);
66 let hi = last.saturating_sub(1) as u32;
67 let raw = crate::arith::div_65535(pos);
68 let idx_u = if raw < hi { raw } else { hi };
69 let rem = crate::arith::rem_65535(pos, idx_u);
70 let idx = idx_u as usize;
71 lerp_q0_16(
72 self.knots[idx],
73 self.knots[idx.saturating_add(1)],
74 Q0_16::from_raw(rem),
75 )
76 }
77
78 #[must_use]
80 pub const fn encode<S: ColorSpace>(&self, color: Color<S, Linear>) -> Color<S, Encoded> {
81 let [c0, c1, c2] = color.ch;
82 Color::new([self.lookup(c0), self.lookup(c1), self.lookup(c2)])
83 }
84
85 #[must_use]
87 pub const fn decode<S: ColorSpace>(&self, color: Color<S, Encoded>) -> Color<S, Linear> {
88 let [c0, c1, c2] = color.ch;
89 Color::new([self.lookup(c0), self.lookup(c1), self.lookup(c2)])
90 }
91
92 #[cfg(feature = "f32")]
97 #[must_use]
98 pub fn lookup_f32(&self, x: f32) -> f32 {
99 let x = crate::color_f32::sat_unit(x);
100 let last = N.saturating_sub(1);
101 let Some(&last_knot) = self.knots.get(last) else {
102 return 0.0;
103 };
104 if last == 0 || x >= 1.0 {
105 return last_knot.to_f32();
106 }
107 if x <= 0.0 {
108 return match self.knots.first() {
109 Some(&k) => k.to_f32(),
110 None => 0.0,
111 };
112 }
113 let segs = last as f32;
114 let pos = x * segs;
115 let mut idx = pos as usize;
116 if idx >= last {
117 idx = last.saturating_sub(1);
118 }
119 let Some(&a_u) = self.knots.get(idx) else {
120 return last_knot.to_f32();
121 };
122 let b_u = match self.knots.get(idx.saturating_add(1)) {
123 Some(&k) => k,
124 None => a_u,
125 };
126 let frac = crate::color_f32::sat_unit(pos - (idx as f32));
127 crate::color_f32::lerp_unit(a_u.to_f32(), b_u.to_f32(), frac)
128 }
129
130 #[cfg(feature = "f32")]
132 #[must_use]
133 pub fn encode_f32<S: ColorSpace>(
134 &self,
135 color: crate::ColorF32<S, Linear>,
136 ) -> crate::ColorF32<S, Encoded> {
137 let [c0, c1, c2] = color.ch;
138 crate::ColorF32::new([
139 self.lookup_f32(c0),
140 self.lookup_f32(c1),
141 self.lookup_f32(c2),
142 ])
143 }
144
145 #[cfg(feature = "f32")]
147 #[must_use]
148 pub fn decode_f32<S: ColorSpace>(
149 &self,
150 color: crate::ColorF32<S, Encoded>,
151 ) -> crate::ColorF32<S, Linear> {
152 let [c0, c1, c2] = color.ch;
153 crate::ColorF32::new([
154 self.lookup_f32(c0),
155 self.lookup_f32(c1),
156 self.lookup_f32(c2),
157 ])
158 }
159}
160
161pub struct Gradient<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> {
163 stops: [Color<S, E>; N],
164 _pd: PhantomData<fn() -> (S, E)>,
165}
166
167impl<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> Copy for Gradient<S, E, N> {}
168
169impl<S: ColorSpace, E: crate::encoding::Encoding, const N: usize> Clone for Gradient<S, E, N> {
170 fn clone(&self) -> Self {
171 *self
172 }
173}
174
175impl<S: ColorSpace, const N: usize> Gradient<S, Linear, N> {
176 #[must_use]
178 pub const fn from_stops(stops: [Color<S, Linear>; N]) -> Self {
179 const { assert!(N >= 2, "Gradient requires at least two stops") };
180 Self {
181 stops,
182 _pd: PhantomData,
183 }
184 }
185
186 #[must_use]
188 pub const fn sample(&self, t: Q0_16) -> Color<S, Linear> {
189 sample_stops(&self.stops, t)
190 }
191}
192
193impl<S: ColorSpace + Perceptual, const N: usize> Gradient<S, Encoded, N> {
194 #[must_use]
196 pub const fn from_stops(stops: [Color<S, Encoded>; N]) -> Self {
197 const { assert!(N >= 2, "Gradient requires at least two stops") };
198 Self {
199 stops,
200 _pd: PhantomData,
201 }
202 }
203
204 #[must_use]
206 pub const fn sample(&self, t: Q0_16) -> Color<S, Encoded> {
207 sample_stops(&self.stops, t)
208 }
209}
210
211#[allow(clippy::indexing_slicing)] const fn sample_stops<S: ColorSpace, E: crate::encoding::Encoding, const N: usize>(
220 stops: &[Color<S, E>; N],
221 t: Q0_16,
222) -> Color<S, E> {
223 let last = N.saturating_sub(1);
224 let segs = if last > u32::MAX as usize {
225 u32::MAX
226 } else {
227 last as u32
228 };
229 let pos = match (t.to_raw() as u32).saturating_mul(segs) {
233 p if p > 65535 * 65535 => 65535 * 65535,
234 p => p,
235 };
236 let hi = last.saturating_sub(1) as u32;
237 let raw = crate::arith::div_65535(pos);
238 let idx_u = if raw < hi { raw } else { hi };
239 let rem = crate::arith::rem_65535(pos, idx_u);
240 let idx = idx_u as usize;
241 lerp_color(
242 stops[idx],
243 stops[idx.saturating_add(1)],
244 Q0_16::from_raw(rem),
245 )
246}
247
248const fn lerp_color<S: ColorSpace, E: crate::encoding::Encoding>(
249 a: Color<S, E>,
250 b: Color<S, E>,
251 t: Q0_16,
252) -> Color<S, E> {
253 let [a0, a1, a2] = a.ch;
254 let [b0, b1, b2] = b.ch;
255 Color::new([
256 lerp_q0_16(a0, b0, t),
257 lerp_q0_16(a1, b1, t),
258 lerp_q0_16(a2, b2, t),
259 ])
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::space::Srgb;
266
267 fn ramp17() -> InterpLut<17> {
268 let mut knots = [0u16; 17];
269 for (i, slot) in knots.iter_mut().enumerate() {
270 let n = i as u32;
271 *slot = match n.saturating_mul(65535).checked_div(16) {
272 Some(v) if v <= u32::from(u16::MAX) => v as u16,
273 _ => 0,
274 };
275 }
276 InterpLut::from_knots(Q0_16::array_from_raw(knots), 0)
277 }
278
279 #[test]
280 fn endpoints_match_knots() {
281 let lut = ramp17();
282 assert_eq!(lut.lookup(Q0_16::ZERO), Q0_16::ZERO);
283 assert_eq!(lut.lookup(Q0_16::ONE), Q0_16::ONE);
284 }
285
286 #[test]
287 fn midpoint_between_first_knots() {
288 let lut = InterpLut::<17>::from_knots(
289 {
290 let mut k = [0u16; 17];
291 if let Some(slot) = k.get_mut(1) {
292 *slot = 1000;
293 }
294 Q0_16::array_from_raw(k)
295 },
296 0,
297 );
298 let rem = match (2048u64)
299 .saturating_mul(16)
300 .checked_rem(u64::from(u16::MAX))
301 {
302 Some(r) if r <= u64::from(u16::MAX) => r as u16,
303 _ => 0,
304 };
305 let y = lut.lookup(Q0_16::from_raw(2048));
306 assert_eq!(
307 y,
308 lerp_q0_16(Q0_16::ZERO, Q0_16::from_raw(1000), Q0_16::from_raw(rem))
309 );
310 }
311
312 #[test]
313 fn increasing_table_is_monotonic() {
314 let lut = ramp17();
315 let mut prev = lut.lookup(Q0_16::ZERO);
316 for x in 1..=u16::MAX {
317 let y = lut.lookup(Q0_16::from_raw(x));
318 assert!(y >= prev, "x={x} y={y:?} prev={prev:?}");
319 prev = y;
320 }
321 }
322
323 #[test]
324 fn decreasing_table_is_monotonic() {
325 let mut knots = [0u16; 17];
326 for (i, slot) in knots.iter_mut().enumerate() {
327 let n = 16u32.saturating_sub(i as u32);
328 *slot = match n.saturating_mul(65535).checked_div(16) {
329 Some(v) if v <= u32::from(u16::MAX) => v as u16,
330 _ => 0,
331 };
332 }
333 let lut = InterpLut::from_knots(Q0_16::array_from_raw(knots), 0);
334 let mut prev = lut.lookup(Q0_16::ZERO);
335 for x in 1..=u16::MAX {
336 let y = lut.lookup(Q0_16::from_raw(x));
337 assert!(y <= prev, "x={x} y={y:?} prev={prev:?}");
338 prev = y;
339 }
340 }
341
342 #[test]
343 fn lerp_endpoints() {
344 let a = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([0, 0, 0]));
345 let b = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([100, 200, 300]));
346 assert_eq!(a.lerp(b, Q0_16::ZERO), a);
347 assert_eq!(a.lerp(b, Q0_16::ONE), b);
348 }
349
350 #[test]
351 fn gradient_endpoints() {
352 let g = Gradient::<Srgb, Linear, 2>::from_stops([
353 Color::new(Q0_16::array_from_raw([0, 0, 0])),
354 Color::new(Q0_16::array_from_raw([10, 20, 30])),
355 ]);
356 assert_eq!(g.sample(Q0_16::ZERO).ch, Q0_16::array_from_raw([0, 0, 0]));
357 assert_eq!(g.sample(Q0_16::ONE).ch, Q0_16::array_from_raw([10, 20, 30]));
358 }
359}