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 linear 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] = color.to_linear_rgba();
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] = color.to_linear_rgba();
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 linear RGBA values. The GPU
339/// compositor uses the same equations as [`blend_premultiplied`].
340#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
341pub enum BlendMode {
342    #[default]
343    SrcOver,
344    Src,
345    DstOver,
346    SrcIn,
347    DstIn,
348    SrcOut,
349    DstOut,
350    SrcAtop,
351    DstAtop,
352    Xor,
353    Plus,
354    Multiply,
355    Screen,
356    Overlay,
357    Darken,
358    Lighten,
359    ColorDodge,
360    ColorBurn,
361    HardLight,
362    SoftLight,
363    Difference,
364    Exclusion,
365    /// Component-wise source/destination modulation. Flutter uses this as
366    /// the default blend mode for [`ShaderMask`](https://api.flutter.dev/flutter/widgets/ShaderMask-class.html).
367    Modulate,
368}
369impl BlendMode {
370    #[must_use]
371    pub const fn requires_destination_read(self) -> bool {
372        !matches!(
373            self,
374            Self::SrcOver
375                | Self::Src
376                | Self::DstOver
377                | Self::SrcIn
378                | Self::DstIn
379                | Self::SrcOut
380                | Self::DstOut
381                | Self::SrcAtop
382                | Self::DstAtop
383                | Self::Xor
384                | Self::Plus
385        )
386    }
387    #[must_use]
388    pub const fn code(self) -> u32 {
389        match self {
390            Self::SrcOver => 0,
391            Self::Src => 1,
392            Self::DstOver => 2,
393            Self::SrcIn => 3,
394            Self::DstIn => 4,
395            Self::SrcOut => 5,
396            Self::DstOut => 6,
397            Self::SrcAtop => 7,
398            Self::DstAtop => 8,
399            Self::Xor => 9,
400            Self::Plus => 10,
401            Self::Multiply => 11,
402            Self::Screen => 12,
403            Self::Overlay => 13,
404            Self::Darken => 14,
405            Self::Lighten => 15,
406            Self::ColorDodge => 16,
407            Self::ColorBurn => 17,
408            Self::HardLight => 18,
409            Self::SoftLight => 19,
410            Self::Difference => 20,
411            Self::Exclusion => 21,
412            Self::Modulate => 22,
413        }
414    }
415}
416
417/// Reference blend implementation for premultiplied normalized RGBA. It is
418/// public so tests and alternate backends can verify their shader formulas.
419#[must_use]
420pub fn blend_premultiplied(mode: BlendMode, source: [f32; 4], destination: [f32; 4]) -> [f32; 4] {
421    let src = sanitize_premultiplied(source);
422    let dst = sanitize_premultiplied(destination);
423    let as_ = src[3];
424    let ad = dst[3];
425    let ao = (as_ + ad - as_ * ad).clamp(0., 1.);
426    let result = match mode {
427        BlendMode::SrcOver => [
428            src[0] + dst[0] * (1. - as_),
429            src[1] + dst[1] * (1. - as_),
430            src[2] + dst[2] * (1. - as_),
431            ao,
432        ],
433        BlendMode::Src => src,
434        BlendMode::DstOver => [
435            dst[0] + src[0] * (1. - ad),
436            dst[1] + src[1] * (1. - ad),
437            dst[2] + src[2] * (1. - ad),
438            ao,
439        ],
440        BlendMode::SrcIn => [src[0] * ad, src[1] * ad, src[2] * ad, as_ * ad],
441        BlendMode::DstIn => [dst[0] * as_, dst[1] * as_, dst[2] * as_, ad * as_],
442        BlendMode::SrcOut => [
443            src[0] * (1. - ad),
444            src[1] * (1. - ad),
445            src[2] * (1. - ad),
446            as_ * (1. - ad),
447        ],
448        BlendMode::DstOut => [
449            dst[0] * (1. - as_),
450            dst[1] * (1. - as_),
451            dst[2] * (1. - as_),
452            ad * (1. - as_),
453        ],
454        BlendMode::SrcAtop => [
455            src[0] * ad + dst[0] * (1. - as_),
456            src[1] * ad + dst[1] * (1. - as_),
457            src[2] * ad + dst[2] * (1. - as_),
458            ad,
459        ],
460        BlendMode::DstAtop => [
461            dst[0] * as_ + src[0] * (1. - ad),
462            dst[1] * as_ + src[1] * (1. - ad),
463            dst[2] * as_ + src[2] * (1. - ad),
464            as_,
465        ],
466        BlendMode::Xor => [
467            src[0] * (1. - ad) + dst[0] * (1. - as_),
468            src[1] * (1. - ad) + dst[1] * (1. - as_),
469            src[2] * (1. - ad) + dst[2] * (1. - as_),
470            (as_ + ad - 2. * as_ * ad).clamp(0., 1.),
471        ],
472        BlendMode::Plus => [
473            (src[0] + dst[0]).min(1.),
474            (src[1] + dst[1]).min(1.),
475            (src[2] + dst[2]).min(1.),
476            (as_ + ad).min(1.),
477        ],
478        BlendMode::Modulate => [src[0] * dst[0], src[1] * dst[1], src[2] * dst[2], as_ * ad],
479        artistic => {
480            let cs = straight_rgb(src);
481            let cd = straight_rgb(dst);
482            let blended = artistic_blend_rgb(artistic, cs, cd);
483            [
484                (src[0] * (1. - ad) + dst[0] * (1. - as_) + as_ * ad * blended[0]).clamp(0., 1.),
485                (src[1] * (1. - ad) + dst[1] * (1. - as_) + as_ * ad * blended[1]).clamp(0., 1.),
486                (src[2] * (1. - ad) + dst[2] * (1. - as_) + as_ * ad * blended[2]).clamp(0., 1.),
487                ao,
488            ]
489        }
490    };
491    sanitize_premultiplied(result)
492}
493
494fn sanitize_premultiplied([r, g, b, a]: [f32; 4]) -> [f32; 4] {
495    let alpha = finite_unit(a);
496    let channel = |value: f32| finite_or_zero(value).clamp(0., alpha);
497    [channel(r), channel(g), channel(b), alpha]
498}
499fn straight_rgb(value: [f32; 4]) -> [f32; 3] {
500    if value[3] <= f32::EPSILON {
501        [0.; 3]
502    } else {
503        [
504            (value[0] / value[3]).clamp(0., 1.),
505            (value[1] / value[3]).clamp(0., 1.),
506            (value[2] / value[3]).clamp(0., 1.),
507        ]
508    }
509}
510fn artistic_blend_rgb(mode: BlendMode, source: [f32; 3], destination: [f32; 3]) -> [f32; 3] {
511    let mut result = [0.; 3];
512    for index in 0..3 {
513        let s = source[index];
514        let d = destination[index];
515        result[index] = match mode {
516            BlendMode::Multiply => s * d,
517            BlendMode::Screen => s + d - s * d,
518            BlendMode::Overlay => {
519                if d <= 0.5 {
520                    2. * s * d
521                } else {
522                    1. - 2. * (1. - s) * (1. - d)
523                }
524            }
525            BlendMode::Darken => s.min(d),
526            BlendMode::Lighten => s.max(d),
527            BlendMode::ColorDodge => {
528                if s >= 1. {
529                    1.
530                } else {
531                    (d / (1. - s)).min(1.)
532                }
533            }
534            BlendMode::ColorBurn => {
535                if s <= 0. {
536                    0.
537                } else {
538                    (1. - (1. - d) / s).max(0.)
539                }
540            }
541            BlendMode::HardLight => {
542                if s <= 0.5 {
543                    2. * s * d
544                } else {
545                    1. - 2. * (1. - s) * (1. - d)
546                }
547            }
548            BlendMode::SoftLight => {
549                if s <= 0.5 {
550                    d - (1. - 2. * s) * d * (1. - d)
551                } else {
552                    let g = if d <= 0.25 {
553                        ((16. * d - 12.) * d + 4.) * d
554                    } else {
555                        d.sqrt()
556                    };
557                    d + (2. * s - 1.) * (g - d)
558                }
559            }
560            BlendMode::Difference => (d - s).abs(),
561            BlendMode::Exclusion => s + d - 2. * s * d,
562            BlendMode::Modulate => s * d,
563            _ => s,
564        };
565    }
566    result.map(|value| value.clamp(0., 1.))
567}
568
569/// Ordered single-input effect descriptions. Drop shadows remain a separate
570/// layer because they intentionally produce both a shadow and the original
571/// source output.
572#[derive(Clone, Copy, Debug, PartialEq)]
573pub enum Effect {
574    GaussianBlur(GaussianBlur),
575    ColorMatrix(ColorFilter),
576}
577#[derive(Clone, Debug, Default, PartialEq)]
578pub struct EffectChain {
579    effects: Vec<Effect>,
580}
581impl EffectChain {
582    #[must_use]
583    pub const fn new() -> Self {
584        Self {
585            effects: Vec::new(),
586        }
587    }
588    #[must_use]
589    pub fn from_effects(effects: Vec<Effect>) -> Self {
590        Self { effects }
591    }
592    #[must_use]
593    pub fn effects(&self) -> &[Effect] {
594        &self.effects
595    }
596    pub fn push(&mut self, effect: Effect) {
597        self.effects.push(effect);
598    }
599    #[must_use]
600    pub fn with_effect(mut self, effect: Effect) -> Self {
601        self.push(effect);
602        self
603    }
604    #[must_use]
605    pub fn color_filter(self, filter: ColorFilter) -> Self {
606        self.with_effect(Effect::ColorMatrix(filter))
607    }
608    #[must_use]
609    pub fn blur(self, sigma: f32) -> Self {
610        self.with_effect(Effect::GaussianBlur(GaussianBlur::uniform(sigma)))
611    }
612    /// Fuses only adjacent color matrices. Spatial effects remain explicit so
613    /// their ordering and cache dependencies cannot be changed accidentally.
614    #[must_use]
615    pub fn optimized(&self) -> Self {
616        let mut out = Self::new();
617        for effect in &self.effects {
618            if let (Some(Effect::ColorMatrix(previous)), Effect::ColorMatrix(next)) =
619                (out.effects.last_mut(), effect)
620            {
621                *previous = previous.then(*next);
622            } else {
623                out.effects.push(*effect);
624            }
625        }
626        out
627    }
628    #[must_use]
629    pub fn fusion_count(&self) -> usize {
630        self.effects
631            .len()
632            .saturating_sub(self.optimized().effects.len())
633    }
634}
635impl DropShadowEffect {
636    #[must_use]
637    pub fn new(offset: Offset, sigma: f32, color: Color) -> Self {
638        Self {
639            offset: Offset::finite_or_zero(offset),
640            sigma_x: normalize_sigma(sigma),
641            sigma_y: normalize_sigma(sigma),
642            color,
643        }
644    }
645    #[must_use]
646    pub fn asymmetric(offset: Offset, sigma_x: f32, sigma_y: f32, color: Color) -> Self {
647        Self {
648            offset: Offset::finite_or_zero(offset),
649            sigma_x: normalize_sigma(sigma_x),
650            sigma_y: normalize_sigma(sigma_y),
651            color,
652        }
653    }
654    #[must_use]
655    pub fn blur(self) -> GaussianBlur {
656        GaussianBlur::new(self.sigma_x, self.sigma_y)
657    }
658}
659
660/// Canonical sigma normalization used by painting, widgets, and GPU
661/// lowering. Non-finite values follow the existing "invalid numeric input is
662/// transparent/zero" policy.
663#[must_use]
664pub fn normalize_sigma(sigma: f32) -> f32 {
665    finite_non_negative(sigma)
666}
667
668/// Conservative logical blur margin for the finite 3-sigma cutoff.
669#[must_use]
670pub fn blur_margin(sigma: f32) -> f32 {
671    (3. * normalize_sigma(sigma)).max(0.)
672}
673
674/// Expands source bounds by the finite Gaussian support in logical pixels.
675#[must_use]
676pub fn blur_bounds(source: Rect, sigma_x: f32, sigma_y: f32) -> Rect {
677    let mx = blur_margin(sigma_x);
678    let my = blur_margin(sigma_y);
679    Rect::from_origin_size(
680        Offset::new(source.origin.x - mx, source.origin.y - my),
681        Size::new(source.size.width + 2. * mx, source.size.height + 2. * my),
682    )
683}
684
685/// Bounds of a shadow plus the original source. The union is deliberately
686/// conservative and works for positive, negative, and fractional offsets.
687#[must_use]
688pub fn drop_shadow_bounds(source: Rect, offset: Offset, sigma_x: f32, sigma_y: f32) -> Rect {
689    union_rect(
690        source,
691        blur_bounds(
692            Rect::from_origin_size(source.origin + Offset::finite_or_zero(offset), source.size),
693            sigma_x,
694            sigma_y,
695        ),
696    )
697}
698
699/// A normalized, symmetric one-dimensional Gaussian kernel represented in
700/// order `[-radius, ..., 0, ..., +radius]`. The cutoff is exactly three
701/// physical sigma, and sigma zero is the identity kernel.
702#[must_use]
703pub fn gaussian_kernel_weights(sigma_physical: f32) -> Vec<f32> {
704    let sigma = normalize_sigma(sigma_physical);
705    if sigma <= f32::EPSILON {
706        return vec![1.];
707    }
708    let radius = (3. * sigma).ceil().max(1.) as i32;
709    let denominator = 2. * sigma * sigma;
710    let mut weights = (-radius..=radius)
711        .map(|i| (-(i * i) as f32 / denominator).exp())
712        .collect::<Vec<_>>();
713    let sum: f32 = weights.iter().sum();
714    if sum.is_finite() && sum > 0. {
715        for weight in &mut weights {
716            *weight /= sum;
717        }
718    } else {
719        weights.fill(0.);
720        weights[radius as usize] = 1.;
721    }
722    weights
723}
724
725/// Premultiplied shadow color for one blurred alpha sample. This helper is
726/// shared by deterministic CPU tests and documents the exact GPU equation.
727#[must_use]
728pub fn premultiplied_shadow_sample(color: Color, mask: f32) -> [f32; 4] {
729    let alpha = (f32::from(color.alpha) / 255.) * mask.clamp(0., 1.);
730    let [r, g, b, _] = color.to_linear_rgba();
731    [r * alpha, g * alpha, b * alpha, alpha]
732}