zng_color/
mix.rs

1use super::{Hsla, Hsva, PreMulRgba, Rgba, clamp_normal};
2use zng_layout::unit::Factor;
3
4use pastey::*;
5
6/// Webrender [`MixBlendMode`].
7pub type RenderMixBlendMode = zng_view_api::MixBlendMode;
8
9macro_rules! impl_mix {
10    (
11        separable {$(
12            $(#[$meta:meta])*
13            $Mode:ident => |$fg:ident, $bg:ident, $ca0:ident, $ca1:ident| $mix:expr,
14        )+}
15
16        non_separable {$(
17            $(#[$ns_meta:meta])*
18            $NsMode:ident => |[$fgh:ident, $fgs:ident, $fgl:ident], [$bgh:ident, $bgs:ident, $bgl:ident]| $ns_mix:expr,
19        )+}
20    ) => {
21        /// Color mix blend mode.
22        #[repr(u8)]
23        #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24        pub enum MixBlendMode {
25            $(
26                $(#[$meta])*
27                $Mode,
28            )+
29            $(
30                $(#[$ns_meta])*
31                $NsMode,
32            )+
33        }
34
35        impl From<MixBlendMode> for RenderMixBlendMode {
36            fn from(mode: MixBlendMode) -> Self {
37                match mode {
38                    $(MixBlendMode::$Mode => RenderMixBlendMode::$Mode,)+
39                    $(MixBlendMode::$NsMode => RenderMixBlendMode::$NsMode,)+
40                }
41            }
42        }
43
44        /// Color mix and adjustment methods.
45        pub trait MixAdjust {
46            /// MixAdjust `background` over `self` using the `mode`.
47            fn mix(self, mode: MixBlendMode, background: Self) -> Self where Self:Sized {
48                match mode {
49                    $(MixBlendMode::$Mode => paste!(self.[<mix_ $Mode:lower>](background)),)+
50                    $(MixBlendMode::$NsMode => paste!(self.[<mix_ $NsMode:lower>](background)),)+
51                }
52            }
53
54            $(
55                paste! {
56                    #[doc = "MixAdjust `background` over `self` using the [`MixBlendMode::" $Mode "`]."]
57                    ///
58                    $(#[$meta])*
59                    fn [<mix_ $Mode:lower>](self, background: Self) -> Self;
60                }
61            )+
62
63            $(
64                paste! {
65                    #[doc = "MixAdjust `self` over `background` using the [`MixBlendMode::`" $NsMode "`]."]
66                    ///
67                    $(#[$ns_meta])*
68                    ///
69                    /// This method converts both inputs to [`Hsla`] and the result back to `Rgba`.
70                    fn [<mix_ $NsMode:lower>](self, background: Self) -> Self;
71                }
72            )+
73
74            /// Adds the `amount` to the color *lightness*.
75            ///
76            /// # Examples
77            ///
78            /// Add `10%` of the current lightness to the `DARK_RED` color:
79            ///
80            /// ```
81            /// # use zng_color::*;
82            /// # use zng_layout::unit::*;
83            /// web_colors::DARK_RED.lighten(10.pct())
84            /// # ;
85            /// ```
86            fn lighten<A: Into<Factor>>(self, amount: A) -> Self;
87
88            /// Subtracts the `amount` from the color *lightness*.
89            ///
90            /// # Examples
91            ///
92            /// Removes `10%` of the current lightness from the `DARK_RED` color:
93            ///
94            /// ```
95            /// # use zng_color::*;
96            /// # use zng_layout::unit::*;
97            /// web_colors::DARK_RED.darken(10.pct())
98            /// # ;
99            fn darken<A: Into<Factor>>(self, amount: A) -> Self;
100
101            /// Subtracts the `amount` from the color *saturation*.
102            ///
103            /// # Examples
104            ///
105            /// Removes `10%` of the current saturation from the `RED` color:
106            ///
107            /// ```
108            /// # use zng_color::*;
109            /// # use zng_layout::unit::*;
110            /// colors::RED.desaturate(10.pct())
111            /// # ;
112            fn desaturate<A: Into<Factor>>(self, amount: A) -> Self;
113
114            /// Returns a copy of this color with a new `lightness`.
115            fn with_lightness<L: Into<Factor>>(self, lightness: L) -> Self;
116        }
117
118        impl PreMulRgba {
119            $(
120                paste! {
121                    fn [<mix_ $Mode:lower _impl>](self, background: PreMulRgba) -> PreMulRgba {
122                        PreMulRgba {
123                            red: {
124                                let $fg = self.red;
125                                let $bg = background.red;
126                                let $ca0 = self.alpha;
127                                let $ca1 = background.alpha;
128                                $mix
129                            },
130                            green: {
131                                let $fg = self.green;
132                                let $bg = background.green;
133                                let $ca0 = self.alpha;
134                                let $ca1 = background.alpha;
135                                $mix
136                            },
137                            blue: {
138                                let $fg = self.blue;
139                                let $bg = background.blue;
140                                let $ca0 = self.alpha;
141                                let $ca1 = background.alpha;
142                                $mix
143                            },
144                            alpha: {
145                                let fga = self.alpha;
146                                let bga = background.alpha;
147                                (fga + bga - fga * bga).max(0.0).min(1.0)
148                            },
149                        }
150                    }
151                }
152            )+
153        }
154
155        impl Hsla {
156            $(
157                paste! {
158                    fn [<mix_ $NsMode:lower _impl>](self, background: Hsla) -> Hsla {
159                        let $fgh = self.hue;
160                        let $fgs = self.saturation;
161                        let $fgl = self.lightness;
162
163                        let $bgh = background.hue;
164                        let $bgs = background.saturation;
165                        let $bgl = background.lightness;
166
167                        let [h, s, l] = { $ns_mix };
168
169                        Hsla {
170                            hue: h,
171                            saturation: s,
172                            lightness: l,
173                            alpha: {
174                                let fga = self.alpha;
175                                let bga = background.alpha;
176                                (fga + bga - fga * bga).max(0.0).min(1.0)
177                            }
178                        }
179                    }
180                }
181            )+
182        }
183
184        impl MixAdjust for PreMulRgba {
185            $(
186                paste! {
187                    fn [<mix_ $Mode:lower>](self, background: Self) -> Self {
188                       self.[<mix_ $Mode:lower _impl>](background)
189                    }
190                }
191            )+
192            $(
193                paste! {
194                    fn [<mix_ $NsMode:lower>](self, background: Self) -> Self {
195                        Hsla::from(self).[<mix_ $NsMode:lower>](Hsla::from(background)).into()
196                    }
197                }
198            )+
199
200            fn lighten<A: Into<Factor>>(self, amount: A) -> Self {
201                Hsla::from(self).lighten(amount.into()).into()
202            }
203
204            fn darken<A: Into<Factor>>(self, amount: A) -> Self {
205                Hsla::from(self).darken(amount.into()).into()
206            }
207
208            fn desaturate<A: Into<Factor>>(self, amount: A) -> Self {
209                Hsla::from(self).desaturate(amount).into()
210            }
211
212            fn with_lightness<L: Into<Factor>>(self, lightness: L) -> Self {
213                Hsla::from(self).with_lightness(lightness).into()
214            }
215        }
216
217        impl MixAdjust for Hsla {
218            $(
219                paste! {
220                    fn [<mix_ $Mode:lower>](self, background: Self) -> Self {
221                        PreMulRgba::from(self).[<mix_ $Mode:lower>](PreMulRgba::from(background)).into()
222                    }
223                }
224            )+
225
226            $(
227                paste! {
228                    fn [<mix_ $NsMode:lower>](self, background: Self) -> Self {
229                       self.[<mix_ $NsMode:lower _impl>](background)
230                    }
231                }
232            )+
233
234            fn lighten<A: Into<Factor>>(self, amount: A) -> Self {
235                let mut lighter = self;
236                lighter.lightness = clamp_normal(lighter.lightness + (lighter.lightness * amount.into().0));
237                lighter
238            }
239
240            fn darken<A: Into<Factor>>(self, amount: A) -> Self {
241                let mut darker = self;
242                darker.lightness = clamp_normal(darker.lightness - (darker.lightness * amount.into().0));
243                darker
244            }
245
246            fn desaturate<A: Into<Factor>>(self, amount: A) -> Self {
247                let mut d = self;
248                d.saturation = clamp_normal(d.saturation - (d.saturation * amount.into().0));
249                d
250            }
251
252            fn with_lightness<L: Into<Factor>>(mut self, lightness: L) -> Self {
253                self.set_lightness(lightness);
254                self
255            }
256        }
257
258        impl MixAdjust for Rgba {
259            $(
260                paste! {
261                    fn [<mix_ $Mode:lower>](self, background: Self) -> Self {
262                        PreMulRgba::from(self).[<mix_ $Mode:lower>](PreMulRgba::from(background)).into()
263                    }
264                }
265            )+
266            $(
267                paste! {
268                    fn [<mix_ $NsMode:lower>](self, background: Self) -> Self {
269                        Hsla::from(self).[<mix_ $NsMode:lower>](Hsla::from(background)).into()
270                    }
271                }
272            )+
273
274            fn lighten<A: Into<Factor>>(self, amount: A) -> Self {
275                Hsla::from(self).lighten(amount.into()).into()
276            }
277
278            fn darken<A: Into<Factor>>(self, amount: A) -> Self {
279                Hsla::from(self).darken(amount.into()).into()
280            }
281
282            fn desaturate<A: Into<Factor>>(self, amount: A) -> Self {
283                Hsla::from(self).desaturate(amount).into()
284            }
285
286            fn with_lightness<L: Into<Factor>>(self, lightness: L) -> Self {
287                Hsla::from(self).with_lightness(lightness).into()
288            }
289        }
290
291        impl MixAdjust for Hsva {
292            $(
293                paste! {
294                    fn [<mix_ $Mode:lower>](self, background: Self) -> Self {
295                        PreMulRgba::from(self).[<mix_ $Mode:lower>](PreMulRgba::from(background)).into()
296                    }
297                }
298            )+
299            $(
300                paste! {
301                    fn [<mix_ $NsMode:lower>](self, background: Self) -> Self {
302                        Hsla::from(self).[<mix_ $NsMode:lower>](Hsla::from(background)).into()
303                    }
304                }
305            )+
306
307            fn lighten<A: Into<Factor>>(self, amount: A) -> Self {
308                Hsla::from(self).lighten(amount.into()).into()
309            }
310
311            fn darken<A: Into<Factor>>(self, amount: A) -> Self {
312                Hsla::from(self).darken(amount.into()).into()
313            }
314
315            fn desaturate<A: Into<Factor>>(self, amount: A) -> Self {
316                Hsla::from(self).desaturate(amount).into()
317            }
318
319            fn with_lightness<L: Into<Factor>>(self, lightness: L) -> Self {
320                Hsla::from(self).with_lightness(lightness).into()
321            }
322        }
323    };
324}
325
326impl_mix! {
327    separable {
328        /// Normal alpha blend of the foreground color over the background.
329        Normal => |fg, bg, fga, _bga| fg + bg * (1.0 - fga),
330
331        /// Multiply the colors.
332        ///
333        /// The resultant color is always at least as dark as either color.
334        /// Multiplying any color with black results in black.
335        /// Multiplying any color with white preserves the original color.
336        Multiply => |fg, bg, fga, bga| fg * bg + fg * (1.0 - bga) + bg * (1.0 - fga),
337
338        /// Multiply the colors, then complements the result.
339        ///
340        /// The result color is always at least as light as either of the two constituent colors.
341        /// Screening any color with white produces white; screening with black leaves the original color unchanged.
342        /// The effect is similar to projecting multiple photographic slides simultaneously onto a single screen.
343        Screen => |fg, bg, _fga, _bga| fg + bg - fg * bg,
344
345        /// Multiplies or screens the colors, depending on the background color value.
346        ///
347        /// Foreground color overlays the background while preserving its highlights and shadows.
348        /// The background color is not replaced but is mixed with the foreground color to reflect the lightness or darkness
349        /// of the background.
350        ///
351        /// This is the inverse of *hardlight*.
352        Overlay => |fg, bg, fga, bga| if bg * 2.0 <= bga {
353            2.0 * fg * bg + fg * (1.0 - bga) + bg * (1.0 - fga)
354        } else {
355            fg * (1.0 + bga) + bg * (1.0 + fga) - 2.0 * fg * bg - fga * bga
356        },
357
358        /// Selects the darker of the colors.
359        ///
360        /// The background color is replaced with the foreground where the background is darker; otherwise, it is left unchanged.
361        Darken => |fg, bg, fga, bga| (fg * bga).min(bg * fga) + fg * (1.0 - bga) + bg * (1.0 - fga),
362
363        /// Selects the lighter of the colors.
364        ///
365        /// The background color is replaced with the foreground where the background is lighter; otherwise, it is left unchanged.
366        Lighten => |fg, bg, fga, bga| (fg * bga).max(bg * fga) + fg * (1.0 - bga) + bg * (1.0 - fga),
367
368        /// Brightens the background color to reflect the foreground color. Painting with black produces no changes.
369        ColorDodge => |fg, bg, fga, bga| if fg == fga {
370            fga * bga + fg * (1.0 - bga) + bg * (1.0 - fga)
371        } else {
372            fga * bga * 1.0_f32.min((bg / bga) * fga / (fga - fg)) + fg * (1.0 - bga) + bg * (1.0 - fga)
373        },
374
375        /// Darkens the background color to reflect the foreground color. Painting with white produces no change.
376        ColorBurn => |fg, bg, fga, bga| fga * bga * (1.0 - 1.0_f32.min((1.0 - bg / bga) * fga / fg)) + fg * (1.0 - bga) + bg * (1.0 - fga),
377
378        /// Multiplies or screens the colors, depending on the foreground color value.
379        ///
380        /// The effect is similar to shining a harsh spotlight on the background color.
381        HardLight => |fg, bg, fga, bga| if fg * 2.0 <= fga {
382            2.0 * fg * bg + fg * (1.0 - bga) + bg * (1.0 - fga)
383        } else {
384            fg * (1.0 + bga) + bg * (1.0 + fga) - 2.0 * fg * bg - fga * bga
385        },
386
387        /// Darkens or lightens the colors, depending on the foreground color value.
388        ///
389        /// The effect is similar to shining a diffused spotlight on the background color.
390        SoftLight => |fg, bg, fga, bga| {
391            let m = bg / bga;
392
393            if fg * 2.0 <= fga {
394                bg * (fga + (2.0 * fg - fga) * (1.0 - m)) + fg * (1.0 - bga) + bg * (1.0 - fga)
395            } else if bg * 4.0 <= bga {
396                let m2 = m * m;
397                let m3 = m2 * m;
398                bga * (2.0 * fg - fga) * (m3 * 16.0 - m2 * 12.0 - m * 3.0) + fg - fg * bga + bg
399            } else {
400                bga * (2.0 * fg - fga) * (m.sqrt() - m) + fg - fg * bga + bg
401            }
402        },
403
404        /// Subtracts the darker of the two constituent colors from the lighter color.
405        ///
406        /// Painting with white inverts the background color; painting with black produces no change.
407        Difference => |fg, bg, fga, bga| fg + bg - 2.0 * (fg * bga).min(bg * fga),
408
409        /// Produces an effect similar to that of the *difference* mode but lower in contrast.
410        ///
411        /// Painting with white inverts the background color; painting with black produces no change.
412        Exclusion => |fg, bg, _fga, _bga| fg + bg - 2.0 * fg * bg,
413    }
414
415    non_separable {
416        /// Creates a color with the hue of the foreground color and the saturation and luminosity of the background color.
417        Hue => |[fgh, _fgs, _fgl], [_bgh, bgs, bgl]| [fgh, bgs, bgl],
418
419        /// Creates a color with the saturation of the foreground color and the hue and luminosity of the background color.
420        Saturation => |[_fgh, fgs, _fgl], [bgh, _bgs, bgl]| [bgh, fgs, bgl],
421
422        /// Creates a color with the hue and saturation of the foreground color and the luminosity of the background color.
423        Color => |[fgh, fgs, _fgl], [_bgh, _bgs, bgl]| [fgh, fgs, bgl],
424
425        /// Creates a color with the luminosity of the foreground color and the hue and saturation of the background color.
426        Luminosity => |[fgh, _fgs, _fbl], [bgh, bgs, _bgl]| [bgh, bgs, fgh],
427    }
428}
429#[expect(clippy::derivable_impls)] // macro generated enum
430impl Default for MixBlendMode {
431    fn default() -> Self {
432        MixBlendMode::Normal
433    }
434}