Use Number as to represent color channels so precision is not lost

This commit is contained in:
ConnorSkees 2020-02-09 18:53:48 -05:00
parent 0ec2c46744
commit f6b27177ba
2 changed files with 67 additions and 70 deletions

View File

@ -10,9 +10,9 @@ mod name;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct Color { pub(crate) struct Color {
red: u8, red: Number,
green: u8, green: Number,
blue: u8, blue: Number,
alpha: Number, alpha: Number,
repr: String, repr: String,
} }
@ -20,32 +20,32 @@ pub(crate) struct Color {
impl Color { impl Color {
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self { pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self {
Color { Color {
red, red: red.into(),
green, green: green.into(),
blue, blue: blue.into(),
alpha: alpha.into(), alpha: alpha.into(),
repr, repr,
} }
} }
pub const fn red(&self) -> u8 { pub fn red(&self) -> Number {
self.red self.red.clone()
} }
pub const fn blue(&self) -> u8 { pub fn blue(&self) -> Number {
self.blue self.blue.clone()
} }
pub const fn green(&self) -> u8 { pub fn green(&self) -> Number {
self.green self.green.clone()
} }
/// Calculate hue from RGBA values /// Calculate hue from RGBA values
/// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
pub fn hue(&self) -> Number { pub fn hue(&self) -> Number {
let red = Number::ratio(self.red, 255); let red = self.red.clone() / Number::from(255);
let green = Number::ratio(self.green, 255); let green = self.green.clone() / Number::from(255);
let blue = Number::ratio(self.blue, 255); let blue = self.blue.clone() / Number::from(255);
let min = red.clone().min(green.clone().min(blue.clone())); let min = red.clone().min(green.clone().min(blue.clone()));
let max = red.clone().max(green.clone().max(blue.clone())); let max = red.clone().max(green.clone().max(blue.clone()));
if &min == &max { if &min == &max {
@ -70,33 +70,29 @@ impl Color {
/// Calculate saturation from RGBA values /// Calculate saturation from RGBA values
/// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
pub fn saturation(&self) -> Number { pub fn saturation(&self) -> Number {
let red = Number::ratio(self.red, 255); let red = self.red.clone() / Number::from(255);
let green = Number::ratio(self.green, 255); let green = self.green.clone() / Number::from(255);
let blue = Number::ratio(self.blue, 255); let blue = self.blue.clone() / Number::from(255);
let mut min = red.clone().min(green.clone().min(blue.clone()));
min = Number::ratio((min * Number::from(100)).to_integer(), 100); let min = red.clone().min(green.clone().min(blue.clone()));
let mut max = red.max(green.max(blue)); let max = red.max(green.max(blue));
max = Number::ratio((max * Number::from(100)).to_integer(), 100);
let luminance = (min.clone() + max.clone()) / Number::from(2);
if &min == &max { if &min == &max {
return Number::from(0); return Number::from(0);
} }
let saturation = if luminance < Number::ratio(1, 2) { let d = max.clone() - min.clone();
(max.clone() - min.clone()) / (max + min) let mm = max + min;
} else { let s = d / if mm > Number::from(1) { Number::from(2) - mm } else { mm };
(max.clone() - min.clone()) / (Number::from(2) - max - min) (s * Number::from(100)).round()
} * Number::from(100);
saturation.round()
} }
/// Calculate luminance from RGBA values /// Calculate luminance from RGBA values
/// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
pub fn lightness(&self) -> Number { pub fn lightness(&self) -> Number {
let red = Number::ratio(self.red, 255); let red = self.red.clone() / Number::from(255);
let green = Number::ratio(self.green, 255); let green = self.green.clone() / Number::from(255);
let blue = Number::ratio(self.blue, 255); let blue = self.blue.clone() / Number::from(255);
let min = red.clone().min(green.clone().min(blue.clone())); let min = red.clone().min(green.clone().min(blue.clone()));
let max = red.max(green.max(blue)); let max = red.max(green.max(blue));
(((min + max) / Number::from(2)) * Number::from(100)).round() (((min + max) / Number::from(2)) * Number::from(100)).round()
@ -114,7 +110,6 @@ impl Color {
mut luminance: Number, mut luminance: Number,
mut alpha: Number, mut alpha: Number,
) -> Self { ) -> Self {
macro_rules! clamp { macro_rules! clamp {
($c:ident, $min:literal, $max:literal) => { ($c:ident, $min:literal, $max:literal) => {
if $c > Number::from($max) { if $c > Number::from($max) {
@ -122,7 +117,7 @@ impl Color {
} else if $c < Number::from($min) { } else if $c < Number::from($min) {
$c = Number::from($min) $c = Number::from($min)
} }
} };
} }
clamp!(hue, 0, 360); clamp!(hue, 0, 360);
@ -136,14 +131,11 @@ impl Color {
} else { } else {
luminance luminance
}; };
let val = (luminance.clone() * Number::from(255)) let val = luminance.clone() * Number::from(255);
.to_integer() let repr = repr(&val, &val, &val, &alpha);
.to_u8()
.unwrap();
let repr = repr(val, val, val, &alpha);
return Color { return Color {
red: val, red: val.clone(),
green: val, green: val.clone(),
blue: val, blue: val,
alpha: Number::from(alpha), alpha: Number::from(alpha),
repr, repr,
@ -176,7 +168,7 @@ impl Color {
macro_rules! channel { macro_rules! channel {
($name:ident, $temp:ident, $temp1:ident, $temp2:ident) => { ($name:ident, $temp:ident, $temp1:ident, $temp2:ident) => {
let $name = (if Number::from(6) * $temp.clone() < Number::from(1) { let $name = if Number::from(6) * $temp.clone() < Number::from(1) {
$temp2.clone() $temp2.clone()
+ ($temp1.clone() - $temp2.clone()) * Number::from(6) * $temp.clone() + ($temp1.clone() - $temp2.clone()) * Number::from(6) * $temp.clone()
} else if Number::from(2) * $temp.clone() < Number::from(1) { } else if Number::from(2) * $temp.clone() < Number::from(1) {
@ -188,11 +180,7 @@ impl Color {
* Number::from(6) * Number::from(6)
} else { } else {
$temp2.clone() $temp2.clone()
} * Number::from(255)) } * Number::from(255);
.round()
.to_integer()
.to_u8()
.expect("expected channel to fit inside u8");
}; };
} }
@ -200,7 +188,7 @@ impl Color {
channel!(green, temporary_g, temporary_1, temporary_2); channel!(green, temporary_g, temporary_1, temporary_2);
channel!(blue, temporary_b, temporary_1, temporary_2); channel!(blue, temporary_b, temporary_1, temporary_2);
let repr = repr(red, green, blue, &alpha); let repr = repr(&red, &green, &blue, &alpha);
Color { Color {
red, red,
green, green,
@ -214,11 +202,11 @@ impl Color {
macro_rules! clamp { macro_rules! clamp {
($channel:ident) => { ($channel:ident) => {
let $channel = if $channel > Number::from(255) { let $channel = if $channel > Number::from(255) {
255_u8 Number::from(255)
} else if $channel < Number::from(0) { } else if $channel < Number::from(0) {
0_u8 Number::from(0)
} else { } else {
$channel.round().to_integer().to_u8().unwrap() $channel
}; };
}; };
} }
@ -235,7 +223,7 @@ impl Color {
alpha alpha
}; };
let repr = repr(red, green, blue, &alpha); let repr = repr(&red, &green, &blue, &alpha);
Color { Color {
red, red,
green, green,
@ -253,22 +241,10 @@ impl Color {
} else { } else {
weight weight
}; };
let red = (Number::from(std::u8::MAX) - (Number::from(self.red) * weight.clone())) let red = Number::from(std::u8::MAX) - (Number::from(self.red.clone()) * weight.clone());
.round() let green = Number::from(std::u8::MAX) - (Number::from(self.green.clone()) * weight.clone());
.to_integer() let blue = Number::from(std::u8::MAX) - (Number::from(self.blue.clone()) * weight);
.to_u8() let repr = repr(&red, &green, &blue, &self.alpha);
.unwrap();
let green = (Number::from(std::u8::MAX) - (Number::from(self.green) * weight.clone()))
.round()
.to_integer()
.to_u8()
.unwrap();
let blue = (Number::from(std::u8::MAX) - (Number::from(self.blue) * weight))
.round()
.to_integer()
.to_u8()
.unwrap();
let repr = repr(red, green, blue, &self.alpha);
Color { Color {
red, red,
green, green,
@ -280,7 +256,23 @@ impl Color {
} }
/// Get the proper representation from RGBA values /// Get the proper representation from RGBA values
fn repr(red: u8, green: u8, blue: u8, alpha: &Number) -> String { fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
macro_rules! into_u8 {
($channel:ident) => {
let $channel = if $channel > &Number::from(255) {
255_u8
} else if $channel < &Number::from(0) {
0_u8
} else {
$channel.clone().round().to_integer().to_u8().unwrap()
};
};
}
into_u8!(red);
into_u8!(green);
into_u8!(blue);
if alpha < &Number::from(1) { if alpha < &Number::from(1) {
format!("rgba({}, {}, {}, {})", red, green, blue, alpha) format!("rgba({}, {}, {}, {})", red, green, blue, alpha)
} else if let Ok(c) = ColorName::try_from([red, green, blue]) { } else if let Ok(c) = ColorName::try_from([red, green, blue]) {

View File

@ -171,6 +171,11 @@ test!(
"$a: hsl(193, 67%, 28%);\n\na {\n color: saturation($a);\n}\n", "$a: hsl(193, 67%, 28%);\n\na {\n color: saturation($a);\n}\n",
"a {\n color: 67%;\n}\n" "a {\n color: 67%;\n}\n"
); );
test!(
saturation_2,
"$a: hsl(1, 1, 10);\n\na {\n color: saturation($a);\n}\n",
"a {\n color: 1%;\n}\n"
);
test!( test!(
lightness, lightness,
"$a: hsl(193, 67%, 28%);\n\na {\n color: lightness($a);\n}\n", "$a: hsl(193, 67%, 28%);\n\na {\n color: lightness($a);\n}\n",