Skip to content
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

Add traits for color schemes from traditional color theory #384

Merged
merged 3 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
289 changes: 289 additions & 0 deletions palette/src/color_theory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
//! Traits related to traditional color theory.
//!
//! Traditional color theory is sometimes used as a guide when selecting colors
//! for artistic purposes. While it's not the same as modern color science, and
//! much more subjective, it may still be a helpful set of principles.
//!
//! This module is primarily based on the 12 color wheel, meaning that they use
//! colors that are separated by 30° around the hue circle. There are however
//! some concepts, such as [`Complementary`] colors, that are generally
//! independent from the 12 color wheel concept.
//!
//! Most of the traits in this module require the color space to have a hue
//! component. You will often see people use [`Hsv`][crate::Hsv] or
//! [`Hsl`][crate::Hsl] when demonstrating some of these techniques, but Palette
//! lets you use any hue based color space. Some traits are also implemented for
//! other color spaces, when it's possible to avoid converting them to their hue
//! based counterparts.

use crate::{angle::HalfRotation, num::Real, ShiftHue};

/// Represents the complementary color scheme.
///
/// A complementary color scheme consists of two colors on the opposite sides of
/// the color wheel.
pub trait Complementary: Sized {
/// Return the complementary color of `self`.
///
/// This is the same as if the hue of `self` would be rotated by 180°.
///
/// The following example makes a complementary color pair:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(300deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::Complementary};
///
/// let primary = Hsl::new_srgb(120.0f32, 8.0, 0.5);
/// let complementary = primary.complementary();
///
/// let hues = (
/// primary.hue.into_positive_degrees(),
/// complementary.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (120.0, 300.0));
/// ```
fn complementary(self) -> Self;
}

impl<T> Complementary for T
where
T: ShiftHue,
T::Scalar: HalfRotation,
{
fn complementary(self) -> Self {
self.shift_hue(T::Scalar::half_rotation())
}
}

/// Represents the split complementary color scheme.
///
/// A split complementary color scheme consists of three colors, where the
/// second and third are adjacent to (30° away from) the complementary color of
/// the first.
pub trait SplitComplementary: Sized {
/// Return the two split complementary colors of `self`.
///
/// The colors are ordered by ascending hue, or `(hue+150°, hue+210°)`.
/// Combined with the input color, these make up 3 adjacent colors.
///
/// The following example makes a split complementary color scheme:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(270deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(330deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::SplitComplementary};
///
/// let primary = Hsl::new_srgb(120.0f32, 8.0, 0.5);
/// let (complementary1, complementary2) = primary.split_complementary();
///
/// let hues = (
/// primary.hue.into_positive_degrees(),
/// complementary1.hue.into_positive_degrees(),
/// complementary2.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (120.0, 270.0, 330.0));
/// ```
fn split_complementary(self) -> (Self, Self);
}

impl<T> SplitComplementary for T
where
T: ShiftHue + Clone,
T::Scalar: Real,
{
fn split_complementary(self) -> (Self, Self) {
let first = self.clone().shift_hue(T::Scalar::from_f64(150.0));
let second = self.shift_hue(T::Scalar::from_f64(210.0));

(first, second)
}
}

/// Represents the analogous color scheme on a 12 color wheel.
///
/// An analogous color scheme consists of three colors next to each other (30°
/// apart) on the color wheel.
pub trait Analogous: Sized {
/// Return the two additional colors of an analogous color scheme.
///
/// The colors are ordered by ascending hue difference, or `(hue-30°,
/// hue+30°)`. Combined with the input color, these make up 3 adjacent
/// colors.
///
/// The following example makes a 3 color analogous scheme:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(90deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(150deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::Analogous};
///
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
/// let (analog_down, analog_up) = primary.analogous();
///
/// let hues = (
/// analog_down.hue.into_positive_degrees(),
/// primary.hue.into_positive_degrees(),
/// analog_up.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (90.0, 120.0, 150.0));
/// ```
fn analogous(self) -> (Self, Self);

/// Return the two furthest colors of a 5 color analogous color scheme.
///
/// The colors are ordered by ascending hue difference, or `(hue-60°,
/// hue+60°)`. Combined with the input color and the colors from
/// `analogous`, these make up 5 adjacent colors.
///
/// The following example makes a 5 color analogous scheme:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(60deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(90deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(150deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(180deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::Analogous};
///
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
/// let (analog_down1, analog_up1) = primary.analogous();
/// let (analog_down2, analog_up2) = primary.analogous2();
///
/// let hues = (
/// analog_down2.hue.into_positive_degrees(),
/// analog_down1.hue.into_positive_degrees(),
/// primary.hue.into_positive_degrees(),
/// analog_up1.hue.into_positive_degrees(),
/// analog_up2.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (60.0, 90.0, 120.0, 150.0, 180.0));
/// ```
fn analogous2(self) -> (Self, Self);
}

impl<T> Analogous for T
where
T: ShiftHue + Clone,
T::Scalar: Real,
{
fn analogous(self) -> (Self, Self) {
let first = self.clone().shift_hue(T::Scalar::from_f64(330.0));
let second = self.shift_hue(T::Scalar::from_f64(30.0));

(first, second)
}

fn analogous2(self) -> (Self, Self) {
let first = self.clone().shift_hue(T::Scalar::from_f64(300.0));
let second = self.shift_hue(T::Scalar::from_f64(60.0));

(first, second)
}
}

/// Represents the triadic color scheme.
///
/// A triadic color scheme consists of thee colors at a 120° distance from each
/// other.
pub trait Triadic: Sized {
/// Return the two additional colors of a triadic color scheme.
///
/// The colors are ordered by ascending relative hues, or `(hue+120°,
/// hue+240°)`.
///
/// The following example makes a triadic scheme:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(240deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(0deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::Triadic};
///
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
/// let (triadic1, triadic2) = primary.triadic();
///
/// let hues = (
/// primary.hue.into_positive_degrees(),
/// triadic1.hue.into_positive_degrees(),
/// triadic2.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (120.0, 240.0, 0.0));
/// ```
fn triadic(self) -> (Self, Self);
}

impl<T> Triadic for T
where
T: ShiftHue + Clone,
T::Scalar: Real,
{
fn triadic(self) -> (Self, Self) {
let first = self.clone().shift_hue(T::Scalar::from_f64(120.0));
let second = self.shift_hue(T::Scalar::from_f64(240.0));

(first, second)
}
}

/// Represents the tetradic, or square, color scheme.
///
/// A tetradic color scheme consists of four colors at a 90° distance from each
/// other. These form two pairs of complementary colors.
#[doc(alias = "Square")]
pub trait Tetradic: Sized {
/// Return the three additional colors of a tetradic color scheme.
///
/// The colors are ordered by ascending relative hues, or `(hue+90°,
/// hue+180°, hue+270°)`.
///
/// The following example makes a tetradic scheme:
///
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(210deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(300deg, 80%, 50%);"></div>
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(30deg, 80%, 50%);"></div>
///
/// ```
/// use palette::{Hsl, color_theory::Tetradic};
///
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
/// let (tetradic1, tetradic2, tetradic3) = primary.tetradic();
///
/// let hues = (
/// primary.hue.into_positive_degrees(),
/// tetradic1.hue.into_positive_degrees(),
/// tetradic2.hue.into_positive_degrees(),
/// tetradic3.hue.into_positive_degrees(),
/// );
///
/// assert_eq!(hues, (120.0, 210.0, 300.0, 30.0));
/// ```
fn tetradic(self) -> (Self, Self, Self);
}

impl<T> Tetradic for T
where
T: ShiftHue + Clone,
T::Scalar: Real,
{
fn tetradic(self) -> (Self, Self, Self) {
let first = self.clone().shift_hue(T::Scalar::from_f64(90.0));
let second = self.clone().shift_hue(T::Scalar::from_f64(180.0));
let third = self.shift_hue(T::Scalar::from_f64(270.0));

(first, second, third)
}
}
6 changes: 6 additions & 0 deletions palette/src/lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ impl_lighten!(Lab<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {a, b
impl_premultiply!(Lab<Wp> {l, a, b} phantom: white_point);
impl_euclidean_distance!(Lab<Wp> {l, a, b});
impl_hyab!(Lab<Wp> {lightness: l, chroma1: a, chroma2: b});
impl_lab_color_schemes!(Lab<Wp>[l, white_point]);

impl<Wp, T> GetHue for Lab<Wp, T>
where
Expand Down Expand Up @@ -389,6 +390,9 @@ mod test {
use super::Lab;
use crate::white_point::D65;

#[cfg(feature = "approx")]
use crate::Lch;

test_convert_into_from_xyz!(Lab);

#[cfg(feature = "approx")]
Expand Down Expand Up @@ -476,4 +480,6 @@ mod test {
min: Lab::new(0.0f32, -128.0, -128.0),
max: Lab::new(100.0, 127.0, 127.0)
}

test_lab_color_schemes!(Lab/Lch [l, white_point]);
}
1 change: 1 addition & 0 deletions palette/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ pub mod bool_mask;
pub mod cast;
pub mod chromatic_adaptation;
pub mod color_difference;
pub mod color_theory;
pub mod convert;
pub mod encoding;
pub mod hsl;
Expand Down
6 changes: 6 additions & 0 deletions palette/src/luv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v
impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
impl_euclidean_distance!(Luv<Wp> {l, u, v});
impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});
impl_lab_color_schemes!(Luv<Wp>[u, v][l, white_point]);

impl<Wp, T> GetHue for Luv<Wp, T>
where
Expand Down Expand Up @@ -314,6 +315,9 @@ mod test {
use super::Luv;
use crate::white_point::D65;

#[cfg(feature = "approx")]
use crate::Lchuv;

test_convert_into_from_xyz!(Luv);

#[cfg(feature = "approx")]
Expand Down Expand Up @@ -417,4 +421,6 @@ mod test {
min: Luv::new(0.0f32, -84.0, -135.0),
max: Luv::new(100.0, 176.0, 108.0)
}

test_lab_color_schemes!(Luv / Lchuv [u, v][l, white_point]);
}
2 changes: 2 additions & 0 deletions palette/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ mod copy_clone;
mod hue;
#[macro_use]
mod random;
#[macro_use]
mod color_theory;
Loading
Loading