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