colr-types 0.3.1

Color model ZSTs and marker traits for colr.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Spectral color space models and traits.

#![allow(clippy::excessive_precision)]

use core::marker::PhantomData;

use crate::BackingStore;
use crate::observer::{Cie1931, StandardObserver};

/// Identifies the physical quantity encoded by a spectral color value.
///
/// The four variants are mutually exclusive at the type level: a `Spectral<N, G, K>`
/// can only carry one kind. The blanket traits [`Radiance`], [`Reflectance`],
/// [`Transmittance`], and[`Bispectral`] are derived from this via blanket impls
/// so bounds can name the concept rather than the marker.
///
/// Storage note: [`IsBispectral`] is the odd one out. The other three kinds store
/// N independent spectral samples in `[f32; N]`. A bispectral measurement is an
/// N×N matrix (or a reduced form of one) and cannot be meaningfully carried in
/// `[f32; N]`. For that reason `Spectral<N, G, IsBispectral>` deliberately does
/// not implement `BackingStore<[f32; N]>`. See [`IsBispectral`] for details.
pub trait SpectralKind: 'static {}

/// Spectral kind marker for spectral radiance, unbounded above.
///
/// Used for emissive spectra: light sources, illuminants, display primaries.
/// Converting to XYZ is a direct dot product of the SPD with the CMFs.
#[derive(Debug, Clone, Copy)]
pub struct IsRadiance;

/// Spectral kind marker for spectral reflectance in [0, 1].
///
/// Encodes the fraction of incident light reflected at each wavelength.
/// Converting to XYZ requires an illuminant SPD; the dot product is
/// (illuminant * reflectance) dot CMF, not just reflectance dot CMF.
#[derive(Debug, Clone, Copy)]
pub struct IsReflectance;

/// Spectral kind marker for spectral transmittance in[0, 1].
///
/// Encodes the fraction of incident light transmitted at each wavelength.
/// Physically identical in structure to reflectance; distinct at the type
/// level because the two quantities are not interchangeable in a pipeline.
#[derive(Debug, Clone, Copy)]
pub struct IsTransmittance;

/// Spectral kind marker for bispectral (fluorescent) reflectance.
///
/// A bispectral measurement captures re-emission: how much energy absorbed
/// at one wavelength is re-emitted at another. The canonical representation
/// is the Donaldson matrix, an N-by-N array where entry \[i\]\[j\] is the
/// bispectral reflectance factor from band i to band j. Stokes' law
/// (emitted wavelength >= absorbed wavelength) makes the upper triangle zero,
/// but the full matrix is still N-by-N, not the N-by-1 vector the other kinds use.
///
/// Other compact representations exist (factored excitation/emission spectra,
/// single-peak Gaussian approximations, lower-triangular packing) but they
/// all have different storage shapes. Bispectral storage is intentionally left
/// to a future explicit design; this marker is present to close the kind
/// taxonomy and allow bounds to express "not bispectral".
#[derive(Debug, Clone, Copy)]
pub struct IsBispectral;

impl SpectralKind for IsRadiance {}
impl SpectralKind for IsReflectance {}
impl SpectralKind for IsTransmittance {}
impl SpectralKind for IsBispectral {}

/// Implemented by any spectral model that declares a physical kind.
///
/// The blanket alias traits below (`Radiance`, `Reflectance`, etc.) let
/// bounds name the concept without spelling out the associated type.
pub trait SpectralSpace: 'static {
    /// The physical quantity this space encodes.
    type Kind: SpectralKind;
}

/// Blanket alias: any spectral space whose kind is[`IsRadiance`].
pub trait Radiance: SpectralSpace<Kind = IsRadiance> {}
impl<M: SpectralSpace<Kind = IsRadiance>> Radiance for M {}

/// Blanket alias: any spectral space whose kind is [`IsReflectance`].
pub trait Reflectance: SpectralSpace<Kind = IsReflectance> {}
impl<M: SpectralSpace<Kind = IsReflectance>> Reflectance for M {}

/// Blanket alias: any spectral space whose kind is [`IsTransmittance`].
pub trait Transmittance: SpectralSpace<Kind = IsTransmittance> {}
impl<M: SpectralSpace<Kind = IsTransmittance>> Transmittance for M {}

/// Blanket alias: any spectral space whose kind is [`IsBispectral`].
pub trait Bispectral: SpectralSpace<Kind = IsBispectral> {}
impl<M: SpectralSpace<Kind = IsBispectral>> Bispectral for M {}

/// A physical wavelength axis.
///
/// # Design notes
///
/// **BANDS is on the trait, not the implementing type.** Ideally BANDS would
/// be an associated constant so that `G::BANDS` could be used as a const
/// generic argument and the band count would be fully implied by the grid.
/// Current Rust does not allow associated constants to appear as const generic
/// arguments in type-level positions (e.g. `[f32; G::BANDS]`), so the band
/// count is expressed as a const generic on the trait instead. Each concrete
/// grid type is expected to implement this trait for exactly one value of BANDS
/// (the one consistent with its physical definition), but the compiler cannot
/// enforce that invariant today. Type aliases such as [`Spectral41Radiance`]
/// exist precisely to hide the redundant BANDS argument from call sites.
///
/// **Separation of Physics and Psychophysics.** This trait represents only the
/// physical wavelength axis. Human perception is layered on top via the
/// [`ColorMatchingFunctions`] trait, which maps standard observers to grids.
/// This allows a single physical spectrum to be integrated into XYZ under
/// multiple different observers without changing the underlying spectral type.
pub trait WavelengthGrid<const BANDS: usize>: 'static {
    /// First wavelength in nanometers.
    const START_NM: f32;
    /// Wavelength interval in nanometers.
    const STEP_NM: f32;
}

/// Color matching functions for a specific observer evaluated on a specific grid.
pub trait ColorMatchingFunctions<const BANDS: usize, O: StandardObserver>:
    WavelengthGrid<BANDS>
{
    /// x-bar(lambda) for `O` sampled at each band on this grid.
    const CMF_X: &'static [f32; BANDS];
    /// y-bar(lambda) for `O` sampled at each band on this grid.
    const CMF_Y: &'static [f32; BANDS];
    /// z-bar(lambda) for `O` sampled at each band on this grid.
    const CMF_Z: &'static [f32; BANDS];
}

/// A spectral color value on grid `G` encoding physical quantity `K`.
///
/// The storage type is `[f32; BANDS]`. BANDS must match the single value for
/// which `G` implements `WavelengthGrid`; the bound `G: WavelengthGrid<BANDS>`
/// enforces consistency. Use the provided type aliases rather than naming
/// `Spectral` directly.
///
/// `Spectral<N, G, IsBispectral>` does **not** implement `BackingStore<[f32; N]>`
/// because bispectral data is N-by-N, not N-by-1. See [`IsBispectral`] for context.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Spectral<const BANDS: usize, G, K>(PhantomData<(G, K)>);

impl<const BANDS: usize, G, K> SpectralSpace for Spectral<BANDS, G, K>
where
    G: WavelengthGrid<BANDS>,
    K: SpectralKind,
{
    type Kind = K;
}

impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsRadiance> where
    G: WavelengthGrid<BANDS>
{
}

impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsReflectance> where
    G: WavelengthGrid<BANDS>
{
}

impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsTransmittance> where
    G: WavelengthGrid<BANDS>
{
}

/// CIE 10 nm spectral grid, 380–780 nm (41 bands).
///
/// Covers the full practical visible range at the coarser 10 nm step.
/// Suitable for colorimetry where 10 nm resolution is acceptable and
/// compact storage is preferred.
#[derive(Debug, Clone, Copy)]
pub struct Grid380_780_10nm;

impl WavelengthGrid<41> for Grid380_780_10nm {
    const START_NM: f32 = 380.0;
    const STEP_NM: f32 = 10.0;
}

impl ColorMatchingFunctions<41, Cie1931> for Grid380_780_10nm {
    #[rustfmt::skip]
    const CMF_X: &'static [f32; 41] = &[
        0.001368, 0.004243, 0.014310, 0.043510, 0.134380,
        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
        0.046770, 0.022700, 0.011359, 0.005790, 0.002899,
        0.001440, 0.000690, 0.000332, 0.000166, 0.000083,
        0.000042,
    ];
    #[rustfmt::skip]
    const CMF_Y: &'static [f32; 41] = &[
        0.000039, 0.000120, 0.000396, 0.001210, 0.004000,
        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
        0.017000, 0.008210, 0.004102, 0.002091, 0.001047,
        0.000520, 0.000249, 0.000120, 0.000060, 0.000030,
        0.000015,
    ];
    #[rustfmt::skip]
    const CMF_Z: &'static[f32; 41] = &[
        0.006450, 0.020050, 0.067850, 0.207400, 0.645600,
        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000,
    ];
}

/// CIE 10 nm spectral grid, 380–730 nm (36 bands).
///
/// The 36-band grid used in Stam's spectral rendering framework (1999).
/// Drops the 740–780 nm tail, which contributes negligible energy under
/// most natural and artificial illuminants.
#[derive(Debug, Clone, Copy)]
pub struct Grid380_730_10nm;

impl WavelengthGrid<36> for Grid380_730_10nm {
    const START_NM: f32 = 380.0;
    const STEP_NM: f32 = 10.0;
}

impl ColorMatchingFunctions<36, Cie1931> for Grid380_730_10nm {
    #[rustfmt::skip]
    const CMF_X: &'static[f32; 36] = &[
        0.001368, 0.004243, 0.014310, 0.043510, 0.134380,
        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
        0.046770, 0.022700, 0.011359, 0.005790, 0.002899,
        0.001440,
    ];
    #[rustfmt::skip]
    const CMF_Y: &'static [f32; 36] = &[
        0.000039, 0.000120, 0.000396, 0.001210, 0.004000,
        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
        0.017000, 0.008210, 0.004102, 0.002091, 0.001047,
        0.000520,
    ];
    #[rustfmt::skip]
    const CMF_Z: &'static [f32; 36] = &[
        0.006450, 0.020050, 0.067850, 0.207400, 0.645600,
        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000,
    ];
}

/// CIE 10 nm spectral grid, 400–700 nm (31 bands).
///
/// The ICC spectral measurement standard (ISO 13655). Also the basis of
/// the Munsell atlas and most reflectance spectrophotometer outputs.
#[derive(Debug, Clone, Copy)]
pub struct Grid400_700_10nm;

impl WavelengthGrid<31> for Grid400_700_10nm {
    const START_NM: f32 = 400.0;
    const STEP_NM: f32 = 10.0;
}

impl ColorMatchingFunctions<31, Cie1931> for Grid400_700_10nm {
    #[rustfmt::skip]
    const CMF_X: &'static [f32; 31] = &[
        0.014310, 0.043510, 0.134380,
        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
        0.046770, 0.022700, 0.011359,
    ];
    #[rustfmt::skip]
    const CMF_Y: &'static [f32; 31] = &[
        0.000396, 0.001210, 0.004000,
        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
        0.017000, 0.008210, 0.004102,
    ];
    #[rustfmt::skip]
    const CMF_Z: &'static [f32; 31] = &[
        0.067850, 0.207400, 0.645600,
        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000,
    ];
}

/// CIE 5 nm spectral grid, 380–780 nm (81 bands).
///
/// The CIE practical standard grid (CIE 015:2018). Preferred when 10 nm
/// sampling introduces visible interpolation artifacts, particularly in
/// the blue-violet region where the CMFs change rapidly.
#[derive(Debug, Clone, Copy)]
pub struct Grid380_780_5nm;

impl WavelengthGrid<81> for Grid380_780_5nm {
    const START_NM: f32 = 380.0;
    const STEP_NM: f32 = 5.0;
}

impl ColorMatchingFunctions<81, Cie1931> for Grid380_780_5nm {
    #[rustfmt::skip]
    const CMF_X: &'static [f32; 81] = &[
        0.001368, 0.002236, 0.004243, 0.007650, 0.014310,
        0.023190, 0.043510, 0.077630, 0.134380, 0.214770,
        0.283900, 0.328500, 0.348280, 0.348060, 0.336200,
        0.318700, 0.290800, 0.251100, 0.195360, 0.142100,
        0.095640, 0.057950, 0.032010, 0.014700, 0.004900,
        0.002400, 0.009300, 0.029100, 0.063270, 0.109600,
        0.165500, 0.225750, 0.290400, 0.359700, 0.433450,
        0.512050, 0.594500, 0.678400, 0.762100, 0.842500,
        0.916300, 0.978600, 1.026300, 1.056700, 1.062200,
        1.045600, 1.002600, 0.938400, 0.854450, 0.751400,
        0.642400, 0.541900, 0.447900, 0.360800, 0.283500,
        0.218700, 0.164900, 0.121200, 0.087400, 0.063600,
        0.046770, 0.032900, 0.022700, 0.015840, 0.011359,
        0.008111, 0.005790, 0.004109, 0.002899, 0.002049,
        0.001440, 0.001000, 0.000690, 0.000476, 0.000332,
        0.000235, 0.000166, 0.000117, 0.000083, 0.000059,
        0.000042,
    ];
    #[rustfmt::skip]
    const CMF_Y: &'static [f32; 81] = &[
        0.000039, 0.000064, 0.000120, 0.000217, 0.000396,
        0.000640, 0.001210, 0.002180, 0.004000, 0.007300,
        0.011600, 0.016840, 0.023000, 0.029800, 0.038000,
        0.048000, 0.060000, 0.073900, 0.090980, 0.112600,
        0.139020, 0.169300, 0.208020, 0.258600, 0.323000,
        0.407300, 0.503000, 0.608200, 0.710000, 0.793200,
        0.862000, 0.914850, 0.954000, 0.980300, 0.994950,
        1.000000, 0.995000, 0.978600, 0.952000, 0.915400,
        0.870000, 0.816300, 0.757000, 0.694900, 0.631000,
        0.566800, 0.503000, 0.441200, 0.381000, 0.321000,
        0.265000, 0.217000, 0.175000, 0.138200, 0.107000,
        0.081600, 0.061000, 0.044580, 0.032000, 0.023200,
        0.017000, 0.011920, 0.008210, 0.005723, 0.004102,
        0.002929, 0.002091, 0.001484, 0.001047, 0.000740,
        0.000520, 0.000361, 0.000249, 0.000172, 0.000120,
        0.000085, 0.000060, 0.000042, 0.000030, 0.000021,
        0.000015,
    ];
    #[rustfmt::skip]
    const CMF_Z: &'static [f32; 81] = &[
        0.006450, 0.010550, 0.020050, 0.036210, 0.067850,
        0.110200, 0.207400, 0.371300, 0.645600, 1.039050,
        1.385600, 1.622960, 1.747060, 1.782600, 1.772110,
        1.744100, 1.669200, 1.528100, 1.287640, 1.041900,
        0.812950, 0.616200, 0.465180, 0.353300, 0.272000,
        0.212300, 0.158200, 0.111700, 0.078250, 0.057250,
        0.042160, 0.029840, 0.020300, 0.013400, 0.008750,
        0.005750, 0.003900, 0.002750, 0.002100, 0.001800,
        0.001650, 0.001400, 0.001100, 0.001000, 0.000800,
        0.000600, 0.000340, 0.000240, 0.000190, 0.000100,
        0.000050, 0.000030, 0.000020, 0.000010, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
        0.000000,
    ];
}

/// Spectral radiance on the 31-band ICC grid (400–700 nm, 10 nm).
pub type Spectral31Radiance = Spectral<31, Grid400_700_10nm, IsRadiance>;
/// Spectral reflectance on the 31-band ICC grid (400–700 nm, 10 nm).
pub type Spectral31Reflectance = Spectral<31, Grid400_700_10nm, IsReflectance>;
/// Spectral transmittance on the 31-band ICC grid (400–700 nm, 10 nm).
pub type Spectral31Transmittance = Spectral<31, Grid400_700_10nm, IsTransmittance>;
/// Spectral radiance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
pub type Spectral36Radiance = Spectral<36, Grid380_730_10nm, IsRadiance>;
/// Spectral reflectance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
pub type Spectral36Reflectance = Spectral<36, Grid380_730_10nm, IsReflectance>;
/// Spectral transmittance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
pub type Spectral36Transmittance = Spectral<36, Grid380_730_10nm, IsTransmittance>;
/// Spectral radiance on the 41-band full-visible grid (380–780 nm, 10 nm).
pub type Spectral41Radiance = Spectral<41, Grid380_780_10nm, IsRadiance>;
/// Spectral reflectance on the 41-band full-visible grid (380–780 nm, 10 nm).
pub type Spectral41Reflectance = Spectral<41, Grid380_780_10nm, IsReflectance>;
/// Spectral transmittance on the 41-band full-visible grid (380–780 nm, 10 nm).
pub type Spectral41Transmittance = Spectral<41, Grid380_780_10nm, IsTransmittance>;
/// Spectral radiance on the 81-band CIE standard grid (380–780 nm, 5 nm).
pub type Spectral81Radiance = Spectral<81, Grid380_780_5nm, IsRadiance>;
/// Spectral reflectance on the 81-band CIE standard grid (380–780 nm, 5 nm).
pub type Spectral81Reflectance = Spectral<81, Grid380_780_5nm, IsReflectance>;
/// Spectral transmittance on the 81-band CIE standard grid (380–780 nm, 5 nm).
pub type Spectral81Transmittance = Spectral<81, Grid380_780_5nm, IsTransmittance>;