Skip to main content

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