Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 46 additions & 30 deletions plotters/src/style/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,39 @@ use serde::{Deserialize, Serialize};

use std::marker::PhantomData;

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

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

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

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

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

/// Make a filled style form the color
fn filled(&self) -> ShapeStyle
where
Self: Sized,
{
Into::<ShapeStyle>::into(self).filled()
}

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

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

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

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

/// The color described by its RGB value
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct RGBColor(pub u8, pub u8, pub u8);
Expand All @@ -128,31 +114,40 @@ impl Color for RGBColor {
}
}
}

impl BackendStyle for RGBColor {
fn color(&self) -> BackendColor {
self.to_backend_color()
}
}

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

impl HSLColor {
#[inline]
pub fn from_degrees(h_deg: f64, s: f64, l: f64) -> Self {
Comment thread
zxq82lm marked this conversation as resolved.
Outdated
Self(h_deg / 360.0, s, l)
}
}

impl Color for HSLColor {
#[inline(always)]
#[allow(clippy::many_single_char_names)]
fn to_backend_color(&self) -> BackendColor {
let (h, s, l) = (
self.0.clamp(0.0, 1.0),
self.1.clamp(0.0, 1.0),
self.2.clamp(0.0, 1.0),
);
let h = if self.0 > 1.0 {
Comment thread
zxq82lm marked this conversation as resolved.
Outdated
(self.0 / 360.0).rem_euclid(1.0)
} else {
self.0.rem_euclid(1.0)
};
let s = self.1.clamp(0.0, 1.0);
let l = self.2.clamp(0.0, 1.0);

if s == 0.0 {
let value = (l * 255.0).round() as u8;
let v = (l * 255.0).round() as u8;
return BackendColor {
rgb: (value, value, value),
rgb: (v, v, v),
alpha: 1.0,
};
}
Expand All @@ -164,13 +159,8 @@ impl Color for HSLColor {
};
let p = 2.0 * l - q;

let cvt = |mut t| {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
let cvt = |t: f64| {
let t = t.rem_euclid(1.0);
let value = if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
Expand All @@ -189,3 +179,29 @@ impl Color for HSLColor {
}
}
}

#[cfg(test)]
mod hue_robustness_tests {
use super::*;

#[test]
fn degrees_passed_directly_should_work_for_common_cases() {
let red = HSLColor(0.0, 1.0, 0.5).to_backend_color().rgb;
assert_eq!(red, (255, 0, 0));

let green = HSLColor(120.0, 1.0, 0.5).to_backend_color().rgb;
assert_eq!(green, (0, 255, 0));

let blue = HSLColor(240.0, 1.0, 0.5).to_backend_color().rgb;
assert_eq!(blue, (0, 0, 255));
}

#[test]
fn from_degrees_and_direct_degrees_are_equivalent() {
for &deg in &[0.0, 30.0, 60.0, 120.0, 180.0, 240.0, 300.0, 360.0] {
let a = HSLColor(deg, 1.0, 0.5).to_backend_color().rgb;
let b = HSLColor::from_degrees(deg, 1.0, 0.5).to_backend_color().rgb;
assert_eq!(a, b);
}
}
}