Skip to content

Commit 77b8469

Browse files
committed
fix: wrap hue in HSLColor + add from_degrees
1 parent 0f195ea commit 77b8469

1 file changed

Lines changed: 46 additions & 30 deletions

File tree

plotters/src/style/color.rs

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,39 @@ use serde::{Deserialize, Serialize};
88

99
use std::marker::PhantomData;
1010

11-
/// Any color representation
11+
/// Common trait for all color representations.
1212
pub trait Color {
13-
/// Normalize this color representation to the backend color
1413
fn to_backend_color(&self) -> BackendColor;
1514

16-
/// Convert the RGB representation to the standard RGB tuple
1715
#[inline(always)]
1816
fn rgb(&self) -> (u8, u8, u8) {
1917
self.to_backend_color().rgb
2018
}
2119

22-
/// Get the alpha channel of the color
2320
#[inline(always)]
2421
fn alpha(&self) -> f64 {
2522
self.to_backend_color().alpha
2623
}
2724

28-
/// Mix the color with given opacity
2925
fn mix(&self, value: f64) -> RGBAColor {
3026
let (r, g, b) = self.rgb();
3127
let a = self.alpha() * value;
3228
RGBAColor(r, g, b, a)
3329
}
3430

35-
/// Convert the color into the RGBA color which is internally used by Plotters
3631
fn to_rgba(&self) -> RGBAColor {
3732
let (r, g, b) = self.rgb();
3833
let a = self.alpha();
3934
RGBAColor(r, g, b, a)
4035
}
4136

42-
/// Make a filled style form the color
4337
fn filled(&self) -> ShapeStyle
4438
where
4539
Self: Sized,
4640
{
4741
Into::<ShapeStyle>::into(self).filled()
4842
}
4943

50-
/// Make a shape style with stroke width from a color
5144
fn stroke_width(&self, width: u32) -> ShapeStyle
5245
where
5346
Self: Sized,
@@ -62,10 +55,6 @@ impl<T: Color> Color for &'_ T {
6255
}
6356
}
6457

65-
/// The RGBA representation of the color, Plotters use RGBA as the internal representation
66-
/// of color
67-
///
68-
/// If you want to directly create a RGB color with transparency use [RGBColor::mix]
6958
#[derive(Copy, Clone, PartialEq, Debug, Default)]
7059
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
7160
pub struct RGBAColor(pub u8, pub u8, pub u8, pub f64);
@@ -86,13 +75,11 @@ impl From<RGBColor> for RGBAColor {
8675
}
8776
}
8877

89-
/// A color in the given palette
9078
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
9179
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
9280
pub struct PaletteColor<P: Palette>(usize, PhantomData<P>);
9381

9482
impl<P: Palette> PaletteColor<P> {
95-
/// Pick a color from the palette
9683
pub fn pick(idx: usize) -> PaletteColor<P> {
9784
PaletteColor(idx % P::COLORS.len(), PhantomData)
9885
}
@@ -108,7 +95,6 @@ impl<P: Palette> Color for PaletteColor<P> {
10895
}
10996
}
11097

111-
/// The color described by its RGB value
11298
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
11399
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
114100
pub struct RGBColor(pub u8, pub u8, pub u8);
@@ -128,31 +114,40 @@ impl Color for RGBColor {
128114
}
129115
}
130116
}
117+
131118
impl BackendStyle for RGBColor {
132119
fn color(&self) -> BackendColor {
133120
self.to_backend_color()
134121
}
135122
}
136123

137-
/// The color described by HSL color space
138124
#[derive(Copy, Clone, PartialEq, Debug, Default)]
139125
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
140126
pub struct HSLColor(pub f64, pub f64, pub f64);
141127

128+
impl HSLColor {
129+
#[inline]
130+
pub fn from_degrees(h_deg: f64, s: f64, l: f64) -> Self {
131+
Self(h_deg / 360.0, s, l)
132+
}
133+
}
134+
142135
impl Color for HSLColor {
143136
#[inline(always)]
144137
#[allow(clippy::many_single_char_names)]
145138
fn to_backend_color(&self) -> BackendColor {
146-
let (h, s, l) = (
147-
self.0.clamp(0.0, 1.0),
148-
self.1.clamp(0.0, 1.0),
149-
self.2.clamp(0.0, 1.0),
150-
);
139+
let h = if self.0 > 1.0 {
140+
(self.0 / 360.0).rem_euclid(1.0)
141+
} else {
142+
self.0.rem_euclid(1.0)
143+
};
144+
let s = self.1.clamp(0.0, 1.0);
145+
let l = self.2.clamp(0.0, 1.0);
151146

152147
if s == 0.0 {
153-
let value = (l * 255.0).round() as u8;
148+
let v = (l * 255.0).round() as u8;
154149
return BackendColor {
155-
rgb: (value, value, value),
150+
rgb: (v, v, v),
156151
alpha: 1.0,
157152
};
158153
}
@@ -164,13 +159,8 @@ impl Color for HSLColor {
164159
};
165160
let p = 2.0 * l - q;
166161

167-
let cvt = |mut t| {
168-
if t < 0.0 {
169-
t += 1.0;
170-
}
171-
if t > 1.0 {
172-
t -= 1.0;
173-
}
162+
let cvt = |t: f64| {
163+
let t = t.rem_euclid(1.0);
174164
let value = if t < 1.0 / 6.0 {
175165
p + (q - p) * 6.0 * t
176166
} else if t < 1.0 / 2.0 {
@@ -189,3 +179,29 @@ impl Color for HSLColor {
189179
}
190180
}
191181
}
182+
183+
#[cfg(test)]
184+
mod hue_robustness_tests {
185+
use super::*;
186+
187+
#[test]
188+
fn degrees_passed_directly_should_work_for_common_cases() {
189+
let red = HSLColor(0.0, 1.0, 0.5).to_backend_color().rgb;
190+
assert_eq!(red, (255, 0, 0));
191+
192+
let green = HSLColor(120.0, 1.0, 0.5).to_backend_color().rgb;
193+
assert_eq!(green, (0, 255, 0));
194+
195+
let blue = HSLColor(240.0, 1.0, 0.5).to_backend_color().rgb;
196+
assert_eq!(blue, (0, 0, 255));
197+
}
198+
199+
#[test]
200+
fn from_degrees_and_direct_degrees_are_equivalent() {
201+
for &deg in &[0.0, 30.0, 60.0, 120.0, 180.0, 240.0, 300.0, 360.0] {
202+
let a = HSLColor(deg, 1.0, 0.5).to_backend_color().rgb;
203+
let b = HSLColor::from_degrees(deg, 1.0, 0.5).to_backend_color().rgb;
204+
assert_eq!(a, b);
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)