Skip to main content

chromashift/
gamut.rs

1use crate::*;
2
3/// Whether a colour is within the natural bounds of its colour space, and the ability to produce a naively clamped
4/// version or a perceptually gamut-mapped version.
5///
6/// CSS Color 4 12.1 and 13.1 state that out-of-gamut values must be preserved through intermediate computations. Gamut
7/// mapping (reducing to displayable range) only happens at "actual-value" / display time. This trait lets callers query
8/// and map when appropriate.
9pub trait Gamut: Sized {
10	/// Returns `true` if all colour channels are within the natural bounds of this colour space.
11	/// Alpha is not considered — it is always clamped on construction.
12	fn in_gamut(&self) -> bool;
13
14	/// Returns a copy with all colour channels naively clamped to the natural bounds.
15	///
16	/// This is simple per-channel clipping for fast but less perceptually pleasing results.
17	fn clamp_to_gamut(&self) -> Self;
18
19	/// Perceptually maps this colour into gamut.
20	///
21	/// For RGB-based colour spaces this should use the ray trace algorithm in CSS Color 4 13.2, to casting a ray from an
22	/// achromatic anchor toward the out-of-gamut colour and finding the intersection with the gamut's RGB cube via the
23	/// slab method.
24	///
25	/// Reference: <https://facelessuser.github.io/coloraide/gamut/#ray-tracing-chroma-reduction>
26	/// CSS Color 4 spec: <https://drafts.csswg.org/css-color-4/#pseudo-raytrace>
27	fn map_to_gamut(self) -> Self;
28}
29
30/// CSS Color 4 13.2.6 ray trace gamut mapping, steps 3–14.
31///
32/// Caller has already performed steps 1–2 (in-gamut check and conversion to OkLCh).
33///
34/// <https://drafts.csswg.org/css-color-4/#pseudo-raytrace>
35fn raytrace(oklch: Oklch, destination: &RgbSpace) -> [f64; 3] {
36	let alpha = oklch.alpha;
37	let to_destination = XyzD65::WHITE.adapt_to(destination.white).then(&destination.from_xyz);
38	let from_destination = destination.to_xyz.then(&destination.white.adapt_to(XyzD65::WHITE));
39	let to_linear = |oklch: Oklch| {
40		let XyzD65 { x, y, z, .. } = XyzD65::from(Oklab::from(oklch));
41		to_destination.transform([x / 100.0, y / 100.0, z / 100.0])
42	};
43
44	// 3. if the Lightness of |origin_OkLCh| is >= 100%, return white.
45	if oklch.lightness >= 1.0 {
46		return [1.0, 1.0, 1.0];
47	}
48	// 4. if the Lightness of |origin_OkLCh| is <= 0%, return black.
49	if oklch.lightness <= 0.0 {
50		return [0.0, 0.0, 0.0];
51	}
52
53	// 5. let |l_origin| be the OkLCh lightness of |origin_OkLCh|.
54	let l_origin = oklch.lightness;
55
56	// 6. let |h_origin| be the OkLCh hue of |origin_OkLCh|.
57	let h_origin = oklch.hue;
58
59	// 7. let |anchor| be an achromatic OkLCh color (l_origin, 0, h_origin),
60	//    converted to the linear-light form of |destination|.
61	let mut anchor = to_linear(Oklch::new(l_origin, 0.0, h_origin, alpha));
62
63	// 8. let |origin_rgb| be |origin_OkLCh| converted to the linear-light form of |destination|.
64	let mut origin_rgb = to_linear(oklch);
65
66	// 9. let |low| be 1E-6.
67	let low = 1e-6;
68
69	// 10. let |high| be 1.0 - |low|.
70	let high = 1.0 - low;
71
72	// 11. let |last| be |origin_rgb|.
73	let mut last = origin_rgb;
74
75	// 12. for (i=0; i<4; i++)
76	for i in 0..4 {
77		// 12.1. if (i > 0)
78		if i > 0 {
79			// 12.1.1. let |current_OkLCh| be |origin_rgb| converted to OkLCh.
80			let [x, y, z] = from_destination.transform(origin_rgb);
81			let mut current_oklch = Oklch::from(Oklab::from(XyzD65::new(x * 100.0, y * 100.0, z * 100.0, alpha)));
82
83			// 12.1.2. let the lightness of |current_OkLCh| be |l_origin|.
84			current_oklch.lightness = l_origin;
85
86			// 12.1.3. let the hue of |current_OkLCh| be |h_origin|.
87			current_oklch.hue = h_origin;
88
89			// 12.1.4. let |origin_rgb| be |current_OkLCh| converted to the linear-light
90			//         form of |destination|.
91			origin_rgb = to_linear(current_oklch);
92		}
93
94		// 12.2. Cast a ray from |anchor| to |origin_rgb| and let |intersection| be
95		//       the intersection of this ray with the gamut boundary.
96		let intersection = raytrace_box(&anchor, &origin_rgb);
97
98		match intersection {
99			// 12.3. if an intersection was not found, let |origin_rgb| be |last|
100			//       and exit the loop.
101			None => {
102				origin_rgb = last;
103				break;
104			}
105			Some(hit) => {
106				// 12.4. if (i > 0) AND (each component of |origin_rgb| is between
107				//       |low| and |high|) then let |anchor| be |origin_rgb|.
108				if i > 0 && origin_rgb.iter().all(|&x| low < x && x < high) {
109					anchor = origin_rgb;
110				}
111
112				// 12.5. let |origin_rgb| be |intersection|.
113				// 12.6. let |last| be |intersection|.
114				origin_rgb = hit;
115				last = hit;
116			}
117		}
118	}
119
120	// 13. let |clipped| be |origin_rgb| clipped to gamut (components in range 0 to 1),
121	//     trimming off any noise due to floating point inaccuracy.
122	// 14. return |clipped|, converted to |destination| as the gamut mapped color.
123	origin_rgb.map(|channel| channel.clamp(0.0, 1.0))
124}
125
126fn raytrace_to_linear_rgb(oklch: Oklch) -> LinearRgb {
127	let [red, green, blue] = raytrace(oklch, &LinearRgb::SPACE);
128	LinearRgb::new(red, green, blue, oklch.alpha)
129}
130
131/// Implements `map_to_gamut` for an RGB colour type, mapping into the gamut of its own `SPACE`.
132///
133/// Steps 1–2 of the spec algorithm live here; steps 3–14 are in [`raytrace`].
134macro_rules! impl_map_to_gamut_raytrace {
135	($($ty:ident),+ $(,)?) => {
136		$(impl $ty {
137			fn raytrace_map_to_gamut(self) -> Self {
138				// 1. if |origin| is in gamut for |destination|, return it.
139				if self.in_gamut() {
140					return self;
141				}
142
143				// 2. let |origin_OkLCh| be |origin| converted to the OkLCh color space.
144				let oklch = Oklch::from(self);
145
146				// Steps 3–14.
147				let [red, green, blue] = Self::SPACE.transfer.encode_all(raytrace(oklch, &Self::SPACE));
148				Self::new(red, green, blue, self.alpha).clamp_to_gamut()
149			}
150		})+
151	};
152}
153
154impl_map_to_gamut_raytrace!(Srgb, LinearRgb, DisplayP3, A98Rgb, ProphotoRgb, Rec2020);
155
156/// CSS Color 4 13.2.6 "cast a ray" — ray–box intersection using the slab method.
157///
158/// Given a ray from `start` through `end`, finds where it intersects the unit cube [0,1]³.
159/// Returns `None` if no valid intersection exists (parallel miss, behind ray, or degenerate).
160///
161/// <https://drafts.csswg.org/css-color-4/#pseudo-raytrace>
162/// <https://en.wikipedia.org/wiki/Slab_method>
163fn raytrace_box(start: &[f64; 3], end: &[f64; 3]) -> Option<[f64; 3]> {
164	// 1. (bmin and bmax are [0,0,0] and [1,1,1] for unit-range RGB gamuts.)
165
166	// 2. let |tfar| be infinity.
167	let mut tfar = f64::INFINITY;
168
169	// 3. let |tnear| be -infinity.
170	let mut tnear = f64::NEG_INFINITY;
171
172	// 4. let |direction| be a 3-element array.
173	let mut direction = [0.0_f64; 3];
174
175	// 5. for (i = 0; i < 3; i++):
176	for i in 0..3 {
177		let a = start[i]; //     let |a| be |start|[i].
178		let b = end[i]; //       let |b| be |end|[i].
179		let d = b - a; //        let |d| be |b| - |a|.
180		direction[i] = d; //     let |direction|[i] be |d|.
181
182		// if abs(|d|) > 1E-12:
183		// (Corrected per https://github.com/w3c/csswg-drafts/pull/13416 — using an
184		// epsilon to prevent numerical instability when d approaches zero.)
185		if d.abs() > 1e-12 {
186			let inv_d = 1.0 / d; //          let |inv_d| be 1 / |d|.
187			let t1 = (0.0 - a) * inv_d; //   let |t1| be (|bmin|[i] - |a|) * |inv_d|.
188			let t2 = (1.0 - a) * inv_d; //   let |t2| be (|bmax|[i] - |a|) * |inv_d|.
189			tnear = tnear.max(t1.min(t2)); // let |tnear| be max(min(|t1|, |t2|), |tnear|).
190			tfar = tfar.min(t1.max(t2)); //   let |tfar| be min(max(|t1|, |t2|), |tfar|).
191		}
192		// else if (|a| < |bmin|[i] or |a| > |bmax|[i]): return INTERSECTION NOT FOUND.
193		else if !(0.0..=1.0).contains(&a) {
194			return None;
195		}
196	}
197
198	// 6. if (|tnear| > |tfar| or |tfar| < 0): return INTERSECTION NOT FOUND.
199	if tnear > tfar || tfar < 0.0 {
200		return None;
201	}
202
203	// 7. if |tnear| < 0: let |tnear| be |tfar|.
204	//    (Favoring the first intersection in the direction |start| -> |end|.)
205	if tnear < 0.0 {
206		tnear = tfar;
207	}
208
209	// 8. if |tnear| is infinite: return INTERSECTION NOT FOUND.
210	//    (Corrected per https://github.com/w3c/csswg-drafts/pull/13416 — checking
211	//    for infinite rather than an arbitrary threshold.)
212	if !tnear.is_finite() {
213		return None;
214	}
215
216	// 9. for (i = 0; i < 3; i++): let |result|[i] be |start|[i] + |direction|[i] * |tnear|.
217	// 10. return |result|.
218	Some([start[0] + direction[0] * tnear, start[1] + direction[1] * tnear, start[2] + direction[2] * tnear])
219}
220
221const GAMUT_EPSILON: f64 = 1e-6;
222
223/// Helper: checks an f64 is in [0.0, 1.0] with [`GAMUT_EPSILON`] tolerance.
224fn in_unit(v: f64) -> bool {
225	(-GAMUT_EPSILON..=1.0 + GAMUT_EPSILON).contains(&v)
226}
227
228/// Helper: checks an f64 is in [0.0, 100.0]
229fn in_percent(v: f64) -> bool {
230	(0.0..=100.0).contains(&v)
231}
232
233macro_rules! impl_gamut_rgb_f64 {
234	($ty:ident) => {
235		impl Gamut for $ty {
236			fn in_gamut(&self) -> bool {
237				in_unit(self.red) && in_unit(self.green) && in_unit(self.blue)
238			}
239
240			fn clamp_to_gamut(&self) -> Self {
241				Self::new(self.red.clamp(0.0, 1.0), self.green.clamp(0.0, 1.0), self.blue.clamp(0.0, 1.0), self.alpha)
242			}
243
244			fn map_to_gamut(self) -> Self {
245				self.raytrace_map_to_gamut()
246			}
247		}
248	};
249}
250
251impl_gamut_rgb_f64!(Srgb);
252impl_gamut_rgb_f64!(LinearRgb);
253impl_gamut_rgb_f64!(A98Rgb);
254impl_gamut_rgb_f64!(DisplayP3);
255impl_gamut_rgb_f64!(ProphotoRgb);
256impl_gamut_rgb_f64!(Rec2020);
257
258impl Gamut for Hex {
259	fn in_gamut(&self) -> bool {
260		true
261	}
262
263	fn clamp_to_gamut(&self) -> Self {
264		*self
265	}
266
267	fn map_to_gamut(self) -> Self {
268		self.clamp_to_gamut()
269	}
270}
271
272impl Gamut for Lab {
273	fn in_gamut(&self) -> bool {
274		(0.0..=100.0).contains(&self.lightness)
275			&& (-125.0..=125.0).contains(&self.a)
276			&& (-125.0..=125.0).contains(&self.b)
277	}
278
279	fn clamp_to_gamut(&self) -> Self {
280		Self::new(
281			self.lightness.clamp(0.0, 100.0),
282			self.a.clamp(-125.0, 125.0),
283			self.b.clamp(-125.0, 125.0),
284			self.alpha,
285		)
286	}
287
288	fn map_to_gamut(self) -> Self {
289		let rgb = LinearRgb::from(XyzD65::from(XyzD50::from(self)));
290		if rgb.in_gamut() {
291			return self;
292		}
293		let oklch = Oklch::from(Oklab::from(XyzD65::from(XyzD50::from(self))));
294		let mapped_rgb = raytrace_to_linear_rgb(oklch);
295		Lab::from(XyzD50::from(XyzD65::from(mapped_rgb))).clamp_to_gamut()
296	}
297}
298
299impl Gamut for Oklab {
300	fn in_gamut(&self) -> bool {
301		(0.0..=1.0).contains(&self.lightness) && (-0.4..=0.4).contains(&self.a) && (-0.4..=0.4).contains(&self.b)
302	}
303
304	fn clamp_to_gamut(&self) -> Self {
305		Self::new(self.lightness.clamp(0.0, 1.0), self.a.clamp(-0.4, 0.4), self.b.clamp(-0.4, 0.4), self.alpha)
306	}
307
308	fn map_to_gamut(self) -> Self {
309		let rgb = LinearRgb::from(XyzD65::from(self));
310		if rgb.in_gamut() {
311			return self;
312		}
313		let oklch = Oklch::from(self);
314		let mapped_rgb = raytrace_to_linear_rgb(oklch);
315		Oklab::from(XyzD65::from(mapped_rgb)).clamp_to_gamut()
316	}
317}
318
319impl Gamut for Lch {
320	fn in_gamut(&self) -> bool {
321		(0.0..=100.0).contains(&self.lightness) && (0.0..=150.0).contains(&self.chroma)
322	}
323
324	fn clamp_to_gamut(&self) -> Self {
325		Self::new(self.lightness.clamp(0.0, 100.0), self.chroma.clamp(0.0, 150.0), self.hue, self.alpha)
326	}
327
328	fn map_to_gamut(self) -> Self {
329		let rgb = LinearRgb::from(XyzD65::from(XyzD50::from(Lab::from(self))));
330		if rgb.in_gamut() {
331			return self;
332		}
333		let oklch = Oklch::from(Oklab::from(XyzD65::from(XyzD50::from(Lab::from(self)))));
334		let mapped_rgb = raytrace_to_linear_rgb(oklch);
335		Lch::from(Lab::from(XyzD50::from(XyzD65::from(mapped_rgb)))).clamp_to_gamut()
336	}
337}
338
339impl Gamut for Oklch {
340	fn in_gamut(&self) -> bool {
341		(0.0..=1.0).contains(&self.lightness) && (0.0..=0.4).contains(&self.chroma)
342	}
343
344	fn clamp_to_gamut(&self) -> Self {
345		Self::new(self.lightness.clamp(0.0, 1.0), self.chroma.clamp(0.0, 0.4), self.hue, self.alpha)
346	}
347
348	fn map_to_gamut(self) -> Self {
349		let rgb = LinearRgb::from(XyzD65::from(Oklab::from(self)));
350		if rgb.in_gamut() {
351			return self;
352		}
353		let mapped_rgb = raytrace_to_linear_rgb(self);
354		Oklch::from(Oklab::from(XyzD65::from(mapped_rgb))).clamp_to_gamut()
355	}
356}
357
358impl Gamut for Hsl {
359	fn in_gamut(&self) -> bool {
360		in_percent(self.saturation) && in_percent(self.lightness)
361	}
362
363	fn clamp_to_gamut(&self) -> Self {
364		Self::new(self.hue, self.saturation.clamp(0.0, 100.0), self.lightness.clamp(0.0, 100.0), self.alpha)
365	}
366
367	fn map_to_gamut(self) -> Self {
368		let rgb = LinearRgb::from(self);
369		if rgb.in_gamut() {
370			return self;
371		}
372		let oklch = Oklch::from(Oklab::from(XyzD65::from(rgb)));
373		let mapped_rgb = raytrace_to_linear_rgb(oklch);
374		Hsl::from(mapped_rgb).clamp_to_gamut()
375	}
376}
377
378impl Gamut for Hwb {
379	fn in_gamut(&self) -> bool {
380		in_percent(self.whiteness) && in_percent(self.blackness)
381	}
382
383	fn clamp_to_gamut(&self) -> Self {
384		Self::new(self.hue, self.whiteness.clamp(0.0, 100.0), self.blackness.clamp(0.0, 100.0), self.alpha)
385	}
386
387	fn map_to_gamut(self) -> Self {
388		let rgb = LinearRgb::from(self);
389		if rgb.in_gamut() {
390			return self;
391		}
392		let oklch = Oklch::from(Oklab::from(XyzD65::from(rgb)));
393		let mapped_rgb = raytrace_to_linear_rgb(oklch);
394		Hwb::from(mapped_rgb).clamp_to_gamut()
395	}
396}
397
398// Hsv already clamps in its constructor, so it's always in gamut.
399impl Gamut for Hsv {
400	fn in_gamut(&self) -> bool {
401		true
402	}
403
404	fn clamp_to_gamut(&self) -> Self {
405		*self
406	}
407
408	fn map_to_gamut(self) -> Self {
409		self.clamp_to_gamut()
410	}
411}
412
413impl Gamut for XyzD50 {
414	fn in_gamut(&self) -> bool {
415		self.x >= 0.0 && self.y >= 0.0 && self.z >= 0.0 && self.y <= 100.0
416	}
417
418	fn clamp_to_gamut(&self) -> Self {
419		Self::new(self.x.max(0.0), self.y.clamp(0.0, 100.0), self.z.max(0.0), self.alpha)
420	}
421
422	fn map_to_gamut(self) -> Self {
423		let rgb = LinearRgb::from(XyzD65::from(self));
424		if rgb.in_gamut() {
425			return self;
426		}
427		let oklch = Oklch::from(Oklab::from(XyzD65::from(self)));
428		let mapped_rgb = raytrace_to_linear_rgb(oklch);
429		XyzD50::from(XyzD65::from(mapped_rgb)).clamp_to_gamut()
430	}
431}
432
433impl Gamut for XyzD65 {
434	fn in_gamut(&self) -> bool {
435		self.x >= 0.0 && self.y >= 0.0 && self.z >= 0.0 && self.y <= 100.0
436	}
437
438	fn clamp_to_gamut(&self) -> Self {
439		Self::new(self.x.max(0.0), self.y.clamp(0.0, 100.0), self.z.max(0.0), self.alpha)
440	}
441
442	fn map_to_gamut(self) -> Self {
443		let rgb = LinearRgb::from(self);
444		if rgb.in_gamut() {
445			return self;
446		}
447		let oklch = Oklch::from(Oklab::from(self));
448		let mapped_rgb = raytrace_to_linear_rgb(oklch);
449		XyzD65::from(mapped_rgb).clamp_to_gamut()
450	}
451}
452
453impl Color {
454	/// Returns the [`ColorSpace`] of this colour, if it maps to a bounded RGB gamut.
455	///
456	/// Perceptual and CIE spaces (`Lab`, `Lch`, `Oklab`, `Oklch`, `XyzD50`, `XyzD65`) return `None` — they can represent
457	/// colours outside any single RGB gamut.
458	pub fn color_space(&self) -> Option<ColorSpace> {
459		match self {
460			Color::Srgb(_)
461			| Color::Hex(_)
462			| Color::Named(_)
463			| Color::Hsl(_)
464			| Color::Hwb(_)
465			| Color::Hsv(_)
466			| Color::LinearRgb(_) => Some(ColorSpace::Srgb),
467			Color::DisplayP3(_) => Some(ColorSpace::DisplayP3),
468			Color::A98Rgb(_) => Some(ColorSpace::A98Rgb),
469			Color::ProphotoRgb(_) => Some(ColorSpace::ProphotoRgb),
470			Color::Rec2020(_) => Some(ColorSpace::Rec2020),
471			Color::Lab(_) | Color::Lch(_) | Color::Oklab(_) | Color::Oklch(_) | Color::XyzD50(_) | Color::XyzD65(_) => {
472				None
473			}
474		}
475	}
476
477	/// Returns `true` if this colour can be represented in `space` without clamping.
478	///
479	/// If the colour's own space is a subset of `space` and the colour is in gamut of its own space, this returns `true`
480	/// without conversion.  Otherwise the colour is converted to the target space (via `XyzD65`) and the RGB channels are
481	/// checked against `[0,1]`.
482	pub fn in_gamut_of(&self, space: ColorSpace) -> bool {
483		if let Some(src) = self.color_space()
484			&& space.contains(src)
485			&& self.in_gamut()
486		{
487			return true;
488		}
489		match space {
490			ColorSpace::Srgb => LinearRgb::from(XyzD65::from(*self)).in_gamut(),
491			ColorSpace::DisplayP3 => DisplayP3::from(XyzD65::from(*self)).in_gamut(),
492			ColorSpace::A98Rgb => A98Rgb::from(XyzD65::from(*self)).in_gamut(),
493			ColorSpace::ProphotoRgb => ProphotoRgb::from(XyzD65::from(*self)).in_gamut(),
494			ColorSpace::Rec2020 => Rec2020::from(XyzD65::from(*self)).in_gamut(),
495		}
496	}
497}
498
499impl Gamut for Color {
500	fn in_gamut(&self) -> bool {
501		match self {
502			Color::A98Rgb(c) => c.in_gamut(),
503			Color::DisplayP3(c) => c.in_gamut(),
504			Color::Hex(c) => c.in_gamut(),
505			Color::Hsv(c) => c.in_gamut(),
506			Color::Hsl(c) => c.in_gamut(),
507			Color::Hwb(c) => c.in_gamut(),
508			Color::Lab(c) => c.in_gamut(),
509			Color::Lch(c) => c.in_gamut(),
510			Color::LinearRgb(c) => c.in_gamut(),
511			Color::Named(_) => true,
512			Color::Oklab(c) => c.in_gamut(),
513			Color::Oklch(c) => c.in_gamut(),
514			Color::ProphotoRgb(c) => c.in_gamut(),
515			Color::Rec2020(c) => c.in_gamut(),
516			Color::Srgb(c) => c.in_gamut(),
517			Color::XyzD50(c) => c.in_gamut(),
518			Color::XyzD65(c) => c.in_gamut(),
519		}
520	}
521
522	fn clamp_to_gamut(&self) -> Self {
523		match self {
524			Color::A98Rgb(c) => Color::A98Rgb(c.clamp_to_gamut()),
525			Color::DisplayP3(c) => Color::DisplayP3(c.clamp_to_gamut()),
526			Color::Hex(c) => Color::Hex(c.clamp_to_gamut()),
527			Color::Hsv(c) => Color::Hsv(c.clamp_to_gamut()),
528			Color::Hsl(c) => Color::Hsl(c.clamp_to_gamut()),
529			Color::Hwb(c) => Color::Hwb(c.clamp_to_gamut()),
530			Color::Lab(c) => Color::Lab(c.clamp_to_gamut()),
531			Color::Lch(c) => Color::Lch(c.clamp_to_gamut()),
532			Color::LinearRgb(c) => Color::LinearRgb(c.clamp_to_gamut()),
533			Color::Named(n) => Color::Named(*n),
534			Color::Oklab(c) => Color::Oklab(c.clamp_to_gamut()),
535			Color::Oklch(c) => Color::Oklch(c.clamp_to_gamut()),
536			Color::ProphotoRgb(c) => Color::ProphotoRgb(c.clamp_to_gamut()),
537			Color::Rec2020(c) => Color::Rec2020(c.clamp_to_gamut()),
538			Color::Srgb(c) => Color::Srgb(c.clamp_to_gamut()),
539			Color::XyzD50(c) => Color::XyzD50(c.clamp_to_gamut()),
540			Color::XyzD65(c) => Color::XyzD65(c.clamp_to_gamut()),
541		}
542	}
543
544	fn map_to_gamut(self) -> Self {
545		match self {
546			Color::A98Rgb(c) => Color::A98Rgb(c.map_to_gamut()),
547			Color::DisplayP3(c) => Color::DisplayP3(c.map_to_gamut()),
548			Color::Hex(c) => Color::Hex(c.map_to_gamut()),
549			Color::Hsv(c) => Color::Hsv(c.map_to_gamut()),
550			Color::Hsl(c) => Color::Hsl(c.map_to_gamut()),
551			Color::Hwb(c) => Color::Hwb(c.map_to_gamut()),
552			Color::Lab(c) => Color::Lab(c.map_to_gamut()),
553			Color::Lch(c) => Color::Lch(c.map_to_gamut()),
554			Color::LinearRgb(c) => Color::LinearRgb(c.map_to_gamut()),
555			Color::Named(n) => Color::Named(n),
556			Color::Oklab(c) => Color::Oklab(c.map_to_gamut()),
557			Color::Oklch(c) => Color::Oklch(c.map_to_gamut()),
558			Color::ProphotoRgb(c) => Color::ProphotoRgb(c.map_to_gamut()),
559			Color::Rec2020(c) => Color::Rec2020(c.map_to_gamut()),
560			Color::Srgb(c) => Color::Srgb(c.map_to_gamut()),
561			Color::XyzD50(c) => Color::XyzD50(c.map_to_gamut()),
562			Color::XyzD65(c) => Color::XyzD65(c.map_to_gamut()),
563		}
564	}
565}