1use crate::color::{lstar_from_y, Argb};
2#[cfg(all(not(feature = "std"), feature = "libm"))]
3#[allow(unused_imports)]
4use crate::utils::no_std::FloatExt;
5use core::{
6 cmp::Ordering,
7 fmt,
8 hash::{Hash, Hasher},
9};
10#[cfg(feature = "serde")]
11use serde::Serialize;
12pub use {cam16::Cam16, solver::HctSolver, viewing_conditions::ViewingConditions};
13
14pub mod cam16;
15pub mod solver;
16pub mod viewing_conditions;
17
18#[derive(Clone, Copy, Debug, PartialOrd)]
19#[cfg_attr(feature = "serde", derive(Serialize))]
20pub struct Hct {
21 _hue: f64,
22 _chroma: f64,
23 _tone: f64,
24 _argb: Argb,
25}
26
27impl Hct {
28 pub const fn get_hue(&self) -> f64 {
37 self._hue
38 }
39
40 pub fn set_hue(&mut self, value: f64) {
49 self._argb = HctSolver::solve_to_argb(value, self.get_chroma(), self.get_tone());
50
51 let cam16 = Cam16::from(self._argb);
52
53 self._hue = cam16.hue;
54 self._chroma = cam16.chroma;
55 self._tone = self._argb.as_lstar();
56 }
57
58 pub const fn get_chroma(&self) -> f64 {
64 self._chroma
65 }
66
67 pub fn set_chroma(&mut self, value: f64) {
73 self._argb = HctSolver::solve_to_argb(self.get_hue(), value, self.get_tone());
74
75 let cam16 = Cam16::from(self._argb);
76
77 self._hue = cam16.hue;
78 self._chroma = cam16.chroma;
79 self._tone = self._argb.as_lstar();
80 }
81
82 pub const fn get_tone(&self) -> f64 {
90 self._tone
91 }
92
93 pub fn set_tone(&mut self, value: f64) {
101 self._argb = HctSolver::solve_to_argb(self.get_hue(), self.get_chroma(), value);
102
103 let cam16 = Cam16::from(self._argb);
104
105 self._hue = cam16.hue;
106 self._chroma = cam16.chroma;
107 self._tone = self._argb.as_lstar();
108 }
109
110 pub fn new(argb: Argb) -> Self {
111 let cam16 = Cam16::from(argb);
112
113 Self {
114 _hue: cam16.hue,
115 _chroma: cam16.chroma,
116 _tone: argb.as_lstar(),
117 _argb: argb,
118 }
119 }
120
121 pub fn from(hue: f64, chroma: f64, tone: f64) -> Self {
127 let argb = HctSolver::solve_to_argb(hue, chroma, tone);
128
129 Self::new(argb)
130 }
131
132 #[must_use]
145 pub fn in_viewing_conditions(self, vc: &ViewingConditions) -> Self {
146 let cam16 = Cam16::from(Argb::from(self));
148 let viewed_in_vc = cam16.xyz_in_viewing_conditions(vc);
149
150 let recast_in_vc = Cam16::from_xyz_in_viewing_conditions(
152 viewed_in_vc.x,
153 viewed_in_vc.y,
154 viewed_in_vc.z,
155 &ViewingConditions::standard(),
156 );
157
158 Self::from(
162 recast_in_vc.hue,
163 recast_in_vc.chroma,
164 lstar_from_y(viewed_in_vc.y),
165 )
166 }
167}
168
169impl fmt::Display for Hct {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 write!(
172 f,
173 "H{} C{} T{}",
174 self.get_hue().round(),
175 self.get_chroma().round(),
176 self.get_tone().round()
177 )
178 }
179}
180
181impl Ord for Hct {
182 fn cmp(&self, other: &Self) -> Ordering {
183 self.partial_cmp(other).unwrap()
184 }
185}
186
187impl PartialEq for Hct {
188 fn eq(&self, other: &Self) -> bool {
189 self._argb == other._argb
190 }
191}
192
193impl Eq for Hct {}
194
195impl Hash for Hct {
196 fn hash<H: Hasher>(&self, state: &mut H) {
197 self._hue.to_bits().hash(state);
198 self._chroma.to_bits().hash(state);
199 self._tone.to_bits().hash(state);
200 self._argb.hash(state);
201 }
202}
203
204impl From<Argb> for Hct {
205 fn from(value: Argb) -> Self {
206 Self::new(value)
207 }
208}
209
210impl From<Hct> for Argb {
211 fn from(value: Hct) -> Self {
212 value._argb
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::{Cam16, Hct, ViewingConditions};
219 use crate::color::{y_from_lstar, Argb};
220 use ahash::AHasher;
221 #[cfg(not(feature = "std"))]
222 use alloc::format;
223 use core::hash::{Hash, Hasher};
224 use float_cmp::{approx_eq, assert_approx_eq};
225 #[cfg(feature = "std")]
226 use std::format;
227
228 const BLACK: Argb = Argb::from_u32(0xFF000000);
229 const WHITE: Argb = Argb::from_u32(0xFFFFFFFF);
230 const RED: Argb = Argb::from_u32(0xFFFF0000);
231 const GREEN: Argb = Argb::from_u32(0xFF00FF00);
232 const BLUE: Argb = Argb::from_u32(0xFF0000FF);
233 const MIDGRAY: Argb = Argb::from_u32(0xFF777777);
234
235 fn hash_value<T: Hash>(value: T) -> u64 {
236 let mut hasher = AHasher::default();
237
238 value.hash(&mut hasher);
239
240 hasher.finish()
241 }
242
243 const fn color_is_on_boundary(argb: Argb) -> bool {
244 argb.red == 0
245 || argb.red == 255
246 || argb.green == 0
247 || argb.green == 255
248 || argb.blue == 0
249 || argb.blue == 255
250 }
251
252 #[test]
253 fn test_hash_code() {
254 let a: Hct = Argb::from_u32(123).into();
255 let b: Hct = Argb::from_u32(123).into();
256
257 assert_eq!(a, b);
258 assert_eq!(hash_value(a), hash_value(b));
259 }
260
261 #[test]
262 fn test_conversions_are_reflexive() {
263 let cam = Cam16::from(RED);
264 let color = cam.viewed(&ViewingConditions::standard());
265
266 assert_eq!(color, RED);
267 }
268
269 #[test]
270 fn test_ymidgray() {
271 assert_approx_eq!(f64, 18.418, y_from_lstar(50.0), epsilon = 0.001);
272 }
273
274 #[test]
275 fn test_yblack() {
276 assert_approx_eq!(f64, 0.0, y_from_lstar(0.0), epsilon = 0.001);
277 }
278
279 #[test]
280 fn test_ywhite() {
281 assert_approx_eq!(f64, 100.0, y_from_lstar(100.0), epsilon = 0.001);
282 }
283
284 #[test]
285 fn test_cam_red() {
286 let cam = Cam16::from(RED);
287
288 assert_approx_eq!(f64, 46.445, cam.j, epsilon = 0.001);
289 assert_approx_eq!(f64, 113.357, cam.chroma, epsilon = 0.001);
290 assert_approx_eq!(f64, 27.408, cam.hue, epsilon = 0.001);
291 assert_approx_eq!(f64, 89.494, cam.m, epsilon = 0.001);
292 assert_approx_eq!(f64, 91.889, cam.s, epsilon = 0.001);
293 assert_approx_eq!(f64, 105.988, cam.q, epsilon = 0.001);
294 }
295
296 #[test]
297 fn test_cam_green() {
298 let cam = Cam16::from(GREEN);
299
300 assert_approx_eq!(f64, 79.331, cam.j, epsilon = 0.001);
301 assert_approx_eq!(f64, 108.410, cam.chroma, epsilon = 0.001);
302 assert_approx_eq!(f64, 142.139, cam.hue, epsilon = 0.001);
303 assert_approx_eq!(f64, 85.587, cam.m, epsilon = 0.001);
304 assert_approx_eq!(f64, 78.604, cam.s, epsilon = 0.001);
305 assert_approx_eq!(f64, 138.520, cam.q, epsilon = 0.001);
306 }
307
308 #[test]
309 fn test_cam_blue() {
310 let cam = Cam16::from(BLUE);
311
312 assert_approx_eq!(f64, 25.465, cam.j, epsilon = 0.001);
313 assert_approx_eq!(f64, 87.230, cam.chroma, epsilon = 0.001);
314 assert_approx_eq!(f64, 282.788, cam.hue, epsilon = 0.001);
315 assert_approx_eq!(f64, 68.867, cam.m, epsilon = 0.001);
316 assert_approx_eq!(f64, 93.674, cam.s, epsilon = 0.001);
317 assert_approx_eq!(f64, 78.481, cam.q, epsilon = 0.001);
318 }
319
320 #[test]
321 fn test_cam_black() {
322 let cam = Cam16::from(BLACK);
323
324 assert_approx_eq!(f64, 0.0, cam.j, epsilon = 0.001);
325 assert_approx_eq!(f64, 0.0, cam.chroma, epsilon = 0.001);
326 assert_approx_eq!(f64, 0.0, cam.hue, epsilon = 0.001);
327 assert_approx_eq!(f64, 0.0, cam.m, epsilon = 0.001);
328 assert_approx_eq!(f64, 0.0, cam.s, epsilon = 0.001);
329 assert_approx_eq!(f64, 0.0, cam.q, epsilon = 0.001);
330 }
331
332 #[test]
333 fn test_cam_white() {
334 let cam = Cam16::from(WHITE);
335
336 assert_approx_eq!(f64, 100.0, cam.j, epsilon = 0.001);
337 assert_approx_eq!(f64, 2.869, cam.chroma, epsilon = 0.001);
338 assert_approx_eq!(f64, 209.492, cam.hue, epsilon = 0.001);
339 assert_approx_eq!(f64, 2.265, cam.m, epsilon = 0.001);
340 assert_approx_eq!(f64, 12.068, cam.s, epsilon = 0.001);
341 assert_approx_eq!(f64, 155.521, cam.q, epsilon = 0.001);
342 }
343
344 #[test]
345 fn test_camut_map_red() {
346 let color_to_test = RED;
347 let cam = Cam16::from(color_to_test);
348 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
349
350 assert_eq!(color_to_test, color);
351 }
352
353 #[test]
354 fn test_camut_map_green() {
355 let color_to_test = GREEN;
356 let cam = Cam16::from(color_to_test);
357 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
358
359 assert_eq!(color_to_test, color);
360 }
361
362 #[test]
363 fn test_camut_map_blue() {
364 let color_to_test = BLUE;
365 let cam = Cam16::from(color_to_test);
366 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
367
368 assert_eq!(color_to_test, color);
369 }
370
371 #[test]
372 fn test_camut_map_white() {
373 let color_to_test = WHITE;
374 let cam = Cam16::from(color_to_test);
375 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
376
377 assert_eq!(color_to_test, color);
378 }
379
380 #[test]
381 fn test_camut_map_midgray() {
382 let color_to_test = MIDGRAY;
383 let cam = Cam16::from(color_to_test);
384 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
385
386 assert_eq!(color_to_test, color);
387 }
388
389 #[test]
390 fn test_camut_map_black() {
391 let color_to_test = BLACK;
392 let cam = Cam16::from(color_to_test);
393 let color = Hct::from(cam.hue, cam.chroma, color_to_test.as_lstar()).into();
394
395 assert_eq!(color_to_test, color);
396 }
397
398 #[test]
399 fn test_hct_returns_sufficiently_close_color() {
400 for hue in (15..361).step_by(30) {
401 for chroma in (0..100).step_by(10) {
402 for tone in (20..80).step_by(10) {
403 let hct_request_description = format!("H{hue} C{chroma} T{tone}");
404 let hct_color = Hct::from(f64::from(hue), f64::from(chroma), f64::from(tone));
405
406 if chroma > 0 {
407 assert!(
408 approx_eq!(f64, hct_color.get_hue(), f64::from(hue), epsilon = 4.0),
409 "Hue should be close for {hct_request_description}"
410 );
411 }
412
413 assert!(
414 (0.0..(f64::from(chroma) + 2.5)).contains(&hct_color.get_chroma()),
415 "Chroma should be close or less for {hct_request_description}"
416 );
417
418 if hct_color.get_chroma() < f64::from(chroma) - 2.5 {
419 assert!(
420 color_is_on_boundary(hct_color.into()),
421 "HCT request for non-sRGB color should return a color on the boundary of the sRGB cube for {hct_request_description}, but got {} instead",
422 Argb::from(hct_color).to_hex_with_pound()
423 );
424 }
425
426 assert!(
427 approx_eq!(f64, hct_color.get_tone(), f64::from(tone), epsilon = 0.5),
428 "Tone should be close for {hct_request_description}"
429 );
430 }
431 }
432 }
433 }
434
435 #[test]
436 fn test_cam16_to_xyz_without_array() {
437 let color_to_test = RED;
438 let cam = Cam16::from(color_to_test);
439 let xyz = cam.xyz_in_viewing_conditions(&ViewingConditions::s_rgb());
440
441 assert_approx_eq!(f64, xyz.x, 41.23, epsilon = 0.01);
442 assert_approx_eq!(f64, xyz.y, 21.26, epsilon = 0.01);
443 assert_approx_eq!(f64, xyz.z, 1.93, epsilon = 0.01);
444 }
445
446 #[test]
447 fn test_color_relativity_red_in_black() {
448 let color_to_test = RED;
449 let hct: Hct = color_to_test.into();
450
451 let result =
452 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
453
454 assert_eq!(Argb::from(result), Argb::from_u32(0xFF9F5C51));
455 }
456
457 #[test]
458 fn test_color_relativity_red_in_white() {
459 let color_to_test = RED;
460 let hct: Hct = color_to_test.into();
461
462 let result = hct.in_viewing_conditions(&ViewingConditions::make(
463 None,
464 None,
465 Some(100.0),
466 None,
467 None,
468 ));
469
470 assert_eq!(Argb::from(result), Argb::from_u32(0xFFFF5D48));
471 }
472
473 #[test]
474 fn test_color_relativity_green_in_black() {
475 let color_to_test = GREEN;
476 let hct: Hct = color_to_test.into();
477
478 let result =
479 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
480
481 assert_eq!(Argb::from(result), Argb::from_u32(0xFFACD69D));
482 }
483
484 #[test]
485 fn test_color_relativity_green_in_white() {
486 let color_to_test = GREEN;
487 let hct: Hct = color_to_test.into();
488
489 let result = hct.in_viewing_conditions(&ViewingConditions::make(
490 None,
491 None,
492 Some(100.0),
493 None,
494 None,
495 ));
496
497 assert_eq!(Argb::from(result), Argb::from_u32(0xFF8EFF77));
498 }
499
500 #[test]
501 fn test_color_relativity_blue_in_black() {
502 let color_to_test = BLUE;
503 let hct: Hct = color_to_test.into();
504
505 let result =
506 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
507
508 assert_eq!(Argb::from(result), Argb::from_u32(0xFF343654));
509 }
510
511 #[test]
512 fn test_color_relativity_blue_in_white() {
513 let color_to_test = BLUE;
514 let hct: Hct = color_to_test.into();
515
516 let result = hct.in_viewing_conditions(&ViewingConditions::make(
517 None,
518 None,
519 Some(100.0),
520 None,
521 None,
522 ));
523
524 assert_eq!(Argb::from(result), Argb::from_u32(0xFF3F49FF));
525 }
526
527 #[test]
528 fn test_color_relativity_white_in_black() {
529 let color_to_test = WHITE;
530 let hct: Hct = color_to_test.into();
531
532 let result =
533 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
534
535 assert_eq!(Argb::from(result), Argb::from_u32(0xFFFFFFFF));
536 }
537
538 #[test]
539 fn test_color_relativity_white_in_white() {
540 let color_to_test = WHITE;
541 let hct: Hct = color_to_test.into();
542
543 let result = hct.in_viewing_conditions(&ViewingConditions::make(
544 None,
545 None,
546 Some(100.0),
547 None,
548 None,
549 ));
550
551 assert_eq!(Argb::from(result), Argb::from_u32(0xFFFFFFFF));
552 }
553
554 #[test]
555 fn test_color_relativity_midgray_in_black() {
556 let color_to_test = MIDGRAY;
557 let hct: Hct = color_to_test.into();
558
559 let result =
560 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
561
562 assert_eq!(Argb::from(result), Argb::from_u32(0xFF605F5F));
563 }
564
565 #[test]
566 fn test_color_relativity_midgray_in_white() {
567 let color_to_test = MIDGRAY;
568 let hct: Hct = color_to_test.into();
569
570 let result = hct.in_viewing_conditions(&ViewingConditions::make(
571 None,
572 None,
573 Some(100.0),
574 None,
575 None,
576 ));
577
578 assert_eq!(Argb::from(result), Argb::from_u32(0xFF8E8E8E));
579 }
580
581 #[test]
582 fn test_color_relativity_black_in_black() {
583 let color_to_test = BLACK;
584 let hct: Hct = color_to_test.into();
585
586 let result =
587 hct.in_viewing_conditions(&ViewingConditions::make(None, None, Some(0.0), None, None));
588
589 assert_eq!(Argb::from(result), Argb::from_u32(0xFF000000));
590 }
591
592 #[test]
593 fn test_color_relativity_black_in_white() {
594 let color_to_test = BLACK;
595 let hct: Hct = color_to_test.into();
596
597 let result = hct.in_viewing_conditions(&ViewingConditions::make(
598 None,
599 None,
600 Some(100.0),
601 None,
602 None,
603 ));
604
605 assert_eq!(Argb::from(result), Argb::from_u32(0xFF000000));
606 }
607}