Skip to content

Commit bbd6d4b

Browse files
authored
Merge pull request #384 from Ogeon/color_theory
Add traits for color schemes from traditional color theory
2 parents c54efbd + 479beec commit bbd6d4b

File tree

8 files changed

+483
-37
lines changed

8 files changed

+483
-37
lines changed

palette/src/color_theory.rs

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
//! Traits related to traditional color theory.
2+
//!
3+
//! Traditional color theory is sometimes used as a guide when selecting colors
4+
//! for artistic purposes. While it's not the same as modern color science, and
5+
//! much more subjective, it may still be a helpful set of principles.
6+
//!
7+
//! This module is primarily based on the 12 color wheel, meaning that they use
8+
//! colors that are separated by 30° around the hue circle. There are however
9+
//! some concepts, such as [`Complementary`] colors, that are generally
10+
//! independent from the 12 color wheel concept.
11+
//!
12+
//! Most of the traits in this module require the color space to have a hue
13+
//! component. You will often see people use [`Hsv`][crate::Hsv] or
14+
//! [`Hsl`][crate::Hsl] when demonstrating some of these techniques, but Palette
15+
//! lets you use any hue based color space. Some traits are also implemented for
16+
//! other color spaces, when it's possible to avoid converting them to their hue
17+
//! based counterparts.
18+
19+
use crate::{angle::HalfRotation, num::Real, ShiftHue};
20+
21+
/// Represents the complementary color scheme.
22+
///
23+
/// A complementary color scheme consists of two colors on the opposite sides of
24+
/// the color wheel.
25+
pub trait Complementary: Sized {
26+
/// Return the complementary color of `self`.
27+
///
28+
/// This is the same as if the hue of `self` would be rotated by 180°.
29+
///
30+
/// The following example makes a complementary color pair:
31+
///
32+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
33+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(300deg, 80%, 50%);"></div>
34+
///
35+
/// ```
36+
/// use palette::{Hsl, color_theory::Complementary};
37+
///
38+
/// let primary = Hsl::new_srgb(120.0f32, 8.0, 0.5);
39+
/// let complementary = primary.complementary();
40+
///
41+
/// let hues = (
42+
/// primary.hue.into_positive_degrees(),
43+
/// complementary.hue.into_positive_degrees(),
44+
/// );
45+
///
46+
/// assert_eq!(hues, (120.0, 300.0));
47+
/// ```
48+
fn complementary(self) -> Self;
49+
}
50+
51+
impl<T> Complementary for T
52+
where
53+
T: ShiftHue,
54+
T::Scalar: HalfRotation,
55+
{
56+
fn complementary(self) -> Self {
57+
self.shift_hue(T::Scalar::half_rotation())
58+
}
59+
}
60+
61+
/// Represents the split complementary color scheme.
62+
///
63+
/// A split complementary color scheme consists of three colors, where the
64+
/// second and third are adjacent to (30° away from) the complementary color of
65+
/// the first.
66+
pub trait SplitComplementary: Sized {
67+
/// Return the two split complementary colors of `self`.
68+
///
69+
/// The colors are ordered by ascending hue, or `(hue+150°, hue+210°)`.
70+
/// Combined with the input color, these make up 3 adjacent colors.
71+
///
72+
/// The following example makes a split complementary color scheme:
73+
///
74+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
75+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(270deg, 80%, 50%);"></div>
76+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(330deg, 80%, 50%);"></div>
77+
///
78+
/// ```
79+
/// use palette::{Hsl, color_theory::SplitComplementary};
80+
///
81+
/// let primary = Hsl::new_srgb(120.0f32, 8.0, 0.5);
82+
/// let (complementary1, complementary2) = primary.split_complementary();
83+
///
84+
/// let hues = (
85+
/// primary.hue.into_positive_degrees(),
86+
/// complementary1.hue.into_positive_degrees(),
87+
/// complementary2.hue.into_positive_degrees(),
88+
/// );
89+
///
90+
/// assert_eq!(hues, (120.0, 270.0, 330.0));
91+
/// ```
92+
fn split_complementary(self) -> (Self, Self);
93+
}
94+
95+
impl<T> SplitComplementary for T
96+
where
97+
T: ShiftHue + Clone,
98+
T::Scalar: Real,
99+
{
100+
fn split_complementary(self) -> (Self, Self) {
101+
let first = self.clone().shift_hue(T::Scalar::from_f64(150.0));
102+
let second = self.shift_hue(T::Scalar::from_f64(210.0));
103+
104+
(first, second)
105+
}
106+
}
107+
108+
/// Represents the analogous color scheme on a 12 color wheel.
109+
///
110+
/// An analogous color scheme consists of three colors next to each other (30°
111+
/// apart) on the color wheel.
112+
pub trait Analogous: Sized {
113+
/// Return the two additional colors of an analogous color scheme.
114+
///
115+
/// The colors are ordered by ascending hue difference, or `(hue-30°,
116+
/// hue+30°)`. Combined with the input color, these make up 3 adjacent
117+
/// colors.
118+
///
119+
/// The following example makes a 3 color analogous scheme:
120+
///
121+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(90deg, 80%, 50%);"></div>
122+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
123+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(150deg, 80%, 50%);"></div>
124+
///
125+
/// ```
126+
/// use palette::{Hsl, color_theory::Analogous};
127+
///
128+
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
129+
/// let (analog_down, analog_up) = primary.analogous();
130+
///
131+
/// let hues = (
132+
/// analog_down.hue.into_positive_degrees(),
133+
/// primary.hue.into_positive_degrees(),
134+
/// analog_up.hue.into_positive_degrees(),
135+
/// );
136+
///
137+
/// assert_eq!(hues, (90.0, 120.0, 150.0));
138+
/// ```
139+
fn analogous(self) -> (Self, Self);
140+
141+
/// Return the two furthest colors of a 5 color analogous color scheme.
142+
///
143+
/// The colors are ordered by ascending hue difference, or `(hue-60°,
144+
/// hue+60°)`. Combined with the input color and the colors from
145+
/// `analogous`, these make up 5 adjacent colors.
146+
///
147+
/// The following example makes a 5 color analogous scheme:
148+
///
149+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(60deg, 80%, 50%);"></div>
150+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(90deg, 80%, 50%);"></div>
151+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
152+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(150deg, 80%, 50%);"></div>
153+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(180deg, 80%, 50%);"></div>
154+
///
155+
/// ```
156+
/// use palette::{Hsl, color_theory::Analogous};
157+
///
158+
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
159+
/// let (analog_down1, analog_up1) = primary.analogous();
160+
/// let (analog_down2, analog_up2) = primary.analogous2();
161+
///
162+
/// let hues = (
163+
/// analog_down2.hue.into_positive_degrees(),
164+
/// analog_down1.hue.into_positive_degrees(),
165+
/// primary.hue.into_positive_degrees(),
166+
/// analog_up1.hue.into_positive_degrees(),
167+
/// analog_up2.hue.into_positive_degrees(),
168+
/// );
169+
///
170+
/// assert_eq!(hues, (60.0, 90.0, 120.0, 150.0, 180.0));
171+
/// ```
172+
fn analogous2(self) -> (Self, Self);
173+
}
174+
175+
impl<T> Analogous for T
176+
where
177+
T: ShiftHue + Clone,
178+
T::Scalar: Real,
179+
{
180+
fn analogous(self) -> (Self, Self) {
181+
let first = self.clone().shift_hue(T::Scalar::from_f64(330.0));
182+
let second = self.shift_hue(T::Scalar::from_f64(30.0));
183+
184+
(first, second)
185+
}
186+
187+
fn analogous2(self) -> (Self, Self) {
188+
let first = self.clone().shift_hue(T::Scalar::from_f64(300.0));
189+
let second = self.shift_hue(T::Scalar::from_f64(60.0));
190+
191+
(first, second)
192+
}
193+
}
194+
195+
/// Represents the triadic color scheme.
196+
///
197+
/// A triadic color scheme consists of thee colors at a 120° distance from each
198+
/// other.
199+
pub trait Triadic: Sized {
200+
/// Return the two additional colors of a triadic color scheme.
201+
///
202+
/// The colors are ordered by ascending relative hues, or `(hue+120°,
203+
/// hue+240°)`.
204+
///
205+
/// The following example makes a triadic scheme:
206+
///
207+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
208+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(240deg, 80%, 50%);"></div>
209+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(0deg, 80%, 50%);"></div>
210+
///
211+
/// ```
212+
/// use palette::{Hsl, color_theory::Triadic};
213+
///
214+
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
215+
/// let (triadic1, triadic2) = primary.triadic();
216+
///
217+
/// let hues = (
218+
/// primary.hue.into_positive_degrees(),
219+
/// triadic1.hue.into_positive_degrees(),
220+
/// triadic2.hue.into_positive_degrees(),
221+
/// );
222+
///
223+
/// assert_eq!(hues, (120.0, 240.0, 0.0));
224+
/// ```
225+
fn triadic(self) -> (Self, Self);
226+
}
227+
228+
impl<T> Triadic for T
229+
where
230+
T: ShiftHue + Clone,
231+
T::Scalar: Real,
232+
{
233+
fn triadic(self) -> (Self, Self) {
234+
let first = self.clone().shift_hue(T::Scalar::from_f64(120.0));
235+
let second = self.shift_hue(T::Scalar::from_f64(240.0));
236+
237+
(first, second)
238+
}
239+
}
240+
241+
/// Represents the tetradic, or square, color scheme.
242+
///
243+
/// A tetradic color scheme consists of four colors at a 90° distance from each
244+
/// other. These form two pairs of complementary colors.
245+
#[doc(alias = "Square")]
246+
pub trait Tetradic: Sized {
247+
/// Return the three additional colors of a tetradic color scheme.
248+
///
249+
/// The colors are ordered by ascending relative hues, or `(hue+90°,
250+
/// hue+180°, hue+270°)`.
251+
///
252+
/// The following example makes a tetradic scheme:
253+
///
254+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(120deg, 80%, 50%);"></div>
255+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(210deg, 80%, 50%);"></div>
256+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(300deg, 80%, 50%);"></div>
257+
/// <div style="display: inline-block; width: 3em; height: 1em; border: 1px solid black; background: hsl(30deg, 80%, 50%);"></div>
258+
///
259+
/// ```
260+
/// use palette::{Hsl, color_theory::Tetradic};
261+
///
262+
/// let primary = Hsl::new_srgb(120.0f32, 0.8, 0.5);
263+
/// let (tetradic1, tetradic2, tetradic3) = primary.tetradic();
264+
///
265+
/// let hues = (
266+
/// primary.hue.into_positive_degrees(),
267+
/// tetradic1.hue.into_positive_degrees(),
268+
/// tetradic2.hue.into_positive_degrees(),
269+
/// tetradic3.hue.into_positive_degrees(),
270+
/// );
271+
///
272+
/// assert_eq!(hues, (120.0, 210.0, 300.0, 30.0));
273+
/// ```
274+
fn tetradic(self) -> (Self, Self, Self);
275+
}
276+
277+
impl<T> Tetradic for T
278+
where
279+
T: ShiftHue + Clone,
280+
T::Scalar: Real,
281+
{
282+
fn tetradic(self) -> (Self, Self, Self) {
283+
let first = self.clone().shift_hue(T::Scalar::from_f64(90.0));
284+
let second = self.clone().shift_hue(T::Scalar::from_f64(180.0));
285+
let third = self.shift_hue(T::Scalar::from_f64(270.0));
286+
287+
(first, second, third)
288+
}
289+
}

palette/src/lab.rs

+6
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ impl_lighten!(Lab<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {a, b
231231
impl_premultiply!(Lab<Wp> {l, a, b} phantom: white_point);
232232
impl_euclidean_distance!(Lab<Wp> {l, a, b});
233233
impl_hyab!(Lab<Wp> {lightness: l, chroma1: a, chroma2: b});
234+
impl_lab_color_schemes!(Lab<Wp>[l, white_point]);
234235

235236
impl<Wp, T> GetHue for Lab<Wp, T>
236237
where
@@ -389,6 +390,9 @@ mod test {
389390
use super::Lab;
390391
use crate::white_point::D65;
391392

393+
#[cfg(feature = "approx")]
394+
use crate::Lch;
395+
392396
test_convert_into_from_xyz!(Lab);
393397

394398
#[cfg(feature = "approx")]
@@ -476,4 +480,6 @@ mod test {
476480
min: Lab::new(0.0f32, -128.0, -128.0),
477481
max: Lab::new(100.0, 127.0, 127.0)
478482
}
483+
484+
test_lab_color_schemes!(Lab/Lch [l, white_point]);
479485
}

palette/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ pub mod bool_mask;
347347
pub mod cast;
348348
pub mod chromatic_adaptation;
349349
pub mod color_difference;
350+
pub mod color_theory;
350351
pub mod convert;
351352
pub mod encoding;
352353
pub mod hsl;

palette/src/luv.rs

+6
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v
233233
impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
234234
impl_euclidean_distance!(Luv<Wp> {l, u, v});
235235
impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});
236+
impl_lab_color_schemes!(Luv<Wp>[u, v][l, white_point]);
236237

237238
impl<Wp, T> GetHue for Luv<Wp, T>
238239
where
@@ -314,6 +315,9 @@ mod test {
314315
use super::Luv;
315316
use crate::white_point::D65;
316317

318+
#[cfg(feature = "approx")]
319+
use crate::Lchuv;
320+
317321
test_convert_into_from_xyz!(Luv);
318322

319323
#[cfg(feature = "approx")]
@@ -417,4 +421,6 @@ mod test {
417421
min: Luv::new(0.0f32, -84.0, -135.0),
418422
max: Luv::new(100.0, 176.0, 108.0)
419423
}
424+
425+
test_lab_color_schemes!(Luv / Lchuv [u, v][l, white_point]);
420426
}

palette/src/macros.rs

+2
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ mod copy_clone;
3737
mod hue;
3838
#[macro_use]
3939
mod random;
40+
#[macro_use]
41+
mod color_theory;

0 commit comments

Comments
 (0)