-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add SVG Gaussian blur filter effect #4320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, dyn_any::DynAny)] | ||
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | ||
| pub enum SvgFilterEffect { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this name should include SVG since gaussian blur is a standard filter that exists outside of the SVG specification. Other graphic types do not include SVG specific names.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guassian blur has only one input, so can get it implicity from the filter effect before. Other effects take several inputs, so you need to expand the data structure to take this into account. |
||
| GaussianBlur { | ||
| std_deviation_x: f64, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Prompt for AI agents |
||
| std_deviation_y: f64, | ||
| }, | ||
| } | ||
|
|
||
| impl Default for SvgFilterEffect { | ||
| fn default() -> Self { | ||
| Self::GaussianBlur { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If more |
||
| std_deviation_x: 0., | ||
| std_deviation_y: 0., | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,12 @@ | ||
| pub mod artboard; | ||
| pub mod graphic; | ||
| pub mod filter; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this be better in the |
||
|
|
||
| // Re-export all transitive dependencies so downstream crates only need to depend on graphic-types | ||
| pub use core_types; | ||
| pub use raster_types; | ||
| pub use vector_types; | ||
| pub use filter::SvgFilterEffect; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per code organisation, please put in the section below. This section is meant to "Re-export all transitive dependencies". |
||
|
|
||
| // Re-export commonly used types at the crate root | ||
| pub use artboard::Artboard; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ use core_types::bounds::RenderBoundingBox; | |
| use core_types::color::Color; | ||
| use core_types::color::SRGBA8; | ||
| use core_types::consts::DEFAULT_FONT_SIZE; | ||
| use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List}; | ||
| use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, List}; | ||
| use core_types::math::quad::Quad; | ||
| use core_types::render_complexity::RenderComplexity; | ||
| use core_types::transform::Footprint; | ||
|
|
@@ -21,6 +21,7 @@ use dyn_any::DynAny; | |
| use glam::{DAffine2, DMat2, DVec2}; | ||
| use graphene_hash::CacheHashWrapper; | ||
| use graphene_resource::Resource; | ||
| use graphic_types::SvgFilterEffect; | ||
| use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute}; | ||
| use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; | ||
| use graphic_types::vector_types::gradient::{GradientStops, GradientType}; | ||
|
|
@@ -268,6 +269,29 @@ pub fn format_transform_matrix(transform: DAffine2) -> String { | |
| }) + ")" | ||
| } | ||
|
|
||
| fn write_svg_filter_def(svg_defs: &mut String, effects: &[SvgFilterEffect]) -> Option<String> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The emitted Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Identical filter effects are emitted as separate SVG Prompt for AI agents |
||
| if effects.is_empty() { | ||
| return None; | ||
| } | ||
|
|
||
| let id = format!("filter-{}", generate_uuid()); | ||
| write!(svg_defs, r#"<filter id="{id}" x="-50%" y="-50%" width="200%" height="200%" color-interpolation-filters="sRGB">"#).unwrap(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps add comment to explain why the default expansion was not enough to avoid clipping. |
||
|
|
||
| let mut input = "SourceGraphic".to_string(); | ||
| for (index, effect) in effects.iter().enumerate() { | ||
| let result = format!("effect{index}"); | ||
| match effect { | ||
| SvgFilterEffect::GaussianBlur { std_deviation_x, std_deviation_y } => { | ||
| write!(svg_defs, r#"<feGaussianBlur in="{input}" stdDeviation="{std_deviation_x} {std_deviation_y}" result="{result}"/>"#).unwrap(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to specify the |
||
| } | ||
| } | ||
| input = result; | ||
| } | ||
|
|
||
| svg_defs.push_str("</filter>"); | ||
| Some(id) | ||
| } | ||
|
|
||
| /// `(max, min)` factors by which a unit vector is stretched under `transform`'s linear part — the | ||
| /// principal and minor singular values, equal to the semi-axes of the ellipse a unit circle maps to. | ||
| /// 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> { | |
| for index in 0..self.len() { | ||
| let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); | ||
| let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); | ||
| let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index); | ||
|
sahilsahni18 marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per #3779, all things must now be a |
||
| let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); | ||
| let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); | ||
| let element = self.element(index).unwrap(); | ||
|
|
@@ -1061,9 +1086,9 @@ impl Render for List<Vector> { | |
| let Some(vector) = self.element(index) else { continue }; | ||
| let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); | ||
| let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); | ||
| let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); | ||
| let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); | ||
|
|
||
| // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform | ||
| let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); | ||
| 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> { | |
| attributes.push(ATTR_TRANSFORM, matrix); | ||
| } | ||
|
|
||
| if let Some(filter_id) = write_svg_filter_def(&mut attributes.0.svg_defs, &filter_effects) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Applying the filter at this level (inside
Consider moving the filter application to the Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Gaussian blur filter visually expands content beyond the vector's geometric bounds, but Prompt for AI agents |
||
| attributes.push("filter", format!("url(#{filter_id})")); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When zooming the viewport, the amount of blur changes. This is not desirable. |
||
| } | ||
|
Comment on lines
+1189
to
+1191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applying the filter directly to individual
Since every graphic element is already wrapped in a Please remove this block and instead apply the filter to the |
||
|
|
||
| let defs = &mut attributes.0.svg_defs; | ||
| if let Some((ref id, mask_type, ref vector_item)) = push_id { | ||
| let mut svg = SvgRender::new(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU}; | |
| use core::hash::{Hash, Hasher}; | ||
| use core_types::blending::BlendMode; | ||
| use core_types::bounds::{BoundingBox, RenderBoundingBox}; | ||
| use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn}; | ||
| use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn}; | ||
| use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; | ||
| use core_types::transform::{Footprint, Transform}; | ||
| use core_types::uuid::NodeId; | ||
|
|
@@ -15,6 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; | |
| use graphic_types::Vector; | ||
| use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; | ||
| use graphic_types::raster_types::{CPU, GPU, Raster}; | ||
| use graphic_types::SvgFilterEffect; | ||
| use graphic_types::{Graphic, IntoGraphicList}; | ||
| use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; | ||
| 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 + | |
| content | ||
| } | ||
|
|
||
| trait FilterListIterMut { | ||
| fn push_filter_effect(&mut self, effect: SvgFilterEffect); | ||
| } | ||
|
|
||
| impl FilterListIterMut for List<Vector> { | ||
| fn push_filter_effect(&mut self, effect: SvgFilterEffect) { | ||
| for effects in self.iter_attribute_values_mut_or_default::<Vec<SvgFilterEffect>>(ATTR_FILTER_EFFECTS) { | ||
| effects.push(effect.clone()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl FilterListIterMut for List<Graphic> { | ||
| fn push_filter_effect(&mut self, effect: SvgFilterEffect) { | ||
| for graphic in self.iter_element_values_mut() { | ||
| let Some(vector_list) = graphic.as_vector_mut() else { continue }; | ||
| vector_list.push_filter_effect(effect.clone()); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+246
to
+253
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently,
Instead, we should set the impl FilterListIterMut for List<Graphic> {
fn push_filter_effect(&mut self, effect: SvgFilterEffect) {
for effects in self.iter_attribute_values_mut_or_default::<Vec<SvgFilterEffect>>(ATTR_FILTER_EFFECTS) {
effects.push(effect.clone());
}
}
} |
||
|
|
||
| #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] | ||
| async fn svg_gaussian_blur<T>( | ||
| _: impl Ctx, | ||
| #[implementations(List<Vector>, List<Graphic>)] | ||
| mut content: T, | ||
| #[range] | ||
| #[hard(0..)] | ||
| #[soft(..100)] | ||
| std_deviation: PixelLength, | ||
| ) -> T | ||
| where | ||
| T: FilterListIterMut + 'n + Send, | ||
| { | ||
| content.push_filter_effect(SvgFilterEffect::GaussianBlur { | ||
| std_deviation_x: std_deviation, | ||
| std_deviation_y: std_deviation, | ||
|
Comment on lines
+269
to
+270
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not allow users to set the x and y to different values? |
||
| }); | ||
| content | ||
| } | ||
|
|
||
|
|
||
| trait IntoF64Vec { | ||
| fn into_vec(self) -> Vec<f64>; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not relevant to this PR (probably #4319). Please keep your branches separate to allow for reviewing small changes…