style/color/
mix.rs

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
7use super::{AbsoluteColor, ColorFlags, ColorSpace};
8use crate::derives::*;
9use crate::parser::{Parse, ParserContext};
10use crate::values::generics::color::ColorMixFlags;
11use cssparser::Parser;
12use std::fmt::{self, Write};
13use 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)]
33pub 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)]
60pub 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
67impl 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
104impl Default for ColorInterpolationMethod {
105    fn default() -> Self {
106        Self::oklab()
107    }
108}
109
110impl 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
135impl 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.
152pub 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)]
205enum 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
216impl 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
234impl 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
307fn 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
363fn 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]
376fn normalize_hue(v: f32) -> f32 {
377    v - 360. * (v / 360.).floor()
378}
379
380fn 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
442fn 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
453struct 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
464fn 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
510fn 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}