palette/
stimulus.rs

1//! Traits for working with stimulus colors and values, such as RGB and XYZ.
2
3use crate::{
4    clamp,
5    num::{One, Real, Round, Zero},
6};
7
8/// Color components that represent a stimulus intensity.
9///
10/// The term "stimulus" comes from "tristimulus", literally a set of three
11/// stimuli, which is a term for color spaces that measure the intensity of
12/// three primary color values. Classic examples of tristimulus color space are
13/// XYZ and RGB.
14///
15/// Stimulus values are expected to have these properties:
16///  * Has a typical range from `0` to some finite maximum, the "max intensity".
17///    This represents a range from `0%` to `100%`. For example `0u8` to
18///    `255u8`, `0.0f32` to `1.0f32`.
19///  * Values below `0` are considered invalid for display purposes, but may
20///    still be used in calculations.
21///  * Values above the "max intensity" are sometimes supported, depending on
22///    the application. For example in 3D rendering, where high values represent
23///    intense light.
24///  * Unsigned integer values (`u8`, `u16`, `u32`, etc.) have a range from `0`
25///    to their largest representable value. For example `0u8` to `255u8` or
26///    `0u16` to `65535u16`.
27///  * Real values (`f32`, `f64`, fixed point types, etc.) have a range from
28///    `0.0` to `1.0`.
29pub trait Stimulus: Zero {
30    /// The highest displayable value this component type can reach. Integers
31    /// types are expected to return their maximum value, while real numbers
32    /// (like floats) return 1.0. Higher values are allowed, but they may be
33    /// lowered to this before converting to another format.
34    #[must_use]
35    fn max_intensity() -> Self;
36}
37
38impl<T> Stimulus for T
39where
40    T: Real + One + Zero,
41{
42    #[inline]
43    fn max_intensity() -> Self {
44        Self::one()
45    }
46}
47
48macro_rules! impl_uint_components {
49    ($($ty: ident),+) => {
50        $(
51            impl Stimulus for $ty {
52                #[inline]
53                fn max_intensity() -> Self {
54                    $ty::MAX
55                }
56            }
57        )*
58    };
59}
60
61impl_uint_components!(u8, u16, u32, u64, u128);
62
63/// A marker trait for colors where all components are stimuli.
64///
65/// Typical stimulus colors are RGB and XYZ.
66pub trait StimulusColor {}
67
68/// Converts from a stimulus color component type, while performing the
69/// appropriate scaling, rounding and clamping.
70///
71/// ```
72/// use palette::stimulus::FromStimulus;
73///
74/// // Scales the value up to u8::MAX while converting.
75/// let u8_component = u8::from_stimulus(1.0f32);
76/// assert_eq!(u8_component, 255);
77/// ```
78pub trait FromStimulus<T> {
79    /// Converts `other` into `Self`, while performing the appropriate scaling,
80    /// rounding and clamping.
81    #[must_use]
82    fn from_stimulus(other: T) -> Self;
83}
84
85impl<T, U: IntoStimulus<T>> FromStimulus<U> for T {
86    #[inline]
87    fn from_stimulus(other: U) -> T {
88        other.into_stimulus()
89    }
90}
91
92/// Converts into a stimulus color component type, while performing the
93/// appropriate scaling, rounding and clamping.
94///
95/// ```
96/// use palette::stimulus::IntoStimulus;
97///
98/// // Scales the value up to u8::MAX while converting.
99/// let u8_component: u8 = 1.0f32.into_stimulus();
100/// assert_eq!(u8_component, 255);
101/// ```
102pub trait IntoStimulus<T> {
103    /// Converts `self` into `T`, while performing the appropriate scaling,
104    /// rounding and clamping.
105    #[must_use]
106    fn into_stimulus(self) -> T;
107}
108
109impl<T> IntoStimulus<T> for T {
110    #[inline]
111    fn into_stimulus(self) -> T {
112        self
113    }
114}
115
116// C23 = 2^23, in f32
117// C52 = 2^52, in f64
118const C23: u32 = 0x4b00_0000;
119const C52: u64 = 0x4330_0000_0000_0000;
120
121// Float to uint conversion with rounding to nearest even number. Formula
122// follows the form (x_f32 + C23_f32) - C23_u32, where x is the component. From
123// Hacker's Delight, p. 378-380.
124// Works on the range of [-0.25, 2^23] for f32, [-0.25, 2^52] for f64.
125//
126// Special cases:
127// NaN -> uint::MAX
128// inf -> uint::MAX
129// -inf -> 0
130// Greater than 2^23 for f64, 2^52 for f64 -> uint::MAX
131macro_rules! convert_float_to_uint {
132    ($float: ident; direct ($($direct_target: ident),+); $(via $temporary: ident ($($target: ident),+);)*) => {
133        $(
134            impl IntoStimulus<$direct_target> for $float {
135                #[inline]
136                fn into_stimulus(self) -> $direct_target {
137                    let max = $direct_target::max_intensity() as $float;
138                    let scaled = (self * max).min(max);
139                    let f = scaled + f32::from_bits(C23);
140                    (f.to_bits().saturating_sub(C23)) as $direct_target
141                }
142            }
143        )+
144
145        $(
146            $(
147                impl IntoStimulus<$target> for $float {
148                    #[inline]
149                    fn into_stimulus(self) -> $target {
150                        let max = $target::max_intensity() as $temporary;
151                        let scaled = (self as $temporary * max).min(max);
152                        let f = scaled + f64::from_bits(C52);
153                        (f.to_bits().saturating_sub(C52)) as  $target
154                    }
155                }
156            )+
157        )*
158    };
159}
160
161// Double to uint conversion with rounding to nearest even number. Formula
162// follows the form (x_f64 + C52_f64) - C52_u64, where x is the component.
163macro_rules! convert_double_to_uint {
164    ($double: ident; direct ($($direct_target: ident),+);) => {
165        $(
166            impl IntoStimulus<$direct_target> for $double {
167                #[inline]
168                fn into_stimulus(self) -> $direct_target {
169                    let max = $direct_target::max_intensity() as $double;
170                    let scaled = (self * max).min(max);
171                    let f = scaled + f64::from_bits(C52);
172                    (f.to_bits().saturating_sub(C52)) as $direct_target
173                }
174            }
175        )+
176    };
177}
178
179// Uint to float conversion with the formula (x_u32 + C23_u32) - C23_f32, where
180// x is the component. We convert the component to f32 then multiply it by the
181// reciprocal of the float representation max value for u8.
182// Works on the range of [0, 2^23] for f32, [0, 2^52 - 1] for f64.
183impl IntoStimulus<f32> for u8 {
184    #[inline]
185    fn into_stimulus(self) -> f32 {
186        let comp_u = u32::from(self) + C23;
187        let comp_f = f32::from_bits(comp_u) - f32::from_bits(C23);
188        let max_u = u32::from(u8::MAX) + C23;
189        let max_f = (f32::from_bits(max_u) - f32::from_bits(C23)).recip();
190        comp_f * max_f
191    }
192}
193
194// Uint to f64 conversion with the formula (x_u64 + C23_u64) - C23_f64.
195impl IntoStimulus<f64> for u8 {
196    #[inline]
197    fn into_stimulus(self) -> f64 {
198        let comp_u = u64::from(self) + C52;
199        let comp_f = f64::from_bits(comp_u) - f64::from_bits(C52);
200        let max_u = u64::from(u8::MAX) + C52;
201        let max_f = (f64::from_bits(max_u) - f64::from_bits(C52)).recip();
202        comp_f * max_f
203    }
204}
205
206macro_rules! convert_uint_to_float {
207    ($uint: ident; $(via $temporary: ident ($($target: ident),+);)*) => {
208        $(
209            $(
210                impl IntoStimulus<$target> for $uint {
211                    #[inline]
212                    fn into_stimulus(self) -> $target {
213                        let max = $uint::max_intensity() as $temporary;
214                        let scaled = self as $temporary / max;
215                        scaled as $target
216                    }
217                }
218            )+
219        )*
220    };
221}
222
223macro_rules! convert_uint_to_uint {
224    ($uint: ident; $(via $temporary: ident ($($target: ident),+);)*) => {
225        $(
226            $(
227                impl IntoStimulus<$target> for $uint {
228                    #[inline]
229                    fn into_stimulus(self) -> $target {
230                        let target_max = $target::max_intensity() as $temporary;
231                        let own_max = $uint::max_intensity() as $temporary;
232                        let scaled = (self as $temporary / own_max) * target_max;
233                        clamp(Round::round(scaled), 0.0, target_max) as $target
234                    }
235                }
236            )+
237        )*
238    };
239}
240
241impl IntoStimulus<f64> for f32 {
242    #[inline]
243    fn into_stimulus(self) -> f64 {
244        f64::from(self)
245    }
246}
247convert_float_to_uint!(f32; direct (u8, u16); via f64 (u32, u64, u128););
248
249impl IntoStimulus<f32> for f64 {
250    #[inline]
251    fn into_stimulus(self) -> f32 {
252        self as f32
253    }
254}
255convert_double_to_uint!(f64; direct (u8, u16, u32, u64, u128););
256
257convert_uint_to_uint!(u8; via f32 (u16); via f64 (u32, u64, u128););
258
259convert_uint_to_float!(u16; via f32 (f32); via f64 (f64););
260convert_uint_to_uint!(u16; via f32 (u8); via f64 (u32, u64, u128););
261
262convert_uint_to_float!(u32; via f64 (f32, f64););
263convert_uint_to_uint!(u32; via f64 (u8, u16, u64, u128););
264
265convert_uint_to_float!(u64; via f64 (f32, f64););
266convert_uint_to_uint!(u64; via f64 (u8, u16, u32, u128););
267
268convert_uint_to_float!(u128; via f64 (f32, f64););
269convert_uint_to_uint!(u128; via f64 (u8, u16, u32, u64););
270
271#[cfg(test)]
272mod test {
273    use crate::stimulus::IntoStimulus;
274
275    #[test]
276    fn float_to_uint() {
277        let data = vec![
278            -800.0,
279            -0.3,
280            0.0,
281            0.005,
282            0.024983,
283            0.01,
284            0.15,
285            0.3,
286            0.5,
287            0.6,
288            0.7,
289            0.8,
290            0.8444,
291            0.9,
292            0.955,
293            0.999,
294            1.0,
295            1.4,
296            f32::from_bits(0x4b44_0000),
297            core::f32::MAX,
298            core::f32::MIN,
299            core::f32::NAN,
300            core::f32::INFINITY,
301            core::f32::NEG_INFINITY,
302        ];
303
304        let expected = vec![
305            0u8, 0, 0, 1, 6, 3, 38, 76, 128, 153, 178, 204, 215, 230, 244, 255, 255, 255, 255, 255,
306            0, 255, 255, 0,
307        ];
308
309        for (d, e) in data.into_iter().zip(expected) {
310            assert_eq!(IntoStimulus::<u8>::into_stimulus(d), e);
311        }
312    }
313
314    #[test]
315    fn double_to_uint() {
316        let data = vec![
317            -800.0,
318            -0.3,
319            0.0,
320            0.005,
321            0.024983,
322            0.01,
323            0.15,
324            0.3,
325            0.5,
326            0.6,
327            0.7,
328            0.8,
329            0.8444,
330            0.9,
331            0.955,
332            0.999,
333            1.0,
334            1.4,
335            f64::from_bits(0x4334_0000_0000_0000),
336            core::f64::MAX,
337            core::f64::MIN,
338            core::f64::NAN,
339            core::f64::INFINITY,
340            core::f64::NEG_INFINITY,
341        ];
342
343        let expected = vec![
344            0u8, 0, 0, 1, 6, 3, 38, 76, 128, 153, 178, 204, 215, 230, 244, 255, 255, 255, 255, 255,
345            0, 255, 255, 0,
346        ];
347
348        for (d, e) in data.into_iter().zip(expected) {
349            assert_eq!(IntoStimulus::<u8>::into_stimulus(d), e);
350        }
351    }
352
353    #[cfg(feature = "approx")]
354    #[test]
355    fn uint_to_float() {
356        fn into_stimulus_old(n: u8) -> f32 {
357            let max = u8::MAX as f32;
358            n as f32 / max
359        }
360
361        for n in (0..=255).step_by(5) {
362            assert_relative_eq!(IntoStimulus::<f32>::into_stimulus(n), into_stimulus_old(n))
363        }
364    }
365
366    #[cfg(feature = "approx")]
367    #[test]
368    fn uint_to_double() {
369        fn into_stimulus_old(n: u8) -> f64 {
370            let max = u8::MAX as f64;
371            n as f64 / max
372        }
373
374        for n in (0..=255).step_by(5) {
375            assert_relative_eq!(IntoStimulus::<f64>::into_stimulus(n), into_stimulus_old(n))
376        }
377    }
378}