Skip to main content

pax_runtime_api/
color.rs

1//! Color and opacity types used by Pax style properties and render backends.
2
3use super::*;
4
5/// Raw Percent type, which we use for serialization and dynamic traversal.  At the time
6/// of authoring, this type is not used directly at runtime, but is intended for `into` coercion
7/// into downstream types, e.g. ColorChannel, Rotation, and Size.  This allows us to be "dumb"
8/// about how we parse `%`, and allow the context in which it is used to pull forward a specific
9/// type through `into` inference.
10#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
11pub struct Percent(pub Numeric);
12
13impl Display for Percent {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "{}%", self.0)
16    }
17}
18
19impl Interpolatable for Percent {
20    fn interpolate(&self, other: &Self, t: f64) -> Self {
21        Self(self.0.interpolate(&other.0, t))
22    }
23}
24
25/// Describes an opacity value either as normalized alpha or as a percent.
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub enum Opacity {
28    /// Unitless alpha in the normalized [0.0, 1.0] range.
29    Alpha(Numeric),
30    /// Percent alpha in the [0.0, 100.0] range.
31    Percent(Numeric),
32}
33
34impl std::hash::Hash for Opacity {
35    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
36        match self {
37            Self::Alpha(v) => {
38                0u8.hash(state);
39                v.hash(state);
40            }
41            Self::Percent(v) => {
42                1u8.hash(state);
43                v.hash(state);
44            }
45        }
46    }
47}
48
49impl Display for Opacity {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Opacity::Alpha(value) => write!(f, "{}", value),
53            Opacity::Percent(value) => write!(f, "{}%", value),
54        }
55    }
56}
57
58impl Default for Opacity {
59    fn default() -> Self {
60        Self::Alpha(Numeric::F64(1.0))
61    }
62}
63
64impl Interpolatable for Opacity {
65    fn interpolate(&self, other: &Self, t: f64) -> Self {
66        Opacity::Alpha(Numeric::F64(
67            self.to_float_0_1().interpolate(&other.to_float_0_1(), t),
68        ))
69    }
70}
71
72impl From<f64> for Opacity {
73    fn from(value: f64) -> Self {
74        Numeric::F64(value).into()
75    }
76}
77
78impl From<i32> for Opacity {
79    fn from(value: i32) -> Self {
80        Numeric::from(value).into()
81    }
82}
83
84impl From<Numeric> for Opacity {
85    fn from(value: Numeric) -> Self {
86        Opacity::Alpha(value)
87    }
88}
89
90impl From<Percent> for Opacity {
91    fn from(value: Percent) -> Self {
92        Opacity::Percent(value.0)
93    }
94}
95
96impl Opacity {
97    /// Normalizes this Opacity as a float in [0.0, 1.0].
98    pub fn to_float_0_1(&self) -> f64 {
99        let alpha = match self {
100            Opacity::Alpha(value) => value.to_float(),
101            Opacity::Percent(value) => value.to_float() / 100.0,
102        };
103        if alpha.is_finite() {
104            alpha.clamp(0.0, 1.0)
105        } else {
106            log::warn!("Opacity must be finite; rendering transparent");
107            0.0
108        }
109    }
110}
111
112impl From<f64> for ColorChannel {
113    fn from(value: f64) -> Self {
114        Numeric::F64(value).into()
115    }
116}
117
118impl From<i32> for ColorChannel {
119    fn from(value: i32) -> Self {
120        Numeric::from(value).into()
121    }
122}
123
124impl Into<ColorChannel> for Percent {
125    fn into(self) -> ColorChannel {
126        ColorChannel::Percent(self.0)
127    }
128}
129
130impl Into<Size> for Percent {
131    fn into(self) -> Size {
132        Size::Percent(self.0)
133    }
134}
135
136impl Into<Rotation> for Percent {
137    fn into(self) -> Rotation {
138        Rotation::Percent(self.0)
139    }
140}
141
142/// Describes a color channel in a unit appropriate to the surrounding color model.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144pub enum ColorChannel {
145    /// Used, for example, to express hue in HSL or other rotational color models.
146    Rotation(Rotation),
147    /// Integer color channel in the `[0, 255]` range.
148    Integer(u8),
149    /// Percent color channel in the `[0.0, 100.0]` range.
150    Percent(Numeric),
151}
152
153impl Display for ColorChannel {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            ColorChannel::Rotation(rot) => write!(f, "{}", rot),
157            ColorChannel::Integer(int) => write!(f, "{}", int),
158            ColorChannel::Percent(per) => write!(f, "{}%", per),
159        }
160    }
161}
162
163impl Default for ColorChannel {
164    fn default() -> Self {
165        Self::Percent(Numeric::F64(50.0))
166    }
167}
168
169impl From<Numeric> for Rotation {
170    fn from(value: Numeric) -> Self {
171        Rotation::Degrees(value)
172    }
173}
174
175impl From<Numeric> for ColorChannel {
176    fn from(value: Numeric) -> Self {
177        Self::Integer(value.to_int().clamp(0, 255) as u8)
178    }
179}
180
181impl ColorChannel {
182    /// Normalizes this color channel as a float in the `[0.0, 1.0]` range.
183    pub fn to_float_0_1(&self) -> f64 {
184        match self {
185            Self::Percent(per) => (per.to_float() / 100.0).clamp(0_f64, 1_f64),
186            Self::Integer(zero_to_255) => {
187                let f_zero = (*zero_to_255) as f64;
188                (f_zero / 255.0_f64).clamp(0_f64, 1_f64)
189            }
190            Self::Rotation(rot) => rot.to_float_0_1(),
191        }
192    }
193}
194
195/// Entrypoint for specifying and representing colors in Pax.
196#[allow(non_camel_case_types)]
197#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq)]
198pub enum Color {
199    /// Models a color in the RGB space, with an alpha channel of 100%
200    rgb(ColorChannel, ColorChannel, ColorChannel),
201    /// Models a color in the RGBA space
202    rgba(ColorChannel, ColorChannel, ColorChannel, ColorChannel),
203    /// Models a color in the HSL space, with an alpha channel of 100%
204    hsl(Rotation, ColorChannel, ColorChannel),
205    /// Models a color in the HSLA space.
206    hsla(Rotation, ColorChannel, ColorChannel, ColorChannel),
207
208    /// Cool blue-gray, RGB (100, 116, 139).
209    #[default]
210    SLATE,
211    /// Balanced gray, RGB (107, 114, 128).
212    GRAY,
213    /// Crisp cool gray, RGB (113, 113, 122).
214    ZINC,
215    /// Plain neutral gray, RGB (115, 115, 115).
216    NEUTRAL,
217    /// Warm stone gray, RGB (120, 113, 108).
218    STONE,
219    /// Bright warm red, RGB (239, 68, 68).
220    RED,
221    /// Vivid citrus orange, RGB (249, 115, 22).
222    ORANGE,
223    /// Golden amber, RGB (245, 158, 11).
224    AMBER,
225    /// Sunny yellow, RGB (234, 179, 8).
226    YELLOW,
227    /// Electric lime, RGB (132, 204, 22).
228    LIME,
229    /// Fresh green, RGB (34, 197, 94).
230    GREEN,
231    /// Jewel emerald, RGB (16, 185, 129).
232    EMERALD,
233    /// Deep aquatic teal, RGB (20, 184, 166).
234    TEAL,
235    /// Bright clean cyan, RGB (6, 182, 212).
236    CYAN,
237    /// Open sky blue, RGB (14, 165, 233).
238    SKY,
239    /// Saturated primary blue, RGB (59, 130, 246).
240    BLUE,
241    /// Cool electric indigo, RGB (99, 102, 241).
242    INDIGO,
243    /// Soft vivid violet, RGB (139, 92, 246).
244    VIOLET,
245    /// Rich playful purple, RGB (168, 85, 247).
246    PURPLE,
247    /// Brilliant magenta fuchsia, RGB (217, 70, 239).
248    FUCHSIA,
249    /// Bright candy pink, RGB (236, 72, 153).
250    PINK,
251    /// Warm rosy red, RGB (244, 63, 94).
252    ROSE,
253    /// Pure black, RGB (0, 0, 0).
254    BLACK,
255    /// Pure white, RGB (255, 255, 255).
256    WHITE,
257    /// Fully transparent white, RGB (255, 255, 255).
258    TRANSPARENT,
259    /// Non-rendering transparent, RGB (255, 255, 255).
260    NONE,
261}
262
263impl Hash for Color {
264    fn hash<H: Hasher>(&self, state: &mut H) {
265        core::mem::discriminant(self).hash(state);
266    }
267}
268
269// implement display respecting all color enum variants, printing out their names
270impl Display for Color {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        match self {
273            Self::rgb(r, g, b) => write!(f, "rgb({}, {}, {})", r, g, b),
274            Self::rgba(r, g, b, a) => write!(f, "rgba({}, {}, {}, {})", r, g, b, a),
275            Self::hsl(h, s, l) => write!(f, "hsl({}, {}, {})", h, s, l),
276            Self::hsla(h, s, l, a) => write!(f, "hsla({}, {}, {}, {})", h, s, l, a),
277            Self::SLATE => write!(f, "SLATE"),
278            Self::GRAY => write!(f, "GRAY"),
279            Self::ZINC => write!(f, "ZINC"),
280            Self::NEUTRAL => write!(f, "NEUTRAL"),
281            Self::STONE => write!(f, "STONE"),
282            Self::RED => write!(f, "RED"),
283            Self::ORANGE => write!(f, "ORANGE"),
284            Self::AMBER => write!(f, "AMBER"),
285            Self::YELLOW => write!(f, "YELLOW"),
286            Self::LIME => write!(f, "LIME"),
287            Self::GREEN => write!(f, "GREEN"),
288            Self::EMERALD => write!(f, "EMERALD"),
289            Self::TEAL => write!(f, "TEAL"),
290            Self::CYAN => write!(f, "CYAN"),
291            Self::SKY => write!(f, "SKY"),
292            Self::BLUE => write!(f, "BLUE"),
293            Self::INDIGO => write!(f, "INDIGO"),
294            Self::VIOLET => write!(f, "VIOLET"),
295            Self::PURPLE => write!(f, "PURPLE"),
296            Self::FUCHSIA => write!(f, "FUCHSIA"),
297            Self::PINK => write!(f, "PINK"),
298            Self::ROSE => write!(f, "ROSE"),
299            Self::BLACK => write!(f, "BLACK"),
300            Self::WHITE => write!(f, "WHITE"),
301            Self::TRANSPARENT => write!(f, "TRANSPARENT"),
302            Self::NONE => write!(f, "NONE"),
303        }
304    }
305}
306
307impl Color {
308    //TODO: build out tint api and consider other color transforms
309    //pub fn tint(tint_offset_amount) -> Self {...}
310
311    /// Constructs an RGB color with 100% alpha.
312    pub fn rgb(r: ColorChannel, g: ColorChannel, b: ColorChannel) -> Self {
313        Self::rgb(r, g, b)
314    }
315
316    /// Constructs an RGBA color.
317    pub fn rgba(r: ColorChannel, g: ColorChannel, b: ColorChannel, a: ColorChannel) -> Self {
318        Self::rgba(r, g, b, a)
319    }
320
321    /// Constructs an HSL color with 100% alpha.
322    pub fn hsl(h: Rotation, s: ColorChannel, l: ColorChannel) -> Self {
323        Self::hsl(h, s, l)
324    }
325
326    /// Constructs an HSLA color.
327    pub fn hsla(h: Rotation, s: ColorChannel, l: ColorChannel, a: ColorChannel) -> Self {
328        Self::hsla(h, s, l, a)
329    }
330
331    // Converts to the Piet color type used by canvas renderers.
332    pub fn to_piet_color(&self) -> piet::Color {
333        let rgba = self.to_rgba_0_1();
334        piet::Color::rgba(rgba[0], rgba[1], rgba[2], rgba[3])
335    }
336
337    /// Constructs a color from normalized RGBA channels in the `[0.0, 1.0]` range.
338    pub fn from_rgba_0_1(rgba_0_1: [f64; 4]) -> Self {
339        Self::rgba(
340            ColorChannel::Percent(Numeric::F64(rgba_0_1[0] * 100.0)),
341            ColorChannel::Percent(Numeric::F64(rgba_0_1[1] * 100.0)),
342            ColorChannel::Percent(Numeric::F64(rgba_0_1[2] * 100.0)),
343            ColorChannel::Percent(Numeric::F64(rgba_0_1[3] * 100.0)),
344        )
345    }
346
347    /// Constructs a color from a six- or eight-character RGB/RGBA hex string.
348    pub fn from_hex(hex: &str) -> Self {
349        let r = u8::from_str_radix(&hex[0..2], 16).unwrap() as f64 / 255.0;
350        let g = u8::from_str_radix(&hex[2..4], 16).unwrap() as f64 / 255.0;
351        let b = u8::from_str_radix(&hex[4..6], 16).unwrap() as f64 / 255.0;
352        let a = if hex.len() == 8 {
353            u8::from_str_radix(&hex[6..8], 16).unwrap() as f64 / 255.0
354        } else {
355            1.0
356        };
357        Self::rgba(
358            ColorChannel::Percent(Numeric::F64(r * 100.0)),
359            ColorChannel::Percent(Numeric::F64(g * 100.0)),
360            ColorChannel::Percent(Numeric::F64(b * 100.0)),
361            ColorChannel::Percent(Numeric::F64(a * 100.0)),
362        )
363    }
364
365    /// Returns this color's alpha channel normalized to the `[0.0, 1.0]` range.
366    pub fn alpha_0_1(&self) -> f64 {
367        self.to_rgba_0_1()[3]
368    }
369
370    /// Adds weighted colors in premultiplied RGBA, then returns straight RGBA.
371    /// Weights should sum to one. This retains the current RGB encoding; it does
372    /// not perform a linear-light conversion or blend against a backdrop.
373    pub fn blend_premultiplied(colors: impl IntoIterator<Item = (Color, f64)>) -> Self {
374        let mut sum = [0.0; 4];
375        for (color, weight) in colors {
376            let rgba = color.to_rgba_0_1();
377            let alpha = rgba[3] * weight;
378            for i in 0..3 {
379                sum[i] += rgba[i] * alpha;
380            }
381            sum[3] += alpha;
382        }
383        if sum[3] > 0.0 {
384            for i in 0..3 {
385                sum[i] /= sum[3];
386            }
387        }
388        Self::from_rgba_0_1(sum)
389    }
390
391    /// Multiplies this color's alpha channel by `factor`.
392    pub fn with_alpha_factor(&self, factor: f64) -> Self {
393        let mut rgba = self.to_rgba_0_1();
394        rgba[3] = (rgba[3] * factor.clamp(0.0, 1.0)).clamp(0.0, 1.0);
395        Self::from_rgba_0_1(rgba)
396    }
397
398    /// Returns RGBA channels normalized to the `[0.0, 1.0]` range.
399    pub fn to_rgba_0_1(&self) -> [f64; 4] {
400        match self {
401            Self::hsla(h, s, l, a) => {
402                let rgb = hsl_to_rgb(h.to_float_0_1(), s.to_float_0_1(), l.to_float_0_1());
403                [rgb[0], rgb[1], rgb[2], a.to_float_0_1()]
404            }
405            Self::hsl(h, s, l) => {
406                let rgb = hsl_to_rgb(h.to_float_0_1(), s.to_float_0_1(), l.to_float_0_1());
407                [rgb[0], rgb[1], rgb[2], 1.0]
408            }
409            Self::rgba(r, g, b, a) => [
410                r.to_float_0_1(),
411                g.to_float_0_1(),
412                b.to_float_0_1(),
413                a.to_float_0_1(),
414            ],
415            Self::rgb(r, g, b) => [r.to_float_0_1(), g.to_float_0_1(), b.to_float_0_1(), 1.0],
416
417            //Color constants from TailwindCSS
418            Self::SLATE => Self::rgb(
419                Numeric::from(0x64).into(),
420                Numeric::from(0x74).into(),
421                Numeric::from(0x8b).into(),
422            )
423            .to_rgba_0_1(),
424            Self::GRAY => Self::rgb(
425                Numeric::from(0x6b).into(),
426                Numeric::from(0x72).into(),
427                Numeric::from(0x80).into(),
428            )
429            .to_rgba_0_1(),
430            Self::ZINC => Self::rgb(
431                Numeric::from(0x71).into(),
432                Numeric::from(0x71).into(),
433                Numeric::from(0x7a).into(),
434            )
435            .to_rgba_0_1(),
436            Self::NEUTRAL => Self::rgb(
437                Numeric::from(0x73).into(),
438                Numeric::from(0x73).into(),
439                Numeric::from(0x73).into(),
440            )
441            .to_rgba_0_1(),
442            Self::STONE => Self::rgb(
443                Numeric::from(0x78).into(),
444                Numeric::from(0x71).into(),
445                Numeric::from(0x6c).into(),
446            )
447            .to_rgba_0_1(),
448            Self::RED => Self::rgb(
449                Numeric::from(0xeF).into(),
450                Numeric::from(0x44).into(),
451                Numeric::from(0x44).into(),
452            )
453            .to_rgba_0_1(),
454            Self::ORANGE => Self::rgb(
455                Numeric::from(0xf9).into(),
456                Numeric::from(0x73).into(),
457                Numeric::from(0x16).into(),
458            )
459            .to_rgba_0_1(),
460            Self::AMBER => Self::rgb(
461                Numeric::from(0xf5).into(),
462                Numeric::from(0x9e).into(),
463                Numeric::from(0x0b).into(),
464            )
465            .to_rgba_0_1(),
466            Self::YELLOW => Self::rgb(
467                Numeric::from(0xea).into(),
468                Numeric::from(0xb3).into(),
469                Numeric::from(0x08).into(),
470            )
471            .to_rgba_0_1(),
472            Self::LIME => Self::rgb(
473                Numeric::from(0x84).into(),
474                Numeric::from(0xcc).into(),
475                Numeric::from(0x16).into(),
476            )
477            .to_rgba_0_1(),
478            Self::GREEN => Self::rgb(
479                Numeric::from(0x22).into(),
480                Numeric::from(0xc5).into(),
481                Numeric::from(0x5e).into(),
482            )
483            .to_rgba_0_1(),
484            Self::EMERALD => Self::rgb(
485                Numeric::from(0x10).into(),
486                Numeric::from(0xb9).into(),
487                Numeric::from(0x81).into(),
488            )
489            .to_rgba_0_1(),
490            Self::TEAL => Self::rgb(
491                Numeric::from(0x14).into(),
492                Numeric::from(0xb8).into(),
493                Numeric::from(0xa6).into(),
494            )
495            .to_rgba_0_1(),
496            Self::CYAN => Self::rgb(
497                Numeric::from(0x06).into(),
498                Numeric::from(0xb6).into(),
499                Numeric::from(0xd4).into(),
500            )
501            .to_rgba_0_1(),
502            Self::SKY => Self::rgb(
503                Numeric::from(0x0e).into(),
504                Numeric::from(0xa5).into(),
505                Numeric::from(0xe9).into(),
506            )
507            .to_rgba_0_1(),
508            Self::BLUE => Self::rgb(
509                Numeric::from(0x3b).into(),
510                Numeric::from(0x82).into(),
511                Numeric::from(0xf6).into(),
512            )
513            .to_rgba_0_1(),
514            Self::INDIGO => Self::rgb(
515                Numeric::from(0x63).into(),
516                Numeric::from(0x66).into(),
517                Numeric::from(0xf1).into(),
518            )
519            .to_rgba_0_1(),
520            Self::VIOLET => Self::rgb(
521                Numeric::from(0x8b).into(),
522                Numeric::from(0x5c).into(),
523                Numeric::from(0xf6).into(),
524            )
525            .to_rgba_0_1(),
526            Self::PURPLE => Self::rgb(
527                Numeric::from(0xa8).into(),
528                Numeric::from(0x55).into(),
529                Numeric::from(0xf7).into(),
530            )
531            .to_rgba_0_1(),
532            Self::FUCHSIA => Self::rgb(
533                Numeric::from(0xd9).into(),
534                Numeric::from(0x46).into(),
535                Numeric::from(0xef).into(),
536            )
537            .to_rgba_0_1(),
538            Self::PINK => Self::rgb(
539                Numeric::from(0xec).into(),
540                Numeric::from(0x48).into(),
541                Numeric::from(0x99).into(),
542            )
543            .to_rgba_0_1(),
544            Self::ROSE => Self::rgb(
545                Numeric::from(0xf4).into(),
546                Numeric::from(0x3f).into(),
547                Numeric::from(0x5e).into(),
548            )
549            .to_rgba_0_1(),
550            Self::BLACK => Self::rgb(
551                Numeric::from(0x00).into(),
552                Numeric::from(0x00).into(),
553                Numeric::from(0x00).into(),
554            )
555            .to_rgba_0_1(),
556            Self::WHITE => Self::rgb(
557                Numeric::from(0xff).into(),
558                Numeric::from(0xff).into(),
559                Numeric::from(0xff).into(),
560            )
561            .to_rgba_0_1(),
562            Self::TRANSPARENT | Self::NONE => Self::rgba(
563                Numeric::from(0xff).into(),
564                Numeric::from(0xff).into(),
565                Numeric::from(0xFF).into(),
566                Numeric::from(0x00).into(),
567            )
568            .to_rgba_0_1(),
569        }
570    }
571
572    /// Returns HSLA channels normalized to the `[0.0, 1.0]` range.
573    // Credit: Claude 3.5 Sonnet.
574    pub fn to_hsla_0_1(&self) -> [f64; 4] {
575        let [r, g, b, a] = self.to_rgba_0_1();
576
577        let max = r.max(g).max(b);
578        let min = r.min(g).min(b);
579        let chroma = max - min;
580
581        let h = if chroma == 0.0 {
582            0.0
583        } else if max == r {
584            ((g - b) / chroma + 6.0) % 6.0 / 6.0
585        } else if max == g {
586            ((b - r) / chroma + 2.0) / 6.0
587        } else {
588            ((r - g) / chroma + 4.0) / 6.0
589        };
590
591        let l = (max + min) / 2.0;
592
593        let s = if l == 0.0 || l == 1.0 {
594            0.0
595        } else {
596            (max - l) / l.min(1.0 - l)
597        };
598
599        [h, s, l, a]
600    }
601}
602
603//hsl_to_rgb logic borrowed & modified from https://github.com/emgyrz/colorsys.rs, licensed MIT Copyright (c) 2019 mz <emgyrz@gmail.com>
604const RGB_UNIT_MAX: f64 = 255.0;
605fn hsl_to_rgb(h: f64, s: f64, l: f64) -> [f64; 3] {
606    if s == 0.0 {
607        let unit = RGB_UNIT_MAX * l;
608        return [
609            unit / RGB_UNIT_MAX,
610            unit / RGB_UNIT_MAX,
611            unit / RGB_UNIT_MAX,
612        ];
613    }
614
615    let temp1 = if l < 0.5 {
616        l * (1.0 + s)
617    } else {
618        l + s - l * s
619    };
620
621    let temp2 = 2.0 * l - temp1;
622    let hue = h;
623
624    let temp_r = bound(hue + (1.0 / 3.0), 1.0);
625    let temp_g = bound(hue, 1.0);
626    let temp_b = bound(hue - (1.0 / 3.0), 1.0);
627
628    let r = calc_rgb_unit(temp_r, temp1, temp2);
629    let g = calc_rgb_unit(temp_g, temp1, temp2);
630    let b = calc_rgb_unit(temp_b, temp1, temp2);
631    [r / RGB_UNIT_MAX, g / RGB_UNIT_MAX, b / RGB_UNIT_MAX]
632}
633
634fn calc_rgb_unit(unit: f64, temp1: f64, temp2: f64) -> f64 {
635    let mut result = temp2;
636    if 6.0 * unit < 1.0 {
637        result = temp2 + (temp1 - temp2) * 6.0 * unit
638    } else if 2.0 * unit < 1.0 {
639        result = temp1
640    } else if 3.0 * unit < 2.0 {
641        result = temp2 + (temp1 - temp2) * ((2.0 / 3.0) - unit) * 6.0
642    }
643    result * RGB_UNIT_MAX
644}
645
646// Wraps a scalar into the `[0, entire]` interval, used by HSL conversion.
647pub fn bound(r: f64, entire: f64) -> f64 {
648    let mut n = r;
649    loop {
650        let less = n < 0.0;
651        let bigger = n > entire;
652        if !less && !bigger {
653            break n;
654        }
655        if less {
656            n += entire;
657        } else {
658            n -= entire;
659        }
660    }
661}
662
663impl Into<ColorMessage> for &Color {
664    fn into(self) -> ColorMessage {
665        let rgba = self.to_rgba_0_1();
666        ColorMessage::Rgba(rgba)
667    }
668}
669impl PartialEq<ColorMessage> for Color {
670    fn eq(&self, other: &ColorMessage) -> bool {
671        let self_rgba = self.to_rgba_0_1();
672
673        match other {
674            ColorMessage::Rgb(other_rgba) => {
675                self_rgba[0] == other_rgba[0]
676                    && self_rgba[1] == other_rgba[1]
677                    && self_rgba[2] == other_rgba[2]
678                    && self_rgba[3] == 1.0
679            }
680            ColorMessage::Rgba(other_rgba) => {
681                self_rgba[0] == other_rgba[0]
682                    && self_rgba[1] == other_rgba[1]
683                    && self_rgba[2] == other_rgba[2]
684                    && self_rgba[3] == other_rgba[3]
685            }
686        }
687    }
688}
689impl Interpolatable for Color {
690    fn interpolate(&self, other: &Self, t: f64) -> Self {
691        let rgba_s = self.to_rgba_0_1();
692        let rgba_o = other.to_rgba_0_1();
693        let rgba_i = [
694            rgba_s[0].interpolate(&rgba_o[0], t),
695            rgba_s[1].interpolate(&rgba_o[1], t),
696            rgba_s[2].interpolate(&rgba_o[2], t),
697            rgba_s[3].interpolate(&rgba_o[3], t),
698        ];
699        Color::from_rgba_0_1(rgba_i)
700    }
701}