Skip to content

Commit ea990af

Browse files
committed
fix: clarify HSL hue normalization
1 parent 2369300 commit ea990af

1 file changed

Lines changed: 24 additions & 19 deletions

File tree

plotters/src/style/color.rs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,24 +140,21 @@ impl BackendStyle for RGBColor {
140140
pub struct HSLColor(pub f64, pub f64, pub f64);
141141

142142
impl HSLColor {
143+
/// Creates an `HSLColor` from degrees, wrapping into `[0, 360)` before normalizing.
144+
/// Prefer this helper when specifying hue in degrees.
143145
#[inline]
144146
pub fn from_degrees(h_deg: f64, s: f64, l: f64) -> Self {
145-
Self(h_deg / 360.0, s, l)
147+
Self(h_deg.rem_euclid(360.0) / 360.0, s, l)
146148
}
147149
}
148150

149151
impl Color for HSLColor {
150152
#[inline(always)]
151153
#[allow(clippy::many_single_char_names)]
152154
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
156-
let h = if self.0 > 1.0 {
157-
(self.0 / 360.0).rem_euclid(1.0)
158-
} else {
159-
self.0.rem_euclid(1.0)
160-
};
155+
// Hue is expected normalized in [0,1); wrap to keep negative or slightly
156+
// out-of-range inputs usable, but do not reinterpret raw degrees.
157+
let h = self.0.rem_euclid(1.0);
161158

162159
// Saturation & lightness remain clamped to valid ranges
163160
let s = self.1.clamp(0.0, 1.0);
@@ -209,23 +206,31 @@ mod hue_robustness_tests {
209206
use super::*;
210207

211208
#[test]
212-
fn degrees_passed_directly_should_work_for_common_cases() {
213-
let red = HSLColor(0.0, 1.0, 0.5).to_backend_color().rgb;
209+
fn degrees_passed_via_helper_should_work_for_common_cases() {
210+
let red = HSLColor::from_degrees(0.0, 1.0, 0.5).to_backend_color().rgb;
214211
assert_eq!(red, (255, 0, 0));
215212

216-
let green = HSLColor(120.0, 1.0, 0.5).to_backend_color().rgb;
213+
let green = HSLColor::from_degrees(120.0, 1.0, 0.5).to_backend_color().rgb;
217214
assert_eq!(green, (0, 255, 0));
218215

219-
let blue = HSLColor(240.0, 1.0, 0.5).to_backend_color().rgb;
216+
let blue = HSLColor::from_degrees(240.0, 1.0, 0.5).to_backend_color().rgb;
220217
assert_eq!(blue, (0, 0, 255));
221218
}
222219

223220
#[test]
224-
fn from_degrees_and_direct_degrees_are_equivalent() {
225-
for &deg in &[0.0, 30.0, 60.0, 120.0, 180.0, 240.0, 300.0, 360.0] {
226-
let a = HSLColor(deg, 1.0, 0.5).to_backend_color().rgb;
227-
let b = HSLColor::from_degrees(deg, 1.0, 0.5).to_backend_color().rgb;
228-
assert_eq!(a, b);
229-
}
221+
fn from_degrees_wraps_and_matches_normalized() {
222+
let normalized = HSLColor(120.0 / 360.0, 1.0, 0.5).to_backend_color().rgb;
223+
let via_helper = HSLColor::from_degrees(120.0, 1.0, 0.5).to_backend_color().rgb;
224+
assert_eq!(normalized, via_helper);
225+
226+
let wrap_positive =
227+
HSLColor::from_degrees(720.0, 1.0, 0.5).to_backend_color().rgb;
228+
let wrap_negative =
229+
HSLColor::from_degrees(-120.0, 1.0, 0.5).to_backend_color().rgb;
230+
let canonical =
231+
HSLColor::from_degrees(0.0, 1.0, 0.5).to_backend_color().rgb;
232+
233+
assert_eq!(wrap_positive, canonical);
234+
assert_eq!(wrap_negative, HSLColor::from_degrees(240.0, 1.0, 0.5).to_backend_color().rgb);
230235
}
231236
}

0 commit comments

Comments
 (0)