From 1bf983b525f7f53e6488468978f9aa01c103d46c Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 14:15:08 +0100 Subject: [PATCH 01/29] Import filter types from vello_hybrid Signed-off-by: Nico Burns --- Cargo.lock | 1 + crates/anyrender/Cargo.toml | 1 + crates/anyrender/src/filters.rs | 1088 +++++++++++++++++++++++++++++++ crates/anyrender/src/lib.rs | 1 + 4 files changed, 1091 insertions(+) create mode 100644 crates/anyrender/src/filters.rs diff --git a/Cargo.lock b/Cargo.lock index cbf8f6d..581d0ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ dependencies = [ "peniko", "raw-window-handle", "serde", + "smallvec", ] [[package]] diff --git a/crates/anyrender/Cargo.toml b/crates/anyrender/Cargo.toml index 7d87443..86619ac 100644 --- a/crates/anyrender/Cargo.toml +++ b/crates/anyrender/Cargo.toml @@ -23,3 +23,4 @@ raw-window-handle = { workspace = true } # Serde serde = { workspace = true, features = ["derive"], optional = true } +smallvec = "1.15.1" diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs new file mode 100644 index 0000000..2d89d72 --- /dev/null +++ b/crates/anyrender/src/filters.rs @@ -0,0 +1,1088 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Filter effects API based on the W3C Filter Effects specification. +//! +//! This module provides a comprehensive filter system supporting both high-level +//! CSS filter functions and low-level SVG filter primitives. The API is designed +//! to follow the W3C Filter Effects Module Level 1 specification. +//! +//! See: +//! +//! ## Implementation Status +//! +//! ### ✅ Implemented +//! +//! **Filter Functions:** +//! - `Blur` - Gaussian blur effect +//! +//! **Filter Primitives (Single Use Only):** +//! - `Flood` - Solid color fill +//! - `GaussianBlur` - Gaussian blur filter +//! - `DropShadow` - Drop shadow effect (compound primitive) +//! - `Offset` - Translation/shift (single primitive) +//! +//! **Note:** Currently only single primitive filters are supported. Filter graphs with +//! multiple connected primitives are not yet implemented. +//! +//! ### 🚧 Not Yet Implemented +//! +//! **Core Features:** +//! - `FilterGraph` execution - Chaining multiple filter primitives together +//! - `FilterInputs` - Connecting primitives to create complex effects +//! +//! **Filter Functions:** +//! - `Brightness`, `Contrast`, `Grayscale`, `HueRotate`, `Invert`, +//! `Opacity`, `Saturate`, `Sepia` +//! +//! **Filter Primitives:** +//! - `ColorMatrix` - Matrix-based color transformation +//! - `Composite` - Porter-Duff compositing operations +//! - `Blend` - Blend mode operations +//! - `Morphology` - Dilate/erode operations +//! - `ConvolveMatrix` - Custom convolution kernels +//! - `Turbulence` - Perlin noise generation +//! - `DisplacementMap` - Pixel displacement +//! - `ComponentTransfer` - Per-channel transfer functions +//! - `Image` - External image reference +//! - `Tile` - Tiling operation +//! - `DiffuseLighting`, `SpecularLighting` - Lighting effects + +use std::sync::Arc; + +use peniko::color::{AlphaColor, Srgb}; +use kurbo::{Affine, Rect}; +use smallvec::SmallVec; + +/// The main filter system. +/// +/// A filter combines a graph of filter primitives with optional spatial bounds. +/// If bounds are specified, the filter only applies within that region. +#[derive(Debug, Clone, PartialEq)] +pub struct Filter { + /// Filter graph defining the effect pipeline. + graph: Arc, + // TODO: Add bounds restricting where the filter applies. + // Optional bounds restricting where the filter applies. + // If `None`, the filter applies to the entire filtered element. + // pub bounds: Option, +} + +impl Filter { + /// Create a simple filter system from a filter function. + /// + /// Converts a high-level CSS-style filter function into a filter graph. + /// Use this for simple effects like blur, brightness, etc. + pub fn from_function(function: FilterFunction) -> Self { + // Convert function to primitive + let primitive = match function { + FilterFunction::Blur { radius } => FilterPrimitive::GaussianBlur { + std_deviation: radius, + edge_mode: EdgeMode::default(), + }, + _ => unimplemented!("Filter function {:?} not supported", function), + }; + + Self::from_primitive(primitive) + } + + /// Create a filter system from a filter primitive. + /// + /// Creates a simple filter graph with a single primitive. + /// Use this for direct access to low-level SVG filter operations. + pub fn from_primitive(primitive: FilterPrimitive) -> Self { + let mut graph = FilterGraph::new(); + let filter_id = graph.add(primitive, None); + graph.set_output(filter_id); + + Self { + graph: Arc::new(graph), + } + } + + /// Calculate the bounds expansion for this filter in pixel/device space. + /// + /// Returns a `Rect` representing how many extra pixels are needed around the + /// filtered region to correctly compute the filter effect. For example, a blur + /// filter needs to sample beyond the original bounds to avoid edge artifacts. + /// + /// The expansion accounts for the transform (rotation, scale, and shear) to compute + /// the correct axis-aligned bounding box expansion in device space. + /// + /// The returned rect is centered at origin: + /// - x0: negative left expansion (in pixels) + /// - y0: negative top expansion (in pixels) + /// - x1: positive right expansion (in pixels) + /// - y1: positive bottom expansion (in pixels) + /// + /// # Arguments + /// * `transform` - The transform applied to this filter layer + pub fn bounds_expansion(&self, transform: &Affine) -> Rect { + let [a, b, c, d, _e, _f] = transform.as_coeffs(); + let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]); + + self.graph.bounds_expansion(&linear_only) + } +} + +/// A directed acyclic graph (DAG) of filter operations. +/// +/// The graph represents a pipeline of filter primitives where outputs of some +/// primitives can be used as inputs to others. Each primitive has a unique `FilterId`. +#[derive(Debug, Clone, PartialEq)] +pub struct FilterGraph { + /// All filter primitives in the graph, stored in insertion order. + pub primitives: SmallVec<[FilterPrimitive; 1]>, + /// The final output filter ID whose result is the output of this graph. + pub output: FilterId, + /// Next available filter ID (monotonically increasing counter). + next_id: u16, + /// Accumulated bounds expansion from all primitives in the graph, cached in user space. + /// This is the axis-aligned bounding box of the expansion region (centered at origin), + /// which can be transformed to device space when needed. + expansion_rect: Rect, +} + +impl Default for FilterGraph { + fn default() -> Self { + Self::new() + } +} + +impl FilterGraph { + /// Create a new empty filter graph. + pub fn new() -> Self { + Self { + primitives: SmallVec::new(), + output: FilterId(0), + next_id: 0, + expansion_rect: Rect::ZERO, + } + } + + /// Add a filter primitive with optional inputs. + /// + /// Returns a `FilterId` that can be referenced by other primitives. + /// Automatically updates the accumulated bounds expansion based on the primitive's requirements. + pub fn add(&mut self, primitive: FilterPrimitive, _inputs: Option) -> FilterId { + let id = FilterId(self.next_id); + self.next_id += 1; + + // Update accumulated expansion by taking the union of rects + let primitive_rect = primitive.expansion_rect(); + self.expansion_rect = self.expansion_rect.union(primitive_rect); + + self.primitives.push(primitive); + + id + } + + /// Set the output filter for the graph. + pub fn set_output(&mut self, output: FilterId) { + self.output = output; + } + + /// Get the accumulated bounds expansion for all primitives in this graph. + /// + /// This returns the expansion required by all primitives in the graph, + /// representing the padding needed to render all filter effects correctly. + /// + /// The expansion accounts for the transform (rotation, scale, and shear) to compute + /// the correct axis-aligned bounding box expansion in device space. + /// + /// # Arguments + /// * `transform` - The transform applied to this filter layer + pub fn bounds_expansion(&self, transform: &Affine) -> Rect { + // Transform the cached expansion rect to device space + // transform_rect_bbox computes the axis-aligned bounding box of the transformed rect + transform.transform_rect_bbox(self.expansion_rect) + } +} + +/// All possible filter effects. +/// +/// This enum allows choosing between high-level filter functions (simple CSS-style effects) +/// and low-level filter primitives (complex SVG-style effects with full control). +/// Use `FilterFunction` for common effects like blur, and `FilterPrimitive` for +/// advanced composition and custom filter graphs. +#[derive(Debug, Clone)] +pub enum FilterEffect { + /// Simple, high-level filter functions. + Function(FilterFunction), + /// Low-level filter primitives (granular control). + Primitive(FilterPrimitive), +} + +/// High-level filter functions for common effects (CSS filter functions). +/// +/// These match the CSS Filter Effects specification and provide simple, +/// commonly-used visual effects without needing to construct a filter graph. +/// +/// See: +#[derive(Debug, Clone)] +pub enum FilterFunction { + /// Gaussian blur effect. + /// + /// Applies a Gaussian blur to the input image. Larger radius values + /// produce more blur. The blur is applied equally in all directions. + /// + /// Note: Per the W3C Filter Effects specification, this `radius` parameter + /// represents the standard deviation (σ) of the Gaussian function, not the + /// effective blur range. The effective blur range is approximately 3× this value. + Blur { + /// Standard deviation of the Gaussian blur in pixels. Must be non-negative. + /// A value of 0 means no blur. + /// + /// Despite being called "radius" (to match CSS filter syntax), this is + /// actually the standard deviation. The visible blur effect extends + /// approximately 3 times this value in each direction. + radius: f32, + }, + // + // ============================================================ + // TODO: The following filter functions are not yet implemented + // ============================================================ + // + /// Brightness adjustment. + /// + /// Adjusts the brightness of the input image using a linear multiplier. + Brightness { + /// Brightness amount: 0.0 = completely black, 1.0 = no change, >1.0 = brighter. + /// Must be non-negative. + amount: f32, + }, + /// Contrast adjustment. + /// + /// Adjusts the contrast of the input image. + Contrast { + /// Contrast amount: 0.0 = uniform gray, 1.0 = no change, >1.0 = higher contrast. + /// Must be non-negative. + amount: f32, + }, + /// Grayscale conversion. + /// + /// Converts the input to grayscale. Amount controls the strength of the conversion. + Grayscale { + /// Grayscale amount: 0.0 = original colors, 1.0 = full grayscale. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Hue rotation. + /// + /// Rotates the hue of all colors in the input image by the specified angle. + HueRotate { + /// Rotation angle in degrees. Can be negative. + /// 0° = no change, 180° = opposite hue, 360° = back to original. + angle: f32, + }, + /// Color inversion. + /// + /// Inverts the colors of the input image. + Invert { + /// Inversion amount: 0.0 = original colors, 1.0 = fully inverted. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Opacity adjustment. + /// + /// Multiplies the alpha channel by the specified amount. + Opacity { + /// Opacity amount: 0.0 = fully transparent, 1.0 = no change. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Saturation adjustment. + /// + /// Adjusts the color saturation of the input image. + Saturate { + /// Saturation amount: 0.0 = completely desaturated (grayscale), + /// 1.0 = no change, >1.0 = oversaturated. + /// Must be non-negative. + amount: f32, + }, + /// Sepia tone effect. + /// + /// Applies a sepia tone effect (vintage/old photo appearance). + Sepia { + /// Sepia amount: 0.0 = original colors, 1.0 = full sepia tone. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, +} + +/// Edge mode for filter operations. +/// +/// Determines how to extend the input image when filter operations require sampling +/// beyond the original image boundaries. This is particularly important for blur and +/// convolution operations near edges. +/// +/// See: +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum EdgeMode { + /// Extend by duplicating edge pixels (clamp to edge). + /// + /// The input image is extended along each border by replicating the color values + /// at the given edge of the input image. This prevents dark halos around edges. + Duplicate, + /// Extend by wrapping to the opposite edge (repeat/tile). + /// + /// The input image is extended by taking color values from the opposite edge, + /// creating a tiling effect. + Wrap, + /// Extend by mirroring across the edge. + /// + /// The input image is extended by taking color values mirrored across the edge. + /// This creates seamless continuation at boundaries. + Mirror, + /// Extend with transparent black (zeros). + /// + /// The input image is extended with pixel values of zero for R, G, B and A. + /// This is the default and most common mode, creating natural fade-to-transparent edges. + #[default] + None, +} + +/// Low-level filter primitives for granular control (SVG filter primitives). +/// +/// These are the building blocks for complex filter effects, corresponding to SVG +/// filter primitives. They can be combined in a `FilterGraph` to create sophisticated +/// visual effects. +/// +/// See: +#[derive(Debug, Clone, PartialEq)] +pub enum FilterPrimitive { + /// Generate a solid color fill. + /// + /// Creates a rectangle filled with the specified color, typically used as + /// input to other filter operations (e.g., for colored shadows). + Flood { + /// Fill color with alpha channel. + color: AlphaColor, + }, + /// Gaussian blur filter. + /// + /// Applies a Gaussian blur using the specified standard deviation (σ). + /// The effective blur range (distance over which pixels are sampled) is + /// approximately 3 × `std_deviation`, as this captures ~99.7% of the + /// Gaussian distribution. + GaussianBlur { + /// Standard deviation for the blur kernel. Larger values create more blur. + /// Must be non-negative. A value of 0 means no blur. + /// + /// This directly corresponds to the σ (sigma) parameter in the Gaussian + /// function. The visible blur effect extends approximately 3σ in each direction. + /// + /// TODO: Per the W3C specification, this should support separate x and y values. + /// The spec allows `stdDeviation` to be either one number (applied to both axes) + /// or two numbers (first for x-axis, second for y-axis). Currently only uniform + /// blur is supported. Consider changing to `(f32, f32)` or a dedicated type. + std_deviation: f32, + /// Edge mode determining how pixels beyond the input bounds are handled. + edge_mode: EdgeMode, + }, + /// Drop shadow effect (compound primitive). + /// + /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, + /// and compositing it with the original. This is a compound operation that + /// combines multiple primitive operations into one. + /// + /// See: + DropShadow { + /// Horizontal offset of the shadow in pixels. Positive values shift right. + dx: f32, + /// Vertical offset of the shadow in pixels. Positive values shift down. + dy: f32, + /// Blur standard deviation for the shadow. Larger values create softer shadows. + std_deviation: f32, + /// Shadow color with alpha channel. Alpha controls shadow opacity. + color: AlphaColor, + /// Edge mode for handling boundaries during blur operation. + /// Default is `EdgeMode::None` per SVG spec. + edge_mode: EdgeMode, + }, + // + // ============================================================ + // TODO: The following filter primitives are not yet implemented + // ============================================================ + // + /// Matrix-based color transformation. + /// + /// Applies a 4x5 matrix transformation to colors, allowing arbitrary + /// color space transformations, hue shifts, and color adjustments. + ColorMatrix { + /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). + /// Each output channel is computed as a linear combination of input channels plus offset. + matrix: [f32; 20], + }, + /// Geometric offset/translation. + /// + /// Shifts the input image by the specified offset. Useful for creating + /// shadow effects or positioning elements in a filter graph. + Offset { + /// Horizontal offset in pixels. Positive values shift right. + dx: f32, + /// Vertical offset in pixels. Positive values shift down. + dy: f32, + }, + + /// Composite two inputs using Porter-Duff compositing operations. + /// + /// Combines two input images using standard compositing operators + /// (over, in, out, atop, xor) or custom arithmetic combination. + Composite { + /// Porter-Duff compositing operator to apply. + operator: CompositeOperator, + }, + /// Blend two inputs using blend modes. + /// + /// Combines two input images using Photoshop-style blend modes + /// (multiply, screen, overlay, etc.). + Blend { + /// Blend mode determining how colors are combined. + mode: BlendMode, + }, + /// Morphological operations (dilate/erode). + /// + /// Expands (dilate) or contracts (erode) the shapes in the input image. + /// Useful for creating outline effects or cleaning up edges. + Morphology { + /// Morphological operator determining whether to erode or dilate. + operator: MorphologyOperator, + /// Operation radius in pixels. Larger values create stronger effects. + radius: f32, + }, + /// Custom convolution kernel for image processing. + /// + /// Applies a custom convolution matrix to the input image, enabling + /// effects like sharpening, edge detection, embossing, and custom filters. + ConvolveMatrix { + /// Convolution kernel specification including size, values, and normalization. + kernel: ConvolutionKernel, + }, + /// Generate Perlin noise/turbulence patterns. + /// + /// Creates procedural noise patterns useful for textures, clouds, + /// marble effects, and other organic-looking randomness. + Turbulence { + /// Base frequency for noise generation. Higher values create finer detail. + base_frequency: f32, + /// Number of octaves for fractal noise. More octaves add finer detail. + num_octaves: u32, + /// Random seed for reproducible noise generation. + seed: u32, + /// Type of noise: smooth fractal or more chaotic turbulence. + turbulence_type: TurbulenceType, + }, + /// Displace pixels using a displacement map. + /// + /// Uses the color values from a second input to spatially displace pixels + /// in the primary input, creating warping and distortion effects. + DisplacementMap { + /// Scale factor controlling the displacement intensity. + scale: f32, + /// Color channel from the displacement map used for X-axis displacement. + x_channel: ColorChannel, + /// Color channel from the displacement map used for Y-axis displacement. + y_channel: ColorChannel, + }, + /// Per-channel component transfer using lookup tables or functions. + /// + /// Applies independent transfer functions to each color channel, + /// enabling color corrections, gamma adjustments, and custom mappings. + ComponentTransfer { + /// Transfer function applied to the red channel (None = identity). + red_function: Option, + /// Transfer function applied to the green channel (None = identity). + green_function: Option, + /// Transfer function applied to the blue channel (None = identity). + blue_function: Option, + /// Transfer function applied to the alpha channel (None = identity). + alpha_function: Option, + }, + /// Reference an external image as filter input. + /// + /// Allows using pre-existing images (from an atlas or resource) as + /// input to filter operations, useful for texturing and overlays. + Image { + /// Identifier referencing an image in the resource atlas. + image_id: u32, + /// Optional 2D affine transformation matrix [a, b, c, d, e, f]. + /// Transforms the image before using it as filter input. + transform: Option<[f32; 6]>, + }, + /// Tile the input to fill the filter region. + /// + /// Repeats the input image to fill the entire filter primitive subregion, + /// creating a tiling/repeating pattern. + Tile, + /// Diffuse lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating diffuse (matte) reflection from a light source. + DiffuseLighting { + /// Surface scale factor for converting alpha values to heights. + surface_scale: f32, + /// Diffuse reflection constant (kd). Controls lighting intensity. + diffuse_constant: f32, + /// Kernel unit length for gradient calculations in user space. + kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + light_source: LightSource, + }, + /// Specular lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating specular (shiny) reflection highlights from a light source. + SpecularLighting { + /// Surface scale factor for converting alpha values to heights. + surface_scale: f32, + /// Specular reflection constant (ks). Controls highlight intensity. + specular_constant: f32, + /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). + specular_exponent: f32, + /// Kernel unit length for gradient calculations in user space. + kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + light_source: LightSource, + }, +} + +impl FilterPrimitive { + /// Calculate the bounds expansion as a `Rect` in user space. + /// + /// Returns a rectangle centered at the origin representing how much the filter + /// expands the processing region in each direction. The rect coordinates are: + /// - x0: negative left expansion + /// - y0: negative top expansion + /// - x1: positive right expansion + /// - y1: positive bottom expansion + /// + /// A `Rect::ZERO` means no expansion. This representation allows the expansion + /// to be correctly transformed (including rotation) using standard rect transforms. + /// + /// For example, a blur filter needs additional pixels around the edges (3*sigma). + /// Most filters that don't sample neighboring pixels return `Rect::ZERO`. + pub fn expansion_rect(&self) -> Rect { + match self { + Self::GaussianBlur { std_deviation, .. } => { + // Gaussian blur expands uniformly by 3*sigma (covers 99.7% of distribution) + let radius = (*std_deviation * 3.0) as f64; + Rect::new(-radius, -radius, radius, radius) + } + Self::Offset { dx, dy } => { + // Offset shifts pixels; expand bounds asymmetrically so shifted content isn't cut. + let dx = *dx as f64; + let dy = *dy as f64; + Rect::new(dx.min(0.0), dy.min(0.0), dx.max(0.0), dy.max(0.0)) + } + Self::DropShadow { + std_deviation, + dx, + dy, + .. + } => { + // Drop shadow = blur + offset + composite with original + // The expansion rect encompasses both the blur and the offset + let blur_radius = (*std_deviation * 3.0) as f64; + let dx = *dx as f64; + let dy = *dy as f64; + + Rect::new( + -(blur_radius + (-dx).max(0.0)), + -(blur_radius + (-dy).max(0.0)), + blur_radius + dx.max(0.0), + blur_radius + dy.max(0.0), + ) + } + // Most other filters don't expand bounds + _ => Rect::ZERO, + } + } +} + +#[cfg(test)] +mod offset_expansion_tests { + use super::FilterPrimitive; + use kurbo::Rect; + + #[test] + fn offset_expands_in_direction_of_shift() { + let p = FilterPrimitive::Offset { dx: 2.5, dy: -3.0 }; + assert_eq!( + p.expansion_rect(), + Rect::new(0.0, -3.0, 2.5, 0.0), + "Offset expansion should be asymmetric and include the shift vector" + ); + } +} + +/// Unique identifier for a filter primitive in the graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FilterId(pub u16); + +/// Input connections for a filter primitive. +#[derive(Debug, Clone, PartialEq)] +pub struct FilterInputs { + /// Primary input ("in" attribute in SVG). + pub primary: FilterInput, + /// Secondary input ("in2" attribute in SVG, for composite/blend operations). + pub secondary: Option, +} + +impl FilterInputs { + /// Create filter inputs with a single input. + /// + /// Use this for primitives that operate on a single source (blur, color matrix, etc.). + pub fn single(input: FilterInput) -> Self { + Self { + primary: input, + secondary: None, + } + } + + /// Create filter inputs with two inputs (for composite, blend, etc.). + /// + /// Use this for primitives that combine two sources (composite, blend, displacement map, etc.). + pub fn dual(input1: FilterInput, input2: FilterInput) -> Self { + Self { + primary: input1, + secondary: Some(input2), + } + } +} + +/// A single filter input. +#[derive(Debug, Clone, PartialEq)] +pub enum FilterInput { + /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.). + Source(FilterSource), + /// Input from another filter's result. + Result(FilterId), +} + +/// Filter input sources. +/// +/// Defines the various built-in sources that can be used as filter inputs, +/// matching the SVG filter primitive input types. These represent implicit +/// inputs available to any filter primitive without requiring previous operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FilterSource { + /// The original graphic content being filtered. + /// + /// This is the default input - the rendered result of the element + /// the filter is applied to, including all its fill, stroke, and content. + SourceGraphic, + /// Alpha channel only of the original graphic. + /// + /// Useful for creating effects based on shape/transparency, such as + /// shadows that follow the element's outline. + SourceAlpha, + /// Background image content behind the filtered element. + /// + /// Allows filters to incorporate or blend with content behind the element. + /// Not always available depending on the rendering context. + BackgroundImage, + /// Alpha channel only of the background image. + /// + /// The transparency mask of the background content. + BackgroundAlpha, + /// The fill paint of the element as an image input. + /// + /// For elements with gradient or pattern fills, this provides access + /// to the fill as a filter input. + FillPaint, + /// The stroke paint of the element as an image input. + /// + /// For elements with gradient or pattern strokes, this provides access + /// to the stroke as a filter input. + StrokePaint, +} + +/// Pre-built compound effects for common use cases. +/// +/// These effects combine multiple filter primitives into commonly-used visual effects. +/// They provide a convenient high-level API for complex multi-step filter operations. +/// +/// **Note:** These are planned but not yet implemented. Use `FilterGraph` to manually +/// construct these effects from primitives. +#[derive(Debug, Clone)] +pub enum CompoundFilter { + /// Inner shadow effect (shadow inside the shape). + /// + /// Creates a shadow that appears inside the boundaries of the shape, + /// giving a recessed or inset appearance. This is the opposite of a drop shadow. + InnerShadow { + /// Horizontal offset of the shadow in pixels. Positive values shift right. + dx: f32, + /// Vertical offset of the shadow in pixels. Positive values shift down. + dy: f32, + /// Blur radius for the shadow in pixels. Larger values create softer shadows. + blur: f32, + /// Shadow color with alpha channel. + color: AlphaColor, + }, + /// Glow effect around the shape. + /// + /// Creates a soft glowing halo around the shape by blurring and + /// compositing a colored version with the original. + Glow { + /// Blur radius for the glow in pixels. Larger values create softer glows. + blur: f32, + /// Glow color with alpha channel. + color: AlphaColor, + }, + /// Bevel effect (3D raised/recessed appearance). + /// + /// Creates a 3D beveled edge effect using lighting simulation, + /// making the shape appear raised or recessed from the surface. + Bevel { + /// Light source angle in degrees (0° = right, 90° = up). + angle: f32, + /// Width of the bevel edge in pixels. + distance: f32, + /// Color for the highlight (lit) side of the bevel. + highlight: AlphaColor, + /// Color for the shadow (dark) side of the bevel. + shadow: AlphaColor, + }, + /// Emboss effect for a raised relief appearance. + /// + /// Creates an embossed/stamped appearance by simulating lighting + /// on a raised surface based on the shape's alpha channel. + Emboss { + /// Light angle in degrees determining emboss direction. + angle: f32, + /// Depth of the emboss effect. + depth: f32, + /// Overall strength/intensity of the effect (0.0 = none, 1.0 = full). + amount: f32, + }, +} + +/// Composite operators for combining filter inputs. +/// +/// These are the Porter-Duff compositing operators used to combine two images. +/// Each operator defines how the source (input 1) and destination (input 2) +/// are combined based on their color and alpha values. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CompositeOperator { + /// Source over destination (standard alpha blending). + /// + /// The source is composited over the destination. This is the most common + /// blending mode where source alpha determines visibility. + Over, + /// Source in destination (intersection). + /// + /// The source is only visible where the destination is opaque. + /// Result alpha = `source_alpha` × `dest_alpha`. + In, + /// Source out destination (subtract). + /// + /// The source is only visible where the destination is transparent. + /// Useful for masking/cutting out regions. + Out, + /// Source atop destination. + /// + /// Source is composited over destination, but only where destination is opaque. + Atop, + /// Source XOR destination (exclusive or). + /// + /// Shows source where destination is transparent and vice versa, + /// but not where both are opaque. + Xor, + /// Arithmetic combination with custom coefficients. + /// + /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4. + /// Allows creating custom compositing operations beyond the standard Porter-Duff set. + Arithmetic { + /// Coefficient k1 for the (source * destination) term. + k1: f32, + /// Coefficient k2 for the source term. + k2: f32, + /// Coefficient k3 for the destination term. + k3: f32, + /// Constant offset k4 added to the result. + k4: f32, + }, +} + +/// Blend modes for combining colors. +/// +/// These are blend modes that define how to combine the colors +/// of two layers. Unlike compositing operators which deal with alpha, blend modes +/// focus on color mixing while preserving the compositing behavior. +/// +/// See: +pub type BlendMode = peniko::Mix; + +/// Morphological operators for dilate/erode operations. +/// +/// These operators modify the shape of objects by expanding or contracting them. +/// They work by examining neighborhoods of pixels and applying min/max operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MorphologyOperator { + /// Erode operation (shrink/thin shapes). + /// + /// Makes objects smaller by removing pixels at the edges. Takes the minimum + /// value in the neighborhood. Useful for removing noise or separating touching objects. + Erode, + /// Dilate operation (expand/thicken shapes). + /// + /// Makes objects larger by adding pixels at the edges. Takes the maximum + /// value in the neighborhood. Useful for filling holes or connecting nearby objects. + Dilate, +} + +/// Convolution kernel for custom filtering operations. +/// +/// Defines a square matrix of weights used for convolution-based image processing. +/// The kernel is applied to each pixel by multiplying surrounding pixels by the weights, +/// summing the results, dividing by the divisor, and adding the bias. +#[derive(Debug, Clone, PartialEq)] +pub struct ConvolutionKernel { + /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5). + /// The kernel must be square, so this defines both width and height. + pub size: u32, + /// Kernel weight values in row-major order. + /// Length must equal size × size. Center of kernel is typically at (size/2, size/2). + pub values: Vec, + /// Normalization divisor applied to the convolution result. + /// Common practice is to use the sum of all weights for averaging, or 1.0 otherwise. + pub divisor: f32, + /// Bias value added to the result after normalization. + /// Useful for edge detection or emboss effects to shift the result range. + pub bias: f32, + /// Whether to preserve the alpha channel unchanged. + /// If true, convolution only applies to RGB; if false, it applies to RGBA. + pub preserve_alpha: bool, +} + +/// Types of turbulence noise generation. +/// +/// Determines the algorithm used for generating procedural noise patterns. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TurbulenceType { + /// Fractal noise (smooth, natural-looking Perlin noise). + /// + /// Creates smooth, continuous patterns suitable for natural textures + /// like clouds, marble, wood grain, or terrain. + FractalNoise, + /// Turbulence noise (more chaotic and energetic). + /// + /// Creates more chaotic patterns with sharper transitions, + /// suitable for fire, smoke, or turbulent effects. + Turbulence, +} + +/// Color channels for displacement mapping and channel selection. +/// +/// Specifies which color channel to use for operations that need to +/// extract or reference individual channels from an image. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorChannel { + /// Red color channel (R component). + Red, + /// Green color channel (G component). + Green, + /// Blue color channel (B component). + Blue, + /// Alpha channel (transparency/opacity). + Alpha, +} + +/// Transfer functions for component transfer operations. +/// +/// These functions map input color channel values to output values, +/// enabling gamma correction, color grading, and custom color curves. +/// Input and output values are typically in the range [0, 1]. +#[derive(Debug, Clone, PartialEq)] +pub enum TransferFunction { + /// Identity function (output = input, no change). + Identity, + /// Table lookup with linear interpolation. + /// + /// Maps input values using a lookup table with linear interpolation between entries. + /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate. + Table { + /// Lookup table values defining the transfer curve. + /// More values provide smoother curves. Minimum 2 values required. + values: Vec, + }, + /// Discrete step function (posterization). + /// + /// Maps input to discrete output values without interpolation, creating step/banding effects. + /// Each segment gets a constant output value from the table. + Discrete { + /// Step values for each discrete output level. + /// Input range is divided into len(values) segments, each mapping to one value. + values: Vec, + }, + /// Linear function: output = slope × input + intercept. + /// + /// Simple linear transformation of the input value. + Linear { + /// Slope coefficient (rate of change). + slope: f32, + /// Intercept offset (constant added to result). + intercept: f32, + }, + /// Gamma correction: output = amplitude × input^exponent + offset. + /// + /// Applies power-law transformation, commonly used for gamma correction and + /// adjusting midtone brightness without affecting blacks or whites. + Gamma { + /// Amplitude multiplier applied to the result. + amplitude: f32, + /// Gamma exponent (< 1 brightens, > 1 darkens midtones). + exponent: f32, + /// Offset added to the final result. + offset: f32, + }, +} + +/// Light source configurations for lighting effects. +/// +/// Defines different types of light sources used in diffuse and specular lighting +/// filter primitives. Each type has different characteristics and use cases. +#[derive(Debug, Clone, PartialEq)] +pub enum LightSource { + /// Distant light source (infinitely far away, like the sun). + /// + /// All rays are parallel, creating uniform lighting across the surface. + /// Direction is specified using spherical coordinates (azimuth and elevation). + Distant { + /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up). + /// Defines the horizontal direction of the light. + azimuth: f32, + /// Elevation angle in degrees (0° = horizon, 90° = directly overhead). + /// Defines the vertical angle of the light source. + elevation: f32, + }, + /// Point light source at a specific 3D position. + /// + /// Light radiates uniformly in all directions from a single point. + /// Intensity decreases with distance. Like a light bulb. + Point { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + /// Larger values create softer lighting across larger areas. + z: f32, + }, + /// Spot light with position, direction, and cone angle. + /// + /// Light emanates from a point in a specific direction with limited spread. + /// Like a flashlight or stage spotlight with adjustable focus. + Spot { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + z: f32, + /// X coordinate the spotlight is aimed at. + points_at_x: f32, + /// Y coordinate the spotlight is aimed at. + points_at_y: f32, + /// Z coordinate the spotlight is aimed at. + points_at_z: f32, + /// Specular exponent controlling the focus/sharpness of the spotlight beam. + /// Higher values create tighter, more focused beams. + specular_exponent: f32, + /// Optional cone angle in degrees limiting the spotlight spread. + /// If None, the light spreads based only on the specular exponent. + limiting_cone_angle: Option, + }, +} + +/// Common color transformation matrices. +/// +/// These 4x5 matrices are used with the `ColorMatrix` filter primitive. +/// Each row transforms a color channel: [R, G, B, A, offset]. +pub mod matrices { + /// Identity matrix (no change). + pub const IDENTITY: [f32; 20] = [ + 1.0, 0.0, 0.0, 0.0, 0.0, // Red + 0.0, 1.0, 0.0, 0.0, 0.0, // Green + 0.0, 0.0, 1.0, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; + + /// Extract alpha channel to RGB (for shadow effects). + pub const ALPHA_TO_BLACK: [f32; 20] = [ + 0.0, 0.0, 0.0, 1.0, 0.0, // Red = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Green = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha + ]; + + /// Grayscale conversion matrix using luminosity weights. + pub const GRAYSCALE: [f32; 20] = [ + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; + + /// Sepia tone matrix for vintage photo effect. + pub const SEPIA: [f32; 20] = [ + 0.393, 0.769, 0.189, 0.0, 0.0, // Red + 0.349, 0.686, 0.168, 0.0, 0.0, // Green + 0.272, 0.534, 0.131, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; +} + +/// Common convolution kernels. +/// +/// These kernels are used with the `ConvolveMatrix` filter primitive +/// for various image processing effects. All provided kernels are 3x3. +pub mod kernels { + use super::ConvolutionKernel; + + /// 3x3 Gaussian blur kernel for basic smoothing. + pub fn gaussian_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0], + divisor: 16.0, + bias: 0.0, + preserve_alpha: false, + } + } + + /// 3x3 Sharpen kernel to enhance edges and details. + pub fn sharpen_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0], + divisor: 1.0, + bias: 0.0, + preserve_alpha: true, + } + } + + /// 3x3 Edge detection kernel (Laplacian operator). + pub fn edge_detect_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![-1.0, -1.0, -1.0, -1.0, 8.0, -1.0, -1.0, -1.0, -1.0], + divisor: 1.0, + bias: 0.0, + preserve_alpha: true, + } + } + + /// 3x3 Emboss kernel for creating a raised/beveled appearance. + pub fn emboss_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![-2.0, -1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 2.0], + divisor: 1.0, + bias: 0.5, + preserve_alpha: true, + } + } +} diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index 41419e6..ee7a43d 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -43,6 +43,7 @@ use peniko::{BlendMode, Color, Fill, FontData, ImageBrushRef, StyleRef}; use recording::RenderCommand; use std::{any::Any, sync::Arc}; +pub mod filters; pub mod wasm_send_sync; pub use wasm_send_sync::*; pub mod types; From bb0da59e70e0903d319517bbbd49584ac5ee859b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 14:15:59 +0100 Subject: [PATCH 02/29] Remove next_id field from FilterGraph Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 2d89d72..4847d1e 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -50,8 +50,8 @@ use std::sync::Arc; -use peniko::color::{AlphaColor, Srgb}; use kurbo::{Affine, Rect}; +use peniko::color::{AlphaColor, Srgb}; use smallvec::SmallVec; /// The main filter system. @@ -135,8 +135,6 @@ pub struct FilterGraph { pub primitives: SmallVec<[FilterPrimitive; 1]>, /// The final output filter ID whose result is the output of this graph. pub output: FilterId, - /// Next available filter ID (monotonically increasing counter). - next_id: u16, /// Accumulated bounds expansion from all primitives in the graph, cached in user space. /// This is the axis-aligned bounding box of the expansion region (centered at origin), /// which can be transformed to device space when needed. @@ -155,7 +153,6 @@ impl FilterGraph { Self { primitives: SmallVec::new(), output: FilterId(0), - next_id: 0, expansion_rect: Rect::ZERO, } } @@ -165,8 +162,7 @@ impl FilterGraph { /// Returns a `FilterId` that can be referenced by other primitives. /// Automatically updates the accumulated bounds expansion based on the primitive's requirements. pub fn add(&mut self, primitive: FilterPrimitive, _inputs: Option) -> FilterId { - let id = FilterId(self.next_id); - self.next_id += 1; + let id = FilterId(self.primitives.len() as u16); // Update accumulated expansion by taking the union of rects let primitive_rect = primitive.expansion_rect(); From 28601f473a18f0d950baa1edeb9f780fc251cf5b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 15:28:53 +0100 Subject: [PATCH 03/29] Delete FilterFunction and FilterEffect --- crates/anyrender/src/filters.rs | 164 +++++++------------------------- 1 file changed, 34 insertions(+), 130 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 4847d1e..01d940c 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -69,21 +69,32 @@ pub struct Filter { } impl Filter { - /// Create a simple filter system from a filter function. - /// - /// Converts a high-level CSS-style filter function into a filter graph. - /// Use this for simple effects like blur, brightness, etc. - pub fn from_function(function: FilterFunction) -> Self { - // Convert function to primitive - let primitive = match function { - FilterFunction::Blur { radius } => FilterPrimitive::GaussianBlur { - std_deviation: radius, - edge_mode: EdgeMode::default(), - }, - _ => unimplemented!("Filter function {:?} not supported", function), - }; + /// Gaussian blur effect. + /// + /// Applies a Gaussian blur to the input image. Larger radius values + /// produce more blur. The blur is applied equally in all directions. + pub fn blur(radius: f32) -> Self { + Self::from_primitive(FilterPrimitive::GaussianBlur { + std_deviation: radius, + edge_mode: EdgeMode::None, + }) + } - Self::from_primitive(primitive) + /// Drop shadow effect (compound primitive). + /// + /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, + /// and compositing it with the original. This is a compound operation that + /// combines multiple primitive operations into one. + /// + /// See: + pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { + Self::from_primitive(FilterPrimitive::DropShadow { + dx, + dy, + std_deviation, + color, + edge_mode: EdgeMode::None, + }) } /// Create a filter system from a filter primitive. @@ -91,12 +102,8 @@ impl Filter { /// Creates a simple filter graph with a single primitive. /// Use this for direct access to low-level SVG filter operations. pub fn from_primitive(primitive: FilterPrimitive) -> Self { - let mut graph = FilterGraph::new(); - let filter_id = graph.add(primitive, None); - graph.set_output(filter_id); - Self { - graph: Arc::new(graph), + graph: Arc::new(FilterGraph::single(primitive)), } } @@ -157,6 +164,14 @@ impl FilterGraph { } } + /// Create a new filter graph containing a single filter with no inputs + pub fn single(primitive: FilterPrimitive) -> Self { + let mut graph = Self::new(); + let filter_id = graph.add(primitive, None); + graph.set_output(filter_id); + graph + } + /// Add a filter primitive with optional inputs. /// /// Returns a `FilterId` that can be referenced by other primitives. @@ -195,117 +210,6 @@ impl FilterGraph { } } -/// All possible filter effects. -/// -/// This enum allows choosing between high-level filter functions (simple CSS-style effects) -/// and low-level filter primitives (complex SVG-style effects with full control). -/// Use `FilterFunction` for common effects like blur, and `FilterPrimitive` for -/// advanced composition and custom filter graphs. -#[derive(Debug, Clone)] -pub enum FilterEffect { - /// Simple, high-level filter functions. - Function(FilterFunction), - /// Low-level filter primitives (granular control). - Primitive(FilterPrimitive), -} - -/// High-level filter functions for common effects (CSS filter functions). -/// -/// These match the CSS Filter Effects specification and provide simple, -/// commonly-used visual effects without needing to construct a filter graph. -/// -/// See: -#[derive(Debug, Clone)] -pub enum FilterFunction { - /// Gaussian blur effect. - /// - /// Applies a Gaussian blur to the input image. Larger radius values - /// produce more blur. The blur is applied equally in all directions. - /// - /// Note: Per the W3C Filter Effects specification, this `radius` parameter - /// represents the standard deviation (σ) of the Gaussian function, not the - /// effective blur range. The effective blur range is approximately 3× this value. - Blur { - /// Standard deviation of the Gaussian blur in pixels. Must be non-negative. - /// A value of 0 means no blur. - /// - /// Despite being called "radius" (to match CSS filter syntax), this is - /// actually the standard deviation. The visible blur effect extends - /// approximately 3 times this value in each direction. - radius: f32, - }, - // - // ============================================================ - // TODO: The following filter functions are not yet implemented - // ============================================================ - // - /// Brightness adjustment. - /// - /// Adjusts the brightness of the input image using a linear multiplier. - Brightness { - /// Brightness amount: 0.0 = completely black, 1.0 = no change, >1.0 = brighter. - /// Must be non-negative. - amount: f32, - }, - /// Contrast adjustment. - /// - /// Adjusts the contrast of the input image. - Contrast { - /// Contrast amount: 0.0 = uniform gray, 1.0 = no change, >1.0 = higher contrast. - /// Must be non-negative. - amount: f32, - }, - /// Grayscale conversion. - /// - /// Converts the input to grayscale. Amount controls the strength of the conversion. - Grayscale { - /// Grayscale amount: 0.0 = original colors, 1.0 = full grayscale. - /// Values should be in range [0.0, 1.0]. - amount: f32, - }, - /// Hue rotation. - /// - /// Rotates the hue of all colors in the input image by the specified angle. - HueRotate { - /// Rotation angle in degrees. Can be negative. - /// 0° = no change, 180° = opposite hue, 360° = back to original. - angle: f32, - }, - /// Color inversion. - /// - /// Inverts the colors of the input image. - Invert { - /// Inversion amount: 0.0 = original colors, 1.0 = fully inverted. - /// Values should be in range [0.0, 1.0]. - amount: f32, - }, - /// Opacity adjustment. - /// - /// Multiplies the alpha channel by the specified amount. - Opacity { - /// Opacity amount: 0.0 = fully transparent, 1.0 = no change. - /// Values should be in range [0.0, 1.0]. - amount: f32, - }, - /// Saturation adjustment. - /// - /// Adjusts the color saturation of the input image. - Saturate { - /// Saturation amount: 0.0 = completely desaturated (grayscale), - /// 1.0 = no change, >1.0 = oversaturated. - /// Must be non-negative. - amount: f32, - }, - /// Sepia tone effect. - /// - /// Applies a sepia tone effect (vintage/old photo appearance). - Sepia { - /// Sepia amount: 0.0 = original colors, 1.0 = full sepia tone. - /// Values should be in range [0.0, 1.0]. - amount: f32, - }, -} - /// Edge mode for filter operations. /// /// Determines how to extend the input image when filter operations require sampling From f4b3f69ad3e37c72fdde76957874d0591661934e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 15:32:40 +0100 Subject: [PATCH 04/29] Remove comments on which filters are unimplemented in vello_hybrid --- crates/anyrender/src/filters.rs | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 01d940c..b3c5873 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -11,7 +11,7 @@ //! //! ## Implementation Status //! -//! ### ✅ Implemented +//! ### Implemented //! //! **Filter Functions:** //! - `Blur` - Gaussian blur effect @@ -22,31 +22,6 @@ //! - `DropShadow` - Drop shadow effect (compound primitive) //! - `Offset` - Translation/shift (single primitive) //! -//! **Note:** Currently only single primitive filters are supported. Filter graphs with -//! multiple connected primitives are not yet implemented. -//! -//! ### 🚧 Not Yet Implemented -//! -//! **Core Features:** -//! - `FilterGraph` execution - Chaining multiple filter primitives together -//! - `FilterInputs` - Connecting primitives to create complex effects -//! -//! **Filter Functions:** -//! - `Brightness`, `Contrast`, `Grayscale`, `HueRotate`, `Invert`, -//! `Opacity`, `Saturate`, `Sepia` -//! -//! **Filter Primitives:** -//! - `ColorMatrix` - Matrix-based color transformation -//! - `Composite` - Porter-Duff compositing operations -//! - `Blend` - Blend mode operations -//! - `Morphology` - Dilate/erode operations -//! - `ConvolveMatrix` - Custom convolution kernels -//! - `Turbulence` - Perlin noise generation -//! - `DisplacementMap` - Pixel displacement -//! - `ComponentTransfer` - Per-channel transfer functions -//! - `Image` - External image reference -//! - `Tile` - Tiling operation -//! - `DiffuseLighting`, `SpecularLighting` - Lighting effects use std::sync::Arc; From e0f7a902ea8f4fdd50bf1c9812c118838deadad3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 16:10:45 +0100 Subject: [PATCH 05/29] Rename FilterPrimitive to FilterEffect Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index b3c5873..e88e830 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -49,7 +49,7 @@ impl Filter { /// Applies a Gaussian blur to the input image. Larger radius values /// produce more blur. The blur is applied equally in all directions. pub fn blur(radius: f32) -> Self { - Self::from_primitive(FilterPrimitive::GaussianBlur { + Self::from_primitive(FilterEffect::GaussianBlur { std_deviation: radius, edge_mode: EdgeMode::None, }) @@ -63,7 +63,7 @@ impl Filter { /// /// See: pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { - Self::from_primitive(FilterPrimitive::DropShadow { + Self::from_primitive(FilterEffect::DropShadow { dx, dy, std_deviation, @@ -76,7 +76,7 @@ impl Filter { /// /// Creates a simple filter graph with a single primitive. /// Use this for direct access to low-level SVG filter operations. - pub fn from_primitive(primitive: FilterPrimitive) -> Self { + pub fn from_primitive(primitive: FilterEffect) -> Self { Self { graph: Arc::new(FilterGraph::single(primitive)), } @@ -114,7 +114,7 @@ impl Filter { #[derive(Debug, Clone, PartialEq)] pub struct FilterGraph { /// All filter primitives in the graph, stored in insertion order. - pub primitives: SmallVec<[FilterPrimitive; 1]>, + pub primitives: SmallVec<[FilterEffect; 1]>, /// The final output filter ID whose result is the output of this graph. pub output: FilterId, /// Accumulated bounds expansion from all primitives in the graph, cached in user space. @@ -140,7 +140,7 @@ impl FilterGraph { } /// Create a new filter graph containing a single filter with no inputs - pub fn single(primitive: FilterPrimitive) -> Self { + pub fn single(primitive: FilterEffect) -> Self { let mut graph = Self::new(); let filter_id = graph.add(primitive, None); graph.set_output(filter_id); @@ -151,7 +151,7 @@ impl FilterGraph { /// /// Returns a `FilterId` that can be referenced by other primitives. /// Automatically updates the accumulated bounds expansion based on the primitive's requirements. - pub fn add(&mut self, primitive: FilterPrimitive, _inputs: Option) -> FilterId { + pub fn add(&mut self, primitive: FilterEffect, _inputs: Option) -> FilterId { let id = FilterId(self.primitives.len() as u16); // Update accumulated expansion by taking the union of rects @@ -225,7 +225,7 @@ pub enum EdgeMode { /// /// See: #[derive(Debug, Clone, PartialEq)] -pub enum FilterPrimitive { +pub enum FilterEffect { /// Generate a solid color fill. /// /// Creates a rectangle filled with the specified color, typically used as @@ -422,7 +422,7 @@ pub enum FilterPrimitive { }, } -impl FilterPrimitive { +impl FilterEffect { /// Calculate the bounds expansion as a `Rect` in user space. /// /// Returns a rectangle centered at the origin representing how much the filter @@ -477,12 +477,12 @@ impl FilterPrimitive { #[cfg(test)] mod offset_expansion_tests { - use super::FilterPrimitive; + use super::FilterEffect; use kurbo::Rect; #[test] fn offset_expands_in_direction_of_shift() { - let p = FilterPrimitive::Offset { dx: 2.5, dy: -3.0 }; + let p = FilterEffect::Offset { dx: 2.5, dy: -3.0 }; assert_eq!( p.expansion_rect(), Rect::new(0.0, -3.0, 2.5, 0.0), From b94cb339a729e0afe2e2445e356886fcbfa00fa2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 16:15:42 +0100 Subject: [PATCH 06/29] Combine Filter and FilterGraph Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 131 +++++++++++++++----------------- 1 file changed, 60 insertions(+), 71 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index e88e830..3cfb78e 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -23,33 +23,43 @@ //! - `Offset` - Translation/shift (single primitive) //! -use std::sync::Arc; - use kurbo::{Affine, Rect}; use peniko::color::{AlphaColor, Srgb}; use smallvec::SmallVec; -/// The main filter system. +/// A directed acyclic graph (DAG) of filter operations. /// -/// A filter combines a graph of filter primitives with optional spatial bounds. -/// If bounds are specified, the filter only applies within that region. +/// The graph represents a pipeline of filter primitives where outputs of some +/// primitives can be used as inputs to others. Each primitive has a unique `FilterId`. #[derive(Debug, Clone, PartialEq)] pub struct Filter { - /// Filter graph defining the effect pipeline. - graph: Arc, + /// All filter primitives in the graph, stored in insertion order. + primitives: SmallVec<[FilterEffect; 1]>, + /// The final output filter ID whose result is the output of this graph. + output: FilterId, + /// Accumulated bounds expansion from all primitives in the graph, cached in user space. + /// This is the axis-aligned bounding box of the expansion region (centered at origin), + /// which can be transformed to device space when needed. + expansion_rect: Rect, // TODO: Add bounds restricting where the filter applies. // Optional bounds restricting where the filter applies. // If `None`, the filter applies to the entire filtered element. // pub bounds: Option, } +impl Default for Filter { + fn default() -> Self { + Self::empty() + } +} + impl Filter { /// Gaussian blur effect. /// /// Applies a Gaussian blur to the input image. Larger radius values /// produce more blur. The blur is applied equally in all directions. pub fn blur(radius: f32) -> Self { - Self::from_primitive(FilterEffect::GaussianBlur { + Self::single(FilterEffect::GaussianBlur { std_deviation: radius, edge_mode: EdgeMode::None, }) @@ -63,7 +73,7 @@ impl Filter { /// /// See: pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { - Self::from_primitive(FilterEffect::DropShadow { + Self::single(FilterEffect::DropShadow { dx, dy, std_deviation, @@ -72,66 +82,8 @@ impl Filter { }) } - /// Create a filter system from a filter primitive. - /// - /// Creates a simple filter graph with a single primitive. - /// Use this for direct access to low-level SVG filter operations. - pub fn from_primitive(primitive: FilterEffect) -> Self { - Self { - graph: Arc::new(FilterGraph::single(primitive)), - } - } - - /// Calculate the bounds expansion for this filter in pixel/device space. - /// - /// Returns a `Rect` representing how many extra pixels are needed around the - /// filtered region to correctly compute the filter effect. For example, a blur - /// filter needs to sample beyond the original bounds to avoid edge artifacts. - /// - /// The expansion accounts for the transform (rotation, scale, and shear) to compute - /// the correct axis-aligned bounding box expansion in device space. - /// - /// The returned rect is centered at origin: - /// - x0: negative left expansion (in pixels) - /// - y0: negative top expansion (in pixels) - /// - x1: positive right expansion (in pixels) - /// - y1: positive bottom expansion (in pixels) - /// - /// # Arguments - /// * `transform` - The transform applied to this filter layer - pub fn bounds_expansion(&self, transform: &Affine) -> Rect { - let [a, b, c, d, _e, _f] = transform.as_coeffs(); - let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]); - - self.graph.bounds_expansion(&linear_only) - } -} - -/// A directed acyclic graph (DAG) of filter operations. -/// -/// The graph represents a pipeline of filter primitives where outputs of some -/// primitives can be used as inputs to others. Each primitive has a unique `FilterId`. -#[derive(Debug, Clone, PartialEq)] -pub struct FilterGraph { - /// All filter primitives in the graph, stored in insertion order. - pub primitives: SmallVec<[FilterEffect; 1]>, - /// The final output filter ID whose result is the output of this graph. - pub output: FilterId, - /// Accumulated bounds expansion from all primitives in the graph, cached in user space. - /// This is the axis-aligned bounding box of the expansion region (centered at origin), - /// which can be transformed to device space when needed. - expansion_rect: Rect, -} - -impl Default for FilterGraph { - fn default() -> Self { - Self::new() - } -} - -impl FilterGraph { /// Create a new empty filter graph. - pub fn new() -> Self { + pub fn empty() -> Self { Self { primitives: SmallVec::new(), output: FilterId(0), @@ -139,9 +91,12 @@ impl FilterGraph { } } - /// Create a new filter graph containing a single filter with no inputs + /// Create a filter from a single filter effect. + /// + /// Creates a simple filter graph with a single primitive. + /// Use this for direct access to low-level SVG filter operations. pub fn single(primitive: FilterEffect) -> Self { - let mut graph = Self::new(); + let mut graph = Self::empty(); let filter_id = graph.add(primitive, None); graph.set_output(filter_id); graph @@ -163,11 +118,45 @@ impl FilterGraph { id } + /// The list of primitives in the graph + pub fn primitives(&mut self) -> &[FilterEffect] { + &self.primitives + } + + /// The output filter for the graph. + pub fn output(&mut self) -> FilterId { + self.output + } + /// Set the output filter for the graph. - pub fn set_output(&mut self, output: FilterId) { + fn set_output(&mut self, output: FilterId) { self.output = output; } + /// Calculate the bounds expansion for this filter in pixel/device space. + /// + /// Returns a `Rect` representing how many extra pixels are needed around the + /// filtered region to correctly compute the filter effect. For example, a blur + /// filter needs to sample beyond the original bounds to avoid edge artifacts. + /// + /// The expansion accounts for the transform (rotation, scale, and shear) to compute + /// the correct axis-aligned bounding box expansion in device space. + /// + /// The returned rect is centered at origin: + /// - x0: negative left expansion (in pixels) + /// - y0: negative top expansion (in pixels) + /// - x1: positive right expansion (in pixels) + /// - y1: positive bottom expansion (in pixels) + /// + /// # Arguments + /// * `transform` - The transform applied to this filter layer + pub fn linear_bounds_expansion(&self, transform: &Affine) -> Rect { + let [a, b, c, d, _e, _f] = transform.as_coeffs(); + let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]); + + self.bounds_expansion(&linear_only) + } + /// Get the accumulated bounds expansion for all primitives in this graph. /// /// This returns the expansion required by all primitives in the graph, From fcfdfb5b1fb570790e8b9b230205c866cf888158 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 16:23:12 +0100 Subject: [PATCH 07/29] Move 'advanced' types to submodules Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 328 +++++++++++++++++--------------- 1 file changed, 170 insertions(+), 158 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 3cfb78e..8f54645 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -27,6 +27,11 @@ use kurbo::{Affine, Rect}; use peniko::color::{AlphaColor, Srgb}; use smallvec::SmallVec; +use crate::filters::{ + component_transfer::TransferFunction, convolution::ConvolutionKernel, lighting::LightSource, + morphology::MorphologyOperator, turbulence::TurbulenceType, +}; + /// A directed acyclic graph (DAG) of filter operations. /// /// The graph represents a pipeline of filter primitives where outputs of some @@ -679,63 +684,43 @@ pub enum CompositeOperator { /// See: pub type BlendMode = peniko::Mix; -/// Morphological operators for dilate/erode operations. -/// -/// These operators modify the shape of objects by expanding or contracting them. -/// They work by examining neighborhoods of pixels and applying min/max operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MorphologyOperator { - /// Erode operation (shrink/thin shapes). +mod morphology { + /// Morphological operators for dilate/erode operations. /// - /// Makes objects smaller by removing pixels at the edges. Takes the minimum - /// value in the neighborhood. Useful for removing noise or separating touching objects. - Erode, - /// Dilate operation (expand/thicken shapes). - /// - /// Makes objects larger by adding pixels at the edges. Takes the maximum - /// value in the neighborhood. Useful for filling holes or connecting nearby objects. - Dilate, -} - -/// Convolution kernel for custom filtering operations. -/// -/// Defines a square matrix of weights used for convolution-based image processing. -/// The kernel is applied to each pixel by multiplying surrounding pixels by the weights, -/// summing the results, dividing by the divisor, and adding the bias. -#[derive(Debug, Clone, PartialEq)] -pub struct ConvolutionKernel { - /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5). - /// The kernel must be square, so this defines both width and height. - pub size: u32, - /// Kernel weight values in row-major order. - /// Length must equal size × size. Center of kernel is typically at (size/2, size/2). - pub values: Vec, - /// Normalization divisor applied to the convolution result. - /// Common practice is to use the sum of all weights for averaging, or 1.0 otherwise. - pub divisor: f32, - /// Bias value added to the result after normalization. - /// Useful for edge detection or emboss effects to shift the result range. - pub bias: f32, - /// Whether to preserve the alpha channel unchanged. - /// If true, convolution only applies to RGB; if false, it applies to RGBA. - pub preserve_alpha: bool, + /// These operators modify the shape of objects by expanding or contracting them. + /// They work by examining neighborhoods of pixels and applying min/max operations. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum MorphologyOperator { + /// Erode operation (shrink/thin shapes). + /// + /// Makes objects smaller by removing pixels at the edges. Takes the minimum + /// value in the neighborhood. Useful for removing noise or separating touching objects. + Erode, + /// Dilate operation (expand/thicken shapes). + /// + /// Makes objects larger by adding pixels at the edges. Takes the maximum + /// value in the neighborhood. Useful for filling holes or connecting nearby objects. + Dilate, + } } -/// Types of turbulence noise generation. -/// -/// Determines the algorithm used for generating procedural noise patterns. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TurbulenceType { - /// Fractal noise (smooth, natural-looking Perlin noise). +mod turbulence { + /// Types of turbulence noise generation. /// - /// Creates smooth, continuous patterns suitable for natural textures - /// like clouds, marble, wood grain, or terrain. - FractalNoise, - /// Turbulence noise (more chaotic and energetic). - /// - /// Creates more chaotic patterns with sharper transitions, - /// suitable for fire, smoke, or turbulent effects. - Turbulence, + /// Determines the algorithm used for generating procedural noise patterns. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum TurbulenceType { + /// Fractal noise (smooth, natural-looking Perlin noise). + /// + /// Creates smooth, continuous patterns suitable for natural textures + /// like clouds, marble, wood grain, or terrain. + FractalNoise, + /// Turbulence noise (more chaotic and energetic). + /// + /// Creates more chaotic patterns with sharper transitions, + /// suitable for fire, smoke, or turbulent effects. + Turbulence, + } } /// Color channels for displacement mapping and channel selection. @@ -754,111 +739,115 @@ pub enum ColorChannel { Alpha, } -/// Transfer functions for component transfer operations. -/// -/// These functions map input color channel values to output values, -/// enabling gamma correction, color grading, and custom color curves. -/// Input and output values are typically in the range [0, 1]. -#[derive(Debug, Clone, PartialEq)] -pub enum TransferFunction { - /// Identity function (output = input, no change). - Identity, - /// Table lookup with linear interpolation. - /// - /// Maps input values using a lookup table with linear interpolation between entries. - /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate. - Table { - /// Lookup table values defining the transfer curve. - /// More values provide smoother curves. Minimum 2 values required. - values: Vec, - }, - /// Discrete step function (posterization). - /// - /// Maps input to discrete output values without interpolation, creating step/banding effects. - /// Each segment gets a constant output value from the table. - Discrete { - /// Step values for each discrete output level. - /// Input range is divided into len(values) segments, each mapping to one value. - values: Vec, - }, - /// Linear function: output = slope × input + intercept. - /// - /// Simple linear transformation of the input value. - Linear { - /// Slope coefficient (rate of change). - slope: f32, - /// Intercept offset (constant added to result). - intercept: f32, - }, - /// Gamma correction: output = amplitude × input^exponent + offset. - /// - /// Applies power-law transformation, commonly used for gamma correction and - /// adjusting midtone brightness without affecting blacks or whites. - Gamma { - /// Amplitude multiplier applied to the result. - amplitude: f32, - /// Gamma exponent (< 1 brightens, > 1 darkens midtones). - exponent: f32, - /// Offset added to the final result. - offset: f32, - }, +pub mod component_transfer { + /// Transfer functions for component transfer operations. + /// + /// These functions map input color channel values to output values, + /// enabling gamma correction, color grading, and custom color curves. + /// Input and output values are typically in the range [0, 1]. + #[derive(Debug, Clone, PartialEq)] + pub enum TransferFunction { + /// Identity function (output = input, no change). + Identity, + /// Table lookup with linear interpolation. + /// + /// Maps input values using a lookup table with linear interpolation between entries. + /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate. + Table { + /// Lookup table values defining the transfer curve. + /// More values provide smoother curves. Minimum 2 values required. + values: Vec, + }, + /// Discrete step function (posterization). + /// + /// Maps input to discrete output values without interpolation, creating step/banding effects. + /// Each segment gets a constant output value from the table. + Discrete { + /// Step values for each discrete output level. + /// Input range is divided into len(values) segments, each mapping to one value. + values: Vec, + }, + /// Linear function: output = slope × input + intercept. + /// + /// Simple linear transformation of the input value. + Linear { + /// Slope coefficient (rate of change). + slope: f32, + /// Intercept offset (constant added to result). + intercept: f32, + }, + /// Gamma correction: output = amplitude × input^exponent + offset. + /// + /// Applies power-law transformation, commonly used for gamma correction and + /// adjusting midtone brightness without affecting blacks or whites. + Gamma { + /// Amplitude multiplier applied to the result. + amplitude: f32, + /// Gamma exponent (< 1 brightens, > 1 darkens midtones). + exponent: f32, + /// Offset added to the final result. + offset: f32, + }, + } } -/// Light source configurations for lighting effects. -/// -/// Defines different types of light sources used in diffuse and specular lighting -/// filter primitives. Each type has different characteristics and use cases. -#[derive(Debug, Clone, PartialEq)] -pub enum LightSource { - /// Distant light source (infinitely far away, like the sun). - /// - /// All rays are parallel, creating uniform lighting across the surface. - /// Direction is specified using spherical coordinates (azimuth and elevation). - Distant { - /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up). - /// Defines the horizontal direction of the light. - azimuth: f32, - /// Elevation angle in degrees (0° = horizon, 90° = directly overhead). - /// Defines the vertical angle of the light source. - elevation: f32, - }, - /// Point light source at a specific 3D position. - /// - /// Light radiates uniformly in all directions from a single point. - /// Intensity decreases with distance. Like a light bulb. - Point { - /// Light source X coordinate in user space. - x: f32, - /// Light source Y coordinate in user space. - y: f32, - /// Light source Z coordinate (height above the surface). - /// Larger values create softer lighting across larger areas. - z: f32, - }, - /// Spot light with position, direction, and cone angle. - /// - /// Light emanates from a point in a specific direction with limited spread. - /// Like a flashlight or stage spotlight with adjustable focus. - Spot { - /// Light source X coordinate in user space. - x: f32, - /// Light source Y coordinate in user space. - y: f32, - /// Light source Z coordinate (height above the surface). - z: f32, - /// X coordinate the spotlight is aimed at. - points_at_x: f32, - /// Y coordinate the spotlight is aimed at. - points_at_y: f32, - /// Z coordinate the spotlight is aimed at. - points_at_z: f32, - /// Specular exponent controlling the focus/sharpness of the spotlight beam. - /// Higher values create tighter, more focused beams. - specular_exponent: f32, - /// Optional cone angle in degrees limiting the spotlight spread. - /// If None, the light spreads based only on the specular exponent. - limiting_cone_angle: Option, - }, +pub mod lighting { + /// Light source configurations for lighting effects. + /// + /// Defines different types of light sources used in diffuse and specular lighting + /// filter primitives. Each type has different characteristics and use cases. + #[derive(Debug, Clone, PartialEq)] + pub enum LightSource { + /// Distant light source (infinitely far away, like the sun). + /// + /// All rays are parallel, creating uniform lighting across the surface. + /// Direction is specified using spherical coordinates (azimuth and elevation). + Distant { + /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up). + /// Defines the horizontal direction of the light. + azimuth: f32, + /// Elevation angle in degrees (0° = horizon, 90° = directly overhead). + /// Defines the vertical angle of the light source. + elevation: f32, + }, + /// Point light source at a specific 3D position. + /// + /// Light radiates uniformly in all directions from a single point. + /// Intensity decreases with distance. Like a light bulb. + Point { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + /// Larger values create softer lighting across larger areas. + z: f32, + }, + /// Spot light with position, direction, and cone angle. + /// + /// Light emanates from a point in a specific direction with limited spread. + /// Like a flashlight or stage spotlight with adjustable focus. + Spot { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + z: f32, + /// X coordinate the spotlight is aimed at. + points_at_x: f32, + /// Y coordinate the spotlight is aimed at. + points_at_y: f32, + /// Z coordinate the spotlight is aimed at. + points_at_z: f32, + /// Specular exponent controlling the focus/sharpness of the spotlight beam. + /// Higher values create tighter, more focused beams. + specular_exponent: f32, + /// Optional cone angle in degrees limiting the spotlight spread. + /// If None, the light spreads based only on the specular exponent. + limiting_cone_angle: Option, + }, + } } /// Common color transformation matrices. @@ -903,8 +892,31 @@ pub mod matrices { /// /// These kernels are used with the `ConvolveMatrix` filter primitive /// for various image processing effects. All provided kernels are 3x3. -pub mod kernels { - use super::ConvolutionKernel; +pub mod convolution { + + /// Convolution kernel for custom filtering operations. + /// + /// Defines a square matrix of weights used for convolution-based image processing. + /// The kernel is applied to each pixel by multiplying surrounding pixels by the weights, + /// summing the results, dividing by the divisor, and adding the bias. + #[derive(Debug, Clone, PartialEq)] + pub struct ConvolutionKernel { + /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5). + /// The kernel must be square, so this defines both width and height. + pub size: u32, + /// Kernel weight values in row-major order. + /// Length must equal size × size. Center of kernel is typically at (size/2, size/2). + pub values: Vec, + /// Normalization divisor applied to the convolution result. + /// Common practice is to use the sum of all weights for averaging, or 1.0 otherwise. + pub divisor: f32, + /// Bias value added to the result after normalization. + /// Useful for edge detection or emboss effects to shift the result range. + pub bias: f32, + /// Whether to preserve the alpha channel unchanged. + /// If true, convolution only applies to RGB; if false, it applies to RGBA. + pub preserve_alpha: bool, + } /// 3x3 Gaussian blur kernel for basic smoothing. pub fn gaussian_3x3() -> ConvolutionKernel { From 578f1a14ef6a1be1229a11d4c4dff7e38d9365ec Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 16:23:38 +0100 Subject: [PATCH 08/29] Delete CompoundFilter type Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 61 --------------------------------- 1 file changed, 61 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 8f54645..ad2255a 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -567,67 +567,6 @@ pub enum FilterSource { StrokePaint, } -/// Pre-built compound effects for common use cases. -/// -/// These effects combine multiple filter primitives into commonly-used visual effects. -/// They provide a convenient high-level API for complex multi-step filter operations. -/// -/// **Note:** These are planned but not yet implemented. Use `FilterGraph` to manually -/// construct these effects from primitives. -#[derive(Debug, Clone)] -pub enum CompoundFilter { - /// Inner shadow effect (shadow inside the shape). - /// - /// Creates a shadow that appears inside the boundaries of the shape, - /// giving a recessed or inset appearance. This is the opposite of a drop shadow. - InnerShadow { - /// Horizontal offset of the shadow in pixels. Positive values shift right. - dx: f32, - /// Vertical offset of the shadow in pixels. Positive values shift down. - dy: f32, - /// Blur radius for the shadow in pixels. Larger values create softer shadows. - blur: f32, - /// Shadow color with alpha channel. - color: AlphaColor, - }, - /// Glow effect around the shape. - /// - /// Creates a soft glowing halo around the shape by blurring and - /// compositing a colored version with the original. - Glow { - /// Blur radius for the glow in pixels. Larger values create softer glows. - blur: f32, - /// Glow color with alpha channel. - color: AlphaColor, - }, - /// Bevel effect (3D raised/recessed appearance). - /// - /// Creates a 3D beveled edge effect using lighting simulation, - /// making the shape appear raised or recessed from the surface. - Bevel { - /// Light source angle in degrees (0° = right, 90° = up). - angle: f32, - /// Width of the bevel edge in pixels. - distance: f32, - /// Color for the highlight (lit) side of the bevel. - highlight: AlphaColor, - /// Color for the shadow (dark) side of the bevel. - shadow: AlphaColor, - }, - /// Emboss effect for a raised relief appearance. - /// - /// Creates an embossed/stamped appearance by simulating lighting - /// on a raised surface based on the shape's alpha channel. - Emboss { - /// Light angle in degrees determining emboss direction. - angle: f32, - /// Depth of the emboss effect. - depth: f32, - /// Overall strength/intensity of the effect (0.0 = none, 1.0 = full). - amount: f32, - }, -} - /// Composite operators for combining filter inputs. /// /// These are the Porter-Duff compositing operators used to combine two images. From 4429d264f8be0bc390d57b369cba3a9cbb4d93e3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 16:56:40 +0100 Subject: [PATCH 09/29] Convert struct-like enum to standlone structs Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 666 ++++++++++++++++++-------------- 1 file changed, 368 insertions(+), 298 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index ad2255a..1353f57 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -8,28 +8,35 @@ //! to follow the W3C Filter Effects Module Level 1 specification. //! //! See: -//! -//! ## Implementation Status -//! -//! ### Implemented -//! -//! **Filter Functions:** -//! - `Blur` - Gaussian blur effect -//! -//! **Filter Primitives (Single Use Only):** -//! - `Flood` - Solid color fill -//! - `GaussianBlur` - Gaussian blur filter -//! - `DropShadow` - Drop shadow effect (compound primitive) -//! - `Offset` - Translation/shift (single primitive) -//! -use kurbo::{Affine, Rect}; +// ## Vello Implementation Status +// +// ### Implemented +// +// **Filter Functions:** +// - `Blur` - Gaussian blur effect +// +// **Filter Primitives (Single Use Only):** +// - `Flood` - Solid color fill +// - `GaussianBlur` - Gaussian blur filter +// - `DropShadow` - Drop shadow effect (compound primitive) +// - `Offset` - Translation/shift (single primitive) +// + +use kurbo::{Affine, Rect, Vec2}; use peniko::color::{AlphaColor, Srgb}; use smallvec::SmallVec; -use crate::filters::{ - component_transfer::TransferFunction, convolution::ConvolutionKernel, lighting::LightSource, - morphology::MorphologyOperator, turbulence::TurbulenceType, +use self::{ + blur::GaussianBlurFilter, + component_transfer::ComponentTransferFilter, + composite::CompositeOperator, + convolution::ConvolutionKernel, + displacement::DisplacementMapFilter, + lighting::{DiffuseLightingFilter, SpecularLightingFilter}, + morphology::MorphologyFilter, + shadow::DropShadow, + turbulence::TurbulenceFilter, }; /// A directed acyclic graph (DAG) of filter operations. @@ -64,10 +71,10 @@ impl Filter { /// Applies a Gaussian blur to the input image. Larger radius values /// produce more blur. The blur is applied equally in all directions. pub fn blur(radius: f32) -> Self { - Self::single(FilterEffect::GaussianBlur { + Self::single(FilterEffect::GaussianBlur(GaussianBlurFilter { std_deviation: radius, edge_mode: EdgeMode::None, - }) + })) } /// Drop shadow effect (compound primitive). @@ -78,13 +85,13 @@ impl Filter { /// /// See: pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { - Self::single(FilterEffect::DropShadow { + Self::single(FilterEffect::DropShadow(DropShadow { dx, dy, std_deviation, color, edge_mode: EdgeMode::None, - }) + })) } /// Create a new empty filter graph. @@ -224,31 +231,16 @@ pub enum FilterEffect { /// /// Creates a rectangle filled with the specified color, typically used as /// input to other filter operations (e.g., for colored shadows). - Flood { - /// Fill color with alpha channel. - color: AlphaColor, - }, + Flood(AlphaColor), + /// Gaussian blur filter. /// /// Applies a Gaussian blur using the specified standard deviation (σ). /// The effective blur range (distance over which pixels are sampled) is /// approximately 3 × `std_deviation`, as this captures ~99.7% of the /// Gaussian distribution. - GaussianBlur { - /// Standard deviation for the blur kernel. Larger values create more blur. - /// Must be non-negative. A value of 0 means no blur. - /// - /// This directly corresponds to the σ (sigma) parameter in the Gaussian - /// function. The visible blur effect extends approximately 3σ in each direction. - /// - /// TODO: Per the W3C specification, this should support separate x and y values. - /// The spec allows `stdDeviation` to be either one number (applied to both axes) - /// or two numbers (first for x-axis, second for y-axis). Currently only uniform - /// blur is supported. Consider changing to `(f32, f32)` or a dedicated type. - std_deviation: f32, - /// Edge mode determining how pixels beyond the input bounds are handled. - edge_mode: EdgeMode, - }, + GaussianBlur(GaussianBlurFilter), + /// Drop shadow effect (compound primitive). /// /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, @@ -256,164 +248,85 @@ pub enum FilterEffect { /// combines multiple primitive operations into one. /// /// See: - DropShadow { - /// Horizontal offset of the shadow in pixels. Positive values shift right. - dx: f32, - /// Vertical offset of the shadow in pixels. Positive values shift down. - dy: f32, - /// Blur standard deviation for the shadow. Larger values create softer shadows. - std_deviation: f32, - /// Shadow color with alpha channel. Alpha controls shadow opacity. - color: AlphaColor, - /// Edge mode for handling boundaries during blur operation. - /// Default is `EdgeMode::None` per SVG spec. - edge_mode: EdgeMode, - }, - // - // ============================================================ - // TODO: The following filter primitives are not yet implemented - // ============================================================ - // + DropShadow(DropShadow), + /// Matrix-based color transformation. /// /// Applies a 4x5 matrix transformation to colors, allowing arbitrary /// color space transformations, hue shifts, and color adjustments. - ColorMatrix { - /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). - /// Each output channel is computed as a linear combination of input channels plus offset. - matrix: [f32; 20], - }, + /// + /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). + /// Each output channel is computed as a linear combination of input channels plus offset. + ColorMatrix([f32; 20]), + /// Geometric offset/translation. /// /// Shifts the input image by the specified offset. Useful for creating /// shadow effects or positioning elements in a filter graph. - Offset { - /// Horizontal offset in pixels. Positive values shift right. - dx: f32, - /// Vertical offset in pixels. Positive values shift down. - dy: f32, - }, + /// + /// Positive values shift right or down. + Offset(Vec2), /// Composite two inputs using Porter-Duff compositing operations. /// /// Combines two input images using standard compositing operators /// (over, in, out, atop, xor) or custom arithmetic combination. - Composite { - /// Porter-Duff compositing operator to apply. - operator: CompositeOperator, - }, + Composite(CompositeOperator), + /// Blend two inputs using blend modes. /// /// Combines two input images using Photoshop-style blend modes /// (multiply, screen, overlay, etc.). - Blend { - /// Blend mode determining how colors are combined. - mode: BlendMode, - }, + Blend(BlendMode), + /// Morphological operations (dilate/erode). /// /// Expands (dilate) or contracts (erode) the shapes in the input image. /// Useful for creating outline effects or cleaning up edges. - Morphology { - /// Morphological operator determining whether to erode or dilate. - operator: MorphologyOperator, - /// Operation radius in pixels. Larger values create stronger effects. - radius: f32, - }, + Morphology(MorphologyFilter), /// Custom convolution kernel for image processing. /// /// Applies a custom convolution matrix to the input image, enabling /// effects like sharpening, edge detection, embossing, and custom filters. - ConvolveMatrix { - /// Convolution kernel specification including size, values, and normalization. - kernel: ConvolutionKernel, - }, + ConvolveMatrix(ConvolutionKernel), + /// Generate Perlin noise/turbulence patterns. /// /// Creates procedural noise patterns useful for textures, clouds, /// marble effects, and other organic-looking randomness. - Turbulence { - /// Base frequency for noise generation. Higher values create finer detail. - base_frequency: f32, - /// Number of octaves for fractal noise. More octaves add finer detail. - num_octaves: u32, - /// Random seed for reproducible noise generation. - seed: u32, - /// Type of noise: smooth fractal or more chaotic turbulence. - turbulence_type: TurbulenceType, - }, + Turbulence(TurbulenceFilter), + /// Displace pixels using a displacement map. /// /// Uses the color values from a second input to spatially displace pixels /// in the primary input, creating warping and distortion effects. - DisplacementMap { - /// Scale factor controlling the displacement intensity. - scale: f32, - /// Color channel from the displacement map used for X-axis displacement. - x_channel: ColorChannel, - /// Color channel from the displacement map used for Y-axis displacement. - y_channel: ColorChannel, - }, + DisplacementMap(DisplacementMapFilter), + /// Per-channel component transfer using lookup tables or functions. /// /// Applies independent transfer functions to each color channel, /// enabling color corrections, gamma adjustments, and custom mappings. - ComponentTransfer { - /// Transfer function applied to the red channel (None = identity). - red_function: Option, - /// Transfer function applied to the green channel (None = identity). - green_function: Option, - /// Transfer function applied to the blue channel (None = identity). - blue_function: Option, - /// Transfer function applied to the alpha channel (None = identity). - alpha_function: Option, - }, - /// Reference an external image as filter input. - /// - /// Allows using pre-existing images (from an atlas or resource) as - /// input to filter operations, useful for texturing and overlays. - Image { - /// Identifier referencing an image in the resource atlas. - image_id: u32, - /// Optional 2D affine transformation matrix [a, b, c, d, e, f]. - /// Transforms the image before using it as filter input. - transform: Option<[f32; 6]>, - }, + ComponentTransfer(ComponentTransferFilter), + + Image(ExternalImageSource), + /// Tile the input to fill the filter region. /// /// Repeats the input image to fill the entire filter primitive subregion, /// creating a tiling/repeating pattern. Tile, + /// Diffuse lighting simulation. /// /// Creates a lighting effect by treating the input's alpha channel as a height map /// and calculating diffuse (matte) reflection from a light source. - DiffuseLighting { - /// Surface scale factor for converting alpha values to heights. - surface_scale: f32, - /// Diffuse reflection constant (kd). Controls lighting intensity. - diffuse_constant: f32, - /// Kernel unit length for gradient calculations in user space. - kernel_unit_length: f32, - /// Configuration of the light source (point, distant, or spot). - light_source: LightSource, - }, + DiffuseLighting(DiffuseLightingFilter), + /// Specular lighting simulation. /// /// Creates a lighting effect by treating the input's alpha channel as a height map /// and calculating specular (shiny) reflection highlights from a light source. - SpecularLighting { - /// Surface scale factor for converting alpha values to heights. - surface_scale: f32, - /// Specular reflection constant (ks). Controls highlight intensity. - specular_constant: f32, - /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). - specular_exponent: f32, - /// Kernel unit length for gradient calculations in user space. - kernel_unit_length: f32, - /// Configuration of the light source (point, distant, or spot). - light_source: LightSource, - }, + SpecularLighting(SpecularLightingFilter), } impl FilterEffect { @@ -433,23 +346,23 @@ impl FilterEffect { /// Most filters that don't sample neighboring pixels return `Rect::ZERO`. pub fn expansion_rect(&self) -> Rect { match self { - Self::GaussianBlur { std_deviation, .. } => { + Self::GaussianBlur(blur) => { // Gaussian blur expands uniformly by 3*sigma (covers 99.7% of distribution) - let radius = (*std_deviation * 3.0) as f64; + let radius = (blur.std_deviation * 3.0) as f64; Rect::new(-radius, -radius, radius, radius) } - Self::Offset { dx, dy } => { + Self::Offset(offset) => { // Offset shifts pixels; expand bounds asymmetrically so shifted content isn't cut. - let dx = *dx as f64; - let dy = *dy as f64; + let dx = offset.x; + let dy = offset.y; Rect::new(dx.min(0.0), dy.min(0.0), dx.max(0.0), dy.max(0.0)) } - Self::DropShadow { + Self::DropShadow(DropShadow { std_deviation, dx, dy, .. - } => { + }) => { // Drop shadow = blur + offset + composite with original // The expansion rect encompasses both the blur and the offset let blur_radius = (*std_deviation * 3.0) as f64; @@ -472,11 +385,11 @@ impl FilterEffect { #[cfg(test)] mod offset_expansion_tests { use super::FilterEffect; - use kurbo::Rect; + use kurbo::{Rect, Vec2}; #[test] fn offset_expands_in_direction_of_shift() { - let p = FilterEffect::Offset { dx: 2.5, dy: -3.0 }; + let p = FilterEffect::Offset(Vec2 { x: 2.5, y: -3.0 }); assert_eq!( p.expansion_rect(), Rect::new(0.0, -3.0, 2.5, 0.0), @@ -567,53 +480,6 @@ pub enum FilterSource { StrokePaint, } -/// Composite operators for combining filter inputs. -/// -/// These are the Porter-Duff compositing operators used to combine two images. -/// Each operator defines how the source (input 1) and destination (input 2) -/// are combined based on their color and alpha values. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum CompositeOperator { - /// Source over destination (standard alpha blending). - /// - /// The source is composited over the destination. This is the most common - /// blending mode where source alpha determines visibility. - Over, - /// Source in destination (intersection). - /// - /// The source is only visible where the destination is opaque. - /// Result alpha = `source_alpha` × `dest_alpha`. - In, - /// Source out destination (subtract). - /// - /// The source is only visible where the destination is transparent. - /// Useful for masking/cutting out regions. - Out, - /// Source atop destination. - /// - /// Source is composited over destination, but only where destination is opaque. - Atop, - /// Source XOR destination (exclusive or). - /// - /// Shows source where destination is transparent and vice versa, - /// but not where both are opaque. - Xor, - /// Arithmetic combination with custom coefficients. - /// - /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4. - /// Allows creating custom compositing operations beyond the standard Porter-Duff set. - Arithmetic { - /// Coefficient k1 for the (source * destination) term. - k1: f32, - /// Coefficient k2 for the source term. - k2: f32, - /// Coefficient k3 for the destination term. - k3: f32, - /// Constant offset k4 added to the result. - k4: f32, - }, -} - /// Blend modes for combining colors. /// /// These are blend modes that define how to combine the colors @@ -623,7 +489,127 @@ pub enum CompositeOperator { /// See: pub type BlendMode = peniko::Mix; -mod morphology { +pub mod composite { + /// Composite operators for combining filter inputs. + /// + /// These are the Porter-Duff compositing operators used to combine two images. + /// Each operator defines how the source (input 1) and destination (input 2) + /// are combined based on their color and alpha values. + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum CompositeOperator { + /// Source over destination (standard alpha blending). + /// + /// The source is composited over the destination. This is the most common + /// blending mode where source alpha determines visibility. + Over, + /// Source in destination (intersection). + /// + /// The source is only visible where the destination is opaque. + /// Result alpha = `source_alpha` × `dest_alpha`. + In, + /// Source out destination (subtract). + /// + /// The source is only visible where the destination is transparent. + /// Useful for masking/cutting out regions. + Out, + /// Source atop destination. + /// + /// Source is composited over destination, but only where destination is opaque. + Atop, + /// Source XOR destination (exclusive or). + /// + /// Shows source where destination is transparent and vice versa, + /// but not where both are opaque. + Xor, + + Arithmetic(ArithmeticCompositeOperator), + } + + /// Arithmetic combination with custom coefficients. + /// + /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4. + /// Allows creating custom compositing operations beyond the standard Porter-Duff set. + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct ArithmeticCompositeOperator { + pub k1: f32, + pub k2: f32, + pub k3: f32, + pub k4: f32, + } +} + +mod blur { + use crate::filters::EdgeMode; + + /// Gaussian blur filter. + /// + /// Applies a Gaussian blur using the specified standard deviation (σ). + /// The effective blur range (distance over which pixels are sampled) is + /// approximately 3 × `std_deviation`, as this captures ~99.7% of the + /// Gaussian distribution. + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct GaussianBlurFilter { + /// Standard deviation for the blur kernel. Larger values create more blur. + /// Must be non-negative. A value of 0 means no blur. + /// + /// This directly corresponds to the σ (sigma) parameter in the Gaussian + /// function. The visible blur effect extends approximately 3σ in each direction. + /// + /// TODO: Per the W3C specification, this should support separate x and y values. + /// The spec allows `stdDeviation` to be either one number (applied to both axes) + /// or two numbers (first for x-axis, second for y-axis). Currently only uniform + /// blur is supported. Consider changing to `(f32, f32)` or a dedicated type. + pub std_deviation: f32, + /// Edge mode determining how pixels beyond the input bounds are handled. + pub edge_mode: EdgeMode, + } +} + +pub mod shadow { + use super::EdgeMode; + use peniko::color::{AlphaColor, Srgb}; + + /// Drop shadow effect (compound primitive). + /// + /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, + /// and compositing it with the original. This is a compound operation that + /// combines multiple primitive operations into one. + /// + /// See: + #[derive(Debug, Clone, PartialEq)] + pub struct DropShadow { + pub dx: f32, + pub dy: f32, + pub std_deviation: f32, + pub color: AlphaColor, + pub edge_mode: EdgeMode, + } +} + +/// Reference an external image as filter input. +/// +/// Allows using pre-existing images (from an atlas or resource) as +/// input to filter operations, useful for texturing and overlays. +#[derive(Debug, Clone, PartialEq)] +pub struct ExternalImageSource { + pub image_id: u32, + pub transform: Option<[f32; 6]>, +} + +pub mod morphology { + + /// Morphological operations (dilate/erode). + /// + /// Expands (dilate) or contracts (erode) the shapes in the input image. + /// Useful for creating outline effects or cleaning up edges. + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct MorphologyFilter { + /// Morphological operator determining whether to erode or dilate. + pub operator: MorphologyOperator, + /// Operation radius in pixels. Larger values create stronger effects. + pub radius: f32, + } + /// Morphological operators for dilate/erode operations. /// /// These operators modify the shape of objects by expanding or contracting them. @@ -643,7 +629,23 @@ mod morphology { } } -mod turbulence { +pub mod turbulence { + /// Generate Perlin noise/turbulence patterns. + /// + /// Creates procedural noise patterns useful for textures, clouds, + /// marble effects, and other organic-looking randomness. + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct TurbulenceFilter { + /// Base frequency for noise generation. Higher values create finer detail. + pub base_frequency: f32, + /// Number of octaves for fractal noise. More octaves add finer detail. + pub num_octaves: u32, + /// Random seed for reproducible noise generation. + pub seed: u32, + /// Type of noise: smooth fractal or more chaotic turbulence. + pub turbulence_type: TurbulenceType, + } + /// Types of turbulence noise generation. /// /// Determines the algorithm used for generating procedural noise patterns. @@ -662,23 +664,57 @@ mod turbulence { } } -/// Color channels for displacement mapping and channel selection. -/// -/// Specifies which color channel to use for operations that need to -/// extract or reference individual channels from an image. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ColorChannel { - /// Red color channel (R component). - Red, - /// Green color channel (G component). - Green, - /// Blue color channel (B component). - Blue, - /// Alpha channel (transparency/opacity). - Alpha, +pub mod displacement { + + /// Displace pixels using a displacement map. + /// + /// Uses the color values from a second input to spatially displace pixels + /// in the primary input, creating warping and distortion effects. + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct DisplacementMapFilter { + /// Scale factor controlling the displacement intensity. + pub scale: f32, + /// Color channel from the displacement map used for X-axis displacement. + pub x_channel: ColorChannel, + /// Color channel from the displacement map used for Y-axis displacement. + pub y_channel: ColorChannel, + } + + /// Color channels for displacement mapping and channel selection. + /// + /// Specifies which color channel to use for operations that need to + /// extract or reference individual channels from an image. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum ColorChannel { + /// Red color channel (R component). + Red, + /// Green color channel (G component). + Green, + /// Blue color channel (B component). + Blue, + /// Alpha channel (transparency/opacity). + Alpha, + } } pub mod component_transfer { + + /// Per-channel component transfer using lookup tables or functions. + /// + /// Applies independent transfer functions to each color channel, + /// enabling color corrections, gamma adjustments, and custom mappings. + #[derive(Debug, Clone, PartialEq)] + pub struct ComponentTransferFilter { + /// Transfer function applied to the red channel (None = identity). + pub red_function: Option, + /// Transfer function applied to the green channel (None = identity). + pub green_function: Option, + /// Transfer function applied to the blue channel (None = identity). + pub blue_function: Option, + /// Transfer function applied to the alpha channel (None = identity). + pub alpha_function: Option, + } + /// Transfer functions for component transfer operations. /// /// These functions map input color channel values to output values, @@ -688,49 +724,89 @@ pub mod component_transfer { pub enum TransferFunction { /// Identity function (output = input, no change). Identity, + /// Table lookup with linear interpolation. /// /// Maps input values using a lookup table with linear interpolation between entries. /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate. - Table { - /// Lookup table values defining the transfer curve. - /// More values provide smoother curves. Minimum 2 values required. - values: Vec, - }, + /// + /// Lookup table values defining the transfer curve. + /// More values provide smoother curves. Minimum 2 values required. + Table(Vec), + /// Discrete step function (posterization). /// /// Maps input to discrete output values without interpolation, creating step/banding effects. /// Each segment gets a constant output value from the table. - Discrete { - /// Step values for each discrete output level. - /// Input range is divided into len(values) segments, each mapping to one value. - values: Vec, - }, - /// Linear function: output = slope × input + intercept. - /// - /// Simple linear transformation of the input value. - Linear { - /// Slope coefficient (rate of change). - slope: f32, - /// Intercept offset (constant added to result). - intercept: f32, - }, - /// Gamma correction: output = amplitude × input^exponent + offset. /// - /// Applies power-law transformation, commonly used for gamma correction and - /// adjusting midtone brightness without affecting blacks or whites. - Gamma { - /// Amplitude multiplier applied to the result. - amplitude: f32, - /// Gamma exponent (< 1 brightens, > 1 darkens midtones). - exponent: f32, - /// Offset added to the final result. - offset: f32, - }, + /// Step values for each discrete output level. + /// Input range is divided into len(values) segments, each mapping to one value. + Discrete(Vec), + + /// Linear function: output = slope × input + intercept. + Linear(Linearfunction), + + // Gamma correction: output = amplitude × input^exponent + offset. + Gamma(Gammafunction), + } + + /// Linear function: output = slope × input + intercept. + /// + /// Simple linear transformation of the input value. + #[derive(Debug, Clone, PartialEq)] + pub struct Linearfunction { + pub slope: f32, + pub intercept: f32, + } + + /// Gamma correction: output = amplitude × input^exponent + offset. + /// + /// Applies power-law transformation, commonly used for gamma correction and + /// adjusting midtone brightness without affecting blacks or whites. + #[derive(Debug, Clone, PartialEq)] + pub struct Gammafunction { + pub amplitude: f32, + pub exponent: f32, + pub offset: f32, } } pub mod lighting { + + /// Diffuse lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating diffuse (matte) reflection from a light source. + #[derive(Debug, Clone, PartialEq)] + pub struct DiffuseLightingFilter { + /// Surface scale factor for converting alpha values to heights. + pub surface_scale: f32, + /// Diffuse reflection constant (kd). Controls lighting intensity. + pub diffuse_constant: f32, + /// Kernel unit length for gradient calculations in user space. + pub kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + pub light_source: LightSource, + } + + /// Specular lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating specular (shiny) reflection highlights from a light source. + #[derive(Debug, Clone, PartialEq)] + pub struct SpecularLightingFilter { + /// Surface scale factor for converting alpha values to heights. + pub surface_scale: f32, + /// Specular reflection constant (ks). Controls highlight intensity. + pub specular_constant: f32, + /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). + pub specular_exponent: f32, + /// Kernel unit length for gradient calculations in user space. + pub kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + pub light_source: LightSource, + } + /// Light source configurations for lighting effects. /// /// Defines different types of light sources used in diffuse and specular lighting @@ -738,54 +814,48 @@ pub mod lighting { #[derive(Debug, Clone, PartialEq)] pub enum LightSource { /// Distant light source (infinitely far away, like the sun). - /// - /// All rays are parallel, creating uniform lighting across the surface. - /// Direction is specified using spherical coordinates (azimuth and elevation). - Distant { - /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up). - /// Defines the horizontal direction of the light. - azimuth: f32, - /// Elevation angle in degrees (0° = horizon, 90° = directly overhead). - /// Defines the vertical angle of the light source. - elevation: f32, - }, + Distant(DistantLightSource), /// Point light source at a specific 3D position. - /// - /// Light radiates uniformly in all directions from a single point. - /// Intensity decreases with distance. Like a light bulb. - Point { - /// Light source X coordinate in user space. - x: f32, - /// Light source Y coordinate in user space. - y: f32, - /// Light source Z coordinate (height above the surface). - /// Larger values create softer lighting across larger areas. - z: f32, - }, + Point(PointLightSource), /// Spot light with position, direction, and cone angle. - /// - /// Light emanates from a point in a specific direction with limited spread. - /// Like a flashlight or stage spotlight with adjustable focus. - Spot { - /// Light source X coordinate in user space. - x: f32, - /// Light source Y coordinate in user space. - y: f32, - /// Light source Z coordinate (height above the surface). - z: f32, - /// X coordinate the spotlight is aimed at. - points_at_x: f32, - /// Y coordinate the spotlight is aimed at. - points_at_y: f32, - /// Z coordinate the spotlight is aimed at. - points_at_z: f32, - /// Specular exponent controlling the focus/sharpness of the spotlight beam. - /// Higher values create tighter, more focused beams. - specular_exponent: f32, - /// Optional cone angle in degrees limiting the spotlight spread. - /// If None, the light spreads based only on the specular exponent. - limiting_cone_angle: Option, - }, + Spot(SpotLightSource), + } + + /// Distant light source (infinitely far away, like the sun). + /// + /// All rays are parallel, creating uniform lighting across the surface. + /// Direction is specified using spherical coordinates (azimuth and elevation). + #[derive(Debug, Clone, PartialEq)] + pub struct DistantLightSource { + pub azimuth: f32, + pub elevation: f32, + } + + /// Point light source at a specific 3D position. + /// + /// Light radiates uniformly in all directions from a single point. + /// Intensity decreases with distance. Like a light bulb. + #[derive(Debug, Clone, PartialEq)] + pub struct PointLightSource { + pub x: f32, + pub y: f32, + pub z: f32, + } + + /// Spot light with position, direction, and cone angle. + /// + /// Light emanates from a point in a specific direction with limited spread. + /// Like a flashlight or stage spotlight with adjustable focus. + #[derive(Debug, Clone, PartialEq)] + pub struct SpotLightSource { + pub x: f32, + pub y: f32, + pub z: f32, + pub points_at_x: f32, + pub points_at_y: f32, + pub points_at_z: f32, + pub specular_exponent: f32, + pub limiting_cone_angle: Option, } } @@ -793,7 +863,7 @@ pub mod lighting { /// /// These 4x5 matrices are used with the `ColorMatrix` filter primitive. /// Each row transforms a color channel: [R, G, B, A, offset]. -pub mod matrices { +pub mod color_transformation { /// Identity matrix (no change). pub const IDENTITY: [f32; 20] = [ 1.0, 0.0, 0.0, 0.0, 0.0, // Red From a598005c04c214a7199f4db7bd3b58b48a3a6243 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 17:07:01 +0100 Subject: [PATCH 10/29] Add serde derives to the filter types --- crates/anyrender/Cargo.toml | 2 +- crates/anyrender/src/filters.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/anyrender/Cargo.toml b/crates/anyrender/Cargo.toml index 86619ac..0899b32 100644 --- a/crates/anyrender/Cargo.toml +++ b/crates/anyrender/Cargo.toml @@ -22,5 +22,5 @@ peniko = { workspace = true } raw-window-handle = { workspace = true } # Serde -serde = { workspace = true, features = ["derive"], optional = true } +serde = { workspace = true, features = ["derive", "rc"], optional = true } smallvec = "1.15.1" diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 1353f57..de37d64 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -44,6 +44,7 @@ use self::{ /// The graph represents a pipeline of filter primitives where outputs of some /// primitives can be used as inputs to others. Each primitive has a unique `FilterId`. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Filter { /// All filter primitives in the graph, stored in insertion order. primitives: SmallVec<[FilterEffect; 1]>, @@ -194,6 +195,7 @@ impl Filter { /// /// See: #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum EdgeMode { /// Extend by duplicating edge pixels (clamp to edge). /// @@ -226,6 +228,7 @@ pub enum EdgeMode { /// /// See: #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum FilterEffect { /// Generate a solid color fill. /// @@ -400,10 +403,12 @@ mod offset_expansion_tests { /// Unique identifier for a filter primitive in the graph. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FilterId(pub u16); /// Input connections for a filter primitive. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FilterInputs { /// Primary input ("in" attribute in SVG). pub primary: FilterInput, @@ -435,6 +440,7 @@ impl FilterInputs { /// A single filter input. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum FilterInput { /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.). Source(FilterSource), @@ -448,6 +454,7 @@ pub enum FilterInput { /// matching the SVG filter primitive input types. These represent implicit /// inputs available to any filter primitive without requiring previous operations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum FilterSource { /// The original graphic content being filtered. /// @@ -496,6 +503,7 @@ pub mod composite { /// Each operator defines how the source (input 1) and destination (input 2) /// are combined based on their color and alpha values. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum CompositeOperator { /// Source over destination (standard alpha blending). /// @@ -530,6 +538,7 @@ pub mod composite { /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4. /// Allows creating custom compositing operations beyond the standard Porter-Duff set. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ArithmeticCompositeOperator { pub k1: f32, pub k2: f32, @@ -548,6 +557,7 @@ mod blur { /// approximately 3 × `std_deviation`, as this captures ~99.7% of the /// Gaussian distribution. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GaussianBlurFilter { /// Standard deviation for the blur kernel. Larger values create more blur. /// Must be non-negative. A value of 0 means no blur. @@ -577,6 +587,7 @@ pub mod shadow { /// /// See: #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DropShadow { pub dx: f32, pub dy: f32, @@ -591,6 +602,7 @@ pub mod shadow { /// Allows using pre-existing images (from an atlas or resource) as /// input to filter operations, useful for texturing and overlays. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ExternalImageSource { pub image_id: u32, pub transform: Option<[f32; 6]>, @@ -603,6 +615,7 @@ pub mod morphology { /// Expands (dilate) or contracts (erode) the shapes in the input image. /// Useful for creating outline effects or cleaning up edges. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MorphologyFilter { /// Morphological operator determining whether to erode or dilate. pub operator: MorphologyOperator, @@ -615,6 +628,7 @@ pub mod morphology { /// These operators modify the shape of objects by expanding or contracting them. /// They work by examining neighborhoods of pixels and applying min/max operations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum MorphologyOperator { /// Erode operation (shrink/thin shapes). /// @@ -635,6 +649,7 @@ pub mod turbulence { /// Creates procedural noise patterns useful for textures, clouds, /// marble effects, and other organic-looking randomness. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TurbulenceFilter { /// Base frequency for noise generation. Higher values create finer detail. pub base_frequency: f32, @@ -650,6 +665,7 @@ pub mod turbulence { /// /// Determines the algorithm used for generating procedural noise patterns. #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TurbulenceType { /// Fractal noise (smooth, natural-looking Perlin noise). /// @@ -671,6 +687,7 @@ pub mod displacement { /// Uses the color values from a second input to spatially displace pixels /// in the primary input, creating warping and distortion effects. #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DisplacementMapFilter { /// Scale factor controlling the displacement intensity. pub scale: f32, @@ -685,6 +702,7 @@ pub mod displacement { /// Specifies which color channel to use for operations that need to /// extract or reference individual channels from an image. #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ColorChannel { /// Red color channel (R component). Red, @@ -704,6 +722,7 @@ pub mod component_transfer { /// Applies independent transfer functions to each color channel, /// enabling color corrections, gamma adjustments, and custom mappings. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ComponentTransferFilter { /// Transfer function applied to the red channel (None = identity). pub red_function: Option, @@ -721,6 +740,7 @@ pub mod component_transfer { /// enabling gamma correction, color grading, and custom color curves. /// Input and output values are typically in the range [0, 1]. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TransferFunction { /// Identity function (output = input, no change). Identity, @@ -754,6 +774,7 @@ pub mod component_transfer { /// /// Simple linear transformation of the input value. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Linearfunction { pub slope: f32, pub intercept: f32, @@ -764,6 +785,7 @@ pub mod component_transfer { /// Applies power-law transformation, commonly used for gamma correction and /// adjusting midtone brightness without affecting blacks or whites. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Gammafunction { pub amplitude: f32, pub exponent: f32, @@ -778,6 +800,7 @@ pub mod lighting { /// Creates a lighting effect by treating the input's alpha channel as a height map /// and calculating diffuse (matte) reflection from a light source. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DiffuseLightingFilter { /// Surface scale factor for converting alpha values to heights. pub surface_scale: f32, @@ -794,6 +817,7 @@ pub mod lighting { /// Creates a lighting effect by treating the input's alpha channel as a height map /// and calculating specular (shiny) reflection highlights from a light source. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SpecularLightingFilter { /// Surface scale factor for converting alpha values to heights. pub surface_scale: f32, @@ -812,6 +836,7 @@ pub mod lighting { /// Defines different types of light sources used in diffuse and specular lighting /// filter primitives. Each type has different characteristics and use cases. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LightSource { /// Distant light source (infinitely far away, like the sun). Distant(DistantLightSource), @@ -826,6 +851,7 @@ pub mod lighting { /// All rays are parallel, creating uniform lighting across the surface. /// Direction is specified using spherical coordinates (azimuth and elevation). #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DistantLightSource { pub azimuth: f32, pub elevation: f32, @@ -836,6 +862,7 @@ pub mod lighting { /// Light radiates uniformly in all directions from a single point. /// Intensity decreases with distance. Like a light bulb. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PointLightSource { pub x: f32, pub y: f32, @@ -847,6 +874,7 @@ pub mod lighting { /// Light emanates from a point in a specific direction with limited spread. /// Like a flashlight or stage spotlight with adjustable focus. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SpotLightSource { pub x: f32, pub y: f32, @@ -909,6 +937,7 @@ pub mod convolution { /// The kernel is applied to each pixel by multiplying surrounding pixels by the weights, /// summing the results, dividing by the divisor, and adding the bias. #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ConvolutionKernel { /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5). /// The kernel must be square, so this defines both width and height. From 9cc85c45d6aa60404a378c37977c3c29e0f69590 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 17:07:48 +0100 Subject: [PATCH 11/29] Add filter and backdrop to PaintScene and LayerCommand Signed-off-by: Nico Burns --- crates/anyrender/src/lib.rs | 5 +++++ crates/anyrender/src/null_backend.rs | 4 +++- crates/anyrender/src/recording.rs | 10 +++++++++- crates/anyrender_serialize/tests/serialize.rs | 4 ++++ crates/anyrender_skia/src/scene.rs | 6 +++++- crates/anyrender_svg/src/render.rs | 4 ++++ crates/anyrender_vello/src/scene.rs | 6 +++++- crates/anyrender_vello_cpu/src/scene.rs | 6 +++++- crates/anyrender_vello_hybrid/src/scene.rs | 6 +++++- crates/anyrender_vello_hybrid/src/webgl_scene.rs | 2 ++ examples/serialize/src/main.rs | 2 ++ 11 files changed, 49 insertions(+), 6 deletions(-) diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index ee7a43d..b0e88e1 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -44,6 +44,7 @@ use recording::RenderCommand; use std::{any::Any, sync::Arc}; pub mod filters; +pub use filters::Filter; pub mod wasm_send_sync; pub use wasm_send_sync::*; pub mod types; @@ -192,6 +193,8 @@ pub trait PaintScene: RenderContext { alpha: f32, transform: Affine, clip: &impl Shape, + filter: Option>, + backdrop_filter: Option>, ); /// Pushes a new clip layer clipped by the specified shape. @@ -260,6 +263,8 @@ pub trait PaintScene: RenderContext { cmd.alpha, scene_transform * cmd.transform, &cmd.clip, + cmd.filter, + cmd.backdrop_filter, ), RenderCommand::PushClipLayer(cmd) => { self.push_clip_layer(scene_transform * cmd.transform, &cmd.clip) diff --git a/crates/anyrender/src/null_backend.rs b/crates/anyrender/src/null_backend.rs index 68cf373..9047aa8 100644 --- a/crates/anyrender/src/null_backend.rs +++ b/crates/anyrender/src/null_backend.rs @@ -1,6 +1,6 @@ //! A dummy implementation of the AnyRender traits while simply ignores all commands -use crate::{ImageRenderer, PaintScene, RenderContext, WindowHandle, WindowRenderer}; +use crate::{Filter, ImageRenderer, PaintScene, RenderContext, WindowHandle, WindowRenderer}; use std::sync::Arc; #[derive(Copy, Clone, Default)] @@ -102,6 +102,8 @@ impl PaintScene for NullScenePainter { _alpha: f32, _transform: kurbo::Affine, _clip: &impl kurbo::Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { } diff --git a/crates/anyrender/src/recording.rs b/crates/anyrender/src/recording.rs index aef1108..85a8232 100644 --- a/crates/anyrender/src/recording.rs +++ b/crates/anyrender/src/recording.rs @@ -1,4 +1,6 @@ -use crate::{Glyph, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; +use std::sync::Arc; + +use crate::{Filter, Glyph, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; use kurbo::{Affine, BezPath, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, Style, StyleRef}; @@ -58,6 +60,8 @@ pub struct LayerCommand { pub transform: Affine, #[cfg_attr(feature = "serde", serde(with = "svg_path"))] pub clip: BezPath, // TODO: more shape options + pub filter: Option>, + pub backdrop_filter: Option>, } /// Pushes a new clip layer clipped by the specified shape. @@ -178,6 +182,8 @@ impl PaintScene for Scene { alpha: f32, transform: Affine, clip: &impl Shape, + filter: Option>, + backdrop_filter: Option>, ) { let blend = blend.into(); let clip = clip.into_path(self.tolerance); @@ -186,6 +192,8 @@ impl PaintScene for Scene { alpha, transform, clip, + filter, + backdrop_filter, }; self.commands.push(RenderCommand::PushLayer(layer)); } diff --git a/crates/anyrender_serialize/tests/serialize.rs b/crates/anyrender_serialize/tests/serialize.rs index ff8bdfd..e9bd513 100644 --- a/crates/anyrender_serialize/tests/serialize.rs +++ b/crates/anyrender_serialize/tests/serialize.rs @@ -30,6 +30,8 @@ fn test_all_command_types_roundtrip() { 0.75, Affine::translate((5.0, 5.0)), &Rect::new(0.0, 0.0, 500.0, 500.0), + None, + None, ); // Fill (NonZero) @@ -85,6 +87,8 @@ fn test_all_command_types_roundtrip() { 1.0, Affine::IDENTITY, &Rect::new(0.0, 0.0, 500.0, 500.0), + None, + None, ); scene.fill( Fill::NonZero, diff --git a/crates/anyrender_skia/src/scene.rs b/crates/anyrender_skia/src/scene.rs index 486c7e6..17114f4 100644 --- a/crates/anyrender_skia/src/scene.rs +++ b/crates/anyrender_skia/src/scene.rs @@ -1,4 +1,6 @@ -use anyrender::{PaintScene, RenderContext}; +use std::sync::Arc; + +use anyrender::{Filter, PaintScene, RenderContext}; use peniko::color::AlphaColor; use skia_safe::{ BlurStyle, Canvas, Color, ColorSpace, Font, FontArguments, FontHinting, FontMgr, GlyphId, @@ -411,6 +413,8 @@ impl PaintScene for SkiaScenePainter<'_> { alpha: f32, transform: kurbo::Affine, clip: &impl kurbo::Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { let blend: peniko::BlendMode = blend.into(); diff --git a/crates/anyrender_svg/src/render.rs b/crates/anyrender_svg/src/render.rs index 902ef7f..029dc23 100644 --- a/crates/anyrender_svg/src/render.rs +++ b/crates/anyrender_svg/src/render.rs @@ -39,6 +39,8 @@ pub(crate) fn render_group( alpha, global_transform * transform, &local_path, + None, + None, ); true @@ -60,6 +62,8 @@ pub(crate) fn render_group( alpha, global_transform * transform, &rect, + None, + None, ); true diff --git a/crates/anyrender_vello/src/scene.rs b/crates/anyrender_vello/src/scene.rs index bce1dc1..0bfe452 100644 --- a/crates/anyrender_vello/src/scene.rs +++ b/crates/anyrender_vello/src/scene.rs @@ -1,4 +1,6 @@ -use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId}; +use std::sync::Arc; + +use anyrender::{Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId}; use kurbo::{Affine, Rect, Shape, Stroke}; use peniko::{BlendMode, BrushRef, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; use rustc_hash::FxHashMap; @@ -70,6 +72,8 @@ impl PaintScene for VelloScenePainter<'_, '_> { alpha: f32, transform: Affine, clip: &impl Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { self.inner .push_layer(Fill::NonZero, blend, alpha, transform, clip); diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index b77132c..e5b979c 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -1,4 +1,6 @@ -use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; +use std::sync::Arc; + +use anyrender::{Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; use glifo::FontEmbolden; use kurbo::{Affine, Diagonal2, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, StyleRef}; @@ -63,6 +65,8 @@ impl PaintScene for VelloCpuScenePainter { alpha: f32, transform: Affine, clip: &impl Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { self.render_ctx.set_transform(transform); self.render_ctx.push_layer( diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index 0570176..e67593e 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -1,4 +1,6 @@ -use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId}; +use std::sync::Arc; + +use anyrender::{Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, ResourceId}; use glifo::FontEmbolden; use kurbo::{Affine, Diagonal2, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; @@ -176,6 +178,8 @@ impl PaintScene for VelloHybridScenePainter<'_> { alpha: f32, transform: Affine, clip: &impl Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index 778e38e..317f0f3 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -107,6 +107,8 @@ impl PaintScene for WebGlScenePainter<'_> { alpha: f32, transform: Affine, clip: &impl Shape, + _filter: Option>, + _backdrop_filter: Option>, ) { self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); diff --git a/examples/serialize/src/main.rs b/examples/serialize/src/main.rs index 566a520..d9e0a77 100644 --- a/examples/serialize/src/main.rs +++ b/examples/serialize/src/main.rs @@ -107,6 +107,8 @@ fn create_demo_scene() -> Scene { 0.8, Affine::IDENTITY, &Rect::new(0.0, 0.0, WIDTH as f64, HEIGHT as f64), + None, + None, ); // Red circle From 76d328a494841e8105c5f7d2b92f2d44eacafeb1 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 17:46:06 +0100 Subject: [PATCH 12/29] Assert size of FilterEffect Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index de37d64..5ad8be3 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -332,6 +332,10 @@ pub enum FilterEffect { SpecularLighting(SpecularLightingFilter), } +// Assert size of FilterEffect. +// This is just for documentation purposes. Feel free to update the value as necessary +const _: [u8; 128] = [0; std::mem::size_of::()]; + impl FilterEffect { /// Calculate the bounds expansion as a `Rect` in user space. /// From 32f29cf32c080e005b9636b8433e7f2989a8ac80 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 17:56:14 +0100 Subject: [PATCH 13/29] Add edge links to the filter graph Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 219 ++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 94 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 5ad8be3..ffd8761 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -21,7 +21,6 @@ // - `GaussianBlur` - Gaussian blur filter // - `DropShadow` - Drop shadow effect (compound primitive) // - `Offset` - Translation/shift (single primitive) -// use kurbo::{Affine, Rect, Vec2}; use peniko::color::{AlphaColor, Srgb}; @@ -47,7 +46,7 @@ use self::{ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Filter { /// All filter primitives in the graph, stored in insertion order. - primitives: SmallVec<[FilterEffect; 1]>, + primitives: SmallVec<[FilterGraphNode; 1]>, /// The final output filter ID whose result is the output of this graph. output: FilterId, /// Accumulated bounds expansion from all primitives in the graph, cached in user space. @@ -110,29 +109,47 @@ impl Filter { /// Use this for direct access to low-level SVG filter operations. pub fn single(primitive: FilterEffect) -> Self { let mut graph = Self::empty(); - let filter_id = graph.add(primitive, None); + let filter_id = graph.add(primitive, FilterInputs::NONE); graph.set_output(filter_id); graph } + /// Create a filter from an iterator of filter effects. + /// + /// Creates a filter graph where the effects are applied in order. + pub fn linear_list(primitives: impl Iterator) -> Self { + let mut graph = Self::empty(); + let mut last_id = None; + for primitive in primitives { + let inputs = FilterInputs { + primary: last_id.map(FilterInput::Result), + secondary: None, + }; + let filter_id = graph.add(primitive, inputs); + graph.set_output(filter_id); + last_id = Some(filter_id); + } + graph + } + /// Add a filter primitive with optional inputs. /// /// Returns a `FilterId` that can be referenced by other primitives. /// Automatically updates the accumulated bounds expansion based on the primitive's requirements. - pub fn add(&mut self, primitive: FilterEffect, _inputs: Option) -> FilterId { + pub fn add(&mut self, effect: FilterEffect, inputs: FilterInputs) -> FilterId { let id = FilterId(self.primitives.len() as u16); // Update accumulated expansion by taking the union of rects - let primitive_rect = primitive.expansion_rect(); + let primitive_rect = effect.expansion_rect(); self.expansion_rect = self.expansion_rect.union(primitive_rect); - self.primitives.push(primitive); + self.primitives.push(FilterGraphNode { effect, inputs }); id } - /// The list of primitives in the graph - pub fn primitives(&mut self) -> &[FilterEffect] { + /// The list of nodes in the graph + pub fn nodes(&mut self) -> &[FilterGraphNode] { &self.primitives } @@ -187,6 +204,106 @@ impl Filter { } } +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FilterGraphNode { + pub effect: FilterEffect, + pub inputs: FilterInputs, +} + +/// Unique identifier for a filter primitive in the graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FilterId(pub u16); + +/// Input connections for a filter primitive. +#[derive(Debug, Clone, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FilterInputs { + /// Primary input ("in" attribute in SVG). + pub primary: Option, + /// Secondary input ("in2" attribute in SVG, for composite/blend operations). + pub secondary: Option, +} + +impl FilterInputs { + pub const NONE: Self = Self { + primary: None, + secondary: None, + }; +} + +impl FilterInputs { + /// Create filter inputs with a single input. + /// + /// Use this for primitives that operate on a single source (blur, color matrix, etc.). + pub fn single(input: FilterInput) -> Self { + Self { + primary: Some(input), + secondary: None, + } + } + + /// Create filter inputs with two inputs (for composite, blend, etc.). + /// + /// Use this for primitives that combine two sources (composite, blend, displacement map, etc.). + pub fn dual(input1: FilterInput, input2: FilterInput) -> Self { + Self { + primary: Some(input1), + secondary: Some(input2), + } + } +} + +/// A single filter input. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum FilterInput { + /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.). + Source(FilterSource), + /// Input from another filter's result. + Result(FilterId), +} + +/// Filter input sources. +/// +/// Defines the various built-in sources that can be used as filter inputs, +/// matching the SVG filter primitive input types. These represent implicit +/// inputs available to any filter primitive without requiring previous operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum FilterSource { + /// The original graphic content being filtered. + /// + /// This is the default input - the rendered result of the element + /// the filter is applied to, including all its fill, stroke, and content. + SourceGraphic, + /// Alpha channel only of the original graphic. + /// + /// Useful for creating effects based on shape/transparency, such as + /// shadows that follow the element's outline. + SourceAlpha, + /// Background image content behind the filtered element. + /// + /// Allows filters to incorporate or blend with content behind the element. + /// Not always available depending on the rendering context. + BackgroundImage, + /// Alpha channel only of the background image. + /// + /// The transparency mask of the background content. + BackgroundAlpha, + /// The fill paint of the element as an image input. + /// + /// For elements with gradient or pattern fills, this provides access + /// to the fill as a filter input. + FillPaint, + /// The stroke paint of the element as an image input. + /// + /// For elements with gradient or pattern strokes, this provides access + /// to the stroke as a filter input. + StrokePaint, +} + /// Edge mode for filter operations. /// /// Determines how to extend the input image when filter operations require sampling @@ -405,92 +522,6 @@ mod offset_expansion_tests { } } -/// Unique identifier for a filter primitive in the graph. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FilterId(pub u16); - -/// Input connections for a filter primitive. -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FilterInputs { - /// Primary input ("in" attribute in SVG). - pub primary: FilterInput, - /// Secondary input ("in2" attribute in SVG, for composite/blend operations). - pub secondary: Option, -} - -impl FilterInputs { - /// Create filter inputs with a single input. - /// - /// Use this for primitives that operate on a single source (blur, color matrix, etc.). - pub fn single(input: FilterInput) -> Self { - Self { - primary: input, - secondary: None, - } - } - - /// Create filter inputs with two inputs (for composite, blend, etc.). - /// - /// Use this for primitives that combine two sources (composite, blend, displacement map, etc.). - pub fn dual(input1: FilterInput, input2: FilterInput) -> Self { - Self { - primary: input1, - secondary: Some(input2), - } - } -} - -/// A single filter input. -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FilterInput { - /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.). - Source(FilterSource), - /// Input from another filter's result. - Result(FilterId), -} - -/// Filter input sources. -/// -/// Defines the various built-in sources that can be used as filter inputs, -/// matching the SVG filter primitive input types. These represent implicit -/// inputs available to any filter primitive without requiring previous operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FilterSource { - /// The original graphic content being filtered. - /// - /// This is the default input - the rendered result of the element - /// the filter is applied to, including all its fill, stroke, and content. - SourceGraphic, - /// Alpha channel only of the original graphic. - /// - /// Useful for creating effects based on shape/transparency, such as - /// shadows that follow the element's outline. - SourceAlpha, - /// Background image content behind the filtered element. - /// - /// Allows filters to incorporate or blend with content behind the element. - /// Not always available depending on the rendering context. - BackgroundImage, - /// Alpha channel only of the background image. - /// - /// The transparency mask of the background content. - BackgroundAlpha, - /// The fill paint of the element as an image input. - /// - /// For elements with gradient or pattern fills, this provides access - /// to the fill as a filter input. - FillPaint, - /// The stroke paint of the element as an image input. - /// - /// For elements with gradient or pattern strokes, this provides access - /// to the stroke as a filter input. - StrokePaint, -} - /// Blend modes for combining colors. /// /// These are blend modes that define how to combine the colors From a8d44bc47fc094d45a839e3c58beeff7813e906c Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 19:26:14 +0100 Subject: [PATCH 14/29] Use SmallVec for TransferFunction::Table (2-value inputs are common) Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index ffd8761..277dc2e 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -751,6 +751,7 @@ pub mod displacement { } pub mod component_transfer { + use smallvec::SmallVec; /// Per-channel component transfer using lookup tables or functions. /// @@ -787,7 +788,7 @@ pub mod component_transfer { /// /// Lookup table values defining the transfer curve. /// More values provide smoother curves. Minimum 2 values required. - Table(Vec), + Table(SmallVec<[f32; 2]>), /// Discrete step function (posterization). /// From af19b07463e79aa1243ceb20e4072c5251950d79 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 20:05:05 +0100 Subject: [PATCH 15/29] Fixup transfer function names --- crates/anyrender/src/filters.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 277dc2e..9efeb7a 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -800,10 +800,10 @@ pub mod component_transfer { Discrete(Vec), /// Linear function: output = slope × input + intercept. - Linear(Linearfunction), + Linear(LinearTransferFunction), // Gamma correction: output = amplitude × input^exponent + offset. - Gamma(Gammafunction), + Gamma(GammaTransferFunction), } /// Linear function: output = slope × input + intercept. @@ -811,7 +811,7 @@ pub mod component_transfer { /// Simple linear transformation of the input value. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct Linearfunction { + pub struct LinearTransferFunction { pub slope: f32, pub intercept: f32, } @@ -822,7 +822,7 @@ pub mod component_transfer { /// adjusting midtone brightness without affecting blacks or whites. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct Gammafunction { + pub struct GammaTransferFunction { pub amplitude: f32, pub exponent: f32, pub offset: f32, From 282570a17430c17b5771f34047773aeed0c17f68 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 2 Jun 2026 19:40:38 +0100 Subject: [PATCH 16/29] Implement shorthand constructors for CSS filter functions WIP Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 128 ++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 32 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 9efeb7a..543857c 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -28,6 +28,7 @@ use smallvec::SmallVec; use self::{ blur::GaussianBlurFilter, + color_transformation::ColorMatrix, component_transfer::ComponentTransferFilter, composite::CompositeOperator, convolution::ConvolutionKernel, @@ -377,7 +378,7 @@ pub enum FilterEffect { /// /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). /// Each output channel is computed as a linear combination of input channels plus offset. - ColorMatrix([f32; 20]), + ColorMatrix(ColorMatrix), /// Geometric offset/translation. /// @@ -770,6 +771,58 @@ pub mod component_transfer { pub alpha_function: Option, } + impl ComponentTransferFilter { + /// Component transfer filter for the CSS opacity() filter + pub fn opacity(amount: f32) -> Self { + let func = TransferFunction::Table(SmallVec::from([0.0, amount])); + Self { + red_function: None, + green_function: None, + blue_function: None, + alpha_function: Some(func), + } + } + + /// Component transfer filter for the CSS invert() filter + pub fn invert(amount: f32) -> Self { + let func = TransferFunction::Table(SmallVec::from([amount, 1.0 - amount])); + Self { + red_function: Some(func.clone()), + green_function: Some(func.clone()), + blue_function: Some(func.clone()), + alpha_function: None, + } + } + + /// Component transfer filter for the CSS brightness() filter + pub fn brightness(amount: f32) -> Self { + let func = TransferFunction::Linear(LinearTransferFunction { + slope: amount, + intercept: 0.0, + }); + Self { + red_function: Some(func.clone()), + green_function: Some(func.clone()), + blue_function: Some(func.clone()), + alpha_function: None, + } + } + + /// Component transfer filter for the CSS contrast() filter + pub fn contrast(amount: f32) -> Self { + let func = TransferFunction::Linear(LinearTransferFunction { + slope: amount, + intercept: -(0.5 * amount) + 0.5, + }); + Self { + red_function: Some(func.clone()), + green_function: Some(func.clone()), + blue_function: Some(func.clone()), + alpha_function: None, + } + } + } + /// Transfer functions for component transfer operations. /// /// These functions map input color channel values to output values, @@ -928,37 +981,48 @@ pub mod lighting { /// These 4x5 matrices are used with the `ColorMatrix` filter primitive. /// Each row transforms a color channel: [R, G, B, A, offset]. pub mod color_transformation { - /// Identity matrix (no change). - pub const IDENTITY: [f32; 20] = [ - 1.0, 0.0, 0.0, 0.0, 0.0, // Red - 0.0, 1.0, 0.0, 0.0, 0.0, // Green - 0.0, 0.0, 1.0, 0.0, 0.0, // Blue - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha - ]; - - /// Extract alpha channel to RGB (for shadow effects). - pub const ALPHA_TO_BLACK: [f32; 20] = [ - 0.0, 0.0, 0.0, 1.0, 0.0, // Red = Alpha - 0.0, 0.0, 0.0, 1.0, 0.0, // Green = Alpha - 0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha - ]; - - /// Grayscale conversion matrix using luminosity weights. - pub const GRAYSCALE: [f32; 20] = [ - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha - ]; - - /// Sepia tone matrix for vintage photo effect. - pub const SEPIA: [f32; 20] = [ - 0.393, 0.769, 0.189, 0.0, 0.0, // Red - 0.349, 0.686, 0.168, 0.0, 0.0, // Green - 0.272, 0.534, 0.131, 0.0, 0.0, // Blue - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha - ]; + + /// Matrix-based color transformation. + /// + /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). + /// Each output channel is computed as a linear combination of input channels plus offset. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct ColorMatrix(pub [f32; 20]); + + impl ColorMatrix { + /// Identity matrix (no change). + pub const IDENTITY: Self = Self([ + 1.0, 0.0, 0.0, 0.0, 0.0, // Red + 0.0, 1.0, 0.0, 0.0, 0.0, // Green + 0.0, 0.0, 1.0, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]); + + /// Extract alpha channel to RGB (for shadow effects). + pub const ALPHA_TO_BLACK: Self = Self([ + 0.0, 0.0, 0.0, 1.0, 0.0, // Red = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Green = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha + ]); + + /// Grayscale conversion matrix using luminosity weights. + pub const GRAYSCALE: Self = Self([ + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]); + + /// Sepia tone matrix for vintage photo effect. + pub const SEPIA: Self = Self([ + 0.393, 0.769, 0.189, 0.0, 0.0, // Red + 0.349, 0.686, 0.168, 0.0, 0.0, // Green + 0.272, 0.534, 0.131, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]); + } } /// Common convolution kernels. From 5c26a8f490197fe2c4555c5ade4e3a9b26c4e7c4 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 12:21:38 +0100 Subject: [PATCH 17/29] Add constructors for hue_rotate and saturate filters --- crates/anyrender/src/filters.rs | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 543857c..32df24c 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -982,6 +982,10 @@ pub mod lighting { /// Each row transforms a color channel: [R, G, B, A, offset]. pub mod color_transformation { + const LUMA_R: f32 = 0.213; + const LUMA_G: f32 = 0.715; + const LUMA_B: f32 = 0.072; + /// Matrix-based color transformation. /// /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). @@ -991,6 +995,61 @@ pub mod color_transformation { pub struct ColorMatrix(pub [f32; 20]); impl ColorMatrix { + /// Color matrix filter for the CSS hue-rotate() filter + pub fn hue_rotate(angle_radians: f32) -> Self { + let sin = angle_radians.sin(); + let cos = angle_radians.cos(); + + Self([ + LUMA_R + cos * (1.0 - LUMA_R) - sin * LUMA_R, + LUMA_G - cos * LUMA_G - sin * LUMA_G, + LUMA_B - cos * LUMA_B + sin * (1.0 - LUMA_B), + 0.0, + 0.0, + LUMA_R - cos * LUMA_R + sin * 0.143, + LUMA_G + cos * (1.0 - LUMA_G) + sin * 0.140, + LUMA_B - cos * LUMA_B - sin * 0.283, + 0.0, + 0.0, + LUMA_R - cos * LUMA_R - sin * (1.0 - LUMA_R), + LUMA_G - cos * LUMA_G + sin * LUMA_G, + LUMA_B + cos * (1.0 - LUMA_B) + sin * LUMA_B, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]) + } + + /// Color matrix filter for the CSS saturate() filter + pub fn saturate(amount: f32) -> Self { + Self([ + LUMA_R + amount * (1.0 - LUMA_R), + LUMA_G - amount * LUMA_G, + LUMA_B - amount * LUMA_B, + 0.0, + 0.0, + LUMA_R - amount * LUMA_R, + LUMA_G + amount * (1.0 - LUMA_G), + LUMA_B - amount * LUMA_B, + 0.0, + 0.0, + LUMA_R - amount * LUMA_R, + LUMA_G - amount * LUMA_G, + LUMA_B + amount * (1.0 - LUMA_B), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]) + } + /// Identity matrix (no change). pub const IDENTITY: Self = Self([ 1.0, 0.0, 0.0, 0.0, 0.0, // Red From 26e99c44c03dccd10e8ac30dd56b4d68a6c27e3c Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 12:36:48 +0100 Subject: [PATCH 18/29] Move color_transformation module up next to component_transfer module Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 188 ++++++++++++++++---------------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 32df24c..8b9efae 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -882,100 +882,6 @@ pub mod component_transfer { } } -pub mod lighting { - - /// Diffuse lighting simulation. - /// - /// Creates a lighting effect by treating the input's alpha channel as a height map - /// and calculating diffuse (matte) reflection from a light source. - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct DiffuseLightingFilter { - /// Surface scale factor for converting alpha values to heights. - pub surface_scale: f32, - /// Diffuse reflection constant (kd). Controls lighting intensity. - pub diffuse_constant: f32, - /// Kernel unit length for gradient calculations in user space. - pub kernel_unit_length: f32, - /// Configuration of the light source (point, distant, or spot). - pub light_source: LightSource, - } - - /// Specular lighting simulation. - /// - /// Creates a lighting effect by treating the input's alpha channel as a height map - /// and calculating specular (shiny) reflection highlights from a light source. - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct SpecularLightingFilter { - /// Surface scale factor for converting alpha values to heights. - pub surface_scale: f32, - /// Specular reflection constant (ks). Controls highlight intensity. - pub specular_constant: f32, - /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). - pub specular_exponent: f32, - /// Kernel unit length for gradient calculations in user space. - pub kernel_unit_length: f32, - /// Configuration of the light source (point, distant, or spot). - pub light_source: LightSource, - } - - /// Light source configurations for lighting effects. - /// - /// Defines different types of light sources used in diffuse and specular lighting - /// filter primitives. Each type has different characteristics and use cases. - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub enum LightSource { - /// Distant light source (infinitely far away, like the sun). - Distant(DistantLightSource), - /// Point light source at a specific 3D position. - Point(PointLightSource), - /// Spot light with position, direction, and cone angle. - Spot(SpotLightSource), - } - - /// Distant light source (infinitely far away, like the sun). - /// - /// All rays are parallel, creating uniform lighting across the surface. - /// Direction is specified using spherical coordinates (azimuth and elevation). - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct DistantLightSource { - pub azimuth: f32, - pub elevation: f32, - } - - /// Point light source at a specific 3D position. - /// - /// Light radiates uniformly in all directions from a single point. - /// Intensity decreases with distance. Like a light bulb. - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct PointLightSource { - pub x: f32, - pub y: f32, - pub z: f32, - } - - /// Spot light with position, direction, and cone angle. - /// - /// Light emanates from a point in a specific direction with limited spread. - /// Like a flashlight or stage spotlight with adjustable focus. - #[derive(Debug, Clone, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct SpotLightSource { - pub x: f32, - pub y: f32, - pub z: f32, - pub points_at_x: f32, - pub points_at_y: f32, - pub points_at_z: f32, - pub specular_exponent: f32, - pub limiting_cone_angle: Option, - } -} - /// Common color transformation matrices. /// /// These 4x5 matrices are used with the `ColorMatrix` filter primitive. @@ -1084,6 +990,100 @@ pub mod color_transformation { } } +pub mod lighting { + + /// Diffuse lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating diffuse (matte) reflection from a light source. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct DiffuseLightingFilter { + /// Surface scale factor for converting alpha values to heights. + pub surface_scale: f32, + /// Diffuse reflection constant (kd). Controls lighting intensity. + pub diffuse_constant: f32, + /// Kernel unit length for gradient calculations in user space. + pub kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + pub light_source: LightSource, + } + + /// Specular lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating specular (shiny) reflection highlights from a light source. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct SpecularLightingFilter { + /// Surface scale factor for converting alpha values to heights. + pub surface_scale: f32, + /// Specular reflection constant (ks). Controls highlight intensity. + pub specular_constant: f32, + /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). + pub specular_exponent: f32, + /// Kernel unit length for gradient calculations in user space. + pub kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + pub light_source: LightSource, + } + + /// Light source configurations for lighting effects. + /// + /// Defines different types of light sources used in diffuse and specular lighting + /// filter primitives. Each type has different characteristics and use cases. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub enum LightSource { + /// Distant light source (infinitely far away, like the sun). + Distant(DistantLightSource), + /// Point light source at a specific 3D position. + Point(PointLightSource), + /// Spot light with position, direction, and cone angle. + Spot(SpotLightSource), + } + + /// Distant light source (infinitely far away, like the sun). + /// + /// All rays are parallel, creating uniform lighting across the surface. + /// Direction is specified using spherical coordinates (azimuth and elevation). + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct DistantLightSource { + pub azimuth: f32, + pub elevation: f32, + } + + /// Point light source at a specific 3D position. + /// + /// Light radiates uniformly in all directions from a single point. + /// Intensity decreases with distance. Like a light bulb. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct PointLightSource { + pub x: f32, + pub y: f32, + pub z: f32, + } + + /// Spot light with position, direction, and cone angle. + /// + /// Light emanates from a point in a specific direction with limited spread. + /// Like a flashlight or stage spotlight with adjustable focus. + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct SpotLightSource { + pub x: f32, + pub y: f32, + pub z: f32, + pub points_at_x: f32, + pub points_at_y: f32, + pub points_at_z: f32, + pub specular_exponent: f32, + pub limiting_cone_angle: Option, + } +} + /// Common convolution kernels. /// /// These kernels are used with the `ConvolveMatrix` filter primitive From 6e8e269d6d8883f29b1c83e681fbcdcb48632e53 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 12:41:21 +0100 Subject: [PATCH 19/29] Make ComponentTransferFilter use T rather than Option for each channel Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 8b9efae..c0b53ea 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -762,13 +762,13 @@ pub mod component_transfer { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ComponentTransferFilter { /// Transfer function applied to the red channel (None = identity). - pub red_function: Option, + pub red_function: TransferFunction, /// Transfer function applied to the green channel (None = identity). - pub green_function: Option, + pub green_function: TransferFunction, /// Transfer function applied to the blue channel (None = identity). - pub blue_function: Option, + pub blue_function: TransferFunction, /// Transfer function applied to the alpha channel (None = identity). - pub alpha_function: Option, + pub alpha_function: TransferFunction, } impl ComponentTransferFilter { @@ -776,10 +776,10 @@ pub mod component_transfer { pub fn opacity(amount: f32) -> Self { let func = TransferFunction::Table(SmallVec::from([0.0, amount])); Self { - red_function: None, - green_function: None, - blue_function: None, - alpha_function: Some(func), + red_function: TransferFunction::Identity, + green_function: TransferFunction::Identity, + blue_function: TransferFunction::Identity, + alpha_function: func, } } @@ -787,10 +787,10 @@ pub mod component_transfer { pub fn invert(amount: f32) -> Self { let func = TransferFunction::Table(SmallVec::from([amount, 1.0 - amount])); Self { - red_function: Some(func.clone()), - green_function: Some(func.clone()), - blue_function: Some(func.clone()), - alpha_function: None, + red_function: func.clone(), + green_function: func.clone(), + blue_function: func.clone(), + alpha_function: TransferFunction::Identity, } } @@ -801,10 +801,10 @@ pub mod component_transfer { intercept: 0.0, }); Self { - red_function: Some(func.clone()), - green_function: Some(func.clone()), - blue_function: Some(func.clone()), - alpha_function: None, + red_function: func.clone(), + green_function: func.clone(), + blue_function: func.clone(), + alpha_function: TransferFunction::Identity, } } @@ -815,10 +815,10 @@ pub mod component_transfer { intercept: -(0.5 * amount) + 0.5, }); Self { - red_function: Some(func.clone()), - green_function: Some(func.clone()), - blue_function: Some(func.clone()), - alpha_function: None, + red_function: func.clone(), + green_function: func.clone(), + blue_function: func.clone(), + alpha_function: TransferFunction::Identity, } } } From 3dbbe4a94ce5d8ae18a8270e91606bbafffc7f39 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 12:53:58 +0100 Subject: [PATCH 20/29] Move css filter constructors from Filter to FilterEffect Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index c0b53ea..9964a51 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -67,34 +67,6 @@ impl Default for Filter { } impl Filter { - /// Gaussian blur effect. - /// - /// Applies a Gaussian blur to the input image. Larger radius values - /// produce more blur. The blur is applied equally in all directions. - pub fn blur(radius: f32) -> Self { - Self::single(FilterEffect::GaussianBlur(GaussianBlurFilter { - std_deviation: radius, - edge_mode: EdgeMode::None, - })) - } - - /// Drop shadow effect (compound primitive). - /// - /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, - /// and compositing it with the original. This is a compound operation that - /// combines multiple primitive operations into one. - /// - /// See: - pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { - Self::single(FilterEffect::DropShadow(DropShadow { - dx, - dy, - std_deviation, - color, - edge_mode: EdgeMode::None, - })) - } - /// Create a new empty filter graph. pub fn empty() -> Self { Self { @@ -455,6 +427,34 @@ pub enum FilterEffect { const _: [u8; 128] = [0; std::mem::size_of::()]; impl FilterEffect { + /// Gaussian blur effect. + /// + /// Applies a Gaussian blur to the input image. Larger radius values + /// produce more blur. The blur is applied equally in all directions. + pub fn blur(radius: f32) -> Self { + Self::GaussianBlur(GaussianBlurFilter { + std_deviation: radius, + edge_mode: EdgeMode::None, + }) + } + + /// Drop shadow effect (compound primitive). + /// + /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, + /// and compositing it with the original. This is a compound operation that + /// combines multiple primitive operations into one. + /// + /// See: + pub fn drop_shadow(dx: f32, dy: f32, std_deviation: f32, color: AlphaColor) -> Self { + Self::DropShadow(DropShadow { + dx, + dy, + std_deviation, + color, + edge_mode: EdgeMode::None, + }) + } + /// Calculate the bounds expansion as a `Rect` in user space. /// /// Returns a rectangle centered at the origin representing how much the filter From d3b2c5a9ce77fa7c7b8e6ffebee9747bbcb8827d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 13:08:10 +0100 Subject: [PATCH 21/29] Make sepia and grayscale definition parameterised Signed-off-by: Nico Burns --- crates/anyrender/src/filters.rs | 70 +++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 9964a51..8a1f7db 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -956,6 +956,60 @@ pub mod color_transformation { ]) } + /// Color matrix filter for the CSS sepia() filter + /// + pub fn sepia(amount: f32) -> Self { + Self([ + (0.393 + 0.607 * (1.0 - amount)), + (0.769 - 0.769 * (1.0 - amount)), + (0.189 - 0.189 * (1.0 - amount)), + 0.0, + 0.0, + (0.349 - 0.349 * (1.0 - amount)), + (0.686 + 0.314 * (1.0 - amount)), + (0.168 - 0.168 * (1.0 - amount)), + 0.0, + 0.0, + (0.272 - 0.272 * (1.0 - amount)), + (0.534 - 0.534 * (1.0 - amount)), + (0.131 + 0.869 * (1.0 - amount)), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]) + } + + /// Color matrix filter for the CSS grayscale() filter + /// + pub fn grayscale(amount: f32) -> Self { + Self([ + (0.2126 + 0.7874 * (1.0 - amount)), + (0.7152 - 0.7152 * (1.0 - amount)), + (0.0722 - 0.0722 * (1.0 - amount)), + 0.0, + 0.0, + (0.2126 - 0.2126 * (1.0 - amount)), + (0.7152 + 0.2848 * (1.0 - amount)), + (0.0722 - 0.0722 * (1.0 - amount)), + 0.0, + 0.0, + (0.2126 - 0.2126 * (1.0 - amount)), + (0.7152 - 0.7152 * (1.0 - amount)), + (0.0722 + 0.9278 * (1.0 - amount)), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]) + } + /// Identity matrix (no change). pub const IDENTITY: Self = Self([ 1.0, 0.0, 0.0, 0.0, 0.0, // Red @@ -971,22 +1025,6 @@ pub mod color_transformation { 0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha ]); - - /// Grayscale conversion matrix using luminosity weights. - pub const GRAYSCALE: Self = Self([ - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green - 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha - ]); - - /// Sepia tone matrix for vintage photo effect. - pub const SEPIA: Self = Self([ - 0.393, 0.769, 0.189, 0.0, 0.0, // Red - 0.349, 0.686, 0.168, 0.0, 0.0, // Green - 0.272, 0.534, 0.131, 0.0, 0.0, // Blue - 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha - ]); } } From 60afc12941e6ce90cb070c2cfffc356b74c8390d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 13:08:38 +0100 Subject: [PATCH 22/29] Add constructors for all CSS filters to FilterEffect --- crates/anyrender/src/filters.rs | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 8a1f7db..fae76ec 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -455,6 +455,46 @@ impl FilterEffect { }) } + /// Construct a CSS opacity() filter effect + pub fn opacity(amount: f32) -> Self { + Self::ComponentTransfer(ComponentTransferFilter::opacity(amount)) + } + + /// Construct a CSS invert() filter effect + pub fn invert(amount: f32) -> Self { + Self::ComponentTransfer(ComponentTransferFilter::invert(amount)) + } + + /// Construct a CSS brightness() filter effect + pub fn brightness(amount: f32) -> Self { + Self::ComponentTransfer(ComponentTransferFilter::brightness(amount)) + } + + /// Construct a CSS contrast() filter effect + pub fn contrast(amount: f32) -> Self { + Self::ComponentTransfer(ComponentTransferFilter::contrast(amount)) + } + + /// Construct a CSS hue-rotate() filter effect + pub fn hue_rotate(angle_radians: f32) -> Self { + Self::ColorMatrix(ColorMatrix::hue_rotate(angle_radians)) + } + + /// Construct a CSS saturate() filter effect + pub fn saturate(amount: f32) -> Self { + Self::ColorMatrix(ColorMatrix::saturate(amount)) + } + + /// Construct a CSS sepia() filter effect + pub fn sepia(amount: f32) -> Self { + Self::ColorMatrix(ColorMatrix::sepia(amount)) + } + + /// Construct a CSS grayscale() filter effect + pub fn grayscale(amount: f32) -> Self { + Self::ColorMatrix(ColorMatrix::grayscale(amount)) + } + /// Calculate the bounds expansion as a `Rect` in user space. /// /// Returns a rectangle centered at the origin representing how much the filter From e385e2edcdf2944ca3a8154a0acf3f58d7f8e801 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 14:58:26 +0100 Subject: [PATCH 23/29] Make nodes and output methods &self not &mut self --- crates/anyrender/src/filters.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index fae76ec..b43db9b 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -122,12 +122,12 @@ impl Filter { } /// The list of nodes in the graph - pub fn nodes(&mut self) -> &[FilterGraphNode] { + pub fn nodes(&self) -> &[FilterGraphNode] { &self.primitives } /// The output filter for the graph. - pub fn output(&mut self) -> FilterId { + pub fn output(&self) -> FilterId { self.output } From 97bd40adda9c1eb6ef5b22a618818d74886413e9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 3 Jun 2026 14:59:30 +0100 Subject: [PATCH 24/29] Implement image filters for skia backend Signed-off-by: Nico Burns --- crates/anyrender_skia/src/scene.rs | 164 +++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/crates/anyrender_skia/src/scene.rs b/crates/anyrender_skia/src/scene.rs index 17114f4..6467669 100644 --- a/crates/anyrender_skia/src/scene.rs +++ b/crates/anyrender_skia/src/scene.rs @@ -1,11 +1,13 @@ use std::sync::Arc; -use anyrender::{Filter, PaintScene, RenderContext}; +use anyrender::filters::component_transfer::TransferFunction; +use anyrender::filters::{Filter, FilterEffect, FilterId, FilterInput}; +use anyrender::{PaintScene, RenderContext}; use peniko::color::AlphaColor; use skia_safe::{ BlurStyle, Canvas, Color, ColorSpace, Font, FontArguments, FontHinting, FontMgr, GlyphId, - MaskFilter, Paint, PaintCap, PaintJoin, PaintStyle, PathEffect, Point, RRect, Rect, Shader, - Typeface, + ImageFilter, MaskFilter, Paint, PaintCap, PaintJoin, PaintStyle, PathEffect, Point, RRect, + Rect, Shader, Typeface, canvas::{GlyphPositions, SaveLayerRec}, font::Edging, font_arguments::{VariationPosition, variation_position::Coordinate}, @@ -15,6 +17,7 @@ use crate::cache::{ FontCacheKey, FontCacheKeyBorrowed, GenerationalCache, NormalizedTypefaceCacheKey, NormalizedTypefaceCacheKeyBorrowed, }; +use crate::scene::sk_peniko::color4f_from_alpha_color; pub struct SkiaSceneCache { paint: Paint, @@ -85,6 +88,10 @@ impl SkiaScenePainter<'_> { self.cache.paint.set_alpha_f(alpha); } + fn set_paint_filter(&mut self, filter: ImageFilter) { + self.cache.paint.set_image_filter(filter); + } + fn set_paint_blend_mode(&mut self, blend_mode: impl Into) { self.cache .paint @@ -413,8 +420,8 @@ impl PaintScene for SkiaScenePainter<'_> { alpha: f32, transform: kurbo::Affine, clip: &impl kurbo::Shape, - _filter: Option>, - _backdrop_filter: Option>, + filter: Option>, + backdrop_filter: Option>, ) { let blend: peniko::BlendMode = blend.into(); @@ -422,13 +429,22 @@ impl PaintScene for SkiaScenePainter<'_> { self.set_paint_alpha(alpha); self.set_paint_blend_mode(blend); + if let Some(filter) = filter.as_ref().and_then(|f| convert_filter(f)) { + self.set_paint_filter(filter); + } + self.inner.save(); self.set_matrix(transform); self.clip(clip); - self.inner - .save_layer(&SaveLayerRec::default().paint(&self.cache.paint)); + let backdrop_filter = backdrop_filter.as_ref().and_then(|f| convert_filter(f)); + let mut save_layer_rec = SaveLayerRec::default().paint(&self.cache.paint); + if let Some(backdrop_filter) = backdrop_filter.as_ref() { + save_layer_rec = save_layer_rec.backdrop(backdrop_filter); + } + + self.inner.save_layer(&save_layer_rec); } fn push_clip_layer(&mut self, transform: kurbo::Affine, clip: &impl kurbo::Shape) { @@ -567,6 +583,140 @@ fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t } +fn convert_filter(filter: &Filter) -> Option { + convert_filter_effect(filter, filter.output()) +} + +fn convert_filter_effect(filter: &Filter, id: FilterId) -> Option { + let node = &filter.nodes()[id.0 as usize]; + let input = node.inputs.primary.as_ref().and_then(|input| match input { + FilterInput::Source(_) => None, + FilterInput::Result(input_id) => convert_filter_effect(filter, *input_id), + }); + + // Clone to avoid borrow checking issues. Cloning is cheap as filters are refcounted. + let input_clone = input.clone(); + + use skia_safe::{color_filters, image_filters}; + + let filter: Option = match &node.effect { + FilterEffect::Flood(color) => { + let rgba8 = color.to_rgba8(); + let sk_color = Color::from_argb(rgba8.a, rgba8.r, rgba8.g, rgba8.b); + let shader = skia_safe::shaders::color(sk_color); + image_filters::shader(shader, None) + } + FilterEffect::GaussianBlur(blur) => { + image_filters::blur((blur.std_deviation, blur.std_deviation), None, input, None) + } + FilterEffect::DropShadow(drop_shadow) => image_filters::drop_shadow( + (drop_shadow.dx, drop_shadow.dy), + (drop_shadow.std_deviation, drop_shadow.std_deviation), + color4f_from_alpha_color(drop_shadow.color), + None, + input, + None, + ), + FilterEffect::ComponentTransfer(ct) => { + fn map_transfer_function(func: &TransferFunction) -> Option<[u8; 256]> { + match func { + TransferFunction::Identity => None, + TransferFunction::Table(table) => { + if table.is_empty() { + None + } else { + let table = table.as_slice(); + let n = table.len(); + + let mut values = [0; 256]; + for (i, out) in values.iter_mut().enumerate() { + let c = i as f64 / 255.0; + let k = (c * (n - 1) as f64) as usize; + let v1 = table[k] as f64; + let v2 = table[(k + 1).min(n - 1)] as f64; + let val = + 255.0 * (v1 + (c * (n - 1) as f64 - k as f64) * (v2 - v1)); + *out = val.clamp(0.0, 255.0) as u8; + } + Some(values) + } + } + TransferFunction::Discrete(items) => { + if items.is_empty() { + None + } else { + let items = items.as_slice(); + let n = items.len(); + + let mut values = [0; 256]; + for (i, out) in values.iter_mut().enumerate() { + let k = (((i * n) as f64 / 255.0) as usize).min(n - 1); + let val = 255.0 * items[k] as f64; + *out = val.clamp(0.0, 255.0) as u8; + } + Some(values) + } + } + TransferFunction::Linear(tf) => { + let mut values = [0; 256]; + for (i, out) in values.iter_mut().enumerate() { + let val: f64 = + (tf.slope as f64 * i as f64) + (255.0 * tf.intercept as f64); + *out = val.clamp(0.0, 255.0) as u8; + } + Some(values) + } + TransferFunction::Gamma(tf) => { + let mut values = [0; 256]; + for (i, out) in values.iter_mut().enumerate() { + let val: f64 = 255.0 + * (tf.amplitude as f64 + * (i as f64 / 255.0).powf(tf.exponent as f64)) + + tf.offset as f64; + *out = val.clamp(0.0, 255.0) as u8; + } + Some(values) + } + } + } + + let color_filter = color_filters::table_argb( + map_transfer_function(&ct.alpha_function).as_ref(), + map_transfer_function(&ct.red_function).as_ref(), + map_transfer_function(&ct.green_function).as_ref(), + map_transfer_function(&ct.blue_function).as_ref(), + ); + + color_filter.and_then(|cf| image_filters::color_filter(cf, input, None)) + } + FilterEffect::ColorMatrix(color_matrix) => { + let color_filter = + color_filters::matrix_row_major(&color_matrix.0, color_filters::Clamp::Yes); + image_filters::color_filter(color_filter, input, None) + } + FilterEffect::Offset(offset) => { + image_filters::offset((offset.x as f32, offset.y as f32), input, None) + } + + // TODO: implement remaining filter effect types + FilterEffect::Composite(_composite_operator) => None, + FilterEffect::Blend(_mix) => None, + FilterEffect::Morphology(_morphology_filter) => None, + FilterEffect::ConvolveMatrix(_convolution_kernel) => None, + FilterEffect::Turbulence(_turbulence_filter) => None, + FilterEffect::DisplacementMap(_displacement_map_filter) => None, + FilterEffect::Image(_external_image_source) => None, + FilterEffect::Tile => None, + FilterEffect::DiffuseLighting(_diffuse_lighting_filter) => None, + FilterEffect::SpecularLighting(_specular_lighting_filter) => None, + }; + + // If we do not implement given type of filter then the match statement above returns None + // In that case, if we have an input then we pass that along instead. In the case of filter chains + // with some unimplemented filters that allows the remaining filters to work + filter.or(input_clone) +} + mod sk_peniko { use peniko::color::{AlphaColor, ColorSpaceTag, HueDirection, Srgb}; use peniko::{ From 958eacb1606727dbeff7079dfaa7efb6975a4b92 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 4 Jun 2026 13:43:12 +0100 Subject: [PATCH 25/29] Add expansion_rect method to Filter type --- crates/anyrender/src/filters.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index b43db9b..92907f8 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -175,6 +175,10 @@ impl Filter { // transform_rect_bbox computes the axis-aligned bounding box of the transformed rect transform.transform_rect_bbox(self.expansion_rect) } + + pub fn expansion_rect(&self) -> Rect { + self.expansion_rect + } } #[derive(Debug, Clone, PartialEq)] From a4b5949fc476fb9c179f8c3df23aad6f43d1e1e0 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 4 Jun 2026 13:48:58 +0100 Subject: [PATCH 26/29] Fix FilterEffect size assert on WASM --- crates/anyrender/src/filters.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/anyrender/src/filters.rs b/crates/anyrender/src/filters.rs index 92907f8..0e0a7b5 100644 --- a/crates/anyrender/src/filters.rs +++ b/crates/anyrender/src/filters.rs @@ -428,7 +428,10 @@ pub enum FilterEffect { // Assert size of FilterEffect. // This is just for documentation purposes. Feel free to update the value as necessary +#[cfg(not(target_arch = "wasm32"))] const _: [u8; 128] = [0; std::mem::size_of::()]; +#[cfg(target_arch = "wasm32")] +const _: [u8; 88] = [0; std::mem::size_of::()]; impl FilterEffect { /// Gaussian blur effect. From 263d32e4773ce7eda186eec41383308041bb600a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 4 Jun 2026 13:50:04 +0100 Subject: [PATCH 27/29] Implement images filters for vello_hybrid and vello_cpu backends Signed-off-by: Nico Burns --- Cargo.lock | 1 + crates/anyrender_vello_cpu/Cargo.toml | 1 + crates/anyrender_vello_cpu/src/scene.rs | 66 ++++++++++++++++++- crates/anyrender_vello_hybrid/src/filters.rs | 60 +++++++++++++++++ crates/anyrender_vello_hybrid/src/lib.rs | 1 + crates/anyrender_vello_hybrid/src/scene.rs | 13 +++- .../anyrender_vello_hybrid/src/webgl_scene.rs | 16 +++-- 7 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 crates/anyrender_vello_hybrid/src/filters.rs diff --git a/Cargo.lock b/Cargo.lock index 581d0ab..a175ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,7 @@ dependencies = [ "peniko", "pixels_window_renderer", "softbuffer_window_renderer", + "vello_common", "vello_cpu", ] diff --git a/crates/anyrender_vello_cpu/Cargo.toml b/crates/anyrender_vello_cpu/Cargo.toml index 1caed8d..69f4b5b 100644 --- a/crates/anyrender_vello_cpu/Cargo.toml +++ b/crates/anyrender_vello_cpu/Cargo.toml @@ -36,6 +36,7 @@ pixels_window_renderer = { workspace = true, optional = true } # External vello_cpu vello_cpu = { workspace = true } +vello_common = { workspace = true } [package.metadata.docs.rs] features = ["pixels_window_renderer", "softbuffer_window_renderer"] \ No newline at end of file diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index e5b979c..e2f9686 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -1,9 +1,13 @@ use std::sync::Arc; -use anyrender::{Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; +use anyrender::{ + Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, + filters::{EdgeMode, FilterEffect}, +}; use glifo::FontEmbolden; use kurbo::{Affine, Diagonal2, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, StyleRef}; +use vello_common::filter_effects::FilterPrimitive; use vello_cpu::{ImageSource, PaintType, Pixmap}; const DEFAULT_TOLERANCE: f64 = 0.1; @@ -65,7 +69,7 @@ impl PaintScene for VelloCpuScenePainter { alpha: f32, transform: Affine, clip: &impl Shape, - _filter: Option>, + filter: Option>, _backdrop_filter: Option>, ) { self.render_ctx.set_transform(transform); @@ -74,7 +78,7 @@ impl PaintScene for VelloCpuScenePainter { Some(blend.into()), Some(alpha), None, - None, + filter.and_then(convert_filter), ); } @@ -189,3 +193,59 @@ impl PaintScene for VelloCpuScenePainter { .fill_blurred_rounded_rect(&rect, radius as f32, std_dev as f32); } } + +fn convert_filter(filter: Arc) -> Option { + let nodes = filter.nodes(); + if nodes.is_empty() { + return None; + } + + // Vello CPU only supports single-node filters at the moment + let node = &filter.nodes()[0]; + let primitive = convert_filter_effect(&node.effect)?; + Some(vello_common::filter_effects::Filter::from_primitive( + primitive, + )) +} + +fn convert_filter_effect(effect: &FilterEffect) -> Option { + Some(match effect { + FilterEffect::Flood(color) => FilterPrimitive::Flood { color: *color }, + FilterEffect::GaussianBlur(blur) => FilterPrimitive::GaussianBlur { + std_deviation: blur.std_deviation, + edge_mode: convert_edge_mode(blur.edge_mode), + }, + FilterEffect::DropShadow(shadow) => FilterPrimitive::DropShadow { + dx: shadow.dx, + dy: shadow.dy, + std_deviation: shadow.std_deviation, + color: shadow.color, + edge_mode: convert_edge_mode(shadow.edge_mode), + }, + FilterEffect::Offset(offset) => FilterPrimitive::Offset { + dx: offset.x as f32, + dy: offset.y as f32, + }, + FilterEffect::ColorMatrix(matrix) => FilterPrimitive::ColorMatrix { matrix: matrix.0 }, + FilterEffect::ComponentTransfer(_component_transfer_filter) => return None, + FilterEffect::Blend(mode) => FilterPrimitive::Blend { mode: *mode }, + FilterEffect::Composite(_composite_operator) => return None, + FilterEffect::Morphology(_morphology_filter) => return None, + FilterEffect::ConvolveMatrix(_convolution_kernel) => return None, + FilterEffect::Turbulence(_turbulence_filter) => return None, + FilterEffect::DisplacementMap(_displacement_map_filter) => return None, + FilterEffect::Image(_external_image_source) => return None, + FilterEffect::Tile => return None, + FilterEffect::DiffuseLighting(_diffuse_lighting_filter) => return None, + FilterEffect::SpecularLighting(_specular_lighting_filter) => return None, + }) +} + +fn convert_edge_mode(edge_mode: EdgeMode) -> vello_common::filter_effects::EdgeMode { + match edge_mode { + EdgeMode::Duplicate => vello_common::filter_effects::EdgeMode::Duplicate, + EdgeMode::Wrap => vello_common::filter_effects::EdgeMode::Wrap, + EdgeMode::Mirror => vello_common::filter_effects::EdgeMode::Mirror, + EdgeMode::None => vello_common::filter_effects::EdgeMode::None, + } +} diff --git a/crates/anyrender_vello_hybrid/src/filters.rs b/crates/anyrender_vello_hybrid/src/filters.rs new file mode 100644 index 0000000..a2536f4 --- /dev/null +++ b/crates/anyrender_vello_hybrid/src/filters.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use anyrender::filters::{EdgeMode, Filter, FilterEffect}; +use vello_common::filter_effects::FilterPrimitive; + +pub(crate) fn convert_filter(filter: Arc) -> Option { + let nodes = filter.nodes(); + if nodes.is_empty() { + return None; + } + + // Vello Hybrid only supports single-node filters at the moment + let node = &filter.nodes()[0]; + let primitive = convert_filter_effect(&node.effect)?; + Some(vello_common::filter_effects::Filter::from_primitive( + primitive, + )) +} + +pub(crate) fn convert_filter_effect(effect: &FilterEffect) -> Option { + Some(match effect { + FilterEffect::Flood(color) => FilterPrimitive::Flood { color: *color }, + FilterEffect::GaussianBlur(blur) => FilterPrimitive::GaussianBlur { + std_deviation: blur.std_deviation, + edge_mode: convert_edge_mode(blur.edge_mode), + }, + FilterEffect::DropShadow(shadow) => FilterPrimitive::DropShadow { + dx: shadow.dx, + dy: shadow.dy, + std_deviation: shadow.std_deviation, + color: shadow.color, + edge_mode: convert_edge_mode(shadow.edge_mode), + }, + FilterEffect::Offset(offset) => FilterPrimitive::Offset { + dx: offset.x as f32, + dy: offset.y as f32, + }, + FilterEffect::ColorMatrix(matrix) => FilterPrimitive::ColorMatrix { matrix: matrix.0 }, + FilterEffect::ComponentTransfer(_component_transfer_filter) => return None, + FilterEffect::Blend(mode) => FilterPrimitive::Blend { mode: *mode }, + FilterEffect::Composite(_composite_operator) => return None, + FilterEffect::Morphology(_morphology_filter) => return None, + FilterEffect::ConvolveMatrix(_convolution_kernel) => return None, + FilterEffect::Turbulence(_turbulence_filter) => return None, + FilterEffect::DisplacementMap(_displacement_map_filter) => return None, + FilterEffect::Image(_external_image_source) => return None, + FilterEffect::Tile => return None, + FilterEffect::DiffuseLighting(_diffuse_lighting_filter) => return None, + FilterEffect::SpecularLighting(_specular_lighting_filter) => return None, + }) +} + +fn convert_edge_mode(edge_mode: EdgeMode) -> vello_common::filter_effects::EdgeMode { + match edge_mode { + EdgeMode::Duplicate => vello_common::filter_effects::EdgeMode::Duplicate, + EdgeMode::Wrap => vello_common::filter_effects::EdgeMode::Wrap, + EdgeMode::Mirror => vello_common::filter_effects::EdgeMode::Mirror, + EdgeMode::None => vello_common::filter_effects::EdgeMode::None, + } +} diff --git a/crates/anyrender_vello_hybrid/src/lib.rs b/crates/anyrender_vello_hybrid/src/lib.rs index 4d7ee0e..206c965 100644 --- a/crates/anyrender_vello_hybrid/src/lib.rs +++ b/crates/anyrender_vello_hybrid/src/lib.rs @@ -1,6 +1,7 @@ //! A [`vello_hybrid`] backend for the [`anyrender`] 2D drawing abstraction #![cfg_attr(docsrs, feature(doc_cfg))] +mod filters; mod scene; #[cfg(all(target_arch = "wasm32", feature = "webgl"))] mod webgl_scene; diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index e67593e..bfe8efb 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -14,6 +14,8 @@ use vello_hybrid::{Renderer, Resources, SampleRect}; use wgpu::{CommandEncoder, Device, Queue, Texture, TextureView, TextureViewDescriptor}; use wgpu_context::DeviceHandle; +use crate::filters::convert_filter; + const DEFAULT_TOLERANCE: f64 = 0.1; fn anyrender_paint_to_vello_hybrid_paint<'a>( @@ -178,15 +180,20 @@ impl PaintScene for VelloHybridScenePainter<'_> { alpha: f32, transform: Affine, clip: &impl Shape, - _filter: Option>, + filter: Option>, _backdrop_filter: Option>, ) { self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); self.scene .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); - self.scene - .push_layer(None, Some(blend.into()), Some(alpha), None, None); + self.scene.push_layer( + None, + Some(blend.into()), + Some(alpha), + None, + filter.and_then(convert_filter), + ); } fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index 317f0f3..c2cd07a 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -1,6 +1,6 @@ //! WebGL-compatible [`PaintScene`] implementation for [`vello_hybrid::Scene`]. -use anyrender::{Glyph, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; +use anyrender::{Filter, Glyph, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; use glifo::FontEmbolden; use kurbo::{Affine, Diagonal2, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, StyleRef}; @@ -10,6 +10,9 @@ use peniko::ImageBrush; use rustc_hash::FxHashMap; use vello_common::paint::{ImageId, ImageSource}; +use crate::filters::convert_filter; +use std::sync::Arc; + const DEFAULT_TOLERANCE: f64 = 0.1; pub struct WebGlImageManager<'a> { @@ -107,15 +110,20 @@ impl PaintScene for WebGlScenePainter<'_> { alpha: f32, transform: Affine, clip: &impl Shape, - _filter: Option>, + filter: Option>, _backdrop_filter: Option>, ) { self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); self.scene .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); - self.scene - .push_layer(None, Some(blend.into()), Some(alpha), None, None); + self.scene.push_layer( + None, + Some(blend.into()), + Some(alpha), + None, + filter.and_then(convert_filter), + ); } fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { From a8ba96c2dd782ea80a6f8573bf9687f3374c7429 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 4 Jun 2026 15:25:47 +0100 Subject: [PATCH 28/29] Hybrid: filter out unsupported filters --- crates/anyrender_vello_hybrid/src/filters.rs | 4 ++-- crates/anyrender_vello_hybrid/src/scene.rs | 12 +++--------- crates/anyrender_vello_hybrid/src/webgl_scene.rs | 11 +++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/crates/anyrender_vello_hybrid/src/filters.rs b/crates/anyrender_vello_hybrid/src/filters.rs index a2536f4..ae0f6b0 100644 --- a/crates/anyrender_vello_hybrid/src/filters.rs +++ b/crates/anyrender_vello_hybrid/src/filters.rs @@ -35,9 +35,9 @@ pub(crate) fn convert_filter_effect(effect: &FilterEffect) -> Option FilterPrimitive::ColorMatrix { matrix: matrix.0 }, + FilterEffect::ColorMatrix(_matrix) => return None, //FilterPrimitive::ColorMatrix { matrix: matrix.0 }, + FilterEffect::Blend(_mode) => return None, //FilterPrimitive::Blend { mode: *mode }, FilterEffect::ComponentTransfer(_component_transfer_filter) => return None, - FilterEffect::Blend(mode) => FilterPrimitive::Blend { mode: *mode }, FilterEffect::Composite(_composite_operator) => return None, FilterEffect::Morphology(_morphology_filter) => return None, FilterEffect::ConvolveMatrix(_convolution_kernel) => return None, diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index bfe8efb..991720d 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -14,8 +14,6 @@ use vello_hybrid::{Renderer, Resources, SampleRect}; use wgpu::{CommandEncoder, Device, Queue, Texture, TextureView, TextureViewDescriptor}; use wgpu_context::DeviceHandle; -use crate::filters::convert_filter; - const DEFAULT_TOLERANCE: f64 = 0.1; fn anyrender_paint_to_vello_hybrid_paint<'a>( @@ -183,17 +181,13 @@ impl PaintScene for VelloHybridScenePainter<'_> { filter: Option>, _backdrop_filter: Option>, ) { + let filter = filter.and_then(crate::filters::convert_filter); self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); self.scene .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); - self.scene.push_layer( - None, - Some(blend.into()), - Some(alpha), - None, - filter.and_then(convert_filter), - ); + self.scene + .push_layer(None, Some(blend.into()), Some(alpha), None, filter); } fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index c2cd07a..902a200 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -10,7 +10,6 @@ use peniko::ImageBrush; use rustc_hash::FxHashMap; use vello_common::paint::{ImageId, ImageSource}; -use crate::filters::convert_filter; use std::sync::Arc; const DEFAULT_TOLERANCE: f64 = 0.1; @@ -113,17 +112,13 @@ impl PaintScene for WebGlScenePainter<'_> { filter: Option>, _backdrop_filter: Option>, ) { + let filter = filter.and_then(crate::filters::convert_filter); self.scene.set_transform(transform); self.layer_stack.push(LayerKind::Layer); self.scene .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); - self.scene.push_layer( - None, - Some(blend.into()), - Some(alpha), - None, - filter.and_then(convert_filter), - ); + self.scene + .push_layer(None, Some(blend.into()), Some(alpha), None, filter); } fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { From 5b30b9f47070fb6a5ae099abf4c6c6a2ef16f83a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 4 Jun 2026 15:26:08 +0100 Subject: [PATCH 29/29] Vello CPU: feature flag filters --- crates/anyrender_vello_cpu/Cargo.toml | 1 + crates/anyrender_vello_cpu/src/filters.rs | 63 +++++++++++++++++++ crates/anyrender_vello_cpu/src/lib.rs | 3 + crates/anyrender_vello_cpu/src/scene.rs | 75 ++++------------------- 4 files changed, 80 insertions(+), 62 deletions(-) create mode 100644 crates/anyrender_vello_cpu/src/filters.rs diff --git a/crates/anyrender_vello_cpu/Cargo.toml b/crates/anyrender_vello_cpu/Cargo.toml index 69f4b5b..63043a3 100644 --- a/crates/anyrender_vello_cpu/Cargo.toml +++ b/crates/anyrender_vello_cpu/Cargo.toml @@ -22,6 +22,7 @@ log_frame_times = [ # Useful for benchmarking (as we expect to eventually eliminate this cost) # but not recommended for use in production experimental_image_cache = [] +filters = [] [dependencies] anyrender = { workspace = true } diff --git a/crates/anyrender_vello_cpu/src/filters.rs b/crates/anyrender_vello_cpu/src/filters.rs new file mode 100644 index 0000000..fd573d2 --- /dev/null +++ b/crates/anyrender_vello_cpu/src/filters.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use anyrender::{ + Filter, + filters::{EdgeMode, FilterEffect}, +}; +use vello_common::filter_effects::FilterPrimitive; + +pub(crate) fn convert_filter(filter: Arc) -> Option { + let nodes = filter.nodes(); + if nodes.is_empty() { + return None; + } + + // Vello CPU only supports single-node filters at the moment + let node = &filter.nodes()[0]; + let primitive = convert_filter_effect(&node.effect)?; + Some(vello_common::filter_effects::Filter::from_primitive( + primitive, + )) +} + +pub(crate) fn convert_filter_effect(effect: &FilterEffect) -> Option { + Some(match effect { + FilterEffect::Flood(color) => FilterPrimitive::Flood { color: *color }, + FilterEffect::GaussianBlur(blur) => FilterPrimitive::GaussianBlur { + std_deviation: blur.std_deviation, + edge_mode: convert_edge_mode(blur.edge_mode), + }, + FilterEffect::DropShadow(shadow) => FilterPrimitive::DropShadow { + dx: shadow.dx, + dy: shadow.dy, + std_deviation: shadow.std_deviation, + color: shadow.color, + edge_mode: convert_edge_mode(shadow.edge_mode), + }, + FilterEffect::Offset(offset) => FilterPrimitive::Offset { + dx: offset.x as f32, + dy: offset.y as f32, + }, + FilterEffect::ColorMatrix(matrix) => FilterPrimitive::ColorMatrix { matrix: matrix.0 }, + FilterEffect::ComponentTransfer(_component_transfer_filter) => return None, + FilterEffect::Blend(mode) => FilterPrimitive::Blend { mode: *mode }, + FilterEffect::Composite(_composite_operator) => return None, + FilterEffect::Morphology(_morphology_filter) => return None, + FilterEffect::ConvolveMatrix(_convolution_kernel) => return None, + FilterEffect::Turbulence(_turbulence_filter) => return None, + FilterEffect::DisplacementMap(_displacement_map_filter) => return None, + FilterEffect::Image(_external_image_source) => return None, + FilterEffect::Tile => return None, + FilterEffect::DiffuseLighting(_diffuse_lighting_filter) => return None, + FilterEffect::SpecularLighting(_specular_lighting_filter) => return None, + }) +} + +fn convert_edge_mode(edge_mode: EdgeMode) -> vello_common::filter_effects::EdgeMode { + match edge_mode { + EdgeMode::Duplicate => vello_common::filter_effects::EdgeMode::Duplicate, + EdgeMode::Wrap => vello_common::filter_effects::EdgeMode::Wrap, + EdgeMode::Mirror => vello_common::filter_effects::EdgeMode::Mirror, + EdgeMode::None => vello_common::filter_effects::EdgeMode::None, + } +} diff --git a/crates/anyrender_vello_cpu/src/lib.rs b/crates/anyrender_vello_cpu/src/lib.rs index d72c750..109aa5f 100644 --- a/crates/anyrender_vello_cpu/src/lib.rs +++ b/crates/anyrender_vello_cpu/src/lib.rs @@ -5,6 +5,9 @@ mod image_renderer; mod scene; mod window_renderer; +#[cfg(feature = "filters")] +mod filters; + pub use image_renderer::VelloCpuImageRenderer; pub use scene::VelloCpuScenePainter; diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index e2f9686..7aee289 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -1,13 +1,9 @@ use std::sync::Arc; -use anyrender::{ - Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext, - filters::{EdgeMode, FilterEffect}, -}; +use anyrender::{Filter, NormalizedCoord, Paint, PaintRef, PaintScene, RenderContext}; use glifo::FontEmbolden; use kurbo::{Affine, Diagonal2, Rect, Shape, Stroke}; use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, StyleRef}; -use vello_common::filter_effects::FilterPrimitive; use vello_cpu::{ImageSource, PaintType, Pixmap}; const DEFAULT_TOLERANCE: f64 = 0.1; @@ -72,13 +68,24 @@ impl PaintScene for VelloCpuScenePainter { filter: Option>, _backdrop_filter: Option>, ) { + #[cfg(feature = "filters")] + let filter = filter + .and_then(crate::filters::convert_filter) + .filter(|_| cfg!(not(feature = "multithreading"))); + + #[cfg(not(feature = "filters"))] + let filter = { + let _ = filter; + None + }; + self.render_ctx.set_transform(transform); self.render_ctx.push_layer( Some(&clip.into_path(DEFAULT_TOLERANCE)), Some(blend.into()), Some(alpha), None, - filter.and_then(convert_filter), + filter, ); } @@ -193,59 +200,3 @@ impl PaintScene for VelloCpuScenePainter { .fill_blurred_rounded_rect(&rect, radius as f32, std_dev as f32); } } - -fn convert_filter(filter: Arc) -> Option { - let nodes = filter.nodes(); - if nodes.is_empty() { - return None; - } - - // Vello CPU only supports single-node filters at the moment - let node = &filter.nodes()[0]; - let primitive = convert_filter_effect(&node.effect)?; - Some(vello_common::filter_effects::Filter::from_primitive( - primitive, - )) -} - -fn convert_filter_effect(effect: &FilterEffect) -> Option { - Some(match effect { - FilterEffect::Flood(color) => FilterPrimitive::Flood { color: *color }, - FilterEffect::GaussianBlur(blur) => FilterPrimitive::GaussianBlur { - std_deviation: blur.std_deviation, - edge_mode: convert_edge_mode(blur.edge_mode), - }, - FilterEffect::DropShadow(shadow) => FilterPrimitive::DropShadow { - dx: shadow.dx, - dy: shadow.dy, - std_deviation: shadow.std_deviation, - color: shadow.color, - edge_mode: convert_edge_mode(shadow.edge_mode), - }, - FilterEffect::Offset(offset) => FilterPrimitive::Offset { - dx: offset.x as f32, - dy: offset.y as f32, - }, - FilterEffect::ColorMatrix(matrix) => FilterPrimitive::ColorMatrix { matrix: matrix.0 }, - FilterEffect::ComponentTransfer(_component_transfer_filter) => return None, - FilterEffect::Blend(mode) => FilterPrimitive::Blend { mode: *mode }, - FilterEffect::Composite(_composite_operator) => return None, - FilterEffect::Morphology(_morphology_filter) => return None, - FilterEffect::ConvolveMatrix(_convolution_kernel) => return None, - FilterEffect::Turbulence(_turbulence_filter) => return None, - FilterEffect::DisplacementMap(_displacement_map_filter) => return None, - FilterEffect::Image(_external_image_source) => return None, - FilterEffect::Tile => return None, - FilterEffect::DiffuseLighting(_diffuse_lighting_filter) => return None, - FilterEffect::SpecularLighting(_specular_lighting_filter) => return None, - }) -} - -fn convert_edge_mode(edge_mode: EdgeMode) -> vello_common::filter_effects::EdgeMode { - match edge_mode { - EdgeMode::Duplicate => vello_common::filter_effects::EdgeMode::Duplicate, - EdgeMode::Wrap => vello_common::filter_effects::EdgeMode::Wrap, - EdgeMode::Mirror => vello_common::filter_effects::EdgeMode::Mirror, - EdgeMode::None => vello_common::filter_effects::EdgeMode::None, - } -}