Skip to content

Commit 95280f7

Browse files
committed
Add traits for color schemes from traditional color theory
1 parent c54efbd commit 95280f7

File tree

7 files changed

+442
-0
lines changed

7 files changed

+442
-0
lines changed

palette/src/color_theory.rs

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

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)