Skip to main content

vello_common/
filter_effects.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Filter effects API based on the W3C Filter Effects specification.
5//!
6//! This module provides a comprehensive filter system supporting both high-level
7//! CSS filter functions and low-level SVG filter primitives. The API is designed
8//! to follow the W3C Filter Effects Module Level 1 specification.
9//!
10//! See: <https://drafts.fxtf.org/filter-effects/>
11//!
12//! ## Implementation Status
13//!
14//! ### ✅ Implemented
15//!
16//! **Filter Functions:**
17//! - `Blur` - Gaussian blur effect
18//!
19//! **Filter Primitives (Single Use Only):**
20//! - `Flood` - Solid color fill
21//! - `GaussianBlur` - Gaussian blur filter
22//! - `DropShadow` - Drop shadow effect (compound primitive)
23//! - `DropShadowOnly` - Drop shadow effect without the original input
24//! - `Offset` - Translation/shift (single primitive)
25//!
26//! **Note:** Currently only single primitive filters are supported. Filter graphs with
27//! multiple connected primitives are not yet implemented.
28//!
29//! ### 🚧 Not Yet Implemented
30//!
31//! **Core Features:**
32//! - `FilterGraph` execution - Chaining multiple filter primitives together
33//! - `FilterInputs` - Connecting primitives to create complex effects
34//!
35//! **Filter Functions:**
36//! - `Brightness`, `Contrast`, `Grayscale`, `HueRotate`, `Invert`,
37//!   `Opacity`, `Saturate`, `Sepia`
38//!
39//! **Filter Primitives:**
40//! - `ColorMatrix` - Matrix-based color transformation
41//! - `Composite` - Porter-Duff compositing operations
42//! - `Blend` - Blend mode operations
43//! - `Morphology` - Dilate/erode operations
44//! - `ConvolveMatrix` - Custom convolution kernels
45//! - `Turbulence` - Perlin noise generation
46//! - `DisplacementMap` - Pixel displacement
47//! - `ComponentTransfer` - Per-channel transfer functions
48//! - `Image` - External image reference
49//! - `Tile` - Tiling operation
50//! - `DiffuseLighting`, `SpecularLighting` - Lighting effects
51
52use crate::color::{AlphaColor, Srgb};
53use crate::kurbo::{Affine, Rect, Vec2};
54use alloc::sync::Arc;
55use alloc::vec::Vec;
56use smallvec::SmallVec;
57
58/// The main filter system.
59///
60/// A filter combines a graph of filter primitives with optional spatial bounds.
61/// If bounds are specified, the filter only applies within that region.
62#[derive(Debug, Clone, PartialEq)]
63pub struct Filter {
64    /// Filter graph defining the effect pipeline.
65    pub graph: Arc<FilterGraph>,
66    // TODO: Add bounds restricting where the filter applies.
67    // Optional bounds restricting where the filter applies.
68    // If `None`, the filter applies to the entire filtered element.
69    // pub bounds: Option<Rect>,
70}
71
72impl Filter {
73    /// Create a simple filter system from a filter function.
74    ///
75    /// Converts a high-level CSS-style filter function into a filter graph.
76    /// Use this for simple effects like blur, brightness, etc.
77    pub fn from_function(function: FilterFunction) -> Self {
78        // Convert function to primitive
79        let primitive = match function {
80            FilterFunction::Blur { radius } => FilterPrimitive::GaussianBlur {
81                std_deviation: radius,
82                edge_mode: EdgeMode::default(),
83            },
84            _ => unimplemented!("Filter function {:?} not supported", function),
85        };
86
87        Self::from_primitive(primitive)
88    }
89
90    /// Create a filter system from a filter primitive.
91    ///
92    /// Creates a simple filter graph with a single primitive.
93    /// Use this for direct access to low-level SVG filter operations.
94    pub fn from_primitive(primitive: FilterPrimitive) -> Self {
95        let mut graph = FilterGraph::new();
96        let filter_id = graph.add(primitive, None);
97        graph.set_output(filter_id);
98
99        Self {
100            graph: Arc::new(graph),
101        }
102    }
103
104    // Note: We could simplify this by just returning a single union rect and combining
105    // `filter_expansion` and `source_expansion`. However, they are conceptually different
106    // and therefore worth being treated separately. For example, if we combined them, we'd
107    // end up computing the filter effect for the source expansion area as well, even though
108    // it's not necessary there since the filter itself doesn't touch that area.
109
110    /// Calculate how far the filtered output can extend beyond the source content.
111    ///
112    /// The returned `Rect` is an expansion around the source content bounds, expressed
113    /// in device space after applying the linear part of `transform`. Negative `x0`/`y0`
114    /// values expand to the left/top; positive `x1`/`y1` values expand to the right/bottom.
115    ///
116    /// # Arguments
117    /// * `transform` - The transform applied to this filter layer
118    pub fn filter_expansion(&self, transform: &Affine) -> Rect {
119        let [a, b, c, d, ..] = transform.as_coeffs();
120        let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]);
121
122        self.graph.filter_expansion(&linear_only)
123    }
124
125    /// Calculate how far the source input must extend outside the visible source bounds
126    /// to render the filter correctly.
127    ///
128    /// In most cases (for example for Gaussian blurs), this is the same as
129    /// [`Filter::filter_expansion`]. However, it isn't the same for drop shadows.
130    /// Let's say we have a drop shadow of dx = 20 and dy = 20. In this case,
131    /// the filter expansion rect will be [0, 0, 20, 20], meaning we need to extend
132    /// our pixmap 20 pixels to the right/bottom to fully render the filter effect.
133    /// [`Filter::source_expansion`] will instead return [-20, -20, 0, 0]. For example,
134    /// let's say the rect is placed in such a way that the drop shadow starts at (0, 0).
135    /// In this case, we need to expand the source area by twenty pixels to the _top/left_
136    /// to ensure that the drop shadow is not cut off.
137    pub fn source_expansion(&self, transform: &Affine) -> Rect {
138        let [a, b, c, d, ..] = transform.as_coeffs();
139        let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]);
140
141        self.graph.source_expansion(&linear_only)
142    }
143}
144
145/// A directed acyclic graph (DAG) of filter operations.
146///
147/// The graph represents a pipeline of filter primitives where outputs of some
148/// primitives can be used as inputs to others. Each primitive has a unique `FilterId`.
149#[derive(Debug, Clone, PartialEq)]
150pub struct FilterGraph {
151    /// All filter primitives in the graph, stored in insertion order.
152    pub primitives: SmallVec<[FilterPrimitive; 1]>,
153    /// The final output filter ID whose result is the output of this graph.
154    pub output: FilterId,
155    /// Next available filter ID (monotonically increasing counter).
156    next_id: u16,
157    /// Accumulated filter expansion from all primitives in the graph, cached in user space.
158    filter_expansion: Rect,
159    /// Accumulated source expansion from all primitives in the graph, cached in user space.
160    source_expansion: Rect,
161}
162
163impl Default for FilterGraph {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl FilterGraph {
170    /// Create a new empty filter graph.
171    pub fn new() -> Self {
172        Self {
173            primitives: SmallVec::new(),
174            output: FilterId(0),
175            next_id: 0,
176            filter_expansion: Rect::ZERO,
177            source_expansion: Rect::ZERO,
178        }
179    }
180
181    /// Add a filter primitive with optional inputs.
182    ///
183    /// Returns a `FilterId` that can be referenced by other primitives.
184    /// Automatically updates the accumulated source and filter expansion requirements.
185    pub fn add(&mut self, primitive: FilterPrimitive, _inputs: Option<FilterInputs>) -> FilterId {
186        let id = FilterId(self.next_id);
187        self.next_id += 1;
188
189        self.filter_expansion = self.filter_expansion.union(primitive.filter_expansion());
190        self.source_expansion = self.source_expansion.union(primitive.source_expansion());
191
192        self.primitives.push(primitive);
193
194        id
195    }
196
197    /// Set the output filter for the graph.
198    pub fn set_output(&mut self, output: FilterId) {
199        self.output = output;
200    }
201
202    /// The filter expansion of all filters in the graph, see [`Filter::filter_expansion`].
203    pub fn filter_expansion(&self, transform: &Affine) -> Rect {
204        transform.transform_rect_bbox(self.filter_expansion)
205    }
206
207    /// The source expansion of all filters in the graph, see [`Filter::source_expansion`].
208    pub fn source_expansion(&self, transform: &Affine) -> Rect {
209        transform.transform_rect_bbox(self.source_expansion)
210    }
211}
212
213/// All possible filter effects.
214///
215/// This enum allows choosing between high-level filter functions (simple CSS-style effects)
216/// and low-level filter primitives (complex SVG-style effects with full control).
217/// Use `FilterFunction` for common effects like blur, and `FilterPrimitive` for
218/// advanced composition and custom filter graphs.
219#[derive(Debug, Clone)]
220pub enum FilterEffect {
221    /// Simple, high-level filter functions.
222    Function(FilterFunction),
223    /// Low-level filter primitives (granular control).
224    Primitive(FilterPrimitive),
225}
226
227/// High-level filter functions for common effects (CSS filter functions).
228///
229/// These match the CSS Filter Effects specification and provide simple,
230/// commonly-used visual effects without needing to construct a filter graph.
231///
232/// See: <https://drafts.fxtf.org/filter-effects/#filter-functions>
233#[derive(Debug, Clone)]
234pub enum FilterFunction {
235    /// Gaussian blur effect.
236    ///
237    /// Applies a Gaussian blur to the input image. Larger radius values
238    /// produce more blur. The blur is applied equally in all directions.
239    ///
240    /// Note: Per the W3C Filter Effects specification, this `radius` parameter
241    /// represents the standard deviation (σ) of the Gaussian function, not the
242    /// effective blur range. The effective blur range is approximately 3× this value.
243    Blur {
244        /// Standard deviation of the Gaussian blur in pixels. Must be non-negative.
245        /// A value of 0 means no blur.
246        ///
247        /// Despite being called "radius" (to match CSS filter syntax), this is
248        /// actually the standard deviation. The visible blur effect extends
249        /// approximately 3 times this value in each direction.
250        radius: f32,
251    },
252    //
253    // ============================================================
254    // TODO: The following filter functions are not yet implemented
255    // ============================================================
256    //
257    /// Brightness adjustment.
258    ///
259    /// Adjusts the brightness of the input image using a linear multiplier.
260    Brightness {
261        /// Brightness amount: 0.0 = completely black, 1.0 = no change, >1.0 = brighter.
262        /// Must be non-negative.
263        amount: f32,
264    },
265    /// Contrast adjustment.
266    ///
267    /// Adjusts the contrast of the input image.
268    Contrast {
269        /// Contrast amount: 0.0 = uniform gray, 1.0 = no change, >1.0 = higher contrast.
270        /// Must be non-negative.
271        amount: f32,
272    },
273    /// Grayscale conversion.
274    ///
275    /// Converts the input to grayscale. Amount controls the strength of the conversion.
276    Grayscale {
277        /// Grayscale amount: 0.0 = original colors, 1.0 = full grayscale.
278        /// Values should be in range [0.0, 1.0].
279        amount: f32,
280    },
281    /// Hue rotation.
282    ///
283    /// Rotates the hue of all colors in the input image by the specified angle.
284    HueRotate {
285        /// Rotation angle in degrees. Can be negative.
286        /// 0° = no change, 180° = opposite hue, 360° = back to original.
287        angle: f32,
288    },
289    /// Color inversion.
290    ///
291    /// Inverts the colors of the input image.
292    Invert {
293        /// Inversion amount: 0.0 = original colors, 1.0 = fully inverted.
294        /// Values should be in range [0.0, 1.0].
295        amount: f32,
296    },
297    /// Opacity adjustment.
298    ///
299    /// Multiplies the alpha channel by the specified amount.
300    Opacity {
301        /// Opacity amount: 0.0 = fully transparent, 1.0 = no change.
302        /// Values should be in range [0.0, 1.0].
303        amount: f32,
304    },
305    /// Saturation adjustment.
306    ///
307    /// Adjusts the color saturation of the input image.
308    Saturate {
309        /// Saturation amount: 0.0 = completely desaturated (grayscale),
310        /// 1.0 = no change, >1.0 = oversaturated.
311        /// Must be non-negative.
312        amount: f32,
313    },
314    /// Sepia tone effect.
315    ///
316    /// Applies a sepia tone effect (vintage/old photo appearance).
317    Sepia {
318        /// Sepia amount: 0.0 = original colors, 1.0 = full sepia tone.
319        /// Values should be in range [0.0, 1.0].
320        amount: f32,
321    },
322}
323
324/// Edge mode for filter operations.
325///
326/// Determines how to extend the input image when filter operations require sampling
327/// beyond the original image boundaries. This is particularly important for blur and
328/// convolution operations near edges.
329///
330/// See: <https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-primitive-edgemode>
331#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
332pub enum EdgeMode {
333    /// Extend by duplicating edge pixels (clamp to edge).
334    ///
335    /// The input image is extended along each border by replicating the color values
336    /// at the given edge of the input image. This prevents dark halos around edges.
337    Duplicate,
338    /// Extend by wrapping to the opposite edge (repeat/tile).
339    ///
340    /// The input image is extended by taking color values from the opposite edge,
341    /// creating a tiling effect.
342    Wrap,
343    /// Extend by mirroring across the edge.
344    ///
345    /// The input image is extended by taking color values mirrored across the edge.
346    /// This creates seamless continuation at boundaries.
347    Mirror,
348    /// Extend with transparent black (zeros).
349    ///
350    /// The input image is extended with pixel values of zero for R, G, B and A.
351    /// This is the default and most common mode, creating natural fade-to-transparent edges.
352    #[default]
353    None,
354}
355
356/// Low-level filter primitives for granular control (SVG filter primitives).
357///
358/// These are the building blocks for complex filter effects, corresponding to SVG
359/// filter primitives. They can be combined in a `FilterGraph` to create sophisticated
360/// visual effects.
361///
362/// See: <https://drafts.fxtf.org/filter-effects/#FilterPrimitivesOverview>
363#[derive(Debug, Clone, PartialEq)]
364pub enum FilterPrimitive {
365    /// Generate a solid color fill.
366    ///
367    /// Creates a rectangle filled with the specified color, typically used as
368    /// input to other filter operations (e.g., for colored shadows).
369    Flood {
370        /// Fill color with alpha channel.
371        color: AlphaColor<Srgb>,
372    },
373    /// Gaussian blur filter.
374    ///
375    /// Applies a Gaussian blur using the specified standard deviation (σ).
376    /// The effective blur range (distance over which pixels are sampled) is
377    /// approximately 3 × `std_deviation`, as this captures ~99.7% of the
378    /// Gaussian distribution.
379    GaussianBlur {
380        /// Standard deviation for the blur kernel. Larger values create more blur.
381        /// Must be non-negative. A value of 0 means no blur.
382        ///
383        /// This directly corresponds to the σ (sigma) parameter in the Gaussian
384        /// function. The visible blur effect extends approximately 3σ in each direction.
385        ///
386        /// TODO: Per the W3C specification, this should support separate x and y values.
387        /// The spec allows `stdDeviation` to be either one number (applied to both axes)
388        /// or two numbers (first for x-axis, second for y-axis). Currently only uniform
389        /// blur is supported. Consider changing to `(f32, f32)` or a dedicated type.
390        std_deviation: f32,
391        /// Edge mode determining how pixels beyond the input bounds are handled.
392        edge_mode: EdgeMode,
393    },
394    /// Drop shadow effect (compound primitive).
395    ///
396    /// Creates a drop shadow by blurring the input's alpha channel, offsetting it,
397    /// and compositing it with the original. This is a compound operation that
398    /// combines multiple primitive operations into one.
399    ///
400    /// See: <https://drafts.fxtf.org/filter-effects-2/#feDropShadowElement>
401    DropShadow {
402        /// Horizontal offset of the shadow in pixels. Positive values shift right.
403        dx: f32,
404        /// Vertical offset of the shadow in pixels. Positive values shift down.
405        dy: f32,
406        /// Blur standard deviation for the shadow. Larger values create softer shadows.
407        std_deviation: f32,
408        /// Shadow color with alpha channel. Alpha controls shadow opacity.
409        color: AlphaColor<Srgb>,
410        /// Edge mode for handling boundaries during blur operation.
411        /// Default is `EdgeMode::None` per SVG spec.
412        edge_mode: EdgeMode,
413    },
414    /// Same as [`FilterPrimitive::DropShadow`], but without compositing the original
415    /// layer on top of it.
416    DropShadowOnly {
417        /// Horizontal offset of the shadow in pixels. Positive values shift right.
418        dx: f32,
419        /// Vertical offset of the shadow in pixels. Positive values shift down.
420        dy: f32,
421        /// Blur standard deviation for the shadow. Larger values create softer shadows.
422        std_deviation: f32,
423        /// Color applied to the blurred, offset input alpha mask. The input's color
424        /// channels are ignored, and this color's alpha controls shadow opacity.
425        color: AlphaColor<Srgb>,
426        /// Edge mode for handling boundaries during blur operation.
427        edge_mode: EdgeMode,
428    },
429    //
430    // ============================================================
431    // TODO: The following filter primitives are not yet implemented
432    // ============================================================
433    //
434    /// Matrix-based color transformation.
435    ///
436    /// Applies a 4x5 matrix transformation to colors, allowing arbitrary
437    /// color space transformations, hue shifts, and color adjustments.
438    ColorMatrix {
439        /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset).
440        /// Each output channel is computed as a linear combination of input channels plus offset.
441        matrix: [f32; 20],
442    },
443    /// Geometric offset/translation.
444    ///
445    /// Shifts the input image by the specified offset. Useful for creating
446    /// shadow effects or positioning elements in a filter graph.
447    Offset {
448        /// Horizontal offset in pixels. Positive values shift right.
449        dx: f32,
450        /// Vertical offset in pixels. Positive values shift down.
451        dy: f32,
452    },
453
454    /// Composite two inputs using Porter-Duff compositing operations.
455    ///
456    /// Combines two input images using standard compositing operators
457    /// (over, in, out, atop, xor) or custom arithmetic combination.
458    Composite {
459        /// Porter-Duff compositing operator to apply.
460        operator: CompositeOperator,
461    },
462    /// Blend two inputs using blend modes.
463    ///
464    /// Combines two input images using Photoshop-style blend modes
465    /// (multiply, screen, overlay, etc.).
466    Blend {
467        /// Blend mode determining how colors are combined.
468        mode: BlendMode,
469    },
470    /// Morphological operations (dilate/erode).
471    ///
472    /// Expands (dilate) or contracts (erode) the shapes in the input image.
473    /// Useful for creating outline effects or cleaning up edges.
474    Morphology {
475        /// Morphological operator determining whether to erode or dilate.
476        operator: MorphologyOperator,
477        /// Operation radius in pixels. Larger values create stronger effects.
478        radius: f32,
479    },
480    /// Custom convolution kernel for image processing.
481    ///
482    /// Applies a custom convolution matrix to the input image, enabling
483    /// effects like sharpening, edge detection, embossing, and custom filters.
484    ConvolveMatrix {
485        /// Convolution kernel specification including size, values, and normalization.
486        kernel: ConvolutionKernel,
487    },
488    /// Generate Perlin noise/turbulence patterns.
489    ///
490    /// Creates procedural noise patterns useful for textures, clouds,
491    /// marble effects, and other organic-looking randomness.
492    Turbulence {
493        /// Base frequency for noise generation. Higher values create finer detail.
494        base_frequency: f32,
495        /// Number of octaves for fractal noise. More octaves add finer detail.
496        num_octaves: u32,
497        /// Random seed for reproducible noise generation.
498        seed: u32,
499        /// Type of noise: smooth fractal or more chaotic turbulence.
500        turbulence_type: TurbulenceType,
501    },
502    /// Displace pixels using a displacement map.
503    ///
504    /// Uses the color values from a second input to spatially displace pixels
505    /// in the primary input, creating warping and distortion effects.
506    DisplacementMap {
507        /// Scale factor controlling the displacement intensity.
508        scale: f32,
509        /// Color channel from the displacement map used for X-axis displacement.
510        x_channel: ColorChannel,
511        /// Color channel from the displacement map used for Y-axis displacement.
512        y_channel: ColorChannel,
513    },
514    /// Per-channel component transfer using lookup tables or functions.
515    ///
516    /// Applies independent transfer functions to each color channel,
517    /// enabling color corrections, gamma adjustments, and custom mappings.
518    ComponentTransfer {
519        /// Transfer function applied to the red channel (None = identity).
520        red_function: Option<TransferFunction>,
521        /// Transfer function applied to the green channel (None = identity).
522        green_function: Option<TransferFunction>,
523        /// Transfer function applied to the blue channel (None = identity).
524        blue_function: Option<TransferFunction>,
525        /// Transfer function applied to the alpha channel (None = identity).
526        alpha_function: Option<TransferFunction>,
527    },
528    /// Reference an external image as filter input.
529    ///
530    /// Allows using pre-existing images (from an atlas or resource) as
531    /// input to filter operations, useful for texturing and overlays.
532    Image {
533        /// Identifier referencing an image in the resource atlas.
534        image_id: u32,
535        /// Optional 2D affine transformation matrix [a, b, c, d, e, f].
536        /// Transforms the image before using it as filter input.
537        transform: Option<[f32; 6]>,
538    },
539    /// Tile the input to fill the filter region.
540    ///
541    /// Repeats the input image to fill the entire filter primitive subregion,
542    /// creating a tiling/repeating pattern.
543    Tile,
544    /// Diffuse lighting simulation.
545    ///
546    /// Creates a lighting effect by treating the input's alpha channel as a height map
547    /// and calculating diffuse (matte) reflection from a light source.
548    DiffuseLighting {
549        /// Surface scale factor for converting alpha values to heights.
550        surface_scale: f32,
551        /// Diffuse reflection constant (kd). Controls lighting intensity.
552        diffuse_constant: f32,
553        /// Kernel unit length for gradient calculations in user space.
554        kernel_unit_length: f32,
555        /// Configuration of the light source (point, distant, or spot).
556        light_source: LightSource,
557    },
558    /// Specular lighting simulation.
559    ///
560    /// Creates a lighting effect by treating the input's alpha channel as a height map
561    /// and calculating specular (shiny) reflection highlights from a light source.
562    SpecularLighting {
563        /// Surface scale factor for converting alpha values to heights.
564        surface_scale: f32,
565        /// Specular reflection constant (ks). Controls highlight intensity.
566        specular_constant: f32,
567        /// Specular reflection exponent. Controls highlight sharpness (higher = sharper).
568        specular_exponent: f32,
569        /// Kernel unit length for gradient calculations in user space.
570        kernel_unit_length: f32,
571        /// Configuration of the light source (point, distant, or spot).
572        light_source: LightSource,
573    },
574}
575
576impl FilterPrimitive {
577    /// The filter expansion of the primitive, see [`Filter::filter_expansion`].
578    pub fn filter_expansion(&self) -> Rect {
579        match self {
580            Self::GaussianBlur { std_deviation, .. } => {
581                let radius = blur_radius(*std_deviation);
582                Rect::new(-radius, -radius, radius, radius)
583            }
584            Self::Offset { dx, dy } => {
585                // Offset shifts pixels; expand bounds asymmetrically so shifted content isn't cut.
586                let dx = *dx as f64;
587                let dy = *dy as f64;
588                Rect::new(dx.min(0.0), dy.min(0.0), dx.max(0.0), dy.max(0.0))
589            }
590            Self::DropShadow {
591                std_deviation,
592                dx,
593                dy,
594                ..
595            }
596            | Self::DropShadowOnly {
597                std_deviation,
598                dx,
599                dy,
600                ..
601            } => {
602                let blur_radius = blur_radius(*std_deviation);
603                let dx = f64::from(*dx);
604                let dy = f64::from(*dy);
605
606                Rect::new(
607                    (dx - blur_radius).min(0.0),
608                    (dy - blur_radius).min(0.0),
609                    (dx + blur_radius).max(0.0),
610                    (dy + blur_radius).max(0.0),
611                )
612            }
613            // Most other filters don't expand bounds
614            _ => Rect::ZERO,
615        }
616    }
617
618    /// The source expansion of the primitive, see [`Filter::source_expansion`].
619    pub fn source_expansion(&self) -> Rect {
620        match self {
621            Self::Offset { dx, dy } => {
622                self.filter_expansion() - Vec2::new(f64::from(*dx), f64::from(*dy))
623            }
624            Self::DropShadow {
625                std_deviation,
626                dx,
627                dy,
628                ..
629            }
630            | Self::DropShadowOnly {
631                std_deviation,
632                dx,
633                dy,
634                ..
635            } => {
636                let blur_radius = blur_radius(*std_deviation);
637                let dx = -f64::from(*dx);
638                let dy = -f64::from(*dy);
639
640                Rect::new(
641                    (dx - blur_radius).min(0.0),
642                    (dy - blur_radius).min(0.0),
643                    (dx + blur_radius).max(0.0),
644                    (dy + blur_radius).max(0.0),
645                )
646            }
647            _ => self.filter_expansion(),
648        }
649    }
650}
651
652fn blur_radius(std_deviation: f32) -> f64 {
653    // Gaussian blur expands uniformly by 3*sigma (covers 99.7% of distribution)
654    f64::from(std_deviation * 3.0)
655}
656
657#[cfg(test)]
658mod expansion_tests {
659    use super::FilterPrimitive;
660    use crate::color::palette::css::RED;
661    use crate::filter_effects::EdgeMode;
662    use crate::kurbo::Rect;
663
664    #[test]
665    fn offset_expands_in_direction_of_shift() {
666        let p = FilterPrimitive::Offset { dx: 2.5, dy: -3.0 };
667        assert_eq!(
668            p.filter_expansion(),
669            Rect::new(0.0, -3.0, 2.5, 0.0),
670            "Offset expansion should be asymmetric and include the shift vector"
671        );
672    }
673
674    #[test]
675    fn drop_shadow_expansion_combines_blur_and_offset_tightly() {
676        let p = FilterPrimitive::DropShadow {
677            dx: 20.0,
678            dy: -10.0,
679            std_deviation: 8.0,
680            color: RED,
681            edge_mode: EdgeMode::None,
682        };
683
684        assert_eq!(p.filter_expansion(), Rect::new(-4.0, -34.0, 44.0, 14.0));
685        assert_eq!(p.source_expansion(), Rect::new(-44.0, -14.0, 4.0, 34.0));
686    }
687}
688
689/// Unique identifier for a filter primitive in the graph.
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
691pub struct FilterId(pub u16);
692
693/// Input connections for a filter primitive.
694#[derive(Debug, Clone, PartialEq)]
695pub struct FilterInputs {
696    /// Primary input ("in" attribute in SVG).
697    pub primary: FilterInput,
698    /// Secondary input ("in2" attribute in SVG, for composite/blend operations).
699    pub secondary: Option<FilterInput>,
700}
701
702impl FilterInputs {
703    /// Create filter inputs with a single input.
704    ///
705    /// Use this for primitives that operate on a single source (blur, color matrix, etc.).
706    pub fn single(input: FilterInput) -> Self {
707        Self {
708            primary: input,
709            secondary: None,
710        }
711    }
712
713    /// Create filter inputs with two inputs (for composite, blend, etc.).
714    ///
715    /// Use this for primitives that combine two sources (composite, blend, displacement map, etc.).
716    pub fn dual(input1: FilterInput, input2: FilterInput) -> Self {
717        Self {
718            primary: input1,
719            secondary: Some(input2),
720        }
721    }
722}
723
724/// A single filter input.
725#[derive(Debug, Clone, PartialEq)]
726pub enum FilterInput {
727    /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.).
728    Source(FilterSource),
729    /// Input from another filter's result.
730    Result(FilterId),
731}
732
733/// Filter input sources.
734///
735/// Defines the various built-in sources that can be used as filter inputs,
736/// matching the SVG filter primitive input types. These represent implicit
737/// inputs available to any filter primitive without requiring previous operations.
738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub enum FilterSource {
740    /// The original graphic content being filtered.
741    ///
742    /// This is the default input - the rendered result of the element
743    /// the filter is applied to, including all its fill, stroke, and content.
744    SourceGraphic,
745    /// Alpha channel only of the original graphic.
746    ///
747    /// Useful for creating effects based on shape/transparency, such as
748    /// shadows that follow the element's outline.
749    SourceAlpha,
750    /// Background image content behind the filtered element.
751    ///
752    /// Allows filters to incorporate or blend with content behind the element.
753    /// Not always available depending on the rendering context.
754    BackgroundImage,
755    /// Alpha channel only of the background image.
756    ///
757    /// The transparency mask of the background content.
758    BackgroundAlpha,
759    /// The fill paint of the element as an image input.
760    ///
761    /// For elements with gradient or pattern fills, this provides access
762    /// to the fill as a filter input.
763    FillPaint,
764    /// The stroke paint of the element as an image input.
765    ///
766    /// For elements with gradient or pattern strokes, this provides access
767    /// to the stroke as a filter input.
768    StrokePaint,
769}
770
771/// Pre-built compound effects for common use cases.
772///
773/// These effects combine multiple filter primitives into commonly-used visual effects.
774/// They provide a convenient high-level API for complex multi-step filter operations.
775///
776/// **Note:** These are planned but not yet implemented. Use `FilterGraph` to manually
777/// construct these effects from primitives.
778#[derive(Debug, Clone)]
779pub enum CompoundFilter {
780    /// Inner shadow effect (shadow inside the shape).
781    ///
782    /// Creates a shadow that appears inside the boundaries of the shape,
783    /// giving a recessed or inset appearance. This is the opposite of a drop shadow.
784    InnerShadow {
785        /// Horizontal offset of the shadow in pixels. Positive values shift right.
786        dx: f32,
787        /// Vertical offset of the shadow in pixels. Positive values shift down.
788        dy: f32,
789        /// Blur radius for the shadow in pixels. Larger values create softer shadows.
790        blur: f32,
791        /// Shadow color with alpha channel.
792        color: AlphaColor<Srgb>,
793    },
794    /// Glow effect around the shape.
795    ///
796    /// Creates a soft glowing halo around the shape by blurring and
797    /// compositing a colored version with the original.
798    Glow {
799        /// Blur radius for the glow in pixels. Larger values create softer glows.
800        blur: f32,
801        /// Glow color with alpha channel.
802        color: AlphaColor<Srgb>,
803    },
804    /// Bevel effect (3D raised/recessed appearance).
805    ///
806    /// Creates a 3D beveled edge effect using lighting simulation,
807    /// making the shape appear raised or recessed from the surface.
808    Bevel {
809        /// Light source angle in degrees (0° = right, 90° = up).
810        angle: f32,
811        /// Width of the bevel edge in pixels.
812        distance: f32,
813        /// Color for the highlight (lit) side of the bevel.
814        highlight: AlphaColor<Srgb>,
815        /// Color for the shadow (dark) side of the bevel.
816        shadow: AlphaColor<Srgb>,
817    },
818    /// Emboss effect for a raised relief appearance.
819    ///
820    /// Creates an embossed/stamped appearance by simulating lighting
821    /// on a raised surface based on the shape's alpha channel.
822    Emboss {
823        /// Light angle in degrees determining emboss direction.
824        angle: f32,
825        /// Depth of the emboss effect.
826        depth: f32,
827        /// Overall strength/intensity of the effect (0.0 = none, 1.0 = full).
828        amount: f32,
829    },
830}
831
832/// Composite operators for combining filter inputs.
833///
834/// These are the Porter-Duff compositing operators used to combine two images.
835/// Each operator defines how the source (input 1) and destination (input 2)
836/// are combined based on their color and alpha values.
837#[derive(Debug, Clone, Copy, PartialEq)]
838pub enum CompositeOperator {
839    /// Source over destination (standard alpha blending).
840    ///
841    /// The source is composited over the destination. This is the most common
842    /// blending mode where source alpha determines visibility.
843    Over,
844    /// Source in destination (intersection).
845    ///
846    /// The source is only visible where the destination is opaque.
847    /// Result alpha = `source_alpha` × `dest_alpha`.
848    In,
849    /// Source out destination (subtract).
850    ///
851    /// The source is only visible where the destination is transparent.
852    /// Useful for masking/cutting out regions.
853    Out,
854    /// Source atop destination.
855    ///
856    /// Source is composited over destination, but only where destination is opaque.
857    Atop,
858    /// Source XOR destination (exclusive or).
859    ///
860    /// Shows source where destination is transparent and vice versa,
861    /// but not where both are opaque.
862    Xor,
863    /// Arithmetic combination with custom coefficients.
864    ///
865    /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4.
866    /// Allows creating custom compositing operations beyond the standard Porter-Duff set.
867    Arithmetic {
868        /// Coefficient k1 for the (source * destination) term.
869        k1: f32,
870        /// Coefficient k2 for the source term.
871        k2: f32,
872        /// Coefficient k3 for the destination term.
873        k3: f32,
874        /// Constant offset k4 added to the result.
875        k4: f32,
876    },
877}
878
879/// Blend modes for combining colors.
880///
881/// These are blend modes that define how to combine the colors
882/// of two layers. Unlike compositing operators which deal with alpha, blend modes
883/// focus on color mixing while preserving the compositing behavior.
884///
885/// See: <https://drafts.fxtf.org/compositing/#blending>
886pub type BlendMode = peniko::Mix;
887
888/// Morphological operators for dilate/erode operations.
889///
890/// These operators modify the shape of objects by expanding or contracting them.
891/// They work by examining neighborhoods of pixels and applying min/max operations.
892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893pub enum MorphologyOperator {
894    /// Erode operation (shrink/thin shapes).
895    ///
896    /// Makes objects smaller by removing pixels at the edges. Takes the minimum
897    /// value in the neighborhood. Useful for removing noise or separating touching objects.
898    Erode,
899    /// Dilate operation (expand/thicken shapes).
900    ///
901    /// Makes objects larger by adding pixels at the edges. Takes the maximum
902    /// value in the neighborhood. Useful for filling holes or connecting nearby objects.
903    Dilate,
904}
905
906/// Convolution kernel for custom filtering operations.
907///
908/// Defines a square matrix of weights used for convolution-based image processing.
909/// The kernel is applied to each pixel by multiplying surrounding pixels by the weights,
910/// summing the results, dividing by the divisor, and adding the bias.
911#[derive(Debug, Clone, PartialEq)]
912pub struct ConvolutionKernel {
913    /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5).
914    /// The kernel must be square, so this defines both width and height.
915    pub size: u32,
916    /// Kernel weight values in row-major order.
917    /// Length must equal size × size. Center of kernel is typically at (size/2, size/2).
918    pub values: Vec<f32>,
919    /// Normalization divisor applied to the convolution result.
920    /// Common practice is to use the sum of all weights for averaging, or 1.0 otherwise.
921    pub divisor: f32,
922    /// Bias value added to the result after normalization.
923    /// Useful for edge detection or emboss effects to shift the result range.
924    pub bias: f32,
925    /// Whether to preserve the alpha channel unchanged.
926    /// If true, convolution only applies to RGB; if false, it applies to RGBA.
927    pub preserve_alpha: bool,
928}
929
930/// Types of turbulence noise generation.
931///
932/// Determines the algorithm used for generating procedural noise patterns.
933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934pub enum TurbulenceType {
935    /// Fractal noise (smooth, natural-looking Perlin noise).
936    ///
937    /// Creates smooth, continuous patterns suitable for natural textures
938    /// like clouds, marble, wood grain, or terrain.
939    FractalNoise,
940    /// Turbulence noise (more chaotic and energetic).
941    ///
942    /// Creates more chaotic patterns with sharper transitions,
943    /// suitable for fire, smoke, or turbulent effects.
944    Turbulence,
945}
946
947/// Color channels for displacement mapping and channel selection.
948///
949/// Specifies which color channel to use for operations that need to
950/// extract or reference individual channels from an image.
951#[derive(Debug, Clone, Copy, PartialEq, Eq)]
952pub enum ColorChannel {
953    /// Red color channel (R component).
954    Red,
955    /// Green color channel (G component).
956    Green,
957    /// Blue color channel (B component).
958    Blue,
959    /// Alpha channel (transparency/opacity).
960    Alpha,
961}
962
963/// Transfer functions for component transfer operations.
964///
965/// These functions map input color channel values to output values,
966/// enabling gamma correction, color grading, and custom color curves.
967/// Input and output values are typically in the range [0, 1].
968#[derive(Debug, Clone, PartialEq)]
969pub enum TransferFunction {
970    /// Identity function (output = input, no change).
971    Identity,
972    /// Table lookup with linear interpolation.
973    ///
974    /// Maps input values using a lookup table with linear interpolation between entries.
975    /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate.
976    Table {
977        /// Lookup table values defining the transfer curve.
978        /// More values provide smoother curves. Minimum 2 values required.
979        values: Vec<f32>,
980    },
981    /// Discrete step function (posterization).
982    ///
983    /// Maps input to discrete output values without interpolation, creating step/banding effects.
984    /// Each segment gets a constant output value from the table.
985    Discrete {
986        /// Step values for each discrete output level.
987        /// Input range is divided into len(values) segments, each mapping to one value.
988        values: Vec<f32>,
989    },
990    /// Linear function: output = slope × input + intercept.
991    ///
992    /// Simple linear transformation of the input value.
993    Linear {
994        /// Slope coefficient (rate of change).
995        slope: f32,
996        /// Intercept offset (constant added to result).
997        intercept: f32,
998    },
999    /// Gamma correction: output = amplitude × input^exponent + offset.
1000    ///
1001    /// Applies power-law transformation, commonly used for gamma correction and
1002    /// adjusting midtone brightness without affecting blacks or whites.
1003    Gamma {
1004        /// Amplitude multiplier applied to the result.
1005        amplitude: f32,
1006        /// Gamma exponent (< 1 brightens, > 1 darkens midtones).
1007        exponent: f32,
1008        /// Offset added to the final result.
1009        offset: f32,
1010    },
1011}
1012
1013/// Light source configurations for lighting effects.
1014///
1015/// Defines different types of light sources used in diffuse and specular lighting
1016/// filter primitives. Each type has different characteristics and use cases.
1017#[derive(Debug, Clone, PartialEq)]
1018pub enum LightSource {
1019    /// Distant light source (infinitely far away, like the sun).
1020    ///
1021    /// All rays are parallel, creating uniform lighting across the surface.
1022    /// Direction is specified using spherical coordinates (azimuth and elevation).
1023    Distant {
1024        /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up).
1025        /// Defines the horizontal direction of the light.
1026        azimuth: f32,
1027        /// Elevation angle in degrees (0° = horizon, 90° = directly overhead).
1028        /// Defines the vertical angle of the light source.
1029        elevation: f32,
1030    },
1031    /// Point light source at a specific 3D position.
1032    ///
1033    /// Light radiates uniformly in all directions from a single point.
1034    /// Intensity decreases with distance. Like a light bulb.
1035    Point {
1036        /// Light source X coordinate in user space.
1037        x: f32,
1038        /// Light source Y coordinate in user space.
1039        y: f32,
1040        /// Light source Z coordinate (height above the surface).
1041        /// Larger values create softer lighting across larger areas.
1042        z: f32,
1043    },
1044    /// Spot light with position, direction, and cone angle.
1045    ///
1046    /// Light emanates from a point in a specific direction with limited spread.
1047    /// Like a flashlight or stage spotlight with adjustable focus.
1048    Spot {
1049        /// Light source X coordinate in user space.
1050        x: f32,
1051        /// Light source Y coordinate in user space.
1052        y: f32,
1053        /// Light source Z coordinate (height above the surface).
1054        z: f32,
1055        /// X coordinate the spotlight is aimed at.
1056        points_at_x: f32,
1057        /// Y coordinate the spotlight is aimed at.
1058        points_at_y: f32,
1059        /// Z coordinate the spotlight is aimed at.
1060        points_at_z: f32,
1061        /// Specular exponent controlling the focus/sharpness of the spotlight beam.
1062        /// Higher values create tighter, more focused beams.
1063        specular_exponent: f32,
1064        /// Optional cone angle in degrees limiting the spotlight spread.
1065        /// If None, the light spreads based only on the specular exponent.
1066        limiting_cone_angle: Option<f32>,
1067    },
1068}
1069
1070/// Common color transformation matrices.
1071///
1072/// These 4x5 matrices are used with the `ColorMatrix` filter primitive.
1073/// Each row transforms a color channel: [R, G, B, A, offset].
1074pub mod matrices {
1075    /// Identity matrix (no change).
1076    pub const IDENTITY: [f32; 20] = [
1077        1.0, 0.0, 0.0, 0.0, 0.0, // Red
1078        0.0, 1.0, 0.0, 0.0, 0.0, // Green
1079        0.0, 0.0, 1.0, 0.0, 0.0, // Blue
1080        0.0, 0.0, 0.0, 1.0, 0.0, // Alpha
1081    ];
1082
1083    /// Extract alpha channel to RGB (for shadow effects).
1084    pub const ALPHA_TO_BLACK: [f32; 20] = [
1085        0.0, 0.0, 0.0, 1.0, 0.0, // Red = Alpha
1086        0.0, 0.0, 0.0, 1.0, 0.0, // Green = Alpha
1087        0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha
1088        0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha
1089    ];
1090
1091    /// Grayscale conversion matrix using luminosity weights.
1092    pub const GRAYSCALE: [f32; 20] = [
1093        0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red
1094        0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green
1095        0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue
1096        0.0, 0.0, 0.0, 1.0, 0.0, // Alpha
1097    ];
1098
1099    /// Sepia tone matrix for vintage photo effect.
1100    pub const SEPIA: [f32; 20] = [
1101        0.393, 0.769, 0.189, 0.0, 0.0, // Red
1102        0.349, 0.686, 0.168, 0.0, 0.0, // Green
1103        0.272, 0.534, 0.131, 0.0, 0.0, // Blue
1104        0.0, 0.0, 0.0, 1.0, 0.0, // Alpha
1105    ];
1106}
1107
1108/// Common convolution kernels.
1109///
1110/// These kernels are used with the `ConvolveMatrix` filter primitive
1111/// for various image processing effects. All provided kernels are 3x3.
1112pub mod kernels {
1113    use super::ConvolutionKernel;
1114    use alloc::vec;
1115
1116    /// 3x3 Gaussian blur kernel for basic smoothing.
1117    pub fn gaussian_3x3() -> ConvolutionKernel {
1118        ConvolutionKernel {
1119            size: 3,
1120            values: vec![1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0],
1121            divisor: 16.0,
1122            bias: 0.0,
1123            preserve_alpha: false,
1124        }
1125    }
1126
1127    /// 3x3 Sharpen kernel to enhance edges and details.
1128    pub fn sharpen_3x3() -> ConvolutionKernel {
1129        ConvolutionKernel {
1130            size: 3,
1131            values: vec![0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0],
1132            divisor: 1.0,
1133            bias: 0.0,
1134            preserve_alpha: true,
1135        }
1136    }
1137
1138    /// 3x3 Edge detection kernel (Laplacian operator).
1139    pub fn edge_detect_3x3() -> ConvolutionKernel {
1140        ConvolutionKernel {
1141            size: 3,
1142            values: vec![-1.0, -1.0, -1.0, -1.0, 8.0, -1.0, -1.0, -1.0, -1.0],
1143            divisor: 1.0,
1144            bias: 0.0,
1145            preserve_alpha: true,
1146        }
1147    }
1148
1149    /// 3x3 Emboss kernel for creating a raised/beveled appearance.
1150    pub fn emboss_3x3() -> ConvolutionKernel {
1151        ConvolutionKernel {
1152            size: 3,
1153            values: vec![-2.0, -1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 2.0],
1154            divisor: 1.0,
1155            bias: 0.5,
1156            preserve_alpha: true,
1157        }
1158    }
1159}