use crate::{matrix::Matrix3, space::Xyz};
const XYZ_TO_LMS: Matrix3 = Matrix3::new([[0.4002, 0.7076, -0.0808], [-0.2263, 1.1653, 0.0457], [0.0, 0.0, 0.9182]]);
const LMS_TO_XYZ: Matrix3 = XYZ_TO_LMS.inverse();
mod protan {
use super::Matrix3;
pub const SEPARATOR: [f64; 3] = [0.0, 1.0, -1.0];
pub const PLANE1: Matrix3 = Matrix3::new([[0.0, 2.02344, -2.52581], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
pub const PLANE2: Matrix3 = Matrix3::new([[0.0, 2.02344, -2.52581], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
}
mod deutan {
use super::Matrix3;
pub const SEPARATOR: [f64; 3] = [1.0, 0.0, -1.0];
pub const PLANE1: Matrix3 = Matrix3::new([[1.0, 0.0, 0.0], [0.494207, 0.0, 1.24827], [0.0, 0.0, 1.0]]);
pub const PLANE2: Matrix3 = Matrix3::new([[1.0, 0.0, 0.0], [0.494207, 0.0, 1.24827], [0.0, 0.0, 1.0]]);
}
mod tritan {
use super::Matrix3;
pub const SEPARATOR: [f64; 3] = [1.0, -1.0, 0.0];
pub const PLANE1: Matrix3 = Matrix3::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-0.395913, 0.801109, 0.0]]);
pub const PLANE2: Matrix3 = Matrix3::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-0.395913, 0.801109, 0.0]]);
}
fn simulate(xyz: Xyz, separator: [f64; 3], plane1: Matrix3, plane2: Matrix3) -> Xyz {
let [x, y, z] = xyz.components();
let lms = XYZ_TO_LMS * [x, y, z];
let dot = separator[0] * lms[0] + separator[1] * lms[1] + separator[2] * lms[2];
let projected = if dot >= 0.0 { plane1 * lms } else { plane2 * lms };
let [rx, ry, rz] = LMS_TO_XYZ * projected;
Xyz::new(rx, ry, rz)
}
pub fn protanopia(color: impl Into<Xyz>) -> Xyz {
simulate(color.into(), protan::SEPARATOR, protan::PLANE1, protan::PLANE2)
}
pub fn deuteranopia(color: impl Into<Xyz>) -> Xyz {
simulate(color.into(), deutan::SEPARATOR, deutan::PLANE1, deutan::PLANE2)
}
pub fn tritanopia(color: impl Into<Xyz>) -> Xyz {
simulate(color.into(), tritan::SEPARATOR, tritan::PLANE1, tritan::PLANE2)
}
#[cfg(test)]
mod test {
use super::*;
mod protanopia_fn {
use super::*;
#[test]
fn it_returns_valid_xyz() {
let result = protanopia(Xyz::new(0.4, 0.3, 0.2));
assert!(result.x().is_finite());
assert!(result.y().is_finite());
assert!(result.z().is_finite());
}
#[test]
fn it_preserves_black() {
let result = protanopia(Xyz::new(0.0, 0.0, 0.0));
assert!((result.x()).abs() < 1e-10);
assert!((result.y()).abs() < 1e-10);
assert!((result.z()).abs() < 1e-10);
}
#[test]
fn it_changes_chromatic_colors() {
let original = Xyz::new(0.4, 0.2, 0.1);
let result = protanopia(original);
assert!((result.x() - original.x()).abs() > 1e-6 || (result.z() - original.z()).abs() > 1e-6);
}
#[test]
fn it_is_idempotent() {
let color = Xyz::new(0.4, 0.3, 0.2);
let once = protanopia(color);
let twice = protanopia(once);
assert!((once.x() - twice.x()).abs() < 1e-10);
assert!((once.y() - twice.y()).abs() < 1e-10);
assert!((once.z() - twice.z()).abs() < 1e-10);
}
}
mod deuteranopia_fn {
use super::*;
#[test]
fn it_returns_valid_xyz() {
let result = deuteranopia(Xyz::new(0.4, 0.3, 0.2));
assert!(result.x().is_finite());
assert!(result.y().is_finite());
assert!(result.z().is_finite());
}
#[test]
fn it_preserves_black() {
let result = deuteranopia(Xyz::new(0.0, 0.0, 0.0));
assert!((result.x()).abs() < 1e-10);
assert!((result.y()).abs() < 1e-10);
assert!((result.z()).abs() < 1e-10);
}
#[test]
fn it_changes_chromatic_colors() {
let original = Xyz::new(0.2, 0.4, 0.1);
let result = deuteranopia(original);
assert!((result.x() - original.x()).abs() > 1e-6 || (result.y() - original.y()).abs() > 1e-6);
}
#[test]
fn it_is_idempotent() {
let color = Xyz::new(0.4, 0.3, 0.2);
let once = deuteranopia(color);
let twice = deuteranopia(once);
assert!((once.x() - twice.x()).abs() < 1e-10);
assert!((once.y() - twice.y()).abs() < 1e-10);
assert!((once.z() - twice.z()).abs() < 1e-10);
}
}
mod tritanopia_fn {
use super::*;
#[test]
fn it_returns_valid_xyz() {
let result = tritanopia(Xyz::new(0.4, 0.3, 0.2));
assert!(result.x().is_finite());
assert!(result.y().is_finite());
assert!(result.z().is_finite());
}
#[test]
fn it_preserves_black() {
let result = tritanopia(Xyz::new(0.0, 0.0, 0.0));
assert!((result.x()).abs() < 1e-10);
assert!((result.y()).abs() < 1e-10);
assert!((result.z()).abs() < 1e-10);
}
#[test]
fn it_changes_chromatic_colors() {
let original = Xyz::new(0.2, 0.2, 0.4);
let result = tritanopia(original);
assert!((result.z() - original.z()).abs() > 1e-6);
}
#[test]
fn it_is_idempotent() {
let color = Xyz::new(0.4, 0.3, 0.2);
let once = tritanopia(color);
let twice = tritanopia(once);
assert!((once.x() - twice.x()).abs() < 1e-10);
assert!((once.y() - twice.y()).abs() < 1e-10);
assert!((once.z() - twice.z()).abs() < 1e-10);
}
}
}