palette/cam16/
parameters.rs

1use core::marker::PhantomData;
2
3use crate::{
4    bool_mask::LazySelect,
5    num::{
6        Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero,
7    },
8    white_point::{self, WhitePoint},
9    Xyz,
10};
11
12/// Parameters for CAM16 that describe the viewing conditions.
13///
14/// These parameters describe the viewing conditions for a more accurate color
15/// appearance metric. The CAM16 attributes and derived values are only really
16/// comparable if they were calculated with the same parameters. The parameters
17/// are, however, too dynamic to all be part of the type parameters of
18/// [`Cam16`][super::Cam16].
19///
20/// The default values are mostly a "blank slate", with a couple of educated
21/// guesses. Be sure to at least customize the luminances according to the
22/// expected environment:
23///
24/// ```
25/// use palette::{Srgb, Xyz, IntoColor, cam16::{Parameters, Surround, Cam16}};
26///
27/// // 40 nits, 50% background luminance and a dim surrounding:
28/// let mut example_parameters = Parameters::default_static_wp(40.0);
29/// example_parameters.background_luminance = 0.5;
30/// example_parameters.surround = Surround::Dim;
31///
32/// let example_color_xyz = Srgb::from(0x5588cc).into_linear().into_color();
33/// let cam16: Cam16<f64> = Cam16::from_xyz(example_color_xyz, example_parameters);
34/// ```
35///
36/// See also Moroney (2000) [Usage Guidelines for CIECAM97s][moroney_2000] for
37/// more information and advice on how to customize these parameters.
38///
39/// [moroney_2000]:
40///     https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf
41#[derive(Clone, Copy)]
42#[non_exhaustive]
43pub struct Parameters<WpParam, T> {
44    /// White point of the test illuminant, *X<sub>w</sub>* *Y<sub>w</sub>*
45    /// *Z<sub>w</sub>*. *Y<sub>w</sub>* should be normalized to 1.0.
46    ///
47    /// Defaults to `Wp` when it implements [`WhitePoint`]. It can also be set
48    /// to a custom value if `Wp` results in the wrong white point.
49    pub white_point: WpParam,
50
51    /// The average luminance of the environment (test adapting field)
52    /// (*L<sub>A</sub>*) in *cd/m<sup>2</sup>* (nits).
53    ///
54    /// Under a “gray world” assumption this is 20% of the luminance of the
55    /// reference white.
56    pub adapting_luminance: T,
57
58    /// The luminance factor of the background (*Y<sub>b</sub>*), on a scale
59    /// from `0.0` to `1.0` (relative to *Y<sub>w</sub>* = 1.0).
60    ///
61    /// Defaults to `0.2`, medium grey.
62    pub background_luminance: T,
63
64    /// A description of the peripheral area, with a value from 0% to 20%. Any
65    /// value outside that range will be clamped to 0% or 20%. It has presets
66    /// for "dark", "dim" and "average".
67    ///
68    /// Defaults to "average" (20%).
69    pub surround: Surround<T>,
70
71    /// The degree of discounting of (or adaptation to) the reference
72    /// illuminant. Defaults to `Auto`, making the degree of discounting depend
73    /// on the other parameters, but can be customized if necessary.
74    pub discounting: Discounting<T>,
75}
76
77impl<WpParam, T> Parameters<WpParam, T>
78where
79    WpParam: WhitePointParameter<T>,
80{
81    fn into_any_white_point(self) -> Parameters<Xyz<white_point::Any, T>, T> {
82        Parameters {
83            white_point: self.white_point.into_xyz(),
84            adapting_luminance: self.adapting_luminance,
85            background_luminance: self.background_luminance,
86            surround: self.surround,
87            discounting: self.discounting,
88        }
89    }
90}
91
92impl<Wp, T> Parameters<StaticWp<Wp>, T> {
93    /// Creates a new set of parameters with a static white point and their
94    /// default values set.
95    ///
96    /// These parameters may need to be further customized according to the
97    /// viewing conditions.
98    #[inline]
99    pub fn default_static_wp(adapting_luminance: T) -> Self
100    where
101        T: Real,
102    {
103        Self {
104            white_point: StaticWp(PhantomData),
105            adapting_luminance,
106            background_luminance: T::from_f64(0.2),
107            surround: Surround::Average,
108            discounting: Discounting::Auto,
109        }
110    }
111}
112
113impl<T> Parameters<Xyz<white_point::Any, T>, T> {
114    /// Creates a new set of parameters with a dynamic white point and their
115    /// default values set.
116    ///
117    /// These parameters may need to be further customized according to the
118    /// viewing conditions.
119    #[inline]
120    pub fn default_dynamic_wp(white_point: Xyz<white_point::Any, T>, adapting_luminance: T) -> Self
121    where
122        T: Real,
123    {
124        Self {
125            white_point,
126            adapting_luminance,
127            background_luminance: T::from_f64(0.2),
128            surround: Surround::Average,
129            discounting: Discounting::Auto,
130        }
131    }
132}
133
134impl<WpParam, T> Parameters<WpParam, T> {
135    /// Pre-bakes the parameters to avoid repeating parts of the calculaitons
136    /// when converting to and from CAM16.
137    pub fn bake(self) -> BakedParameters<WpParam, T>
138    where
139        BakedParameters<WpParam, T>: From<Self>,
140    {
141        self.into()
142    }
143}
144
145#[cfg(all(test, feature = "approx"))]
146impl<Wp> Parameters<StaticWp<Wp>, f64> {
147    /// Only used in unit tests and corresponds to the defaults from https://observablehq.com/@jrus/cam16.
148    pub(crate) const TEST_DEFAULTS: Self = Self {
149        white_point: StaticWp(PhantomData),
150        adapting_luminance: 40.0f64,
151        background_luminance: 0.2f64, // 20 / 100, since our XYZ is in the range from 0.0 to 1.0
152        surround: Surround::Average,
153        discounting: Discounting::Auto,
154    };
155}
156
157/// Pre-calculated variables for CAM16, that only depend on the viewing
158/// conditions.
159///
160/// Derived from [`Parameters`], the `BakedParameters` can help reducing the
161/// amount of repeated work required for converting multiple colors.
162pub struct BakedParameters<WpParam, T> {
163    pub(crate) inner: super::math::DependentParameters<T>,
164    white_point: PhantomData<WpParam>,
165}
166
167impl<WpParam, T> Clone for BakedParameters<WpParam, T>
168where
169    T: Clone,
170{
171    fn clone(&self) -> Self {
172        Self {
173            inner: self.inner.clone(),
174            white_point: PhantomData,
175        }
176    }
177}
178
179impl<WpParam, T> Copy for BakedParameters<WpParam, T> where T: Copy {}
180
181impl<WpParam, T> From<Parameters<WpParam, T>> for BakedParameters<WpParam, T>
182where
183    WpParam: WhitePointParameter<T>,
184    T: Real
185        + FromScalar<Scalar = T>
186        + One
187        + Zero
188        + Clamp
189        + PartialCmp
190        + Arithmetics
191        + Powf
192        + Sqrt
193        + Exp
194        + Abs
195        + Signum
196        + Clone,
197    T::Mask: LazySelect<T>,
198{
199    fn from(value: Parameters<WpParam, T>) -> Self {
200        Self {
201            inner: super::math::prepare_parameters(value.into_any_white_point()),
202            white_point: PhantomData,
203        }
204    }
205}
206
207/// A description of the peripheral area.
208#[derive(Clone, Copy)]
209#[non_exhaustive]
210pub enum Surround<T> {
211    /// Represents a dark room, such as a movie theatre. Corresponds to a
212    /// surround value of 0%.
213    Dark,
214
215    /// Represents a dimly lit room with a bright TV or monitor. Corresponds to
216    /// a surround value of 10%.
217    Dim,
218
219    /// Represents a surface color, such as a print on a 20% reflective,
220    /// uniformly lit background surface. Corresponds to a surround value of
221    /// 20%.
222    Average,
223
224    /// Any custom value from 0% to 20%. Any value outside that range will be
225    /// clamped to either `0.0` or `20.0`.
226    Percent(T),
227}
228
229impl<T> Surround<T> {
230    pub(crate) fn into_percent(self) -> T
231    where
232        T: Real + Clamp,
233    {
234        match self {
235            Surround::Dark => T::from_f64(0.0),
236            Surround::Dim => T::from_f64(10.0),
237            Surround::Average => T::from_f64(20.0),
238            Surround::Percent(value) => value.clamp(T::from_f64(0.0), T::from_f64(20.0)),
239        }
240    }
241}
242
243/// The degree of discounting of (or adaptation to) the illuminant.
244///
245/// See also: <https://en.wikipedia.org/wiki/CIECAM02#CAT02>.
246#[derive(Clone, Copy)]
247#[non_exhaustive]
248pub enum Discounting<T> {
249    /// Uses luminance levels and surround conditions to calculate the
250    /// discounting, using the original CIECAM16 *D* function. Ranges from
251    /// `0.65` to `1.0`.
252    Auto,
253
254    /// A value between `0.0` and `1.0`, where `0.0` represents no adaptation,
255    /// and `1.0` represents that the observer's vision is fully adapted to the
256    /// illuminant. Values outside that range will be clamped.
257    Custom(T),
258}
259
260/// A trait for types that can be used as white point parameters in
261/// [`Parameters`].
262pub trait WhitePointParameter<T> {
263    /// The static representation of this white point, or [`white_point::Any`]
264    /// if it's dynamic.
265    type StaticWp;
266
267    /// Returns the XYZ value for this white point.
268    fn into_xyz(self) -> Xyz<white_point::Any, T>;
269}
270
271impl<T> WhitePointParameter<T> for Xyz<white_point::Any, T> {
272    type StaticWp = white_point::Any;
273
274    fn into_xyz(self) -> Xyz<white_point::Any, T> {
275        self
276    }
277}
278
279/// Represents a static white point in [`Parameters`], as opposed to a dynamic
280/// [`Xyz`] value.
281pub struct StaticWp<Wp>(PhantomData<Wp>);
282
283impl<T, Wp> WhitePointParameter<T> for StaticWp<Wp>
284where
285    Wp: WhitePoint<T>,
286{
287    type StaticWp = Wp;
288
289    fn into_xyz(self) -> Xyz<white_point::Any, T> {
290        Wp::get_xyz()
291    }
292}
293
294impl<Wp> Clone for StaticWp<Wp> {
295    fn clone(&self) -> Self {
296        *self
297    }
298}
299
300impl<Wp> Copy for StaticWp<Wp> {}