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(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 if oklch.lightness >= 1.0 {
46 return [1.0, 1.0, 1.0];
47 }
48 if oklch.lightness <= 0.0 {
50 return [0.0, 0.0, 0.0];
51 }
52
53 let l_origin = oklch.lightness;
55
56 let h_origin = oklch.hue;
58
59 let mut anchor = to_linear(Oklch::new(l_origin, 0.0, h_origin, alpha));
62
63 let mut origin_rgb = to_linear(oklch);
65
66 let low = 1e-6;
68
69 let high = 1.0 - low;
71
72 let mut last = origin_rgb;
74
75 for i in 0..4 {
77 if i > 0 {
79 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 current_oklch.lightness = l_origin;
85
86 current_oklch.hue = h_origin;
88
89 origin_rgb = to_linear(current_oklch);
92 }
93
94 let intersection = raytrace_box(&anchor, &origin_rgb);
97
98 match intersection {
99 None => {
102 origin_rgb = last;
103 break;
104 }
105 Some(hit) => {
106 if i > 0 && origin_rgb.iter().all(|&x| low < x && x < high) {
109 anchor = origin_rgb;
110 }
111
112 origin_rgb = hit;
115 last = hit;
116 }
117 }
118 }
119
120 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
131macro_rules! impl_map_to_gamut_raytrace {
135 ($($ty:ident),+ $(,)?) => {
136 $(impl $ty {
137 fn raytrace_map_to_gamut(self) -> Self {
138 if self.in_gamut() {
140 return self;
141 }
142
143 let oklch = Oklch::from(self);
145
146 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
156fn raytrace_box(start: &[f64; 3], end: &[f64; 3]) -> Option<[f64; 3]> {
164 let mut tfar = f64::INFINITY;
168
169 let mut tnear = f64::NEG_INFINITY;
171
172 let mut direction = [0.0_f64; 3];
174
175 for i in 0..3 {
177 let a = start[i]; let b = end[i]; let d = b - a; direction[i] = d; if d.abs() > 1e-12 {
186 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)); }
192 else if !(0.0..=1.0).contains(&a) {
194 return None;
195 }
196 }
197
198 if tnear > tfar || tfar < 0.0 {
200 return None;
201 }
202
203 if tnear < 0.0 {
206 tnear = tfar;
207 }
208
209 if !tnear.is_finite() {
213 return None;
214 }
215
216 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
223fn in_unit(v: f64) -> bool {
225 (-GAMUT_EPSILON..=1.0 + GAMUT_EPSILON).contains(&v)
226}
227
228fn 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
398impl 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 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 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}