-
-
Notifications
You must be signed in to change notification settings - Fork 115
feat: add support for transparency #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c92b093
d536258
1e480f0
4a33aca
c92072c
a8b209c
c5aba8d
abd0b3f
74c9ec7
dae9309
5be38d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,11 +8,11 @@ pub mod parser; | |
pub mod random; | ||
mod types; | ||
|
||
use std::fmt; | ||
use std::{fmt, str::FromStr}; | ||
|
||
use colorspace::ColorSpace; | ||
pub use helper::Fraction; | ||
use helper::{clamp, interpolate, interpolate_angle, mod_positive}; | ||
use helper::{clamp, interpolate, interpolate_angle, mod_positive, MaxPrecision}; | ||
use types::{Hue, Scalar}; | ||
|
||
/// The representation of a color. | ||
|
@@ -141,14 +141,30 @@ impl Color { | |
HSLA::from(self) | ||
} | ||
|
||
/// Format the color as a HSL-representation string (`hsl(123, 50.3%, 80.1%)`). | ||
/// Format the color as a HSL-representation string (`hsla(123, 50.3%, 80.1%, 0.4)`). If the | ||
/// alpha channel is `1.0`, the simplified `hsl()` format will be used instead. | ||
pub fn to_hsl_string(&self, format: Format) -> String { | ||
let space = if format == Format::Spaces { " " } else { "" }; | ||
let (a_prefix, a) = if self.alpha == 1.0 { | ||
("", "".to_string()) | ||
} else { | ||
( | ||
"a", | ||
format!( | ||
",{space}{alpha}", | ||
alpha = MaxPrecision::wrap(3, self.alpha), | ||
space = space | ||
), | ||
) | ||
}; | ||
format!( | ||
"hsl({:.0},{space}{:.1}%,{space}{:.1}%)", | ||
self.hue.value(), | ||
100.0 * self.saturation, | ||
100.0 * self.lightness, | ||
space = if format == Format::Spaces { " " } else { "" } | ||
"hsl{a_prefix}({h:.0},{space}{s:.1}%,{space}{l:.1}%{a})", | ||
space = space, | ||
a_prefix = a_prefix, | ||
h = self.hue.value(), | ||
s = 100.0 * self.saturation, | ||
l = 100.0 * self.lightness, | ||
a = a, | ||
) | ||
} | ||
|
||
|
@@ -158,15 +174,31 @@ impl Color { | |
RGBA::<u8>::from(self) | ||
} | ||
|
||
/// Format the color as a RGB-representation string (`rgb(255, 127, 0)`). | ||
/// Format the color as a RGB-representation string (`rgba(255, 127, 0, 0.5)`). If the alpha channel | ||
/// is `1.0`, the simplified `rgb()` format will be used instead. | ||
pub fn to_rgb_string(&self, format: Format) -> String { | ||
let rgba = RGBA::<u8>::from(self); | ||
let space = if format == Format::Spaces { " " } else { "" }; | ||
let (a_prefix, a) = if self.alpha == 1.0 { | ||
("", "".to_string()) | ||
} else { | ||
( | ||
"a", | ||
format!( | ||
",{space}{alpha}", | ||
alpha = MaxPrecision::wrap(3, rgba.alpha), | ||
space = space | ||
), | ||
) | ||
}; | ||
format!( | ||
"rgb({r},{space}{g},{space}{b})", | ||
"rgb{a_prefix}({r},{space}{g},{space}{b}{a})", | ||
space = space, | ||
a_prefix = a_prefix, | ||
r = rgba.r, | ||
g = rgba.g, | ||
b = rgba.b, | ||
space = if format == Format::Spaces { " " } else { "" } | ||
a = a, | ||
) | ||
} | ||
|
||
|
@@ -189,27 +221,49 @@ impl Color { | |
) | ||
} | ||
|
||
/// Format the color as a floating point RGB-representation string (`rgb(1.0, 0.5, 0)`). | ||
/// Format the color as a floating point RGB-representation string (`rgb(1.0, 0.5, 0)`). If the alpha channel | ||
/// is `1.0`, the simplified `rgb()` format will be used instead. | ||
pub fn to_rgb_float_string(&self, format: Format) -> String { | ||
let rgba = RGBA::<f64>::from(self); | ||
let space = if format == Format::Spaces { " " } else { "" }; | ||
let (a_prefix, a) = if self.alpha == 1.0 { | ||
("", "".to_string()) | ||
} else { | ||
( | ||
"a", | ||
format!( | ||
",{space}{alpha}", | ||
alpha = MaxPrecision::wrap(3, rgba.alpha), | ||
space = space | ||
), | ||
) | ||
}; | ||
format!( | ||
"rgb({r:.3},{space}{g:.3},{space}{b:.3})", | ||
"rgb{a_prefix}({r:.3},{space}{g:.3},{space}{b:.3}{a})", | ||
space = space, | ||
a_prefix = a_prefix, | ||
r = rgba.r, | ||
g = rgba.g, | ||
b = rgba.b, | ||
space = if format == Format::Spaces { " " } else { "" } | ||
a = a, | ||
) | ||
} | ||
|
||
/// Format the color as a RGB-representation string (`#fc0070`). | ||
/// Format the color as a RGB-representation string (`#fc0070`). The output will contain 6 hex | ||
/// digits if the alpha channel is `1.0`, or 8 hex digits otherwise. | ||
pub fn to_rgb_hex_string(&self, leading_hash: bool) -> String { | ||
let rgba = self.to_rgba(); | ||
format!( | ||
"{}{:02x}{:02x}{:02x}", | ||
"{}{:02x}{:02x}{:02x}{}", | ||
if leading_hash { "#" } else { "" }, | ||
rgba.r, | ||
rgba.g, | ||
rgba.b | ||
rgba.b, | ||
if rgba.alpha == 1.0 { | ||
"".to_string() | ||
} else { | ||
format!("{:02x}", (rgba.alpha * 255.).round() as u8) | ||
} | ||
) | ||
} | ||
|
||
|
@@ -249,15 +303,26 @@ impl Color { | |
Lab::from(self) | ||
} | ||
|
||
/// Format the color as a Lab-representation string (`Lab(41, 83, -93)`). | ||
/// Format the color as a Lab-representation string (`Lab(41, 83, -93, 0.5)`). If the alpha channel | ||
/// is `1.0`, it won't be included in the output. | ||
pub fn to_lab_string(&self, format: Format) -> String { | ||
let lab = Lab::from(self); | ||
let space = if format == Format::Spaces { " " } else { "" }; | ||
format!( | ||
"Lab({l:.0},{space}{a:.0},{space}{b:.0})", | ||
"Lab({l:.0},{space}{a:.0},{space}{b:.0}{alpha})", | ||
l = lab.l, | ||
a = lab.a, | ||
b = lab.b, | ||
space = if format == Format::Spaces { " " } else { "" } | ||
space = space, | ||
alpha = if self.alpha == 1.0 { | ||
"".to_string() | ||
} else { | ||
format!( | ||
",{space}{alpha}", | ||
alpha = MaxPrecision::wrap(3, self.alpha), | ||
space = space | ||
) | ||
} | ||
) | ||
} | ||
|
||
|
@@ -268,15 +333,26 @@ impl Color { | |
LCh::from(self) | ||
} | ||
|
||
/// Format the color as a LCh-representation string (`LCh(0.3, 0.2, 0.1)`). | ||
/// Format the color as a LCh-representation string (`LCh(0.3, 0.2, 0.1, 0.5)`). If the alpha channel | ||
/// is `1.0`, it won't be included in the output. | ||
pub fn to_lch_string(&self, format: Format) -> String { | ||
let lch = LCh::from(self); | ||
let space = if format == Format::Spaces { " " } else { "" }; | ||
format!( | ||
"LCh({l:.0},{space}{c:.0},{space}{h:.0})", | ||
"LCh({l:.0},{space}{c:.0},{space}{h:.0}{alpha})", | ||
l = lch.l, | ||
c = lch.c, | ||
h = lch.h, | ||
space = if format == Format::Spaces { " " } else { "" } | ||
space = space, | ||
alpha = if self.alpha == 1.0 { | ||
"".to_string() | ||
} else { | ||
format!( | ||
",{space}{alpha}", | ||
alpha = MaxPrecision::wrap(3, self.alpha), | ||
space = space | ||
) | ||
} | ||
) | ||
} | ||
|
||
|
@@ -547,6 +623,36 @@ impl Color { | |
.mix(&C::from_color(other), fraction) | ||
.into_color() | ||
} | ||
|
||
/// Alpha composite two colors, placing the second over the first. | ||
pub fn composite(&self, source: &Color) -> Color { | ||
superhawk610 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let backdrop = self.to_rgba(); | ||
let source = source.to_rgba(); | ||
|
||
// Composite A over B (see https://en.wikipedia.org/wiki/Alpha_compositing) | ||
// | ||
// αo = αa + αb(1 - αa) | ||
// | ||
// Ca * αa + Cb * αb(1 - αa) | ||
// Co = ------------------------- | ||
// αo | ||
// | ||
// αo: output alpha | ||
// αa, αb: A/B alpha | ||
// Co: output color | ||
// Ca, Cb: A/B color | ||
// | ||
fn composite_channel(c_a: u8, a_a: f64, c_b: u8, a_b: f64, a_o: f64) -> u8 { | ||
((c_a as f64 * a_a + c_b as f64 * a_b * (1.0 - a_a)) / a_o).floor() as u8 | ||
} | ||
|
||
let a = source.alpha + backdrop.alpha * (1.0 - source.alpha); | ||
let r = composite_channel(source.r, source.alpha, backdrop.r, backdrop.alpha, a); | ||
let g = composite_channel(source.g, source.alpha, backdrop.g, backdrop.alpha, a); | ||
let b = composite_channel(source.b, source.alpha, backdrop.b, backdrop.alpha, a); | ||
|
||
Color::from_rgba(r, g, b, a) | ||
} | ||
} | ||
|
||
// by default Colors will be printed into HSLA fromat | ||
|
@@ -568,6 +674,14 @@ impl PartialEq for Color { | |
} | ||
} | ||
|
||
impl FromStr for Color { | ||
type Err = &'static str; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
parser::parse_color(s).ok_or("invalid color string") | ||
} | ||
} | ||
|
||
impl From<&HSLA> for Color { | ||
fn from(color: &HSLA) -> Self { | ||
Color { | ||
|
@@ -1669,4 +1783,19 @@ mod tests { | |
let c3 = Color::from_rgb(143, 111, 76); | ||
assert_eq!("cmyk(0, 22, 47, 44)", c3.to_cmyk_string(Format::Spaces)); | ||
} | ||
|
||
#[test] | ||
fn alpha_roundtrip_hex_to_decimal() { | ||
// We use a max of 3 decimal places when displaying RGB floating point | ||
// alpha values. This test insures that is sufficient to "roundtrip" | ||
// from hex (0 < n < 255) to float (0 < n < 1) and back again, | ||
// e.g. hex `80` is float `0.502`, which parses to hex `80`, and so on. | ||
for alpha_int in 0..255 { | ||
let hex_string = format!("#000000{:02x}", alpha_int); | ||
let parsed_from_hex = hex_string.parse::<Color>().unwrap(); | ||
let rgba_string = parsed_from_hex.to_rgb_float_string(Format::Spaces); | ||
let parsed_from_rgba = rgba_string.parse::<Color>().unwrap(); | ||
assert_eq!(hex_string, parsed_from_rgba.to_rgb_hex_string(true)); | ||
} | ||
Comment on lines
+1787
to
+1799
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, I think this should provide proper coverage. This asserts that:
For example, I also threw in an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome, thank you! |
||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.