Skip to content

Commit 2369300

Browse files
committed
fix: robust HSL hue handling + add from_degrees and tests
1 parent 77b8469 commit 2369300

1 file changed

Lines changed: 30 additions & 6 deletions

File tree

plotters/src/style/color.rs

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

99
use std::marker::PhantomData;
1010

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

16+
/// Convert the RGB representation to the standard RGB tuple
1517
#[inline(always)]
1618
fn rgb(&self) -> (u8, u8, u8) {
1719
self.to_backend_color().rgb
1820
}
1921

22+
/// Get the alpha channel of the color
2023
#[inline(always)]
2124
fn alpha(&self) -> f64 {
2225
self.to_backend_color().alpha
2326
}
2427

28+
/// Mix the color with given opacity
2529
fn mix(&self, value: f64) -> RGBAColor {
2630
let (r, g, b) = self.rgb();
2731
let a = self.alpha() * value;
2832
RGBAColor(r, g, b, a)
2933
}
3034

35+
/// Convert the color into the RGBA color which is internally used by Plotters
3136
fn to_rgba(&self) -> RGBAColor {
3237
let (r, g, b) = self.rgb();
3338
let a = self.alpha();
3439
RGBAColor(r, g, b, a)
3540
}
3641

42+
/// Make a filled style form the color
3743
fn filled(&self) -> ShapeStyle
3844
where
3945
Self: Sized,
4046
{
4147
Into::<ShapeStyle>::into(self).filled()
4248
}
4349

50+
/// Make a shape style with stroke width from a color
4451
fn stroke_width(&self, width: u32) -> ShapeStyle
4552
where
4653
Self: Sized,
@@ -55,6 +62,10 @@ impl<T: Color> Color for &'_ T {
5562
}
5663
}
5764

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]
5869
#[derive(Copy, Clone, PartialEq, Debug, Default)]
5970
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
6071
pub struct RGBAColor(pub u8, pub u8, pub u8, pub f64);
@@ -75,11 +86,13 @@ impl From<RGBColor> for RGBAColor {
7586
}
7687
}
7788

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

8294
impl<P: Palette> PaletteColor<P> {
95+
/// Pick a color from the palette
8396
pub fn pick(idx: usize) -> PaletteColor<P> {
8497
PaletteColor(idx % P::COLORS.len(), PhantomData)
8598
}
@@ -95,6 +108,7 @@ impl<P: Palette> Color for PaletteColor<P> {
95108
}
96109
}
97110

111+
/// The color described by its RGB value
98112
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
99113
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
100114
pub struct RGBColor(pub u8, pub u8, pub u8);
@@ -114,13 +128,13 @@ impl Color for RGBColor {
114128
}
115129
}
116130
}
117-
118131
impl BackendStyle for RGBColor {
119132
fn color(&self) -> BackendColor {
120133
self.to_backend_color()
121134
}
122135
}
123136

137+
/// The color described by HSL color space
124138
#[derive(Copy, Clone, PartialEq, Debug, Default)]
125139
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
126140
pub struct HSLColor(pub f64, pub f64, pub f64);
@@ -136,18 +150,23 @@ impl Color for HSLColor {
136150
#[inline(always)]
137151
#[allow(clippy::many_single_char_names)]
138152
fn to_backend_color(&self) -> BackendColor {
153+
// Hue: do not clamp; normalize
154+
// - If >1.0, treat as degrees (divide by 360), then wrap to [0,1)
155+
// - Else, wrap value to [0,1) to accept negatives
139156
let h = if self.0 > 1.0 {
140157
(self.0 / 360.0).rem_euclid(1.0)
141158
} else {
142159
self.0.rem_euclid(1.0)
143160
};
161+
162+
// Saturation & lightness remain clamped to valid ranges
144163
let s = self.1.clamp(0.0, 1.0);
145164
let l = self.2.clamp(0.0, 1.0);
146165

147166
if s == 0.0 {
148-
let v = (l * 255.0).round() as u8;
167+
let value = (l * 255.0).round() as u8;
149168
return BackendColor {
150-
rgb: (v, v, v),
169+
rgb: (value, value, value),
151170
alpha: 1.0,
152171
};
153172
}
@@ -159,8 +178,13 @@ impl Color for HSLColor {
159178
};
160179
let p = 2.0 * l - q;
161180

162-
let cvt = |t: f64| {
163-
let t = t.rem_euclid(1.0);
181+
let cvt = |mut t| {
182+
if t < 0.0 {
183+
t += 1.0;
184+
}
185+
if t > 1.0 {
186+
t -= 1.0;
187+
}
164188
let value = if t < 1.0 / 6.0 {
165189
p + (q - p) * 6.0 * t
166190
} else if t < 1.0 / 2.0 {

0 commit comments

Comments
 (0)