Skip to main content

incular_rendering/
effects.rs

1use crate::geometry::union_rect;
2use incular_core::finite_non_negative;
3use incular_core::{Color, Offset, Rect, Size, finite_or_zero};
4
5/// Renderer-neutral separable Gaussian blur description. Sigma is expressed
6/// in logical pixels; a backend converts it to physical pixels at render
7/// time. Negative and non-finite values are normalized to zero at this
8/// boundary so every backend observes the same effect semantics.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct GaussianBlur {
11    pub sigma_x: f32,
12    pub sigma_y: f32,
13}
14impl GaussianBlur {
15    #[must_use]
16    pub fn new(sigma_x: f32, sigma_y: f32) -> Self {
17        Self {
18            sigma_x: normalize_sigma(sigma_x),
19            sigma_y: normalize_sigma(sigma_y),
20        }
21    }
22    #[must_use]
23    pub fn uniform(sigma: f32) -> Self {
24        Self::new(sigma, sigma)
25    }
26}
27
28/// Renderer-neutral arbitrary-subtree shadow description. The source is
29/// isolated first, then its alpha is blurred and colorized by the compositor.
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct DropShadowEffect {
32    pub offset: Offset,
33    pub sigma_x: f32,
34    pub sigma_y: f32,
35    pub color: Color,
36}
37
38/// A renderer-independent 4x5 color matrix. Matrix inputs and outputs are
39/// straight (unpremultiplied) RGBA in the normalized 0..=1 range. The GPU
40/// backend converts its retained premultiplied texture to straight color,
41/// applies this matrix, clamps the result, and premultiplies it again.
42#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct ColorFilter {
44    matrix: [f32; 20],
45}
46/// Compatibility spelling for callers that model a 4×5 filter as a color
47/// matrix.  The alias keeps one canonical representation and one set of
48/// straight/premultiplied semantics.
49pub type ColorMatrix = ColorFilter;
50impl ColorFilter {
51    #[must_use]
52    pub fn new(matrix: [f32; 20]) -> Self {
53        Self::matrix(matrix)
54    }
55    #[must_use]
56    pub fn matrix(matrix: [f32; 20]) -> Self {
57        Self {
58            matrix: matrix.map(finite_or_zero),
59        }
60    }
61    #[must_use]
62    pub fn identity() -> Self {
63        Self::matrix([
64            1., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0.,
65        ])
66    }
67    #[must_use]
68    pub fn to_matrix(self) -> [f32; 20] {
69        self.matrix
70    }
71    #[must_use]
72    pub fn is_identity(self) -> bool {
73        self == Self::identity()
74    }
75    /// Applies the matrix to straight normalized RGBA and clamps every output
76    /// component. Zero-alpha input has zero straight RGB to avoid undefined
77    /// premultiplied-to-straight division at transparent texels.
78    #[must_use]
79    pub fn apply(self, rgba: [f32; 4]) -> [f32; 4] {
80        let alpha = finite_unit(rgba[3]);
81        let input = [
82            if alpha > f32::EPSILON {
83                finite_unit(rgba[0])
84            } else {
85                0.
86            },
87            if alpha > f32::EPSILON {
88                finite_unit(rgba[1])
89            } else {
90                0.
91            },
92            if alpha > f32::EPSILON {
93                finite_unit(rgba[2])
94            } else {
95                0.
96            },
97            alpha,
98        ];
99        let mut out = [0.; 4];
100        for (row, value) in out.iter_mut().enumerate() {
101            let base = row * 5;
102            *value = self.matrix[base] * input[0]
103                + self.matrix[base + 1] * input[1]
104                + self.matrix[base + 2] * input[2]
105                + self.matrix[base + 3] * input[3]
106                + self.matrix[base + 4];
107        }
108        let out = out.map(finite_unit);
109        if out[3] <= f32::EPSILON {
110            [0., 0., 0., 0.]
111        } else {
112            out
113        }
114    }
115    /// Returns a matrix for `self` followed by `next` (`next * self`). The
116    /// affine bias column is composed as part of the same multiplication.
117    #[must_use]
118    pub fn then(self, next: Self) -> Self {
119        let a = self.matrix;
120        let b = next.matrix;
121        let mut out = [0.; 20];
122        for row in 0..4 {
123            for column in 0..4 {
124                out[row * 5 + column] = (0..4)
125                    .map(|index| b[row * 5 + index] * a[index * 5 + column])
126                    .sum();
127            }
128            out[row * 5 + 4] = b[row * 5 + 4]
129                + (0..4)
130                    .map(|index| b[row * 5 + index] * a[index * 5 + 4])
131                    .sum::<f32>();
132        }
133        Self::matrix(out)
134    }
135    /// Alias for [`Self::then`] that reads naturally at call sites building a
136    /// filter pipeline.
137    #[must_use]
138    pub fn compose(self, next: Self) -> Self {
139        self.then(next)
140    }
141    #[must_use]
142    pub fn grayscale(amount: f32) -> Self {
143        let amount = finite_unit(amount);
144        let inv = 1. - amount;
145        let r = 0.2126 * amount;
146        let g = 0.7152 * amount;
147        let b = 0.0722 * amount;
148        Self::matrix([
149            r + inv,
150            g,
151            b,
152            0.,
153            0.,
154            r,
155            g + inv,
156            b,
157            0.,
158            0.,
159            r,
160            g,
161            b + inv,
162            0.,
163            0.,
164            0.,
165            0.,
166            0.,
167            1.,
168            0.,
169        ])
170    }
171    #[must_use]
172    pub fn sepia(amount: f32) -> Self {
173        let amount = finite_unit(amount);
174        let inv = 1. - amount;
175        Self::matrix([
176            0.393 * amount + inv,
177            0.769 * amount,
178            0.189 * amount,
179            0.,
180            0.,
181            0.349 * amount,
182            0.686 * amount + inv,
183            0.168 * amount,
184            0.,
185            0.,
186            0.272 * amount,
187            0.534 * amount,
188            0.131 * amount + inv,
189            0.,
190            0.,
191            0.,
192            0.,
193            0.,
194            1.,
195            0.,
196        ])
197    }
198    /// Brightness uses one as identity and zero as black.
199    #[must_use]
200    pub fn brightness(amount: f32) -> Self {
201        let amount = finite_non_negative(amount);
202        Self::matrix([
203            amount, 0., 0., 0., 0., 0., amount, 0., 0., 0., 0., 0., amount, 0., 0., 0., 0., 0., 1.,
204            0.,
205        ])
206    }
207    /// Contrast uses one as identity; the bias keeps middle gray fixed.
208    #[must_use]
209    pub fn contrast(amount: f32) -> Self {
210        let amount = finite_non_negative(amount);
211        let bias = 0.5 * (1. - amount);
212        Self::matrix([
213            amount, 0., 0., 0., bias, 0., amount, 0., 0., bias, 0., 0., amount, 0., bias, 0., 0.,
214            0., 1., 0.,
215        ])
216    }
217    /// Saturation uses one as identity and zero as grayscale.
218    #[must_use]
219    pub fn saturate(amount: f32) -> Self {
220        let amount = finite_non_negative(amount);
221        let inv = 1. - amount;
222        let r = 0.2126 * inv;
223        let g = 0.7152 * inv;
224        let b = 0.0722 * inv;
225        Self::matrix([
226            r + amount,
227            g,
228            b,
229            0.,
230            0.,
231            r,
232            g + amount,
233            b,
234            0.,
235            0.,
236            r,
237            g,
238            b + amount,
239            0.,
240            0.,
241            0.,
242            0.,
243            0.,
244            1.,
245            0.,
246        ])
247    }
248    /// Invert uses one as the fully inverted result and zero as identity.
249    #[must_use]
250    pub fn invert(amount: f32) -> Self {
251        let amount = finite_unit(amount);
252        Self::matrix([
253            1. - 2. * amount,
254            0.,
255            0.,
256            0.,
257            amount,
258            0.,
259            1. - 2. * amount,
260            0.,
261            0.,
262            amount,
263            0.,
264            0.,
265            1. - 2. * amount,
266            0.,
267            amount,
268            0.,
269            0.,
270            0.,
271            1.,
272            0.,
273        ])
274    }
275    /// Component-wise modulation: every straight channel is multiplied by
276    /// the color's straight working-space components, including alpha. This
277    /// recolors only white masks; it is not a flat tint (see [`Self::tint`]).
278    #[must_use]
279    pub fn modulate(color: Color) -> Self {
280        let [red, green, blue, alpha] = crate::working_rgba(color);
281        Self::matrix([
282            red, 0., 0., 0., 0., 0., green, 0., 0., 0., 0., 0., blue, 0., 0., 0., 0., 0., alpha, 0.,
283        ])
284    }
285    /// Constant-color tint in straight RGBA: `R' = tint.r`, `G' = tint.g`,
286    /// `B' = tint.b`, `A' = source.a * tint.a`.
287    ///
288    /// A 4x5 matrix expresses this through the RGB bias column and the
289    /// alpha row's source-alpha coefficient; the shader and [`Self::apply`]
290    /// share that bias convention, so both agree. Transparent sources stay
291    /// transparent: zero source alpha yields zero straight RGB, and the
292    /// biased RGB is multiplied back by the zero alpha at output.
293    #[must_use]
294    pub fn tint(color: Color) -> Self {
295        let [red, green, blue, alpha] = crate::working_rgba(color);
296        Self::matrix([
297            0., 0., 0., 0., red, 0., 0., 0., 0., green, 0., 0., 0., 0., blue, 0., 0., 0., alpha, 0.,
298        ])
299    }
300    /// Opacity changes alpha only; RGB remains straight and is premultiplied
301    /// by the resulting alpha by the renderer.
302    #[must_use]
303    pub fn opacity(amount: f32) -> Self {
304        Self::matrix([
305            1.,
306            0.,
307            0.,
308            0.,
309            0.,
310            0.,
311            1.,
312            0.,
313            0.,
314            0.,
315            0.,
316            0.,
317            1.,
318            0.,
319            0.,
320            0.,
321            0.,
322            0.,
323            finite_unit(amount),
324            0.,
325        ])
326    }
327}
328impl Default for ColorFilter {
329    fn default() -> Self {
330        Self::identity()
331    }
332}
333
334fn finite_unit(value: f32) -> f32 {
335    finite_or_zero(value).clamp(0., 1.)
336}
337
338/// A blend operation defined over premultiplied encoded-sRGB RGBA values
339/// (see [`crate::working_rgba`]). The GPU
340/// compositor uses the same equations as [`blend_premultiplied`].
341#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
342pub enum BlendMode {
343    #[default]
344    SrcOver,
345    Src,
346    DstOver,
347    SrcIn,
348    DstIn,
349    SrcOut,
350    DstOut,
351    SrcAtop,
352    DstAtop,
353    Xor,
354    Plus,
355    Multiply,
356    Screen,
357    Overlay,
358    Darken,
359    Lighten,
360    ColorDodge,
361    ColorBurn,
362    HardLight,
363    SoftLight,
364    Difference,
365    Exclusion,
366    /// Component-wise source/destination modulation. Flutter uses this as
367    /// the default blend mode for [`ShaderMask`](https://api.flutter.dev/flutter/widgets/ShaderMask-class.html).
368    Modulate,
369}
370impl BlendMode {
371    #[must_use]
372    pub const fn requires_destination_read(self) -> bool {
373        !matches!(
374            self,
375            Self::SrcOver
376                | Self::Src
377                | Self::DstOver
378                | Self::SrcIn
379                | Self::DstIn
380                | Self::SrcOut
381                | Self::DstOut
382                | Self::SrcAtop
383                | Self::DstAtop
384                | Self::Xor
385                | Self::Plus
386        )
387    }
388    #[must_use]
389    pub const fn code(self) -> u32 {
390        match self {
391            Self::SrcOver => 0,
392            Self::Src => 1,
393            Self::DstOver => 2,
394            Self::SrcIn => 3,
395            Self::DstIn => 4,
396            Self::SrcOut => 5,
397            Self::DstOut => 6,
398            Self::SrcAtop => 7,
399            Self::DstAtop => 8,
400            Self::Xor => 9,
401            Self::Plus => 10,
402            Self::Multiply => 11,
403            Self::Screen => 12,
404            Self::Overlay => 13,
405            Self::Darken => 14,
406            Self::Lighten => 15,
407            Self::ColorDodge => 16,
408            Self::ColorBurn => 17,
409            Self::HardLight => 18,
410            Self::SoftLight => 19,
411            Self::Difference => 20,
412            Self::Exclusion => 21,
413            Self::Modulate => 22,
414        }
415    }
416}
417
418/// Reference blend implementation for premultiplied normalized RGBA. It is
419/// public so tests and alternate backends can verify their shader formulas.
420#[must_use]
421pub fn blend_premultiplied(mode: BlendMode, source: [f32; 4], destination: [f32; 4]) -> [f32; 4] {
422    let src = sanitize_premultiplied(source);
423    let dst = sanitize_premultiplied(destination);
424    let as_ = src[3];
425    let ad = dst[3];
426    let ao = (as_ + ad - as_ * ad).clamp(0., 1.);
427    let result = match mode {
428        BlendMode::SrcOver => [
429            src[0] + dst[0] * (1. - as_),
430            src[1] + dst[1] * (1. - as_),
431            src[2] + dst[2] * (1. - as_),
432            ao,
433        ],
434        BlendMode::Src => src,
435        BlendMode::DstOver => [
436            dst[0] + src[0] * (1. - ad),
437            dst[1] + src[1] * (1. - ad),
438            dst[2] + src[2] * (1. - ad),
439            ao,
440        ],
441        BlendMode::SrcIn => [src[0] * ad, src[1] * ad, src[2] * ad, as_ * ad],
442        BlendMode::DstIn => [dst[0] * as_, dst[1] * as_, dst[2] * as_, ad * as_],
443        BlendMode::SrcOut => [
444            src[0] * (1. - ad),
445            src[1] * (1. - ad),
446            src[2] * (1. - ad),
447            as_ * (1. - ad),
448        ],
449        BlendMode::DstOut => [
450            dst[0] * (1. - as_),
451            dst[1] * (1. - as_),
452            dst[2] * (1. - as_),
453            ad * (1. - as_),
454        ],
455        BlendMode::SrcAtop => [
456            src[0] * ad + dst[0] * (1. - as_),
457            src[1] * ad + dst[1] * (1. - as_),
458            src[2] * ad + dst[2] * (1. - as_),
459            ad,
460        ],
461        BlendMode::DstAtop => [
462            dst[0] * as_ + src[0] * (1. - ad),
463            dst[1] * as_ + src[1] * (1. - ad),
464            dst[2] * as_ + src[2] * (1. - ad),
465            as_,
466        ],
467        BlendMode::Xor => [
468            src[0] * (1. - ad) + dst[0] * (1. - as_),
469            src[1] * (1. - ad) + dst[1] * (1. - as_),
470            src[2] * (1. - ad) + dst[2] * (1. - as_),
471            (as_ + ad - 2. * as_ * ad).clamp(0., 1.),
472        ],
473        BlendMode::Plus => [
474            (src[0] + dst[0]).min(1.),
475            (src[1] + dst[1]).min(1.),
476            (src[2] + dst[2]).min(1.),
477            (as_ + ad).min(1.),
478        ],
479        BlendMode::Modulate => [src[0] * dst[0], src[1] * dst[1], src[2] * dst[2], as_ * ad],
480        artistic => {
481            let cs = straight_rgb(src);
482            let cd = straight_rgb(dst);
483            let blended = artistic_blend_rgb(artistic, cs, cd);
484            [
485                (src[0] * (1. - ad) + dst[0] * (1. - as_) + as_ * ad * blended[0]).clamp(0., 1.),
486                (src[1] * (1. - ad) + dst[1] * (1. - as_) + as_ * ad * blended[1]).clamp(0., 1.),
487                (src[2] * (1. - ad) + dst[2] * (1. - as_) + as_ * ad * blended[2]).clamp(0., 1.),
488                ao,
489            ]
490        }
491    };
492    sanitize_premultiplied(result)
493}
494
495fn sanitize_premultiplied([r, g, b, a]: [f32; 4]) -> [f32; 4] {
496    let alpha = finite_unit(a);
497    let channel = |value: f32| finite_or_zero(value).clamp(0., alpha);
498    [channel(r), channel(g), channel(b), alpha]
499}
500fn straight_rgb(value: [f32; 4]) -> [f32; 3] {
501    if value[3] <= f32::EPSILON {
502        [0.; 3]
503    } else {
504        [
505            (value[0] / value[3]).clamp(0., 1.),
506            (value[1] / value[3]).clamp(0., 1.),
507            (value[2] / value[3]).clamp(0., 1.),
508        ]
509    }
510}
511fn artistic_blend_rgb(mode: BlendMode, source: [f32; 3], destination: [f32; 3]) -> [f32; 3] {
512    let mut result = [0.; 3];
513    for index in 0..3 {
514        let s = source[index];
515        let d = destination[index];
516        result[index] = match mode {
517            BlendMode::Multiply => s * d,
518            BlendMode::Screen => s + d - s * d,
519            BlendMode::Overlay => {
520                if d <= 0.5 {
521                    2. * s * d
522                } else {
523                    1. - 2. * (1. - s) * (1. - d)
524                }
525            }
526            BlendMode::Darken => s.min(d),
527            BlendMode::Lighten => s.max(d),
528            BlendMode::ColorDodge => {
529                if s >= 1. {
530                    1.
531                } else {
532                    (d / (1. - s)).min(1.)
533                }
534            }
535            BlendMode::ColorBurn => {
536                if s <= 0. {
537                    0.
538                } else {
539                    (1. - (1. - d) / s).max(0.)
540                }
541            }
542            BlendMode::HardLight => {
543                if s <= 0.5 {
544                    2. * s * d
545                } else {
546                    1. - 2. * (1. - s) * (1. - d)
547                }
548            }
549            BlendMode::SoftLight => {
550                if s <= 0.5 {
551                    d - (1. - 2. * s) * d * (1. - d)
552                } else {
553                    let g = if d <= 0.25 {
554                        ((16. * d - 12.) * d + 4.) * d
555                    } else {
556                        d.sqrt()
557                    };
558                    d + (2. * s - 1.) * (g - d)
559                }
560            }
561            BlendMode::Difference => (d - s).abs(),
562            BlendMode::Exclusion => s + d - 2. * s * d,
563            BlendMode::Modulate => s * d,
564            _ => s,
565        };
566    }
567    result.map(|value| value.clamp(0., 1.))
568}
569
570/// Ordered single-input effect descriptions. Drop shadows remain a separate
571/// layer because they intentionally produce both a shadow and the original
572/// source output.
573#[derive(Clone, Copy, Debug, PartialEq)]
574pub enum Effect {
575    GaussianBlur(GaussianBlur),
576    ColorMatrix(ColorFilter),
577}
578#[derive(Clone, Debug, Default, PartialEq)]
579pub struct EffectChain {
580    effects: Vec<Effect>,
581}
582impl EffectChain {
583    #[must_use]
584    pub const fn new() -> Self {
585        Self {
586            effects: Vec::new(),
587        }
588    }
589    #[must_use]
590    pub fn from_effects(effects: Vec<Effect>) -> Self {
591        Self { effects }
592    }
593    #[must_use]
594    pub fn effects(&self) -> &[Effect] {
595        &self.effects
596    }
597    pub fn push(&mut self, effect: Effect) {
598        self.effects.push(effect);
599    }
600    #[must_use]
601    pub fn with_effect(mut self, effect: Effect) -> Self {
602        self.push(effect);
603        self
604    }
605    #[must_use]
606    pub fn color_filter(self, filter: ColorFilter) -> Self {
607        self.with_effect(Effect::ColorMatrix(filter))
608    }
609    #[must_use]
610    pub fn blur(self, sigma: f32) -> Self {
611        self.with_effect(Effect::GaussianBlur(GaussianBlur::uniform(sigma)))
612    }
613    /// Fuses only adjacent color matrices. Spatial effects remain explicit so
614    /// their ordering and cache dependencies cannot be changed accidentally.
615    #[must_use]
616    pub fn optimized(&self) -> Self {
617        let mut out = Self::new();
618        for effect in &self.effects {
619            if let (Some(Effect::ColorMatrix(previous)), Effect::ColorMatrix(next)) =
620                (out.effects.last_mut(), effect)
621            {
622                *previous = previous.then(*next);
623            } else {
624                out.effects.push(*effect);
625            }
626        }
627        out
628    }
629    #[must_use]
630    pub fn fusion_count(&self) -> usize {
631        self.effects
632            .len()
633            .saturating_sub(self.optimized().effects.len())
634    }
635}
636impl DropShadowEffect {
637    #[must_use]
638    pub fn new(offset: Offset, sigma: f32, color: Color) -> Self {
639        Self {
640            offset: Offset::finite_or_zero(offset),
641            sigma_x: normalize_sigma(sigma),
642            sigma_y: normalize_sigma(sigma),
643            color,
644        }
645    }
646    #[must_use]
647    pub fn asymmetric(offset: Offset, sigma_x: f32, sigma_y: f32, color: Color) -> Self {
648        Self {
649            offset: Offset::finite_or_zero(offset),
650            sigma_x: normalize_sigma(sigma_x),
651            sigma_y: normalize_sigma(sigma_y),
652            color,
653        }
654    }
655    #[must_use]
656    pub fn blur(self) -> GaussianBlur {
657        GaussianBlur::new(self.sigma_x, self.sigma_y)
658    }
659}
660
661/// Canonical sigma normalization used by painting, widgets, and GPU
662/// lowering. Non-finite values follow the existing "invalid numeric input is
663/// transparent/zero" policy.
664#[must_use]
665pub fn normalize_sigma(sigma: f32) -> f32 {
666    finite_non_negative(sigma)
667}
668
669/// Conservative logical blur margin for the finite 3-sigma cutoff.
670#[must_use]
671pub fn blur_margin(sigma: f32) -> f32 {
672    (3. * normalize_sigma(sigma)).max(0.)
673}
674
675/// Expands source bounds by the finite Gaussian support in logical pixels.
676#[must_use]
677pub fn blur_bounds(source: Rect, sigma_x: f32, sigma_y: f32) -> Rect {
678    let mx = blur_margin(sigma_x);
679    let my = blur_margin(sigma_y);
680    Rect::from_origin_size(
681        Offset::new(source.origin.x - mx, source.origin.y - my),
682        Size::new(source.size.width + 2. * mx, source.size.height + 2. * my),
683    )
684}
685
686/// Bounds of a shadow plus the original source. The union is deliberately
687/// conservative and works for positive, negative, and fractional offsets.
688#[must_use]
689pub fn drop_shadow_bounds(source: Rect, offset: Offset, sigma_x: f32, sigma_y: f32) -> Rect {
690    union_rect(
691        source,
692        blur_bounds(
693            Rect::from_origin_size(source.origin + Offset::finite_or_zero(offset), source.size),
694            sigma_x,
695            sigma_y,
696        ),
697    )
698}
699
700/// A normalized, symmetric one-dimensional Gaussian kernel represented in
701/// order `[-radius, ..., 0, ..., +radius]`. The cutoff is exactly three
702/// physical sigma, and sigma zero is the identity kernel.
703#[must_use]
704pub fn gaussian_kernel_weights(sigma_physical: f32) -> Vec<f32> {
705    let sigma = normalize_sigma(sigma_physical);
706    if sigma <= f32::EPSILON {
707        return vec![1.];
708    }
709    let radius = (3. * sigma).ceil().max(1.) as i32;
710    let denominator = 2. * sigma * sigma;
711    let mut weights = (-radius..=radius)
712        .map(|i| (-(i * i) as f32 / denominator).exp())
713        .collect::<Vec<_>>();
714    let sum: f32 = weights.iter().sum();
715    if sum.is_finite() && sum > 0. {
716        for weight in &mut weights {
717            *weight /= sum;
718        }
719    } else {
720        weights.fill(0.);
721        weights[radius as usize] = 1.;
722    }
723    weights
724}
725
726/// Premultiplied shadow color for one blurred alpha sample. This helper is
727/// shared by deterministic CPU tests and documents the exact GPU equation.
728#[must_use]
729pub fn premultiplied_shadow_sample(color: Color, mask: f32) -> [f32; 4] {
730    let alpha = (f32::from(color.alpha) / 255.) * mask.clamp(0., 1.);
731    let [r, g, b, _] = crate::working_rgba(color);
732    [r * alpha, g * alpha, b * alpha, alpha]
733}