Skip to content

Commit 816babc

Browse files
committed
Add SVG Gaussian blur filter effect
1 parent 1dc83ca commit 816babc

5 files changed

Lines changed: 96 additions & 3 deletions

File tree

node-graph/libraries/core-types/src/list.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use std::fmt::Debug;
1414
pub const ATTR_TRANSFORM: &str = "transform";
1515
/// Item's `BlendMode`, controlling how it composites with content beneath it.
1616
pub const ATTR_BLEND_MODE: &str = "blend_mode";
17+
/// Item's SVG-compatible filter effects, applied in order during rendering.
18+
pub const ATTR_FILTER_EFFECTS: &str = "filter_effects";
1719
/// Item's opacity multiplier (`f64`, implicit default `1.`).
1820
/// Composed multiplicatively through nested groups. Affects content clipped to the item.
1921
pub const ATTR_OPACITY: &str = "opacity";
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, dyn_any::DynAny)]
2+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3+
pub enum SvgFilterEffect {
4+
GaussianBlur {
5+
std_deviation_x: f64,
6+
std_deviation_y: f64,
7+
},
8+
}
9+
10+
impl Default for SvgFilterEffect {
11+
fn default() -> Self {
12+
Self::GaussianBlur {
13+
std_deviation_x: 0.,
14+
std_deviation_y: 0.,
15+
}
16+
}
17+
}

node-graph/libraries/graphic-types/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
pub mod artboard;
22
pub mod graphic;
3+
pub mod filter;
34

45
// Re-export all transitive dependencies so downstream crates only need to depend on graphic-types
56
pub use core_types;
67
pub use raster_types;
78
pub use vector_types;
9+
pub use filter::SvgFilterEffect;
810

911
// Re-export commonly used types at the crate root
1012
pub use artboard::Artboard;

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use core_types::bounds::RenderBoundingBox;
77
use core_types::color::Color;
88
use core_types::color::SRGBA8;
99
use core_types::consts::DEFAULT_FONT_SIZE;
10-
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
10+
use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, List};
1111
use core_types::math::quad::Quad;
1212
use core_types::render_complexity::RenderComplexity;
1313
use core_types::transform::Footprint;
@@ -21,6 +21,7 @@ use dyn_any::DynAny;
2121
use glam::{DAffine2, DMat2, DVec2};
2222
use graphene_hash::CacheHashWrapper;
2323
use graphene_resource::Resource;
24+
use graphic_types::SvgFilterEffect;
2425
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
2526
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
2627
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
@@ -268,6 +269,29 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
268269
}) + ")"
269270
}
270271

272+
fn write_svg_filter_def(svg_defs: &mut String, effects: &[SvgFilterEffect]) -> Option<String> {
273+
if effects.is_empty() {
274+
return None;
275+
}
276+
277+
let id = format!("filter-{}", generate_uuid());
278+
write!(svg_defs, r#"<filter id="{id}" x="-50%" y="-50%" width="200%" height="200%" color-interpolation-filters="sRGB">"#).unwrap();
279+
280+
let mut input = "SourceGraphic".to_string();
281+
for (index, effect) in effects.iter().enumerate() {
282+
let result = format!("effect{index}");
283+
match effect {
284+
SvgFilterEffect::GaussianBlur { std_deviation_x, std_deviation_y } => {
285+
write!(svg_defs, r#"<feGaussianBlur in="{input}" stdDeviation="{std_deviation_x} {std_deviation_y}" result="{result}"/>"#).unwrap();
286+
}
287+
}
288+
input = result;
289+
}
290+
291+
svg_defs.push_str("</filter>");
292+
Some(id)
293+
}
294+
271295
/// `(max, min)` factors by which a unit vector is stretched under `transform`'s linear part — the
272296
/// principal and minor singular values, equal to the semi-axes of the ellipse a unit circle maps to.
273297
/// Equivalent to `(max(sx, sy), min(sx, sy))` for axis-aligned scales, but accounts for shear.
@@ -835,6 +859,7 @@ impl Render for List<Graphic> {
835859
for index in 0..self.len() {
836860
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
837861
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
862+
let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index);
838863
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
839864
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
840865
let element = self.element(index).unwrap();
@@ -1061,9 +1086,9 @@ impl Render for List<Vector> {
10611086
let Some(vector) = self.element(index) else { continue };
10621087
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
10631088
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
1089+
let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index);
10641090
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
10651091
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
1066-
10671092
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
10681093
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
10691094
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
@@ -1161,6 +1186,10 @@ impl Render for List<Vector> {
11611186
attributes.push(ATTR_TRANSFORM, matrix);
11621187
}
11631188

1189+
if let Some(filter_id) = write_svg_filter_def(&mut attributes.0.svg_defs, &filter_effects) {
1190+
attributes.push("filter", format!("url(#{filter_id})"));
1191+
}
1192+
11641193
let defs = &mut attributes.0.svg_defs;
11651194
if let Some((ref id, mask_type, ref vector_item)) = push_id {
11661195
let mut svg = SvgRender::new();

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
33
use core::hash::{Hash, Hasher};
44
use core_types::blending::BlendMode;
55
use core_types::bounds::{BoundingBox, RenderBoundingBox};
6-
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
6+
use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
77
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
88
use core_types::transform::{Footprint, Transform};
99
use core_types::uuid::NodeId;
@@ -15,6 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
1515
use graphic_types::Vector;
1616
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
1717
use graphic_types::raster_types::{CPU, GPU, Raster};
18+
use graphic_types::SvgFilterEffect;
1819
use graphic_types::{Graphic, IntoGraphicList};
1920
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
2021
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
@@ -230,6 +231,48 @@ async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send +
230231
content
231232
}
232233

234+
trait FilterListIterMut {
235+
fn push_filter_effect(&mut self, effect: SvgFilterEffect);
236+
}
237+
238+
impl FilterListIterMut for List<Vector> {
239+
fn push_filter_effect(&mut self, effect: SvgFilterEffect) {
240+
for effects in self.iter_attribute_values_mut_or_default::<Vec<SvgFilterEffect>>(ATTR_FILTER_EFFECTS) {
241+
effects.push(effect.clone());
242+
}
243+
}
244+
}
245+
246+
impl FilterListIterMut for List<Graphic> {
247+
fn push_filter_effect(&mut self, effect: SvgFilterEffect) {
248+
for graphic in self.iter_element_values_mut() {
249+
let Some(vector_list) = graphic.as_vector_mut() else { continue };
250+
vector_list.push_filter_effect(effect.clone());
251+
}
252+
}
253+
}
254+
255+
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
256+
async fn svg_gaussian_blur<T>(
257+
_: impl Ctx,
258+
#[implementations(List<Vector>, List<Graphic>)]
259+
mut content: T,
260+
#[range]
261+
#[hard(0..)]
262+
#[soft(..100)]
263+
std_deviation: PixelLength,
264+
) -> T
265+
where
266+
T: FilterListIterMut + 'n + Send,
267+
{
268+
content.push_filter_effect(SvgFilterEffect::GaussianBlur {
269+
std_deviation_x: std_deviation,
270+
std_deviation_y: std_deviation,
271+
});
272+
content
273+
}
274+
275+
233276
trait IntoF64Vec {
234277
fn into_vec(self) -> Vec<f64>;
235278
}

0 commit comments

Comments
 (0)