palette/
color_theory.rs

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
19use 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.
25pub 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
51impl<T> Complementary for T
52where
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.
66pub 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
95impl<T> SplitComplementary for T
96where
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.
112pub 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 next two analogous colors, after the colors `analogous` returns.
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.analogous_secondary();
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 analogous_secondary(self) -> (Self, Self);
173}
174
175impl<T> Analogous for T
176where
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 analogous_secondary(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.
199pub 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
228impl<T> Triadic for T
229where
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")]
246pub 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
277impl<T> Tetradic for T
278where
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}