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