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}