#![allow(clippy::excessive_precision)]
pub fn xy_to_rgb(x_raw: u16, y_raw: u16, brightness: u8) -> (u8, u8, u8) {
let x = x_raw as f32 / 65536.0;
let y = y_raw as f32 / 65536.0;
if y < 0.0001 {
return (0, 0, 0);
}
let z = 1.0 - x - y;
let big_y = 1.0;
let big_x = (big_y / y) * x;
let big_z = (big_y / y) * z;
let mut r = big_x * 3.2404542 - big_y * 1.5371385 - big_z * 0.4985314;
let mut g = -big_x * 0.9692660 + big_y * 1.8760108 + big_z * 0.0415560;
let mut b = big_x * 0.0556434 - big_y * 0.2040259 + big_z * 1.0572252;
let max_val = r.max(g).max(b);
if max_val > 1.0 {
r /= max_val;
g /= max_val;
b /= max_val;
}
r = r.max(0.0);
g = g.max(0.0);
b = b.max(0.0);
let full = ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8);
scale_rgb(full, brightness)
}
pub fn mireds_to_rgb(mireds: u16) -> (u8, u8, u8) {
let mireds = mireds.clamp(153, 500);
let kelvin = 1_000_000.0 / mireds as f32;
kelvin_to_rgb(kelvin)
}
pub fn scale_rgb(rgb: (u8, u8, u8), brightness: u8) -> (u8, u8, u8) {
let scale = u16::from(brightness.min(254));
let scale_channel = |c: u8| ((u16::from(c) * scale + 127) / 254) as u8;
(
scale_channel(rgb.0),
scale_channel(rgb.1),
scale_channel(rgb.2),
)
}
pub fn cct_mix_from_mireds(
mireds: u16,
brightness: u8,
min_mireds: u16,
max_mireds: u16,
) -> (u8, u8) {
if brightness == 0 {
return (0, 0);
}
let min = min_mireds.min(max_mireds);
let max = max_mireds.max(min_mireds);
let span = u32::from(max.saturating_sub(min)).max(1);
let clamped = mireds.clamp(min, max);
let position = f32::from(clamped - min) / span as f32;
let (cool_ratio, warm_ratio) = if position <= 0.5 {
(1.0, position * 2.0)
} else {
((1.0 - position) * 2.0, 1.0)
};
let cool = (f32::from(brightness) * cool_ratio)
.round()
.clamp(0.0, f32::from(brightness)) as u8;
let warm = (f32::from(brightness) * warm_ratio)
.round()
.clamp(0.0, f32::from(brightness)) as u8;
(cool, warm)
}
pub fn cct_mireds_from_rgb(r: u8, _g: u8, b: u8, min_mireds: u16, max_mireds: u16) -> u16 {
let min = min_mireds.min(max_mireds);
let max = max_mireds.max(min_mireds);
let red_blue = u16::from(r) + u16::from(b);
let warm_ratio = if red_blue == 0 {
0.5
} else {
f32::from(r) / f32::from(red_blue)
};
let span = f32::from(max - min);
(f32::from(min) + span * warm_ratio).round() as u16
}
pub fn kelvin_to_rgb(kelvin: f32) -> (u8, u8, u8) {
let temp = kelvin / 100.0;
let r = if temp <= 66.0 {
255.0
} else {
(329.698727446 * (temp - 60.0).powf(-0.1332047592)).clamp(0.0, 255.0)
};
let g = if temp <= 66.0 {
(99.4708025861 * temp.ln() - 161.1195681661).clamp(0.0, 255.0)
} else {
(288.1221695283 * (temp - 60.0).powf(-0.0755148492)).clamp(0.0, 255.0)
};
let b = if temp >= 66.0 {
255.0
} else if temp <= 19.0 {
0.0
} else {
(138.5177312231 * (temp - 10.0).ln() - 305.0447927307).clamp(0.0, 255.0)
};
(r as u8, g as u8, b as u8)
}
pub fn hsv_to_rgb(hue: u8, saturation: u8, value: u8) -> (u8, u8, u8) {
let h = hue.min(254) as f32 / 254.0 * 360.0;
let s = saturation.min(254) as f32 / 254.0;
let v = value.min(254) as f32 / 254.0;
if s < 0.001 {
let gray = (v * 255.0) as u8;
return (gray, gray, gray);
}
let h_sector = h / 60.0;
let i = h_sector.floor() as u32;
let f = h_sector - i as f32;
let p = v * (1.0 - s);
let q = v * (1.0 - s * f);
let t = v * (1.0 - s * (1.0 - f));
let (r, g, b) = match i % 6 {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
_ => (v, p, q),
};
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
}
pub fn enhanced_hue_to_hue(enhanced_hue: u16) -> u8 {
((enhanced_hue as u32 * 254 + 32768) / 65536) as u8
}
pub fn hue_to_enhanced_hue(hue: u8) -> u16 {
((u32::from(hue.min(254)) * 65536 + 127) / 254).min(65535) as u16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xy_d65_white_is_neutral() {
let (r, g, b) = xy_to_rgb(20495, 21561, 254);
assert!(r > 200, "r={r} too low for white");
assert!(g > 200, "g={g} too low for white");
assert!(b > 200, "b={b} too low for white");
}
#[test]
fn xy_zero_brightness_is_black() {
let (r, g, b) = xy_to_rgb(20495, 21561, 0);
assert_eq!((r, g, b), (0, 0, 0));
}
#[test]
fn xy_red_region() {
let x = (0.64 * 65535.0) as u16;
let y = (0.33 * 65535.0) as u16;
let (r, g, b) = xy_to_rgb(x, y, 254);
assert!(r > g && r > b, "red region: r={r} g={g} b={b}");
}
#[test]
fn xy_dimming_scales_all_channels_together() {
let x = (0.64 * 65535.0) as u16; let y = (0.33 * 65535.0) as u16;
let full = xy_to_rgb(x, y, 254);
for level in [1u8, 2, 5, 10, 40, 127] {
assert_eq!(
xy_to_rgb(x, y, level),
scale_rgb(full, level),
"xy_to_rgb({x}, {y}, {level}) must equal scale_rgb(full_color, {level})"
);
}
}
#[test]
fn mireds_warm_white() {
let (r, g, b) = mireds_to_rgb(370);
assert!(r > g && g > b, "warm white r>g>b: r={r} g={g} b={b}");
}
#[test]
fn mireds_cool_daylight() {
let (r, g, b) = mireds_to_rgb(153);
assert!(r > 200 && g > 200 && b > 200, "daylight: r={r} g={g} b={b}");
}
#[test]
fn hsv_pure_red() {
let (r, g, b) = hsv_to_rgb(0, 254, 254);
assert!(r > 200, "r={r}");
assert!(g < 30, "g={g}");
assert!(b < 30, "b={b}");
}
#[test]
fn hsv_zero_saturation_is_gray() {
let (r, g, b) = hsv_to_rgb(100, 0, 127);
assert_eq!(r, g);
assert_eq!(g, b);
}
#[test]
fn enhanced_hue_conversion() {
assert_eq!(enhanced_hue_to_hue(0), 0);
assert_eq!(enhanced_hue_to_hue(65535), 254);
assert_eq!(enhanced_hue_to_hue(32768), 127);
}
#[test]
fn hue_to_enhanced_hue_boundaries_and_roundtrip() {
assert_eq!(hue_to_enhanced_hue(0), 0);
assert_eq!(hue_to_enhanced_hue(127), 32768);
assert_eq!(hue_to_enhanced_hue(254), 65535);
assert_eq!(hue_to_enhanced_hue(255), 65535);
for hue in 0..=254u8 {
assert_eq!(enhanced_hue_to_hue(hue_to_enhanced_hue(hue)), hue);
}
}
#[test]
fn enhanced_hue_intermediate_values_round_to_nearest() {
assert_eq!(enhanced_hue_to_hue(128), 0);
assert_eq!(enhanced_hue_to_hue(130), 1);
assert_eq!(enhanced_hue_to_hue(256), 1);
assert_eq!(enhanced_hue_to_hue(16384), 64);
}
#[test]
fn kelvin_extremes_dont_panic() {
let _ = kelvin_to_rgb(1000.0);
let _ = kelvin_to_rgb(6500.0);
let _ = kelvin_to_rgb(40000.0);
}
#[test]
fn mireds_boundary_values() {
let _ = mireds_to_rgb(153);
let _ = mireds_to_rgb(500);
let _ = mireds_to_rgb(250);
}
#[test]
fn scale_rgb_full_level_is_identity() {
assert_eq!(scale_rgb((255, 128, 0), 254), (255, 128, 0));
}
#[test]
fn scale_rgb_zero_level_is_black() {
assert_eq!(scale_rgb((255, 255, 255), 0), (0, 0, 0));
}
#[test]
fn scale_rgb_half_level_halves() {
assert_eq!(scale_rgb((254, 100, 0), 127), (127, 50, 0));
}
#[test]
fn scale_rgb_out_of_spec_255_does_not_wrap_to_black() {
assert_eq!(scale_rgb((255, 255, 255), 255), (255, 255, 255));
}
#[test]
fn scale_rgb_rounds_to_nearest_keeping_small_channels_alive() {
assert_eq!(scale_rgb((255, 63, 0), 3), (3, 1, 0));
assert_eq!(scale_rgb((1, 1, 1), 127), (1, 1, 1));
}
}