Skip to main content

inset_embedder/geometry/
color.rs

1//! Flutter counterpart: `engine/src/flutter/lib/ui/painting.dart` (`Color`, `ColorSpace`).
2
3use std::fmt::{self, Debug};
4
5use super::lerp::lerp_double_non_null;
6use super::math::clamp_double;
7
8fn scale_alpha(x: Color, factor: f64) -> Color {
9    x.with_values(
10        Some(clamp_double(x.a * factor, 0.0, 1.0)),
11        None,
12        None,
13        None,
14        None,
15    )
16}
17
18fn wider_color_space(a: ColorSpace, b: ColorSpace) -> ColorSpace {
19    if a == ColorSpace::DisplayP3 || b == ColorSpace::DisplayP3 {
20        ColorSpace::DisplayP3
21    } else {
22        a
23    }
24}
25
26/// An immutable color value in ARGB format.
27///
28/// Consider the light teal of the [Flutter logo](https://flutter.dev/brand). It
29/// is fully opaque, with a red [`r`](Color::r) channel value of `0.2588` (or
30/// `0x42` or `66` as an 8-bit value), a green [`g`](Color::g) channel value of
31/// `0.6471` (or `0xA5` or `165` as an 8-bit value), and a blue [`b`](Color::b)
32/// channel value of `0.9608` (or `0xF5` or `245` as an 8-bit value). In a common
33/// [CSS hex color syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color)
34/// for RGB color values, it would be described as `#42A5F5`.
35///
36/// Here are some ways it could be constructed:
37///
38/// ```
39/// # use inset_embedder::{Color, ColorSpace};
40/// let c1 = Color::from(1.0, 0.2588, 0.6471, 0.9608, ColorSpace::Srgb);
41/// let c2 = Color::new(0xFF42A5F5);
42/// let c3 = Color::from_argb(0xFF, 0x42, 0xA5, 0xF5);
43/// let c4 = Color::from_argb(255, 66, 165, 245);
44/// let c5 = Color::from_rgbo(66, 165, 245, 1.0);
45/// ```
46///
47/// If you are having a problem with [`Color::new`] wherein it seems your color
48/// is just not painting, check to make sure you are specifying the full 8
49/// hexadecimal digits. If you only specify six, then the leading two digits are
50/// assumed to be zero, which means fully-transparent:
51///
52/// ```
53/// # use inset_embedder::{Color, ColorSpace};
54/// let c1 = Color::new(0xFFFFFF); // fully transparent white (invisible)
55/// let c2 = Color::new(0xFFFFFFFF); // fully opaque white (visible)
56///
57/// // Or use double-based channel values:
58/// let c3 = Color::from(1.0, 1.0, 1.0, 1.0, ColorSpace::Srgb);
59/// ```
60///
61/// [`Color`]'s color components are stored as floating-point values. Care should
62/// be taken if one does not want the literal equality provided by [`PartialEq`].
63/// To test equality inside of Flutter tests consider using [`isSameColorAs`][].
64///
65/// See also:
66///
67///  * [Colors](https://api.flutter.dev/flutter/material/Colors-class.html),
68///    which defines the colors found in the Material Design specification.
69///  * [`isSameColorAs`][],
70///    a Matcher to handle floating-point deltas when checking [`Color`] equality.
71///
72/// [`isSameColorAs`]: https://api.flutter.dev/flutter/flutter_test/isSameColorAs.html
73#[derive(Clone, Copy, PartialEq)]
74pub struct Color {
75    /// The alpha channel of this color.
76    pub a: f64,
77    /// The red channel of this color.
78    pub r: f64,
79    /// The green channel of this color.
80    pub g: f64,
81    /// The blue channel of this color.
82    pub b: f64,
83    /// The color space of this color.
84    pub color_space: ColorSpace,
85}
86
87impl Color {
88    /// Construct a [`ColorSpace::Srgb`] color from the lower 32 bits of an integer.
89    ///
90    /// The bits are interpreted as follows:
91    ///
92    /// * Bits 24-31 are the alpha value.
93    /// * Bits 16-23 are the red value.
94    /// * Bits 8-15 are the green value.
95    /// * Bits 0-7 are the blue value.
96    ///
97    /// In other words, if AA is the alpha value in hex, RR the red value in hex,
98    /// GG the green value in hex, and BB the blue value in hex, a color can be
99    /// expressed as `Color::new(0xAARRGGBB)`.
100    ///
101    /// For example, to get a fully opaque orange, you would use
102    /// `Color::new(0xFFFF9000)` (`FF` for the alpha, `FF` for the red, `90` for
103    /// the green, and `00` for the blue).
104    ///
105    /// > **Note**
106    /// > Each color is stored as floating-point color components, where the final
107    /// > value of each component is approximated by storing `c / 255`, where `c`
108    /// > is one of the four components (alpha, red, green, blue).
109    pub const fn new(value: u32) -> Color {
110        Self::from_argbc(
111            (value >> 24) as i32,
112            (value >> 16) as i32,
113            (value >> 8) as i32,
114            value as i32,
115            ColorSpace::Srgb,
116        )
117    }
118
119    /// Construct a color with floating-point color components.
120    ///
121    /// Color components allows arbitrary bit depths for color components to be
122    /// supported. The values are interpreted relative to the [`ColorSpace`]
123    /// argument.
124    ///
125    /// ## Example
126    ///
127    /// ```
128    /// # use inset_embedder::{Color, ColorSpace};
129    /// // Fully opaque maximum red color
130    /// let c1 = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::Srgb);
131    ///
132    /// // Partially transparent moderately blue and green color
133    /// let c2 = Color::from(0.5, 0.0, 0.5, 0.5, ColorSpace::Srgb);
134    ///
135    /// // Fully transparent color
136    /// let c3 = Color::from(0.0, 0.0, 0.0, 0.0, ColorSpace::Srgb);
137    /// ```
138    pub const fn from(
139        alpha: f64,
140        red: f64,
141        green: f64,
142        blue: f64,
143        color_space: ColorSpace,
144    ) -> Color {
145        Color {
146            a: alpha,
147            r: red,
148            g: green,
149            b: blue,
150            color_space,
151        }
152    }
153
154    /// Construct an sRGB color from the lower 8 bits of four integers.
155    ///
156    /// * `a` is the alpha value, with 0 being transparent and 255 being fully
157    ///   opaque.
158    /// * `r` is [red](Color::red), from 0 to 255.
159    /// * `g` is [green](Color::green), from 0 to 255.
160    /// * `b` is [blue](Color::blue), from 0 to 255.
161    ///
162    /// Out of range values are brought into range using modulo 255.
163    ///
164    /// See also [`from_rgbo`](Color::from_rgbo), which takes the alpha value as
165    /// a floating point value.
166    ///
167    /// > **Note**
168    /// > Each color is stored as floating-point color components, where the final
169    /// > value of each component is approximated by storing `c / 255`, where `c`
170    /// > is one of the four components (alpha, red, green, blue).
171    pub const fn from_argb(a: i32, r: i32, g: i32, b: i32) -> Color {
172        Self::from_argbc(a, r, g, b, ColorSpace::Srgb)
173    }
174
175    const fn from_argbc(
176        alpha: i32,
177        red: i32,
178        green: i32,
179        blue: i32,
180        color_space: ColorSpace,
181    ) -> Color {
182        Self::from_rgboc(
183            red,
184            green,
185            blue,
186            ((alpha & 0xff) as f64) / 255.0,
187            color_space,
188        )
189    }
190
191    /// Create an sRGB color from red, green, blue, and opacity, similar to
192    /// `rgba()` in CSS.
193    ///
194    /// * `r` is [red](Color::red), from 0 to 255.
195    /// * `g` is [green](Color::green), from 0 to 255.
196    /// * `b` is [blue](Color::blue), from 0 to 255.
197    /// * `opacity` is alpha channel of this color as a double, with 0.0 being
198    ///   transparent and 1.0 being fully opaque.
199    ///
200    /// Out of range values are brought into range using modulo 255.
201    ///
202    /// See also [`from_argb`](Color::from_argb), which takes the opacity as an
203    /// integer value.
204    ///
205    /// > **Note**
206    /// > Each color is stored as floating-point color components, where the final
207    /// > value of each component is approximated by storing `c / 255`, where `c`
208    /// > is one of the four components (alpha, red, green, blue).
209    pub const fn from_rgbo(r: i32, g: i32, b: i32, opacity: f64) -> Color {
210        Self::from_rgboc(r, g, b, opacity, ColorSpace::Srgb)
211    }
212
213    const fn from_rgboc(r: i32, g: i32, b: i32, opacity: f64, color_space: ColorSpace) -> Color {
214        Color {
215            a: opacity,
216            r: ((r & 0xff) as f64) / 255.0,
217            g: ((g & 0xff) as f64) / 255.0,
218            b: ((b & 0xff) as f64) / 255.0,
219            color_space,
220        }
221    }
222
223    fn float_to_int8(x: f64) -> i32 {
224        (x * 255.0).round().clamp(0.0, 255.0) as i32
225    }
226
227    /// A 32 bit value representing this color.
228    ///
229    /// This getter is a *stub*. It is recommended instead to use the explicit
230    /// [`to_argb32`](Color::to_argb32) method.
231    #[deprecated(
232        note = "Use component accessors like .r or .g, or to_argb32 for an explicit conversion"
233    )]
234    pub fn value(&self) -> u32 {
235        self.to_argb32()
236    }
237
238    /// Returns a 32-bit value representing this color.
239    ///
240    /// The returned value is compatible with the default constructor
241    /// ([`Color::new`]) but does *not* guarantee to result in the same color due
242    /// to [imprecisions in numeric conversions](https://en.wikipedia.org/wiki/Floating-point_error_mitigation).
243    ///
244    /// Unlike accessing the floating point equivalent channels individually
245    /// ([`a`](Color::a), [`r`](Color::r), [`g`](Color::g), [`b`](Color::b)),
246    /// this method is intentionally *lossy*, and scales each channel using
247    /// `(channel * 255.0).round().clamp(0, 255)`.
248    ///
249    /// While useful for storing a 32-bit integer value, prefer accessing the
250    /// individual channels (and storing the double equivalent) where higher
251    /// precision is required.
252    ///
253    /// The bits are assigned as follows:
254    ///
255    /// * Bits 24-31 represents the [`a`](Color::a) channel as an 8-bit unsigned integer.
256    /// * Bits 16-23 represents the [`r`](Color::r) channel as an 8-bit unsigned integer.
257    /// * Bits 8-15 represents the [`g`](Color::g) channel as an 8-bit unsigned integer.
258    /// * Bits 0-7 represents the [`b`](Color::b) channel as an 8-bit unsigned integer.
259    ///
260    /// > **Warning**
261    /// > The value returned by this getter implicitly converts floating-point
262    /// > component values (such as `0.5`) into their 8-bit equivalent by using
263    /// > the [`to_argb32`](Color::to_argb32) method; the returned value is not
264    /// > guaranteed to be stable across different platforms or executions due to
265    /// > the complexity of floating-point math.
266    pub fn to_argb32(&self) -> u32 {
267        #[allow(clippy::identity_op)]
268        {
269            ((Self::float_to_int8(self.a) as u32) << 24)
270                | ((Self::float_to_int8(self.r) as u32) << 16)
271                | ((Self::float_to_int8(self.g) as u32) << 8)
272                | ((Self::float_to_int8(self.b) as u32) << 0)
273        }
274    }
275
276    /// The alpha channel of this color in an 8 bit value.
277    ///
278    /// A value of 0 means this color is fully transparent. A value of 255 means
279    /// this color is fully opaque.
280    #[deprecated(note = "Use (*.a * 255.0).round().clamp(0, 255)")]
281    #[allow(deprecated)]
282    pub fn alpha(&self) -> i32 {
283        ((0xff000000 & self.value()) >> 24) as i32
284    }
285
286    /// The alpha channel of this color as a double.
287    ///
288    /// A value of 0.0 means this color is fully transparent. A value of 1.0 means
289    /// this color is fully opaque.
290    #[deprecated(note = "Use .a.")]
291    #[allow(deprecated)]
292    pub fn opacity(&self) -> f64 {
293        (self.alpha() as f64) / (0xFF as f64)
294    }
295
296    /// The red channel of this color in an 8 bit value.
297    #[deprecated(note = "Use (*.r * 255.0).round().clamp(0, 255)")]
298    #[allow(deprecated)]
299    pub fn red(&self) -> i32 {
300        ((0x00ff0000 & self.value()) >> 16) as i32
301    }
302
303    /// The green channel of this color in an 8 bit value.
304    #[deprecated(note = "Use (*.g * 255.0).round().clamp(0, 255)")]
305    #[allow(deprecated)]
306    pub fn green(&self) -> i32 {
307        ((0x0000ff00 & self.value()) >> 8) as i32
308    }
309
310    /// The blue channel of this color in an 8 bit value.
311    #[deprecated(note = "Use (*.b * 255.0).round().clamp(0, 255)")]
312    #[allow(deprecated)]
313    pub fn blue(&self) -> i32 {
314        #[allow(clippy::identity_op)]
315        {
316            ((0x000000ff & self.value()) >> 0) as i32
317        }
318    }
319
320    /// Returns a new color with the provided components updated.
321    ///
322    /// Each component (`alpha`, `red`, `green`, `blue`) represents a
323    /// floating-point value; see [`Color::from`] for details and examples.
324    ///
325    /// If `color_space` is provided, and is different than the current color
326    /// space, the component values are updated before transforming them to the
327    /// provided [`ColorSpace`].
328    ///
329    /// Example:
330    /// ```
331    /// # use inset_embedder::Color;
332    /// /// Create a color with 50% opacity.
333    /// fn make_transparent(color: Color) -> Color {
334    ///     color.with_values(Some(0.5), None, None, None, None)
335    /// }
336    /// ```
337    pub fn with_values(
338        &self,
339        alpha: Option<f64>,
340        red: Option<f64>,
341        green: Option<f64>,
342        blue: Option<f64>,
343        color_space: Option<ColorSpace>,
344    ) -> Color {
345        let mut updated_components = None;
346        if alpha.is_some() || red.is_some() || green.is_some() || blue.is_some() {
347            updated_components = Some(Color::from(
348                alpha.unwrap_or(self.a),
349                red.unwrap_or(self.r),
350                green.unwrap_or(self.g),
351                blue.unwrap_or(self.b),
352                self.color_space,
353            ));
354        }
355        if let Some(color_space) = color_space
356            && color_space != self.color_space
357        {
358            let transform = get_color_transform(self.color_space, color_space);
359            transform.transform(updated_components.unwrap_or(*self), color_space)
360        } else {
361            updated_components.unwrap_or(*self)
362        }
363    }
364
365    /// Returns a new color that matches this color with the alpha channel
366    /// replaced with `a` (which ranges from 0 to 255).
367    ///
368    /// Out of range values will have unexpected effects.
369    #[allow(deprecated)]
370    pub fn with_alpha(&self, a: i32) -> Color {
371        Color::from_argb(a, self.red(), self.green(), self.blue())
372    }
373
374    /// Returns a new color that matches this color with the alpha channel
375    /// replaced with the given `opacity` (which ranges from 0.0 to 1.0).
376    ///
377    /// Out of range values will have unexpected effects.
378    #[deprecated(note = "Use .with_values() to avoid precision loss.")]
379    pub fn with_opacity(&self, opacity: f64) -> Color {
380        debug_assert!((0.0..=1.0).contains(&opacity));
381        self.with_alpha((255.0 * opacity).round() as i32)
382    }
383
384    /// Returns a new color that matches this color with the red channel replaced
385    /// with `r` (which ranges from 0 to 255).
386    ///
387    /// Out of range values will have unexpected effects.
388    #[allow(deprecated)]
389    pub fn with_red(&self, r: i32) -> Color {
390        Color::from_argb(self.alpha(), r, self.green(), self.blue())
391    }
392
393    /// Returns a new color that matches this color with the green channel
394    /// replaced with `g` (which ranges from 0 to 255).
395    ///
396    /// Out of range values will have unexpected effects.
397    #[allow(deprecated)]
398    pub fn with_green(&self, g: i32) -> Color {
399        Color::from_argb(self.alpha(), self.red(), g, self.blue())
400    }
401
402    /// Returns a new color that matches this color with the blue channel replaced
403    /// with `b` (which ranges from 0 to 255).
404    ///
405    /// Out of range values will have unexpected effects.
406    #[allow(deprecated)]
407    pub fn with_blue(&self, b: i32) -> Color {
408        Color::from_argb(self.alpha(), self.red(), self.green(), b)
409    }
410
411    // See <https://www.w3.org/TR/WCAG20/#relativeluminancedef>
412    fn linearize_color_component(component: f64) -> f64 {
413        if component <= 0.03928 {
414            return component / 12.92;
415        }
416        ((component + 0.055) / 1.055).powf(2.4)
417    }
418
419    /// Returns a brightness value between 0 for darkest and 1 for lightest.
420    ///
421    /// Represents the relative luminance of the color. This value is computationally
422    /// expensive to calculate.
423    ///
424    /// See <https://en.wikipedia.org/wiki/Relative_luminance>.
425    pub fn compute_luminance(&self) -> f64 {
426        debug_assert!(self.color_space != ColorSpace::ExtendedSrgb);
427        // See <https://www.w3.org/TR/WCAG20/#relativeluminancedef>
428        let r = Self::linearize_color_component(self.r);
429        let g = Self::linearize_color_component(self.g);
430        let b = Self::linearize_color_component(self.b);
431        0.2126 * r + 0.7152 * g + 0.0722 * b
432    }
433
434    /// Linearly interpolate between two colors.
435    ///
436    /// This is intended to be fast but as a result may be ugly. Consider
437    /// `HSVColor` or writing custom logic for interpolating colors.
438    ///
439    /// If either color is null, this function linearly interpolates from a
440    /// transparent instance of the other color. This is usually preferable to
441    /// interpolating from `material.Colors.transparent` (`Color::new(0x00000000)`),
442    /// which is specifically transparent *black*.
443    ///
444    /// The `t` argument represents position on the timeline, with 0.0 meaning
445    /// that the interpolation has not started, returning `a` (or something
446    /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
447    /// returning `b` (or something equivalent to `b`), and values in between
448    /// meaning that the interpolation is at the relevant point on the timeline
449    /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
450    /// 1.0, so negative values and values greater than 1.0 are valid (and can
451    /// easily be generated by curves such as `Curves.elasticInOut`). Each channel
452    /// will be clamped to the range 0 to 255.
453    ///
454    /// Values for `t` are usually obtained from an `Animation<f64>`, such as
455    /// an `AnimationController`.
456    ///
457    /// If the two colors are in different color spaces, both are converted to
458    /// the wider gamut color space before interpolating. The result will be in
459    /// the wider gamut color space. For example, interpolating between an sRGB
460    /// color and a Display P3 color will produce a Display P3 result.
461    pub fn lerp(x: Option<Color>, y: Option<Color>, t: f64) -> Option<Color> {
462        debug_assert!(x.is_none_or(|c| c.color_space != ColorSpace::ExtendedSrgb));
463        debug_assert!(y.is_none_or(|c| c.color_space != ColorSpace::ExtendedSrgb));
464        match (x, y) {
465            (None, None) => None,
466            (Some(x), None) => Some(scale_alpha(x, 1.0 - t)),
467            (None, Some(y)) => Some(scale_alpha(y, t)),
468            (Some(x), Some(y)) => {
469                let a;
470                let b;
471                let result_color_space;
472                if x.color_space == y.color_space {
473                    a = x;
474                    b = y;
475                    result_color_space = x.color_space;
476                } else {
477                    result_color_space = wider_color_space(x.color_space, y.color_space);
478                    a = x.with_values(None, None, None, None, Some(result_color_space));
479                    b = y.with_values(None, None, None, None, Some(result_color_space));
480                }
481                Some(Color::from(
482                    clamp_double(lerp_double_non_null(a.a, b.a, t), 0.0, 1.0),
483                    clamp_double(lerp_double_non_null(a.r, b.r, t), 0.0, 1.0),
484                    clamp_double(lerp_double_non_null(a.g, b.g, t), 0.0, 1.0),
485                    clamp_double(lerp_double_non_null(a.b, b.b, t), 0.0, 1.0),
486                    result_color_space,
487                ))
488            }
489        }
490    }
491
492    /// Combine the foreground color as a transparent color over top
493    /// of a background color, and return the resulting combined color.
494    ///
495    /// This uses standard alpha blending ("SRC over DST") rules to produce a
496    /// blended color from two colors. This can be used as a performance
497    /// enhancement when trying to avoid needless alpha blending compositing
498    /// operations for two things that are solid colors with the same shape, but
499    /// overlay each other: instead, just paint one with the combined color.
500    pub fn alpha_blend(foreground: Color, background: Color) -> Color {
501        debug_assert!(foreground.color_space == background.color_space);
502        debug_assert!(foreground.color_space != ColorSpace::ExtendedSrgb);
503        let alpha = foreground.a;
504        if alpha == 0.0 {
505            // Foreground completely transparent.
506            return background;
507        }
508        let inv_alpha = 1.0 - alpha;
509        let mut back_alpha = background.a;
510        if back_alpha == 1.0 {
511            // Opaque background case
512            Color::from(
513                1.0,
514                alpha * foreground.r + inv_alpha * background.r,
515                alpha * foreground.g + inv_alpha * background.g,
516                alpha * foreground.b + inv_alpha * background.b,
517                foreground.color_space,
518            )
519        } else {
520            // General case
521            back_alpha *= inv_alpha;
522            let out_alpha = alpha + back_alpha;
523            debug_assert!(out_alpha != 0.0);
524            Color::from(
525                out_alpha,
526                (foreground.r * alpha + background.r * back_alpha) / out_alpha,
527                (foreground.g * alpha + background.g * back_alpha) / out_alpha,
528                (foreground.b * alpha + background.b * back_alpha) / out_alpha,
529                foreground.color_space,
530            )
531        }
532    }
533
534    /// Returns an alpha value representative of the provided `opacity` value.
535    ///
536    /// The `opacity` value may not be null.
537    pub fn get_alpha_from_opacity(opacity: f64) -> i32 {
538        (clamp_double(opacity, 0.0, 1.0) * 255.0).round() as i32
539    }
540}
541
542impl Debug for Color {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        write!(
545            f,
546            "Color(alpha: {:.4}, red: {:.4}, green: {:.4}, blue: {:.4}, colorSpace: {:?})",
547            self.a, self.r, self.g, self.b, self.color_space
548        )
549    }
550}
551
552/// The color space describes the colors that are available to an `Image`.
553///
554/// This value can help decide which `ImageByteFormat` to use with
555/// `Image.toByteData`. Images that are in the [`ColorSpace::ExtendedSrgb`]
556/// color space should use something like `ImageByteFormat.rawExtendedRgba128`
557/// so that colors outside of the sRGB gamut aren't lost.
558///
559/// This is also the result of `Image.colorSpace`.
560///
561/// See also: <https://en.wikipedia.org/wiki/Color_space>
562#[derive(Clone, Copy, PartialEq, Eq)]
563pub enum ColorSpace {
564    /// The sRGB color space.
565    ///
566    /// You may know this as the standard color space for the web or the color
567    /// space of non-wide-gamut Flutter apps.
568    ///
569    /// See also: <https://en.wikipedia.org/wiki/SRGB>
570    Srgb,
571
572    /// A color space that is backwards compatible with sRGB but can represent
573    /// colors outside of that gamut with values outside of \[0..1\]. In order to
574    /// see the extended values an `ImageByteFormat` like
575    /// `ImageByteFormat.rawExtendedRgba128` must be used.
576    ExtendedSrgb,
577
578    /// The Display P3 color space.
579    ///
580    /// This is a wide gamut color space that has broad hardware support. It's
581    /// supported in cases like using Impeller on iOS. When used on a platform
582    /// that doesn't support Display P3, the colors will be clamped to sRGB.
583    ///
584    /// See also: <https://en.wikipedia.org/wiki/DCI-P3>
585    DisplayP3,
586}
587
588impl Debug for ColorSpace {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        match self {
591            ColorSpace::Srgb => write!(f, "ColorSpace.sRGB"),
592            ColorSpace::ExtendedSrgb => write!(f, "ColorSpace.extendedSRGB"),
593            ColorSpace::DisplayP3 => write!(f, "ColorSpace.displayP3"),
594        }
595    }
596}
597
598enum ColorTransform {
599    Identity,
600    Clamp,
601    P3ToSrgb,
602    SrgbToP3,
603}
604
605impl ColorTransform {
606    fn transform(self, color: Color, result_color_space: ColorSpace) -> Color {
607        match self {
608            ColorTransform::Identity => color,
609            ColorTransform::Clamp => Color::from(
610                clamp_double(color.a, 0.0, 1.0),
611                clamp_double(color.r, 0.0, 1.0),
612                clamp_double(color.g, 0.0, 1.0),
613                clamp_double(color.b, 0.0, 1.0),
614                result_color_space,
615            ),
616            ColorTransform::P3ToSrgb => {
617                let r_lin = srgb_eotf_extended(color.r);
618                let g_lin = srgb_eotf_extended(color.g);
619                let b_lin = srgb_eotf_extended(color.b);
620
621                let r_out = K_P3_TO_SRGB_LINEAR[0] * r_lin
622                    + K_P3_TO_SRGB_LINEAR[1] * g_lin
623                    + K_P3_TO_SRGB_LINEAR[2] * b_lin;
624                let g_out = K_P3_TO_SRGB_LINEAR[3] * r_lin
625                    + K_P3_TO_SRGB_LINEAR[4] * g_lin
626                    + K_P3_TO_SRGB_LINEAR[5] * b_lin;
627                let b_out = K_P3_TO_SRGB_LINEAR[6] * r_lin
628                    + K_P3_TO_SRGB_LINEAR[7] * g_lin
629                    + K_P3_TO_SRGB_LINEAR[8] * b_lin;
630
631                Color::from(
632                    color.a,
633                    srgb_oetf_extended(r_out),
634                    srgb_oetf_extended(g_out),
635                    srgb_oetf_extended(b_out),
636                    result_color_space,
637                )
638            }
639            ColorTransform::SrgbToP3 => {
640                let r_lin = srgb_eotf_extended(color.r);
641                let g_lin = srgb_eotf_extended(color.g);
642                let b_lin = srgb_eotf_extended(color.b);
643
644                let r_out = K_SRGB_TO_P3_LINEAR[0] * r_lin
645                    + K_SRGB_TO_P3_LINEAR[1] * g_lin
646                    + K_SRGB_TO_P3_LINEAR[2] * b_lin;
647                let g_out = K_SRGB_TO_P3_LINEAR[3] * r_lin
648                    + K_SRGB_TO_P3_LINEAR[4] * g_lin
649                    + K_SRGB_TO_P3_LINEAR[5] * b_lin;
650                let b_out = K_SRGB_TO_P3_LINEAR[6] * r_lin
651                    + K_SRGB_TO_P3_LINEAR[7] * g_lin
652                    + K_SRGB_TO_P3_LINEAR[8] * b_lin;
653
654                Color::from(
655                    color.a,
656                    srgb_oetf_extended(r_out),
657                    srgb_oetf_extended(g_out),
658                    srgb_oetf_extended(b_out),
659                    result_color_space,
660                )
661            }
662        }
663    }
664}
665
666// sRGB standard constants for transfer functions.
667// See https://en.wikipedia.org/wiki/SRGB.
668const K_SRGB_GAMMA: f64 = 2.4;
669const K_SRGB_LINEAR_THRESHOLD: f64 = 0.04045;
670const K_SRGB_LINEAR_SLOPE: f64 = 12.92;
671const K_SRGB_ENCODED_OFFSET: f64 = 0.055;
672const K_SRGB_ENCODED_DIVISOR: f64 = 1.055;
673const K_SRGB_LINEAR_TO_ENCODED_THRESHOLD: f64 = 0.0031308;
674
675/// sRGB electro-optical transfer function (gamma decode to linear).
676fn srgb_eotf(v: f64) -> f64 {
677    if v <= K_SRGB_LINEAR_THRESHOLD {
678        v / K_SRGB_LINEAR_SLOPE
679    } else {
680        ((v + K_SRGB_ENCODED_OFFSET) / K_SRGB_ENCODED_DIVISOR).powf(K_SRGB_GAMMA)
681    }
682}
683
684/// sRGB opto-electronic transfer function (linear to gamma encode).
685fn srgb_oetf(v: f64) -> f64 {
686    if v <= K_SRGB_LINEAR_TO_ENCODED_THRESHOLD {
687        v * K_SRGB_LINEAR_SLOPE
688    } else {
689        K_SRGB_ENCODED_DIVISOR * v.powf(1.0 / K_SRGB_GAMMA) - K_SRGB_ENCODED_OFFSET
690    }
691}
692
693/// Extended versions that handle negative values by mirroring.
694fn srgb_eotf_extended(v: f64) -> f64 {
695    if v < 0.0 {
696        -srgb_eotf(-v)
697    } else {
698        srgb_eotf(v)
699    }
700}
701
702fn srgb_oetf_extended(v: f64) -> f64 {
703    if v < 0.0 {
704        -srgb_oetf(-v)
705    } else {
706        srgb_oetf(v)
707    }
708}
709
710/// Display P3 to sRGB 3x3 matrix in linear space.
711/// M = sRGB_XYZ_to_RGB * P3_RGB_to_XYZ
712const K_P3_TO_SRGB_LINEAR: [f64; 9] = [
713    1.2249401, -0.2249402, 0.0, -0.0420569, 1.0420571, 0.0, -0.0196376, -0.0786507, 1.0982884,
714];
715
716/// sRGB to Display P3 3x3 matrix in linear space (inverse of [`K_P3_TO_SRGB_LINEAR`]).
717const K_SRGB_TO_P3_LINEAR: [f64; 9] = [
718    0.8224622, 0.1775380, 0.0, 0.0331942, 0.9668058, 0.0, 0.0170806, 0.0723974, 0.9105220,
719];
720
721fn get_color_transform(source: ColorSpace, destination: ColorSpace) -> ColorTransform {
722    match source {
723        ColorSpace::Srgb => match destination {
724            ColorSpace::Srgb => ColorTransform::Identity,
725            ColorSpace::ExtendedSrgb => ColorTransform::Identity,
726            ColorSpace::DisplayP3 => ColorTransform::SrgbToP3,
727        },
728        ColorSpace::ExtendedSrgb => match destination {
729            ColorSpace::Srgb => ColorTransform::Clamp,
730            ColorSpace::ExtendedSrgb => ColorTransform::Identity,
731            ColorSpace::DisplayP3 => ColorTransform::Clamp,
732        },
733        ColorSpace::DisplayP3 => match destination {
734            ColorSpace::Srgb => ColorTransform::Clamp,
735            ColorSpace::ExtendedSrgb => ColorTransform::P3ToSrgb,
736            ColorSpace::DisplayP3 => ColorTransform::Identity,
737        },
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    fn color_matches(actual: Color, expected: Color, threshold: f64) -> bool {
746        actual.color_space == expected.color_space
747            && (actual.a - expected.a).abs() <= threshold
748            && (actual.r - expected.r).abs() <= threshold
749            && (actual.g - expected.g).abs() <= threshold
750            && (actual.b - expected.b).abs() <= threshold
751    }
752
753    // engine color_test.dart plus the requested Color(0xFF42A5F5) pin.
754    #[test]
755    fn packed_constructor_from_argb_and_from_agree() {
756        let packed = Color::new(0xFF42A5F5);
757        let from_argb = Color::from_argb(0xFF, 0x42, 0xA5, 0xF5);
758        let from = Color::from(
759            1.0,
760            0x42 as f64 / 255.0,
761            0xA5 as f64 / 255.0,
762            0xF5 as f64 / 255.0,
763            ColorSpace::Srgb,
764        );
765        let from_rgbo = Color::from_rgbo(0x42, 0xA5, 0xF5, 1.0);
766
767        assert_eq!(packed, from_argb);
768        assert_eq!(packed, from);
769        assert_eq!(packed, from_rgbo);
770        assert_eq!(packed.to_argb32(), 0xFF42A5F5);
771        assert_eq!(packed.a, 1.0);
772        assert_eq!(packed.color_space, ColorSpace::Srgb);
773    }
774
775    #[test]
776    #[allow(deprecated)]
777    fn color_accessors_should_work() {
778        let foo = Color::new(0x12345678);
779        assert_eq!(foo.alpha(), 0x12);
780        assert_eq!(foo.red(), 0x34);
781        assert_eq!(foo.green(), 0x56);
782        assert_eq!(foo.blue(), 0x78);
783    }
784
785    #[test]
786    fn two_colors_are_equal_when_components_and_space_match() {
787        assert_eq!(Color::new(0x12345678), Color::new(0x12345678));
788        assert_ne!(Color::new(0x12345678), Color::new(0x87654321));
789        let srgb = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::Srgb);
790        let p3 = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::DisplayP3);
791        assert_ne!(srgb, p3);
792    }
793
794    #[test]
795    fn lerp_walks_the_line_and_clamps_extrapolation() {
796        assert_eq!(
797            Color::lerp(
798                Some(Color::new(0x00000000)),
799                Some(Color::new(0xFFFFFFFF)),
800                0.0
801            ),
802            Some(Color::new(0x00000000))
803        );
804        assert!(color_matches(
805            Color::lerp(
806                Some(Color::new(0x00000000)),
807                Some(Color::new(0xFFFFFFFF)),
808                0.5
809            )
810            .unwrap(),
811            Color::new(0x7F7F7F7F),
812            1.0 / 255.0
813        ));
814        assert_eq!(
815            Color::lerp(
816                Some(Color::new(0x00000000)),
817                Some(Color::new(0xFFFFFFFF)),
818                1.0
819            ),
820            Some(Color::new(0xFFFFFFFF))
821        );
822        assert_eq!(
823            Color::lerp(
824                Some(Color::new(0x00000000)),
825                Some(Color::new(0xFFFFFFFF)),
826                -0.1
827            ),
828            Some(Color::new(0x00000000))
829        );
830        assert_eq!(
831            Color::lerp(
832                Some(Color::new(0x00000000)),
833                Some(Color::new(0xFFFFFFFF)),
834                1.1
835            ),
836            Some(Color::new(0xFFFFFFFF))
837        );
838
839        // Prevent regression: https://github.com/flutter/flutter/issues/67423
840        assert_eq!(
841            Color::lerp(
842                Some(Color::new(0xFFFFFFFF)),
843                Some(Color::new(0xFFFFFFFF)),
844                0.04
845            ),
846            Some(Color::new(0xFFFFFFFF))
847        );
848
849        assert_eq!(Color::lerp(None, None, 0.5), None);
850        assert_eq!(
851            Color::lerp(Some(Color::new(0xFFFFFFFF)), None, 0.0),
852            Some(Color::new(0xFFFFFFFF))
853        );
854    }
855
856    #[test]
857    fn lerp_same_color_spaces() {
858        assert_eq!(
859            Color::lerp(
860                Some(Color::from(1.0, 0.0, 0.0, 0.0, ColorSpace::DisplayP3)),
861                Some(Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::DisplayP3)),
862                0.2,
863            ),
864            Some(Color::from(1.0, 0.2, 0.0, 0.0, ColorSpace::DisplayP3))
865        );
866    }
867
868    #[test]
869    fn lerp_mixed_color_spaces_produces_display_p3() {
870        let result = Color::lerp(
871            Some(Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::DisplayP3)),
872            Some(Color::from(1.0, 0.0, 0.0, 1.0, ColorSpace::Srgb)),
873            0.0,
874        )
875        .unwrap();
876        assert_eq!(result.color_space, ColorSpace::DisplayP3);
877    }
878
879    #[test]
880    fn lerp_mixed_color_spaces_srgb_x_and_display_p3_y_at_t_0() {
881        let result = Color::lerp(
882            Some(Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::Srgb)),
883            Some(Color::from(1.0, 0.0, 1.0, 0.0, ColorSpace::DisplayP3)),
884            0.0,
885        )
886        .unwrap();
887        assert_eq!(result.color_space, ColorSpace::DisplayP3);
888        let expected_p3 = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::Srgb).with_values(
889            None,
890            None,
891            None,
892            None,
893            Some(ColorSpace::DisplayP3),
894        );
895        assert!(color_matches(result, expected_p3, 1.0 / 255.0));
896    }
897
898    #[test]
899    fn lerp_mixed_color_spaces_at_midpoint() {
900        let srgb_red = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::Srgb);
901        let p3_green = Color::from(1.0, 0.0, 1.0, 0.0, ColorSpace::DisplayP3);
902        let result = Color::lerp(Some(srgb_red), Some(p3_green), 0.5).unwrap();
903        assert_eq!(result.color_space, ColorSpace::DisplayP3);
904        let srgb_red_as_p3 =
905            srgb_red.with_values(None, None, None, None, Some(ColorSpace::DisplayP3));
906        let expected = Color::from(
907            (srgb_red_as_p3.a + p3_green.a) / 2.0,
908            (srgb_red_as_p3.r + p3_green.r) / 2.0,
909            (srgb_red_as_p3.g + p3_green.g) / 2.0,
910            (srgb_red_as_p3.b + p3_green.b) / 2.0,
911            ColorSpace::DisplayP3,
912        );
913        assert!(color_matches(result, expected, 1e-4));
914    }
915
916    #[test]
917    fn alpha_blend_matches_src_over_dst() {
918        assert_eq!(
919            Color::alpha_blend(Color::new(0x00000000), Color::new(0x00000000)),
920            Color::new(0x00000000)
921        );
922        assert_eq!(
923            Color::alpha_blend(Color::new(0x00000000), Color::new(0xFFFFFFFF)),
924            Color::new(0xFFFFFFFF)
925        );
926        assert_eq!(
927            Color::alpha_blend(Color::new(0xFFFFFFFF), Color::new(0x00000000)),
928            Color::new(0xFFFFFFFF)
929        );
930        assert_eq!(
931            Color::alpha_blend(Color::new(0xFFFFFFFF), Color::new(0xFFFFFFFF)),
932            Color::new(0xFFFFFFFF)
933        );
934        assert_eq!(
935            Color::alpha_blend(Color::new(0x80FFFFFF), Color::new(0xFF000000)),
936            Color::new(0xFF808080)
937        );
938        assert!(color_matches(
939            Color::alpha_blend(Color::new(0x80808080), Color::new(0xFFFFFFFF)),
940            Color::new(0xFFBFBFBF),
941            1.0 / 255.0
942        ));
943        assert!(color_matches(
944            Color::alpha_blend(Color::new(0x80808080), Color::new(0xFF000000)),
945            Color::new(0xFF404040),
946            1.0 / 255.0
947        ));
948        assert_eq!(
949            Color::alpha_blend(
950                Color::from(0.5, 1.0, 1.0, 1.0, ColorSpace::DisplayP3),
951                Color::from(1.0, 0.0, 0.0, 0.0, ColorSpace::DisplayP3),
952            ),
953            Color::from(1.0, 0.5, 0.5, 0.5, ColorSpace::DisplayP3)
954        );
955    }
956
957    #[test]
958    fn compute_gray_luminance() {
959        // Each color component is at 20%.
960        let light_gray = Color::new(0xFF333333);
961        // Relative luminance's formula is just the linearized color value for gray.
962        // ((0.2 + 0.055) / 1.055) ^ 2.4.
963        assert_eq!(light_gray.compute_luminance(), 0.033104766570885055);
964    }
965
966    #[test]
967    fn compute_color_luminance() {
968        let bright_red = Color::new(0xFFFF3B30);
969        // 0.2126 * ((1.0 + 0.055) / 1.055) ^ 2.4 +
970        // 0.7152 * ((0.23137254902 +0.055) / 1.055) ^ 2.4 +
971        // 0.0722 * ((0.18823529411 + 0.055) / 1.055) ^ 2.4
972        assert_eq!(bright_red.compute_luminance(), 0.24601329637099723);
973    }
974
975    #[test]
976    #[allow(deprecated)]
977    fn from_and_accessors() {
978        let color = Color::from(0.1, 0.2, 0.3, 0.4, ColorSpace::Srgb);
979        assert_eq!(color.a, 0.1);
980        assert_eq!(color.r, 0.2);
981        assert_eq!(color.g, 0.3);
982        assert_eq!(color.b, 0.4);
983        assert_eq!(color.color_space, ColorSpace::Srgb);
984
985        assert_eq!(color.alpha(), 26);
986        assert_eq!(color.red(), 51);
987        assert_eq!(color.green(), 77);
988        assert_eq!(color.blue(), 102);
989
990        assert_eq!(color.value(), 0x1a334d66);
991        assert_eq!(color.to_argb32(), 0x1a334d66);
992    }
993
994    #[test]
995    #[allow(deprecated)]
996    fn from_argb_and_accessors() {
997        let color = Color::from_argb(10, 20, 35, 47);
998        assert_eq!(color.alpha(), 10);
999        assert_eq!(color.red(), 20);
1000        assert_eq!(color.green(), 35);
1001        assert_eq!(color.blue(), 47);
1002    }
1003
1004    #[test]
1005    #[allow(deprecated)]
1006    fn constructor_and_accessors() {
1007        let color = Color::new(0xffeeddcc);
1008        assert_eq!(color.alpha(), 0xff);
1009        assert_eq!(color.red(), 0xee);
1010        assert_eq!(color.green(), 0xdd);
1011        assert_eq!(color.blue(), 0xcc);
1012    }
1013
1014    #[test]
1015    fn p3_to_extended_srgb() {
1016        let p3 = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::DisplayP3);
1017        let srgb = p3.with_values(None, None, None, None, Some(ColorSpace::ExtendedSrgb));
1018        assert_eq!(srgb.a, 1.0);
1019        assert!((srgb.r - 1.0931).abs() < 1e-4);
1020        assert!((srgb.g - -0.22684034705162098).abs() < 1e-4);
1021        assert!((srgb.b - -0.15007957816123998).abs() < 1e-4);
1022        assert_eq!(srgb.color_space, ColorSpace::ExtendedSrgb);
1023    }
1024
1025    #[test]
1026    fn p3_to_srgb() {
1027        let p3 = Color::from(1.0, 1.0, 0.0, 0.0, ColorSpace::DisplayP3);
1028        let srgb = p3.with_values(None, None, None, None, Some(ColorSpace::Srgb));
1029        assert_eq!(srgb.a, 1.0);
1030        assert!((srgb.r - 1.0).abs() < 1e-4);
1031        assert!((srgb.g - 0.0).abs() < 1e-4);
1032        assert!((srgb.b - 0.0).abs() < 1e-4);
1033        assert_eq!(srgb.color_space, ColorSpace::Srgb);
1034    }
1035
1036    #[test]
1037    fn extended_srgb_to_p3() {
1038        let srgb = Color::from(1.0, 1.0931, -0.2268, -0.1501, ColorSpace::ExtendedSrgb);
1039        let p3 = srgb.with_values(None, None, None, None, Some(ColorSpace::DisplayP3));
1040        assert_eq!(p3.a, 1.0);
1041        assert!((p3.r - 1.0).abs() < 1e-4);
1042        assert!((p3.g - 0.0).abs() < 1e-4);
1043        assert!((p3.b - 0.0).abs() < 1e-4);
1044        assert_eq!(p3.color_space, ColorSpace::DisplayP3);
1045    }
1046
1047    #[test]
1048    fn extended_srgb_to_p3_clamped() {
1049        let srgb = Color::from(1.0, 2.0, 0.0, 0.0, ColorSpace::ExtendedSrgb);
1050        let p3 = srgb.with_values(None, None, None, None, Some(ColorSpace::DisplayP3));
1051        assert_eq!(srgb.a, 1.0);
1052        assert!(p3.r <= 1.0);
1053        assert!(p3.g <= 1.0);
1054        assert!(p3.b <= 1.0);
1055        assert!(p3.r >= 0.0);
1056        assert!(p3.g >= 0.0);
1057        assert!(p3.b >= 0.0);
1058    }
1059
1060    #[test]
1061    fn debug_matches_darts_to_string() {
1062        let color = Color::new(0xFF42A5F5);
1063        assert_eq!(
1064            format!("{color:?}"),
1065            "Color(alpha: 1.0000, red: 0.2588, green: 0.6471, blue: 0.9608, colorSpace: ColorSpace.sRGB)"
1066        );
1067    }
1068}