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}