palette/lms/
lms.rs

1use core::marker::PhantomData;
2
3use crate::{
4    bool_mask::HasBoolMask,
5    convert::{ConvertOnce, FromColorUnclamped, Matrix3},
6    num::{Arithmetics, Zero},
7    stimulus::{FromStimulus, Stimulus, StimulusColor},
8    xyz::meta::HasXyzMeta,
9    Alpha, Xyz,
10};
11
12use super::matrix::{HasLmsMatrix, XyzToLms};
13
14/// Generic LMS with an alpha component. See [`Lmsa` implementation in
15/// `Alpha`][crate::Alpha#Lmsa].
16pub type Lmsa<M, T> = Alpha<Lms<M, T>, T>;
17
18/// Generic LMS.
19///
20/// LMS represents the response of the eye's cone cells. L, M and S are for
21/// "long", "medium" and "short" wavelengths, roughly corresponding to red,
22/// green and blue. Many newer mentions of an LMS representation use the letters
23/// R, G and B instead (or sometimes ρ, γ, β), but this library sticks to LMS to
24/// differentiate it from [`Rgb`][crate::rgb::Rgb].
25///
26/// The LMS color space is a model of the physiological response to color
27/// stimuli. It has some mathematical shortcomings that [`Xyz`] improves on,
28/// such as severe spectral sensitivity overlap between L, M and S. Despite
29/// this, LMS has a lot of uses, include chromatic adaptation and emulating
30/// different types of color vision deficiency, and it's sometimes part of the
31/// conversion process between other color spaces.
32///
33/// # Creating a Value
34///
35/// An LMS value is often derived from another color space, through a conversion
36/// matrix. Two such matrices are [`VonKries`][super::matrix::VonKries] and
37/// [`Bradford`][super::matrix::Bradford], and Palette offers type aliases in the
38/// [`lms`][crate::lms] module to make using them a bit more convenient. It's of
39/// course also possible to simply use [`Lms::new`], but it may not be as
40/// intuitive.
41///
42/// ```
43/// use palette::{
44///     lms::{Lms, VonKriesLms, matrix::VonKries},
45///     white_point::D65,
46///     Srgb, FromColor
47/// };
48///
49/// let von_kries_lms = Lms::<VonKries, f32>::new(0.1, 0.2, 0.3);
50/// let von_kries_d65_lms = VonKriesLms::<D65, f32>::new(0.1, 0.2, 0.3);
51///
52/// // `new` is also `const`:
53/// const VON_KRIES_LMS: Lms<VonKries, f32> = Lms::new(0.1, 0.2, 0.3);
54///
55/// // Von Kries LMS from sRGB:
56/// let lms_from_srgb = VonKriesLms::<D65, f32>::from_color(Srgb::new(0.3f32, 0.8, 0.1));
57///
58/// // It's also possible to convert from (and to) arrays and tuples:
59/// let lms_from_array = VonKriesLms::<D65, f32>::from([0.1, 0.2, 0.3]);
60/// let lms_from_tuple = VonKriesLms::<D65, f32>::from((0.1, 0.2, 0.3));
61/// ```
62#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
63#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
64#[palette(palette_internal, component = "T", skip_derives(Lms, Xyz))]
65#[repr(C)]
66pub struct Lms<M, T> {
67    /// Stimulus from long wavelengths, or red, or ρ. The typical range is
68    /// between 0.0 and 1.0, but it doesn't have an actual upper bound.
69    pub long: T,
70
71    /// Stimulus from medium wavelengths, or green, or γ. The typical range is
72    /// between 0.0 and 1.0, but it doesn't have an actual upper bound.
73    pub medium: T,
74
75    /// Stimulus from short wavelengths, or blue, or β. The typical range is
76    /// between 0.0 and 1.0, but it doesn't have an actual upper bound.
77    pub short: T,
78
79    /// Type level meta information, such as reference white, or which matrix
80    /// was used when converting from XYZ.
81    #[cfg_attr(feature = "serializing", serde(skip))]
82    #[palette(unsafe_zero_sized)]
83    pub meta: PhantomData<M>,
84}
85
86impl<M, T> Lms<M, T> {
87    /// Create a new LMS color.
88    pub const fn new(long: T, medium: T, short: T) -> Self {
89        Self {
90            long,
91            medium,
92            short,
93            meta: PhantomData,
94        }
95    }
96
97    /// Convert the LMS components into another number type.
98    ///
99    /// ```
100    /// use palette::{
101    ///     lms::VonKriesLms,
102    ///     white_point::D65,
103    /// };
104    ///
105    /// let lms_f64: VonKriesLms<D65, f64> = VonKriesLms::new(0.3f32, 0.7, 0.2).into_format();
106    /// ```
107    pub fn into_format<U>(self) -> Lms<M, U>
108    where
109        U: FromStimulus<T>,
110    {
111        Lms {
112            long: U::from_stimulus(self.long),
113            medium: U::from_stimulus(self.medium),
114            short: U::from_stimulus(self.short),
115            meta: PhantomData,
116        }
117    }
118
119    /// Convert the LMS components from another number type.
120    ///
121    /// ```
122    /// use palette::{
123    ///     lms::VonKriesLms,
124    ///     white_point::D65,
125    /// };
126    ///
127    /// let lms_f64 = VonKriesLms::<D65, f64>::from_format(VonKriesLms::new(0.3f32, 0.7, 0.2));
128    /// ```
129    pub fn from_format<U>(color: Lms<M, U>) -> Self
130    where
131        T: FromStimulus<U>,
132    {
133        color.into_format()
134    }
135
136    /// Convert to a `(long, medium, short)` tuple.
137    pub fn into_components(self) -> (T, T, T) {
138        (self.long, self.medium, self.short)
139    }
140
141    /// Convert from a `(long, medium, short)` tuple.
142    pub fn from_components((long, medium, short): (T, T, T)) -> Self {
143        Self::new(long, medium, short)
144    }
145
146    /// Changes the meta type without changing the color value.
147    ///
148    /// This function doesn't change the numerical values, and thus the stimuli
149    /// it represents in an absolute sense. However, the appearance of the color
150    /// may not be the same. The effect may be similar to taking a photo with an
151    /// incorrect white balance.
152    pub fn with_meta<NewM>(self) -> Lms<NewM, T> {
153        Lms {
154            long: self.long,
155            medium: self.medium,
156            short: self.short,
157            meta: PhantomData,
158        }
159    }
160}
161
162impl<M, T> Lms<M, T>
163where
164    T: Zero,
165{
166    /// Return the `short` value minimum.
167    pub fn min_short() -> T {
168        T::zero()
169    }
170
171    /// Return the `medium` value minimum.
172    pub fn min_medium() -> T {
173        T::zero()
174    }
175
176    /// Return the `long` value minimum.
177    pub fn min_long() -> T {
178        T::zero()
179    }
180}
181
182impl<M, T> Lms<M, T> {
183    /// Produce a conversion matrix from [`Xyz`] to [`Lms`].
184    #[inline]
185    pub fn matrix_from_xyz() -> Matrix3<Xyz<M::XyzMeta, T>, Self>
186    where
187        M: HasXyzMeta + HasLmsMatrix,
188        M::LmsMatrix: XyzToLms<T>,
189    {
190        Matrix3::from_array(M::LmsMatrix::xyz_to_lms_matrix())
191    }
192}
193
194/// <span id="Lmsa"></span>[`Lmsa`][Lmsa] implementations.
195impl<S, T, A> Alpha<Lms<S, T>, A> {
196    /// Create an LMSA color.
197    pub const fn new(red: T, green: T, blue: T, alpha: A) -> Self {
198        Alpha {
199            color: Lms::new(red, green, blue),
200            alpha,
201        }
202    }
203
204    /// Convert the LMSA components into other number types.
205    ///
206    /// ```
207    /// use palette::{
208    ///     lms::VonKriesLmsa,
209    ///     white_point::D65,
210    /// };
211    ///
212    /// let lmsa_f64: VonKriesLmsa<D65, f64> = VonKriesLmsa::new(0.3f32, 0.7, 0.2, 0.5).into_format();
213    /// ```
214    pub fn into_format<U, B>(self) -> Alpha<Lms<S, U>, B>
215    where
216        U: FromStimulus<T>,
217        B: FromStimulus<A>,
218    {
219        Alpha {
220            color: self.color.into_format(),
221            alpha: B::from_stimulus(self.alpha),
222        }
223    }
224
225    /// Convert the LMSA components from other number types.
226    ///
227    /// ```
228    /// use palette::{
229    ///     lms::VonKriesLmsa,
230    ///     white_point::D65,
231    /// };
232    ///
233    /// let lmsa_f64 = VonKriesLmsa::<D65, f64>::from_format(VonKriesLmsa::new(0.3f32, 0.7, 0.2, 0.5));
234    /// ```
235    pub fn from_format<U, B>(color: Alpha<Lms<S, U>, B>) -> Self
236    where
237        T: FromStimulus<U>,
238        A: FromStimulus<B>,
239    {
240        color.into_format()
241    }
242
243    /// Convert to a `(long, medium, short, alpha)` tuple.
244    pub fn into_components(self) -> (T, T, T, A) {
245        (
246            self.color.long,
247            self.color.medium,
248            self.color.short,
249            self.alpha,
250        )
251    }
252
253    /// Convert from a `(long, medium, short, alpha)` tuple.
254    pub fn from_components((long, medium, short, alpha): (T, T, T, A)) -> Self {
255        Self::new(long, medium, short, alpha)
256    }
257
258    /// Changes the meta type without changing the color value.
259    ///
260    /// This function doesn't change the numerical values, and thus the stimuli
261    /// it represents in an absolute sense. However, the appearance of the color
262    /// may not be the same. The effect may be similar to taking a photo with an
263    /// incorrect white balance.
264    pub fn with_meta<NewM>(self) -> Alpha<Lms<NewM, T>, A> {
265        Alpha {
266            color: self.color.with_meta(),
267            alpha: self.alpha,
268        }
269    }
270}
271
272impl<M, T> FromColorUnclamped<Lms<M, T>> for Lms<M, T> {
273    #[inline]
274    fn from_color_unclamped(val: Lms<M, T>) -> Self {
275        val
276    }
277}
278
279impl<M, T> FromColorUnclamped<Xyz<M::XyzMeta, T>> for Lms<M, T>
280where
281    M: HasLmsMatrix + HasXyzMeta,
282    M::LmsMatrix: XyzToLms<T>,
283    T: Arithmetics,
284{
285    #[inline]
286    fn from_color_unclamped(val: Xyz<M::XyzMeta, T>) -> Self {
287        Self::matrix_from_xyz().convert_once(val)
288    }
289}
290
291impl<M, T> StimulusColor for Lms<M, T> where T: Stimulus {}
292
293impl<M, T> HasBoolMask for Lms<M, T>
294where
295    T: HasBoolMask,
296{
297    type Mask = T::Mask;
298}
299
300impl<M, T> Default for Lms<M, T>
301where
302    T: Default,
303{
304    fn default() -> Lms<M, T> {
305        Lms::new(T::default(), T::default(), T::default())
306    }
307}
308
309impl<M> From<Lms<M, f32>> for Lms<M, f64> {
310    #[inline]
311    fn from(color: Lms<M, f32>) -> Self {
312        color.into_format()
313    }
314}
315
316impl<M> From<Lmsa<M, f32>> for Lmsa<M, f64> {
317    #[inline]
318    fn from(color: Lmsa<M, f32>) -> Self {
319        color.into_format()
320    }
321}
322
323impl<M> From<Lms<M, f64>> for Lms<M, f32> {
324    #[inline]
325    fn from(color: Lms<M, f64>) -> Self {
326        color.into_format()
327    }
328}
329
330impl<M> From<Lmsa<M, f64>> for Lmsa<M, f32> {
331    #[inline]
332    fn from(color: Lmsa<M, f64>) -> Self {
333        color.into_format()
334    }
335}
336
337#[cfg(feature = "bytemuck")]
338unsafe impl<M, T> bytemuck::Zeroable for Lms<M, T> where T: bytemuck::Zeroable {}
339
340#[cfg(feature = "bytemuck")]
341unsafe impl<M: 'static, T> bytemuck::Pod for Lms<M, T> where T: bytemuck::Pod {}
342
343impl_reference_component_methods!(Lms<M>, [long, medium, short], meta);
344impl_struct_of_arrays_methods!(Lms<M>, [long, medium, short], meta);
345
346impl_is_within_bounds! {
347    Lms<M> {
348        long => [Self::min_long(), None],
349        medium => [Self::min_medium(), None],
350        short => [Self::min_short(), None]
351    }
352    where T: Stimulus
353}
354impl_clamp! {
355    Lms<M> {
356        long => [Self::min_long()],
357        medium => [Self::min_medium()],
358        short => [Self::min_short()]
359    }
360    other {meta}
361    where T: Stimulus
362}
363
364impl_mix!(Lms<M>);
365impl_premultiply!(Lms<M> {long, medium, short} phantom: meta);
366impl_euclidean_distance!(Lms<M> {long, medium, short});
367
368impl_color_add!(Lms<M>, [long, medium, short], meta);
369impl_color_sub!(Lms<M>, [long, medium, short], meta);
370impl_color_mul!(Lms<M>, [long, medium, short], meta);
371impl_color_div!(Lms<M>, [long, medium, short], meta);
372
373impl_tuple_conversion!(Lms<M> as (T, T, T));
374impl_array_casts!(Lms<M, T>, [T; 3]);
375impl_simd_array_conversion!(Lms<M>, [long, medium, short], meta);
376impl_struct_of_array_traits!(Lms<M>, [long, medium, short], meta);
377
378impl_eq!(Lms<M>, [long, medium, short]);
379impl_copy_clone!(Lms<M>, [long, medium, short], meta);
380
381impl_rand_traits_cartesian!(UniformLms, Lms<M> {long, medium, short} phantom: meta: PhantomData<M>);
382
383#[cfg(test)]
384mod test {
385    use crate::{lms::VonKriesLms, white_point::D65};
386
387    #[cfg(feature = "alloc")]
388    use super::Lmsa;
389
390    #[cfg(feature = "random")]
391    use super::Lms;
392
393    #[cfg(feature = "approx")]
394    use crate::{convert::FromColorUnclamped, lms::BradfordLms, Xyz};
395
396    test_convert_into_from_xyz!(VonKriesLms<D65, f32>);
397    raw_pixel_conversion_tests!(VonKriesLms<D65>: long, medium, short);
398    raw_pixel_conversion_fail_tests!(VonKriesLms<D65>: long, medium, short);
399
400    #[cfg(feature = "approx")]
401    #[test]
402    fn von_kries_xyz_roundtrip() {
403        let xyz = Xyz::new(0.2f32, 0.4, 0.8);
404        let lms = VonKriesLms::<D65, _>::from_color_unclamped(xyz);
405        assert_relative_eq!(Xyz::from_color_unclamped(lms), xyz);
406    }
407
408    #[cfg(feature = "approx")]
409    #[test]
410    fn bradford_xyz_roundtrip() {
411        let xyz = Xyz::new(0.2f32, 0.4, 0.8);
412        let lms = BradfordLms::<D65, _>::from_color_unclamped(xyz);
413        assert_relative_eq!(Xyz::from_color_unclamped(lms), xyz);
414    }
415
416    #[cfg(feature = "serializing")]
417    #[test]
418    fn serialize() {
419        let serialized =
420            ::serde_json::to_string(&VonKriesLms::<D65, f32>::new(0.3, 0.8, 0.1)).unwrap();
421
422        assert_eq!(serialized, r#"{"long":0.3,"medium":0.8,"short":0.1}"#);
423    }
424
425    #[cfg(feature = "serializing")]
426    #[test]
427    fn deserialize() {
428        let deserialized: VonKriesLms<D65, f32> =
429            ::serde_json::from_str(r#"{"long":0.3,"medium":0.8,"short":0.1}"#).unwrap();
430
431        assert_eq!(deserialized, VonKriesLms::<D65, f32>::new(0.3, 0.8, 0.1));
432    }
433
434    struct_of_arrays_tests!(
435        VonKriesLms<D65>[long, medium, short] phantom: meta,
436        Lmsa::new(0.1f32, 0.2, 0.3, 0.4),
437        Lmsa::new(0.2, 0.3, 0.4, 0.5),
438        Lmsa::new(0.3, 0.4, 0.5, 0.6)
439    );
440
441    test_uniform_distribution! {
442        VonKriesLms<D65, f32> {
443            long: (0.0, 1.0),
444            medium: (0.0, 1.0),
445            short: (0.0, 1.0)
446        },
447        min: Lms::new(0.0f32, 0.0, 0.0),
448        max: Lms::new(1.0, 1.0, 1.0)
449    }
450}