1use crate::*;
2
3pub trait Gamut: Sized {
10 fn in_gamut(&self) -> bool;
13
14 fn clamp_to_gamut(&self) -> Self;
18
19 fn map_to_gamut(self) -> Self;
28}
29
30fn raytrace_to_linear_rgb(oklch: Oklch) -> LinearRgb {
36 let alpha = oklch.alpha;
37
38 if oklch.lightness >= 1.0 {
40 return LinearRgb::new(1.0, 1.0, 1.0, alpha);
41 }
42 if oklch.lightness <= 0.0 {
44 return LinearRgb::new(0.0, 0.0, 0.0, alpha);
45 }
46
47 let l_origin = oklch.lightness;
49
50 let h_origin = oklch.hue;
52
53 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 let origin_rgb = LinearRgb::from(Oklab::from(oklch));
61 let mut origin_rgb = [origin_rgb.red, origin_rgb.green, origin_rgb.blue];
62
63 let low = 1e-6;
65
66 let high = 1.0 - low;
68
69 let mut last = origin_rgb;
71
72 for i in 0..4 {
74 if i > 0 {
76 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 current_oklch.lightness = l_origin;
82
83 current_oklch.hue = h_origin;
85
86 let rgb = LinearRgb::from(XyzD65::from(Oklab::from(current_oklch)));
89 origin_rgb = [rgb.red, rgb.green, rgb.blue];
90 }
91
92 let intersection = raytrace_box(&anchor, &origin_rgb);
95
96 match intersection {
97 None => {
100 origin_rgb = last;
101 break;
102 }
103 Some(hit) => {
104 if i > 0 && origin_rgb.iter().all(|&x| low < x && x < high) {
107 anchor = origin_rgb;
108 }
109
110 origin_rgb = hit;
113 last = hit;
114 }
115 }
116 }
117
118 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
124macro_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 if self.in_gamut() {
133 return self;
134 }
135
136 let oklch = $to_oklch(self);
138
139 $from_linear(raytrace_to_linear_rgb(oklch))
141 }
142 }
143 };
144}
145
146impl_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
166fn raytrace_box(start: &[f64; 3], end: &[f64; 3]) -> Option<[f64; 3]> {
174 let mut tfar = f64::INFINITY;
178
179 let mut tnear = f64::NEG_INFINITY;
181
182 let mut direction = [0.0_f64; 3];
184
185 for i in 0..3 {
187 let a = start[i]; let b = end[i]; let d = b - a; direction[i] = d; if d.abs() > 1e-12 {
196 let inv_d = 1.0 / d; let t1 = (0.0 - a) * inv_d; let t2 = (1.0 - a) * inv_d; tnear = tnear.max(t1.min(t2)); tfar = tfar.min(t1.max(t2)); }
202 else if !(0.0..=1.0).contains(&a) {
204 return None;
205 }
206 }
207
208 if tnear > tfar || tfar < 0.0 {
210 return None;
211 }
212
213 if tnear < 0.0 {
216 tnear = tfar;
217 }
218
219 if !tnear.is_finite() {
223 return None;
224 }
225
226 Some([start[0] + direction[0] * tnear, start[1] + direction[1] * tnear, start[2] + direction[2] * tnear])
229}
230
231const GAMUT_EPSILON: f64 = 1e-6;
234
235fn in_unit(v: f64) -> bool {
237 (-GAMUT_EPSILON..=1.0 + GAMUT_EPSILON).contains(&v)
238}
239
240fn 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
423impl 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 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 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}