use gem::rgb::Rgb888;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Tint {
#[default]
None,
Multiply {
r: u8,
g: u8,
b: u8,
},
Mix {
r: u8,
g: u8,
b: u8,
amount: u8,
},
}
impl Tint {
#[must_use]
pub const fn multiply(r: u8, g: u8, b: u8) -> Self {
Self::Multiply { r, g, b }
}
#[must_use]
pub const fn mix(r: u8, g: u8, b: u8, amount: u8) -> Self {
Self::Mix { r, g, b, amount }
}
#[must_use]
pub const fn is_identity(self) -> bool {
match self {
Self::None => true,
Self::Multiply { r, g, b } => r == 255 && g == 255 && b == 255,
Self::Mix { amount, .. } => amount == 0,
}
}
#[must_use]
pub const fn apply(self, rgb: (u8, u8, u8)) -> (u8, u8, u8) {
use gem::channel::{mix_u8, multiply_u8};
let (sr, sg, sb) = rgb;
match self {
Self::None => (sr, sg, sb),
Self::Multiply { r, g, b } => {
(multiply_u8(sr, r), multiply_u8(sg, g), multiply_u8(sb, b))
}
Self::Mix { r, g, b, amount } => (
mix_u8(sr, r, amount),
mix_u8(sg, g, amount),
mix_u8(sb, b, amount),
),
}
}
#[must_use]
pub const fn apply_rgb888(self, px: Rgb888) -> Rgb888 {
let (r, g, b) = self.apply(px.to_rgb());
Rgb888::from_rgb(r, g, b)
}
#[must_use]
pub const fn multiply_color(c: crate::color::Color, default: (u8, u8, u8)) -> Self {
let (r, g, b) = c.resolve_rgb(default);
Self::multiply(r, g, b)
}
}
#[cfg(test)]
mod tests {
use super::Tint;
#[test]
fn none_is_the_identity() {
assert_eq!(Tint::None.apply((13, 200, 255)), (13, 200, 255));
assert!(Tint::None.is_identity());
assert_eq!(Tint::default(), Tint::None);
}
#[test]
fn multiply_by_white_is_exact() {
let white = Tint::multiply(255, 255, 255);
for c in [0u8, 1, 63, 127, 128, 200, 254, 255] {
assert_eq!(white.apply((c, c, c)), (c, c, c), "channel {c}");
}
assert!(white.is_identity());
}
#[test]
fn multiply_by_black_is_black() {
assert_eq!(Tint::multiply(0, 0, 0).apply((200, 180, 60)), (0, 0, 0));
}
#[test]
fn multiply_scales_per_channel() {
let green_only = Tint::multiply(255, 128, 255);
assert_eq!(green_only.apply((200, 200, 200)), (200, 100, 200));
}
#[test]
fn multiply_can_only_darken() {
let t = Tint::multiply(200, 200, 200);
for c in 0..=255u8 {
let (r, _, _) = t.apply((c, c, c));
assert!(r <= c, "multiply brightened {c} to {r}");
}
}
#[test]
fn mix_endpoints_are_exact() {
let src = (200, 180, 60);
assert_eq!(Tint::mix(255, 255, 255, 0).apply(src), src);
assert_eq!(Tint::mix(255, 255, 255, 255).apply(src), (255, 255, 255));
assert_eq!(Tint::mix(0, 0, 0, 255).apply(src), (0, 0, 0));
}
#[test]
fn mix_at_zero_amount_is_identity_for_every_colour() {
assert!(Tint::mix(1, 2, 3, 0).is_identity());
assert_eq!(Tint::mix(1, 2, 3, 0).apply((9, 9, 9)), (9, 9, 9));
}
#[test]
fn mix_halfway_is_the_midpoint() {
assert_eq!(
Tint::mix(254, 254, 254, 128).apply((0, 0, 0)),
(127, 127, 127)
);
}
#[test]
fn mix_can_brighten_which_multiply_cannot() {
let src = (10, 10, 10);
let (r, _, _) = Tint::mix(255, 255, 255, 128).apply(src);
assert!(r > 10, "mix toward white should brighten, got {r}");
}
#[test]
fn mix_rounds_symmetrically_in_both_directions() {
let up = Tint::mix(200, 200, 200, 64).apply((100, 100, 100)).0;
let down = Tint::mix(100, 100, 100, 64).apply((200, 200, 200)).0;
assert_eq!(up - 100, 200 - down);
}
#[test]
fn identity_variants_never_change_a_pixel() {
let src = (37, 211, 4);
for t in [
Tint::None,
Tint::multiply(255, 255, 255),
Tint::mix(0, 0, 0, 0),
Tint::mix(255, 255, 255, 0),
] {
assert!(t.is_identity(), "{t:?} should report as identity");
assert_eq!(t.apply(src), src, "{t:?} changed a pixel");
}
}
#[test]
fn apply_is_usable_in_const_context() {
const SHADOWED: (u8, u8, u8) = Tint::multiply(128, 128, 128).apply((200, 180, 60));
assert_eq!(SHADOWED, (100, 90, 30));
}
#[test]
fn doc_example_values_are_what_apply_actually_returns() {
let src = (200, 180, 60);
assert_eq!(Tint::multiply(128, 128, 128).apply(src), (100, 90, 30));
assert_eq!(Tint::mix(255, 255, 255, 192).apply(src), (241, 236, 207));
assert_eq!(Tint::None.apply(src), src);
}
}