const MAX_YIQ_DELTA_SQ: f64 = 35215.0;
pub type Rgba = [u8; 4];
pub fn color_delta(px1: Rgba, px2: Rgba, y_only: bool) -> f64 {
let (r1, g1, b1) = blend(px1);
let (r2, g2, b2) = blend(px2);
let dr = r1 - r2;
let dg = g1 - g2;
let db = b1 - b2;
let y = 0.29889531 * dr + 0.58662247 * dg + 0.11448223 * db;
if y_only {
return y;
}
let i = 0.59597799 * dr - 0.27417610 * dg - 0.32180189 * db;
let q = 0.21147017 * dr - 0.52261711 * dg + 0.31114694 * db;
let delta_sq = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
(delta_sq / MAX_YIQ_DELTA_SQ).sqrt()
}
fn blend(px: Rgba) -> (f64, f64, f64) {
let a = px[3] as f64 / 255.0;
if (a - 1.0).abs() < f64::EPSILON {
return (px[0] as f64, px[1] as f64, px[2] as f64);
}
let bg = 255.0;
(
bg * (1.0 - a) + px[0] as f64 * a,
bg * (1.0 - a) + px[1] as f64 * a,
bg * (1.0 - a) + px[2] as f64 * a,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_pixels_have_zero_delta() {
let px = [128, 64, 200, 255];
assert_eq!(color_delta(px, px, false), 0.0);
}
#[test]
fn black_vs_white_is_high_delta() {
let black = [0, 0, 0, 255];
let white = [255, 255, 255, 255];
let delta = color_delta(black, white, false);
assert!(delta > 0.9, "black vs white delta should be near 1.0, got {delta}");
}
#[test]
fn similar_colors_have_small_delta() {
let a = [100, 100, 100, 255];
let b = [102, 100, 101, 255];
let delta = color_delta(a, b, false);
assert!(delta < 0.02, "similar colors should have small delta, got {delta}");
}
#[test]
fn transparent_vs_opaque_blends_against_white() {
let transparent = [0, 0, 0, 0]; let white = [255, 255, 255, 255];
let delta = color_delta(transparent, white, false);
assert!(delta < 0.001, "transparent pixel should blend to white background, got {delta}");
}
#[test]
fn y_only_returns_luminance_delta() {
let a = [100, 100, 100, 255];
let b = [200, 100, 100, 255];
let y_delta = color_delta(a, b, true);
let full_delta = color_delta(a, b, false);
assert!(y_delta.abs() < full_delta.abs() + 50.0);
}
}