#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[repr(C)]
pub struct Hsv {
pub h: f32,
pub s: f32,
pub v: f32,
}
impl Hsv {
#[must_use]
pub const fn new(h: f32, s: f32, v: f32) -> Self {
Self { h, s, v }
}
#[must_use]
pub fn lerp(self, other: Self, t: f32) -> Self {
use crate::space::math::{lerp_f32, lerp_hue};
Self {
h: lerp_hue(self.h, other.h, t),
s: lerp_f32(self.s, other.s, t),
v: lerp_f32(self.v, other.v, t),
}
}
#[must_use]
pub fn hue_degrees(self) -> f32 {
self.h * 360.0
}
#[must_use]
pub fn from_degrees(h_deg: f32, s: f32, v: f32) -> Self {
Self::new(h_deg / 360.0, s, v)
}
}
impl From<[f32; 3]> for Hsv {
fn from([h, s, v]: [f32; 3]) -> Self {
Self { h, s, v }
}
}
impl From<Hsv> for [f32; 3] {
fn from(c: Hsv) -> Self {
[c.h, c.s, c.v]
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::space::Srgb;
#[test]
fn red_to_hsv() {
let hsv = Hsv::from(Srgb::RED);
assert!(hsv.h.abs() < 1e-4 || (hsv.h - 1.0).abs() < 1e-4);
assert!((hsv.s - 1.0).abs() < 1e-4);
assert!((hsv.v - 1.0).abs() < 1e-4);
}
#[test]
fn black_has_zero_value() {
let hsv = Hsv::from(Srgb::BLACK);
assert!(hsv.v.abs() < 1e-5);
}
#[test]
fn white_has_zero_saturation() {
let hsv = Hsv::from(Srgb::WHITE);
assert!(hsv.s.abs() < 1e-5);
assert!((hsv.v - 1.0).abs() < 1e-5);
}
#[test]
fn roundtrip() {
let original = Srgb::new(0.8, 0.3, 0.5);
let hsv = Hsv::from(original);
let back = Srgb::from(hsv);
assert!((back.r - original.r).abs() < 1e-5);
assert!((back.g - original.g).abs() < 1e-5);
assert!((back.b - original.b).abs() < 1e-5);
}
#[test]
fn lerp_hue() {
let a = Hsv::new(0.0, 1.0, 1.0);
let b = Hsv::new(1.0 / 3.0, 1.0, 1.0);
let mid = a.lerp(b, 0.5);
assert!((mid.h - 1.0 / 6.0).abs() < 1e-5, "hue={}", mid.h);
}
}