use std::convert::TryFrom;
use std::fmt::{self, Display};
use crate::value::Number;
pub(crate) use name::ColorName;
use num_traits::cast::ToPrimitive;
mod name;
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct Color {
red: Number,
green: Number,
blue: Number,
alpha: Number,
repr: String,
}
impl Color {
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self {
Color {
red: red.into(),
green: green.into(),
blue: blue.into(),
alpha: alpha.into(),
repr,
}
}
pub fn red(&self) -> Number {
self.red.clone()
}
pub fn blue(&self) -> Number {
self.blue.clone()
}
pub fn green(&self) -> Number {
self.green.clone()
}
/// Calculate hue from RGBA values
/// Algorithm adapted from
pub fn hue(&self) -> Number {
let red = self.red.clone() / Number::from(255);
let green = self.green.clone() / Number::from(255);
let blue = self.blue.clone() / Number::from(255);
let min = red.clone().min(green.clone().min(blue.clone()));
let max = red.clone().max(green.clone().max(blue.clone()));
if min == max {
return Number::from(0);
}
let mut hue = if red == max {
(green - blue) / (max - min)
} else if green == max {
Number::from(2) + (blue - red) / (max - min)
} else {
Number::from(4) + (red - green) / (max - min)
};
if hue < Number::from(0) {
hue += Number::from(360);
}
(hue * Number::from(60)).round()
}
/// Calculate saturation from RGBA values
/// Algorithm adapted from
pub fn saturation(&self) -> Number {
let red = self.red.clone() / Number::from(255);
let green = self.green.clone() / Number::from(255);
let blue = self.blue.clone() / Number::from(255);
let min = red.clone().min(green.clone().min(blue.clone()));
let max = red.max(green.max(blue));
if min == max {
return Number::from(0);
}
let d = max.clone() - min.clone();
let mm = max + min;
let s = d / if mm > Number::from(1) {
Number::from(2) - mm
} else {
mm
};
(s * Number::from(100)).round()
}
/// Calculate luminance from RGBA values
/// Algorithm adapted from
pub fn lightness(&self) -> Number {
let red = self.red.clone() / Number::from(255);
let green = self.green.clone() / Number::from(255);
let blue = self.blue.clone() / Number::from(255);
let min = red.clone().min(green.clone().min(blue.clone()));
let max = red.max(green.max(blue));
(((min + max) / Number::from(2)) * Number::from(100)).round()
}
pub fn adjust_hue(&self, degrees: Number) -> Self {
let hue = self.hue();
let saturation = Number::ratio(self.saturation(), 100);
let luminance = Number::ratio(self.lightness(), 100);
Color::from_hsla(hue + degrees, saturation, luminance, self.alpha())
}
pub fn alpha(&self) -> Number {
self.alpha.clone()
}
/// Create RGBA representation from HSLA values
/// Algorithm adapted from
pub fn from_hsla(
mut hue: Number,
mut saturation: Number,
mut luminance: Number,
mut alpha: Number,
) -> Self {
macro_rules! clamp {
($c:ident, $min:literal, $max:literal) => {
if $c > Number::from($max) {
$c = Number::from($max)
} else if $c < Number::from($min) {
$c = Number::from($min)
}
};
}
clamp!(hue, 0, 360);
clamp!(saturation, 0, 1);
clamp!(luminance, 0, 1);
clamp!(alpha, 0, 1);
if saturation.clone() == Number::from(0) {
let luminance = if luminance > Number::from(100) {
Number::from(100)
} else {
luminance
};
let val = luminance * Number::from(255);
let repr = repr(&val, &val, &val, &alpha);
return Color {
red: val.clone(),
green: val.clone(),
blue: val,
alpha,
repr,
};
}
let temporary_1 = if luminance.clone() < Number::ratio(1, 2) {
luminance.clone() * (Number::from(1) + saturation)
} else {
luminance.clone() + saturation.clone() - luminance.clone() * saturation
};
let temporary_2 = Number::from(2) * luminance - temporary_1.clone();
hue /= Number::from(360);
let mut temporary_r = hue.clone() + Number::ratio(1, 3);
let mut temporary_g = hue.clone();
let mut temporary_b = hue - Number::ratio(1, 3);
macro_rules! clamp_temp {
($temp:ident) => {
if $temp > Number::from(1) {
$temp -= Number::from(1);
} else if $temp < Number::from(0) {
$temp += Number::from(1);
}
};
}
clamp_temp!(temporary_r);
clamp_temp!(temporary_g);
clamp_temp!(temporary_b);
macro_rules! channel {
($name:ident, $temp:ident, $temp1:ident, $temp2:ident) => {
let $name = if Number::from(6) * $temp.clone() < Number::from(1) {
$temp2.clone()
+ ($temp1.clone() - $temp2.clone()) * Number::from(6) * $temp.clone()
} else if Number::from(2) * $temp.clone() < Number::from(1) {
$temp1.clone()
} else if Number::from(3) * $temp.clone() < Number::from(2) {
$temp2.clone()
+ ($temp1.clone() - $temp2.clone())
* (Number::ratio(2, 3) - $temp)
* Number::from(6)
} else {
$temp2.clone()
} * Number::from(255);
};
}
channel!(red, temporary_r, temporary_1, temporary_2);
channel!(green, temporary_g, temporary_1, temporary_2);
channel!(blue, temporary_b, temporary_1, temporary_2);
let repr = repr(&red, &green, &blue, &alpha);
Color {
red,
green,
blue,
alpha,
repr,
}
}
pub fn from_rgba(red: Number, green: Number, blue: Number, alpha: Number) -> Self {
macro_rules! clamp {
($channel:ident) => {
let $channel = if $channel > Number::from(255) {
Number::from(255)
} else if $channel < Number::from(0) {
Number::from(0)
} else {
$channel
};
};
}
clamp!(red);
clamp!(green);
clamp!(blue);
let alpha = if alpha > Number::from(1) {
Number::from(1)
} else if alpha < Number::from(0) {
Number::from(0)
} else {
alpha
};
let repr = repr(&red, &green, &blue, &alpha);
Color {
red,
green,
blue,
alpha,
repr,
}
}
pub fn invert(&self, weight: Number) -> Self {
let weight = if weight > Number::from(1) {
Number::from(1)
} else if weight < Number::from(0) {
Number::from(0)
} else {
weight
};
let red = Number::from(u8::max_value()) - self.red.clone() * weight.clone();
let green = Number::from(u8::max_value()) - self.green.clone() * weight.clone();
let blue = Number::from(u8::max_value()) - self.blue.clone() * weight;
let repr = repr(&red, &green, &blue, &self.alpha);
Color {
red,
green,
blue,
alpha: self.alpha.clone(),
repr,
}
}
}
/// Get the proper representation from RGBA values
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) {
format!("rgba({}, {}, {}, {})", red, green, blue, alpha)
} else if let Ok(c) = ColorName::try_from([red, green, blue]) {
format!("{}", c)
} else {
format!("#{:0>2x}{:0>2x}{:0>2x}", red, green, blue)
}
}
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.repr)
}
}