diff --git a/Cargo.lock b/Cargo.lock
index cbf8f6d..a175ea0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -159,6 +159,7 @@ dependencies = [
"peniko",
"raw-window-handle",
"serde",
+ "smallvec",
]
[[package]]
@@ -244,6 +245,7 @@ dependencies = [
"peniko",
"pixels_window_renderer",
"softbuffer_window_renderer",
+ "vello_common",
"vello_cpu",
]
diff --git a/crates/anyrender/Cargo.toml b/crates/anyrender/Cargo.toml
index 7d87443..0899b32 100644
--- a/crates/anyrender/Cargo.toml
+++ b/crates/anyrender/Cargo.toml
@@ -22,4 +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
new file mode 100644
index 0000000..0e0a7b5
--- /dev/null
+++ b/crates/anyrender/src/filters.rs
@@ -0,0 +1,1246 @@
+// 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:
+
+// ## 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 self::{
+ blur::GaussianBlurFilter,
+ color_transformation::ColorMatrix,
+ 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.
+///
+/// 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<[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.
+ /// 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 {
+ /// Create a new empty filter graph.
+ pub fn empty() -> Self {
+ Self {
+ primitives: SmallVec::new(),
+ output: FilterId(0),
+ expansion_rect: Rect::ZERO,
+ }
+ }
+
+ /// 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::empty();
+ 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, 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 = effect.expansion_rect();
+ self.expansion_rect = self.expansion_rect.union(primitive_rect);
+
+ self.primitives.push(FilterGraphNode { effect, inputs });
+
+ id
+ }
+
+ /// The list of nodes in the graph
+ pub fn nodes(&self) -> &[FilterGraphNode] {
+ &self.primitives
+ }
+
+ /// The output filter for the graph.
+ pub fn output(&self) -> FilterId {
+ self.output
+ }
+
+ /// Set the output filter for the graph.
+ 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,
+ /// 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)
+ }
+
+ pub fn expansion_rect(&self) -> Rect {
+ self.expansion_rect
+ }
+}
+
+#[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
+/// beyond the original image boundaries. This is particularly important for blur and
+/// convolution operations near edges.
+///
+/// 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).
+ ///
+ /// 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)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum FilterEffect {
+ /// 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(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(GaussianBlurFilter),
+
+ /// 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(DropShadow),
+
+ /// Matrix-based color transformation.
+ ///
+ /// Applies a 4x5 matrix transformation to colors, allowing arbitrary
+ /// color space transformations, hue shifts, and color adjustments.
+ ///
+ /// 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(ColorMatrix),
+
+ /// Geometric offset/translation.
+ ///
+ /// Shifts the input image by the specified offset. Useful for creating
+ /// shadow effects or positioning elements in a filter graph.
+ ///
+ /// 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(CompositeOperator),
+
+ /// Blend two inputs using blend modes.
+ ///
+ /// Combines two input images using Photoshop-style blend modes
+ /// (multiply, screen, overlay, etc.).
+ 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(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(ConvolutionKernel),
+
+ /// Generate Perlin noise/turbulence patterns.
+ ///
+ /// Creates procedural noise patterns useful for textures, clouds,
+ /// marble effects, and other organic-looking randomness.
+ 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(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(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(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(SpecularLightingFilter),
+}
+
+// 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.
+ ///
+ /// 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,
+ })
+ }
+
+ /// 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
+ /// 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(blur) => {
+ // Gaussian blur expands uniformly by 3*sigma (covers 99.7% of distribution)
+ let radius = (blur.std_deviation * 3.0) as f64;
+ Rect::new(-radius, -radius, radius, radius)
+ }
+ Self::Offset(offset) => {
+ // Offset shifts pixels; expand bounds asymmetrically so shifted content isn't cut.
+ 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(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::FilterEffect;
+ use kurbo::{Rect, Vec2};
+
+ #[test]
+ fn offset_expands_in_direction_of_shift() {
+ 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),
+ "Offset expansion should be asymmetric and include the shift vector"
+ );
+ }
+}
+
+/// 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;
+
+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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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)]
+ #[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.
+ ///
+ /// 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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.
+ /// 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).
+ ///
+ /// 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,
+ }
+}
+
+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)]
+ #[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,
+ /// 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.
+ #[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).
+ ///
+ /// 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,
+ }
+}
+
+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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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 {
+ use smallvec::SmallVec;
+
+ /// 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ pub struct ComponentTransferFilter {
+ /// Transfer function applied to the red channel (None = identity).
+ pub red_function: TransferFunction,
+ /// Transfer function applied to the green channel (None = identity).
+ pub green_function: TransferFunction,
+ /// Transfer function applied to the blue channel (None = identity).
+ pub blue_function: TransferFunction,
+ /// Transfer function applied to the alpha channel (None = identity).
+ pub alpha_function: TransferFunction,
+ }
+
+ 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: TransferFunction::Identity,
+ green_function: TransferFunction::Identity,
+ blue_function: TransferFunction::Identity,
+ alpha_function: 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: func.clone(),
+ green_function: func.clone(),
+ blue_function: func.clone(),
+ alpha_function: TransferFunction::Identity,
+ }
+ }
+
+ /// 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: func.clone(),
+ green_function: func.clone(),
+ blue_function: func.clone(),
+ alpha_function: TransferFunction::Identity,
+ }
+ }
+
+ /// 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: func.clone(),
+ green_function: func.clone(),
+ blue_function: func.clone(),
+ alpha_function: TransferFunction::Identity,
+ }
+ }
+ }
+
+ /// 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ 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.
+ ///
+ /// Lookup table values defining the transfer curve.
+ /// More values provide smoother curves. Minimum 2 values required.
+ Table(SmallVec<[f32; 2]>),
+
+ /// 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.
+ ///
+ /// 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(LinearTransferFunction),
+
+ // Gamma correction: output = amplitude × input^exponent + offset.
+ Gamma(GammaTransferFunction),
+ }
+
+ /// Linear function: output = slope × input + intercept.
+ ///
+ /// Simple linear transformation of the input value.
+ #[derive(Debug, Clone, PartialEq)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ pub struct LinearTransferFunction {
+ 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)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ pub struct GammaTransferFunction {
+ pub amplitude: f32,
+ pub exponent: f32,
+ pub offset: f32,
+ }
+}
+
+/// 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 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).
+ /// 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 {
+ /// 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,
+ ])
+ }
+
+ /// 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
+ 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
+ ]);
+ }
+}
+
+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
+/// for various image processing effects. All provided kernels are 3x3.
+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)]
+ #[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.
+ 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 {
+ 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..b0e88e1 100644
--- a/crates/anyrender/src/lib.rs
+++ b/crates/anyrender/src/lib.rs
@@ -43,6 +43,8 @@ use peniko::{BlendMode, Color, Fill, FontData, ImageBrushRef, StyleRef};
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;
@@ -191,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.
@@ -259,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..6467669 100644
--- a/crates/anyrender_skia/src/scene.rs
+++ b/crates/anyrender_skia/src/scene.rs
@@ -1,9 +1,13 @@
+use std::sync::Arc;
+
+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},
@@ -13,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,
@@ -83,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
@@ -411,6 +420,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();
@@ -418,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) {
@@ -563,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::{
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/Cargo.toml b/crates/anyrender_vello_cpu/Cargo.toml
index 1caed8d..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 }
@@ -36,6 +37,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/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 b77132c..7aee289 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,14 +65,27 @@ impl PaintScene for VelloCpuScenePainter {
alpha: f32,
transform: Affine,
clip: &impl Shape,
+ 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,
- None,
+ filter,
);
}
diff --git a/crates/anyrender_vello_hybrid/src/filters.rs b/crates/anyrender_vello_hybrid/src/filters.rs
new file mode 100644
index 0000000..ae0f6b0
--- /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) => return None, //FilterPrimitive::ColorMatrix { matrix: matrix.0 },
+ FilterEffect::Blend(_mode) => return None, //FilterPrimitive::Blend { mode: *mode },
+ FilterEffect::ComponentTransfer(_component_transfer_filter) => return None,
+ 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 0570176..991720d 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,13 +178,16 @@ impl PaintScene for VelloHybridScenePainter<'_> {
alpha: f32,
transform: Affine,
clip: &impl Shape,
+ 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, None);
+ .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 778e38e..902a200 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,8 @@ use peniko::ImageBrush;
use rustc_hash::FxHashMap;
use vello_common::paint::{ImageId, ImageSource};
+use std::sync::Arc;
+
const DEFAULT_TOLERANCE: f64 = 0.1;
pub struct WebGlImageManager<'a> {
@@ -107,13 +109,16 @@ impl PaintScene for WebGlScenePainter<'_> {
alpha: f32,
transform: Affine,
clip: &impl Shape,
+ 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, None);
+ .push_layer(None, Some(blend.into()), Some(alpha), None, filter);
}
fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
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