use super::{rgb, round_denominator};
use nom::error::Error;
use num::{rational::Ratio, CheckedAdd, CheckedDiv, CheckedMul, Integer, One, Unsigned, Zero};
use parser::parse_sht;
use std::{
convert::TryInto,
fmt::{Display, Formatter, Result as FMTResult},
ops::{Div, Rem},
str::FromStr,
};
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub struct SHT<T: Clone + Integer + Unsigned> {
channel_ratios: ChannelRatios<T>,
shade: Ratio<T>,
tint: Ratio<T>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ChannelRatios<T: Clone + Integer + Unsigned> {
OneBrightestChannel {
primary: ColourChannel,
direction_blend: Option<(ColourChannel, Ratio<T>)>,
},
TwoBrightestChannels {
secondary: SecondaryColour,
},
ThreeBrightestChannels,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ColourChannel {
Red,
Green,
Blue,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum SecondaryColour {
Cyan,
Yellow,
Magenta,
}
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum ParsePropertyError {
ValueErrors(Vec<SHTValueError>),
ParseFailure(Error<String>),
InputRemaining(String),
}
impl From<Error<&str>> for ParsePropertyError {
fn from(value: Error<&str>) -> Self {
let Error { input, code } = value;
ParsePropertyError::ParseFailure(Error::new(input.to_owned(), code))
}
}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SHTValueError {
PrimaryShadeZero,
PrimaryTintOne,
SecondaryShadeZero,
SecondaryTintOne,
DirectionEqualsPrimary,
ValueOutOfBounds,
BlendZero,
BlendOne,
}
impl<T: Clone + Integer + Unsigned> SHT<T> {
pub fn new(
channel_ratios: ChannelRatios<T>,
shade: Ratio<T>,
tint: Ratio<T>,
) -> Result<Self, Vec<SHTValueError>> {
let code = SHT {
channel_ratios,
shade,
tint,
};
match code.normal() {
Ok(code) => Ok(code),
Err(errs) => Err(errs),
}
}
pub fn components(self) -> (ChannelRatios<T>, Ratio<T>, Ratio<T>) {
let Self {
channel_ratios,
shade,
tint,
} = self;
(channel_ratios, shade, tint)
}
fn normal(self) -> Result<Self, Vec<SHTValueError>> {
let Self {
channel_ratios,
shade,
tint,
} = self;
let mut errors = Vec::with_capacity(16); match channel_ratios.clone() {
ChannelRatios::OneBrightestChannel {
primary,
direction_blend,
} => {
if shade.is_zero() {
errors.push(SHTValueError::PrimaryShadeZero);
}
if tint.is_one() {
errors.push(SHTValueError::PrimaryTintOne);
}
if let Some((direction, blend)) = direction_blend {
if direction == primary {
errors.push(SHTValueError::DirectionEqualsPrimary);
}
if blend.is_zero() {
errors.push(SHTValueError::BlendZero);
}
if blend.is_one() {
errors.push(SHTValueError::BlendOne);
}
if blend > Ratio::one() {
errors.push(SHTValueError::ValueOutOfBounds);
}
}
}
ChannelRatios::TwoBrightestChannels { .. } => {
if shade.is_zero() {
errors.push(SHTValueError::SecondaryShadeZero);
}
if tint.is_one() {
errors.push(SHTValueError::SecondaryTintOne);
}
}
ChannelRatios::ThreeBrightestChannels => {}
}
if tint > Ratio::one() {
errors.push(SHTValueError::ValueOutOfBounds);
}
if shade > Ratio::one() {
errors.push(SHTValueError::ValueOutOfBounds);
}
if errors.is_empty() {
Ok(Self {
channel_ratios,
shade,
tint,
})
} else {
Err(errors)
}
}
pub fn to_rgb(self, precision: usize) -> rgb::HexRGB<T>
where
T: Integer + Unsigned + From<u8> + Clone + CheckedMul,
{
let round =
|ratio: Ratio<T>| round_denominator::<T>(ratio, 16.into(), precision, <_>::one());
let (channel_ratios, shade, tint) = self.components();
let (max, min) = (
tint.clone() + shade * (<Ratio<_>>::one() - tint.clone()),
tint,
);
let (red, green, blue) = match channel_ratios {
ChannelRatios::ThreeBrightestChannels => (min.clone(), min.clone(), min),
ChannelRatios::TwoBrightestChannels { secondary } => match secondary {
SecondaryColour::Cyan => (min, max.clone(), max),
SecondaryColour::Yellow => (max.clone(), max, min),
SecondaryColour::Magenta => (max.clone(), min, max),
},
ChannelRatios::OneBrightestChannel {
primary,
direction_blend,
} => {
let (mut red, mut green, mut blue) = (min.clone(), min.clone(), min.clone());
if let Some((direction, blend)) = direction_blend {
let centremost_channel = min.clone() + blend * (max.clone() - min);
match direction {
ColourChannel::Red => red = centremost_channel,
ColourChannel::Green => green = centremost_channel,
ColourChannel::Blue => blue = centremost_channel,
}
};
match primary {
ColourChannel::Red => red = max,
ColourChannel::Green => green = max,
ColourChannel::Blue => blue = max,
};
(red, green, blue)
}
};
rgb::HexRGB::new(round(red), round(green), round(blue))
}
}
impl<T> FromStr for SHT<T>
where
T: Clone + Integer + Unsigned + FromStr + CheckedMul + CheckedAdd + CheckedDiv,
u8: Into<T>,
{
type Err = ParsePropertyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_sht(s)
}
}
fn round(input: &[u8], round_up: bool) -> Vec<u8> {
if round_up {
if let Some((&last, rest)) = input.split_last() {
let rounded_last = last.checked_add(1).unwrap_or(12);
if rounded_last >= 12 {
round(rest, round_up)
} else {
let mut mut_rest = rest.to_vec();
mut_rest.push(rounded_last);
mut_rest
}
} else {
vec![12]
}
} else {
input.to_vec()
}
}
fn duodecimal<T>(mut input: Ratio<T>, precision: usize) -> String
where
T: TryInto<usize> + Integer + Zero + Rem<T, Output = T> + Div<T, Output = T> + Clone,
u8: Into<T>,
{
let half = || Ratio::new(1.into(), 2.into());
let digit_characters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X', 'E'];
let mut digits = Vec::with_capacity(precision);
if input >= <_>::one() {
return "W".to_owned();
}
let mut round_up = false;
for digits_left in (0..precision).rev() {
let scaled = input * Ratio::from_integer(12.into());
input = scaled.fract();
if digits_left.is_zero() {
round_up = input >= half();
}
let integer_part = scaled.to_integer();
let next_digit = match integer_part.try_into() {
Ok(n) if n < 12 => n
.try_into()
.expect("usize < 12 could not be converted to u8"),
_ => 12_u8,
};
digits.push(next_digit);
if input.is_zero() {
break;
}
}
round(&digits, round_up)
.iter()
.map(|&c| digit_characters.get(usize::from(c)).unwrap_or(&'W'))
.collect()
}
impl<T> Display for SHT<T>
where
T: TryInto<usize> + Unsigned + Integer + Clone + Display + One,
u8: Into<T>,
{
fn fmt(&self, formatter: &mut Formatter) -> FMTResult {
let precision = formatter.precision().unwrap_or(2);
let ratio_to_str = |ratio: Ratio<T>| duodecimal(ratio, precision);
let primary_to_str = |primary| match primary {
ColourChannel::Red => "r".to_owned(),
ColourChannel::Green => "g".to_owned(),
ColourChannel::Blue => "b".to_owned(),
};
let secondary_to_str = |secondary| match secondary {
SecondaryColour::Cyan => "c".to_owned(),
SecondaryColour::Yellow => "y".to_owned(),
SecondaryColour::Magenta => "m".to_owned(),
};
let (channel_ratios, shade_ratio, tint_ratio) = self.clone().components();
let tint = (!tint_ratio.is_zero()).then(|| tint_ratio);
let shade = (!shade_ratio.is_one()).then(|| shade_ratio);
let (primary, secondary, direction, blend) = match channel_ratios {
ChannelRatios::OneBrightestChannel {
primary,
direction_blend,
} => {
if let Some((direction, blend)) = direction_blend {
(Some(primary), None, Some(direction), Some(blend))
} else {
(Some(primary), None, None, None)
}
}
ChannelRatios::TwoBrightestChannels { secondary } => {
(None, Some(secondary), None, None)
}
ChannelRatios::ThreeBrightestChannels => (None, None, None, None),
};
write!(
formatter,
"{}{}{}{}{}{}",
shade.map_or_else(String::new, ratio_to_str),
primary.map_or_else(String::new, primary_to_str),
blend.map_or_else(String::new, ratio_to_str),
direction.map_or_else(String::new, primary_to_str),
secondary.map_or_else(String::new, secondary_to_str),
tint.map_or_else(String::new, ratio_to_str)
)
}
}
impl<T> Default for SHT<T>
where
T: Clone + Integer + Unsigned + One + Zero,
{
fn default() -> Self {
SHT {
channel_ratios: ChannelRatios::default(),
shade: Ratio::one(),
tint: Ratio::zero(),
}
}
}
impl<T> Default for ChannelRatios<T>
where
T: Clone + Integer + Unsigned + One + Zero,
{
fn default() -> Self {
ChannelRatios::OneBrightestChannel {
primary: ColourChannel::default(),
direction_blend: None,
}
}
}
impl Default for ColourChannel {
fn default() -> Self {
ColourChannel::Red
}
}
impl Default for SecondaryColour {
fn default() -> Self {
SecondaryColour::Cyan
}
}
#[cfg(test)]
mod tests;
mod parser;