tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mix.rs (18692B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! Color mixing/interpolation.
      6 
      7 use super::{AbsoluteColor, ColorFlags, ColorSpace};
      8 use crate::derives::*;
      9 use crate::parser::{Parse, ParserContext};
     10 use crate::values::generics::color::ColorMixFlags;
     11 use cssparser::Parser;
     12 use std::fmt::{self, Write};
     13 use style_traits::{CssWriter, ParseError, ToCss};
     14 
     15 /// A hue-interpolation-method as defined in [1].
     16 ///
     17 /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method
     18 #[derive(
     19    Clone,
     20    Copy,
     21    Debug,
     22    Eq,
     23    MallocSizeOf,
     24    Parse,
     25    PartialEq,
     26    ToAnimatedValue,
     27    ToComputedValue,
     28    ToCss,
     29    ToResolvedValue,
     30    ToShmem,
     31 )]
     32 #[repr(u8)]
     33 pub enum HueInterpolationMethod {
     34    /// https://drafts.csswg.org/css-color-4/#shorter
     35    Shorter,
     36    /// https://drafts.csswg.org/css-color-4/#longer
     37    Longer,
     38    /// https://drafts.csswg.org/css-color-4/#increasing
     39    Increasing,
     40    /// https://drafts.csswg.org/css-color-4/#decreasing
     41    Decreasing,
     42    /// https://drafts.csswg.org/css-color-4/#specified
     43    Specified,
     44 }
     45 
     46 /// https://drafts.csswg.org/css-color-4/#color-interpolation-method
     47 #[derive(
     48    Clone,
     49    Copy,
     50    Debug,
     51    Eq,
     52    MallocSizeOf,
     53    PartialEq,
     54    ToShmem,
     55    ToAnimatedValue,
     56    ToComputedValue,
     57    ToResolvedValue,
     58 )]
     59 #[repr(C)]
     60 pub struct ColorInterpolationMethod {
     61    /// The color-space the interpolation should be done in.
     62    pub space: ColorSpace,
     63    /// The hue interpolation method.
     64    pub hue: HueInterpolationMethod,
     65 }
     66 
     67 impl ColorInterpolationMethod {
     68    /// Returns the srgb interpolation method.
     69    pub const fn srgb() -> Self {
     70        Self {
     71            space: ColorSpace::Srgb,
     72            hue: HueInterpolationMethod::Shorter,
     73        }
     74    }
     75 
     76    /// Return the oklab interpolation method used for default color
     77    /// interpolcation.
     78    pub const fn oklab() -> Self {
     79        Self {
     80            space: ColorSpace::Oklab,
     81            hue: HueInterpolationMethod::Shorter,
     82        }
     83    }
     84 
     85    /// Return true if the this is the default method.
     86    pub fn is_default(&self) -> bool {
     87        self.space == ColorSpace::Oklab
     88    }
     89 
     90    /// Decides the best method for interpolating between the given colors.
     91    /// https://drafts.csswg.org/css-color-4/#interpolation-space
     92    pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
     93        // The default color space to use for interpolation is Oklab. However,
     94        // if either of the colors are in legacy rgb(), hsl() or hwb(), then
     95        // interpolation is done in sRGB.
     96        if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
     97            Self::default()
     98        } else {
     99            Self::srgb()
    100        }
    101    }
    102 }
    103 
    104 impl Default for ColorInterpolationMethod {
    105    fn default() -> Self {
    106        Self::oklab()
    107    }
    108 }
    109 
    110 impl Parse for ColorInterpolationMethod {
    111    fn parse<'i, 't>(
    112        _: &ParserContext,
    113        input: &mut Parser<'i, 't>,
    114    ) -> Result<Self, ParseError<'i>> {
    115        input.expect_ident_matching("in")?;
    116        let space = ColorSpace::parse(input)?;
    117        // https://drafts.csswg.org/css-color-4/#hue-interpolation
    118        //     Unless otherwise specified, if no specific hue interpolation
    119        //     algorithm is selected by the host syntax, the default is shorter.
    120        let hue = if space.is_polar() {
    121            input
    122                .try_parse(|input| -> Result<_, ParseError<'i>> {
    123                    let hue = HueInterpolationMethod::parse(input)?;
    124                    input.expect_ident_matching("hue")?;
    125                    Ok(hue)
    126                })
    127                .unwrap_or(HueInterpolationMethod::Shorter)
    128        } else {
    129            HueInterpolationMethod::Shorter
    130        };
    131        Ok(Self { space, hue })
    132    }
    133 }
    134 
    135 impl ToCss for ColorInterpolationMethod {
    136    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    137    where
    138        W: Write,
    139    {
    140        dest.write_str("in ")?;
    141        self.space.to_css(dest)?;
    142        if self.hue != HueInterpolationMethod::Shorter {
    143            dest.write_char(' ')?;
    144            self.hue.to_css(dest)?;
    145            dest.write_str(" hue")?;
    146        }
    147        Ok(())
    148    }
    149 }
    150 
    151 /// Mix two colors into one.
    152 pub fn mix(
    153    interpolation: ColorInterpolationMethod,
    154    left_color: &AbsoluteColor,
    155    mut left_weight: f32,
    156    right_color: &AbsoluteColor,
    157    mut right_weight: f32,
    158    flags: ColorMixFlags,
    159 ) -> AbsoluteColor {
    160    // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
    161    let mut alpha_multiplier = 1.0;
    162    if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) {
    163        let sum = left_weight + right_weight;
    164        if sum != 1.0 {
    165            let scale = 1.0 / sum;
    166            left_weight *= scale;
    167            right_weight *= scale;
    168            if sum < 1.0 {
    169                alpha_multiplier = sum;
    170            }
    171        }
    172    }
    173 
    174    let result = mix_in(
    175        interpolation.space,
    176        left_color,
    177        left_weight,
    178        right_color,
    179        right_weight,
    180        interpolation.hue,
    181        alpha_multiplier,
    182    );
    183 
    184    if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
    185        // If the result *MUST* be in modern syntax, then make sure it is in a
    186        // color space that allows the modern syntax. So hsl and hwb will be
    187        // converted to srgb.
    188        if result.is_legacy_syntax() {
    189            result.to_color_space(ColorSpace::Srgb)
    190        } else {
    191            result
    192        }
    193    } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
    194        // If both sides of the mix is legacy then convert the result back into
    195        // legacy.
    196        result.into_srgb_legacy()
    197    } else {
    198        result
    199    }
    200 }
    201 
    202 /// What the outcome of each component should be in a mix result.
    203 #[derive(Clone, Copy, PartialEq)]
    204 #[repr(u8)]
    205 enum ComponentMixOutcome {
    206    /// Mix the left and right sides to give the result.
    207    Mix,
    208    /// Carry the left side forward to the result.
    209    UseLeft,
    210    /// Carry the right side forward to the result.
    211    UseRight,
    212    /// The resulting component should also be none.
    213    None,
    214 }
    215 
    216 impl ComponentMixOutcome {
    217    fn from_colors(
    218        left: &AbsoluteColor,
    219        right: &AbsoluteColor,
    220        flags_to_check: ColorFlags,
    221    ) -> Self {
    222        match (
    223            left.flags.contains(flags_to_check),
    224            right.flags.contains(flags_to_check),
    225        ) {
    226            (true, true) => Self::None,
    227            (true, false) => Self::UseRight,
    228            (false, true) => Self::UseLeft,
    229            (false, false) => Self::Mix,
    230        }
    231    }
    232 }
    233 
    234 impl AbsoluteColor {
    235    /// Calculate the flags that should be carried forward a color before converting
    236    /// it to the interpolation color space according to:
    237    /// <https://drafts.csswg.org/css-color-4/#interpolation-missing>
    238    fn carry_forward_analogous_missing_components(&mut self, source: &AbsoluteColor) {
    239        use ColorFlags as F;
    240        use ColorSpace as S;
    241 
    242        if source.color_space == self.color_space {
    243            return;
    244        }
    245 
    246        // Reds             r, x
    247        // Greens           g, y
    248        // Blues            b, z
    249        if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() {
    250            return;
    251        }
    252 
    253        // Lightness        L
    254        if matches!(source.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
    255            if matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
    256                self.flags |= source.flags & F::C0_IS_NONE;
    257            } else if matches!(self.color_space, S::Hsl) {
    258                if source.flags.contains(F::C0_IS_NONE) {
    259                    self.flags.insert(F::C2_IS_NONE)
    260                }
    261            }
    262        } else if matches!(source.color_space, S::Hsl)
    263            && matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch)
    264        {
    265            if source.flags.contains(F::C2_IS_NONE) {
    266                self.flags.insert(F::C0_IS_NONE)
    267            }
    268        }
    269 
    270        // Colorfulness     C, S
    271        if matches!(source.color_space, S::Hsl | S::Lch | S::Oklch)
    272            && matches!(self.color_space, S::Hsl | S::Lch | S::Oklch)
    273        {
    274            self.flags |= source.flags & F::C1_IS_NONE;
    275        }
    276 
    277        // Hue              H
    278        if matches!(source.color_space, S::Hsl | S::Hwb) {
    279            if matches!(self.color_space, S::Hsl | S::Hwb) {
    280                self.flags |= source.flags & F::C0_IS_NONE;
    281            } else if matches!(self.color_space, S::Lch | S::Oklch) {
    282                if source.flags.contains(F::C0_IS_NONE) {
    283                    self.flags.insert(F::C2_IS_NONE)
    284                }
    285            }
    286        } else if matches!(source.color_space, S::Lch | S::Oklch) {
    287            if matches!(self.color_space, S::Hsl | S::Hwb) {
    288                if source.flags.contains(F::C2_IS_NONE) {
    289                    self.flags.insert(F::C0_IS_NONE)
    290                }
    291            } else if matches!(self.color_space, S::Lch | S::Oklch) {
    292                self.flags |= source.flags & F::C2_IS_NONE;
    293            }
    294        }
    295 
    296        // Opponent         a, a
    297        // Opponent         b, b
    298        if matches!(source.color_space, S::Lab | S::Oklab)
    299            && matches!(self.color_space, S::Lab | S::Oklab)
    300        {
    301            self.flags |= source.flags & F::C1_IS_NONE;
    302            self.flags |= source.flags & F::C2_IS_NONE;
    303        }
    304    }
    305 }
    306 
    307 fn mix_in(
    308    color_space: ColorSpace,
    309    left_color: &AbsoluteColor,
    310    left_weight: f32,
    311    right_color: &AbsoluteColor,
    312    right_weight: f32,
    313    hue_interpolation: HueInterpolationMethod,
    314    alpha_multiplier: f32,
    315 ) -> AbsoluteColor {
    316    // Convert both colors into the interpolation color space.
    317    let mut left = left_color.to_color_space(color_space);
    318    left.carry_forward_analogous_missing_components(&left_color);
    319    let mut right = right_color.to_color_space(color_space);
    320    right.carry_forward_analogous_missing_components(&right_color);
    321 
    322    let outcomes = [
    323        ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
    324        ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
    325        ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
    326        ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
    327    ];
    328 
    329    // Convert both sides into just components.
    330    let left = left.raw_components();
    331    let right = right.raw_components();
    332 
    333    let (result, result_flags) = interpolate_premultiplied(
    334        &left,
    335        left_weight,
    336        &right,
    337        right_weight,
    338        color_space.hue_index(),
    339        hue_interpolation,
    340        &outcomes,
    341    );
    342 
    343    let alpha = if alpha_multiplier != 1.0 {
    344        result[3] * alpha_multiplier
    345    } else {
    346        result[3]
    347    };
    348 
    349    // FIXME: In rare cases we end up with 0.999995 in the alpha channel,
    350    //        so we reduce the precision to avoid serializing to
    351    //        rgba(?, ?, ?, 1).  This is not ideal, so we should look into
    352    //        ways to avoid it. Maybe pre-multiply all color components and
    353    //        then divide after calculations?
    354    let alpha = (alpha * 1000.0).round() / 1000.0;
    355 
    356    let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha);
    357 
    358    result.flags = result_flags;
    359 
    360    result
    361 }
    362 
    363 fn interpolate_premultiplied_component(
    364    left: f32,
    365    left_weight: f32,
    366    left_alpha: f32,
    367    right: f32,
    368    right_weight: f32,
    369    right_alpha: f32,
    370 ) -> f32 {
    371    left * left_weight * left_alpha + right * right_weight * right_alpha
    372 }
    373 
    374 // Normalize hue into [0, 360)
    375 #[inline]
    376 fn normalize_hue(v: f32) -> f32 {
    377    v - 360. * (v / 360.).floor()
    378 }
    379 
    380 fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
    381    // Adjust the hue angle as per
    382    // https://drafts.csswg.org/css-color/#hue-interpolation.
    383    //
    384    // If both hue angles are NAN, they should be set to 0. Otherwise, if a
    385    // single hue angle is NAN, it should use the other hue angle.
    386    if left.is_nan() {
    387        if right.is_nan() {
    388            *left = 0.;
    389            *right = 0.;
    390        } else {
    391            *left = *right;
    392        }
    393    } else if right.is_nan() {
    394        *right = *left;
    395    }
    396 
    397    if hue_interpolation == HueInterpolationMethod::Specified {
    398        // Angles are not adjusted. They are interpolated like any other
    399        // component.
    400        return;
    401    }
    402 
    403    *left = normalize_hue(*left);
    404    *right = normalize_hue(*right);
    405 
    406    match hue_interpolation {
    407        // https://drafts.csswg.org/css-color/#shorter
    408        HueInterpolationMethod::Shorter => {
    409            let delta = *right - *left;
    410 
    411            if delta > 180. {
    412                *left += 360.;
    413            } else if delta < -180. {
    414                *right += 360.;
    415            }
    416        },
    417        // https://drafts.csswg.org/css-color/#longer
    418        HueInterpolationMethod::Longer => {
    419            let delta = *right - *left;
    420            if 0. < delta && delta < 180. {
    421                *left += 360.;
    422            } else if -180. < delta && delta <= 0. {
    423                *right += 360.;
    424            }
    425        },
    426        // https://drafts.csswg.org/css-color/#increasing
    427        HueInterpolationMethod::Increasing => {
    428            if *right < *left {
    429                *right += 360.;
    430            }
    431        },
    432        // https://drafts.csswg.org/css-color/#decreasing
    433        HueInterpolationMethod::Decreasing => {
    434            if *left < *right {
    435                *left += 360.;
    436            }
    437        },
    438        HueInterpolationMethod::Specified => unreachable!("Handled above"),
    439    }
    440 }
    441 
    442 fn interpolate_hue(
    443    mut left: f32,
    444    left_weight: f32,
    445    mut right: f32,
    446    right_weight: f32,
    447    hue_interpolation: HueInterpolationMethod,
    448 ) -> f32 {
    449    adjust_hue(&mut left, &mut right, hue_interpolation);
    450    left * left_weight + right * right_weight
    451 }
    452 
    453 struct InterpolatedAlpha {
    454    /// The adjusted left alpha value.
    455    left: f32,
    456    /// The adjusted right alpha value.
    457    right: f32,
    458    /// The interpolated alpha value.
    459    interpolated: f32,
    460    /// Whether the alpha component should be `none`.
    461    is_none: bool,
    462 }
    463 
    464 fn interpolate_alpha(
    465    left: f32,
    466    left_weight: f32,
    467    right: f32,
    468    right_weight: f32,
    469    outcome: ComponentMixOutcome,
    470 ) -> InterpolatedAlpha {
    471    // <https://drafts.csswg.org/css-color-4/#interpolation-missing>
    472    let mut result = match outcome {
    473        ComponentMixOutcome::Mix => {
    474            let interpolated = left * left_weight + right * right_weight;
    475            InterpolatedAlpha {
    476                left,
    477                right,
    478                interpolated,
    479                is_none: false,
    480            }
    481        },
    482        ComponentMixOutcome::UseLeft => InterpolatedAlpha {
    483            left,
    484            right: left,
    485            interpolated: left,
    486            is_none: false,
    487        },
    488        ComponentMixOutcome::UseRight => InterpolatedAlpha {
    489            left: right,
    490            right,
    491            interpolated: right,
    492            is_none: false,
    493        },
    494        ComponentMixOutcome::None => InterpolatedAlpha {
    495            left: 1.0,
    496            right: 1.0,
    497            interpolated: 0.0,
    498            is_none: true,
    499        },
    500    };
    501 
    502    // Clip all alpha values to [0.0..1.0].
    503    result.left = result.left.clamp(0.0, 1.0);
    504    result.right = result.right.clamp(0.0, 1.0);
    505    result.interpolated = result.interpolated.clamp(0.0, 1.0);
    506 
    507    result
    508 }
    509 
    510 fn interpolate_premultiplied(
    511    left: &[f32; 4],
    512    left_weight: f32,
    513    right: &[f32; 4],
    514    right_weight: f32,
    515    hue_index: Option<usize>,
    516    hue_interpolation: HueInterpolationMethod,
    517    outcomes: &[ComponentMixOutcome; 4],
    518 ) -> ([f32; 4], ColorFlags) {
    519    let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
    520    let mut flags = if alpha.is_none {
    521        ColorFlags::ALPHA_IS_NONE
    522    } else {
    523        ColorFlags::empty()
    524    };
    525 
    526    let mut result = [0.; 4];
    527 
    528    for i in 0..3 {
    529        match outcomes[i] {
    530            ComponentMixOutcome::Mix => {
    531                let is_hue = hue_index == Some(i);
    532                result[i] = if is_hue {
    533                    normalize_hue(interpolate_hue(
    534                        left[i],
    535                        left_weight,
    536                        right[i],
    537                        right_weight,
    538                        hue_interpolation,
    539                    ))
    540                } else {
    541                    let interpolated = interpolate_premultiplied_component(
    542                        left[i],
    543                        left_weight,
    544                        alpha.left,
    545                        right[i],
    546                        right_weight,
    547                        alpha.right,
    548                    );
    549 
    550                    if alpha.interpolated == 0.0 {
    551                        interpolated
    552                    } else {
    553                        interpolated / alpha.interpolated
    554                    }
    555                };
    556            },
    557            ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => {
    558                let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft {
    559                    left[i]
    560                } else {
    561                    right[i]
    562                };
    563                result[i] = if hue_interpolation == HueInterpolationMethod::Longer
    564                    && hue_index == Some(i)
    565                {
    566                    // If "longer hue" interpolation is required, we have to actually do
    567                    // the computation even if we're using the same value at both ends,
    568                    // so that interpolating from the starting hue back to the same value
    569                    // produces a full cycle, rather than a constant hue.
    570                    normalize_hue(interpolate_hue(
    571                        used_component,
    572                        left_weight,
    573                        used_component,
    574                        right_weight,
    575                        hue_interpolation,
    576                    ))
    577                } else {
    578                    used_component
    579                };
    580            },
    581            ComponentMixOutcome::None => {
    582                result[i] = 0.0;
    583                match i {
    584                    0 => flags.insert(ColorFlags::C0_IS_NONE),
    585                    1 => flags.insert(ColorFlags::C1_IS_NONE),
    586                    2 => flags.insert(ColorFlags::C2_IS_NONE),
    587                    _ => unreachable!(),
    588                }
    589            },
    590        }
    591    }
    592    result[3] = alpha.interpolated;
    593 
    594    (result, flags)
    595 }