Skip to content

Commit

Permalink
Add support for HSV
Browse files Browse the repository at this point in the history
  • Loading branch information
jameshurst authored and sharkdp committed Sep 4, 2022
1 parent 95296f9 commit 15897c5
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ pub fn build_cli() -> Command<'static> {
unless something else is printed in addition.")
.possible_values(&["rgb", "rgb-float", "hex",
"hsl", "hsl-hue", "hsl-saturation", "hsl-lightness",
"hsv", "hsv-hue", "hsv-saturation", "hsv-value",
"lch", "lch-lightness", "lch-chroma", "lch-hue",
"lab", "lab-a", "lab-b",
"luminance", "brightness",
Expand Down
4 changes: 4 additions & 0 deletions src/cli/commands/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ impl ColorCommand for FormatCommand {
"hsl-hue" => format!("{:.0}", color.to_hsla().h),
"hsl-saturation" => format!("{:.4}", color.to_hsla().s),
"hsl-lightness" => format!("{:.4}", color.to_hsla().l),
"hsv" => color.to_hsv_string(Format::Spaces),
"hsv-hue" => format!("{:.0}", color.to_hsva().h),
"hsv-saturation" => format!("{:.4}", color.to_hsva().s),
"hsv-value" => format!("{:.4}", color.to_hsva().v),
"lch" => color.to_lch_string(Format::Spaces),
"lch-lightness" => format!("{:.2}", color.to_lch().l),
"lch-chroma" => format!("{:.2}", color.to_lch().c),
Expand Down
147 changes: 147 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ impl Color {
})
}

pub fn from_hsva(hue: Scalar, saturation: Scalar, value: Scalar, alpha: Scalar) -> Color {
Self::from(&HSVA {
h: hue,
s: saturation,
v: value,
alpha,
})
}

pub fn from_hsv(hue: Scalar, saturation: Scalar, value: Scalar) -> Color {
Self::from(&HSVA {
h: hue,
s: saturation,
v: value,
alpha: 1.0,
})
}

/// Create a `Color` from integer RGB values between 0 and 255 and a floating
/// point alpha value between 0.0 and 1.0.
pub fn from_rgba(r: u8, g: u8, b: u8, alpha: Scalar) -> Color {
Expand Down Expand Up @@ -168,6 +186,41 @@ impl Color {
)
}

/// Convert a `Color` to its hue, saturation, value and alpha values. The hue is given
/// in degrees, as a number between 0.0 and 360.0. Saturation, value and alpha are numbers
/// between 0.0 and 1.0.
pub fn to_hsva(&self) -> HSVA {
HSVA::from(self)
}

/// Format the color as a HSV-representation string (`hsva(123, 50.3%, 80.1%, 0.4)`). If the
/// alpha channel is `1.0`, the simplified `hsv()` format will be used instead.
pub fn to_hsv_string(&self, format: Format) -> String {
let hsv = HSVA::from(self);
let space = if format == Format::Spaces { " " } else { "" };
let (a_prefix, a) = if hsv.alpha == 1.0 {
("", "".to_string())
} else {
(
"a",
format!(
",{space}{alpha}",
alpha = MaxPrecision::wrap(3, hsv.alpha),
space = space
),
)
};
format!(
"hsv{a_prefix}({h:.0},{space}{s:.1}%,{space}{v:.1}%{a})",
space = space,
a_prefix = a_prefix,
h = hsv.h,
s = 100.0 * hsv.s,
v = 100.0 * hsv.v,
a = a,
)
}

/// Convert a `Color` to its red, green, blue and alpha values. The RGB values are integers in
/// the range from 0 to 255. The alpha channel is a number between 0.0 and 1.0.
pub fn to_rgba(&self) -> RGBA<u8> {
Expand Down Expand Up @@ -693,6 +746,24 @@ impl From<&HSLA> for Color {
}
}

impl From<&HSVA> for Color {
fn from(color: &HSVA) -> Self {
let lightness = color.v * (1.0 - color.s / 2.0);
let saturation = if lightness > 0.0 && lightness < 1.0 {
(color.v - lightness) / lightness.min(1.0 - lightness)
} else {
0.0
};

Color {
hue: Hue::from(color.h),
saturation: clamp(0.0, 1.0, saturation),
lightness: clamp(0.0, 1.0, lightness),
alpha: clamp(0.0, 1.0, color.alpha),
}
}
}

impl From<&RGBA<u8>> for Color {
fn from(color: &RGBA<u8>) -> Self {
let max_chroma = u8::max(u8::max(color.r, color.g), color.b);
Expand Down Expand Up @@ -981,6 +1052,63 @@ impl fmt::Display for HSLA {
}
}

#[derive(Debug, Clone, PartialEq)]
pub struct HSVA {
pub h: Scalar,
pub s: Scalar,
pub v: Scalar,
pub alpha: Scalar,
}

impl ColorSpace for HSVA {
fn from_color(c: &Color) -> Self {
c.to_hsva()
}

fn into_color(self) -> Color {
Color::from_hsva(self.h, self.s, self.v, self.alpha)
}

fn mix(&self, other: &Self, fraction: Fraction) -> Self {
// make sure that the hue is preserved when mixing with gray colors
let self_hue = if self.s < 0.0001 { other.h } else { self.h };
let other_hue = if other.s < 0.0001 { self.h } else { other.h };

Self {
h: interpolate_angle(self_hue, other_hue, fraction),
s: interpolate(self.s, other.s, fraction),
v: interpolate(self.v, other.v, fraction),
alpha: interpolate(self.alpha, other.alpha, fraction),
}
}
}

impl From<&Color> for HSVA {
fn from(color: &Color) -> Self {
let lightness = color.lightness;

let value = lightness + color.saturation * lightness.min(1.0 - lightness);
let saturation = if value > 0.0 {
2.0 * (1.0 - lightness / value)
} else {
0.0
};

HSVA {
h: color.hue.value(),
s: saturation,
v: value,
alpha: color.alpha,
}
}
}

impl fmt::Display for HSVA {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hsv({h}, {s}, {v})", h = self.h, s = self.s, v = self.v)
}
}

#[derive(Debug, Clone, PartialEq)]
pub struct XYZ {
pub x: Scalar,
Expand Down Expand Up @@ -1431,6 +1559,25 @@ mod tests {
assert_eq!(0xf4230f, Color::from_rgb(0xf4, 0x23, 0x0f).to_u32());
}

#[test]
fn hsva_conversion() {
assert_eq!(
Color::from_hsla(0.0, 1.0, 0.5, 0.5),
Color::from_hsva(0.0, 1.0, 1.0, 0.5)
);

let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let hsva1 = color1.to_hsva();
let color2 = Color::from_hsva(hsva1.h, hsva1.s, hsva1.v, hsva1.alpha);
assert_almost_equal(&color1, &color2);
};

for hue in 0..360 {
roundtrip(Scalar::from(hue), 0.2, 0.8);
}
}

#[test]
fn xyz_conversion() {
assert_eq!(Color::white(), Color::from_xyz(0.9505, 1.0, 1.0890, 1.0));
Expand Down
81 changes: 81 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,23 @@ fn parse_hsl(input: &str) -> IResult<&str, Color> {
Ok((input, c))
}

fn parse_hsv(input: &str) -> IResult<&str, Color> {
let (input, _) = alt((tag("hsv("), tag("hsva(")))(input)?;
let (input, _) = space0(input)?;
let (input, h) = parse_angle(input)?;
let (input, _) = parse_separator(input)?;
let (input, s) = parse_percentage(input)?;
let (input, _) = parse_separator(input)?;
let (input, v) = parse_percentage(input)?;
let (input, alpha) = parse_alpha(input)?;
let (input, _) = space0(input)?;
let (input, _) = char(')')(input)?;

let c = Color::from_hsva(h, s, v, alpha);

Ok((input, c))
}

fn parse_gray(input: &str) -> IResult<&str, Color> {
let (input, _) = tag("gray(")(input)?;
let (input, _) = space0(input)?;
Expand Down Expand Up @@ -251,6 +268,7 @@ pub fn parse_color(input: &str) -> Option<Color> {
all_consuming(parse_numeric_rgb),
all_consuming(parse_percentage_rgb),
all_consuming(parse_hsl),
all_consuming(parse_hsv),
all_consuming(parse_gray),
all_consuming(parse_lab),
all_consuming(parse_lch),
Expand Down Expand Up @@ -407,6 +425,69 @@ fn parse_hsl_syntax() {
assert_eq!(None, parse_color("hsl(280,20%)"));
}

#[test]
fn parse_hsv_syntax() {
assert_eq!(
Some(Color::from_hsv(280.0, 0.2, 0.5)),
parse_color("hsv(280,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(280.0, 0.2, 0.5)),
parse_color("hsv(280deg,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(280.0, 0.2, 0.5)),
parse_color("hsv(280°,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(280.33, 0.123, 0.456)),
parse_color("hsv(280.33001,12.3%,45.6%)")
);
assert_eq!(
Some(Color::from_hsv(280.0, 0.2, 0.5)),
parse_color("hsv( 280 , 20% , 50%)")
);
assert_eq!(
Some(Color::from_hsv(270.0, 0.6, 0.7)),
parse_color("hsv(270 60% 70%)")
);

assert_eq!(
Some(Color::from_hsv(-140.0, 0.2, 0.5)),
parse_color("hsv(-140°,20%,50%)")
);

assert_eq!(
Some(Color::from_hsv(90.0, 0.2, 0.5)),
parse_color("hsv(100grad,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(90.05, 0.2, 0.5)),
parse_color("hsv(1.5708rad,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(90.0, 0.2, 0.5)),
parse_color("hsv(0.25turn,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(45.0, 0.2, 0.5)),
parse_color("hsv(50grad,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(45.0, 0.2, 0.5)),
parse_color("hsv(0.7854rad,20%,50%)")
);
assert_eq!(
Some(Color::from_hsv(45.0, 0.2, 0.5)),
parse_color("hsv(0.125turn,20%,50%)")
);

assert_eq!(None, parse_color("hsv(280,20%,50)"));
assert_eq!(None, parse_color("hsv(280,20,50%)"));
assert_eq!(None, parse_color("hsv(280%,20%,50%)"));
assert_eq!(None, parse_color("hsv(280,20%)"));
}

#[test]
fn parse_gray_syntax() {
assert_eq!(Some(Color::graytone(0.2)), parse_color("gray(0.2)"));
Expand Down

0 comments on commit 15897c5

Please sign in to comment.