use crate::rgb::{HasBlue, HasGreen, HasRed, RgbColor};
pub trait LerpChannel: Copy {
#[must_use]
fn lerp_channel(self, other: Self, t: f32) -> Self;
}
impl LerpChannel for u8 {
#[inline]
#[allow(clippy::suboptimal_flops)]
fn lerp_channel(self, other: Self, t: f32) -> Self {
let a = f32::from(self);
let b = f32::from(other);
let v = (a + (b - a) * t).clamp(0.0, 255.0);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "v is clamped to [0, 255]; +0.5 then truncation rounds half away from zero"
)]
{
(v + 0.5) as Self
}
}
}
impl LerpChannel for u16 {
#[inline]
#[allow(clippy::suboptimal_flops)]
fn lerp_channel(self, other: Self, t: f32) -> Self {
let a = f32::from(self);
let b = f32::from(other);
let v = (a + (b - a) * t).clamp(0.0, 65535.0);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "v is clamped to [0, 65535]; +0.5 then truncation rounds half away from zero"
)]
{
(v + 0.5) as Self
}
}
}
impl LerpChannel for f32 {
#[inline]
#[allow(clippy::suboptimal_flops)]
fn lerp_channel(self, other: Self, t: f32) -> Self {
self + (other - self) * t
}
}
pub trait Lerp: Sized {
#[must_use]
fn lerp(self, other: Self, t: f32) -> Self;
}
impl<C> Lerp for C
where
C: RgbColor,
<C as HasRed>::Component: LerpChannel,
<C as HasGreen>::Component: LerpChannel,
<C as HasBlue>::Component: LerpChannel,
{
fn lerp(self, other: Self, t: f32) -> Self {
let red = self.red().lerp_channel(other.red(), t);
let green = self.green().lerp_channel(other.green(), t);
let blue = self.blue().lerp_channel(other.blue(), t);
let mut out = self;
out.set_red(red);
out.set_green(green);
out.set_blue(blue);
out
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::alpha::AlphaFirst;
use crate::rgb::{Bgr888, Rgb, Rgb565, Rgb888, Rgbf32};
#[test]
fn u8_endpoints_and_midpoint() {
assert_eq!(0u8.lerp_channel(255, 0.0), 0);
assert_eq!(0u8.lerp_channel(255, 1.0), 255);
assert_eq!(0u8.lerp_channel(255, 0.5), 128); assert_eq!(100u8.lerp_channel(101, 0.5), 101); }
#[test]
fn u8_midpoint_is_exact_for_all_pairs() {
for a in 0u8..=255 {
for b in 0u8..=255 {
let got = a.lerp_channel(b, 0.5);
let exact = u8::try_from((u16::from(a) + u16::from(b)).div_ceil(2)).unwrap();
assert_eq!(got, exact, "a={a} b={b}");
}
}
}
#[test]
fn u8_clamps_out_of_range_t() {
assert_eq!(10u8.lerp_channel(20, -1.0), 0);
assert_eq!(250u8.lerp_channel(255, 2.0), 255);
}
#[test]
fn f32_channel_is_exact() {
assert_eq!(0.0f32.lerp_channel(1.0, 0.25), 0.25);
assert_eq!((-1.0f32).lerp_channel(1.0, 0.5), 0.0);
}
#[test]
fn rgb888_lerp() {
let a = Rgb888::from_rgb(0, 0, 255);
let b = Rgb888::from_rgb(255, 0, 0);
assert_eq!(a.lerp(b, 0.5), Rgb888::from_rgb(128, 0, 128));
}
#[test]
fn bgr888_lerp_channels_track_color_not_position() {
let a = Bgr888::from_bgr(0, 0, 255);
let b = Bgr888::from_bgr(255, 0, 0);
assert_eq!(a.lerp(b, 0.5), Bgr888::from_bgr(128, 0, 128));
}
#[test]
fn rgb565_lerp_in_native_domain() {
let a = Rgb565::from_rgb(0, 0, 0);
let b = Rgb565::from_rgb(31, 63, 31);
let mid = a.lerp(b, 0.5);
assert_eq!((mid.red(), mid.green(), mid.blue()), (16, 32, 16)); }
#[test]
fn rgbf32_lerp_unclamped() {
let a = Rgbf32::from_rgb(0.0, -1.0, 0.0);
let b = Rgbf32::from_rgb(1.0, 1.0, 2.0);
let mid = a.lerp(b, 0.5);
assert_eq!((mid.red(), mid.green(), mid.blue()), (0.5, 0.0, 1.0));
}
#[test]
fn custom_rgb_u16_lerp() {
let a: Rgb<u16> = Rgb::from_rgb(0, 0, 0);
let b: Rgb<u16> = Rgb::from_rgb(65535, 0, 1000);
let mid = a.lerp(b, 0.5);
assert_eq!((mid.red(), mid.green(), mid.blue()), (32768, 0, 500)); }
#[test]
fn rgba_preserves_alpha() {
let a = AlphaFirst::<u8, Rgb888>::with_color(200, Rgb888::from_rgb(0, 0, 0));
let b = AlphaFirst::<u8, Rgb888>::with_color(50, Rgb888::from_rgb(255, 255, 255));
let mid = a.lerp(b, 0.5);
assert_eq!(mid.alpha(), 200);
assert_eq!((mid.red(), mid.green(), mid.blue()), (128, 128, 128));
}
}