1use crate::channel::{
4 AngularChannel, AngularChannelScalar, ChannelCast, ChannelFormatCast, ColorChannel,
5 PosNormalBoundedChannel, PosNormalChannelScalar,
6};
7use crate::color;
8use crate::color::{Bounded, Color, FromTuple, Invert, Lerp, PolarColor};
9use crate::convert::{decompose_hue_segment, FromColor, GetHue};
10use crate::encoding::EncodableColor;
11use crate::hsi::Hsi;
12use crate::rgb::Rgb;
13use crate::tags::EHsiTag;
14use angle;
15use angle::{Angle, Deg, FromAngle, IntoAngle, Rad};
16#[cfg(feature = "approx")]
17use approx;
18use num_traits;
19use num_traits::Float;
20use std::fmt;
21
22#[repr(C)]
38#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
39pub struct eHsi<T, A = Deg<T>> {
40 hue: AngularChannel<A>,
41 saturation: PosNormalBoundedChannel<T>,
42 intensity: PosNormalBoundedChannel<T>,
43}
44
45impl<T, A> eHsi<T, A>
46where
47 T: PosNormalChannelScalar + Float,
48 A: AngularChannelScalar + Angle<Scalar = T>,
49{
50 pub fn new(hue: A, saturation: T, intensity: T) -> Self {
52 eHsi {
53 hue: AngularChannel::new(hue),
54 saturation: PosNormalBoundedChannel::new(saturation),
55 intensity: PosNormalBoundedChannel::new(intensity),
56 }
57 }
58
59 impl_color_color_cast_angular!(
60 eHsi {
61 hue,
62 saturation,
63 intensity
64 },
65 chan_traits = { PosNormalChannelScalar }
66 );
67
68 pub fn hue(&self) -> A {
70 self.hue.0.clone()
71 }
72 pub fn saturation(&self) -> T {
74 self.saturation.0.clone()
75 }
76 pub fn intensity(&self) -> T {
78 self.intensity.0.clone()
79 }
80 pub fn hue_mut(&mut self) -> &mut A {
82 &mut self.hue.0
83 }
84 pub fn saturation_mut(&mut self) -> &mut T {
86 &mut self.saturation.0
87 }
88 pub fn intensity_mut(&mut self) -> &mut T {
90 &mut self.intensity.0
91 }
92 pub fn set_hue(&mut self, val: A) {
94 self.hue.0 = val;
95 }
96 pub fn set_saturation(&mut self, val: T) {
98 self.saturation.0 = val;
99 }
100 pub fn set_intensity(&mut self, val: T) {
102 self.intensity.0 = val;
103 }
104 pub fn is_same_as_hsi(&self) -> bool {
106 let deg_hue =
107 Deg::from_angle(self.hue().clone()) % Deg(num_traits::cast::<_, T>(120.0).unwrap());
108 let i_limit = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
109 - (deg_hue - Deg(num_traits::cast::<_, T>(60.0).unwrap()))
110 .scalar()
111 .abs()
112 / Deg(num_traits::cast::<_, T>(180.0).unwrap()).scalar();
113
114 self.intensity() <= i_limit
115 }
116 pub fn to_hsi(&self) -> Option<Hsi<T, A>> {
118 if self.is_same_as_hsi() {
119 Some(Hsi::new(
120 self.hue().clone(),
121 self.saturation().clone(),
122 self.intensity().clone(),
123 ))
124 } else {
125 None
126 }
127 }
128 pub fn from_hsi(hsi: &Hsi<T, A>) -> Option<eHsi<T, A>> {
132 let out = eHsi::new(
133 hsi.hue().clone(),
134 hsi.saturation().clone(),
135 hsi.intensity().clone(),
136 );
137 if out.is_same_as_hsi() {
138 Some(out)
139 } else {
140 None
141 }
142 }
143}
144
145impl<T, A> PolarColor for eHsi<T, A>
146where
147 T: PosNormalChannelScalar,
148 A: AngularChannelScalar,
149{
150 type Angular = A;
151 type Cartesian = T;
152}
153
154impl<T, A> Color for eHsi<T, A>
155where
156 T: PosNormalChannelScalar,
157 A: AngularChannelScalar,
158{
159 type Tag = EHsiTag;
160 type ChannelsTuple = (A, T, T);
161
162 fn num_channels() -> u32 {
163 3
164 }
165 fn to_tuple(self) -> Self::ChannelsTuple {
166 (self.hue.0, self.saturation.0, self.intensity.0)
167 }
168}
169
170impl<T, A> FromTuple for eHsi<T, A>
171where
172 T: PosNormalChannelScalar + Float,
173 A: AngularChannelScalar + Angle<Scalar = T>,
174{
175 fn from_tuple(values: Self::ChannelsTuple) -> Self {
176 eHsi::new(values.0, values.1, values.2)
177 }
178}
179
180impl<T, A> Invert for eHsi<T, A>
181where
182 T: PosNormalChannelScalar,
183 A: AngularChannelScalar,
184{
185 impl_color_invert!(eHsi {
186 hue,
187 saturation,
188 intensity
189 });
190}
191
192impl<T, A> Lerp for eHsi<T, A>
193where
194 T: PosNormalChannelScalar + color::Lerp,
195 A: AngularChannelScalar + color::Lerp,
196{
197 type Position = A::Position;
198
199 impl_color_lerp_angular!(eHsi<T> {hue, saturation, intensity});
200}
201
202impl<T, A> Bounded for eHsi<T, A>
203where
204 T: PosNormalChannelScalar,
205 A: AngularChannelScalar,
206{
207 impl_color_bounded!(eHsi {
208 hue,
209 saturation,
210 intensity
211 });
212}
213
214impl<T, A> EncodableColor for eHsi<T, A>
215where
216 T: PosNormalChannelScalar + num_traits::Float,
217 A: AngularChannelScalar + Angle<Scalar = T> + FromAngle<angle::Turns<T>>,
218{
219}
220
221#[cfg(feature = "approx")]
222impl<T, A> approx::AbsDiffEq for eHsi<T, A>
223where
224 T: PosNormalChannelScalar + approx::AbsDiffEq<Epsilon = A::Epsilon>,
225 A: AngularChannelScalar + approx::AbsDiffEq,
226 A::Epsilon: Clone + num_traits::Float,
227{
228 impl_abs_diff_eq!({hue, saturation, intensity});
229}
230#[cfg(feature = "approx")]
231impl<T, A> approx::RelativeEq for eHsi<T, A>
232where
233 T: PosNormalChannelScalar + approx::RelativeEq<Epsilon = A::Epsilon>,
234 A: AngularChannelScalar + approx::RelativeEq,
235 A::Epsilon: Clone + num_traits::Float,
236{
237 impl_rel_eq!({hue, saturation, intensity});
238}
239#[cfg(feature = "approx")]
240impl<T, A> approx::UlpsEq for eHsi<T, A>
241where
242 T: PosNormalChannelScalar + approx::UlpsEq<Epsilon = A::Epsilon>,
243 A: AngularChannelScalar + approx::UlpsEq,
244 A::Epsilon: Clone + num_traits::Float,
245{
246 impl_ulps_eq!({hue, saturation, intensity});
247}
248
249impl<T, A> Default for eHsi<T, A>
250where
251 T: PosNormalChannelScalar + num_traits::Zero,
252 A: AngularChannelScalar + num_traits::Zero,
253{
254 impl_color_default!(eHsi {
255 hue: AngularChannel,
256 saturation: PosNormalBoundedChannel,
257 intensity: PosNormalBoundedChannel
258 });
259}
260
261impl<T, A> fmt::Display for eHsi<T, A>
262where
263 T: PosNormalChannelScalar + fmt::Display,
264 A: AngularChannelScalar + fmt::Display,
265{
266 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267 write!(
268 f,
269 "eHsi({}, {}, {})",
270 self.hue, self.saturation, self.intensity
271 )
272 }
273}
274
275impl<T, A> GetHue for eHsi<T, A>
276where
277 T: PosNormalChannelScalar,
278 A: AngularChannelScalar,
279{
280 impl_color_get_hue_angular!(eHsi);
281}
282
283impl<T, A> FromColor<Rgb<T>> for eHsi<T, A>
284where
285 T: PosNormalChannelScalar + num_traits::Float,
286 A: AngularChannelScalar + Angle<Scalar = T> + FromAngle<Rad<T>>,
287{
288 fn from_color(from: &Rgb<T>) -> Self {
289 let epsilon: T = num_traits::cast(1e-10).unwrap();
290 let coords = from.chromaticity_coordinates();
291
292 let hue_unnormal: A = coords.get_hue::<A>();
293 let hue = Angle::normalize(hue_unnormal);
294 let deg_hue = Deg::from_angle(hue.clone()) % Deg(num_traits::cast::<_, T>(120.0).unwrap());
295
296 let min = from.red().min(from.green().min(from.blue()));
297 let max = from.red().max(from.green().max(from.blue()));
298
299 let sum = from.red() + from.green() + from.blue();
300 let intensity = num_traits::cast::<_, T>(1.0 / 3.0).unwrap() * sum;
301
302 let i_limit: T = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
303 - (deg_hue - Deg(num_traits::cast::<_, T>(60.0).unwrap()))
304 .scalar()
305 .abs()
306 / Deg(num_traits::cast::<_, T>(180.0).unwrap()).scalar();
307
308 let one: T = num_traits::cast(1.0).unwrap();
309
310 let saturation = if intensity <= i_limit {
311 if intensity != num_traits::cast::<_, T>(0.0).unwrap() {
312 one - min / intensity
313 } else {
314 num_traits::cast(0.0).unwrap()
315 }
316 } else {
317 let three: T = num_traits::cast(3.0).unwrap();
318 one - ((three * (one - max)) / (three - sum + epsilon))
319 };
320
321 eHsi::new(hue, saturation, intensity)
322 }
323}
324
325impl<T, A> FromColor<eHsi<T, A>> for Rgb<T>
326where
327 T: PosNormalChannelScalar + num_traits::Float,
328 A: AngularChannelScalar + Angle<Scalar = T>,
329{
330 fn from_color(from: &eHsi<T, A>) -> Rgb<T> {
331 let one = num_traits::cast::<_, T>(1.0).unwrap();
332 let one_eighty = num_traits::cast::<_, T>(180.0).unwrap();
333
334 let (hue_seg, _) = decompose_hue_segment(from);
335 let scaled_frac = Deg::from_angle(from.hue()) % Deg(num_traits::cast(120.0).unwrap());
336
337 let i_threshold = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
340 - (scaled_frac.scalar() - num_traits::cast(60.0).unwrap()).abs() / (one_eighty);
341
342 if from.intensity() < i_threshold {
344 let c1 = from.intensity() * (one - from.saturation());
345 let c2 = from.intensity()
346 * (one
347 + (from.saturation() * scaled_frac.cos())
348 / (Angle::cos(Deg(num_traits::cast(60.0).unwrap()) - scaled_frac)));
349
350 let c3 = num_traits::cast::<_, T>(3.0).unwrap() * from.intensity() - (c1 + c2);
351
352 match hue_seg {
353 0 | 1 => Rgb::new(c2, c3, c1),
354 2 | 3 => Rgb::new(c1, c2, c3),
355 4 | 5 => Rgb::new(c3, c1, c2),
356 _ => unreachable!(),
357 }
358 } else {
360 let deg_hue = Deg::from_angle(from.hue());
361 let shifted_hue = match hue_seg {
362 1 | 2 => deg_hue - Deg(num_traits::cast(240.0).unwrap()),
363 3 | 4 => deg_hue,
364 5 | 0 => deg_hue - Deg(num_traits::cast(120.0).unwrap()),
365 _ => unreachable!(),
366 };
367
368 let c1 = from.intensity() * (one - from.saturation()) + from.saturation();
369 let c2 = one
370 - (one - from.intensity())
371 * (one
372 + (from.saturation() * shifted_hue.cos())
373 / (Deg(num_traits::cast::<_, T>(60.0).unwrap()) - shifted_hue).cos());
374
375 let c3 = num_traits::cast::<_, T>(3.0).unwrap() * from.intensity() - (c1 + c2);
376
377 match hue_seg {
378 1 | 2 => Rgb::new(c3, c1, c2),
379 3 | 4 => Rgb::new(c2, c3, c1),
380 5 | 0 => Rgb::new(c1, c2, c3),
381 _ => unreachable!(),
382 }
383 }
384 }
385}
386
387#[cfg(test)]
388mod test {
389 use super::*;
390 use crate::hsi::Hsi;
391 use crate::rgb::Rgb;
392 use crate::test;
393 use angle::Turns;
394 use approx::*;
395
396 #[test]
397 fn test_construct() {
398 let c1 = eHsi::new(Deg(140.0), 0.68, 0.22);
399 assert_relative_eq!(c1.hue(), Deg(140.0));
400 assert_relative_eq!(c1.saturation(), 0.68);
401 assert_relative_eq!(c1.intensity(), 0.22);
402 assert_eq!(c1.to_tuple(), (Deg(140.0), 0.68, 0.22));
403 assert_eq!(eHsi::from_tuple(c1.to_tuple()), c1);
404
405 let c2 = eHsi::new(Rad(2.0f32), 0.33f32, 0.10);
406 assert_relative_eq!(c2.hue(), Rad(2.0f32));
407 assert_relative_eq!(c2.saturation(), 0.33);
408 assert_relative_eq!(c2.intensity(), 0.10);
409 assert_eq!(c2.to_tuple(), (Rad(2.0f32), 0.33f32, 0.10f32));
410 assert_eq!(eHsi::from_tuple(c2.to_tuple()), c2);
411 }
412
413 #[test]
414 fn test_invert() {
415 let c1 = eHsi::new(Deg(198.0), 0.33, 0.49);
416 assert_relative_eq!(c1.invert(), eHsi::new(Deg(18.0), 0.67, 0.51));
417 assert_relative_eq!(c1.invert().invert(), c1);
418
419 let c2 = eHsi::from_tuple((Turns(0.40), 0.50, 0.00));
420 assert_relative_eq!(c2.invert(), eHsi::new(Turns(0.90), 0.50, 1.00));
421 assert_relative_eq!(c2.invert().invert(), c2);
422 }
423
424 #[test]
425 fn test_lerp() {
426 let c1 = eHsi::new(Turns(0.9), 0.46, 0.20);
427 let c2 = eHsi::new(Turns(0.3), 0.50, 0.50);
428 assert_relative_eq!(c1.lerp(&c2, 0.0), c1);
429 assert_relative_eq!(c1.lerp(&c2, 1.0), c2);
430 assert_relative_eq!(c1.lerp(&c2, 0.5), eHsi::new(Turns(0.1), 0.48, 0.35));
431 assert_relative_eq!(c1.lerp(&c2, 0.25), eHsi::new(Turns(0.0), 0.47, 0.275));
432 }
433
434 #[test]
435 fn test_normalize() {
436 let c1 = eHsi::new(Deg(400.0), 1.25, -0.33);
437 assert!(!c1.is_normalized());
438 assert_relative_eq!(c1.normalize(), eHsi::new(Deg(40.0), 1.00, 0.00));
439 assert_eq!(c1.normalize().normalize(), c1.normalize());
440
441 let c2 = eHsi::new(Deg(20.0), 0.35, 0.99);
442 assert!(c2.is_normalized());
443 assert_eq!(c2.normalize(), c2);
444 }
445
446 #[test]
447 fn hsi_ehsi_convert() {
448 let hsi1 = Hsi::new(Deg(120.0), 0.0, 0.0);
449 let ehsi1 = eHsi::from_hsi(&hsi1);
450 assert_eq!(ehsi1, Some(eHsi::new(Deg(120.0), 0.0, 0.0)));
451 assert_eq!(hsi1, ehsi1.unwrap().to_hsi().unwrap());
452
453 let ehsi2 = eHsi::from_hsi(&Hsi::new(Deg(120.0), 1.0, 1.0));
454 assert_eq!(ehsi2, None);
455
456 let hsi3 = Hsi::new(Deg(180.0), 1.0, 0.60);
457 let ehsi3 = eHsi::from_hsi(&hsi3);
458 assert_relative_eq!(ehsi3.unwrap(), eHsi::new(Deg(180.0), 1.0, 0.60));
459 assert_relative_eq!(hsi3, &ehsi3.unwrap().to_hsi().unwrap());
460
461 let hsi3 = Hsi::new(Deg(180.0), 1.0, 0.70);
462 let ehsi3 = eHsi::from_hsi(&hsi3);
463 assert_eq!(ehsi3, None);
464 }
465
466 #[test]
467 fn test_ehsi_to_rgb() {
468 let test_data = test::build_hs_test_data();
469
470 for item in test_data.iter() {
471 let hsi = eHsi::<_, Deg<_>>::from_color(&item.rgb);
472 let rgb = Rgb::from_color(&hsi);
473 assert_relative_eq!(rgb, item.rgb, epsilon = 2e-3);
474 }
475
476 let big_test_data = test::build_hwb_test_data();
477
478 for item in big_test_data.iter() {
479 let hsi = eHsi::<_, Deg<_>>::from_color(&item.rgb);
480 let rgb = Rgb::from_color(&hsi);
481 assert_relative_eq!(rgb, item.rgb, epsilon = 2e-3);
482 }
483 }
484
485 #[test]
486 fn test_rgb_to_ehsi() {
487 let test_data = test::build_hs_test_data();
488
489 for item in test_data.iter() {
490 let hsi = eHsi::from_color(&item.rgb);
491 if hsi.is_same_as_hsi() {
492 println!("{}; {}; {}", hsi, item.hsi, item.rgb);
493 assert_relative_eq!(hsi.hue(), item.hsi.hue(), epsilon = 1e-1);
494 assert_relative_eq!(hsi.saturation(), item.hsi.saturation(), epsilon = 2e-3);
495 assert_relative_eq!(hsi.intensity(), item.hsi.intensity(), epsilon = 2e-3);
496 }
497 }
498
499 let c1 = Rgb::new(1.0, 1.0, 1.0);
500 let h1 = eHsi::from_color(&c1);
501 assert_relative_eq!(h1, eHsi::new(Deg(0.0), 1.0, 1.0));
502
503 let c2 = Rgb::new(0.5, 1.0, 1.0);
504 let h2 = eHsi::from_color(&c2);
505 assert_relative_eq!(h2, eHsi::new(Deg(180.0), 1.0, 0.833333333), epsilon = 1e-3);
506 }
507
508 #[test]
509 fn test_color_cast() {
510 let c1 = eHsi::new(Deg(240.0f32), 0.22f32, 0.81f32);
511 assert_relative_eq!(c1.color_cast::<f32, Turns<f32>>().color_cast(), c1);
512 assert_relative_eq!(c1.color_cast(), c1);
513 assert_relative_eq!(
514 c1.color_cast(),
515 eHsi::new(Turns(0.6666666), 0.22, 0.81),
516 epsilon = 1e-5
517 );
518 }
519}