use half::f16 as F16;
use crate::entities::attrs::Attrs;
use crate::entities::frame::{Frame, PixelBuffer};
pub fn apply(frame: &Frame, attrs: &Attrs) -> Option<Frame> {
let hue_shift = attrs.get_float("hue_shift").unwrap_or(0.0);
let saturation = attrs.get_float("saturation").unwrap_or(1.0);
let value = attrs.get_float("value").unwrap_or(1.0);
if hue_shift.abs() < 0.01 && (saturation - 1.0).abs() < 0.001 && (value - 1.0).abs() < 0.001 {
return Some(frame.clone());
}
let (width, height) = frame.resolution();
let buffer = frame.buffer();
let out_buffer = match buffer.as_ref() {
PixelBuffer::U8(data) => {
let mut result = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
let r = chunk[0] as f32 / 255.0;
let g = chunk[1] as f32 / 255.0;
let b = chunk[2] as f32 / 255.0;
let a = chunk[3];
let (h, s, v) = rgb_to_hsv(r, g, b);
let h_new = (h + hue_shift).rem_euclid(360.0);
let s_new = (s * saturation).clamp(0.0, 1.0);
let v_new = (v * value).clamp(0.0, 1.0);
let (r_out, g_out, b_out) = hsv_to_rgb(h_new, s_new, v_new);
result.push((r_out * 255.0) as u8);
result.push((g_out * 255.0) as u8);
result.push((b_out * 255.0) as u8);
result.push(a);
}
PixelBuffer::U8(result)
}
PixelBuffer::F16(data) => {
let mut result = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
let r = chunk[0].to_f32();
let g = chunk[1].to_f32();
let b = chunk[2].to_f32();
let a = chunk[3];
let (h, s, v) = rgb_to_hsv(r, g, b);
let h_new = (h + hue_shift).rem_euclid(360.0);
let s_new = (s * saturation).clamp(0.0, 1.0);
let v_new = v * value;
let (r_out, g_out, b_out) = hsv_to_rgb(h_new, s_new, v_new);
result.push(F16::from_f32(r_out));
result.push(F16::from_f32(g_out));
result.push(F16::from_f32(b_out));
result.push(a);
}
PixelBuffer::F16(result)
}
PixelBuffer::F32(data) => {
let mut result = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
let r = chunk[0];
let g = chunk[1];
let b = chunk[2];
let a = chunk[3];
let (h, s, v) = rgb_to_hsv(r, g, b);
let h_new = (h + hue_shift).rem_euclid(360.0);
let s_new = (s * saturation).clamp(0.0, 1.0);
let v_new = v * value;
let (r_out, g_out, b_out) = hsv_to_rgb(h_new, s_new, v_new);
result.push(r_out);
result.push(g_out);
result.push(b_out);
result.push(a);
}
PixelBuffer::F32(result)
}
};
Some(Frame::from_buffer(out_buffer, frame.pixel_format(), width, height))
}
fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let v = max;
let s = if max > 0.0 { delta / max } else { 0.0 };
let h = if delta.abs() < 0.0001 {
0.0 } else if (max - r).abs() < 0.0001 {
60.0 * (((g - b) / delta) % 6.0)
} else if (max - g).abs() < 0.0001 {
60.0 * ((b - r) / delta + 2.0)
} else {
60.0 * ((r - g) / delta + 4.0)
};
let h = if h < 0.0 { h + 360.0 } else { h };
(h, s, v)
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
if s <= 0.0 {
return (v, v, v);
}
let h = h % 360.0;
let h = if h < 0.0 { h + 360.0 } else { h };
let c = v * s; let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = v - c;
let (r1, g1, b1) = match h_prime as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x), };
(r1 + m, g1 + m, b1 + m)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgb_hsv_roundtrip() {
let (h, s, v) = rgb_to_hsv(1.0, 0.0, 0.0);
assert!((h - 0.0).abs() < 1.0); assert!((s - 1.0).abs() < 0.01);
assert!((v - 1.0).abs() < 0.01);
let (r, g, b) = hsv_to_rgb(h, s, v);
assert!((r - 1.0).abs() < 0.01);
assert!(g.abs() < 0.01);
assert!(b.abs() < 0.01);
}
#[test]
fn test_hue_shift_red_to_green() {
let (h, s, v) = rgb_to_hsv(1.0, 0.0, 0.0);
let (r, g, b) = hsv_to_rgb(h + 120.0, s, v);
assert!(g > r); assert!(g > b);
}
#[test]
fn test_gray_unchanged() {
let (h, s, v) = rgb_to_hsv(0.5, 0.5, 0.5);
assert!(s < 0.01);
let (r, g, b) = hsv_to_rgb(h + 180.0, s, v); assert!((r - 0.5).abs() < 0.01); assert!((g - 0.5).abs() < 0.01);
assert!((b - 0.5).abs() < 0.01);
}
}