diff --git a/src/alpha.rs b/src/alpha.rs index 640af9d8a..bf75df4b3 100644 --- a/src/alpha.rs +++ b/src/alpha.rs @@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut, Add, Sub, Mul, Div}; use num::Float; -use {Mix, Shade, GetHue, Hue, Saturate}; +use {Mix, Shade, GetHue, Hue, Saturate, Limited, clamp}; ///An alpha component wrapper for colors. #[derive(Clone, Copy, Debug, PartialEq)] @@ -86,6 +86,24 @@ impl Saturate for Alpha { } } +impl Limited for Alpha { + fn is_valid(&self) -> bool { + self.color.is_valid() && self.alpha >= T::zero() && self.alpha <= T::one() + } + + fn clamp(&self) -> Alpha { + Alpha { + color: self.color.clamp(), + alpha: clamp(self.alpha, T::zero(), T::one()), + } + } + + fn clamp_self(&mut self) { + self.color.clamp_self(); + self.alpha = clamp(self.alpha, T::zero(), T::one()); + } +} + impl Default for Alpha { fn default() -> Alpha { Alpha { diff --git a/src/hsl.rs b/src/hsl.rs index 2179bdce2..a0bcb7412 100644 --- a/src/hsl.rs +++ b/src/hsl.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub}; -use {Color, Alpha, Rgb, Luma, Xyz, Lab, Lch, Hsv, ColorSpace, Mix, Shade, GetHue, Hue, Saturate, RgbHue, clamp}; +use {Color, Alpha, Rgb, Luma, Xyz, Lab, Lch, Hsv, Limited, Mix, Shade, GetHue, Hue, Saturate, RgbHue, clamp}; ///Linear HSL with an alpha component. See the [`Hsla` implementation in `Alpha`](struct.Alpha.html#Hsla). pub type Hsla = Alpha, T>; @@ -54,7 +54,7 @@ impl Alpha, T> { } } -impl ColorSpace for Hsl { +impl Limited for Hsl { fn is_valid(&self) -> bool { self.saturation >= T::zero() && self.saturation <= T::one() && self.lightness >= T::zero() && self.lightness <= T::one() @@ -290,7 +290,7 @@ impl From> for Hsl { #[cfg(test)] mod test { use super::Hsl; - use ::{Rgb, Hsv}; + use {Rgb, Hsv}; #[test] fn red() { @@ -341,4 +341,19 @@ mod test { assert_approx_eq!(a, b, [hue, saturation, lightness]); assert_approx_eq!(a, c, [hue, saturation, lightness]); } + + #[test] + fn ranges() { + assert_ranges!{ + Hsl; + limited { + saturation: 0.0 => 1.0, + lightness: 0.0 => 1.0 + } + limited_min {} + unlimited { + hue: -360.0 => 360.0 + } + } + } } diff --git a/src/hsv.rs b/src/hsv.rs index 788a55bc3..8eda77db6 100644 --- a/src/hsv.rs +++ b/src/hsv.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub}; -use {Color, Alpha, Rgb, Luma, Xyz, Lab, Lch, Hsl, ColorSpace, Mix, Shade, GetHue, Hue, Saturate, RgbHue, clamp}; +use {Color, Alpha, Rgb, Luma, Xyz, Lab, Lch, Hsl, Limited, Mix, Shade, GetHue, Hue, Saturate, RgbHue, clamp}; ///Linear HSV with an alpha component. See the [`Hsva` implementation in `Alpha`](struct.Alpha.html#Hsva). pub type Hsva = Alpha, T>; @@ -53,7 +53,7 @@ impl Alpha, T> { } } -impl ColorSpace for Hsv { +impl Limited for Hsv { fn is_valid(&self) -> bool { self.saturation >= T::zero() && self.saturation <= T::one() && self.value >= T::zero() && self.value <= T::one() @@ -337,4 +337,19 @@ mod test { assert_approx_eq!(a, b, [hue, saturation, value]); assert_approx_eq!(a, c, [hue, saturation, value]); } + + #[test] + fn ranges() { + assert_ranges!{ + Hsv; + limited { + saturation: 0.0 => 1.0, + value: 0.0 => 1.0 + } + limited_min {} + unlimited { + hue: -360.0 => 360.0 + } + } + } } diff --git a/src/hues.rs b/src/hues.rs index 87fbe0d8b..116236900 100644 --- a/src/hues.rs +++ b/src/hues.rs @@ -126,7 +126,7 @@ fn normalize_angle(mut deg: T) -> T { } while deg <= -T::from(180.0).unwrap() { - deg = deg - T::from(360.0).unwrap(); + deg = deg + T::from(360.0).unwrap(); } deg diff --git a/src/lab.rs b/src/lab.rs index fc63f8323..07a50104b 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub, Mul, Div}; -use {Color, Alpha, Rgb, Luma, Xyz, Lch, Hsv, Hsl, ColorSpace, Mix, Shade, GetHue, LabHue, clamp}; +use {Color, Alpha, Rgb, Luma, Xyz, Lch, Hsv, Hsl, Limited, Mix, Shade, GetHue, LabHue, clamp}; use tristimulus::{X_N, Y_N, Z_N}; @@ -55,7 +55,7 @@ impl Alpha, T> { } } -impl ColorSpace for Lab { +impl Limited for Lab { fn is_valid(&self) -> bool { self.l >= T::zero() && self.l <= T::one() && self.a >= -T::one() && self.a <= T::one() && @@ -307,4 +307,18 @@ mod test { let b = Lab::new(32.302586 / 100.0, 79.19668 / 128.0, -107.863686 / 128.0); assert_approx_eq!(a, b, [l, a, b]); } + + #[test] + fn ranges() { + assert_ranges!{ + Lab; + limited { + l: 0.0 => 1.0, + a: -1.0 => 1.0, + b: -1.0 => 1.0 + } + limited_min {} + unlimited {} + } + } } diff --git a/src/lch.rs b/src/lch.rs index d7204304d..b58c9a65d 100644 --- a/src/lch.rs +++ b/src/lch.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub}; -use {Color, Alpha, ColorSpace, Mix, Shade, GetHue, Hue, Rgb, Luma, Xyz, Lab, Hsv, Hsl, Saturate, LabHue, clamp}; +use {Color, Alpha, Limited, Mix, Shade, GetHue, Hue, Rgb, Luma, Xyz, Lab, Hsv, Hsl, Saturate, LabHue, clamp}; ///CIE L*C*h° with an alpha component. See the [`Lcha` implementation in `Alpha`](struct.Alpha.html#Lcha). pub type Lcha = Alpha, T>; @@ -52,10 +52,10 @@ impl Alpha, T> { } } -impl ColorSpace for Lch { +impl Limited for Lch { fn is_valid(&self) -> bool { self.l >= T::zero() && self.l <= T::one() && - self.chroma >= T::zero() && self.chroma <= T::from(1.41421356).unwrap() //should include all of L*a*b*, but will also overshoot... + self.chroma >= T::zero() } fn clamp(&self) -> Lch { @@ -66,7 +66,7 @@ impl ColorSpace for Lch { fn clamp_self(&mut self) { self.l = clamp(self.l, T::zero(), T::one()); - self.chroma = clamp(self.chroma, T::zero(), T::from(1.41421356).unwrap()); //should include all of L*a*b*, but will also overshoot... + self.chroma = self.chroma.max(T::zero()) } } @@ -235,3 +235,24 @@ impl From> for Lch { Lab::from(hsl).into() } } + +#[cfg(test)] +mod test { + use Lch; + + #[test] + fn ranges() { + assert_ranges!{ + Lch; + limited { + l: 0.0 => 1.0 + } + limited_min { + chroma: 0.0 => 2.0 + } + unlimited { + hue: -360.0 => 360.0 + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 102fb0f07..a2bf1518a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,152 @@ macro_rules! assert_approx_eq { }) } +//Helper macro for checking ranges and clamping. +#[cfg(test)] +macro_rules! assert_ranges { + (@make_tuple $first:pat, $next:ident,) => (($first, $next)); + + (@make_tuple $first:pat, $next:ident, $($rest:ident,)*) => ( + assert_ranges!(@make_tuple ($first, $next), $($rest,)*) + ); + + ( + $ty:ident; + limited {$($limited:ident: $limited_from:expr => $limited_to:expr),+} + limited_min {$($limited_min:ident: $limited_min_from:expr => $limited_min_to:expr),*} + unlimited {$($unlimited:ident: $unlimited_from:expr => $unlimited_to:expr),*} + ) => ( + { + use std::iter::repeat; + use Limited; + + { + print!("checking below limits ... "); + $( + let from = $limited_from; + let to = $limited_to; + let diff = to - from; + let $limited = (1..11).map(|i| from - (i as f64 / 10.0) * diff); + )+ + + $( + let from = $limited_min_from; + let to = $limited_min_to; + let diff = to - from; + let $limited_min = (1..11).map(|i| from - (i as f64 / 10.0) * diff); + )* + + $( + let from = $unlimited_from; + let to = $unlimited_to; + let diff = to - from; + let $unlimited = (1..11).map(|i| from - (i as f64 / 10.0) * diff); + )* + + for assert_ranges!(@make_tuple (), $($limited,)+ $($limited_min,)* $($unlimited,)* ) in repeat(()) $(.zip($limited))+ $(.zip($limited_min))* $(.zip($unlimited))* { + let c = $ty:: { + $($limited: $limited.into(),)+ + $($limited_min: $limited_min.into(),)* + $($unlimited: $unlimited.into(),)* + }; + let clamped = c.clamp(); + let expected = $ty { + $($limited: $limited_from.into(),)+ + $($limited_min: $limited_min_from.into(),)* + $($unlimited: $unlimited.into(),)* + }; + + assert!(!c.is_valid()); + assert_eq!(clamped, expected); + } + + println!("ok") + } + + { + print!("checking within limits ... "); + $( + let from = $limited_from; + let to = $limited_to; + let diff = to - from; + let $limited = (0..11).map(|i| from + (i as f64 / 10.0) * diff); + )+ + + $( + let from = $limited_min_from; + let to = $limited_min_to; + let diff = to - from; + let $limited_min = (0..11).map(|i| from + (i as f64 / 10.0) * diff); + )* + + $( + let from = $unlimited_from; + let to = $unlimited_to; + let diff = to - from; + let $unlimited = (0..11).map(|i| from + (i as f64 / 10.0) * diff); + )* + + for assert_ranges!(@make_tuple (), $($limited,)+ $($limited_min,)* $($unlimited,)* ) in repeat(()) $(.zip($limited))+ $(.zip($limited_min))* $(.zip($unlimited))* { + let c = $ty:: { + $($limited: $limited.into(),)+ + $($limited_min: $limited_min.into(),)* + $($unlimited: $unlimited.into(),)* + }; + let clamped = c.clamp(); + + assert!(c.is_valid()); + assert_eq!(clamped, c); + } + + println!("ok") + } + + { + print!("checking above limits ... "); + $( + let from = $limited_from; + let to = $limited_to; + let diff = to - from; + let $limited = (1..11).map(|i| to + (i as f64 / 10.0) * diff); + )+ + + $( + let from = $limited_min_from; + let to = $limited_min_to; + let diff = to - from; + let $limited_min = (1..11).map(|i| to + (i as f64 / 10.0) * diff); + )* + + $( + let from = $unlimited_from; + let to = $unlimited_to; + let diff = to - from; + let $unlimited = (1..11).map(|i| to + (i as f64 / 10.0) * diff); + )* + + for assert_ranges!(@make_tuple (), $($limited,)+ $($limited_min,)* $($unlimited,)* ) in repeat(()) $(.zip($limited))+ $(.zip($limited_min))* $(.zip($unlimited))* { + let c = $ty:: { + $($limited: $limited.into(),)+ + $($limited_min: $limited_min.into(),)* + $($unlimited: $unlimited.into(),)* + }; + let clamped = c.clamp(); + let expected = $ty { + $($limited: $limited_to.into(),)+ + $($limited_min: $limited_min.into(),)* + $($unlimited: $unlimited.into(),)* + }; + + assert!(!c.is_valid()); + assert_eq!(clamped, expected); + } + + println!("ok") + } + } + ); +} + pub mod gradient; pub mod pixel; @@ -320,8 +466,8 @@ make_color! { } } -///Common functionality for color spaces. -pub trait ColorSpace { +///A trait for clamping and checking if colors are within their ranges. +pub trait Limited { ///Check if the color's components are within the expected ranges. fn is_valid(&self) -> bool; diff --git a/src/luma.rs b/src/luma.rs index bd9bb5f61..f274d0ca0 100644 --- a/src/luma.rs +++ b/src/luma.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub, Mul, Div}; -use {Color, Alpha, Rgb, Xyz, Lab, Lch, Hsv, Hsl, ColorSpace, Mix, Shade, clamp}; +use {Color, Alpha, Rgb, Xyz, Lab, Lch, Hsv, Hsl, Limited, Mix, Shade, clamp}; ///Linear luminance with an alpha component. See the [`Lumaa` implementation in `Alpha`](struct.Alpha.html#Lumaa). pub type Lumaa = Alpha, T>; @@ -55,7 +55,7 @@ impl Alpha, T> { } } -impl ColorSpace for Luma { +impl Limited for Luma { fn is_valid(&self) -> bool { self.luma >= T::zero() && self.luma <= T::one() } @@ -222,3 +222,20 @@ impl From> for Luma { Rgb::from(hsl).into() } } + +#[cfg(test)] +mod test { + use Luma; + + #[test] + fn ranges() { + assert_ranges!{ + Luma; + limited { + luma: 0.0 => 1.0 + } + limited_min {} + unlimited {} + } + } +} diff --git a/src/pixel/mod.rs b/src/pixel/mod.rs index 4cc91c01e..d3ce8e2d1 100644 --- a/src/pixel/mod.rs +++ b/src/pixel/mod.rs @@ -2,6 +2,8 @@ use num::Float; +use clamp; + pub use self::srgb::Srgb; pub use self::gamma_rgb::GammaRgb; @@ -69,40 +71,44 @@ impl RgbPixel for (f64, f64, f64) { impl RgbPixel for (u8, u8, u8, u8) { fn from_rgba(red: T, green: T, blue: T, alpha: T) -> (u8, u8, u8, u8) { + let c255 = T::from(255.0).unwrap(); ( - (red * T::from(255.0).unwrap()).to_u8().unwrap(), - (green * T::from(255.0).unwrap()).to_u8().unwrap(), - (blue * T::from(255.0).unwrap()).to_u8().unwrap(), - (alpha * T::from(255.0).unwrap()).to_u8().unwrap(), + clamp(red * c255, T::zero(), c255).to_u8().unwrap(), + clamp(green * c255, T::zero(), c255).to_u8().unwrap(), + clamp(blue * c255, T::zero(), c255).to_u8().unwrap(), + clamp(alpha * c255, T::zero(), c255).to_u8().unwrap(), ) } fn to_rgba(&self) -> (T, T, T, T) { let (r, g, b, a) = *self; + let c255 = T::from(255.0).unwrap(); ( - T::from(r).unwrap() / T::from(255.0).unwrap(), - T::from(g).unwrap() / T::from(255.0).unwrap(), - T::from(b).unwrap() / T::from(255.0).unwrap(), - T::from(a).unwrap() / T::from(255.0).unwrap(), + T::from(r).unwrap() / c255, + T::from(g).unwrap() / c255, + T::from(b).unwrap() / c255, + T::from(a).unwrap() / c255, ) } } impl RgbPixel for (u8, u8, u8) { fn from_rgba(red: T, green: T, blue: T, _alpha: T) -> (u8, u8, u8) { + let c255 = T::from(255.0).unwrap(); ( - (red * T::from(255.0).unwrap()).to_u8().unwrap(), - (green * T::from(255.0).unwrap()).to_u8().unwrap(), - (blue * T::from(255.0).unwrap()).to_u8().unwrap(), + clamp(red * c255, T::zero(), c255).to_u8().unwrap(), + clamp(green * c255, T::zero(), c255).to_u8().unwrap(), + clamp(blue * c255, T::zero(), c255).to_u8().unwrap(), ) } fn to_rgba(&self) -> (T, T, T, T) { let (r, g, b) = *self; + let c255 = T::from(255.0).unwrap(); ( - T::from(r).unwrap() / T::from(255.0).unwrap(), - T::from(g).unwrap() / T::from(255.0).unwrap(), - T::from(b).unwrap() / T::from(255.0).unwrap(), + T::from(r).unwrap() / c255, + T::from(g).unwrap() / c255, + T::from(b).unwrap() / c255, T::one(), ) } @@ -149,38 +155,42 @@ impl RgbPixel for [f64; 3] { impl RgbPixel for [u8; 4] { fn from_rgba(red: T, green: T, blue: T, alpha: T) -> [u8; 4] { + let c255 = T::from(255.0).unwrap(); [ - (red * T::from(255.0).unwrap()).to_u8().unwrap(), - (green * T::from(255.0).unwrap()).to_u8().unwrap(), - (blue * T::from(255.0).unwrap()).to_u8().unwrap(), - (alpha * T::from(255.0).unwrap()).to_u8().unwrap(), + clamp(red * c255, T::zero(), c255).to_u8().unwrap(), + clamp(green * c255, T::zero(), c255).to_u8().unwrap(), + clamp(blue * c255, T::zero(), c255).to_u8().unwrap(), + clamp(alpha * c255, T::zero(), c255).to_u8().unwrap(), ] } fn to_rgba(&self) -> (T, T, T, T) { + let c255 = T::from(255.0).unwrap(); ( - T::from(self[0]).unwrap() / T::from(255.0).unwrap(), - T::from(self[1]).unwrap() / T::from(255.0).unwrap(), - T::from(self[2]).unwrap() / T::from(255.0).unwrap(), - T::from(self[3]).unwrap() / T::from(255.0).unwrap(), + T::from(self[0]).unwrap() / c255, + T::from(self[1]).unwrap() / c255, + T::from(self[2]).unwrap() / c255, + T::from(self[3]).unwrap() / c255, ) } } impl RgbPixel for [u8; 3] { fn from_rgba(red: T, green: T, blue: T, _alpha: T) -> [u8; 3] { + let c255 = T::from(255.0).unwrap(); [ - (red * T::from(255.0).unwrap()).to_u8().unwrap(), - (green * T::from(255.0).unwrap()).to_u8().unwrap(), - (blue * T::from(255.0).unwrap()).to_u8().unwrap(), + clamp(red * c255, T::zero(), c255).to_u8().unwrap(), + clamp(green * c255, T::zero(), c255).to_u8().unwrap(), + clamp(blue * c255, T::zero(), c255).to_u8().unwrap(), ] } fn to_rgba(&self) -> (T, T, T, T) { + let c255 = T::from(255.0).unwrap(); ( - T::from(self[0]).unwrap() / T::from(255.0).unwrap(), - T::from(self[1]).unwrap() / T::from(255.0).unwrap(), - T::from(self[2]).unwrap() / T::from(255.0).unwrap(), + T::from(self[0]).unwrap() / c255, + T::from(self[1]).unwrap() / c255, + T::from(self[2]).unwrap() / c255, T::one(), ) } diff --git a/src/rgb.rs b/src/rgb.rs index f55d53856..16e3aa9d6 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub, Mul, Div}; -use {Color, Alpha, Luma, Xyz, Lab, Lch, Hsv, Hsl, ColorSpace, Mix, Shade, GetHue, RgbHue, clamp}; +use {Color, Alpha, Luma, Xyz, Lab, Lch, Hsv, Hsl, Limited, Mix, Shade, GetHue, RgbHue, clamp}; use pixel::{RgbPixel, Srgb, GammaRgb}; ///Linear RGB with an alpha component. See the [`Rgba` implementation in `Alpha`](struct.Alpha.html#Rgba). @@ -124,7 +124,7 @@ impl Alpha, T> { } } -impl ColorSpace for Rgb { +impl Limited for Rgb { fn is_valid(&self) -> bool { self.red >= T::zero() && self.red <= T::one() && self.green >= T::zero() && self.green <= T::one() && @@ -405,3 +405,22 @@ impl From> for Alpha, T> { gamma_rgb.to_linear() } } + +#[cfg(test)] +mod test { + use Rgb; + + #[test] + fn ranges() { + assert_ranges!{ + Rgb; + limited { + red: 0.0 => 1.0, + green: 0.0 => 1.0, + blue: 0.0 => 1.0 + } + limited_min {} + unlimited {} + } + } +} diff --git a/src/xyz.rs b/src/xyz.rs index 2a518e5c2..87ccc8839 100644 --- a/src/xyz.rs +++ b/src/xyz.rs @@ -2,7 +2,7 @@ use num::traits::Float; use std::ops::{Add, Sub, Mul, Div}; -use {Color, Alpha, Rgb, Luma, Lab, Lch, Hsv, Hsl, ColorSpace, Mix, Shade, clamp}; +use {Color, Alpha, Rgb, Luma, Lab, Lch, Hsv, Hsl, Limited, Mix, Shade, clamp}; use tristimulus::{X_N, Y_N, Z_N}; @@ -22,14 +22,14 @@ pub type Xyza = Alpha, T>; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Xyz { ///X is the scale of what can be seen as a response curve for the cone - ///cells in the human eye. It goes from 0.0 to 1.0. + ///cells in the human eye. It goes from 0.0 to 0.95047. pub x: T, ///Y is the luminance of the color, where 0.0 is black and 1.0 is white. pub y: T, ///Z is the scale of what can be seen as the blue stimulation. It goes - ///from 0.0 to 1.0. + ///from 0.0 to 1.08883. pub z: T, } @@ -55,11 +55,11 @@ impl Alpha, T> { } } -impl ColorSpace for Xyz { +impl Limited for Xyz { fn is_valid(&self) -> bool { - self.x >= T::zero() && self.x <= T::one() && - self.y >= T::zero() && self.y <= T::one() && - self.z >= T::zero() && self.z <= T::one() + self.x >= T::zero() && self.x <= T::from(X_N).unwrap() && + self.y >= T::zero() && self.y <= T::from(Y_N).unwrap() && + self.z >= T::zero() && self.z <= T::from(Z_N).unwrap() } fn clamp(&self) -> Xyz { @@ -69,9 +69,9 @@ impl ColorSpace for Xyz { } fn clamp_self(&mut self) { - self.x = clamp(self.x, T::zero(), T::one()); - self.y = clamp(self.y, T::zero(), T::one()); - self.z = clamp(self.z, T::zero(), T::one()); + self.x = clamp(self.x, T::zero(), T::from(X_N).unwrap()); + self.y = clamp(self.y, T::zero(), T::from(Y_N).unwrap()); + self.z = clamp(self.z, T::zero(), T::from(Z_N).unwrap()); } } @@ -275,7 +275,8 @@ fn f_inv(t: T) -> T { #[cfg(test)] mod test { use super::Xyz; - use ::Rgb; + use Rgb; + use tristimulus::{X_N, Y_N, Z_N}; #[test] fn red() { @@ -297,4 +298,18 @@ mod test { let b = Xyz::new(0.18050, 0.07220, 0.95050); assert_approx_eq!(a, b, [x, y, z]); } + + #[test] + fn ranges() { + assert_ranges!{ + Xyz; + limited { + x: 0.0 => X_N, + y: 0.0 => Y_N, + z: 0.0 => Z_N + } + limited_min {} + unlimited {} + } + } }