svg-filters & basic parser #15
7 changed files with 237 additions and 85 deletions
|
@ -1,5 +1,6 @@
|
||||||
pub mod types;
|
#![feature(lint_reasons)]
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
pub use types::nodes::Node;
|
pub use types::nodes::Node;
|
||||||
pub use types::Edge;
|
pub use types::Edge;
|
||||||
pub use types::Filter;
|
pub use types::Filter;
|
||||||
|
|
|
@ -4,17 +4,6 @@ use svg_filters::{
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut supersimple = Filter::new();
|
|
||||||
|
|
||||||
let cm = supersimple.add_node(Node::color_matrix(ColorMatrixType::Matrix(Box::new([
|
|
||||||
1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.,
|
|
||||||
]))));
|
|
||||||
|
|
||||||
supersimple
|
|
||||||
.graph
|
|
||||||
.extend_with_edges(&[(supersimple.source_graphic(), cm)]);
|
|
||||||
dbg!(supersimple.to_svg());
|
|
||||||
|
|
||||||
let mut filter = Filter::new();
|
let mut filter = Filter::new();
|
||||||
|
|
||||||
// <filter id="chromabb" >
|
// <filter id="chromabb" >
|
||||||
|
@ -51,18 +40,18 @@ fn main() {
|
||||||
let composite_final = filter.add_node(Node::composite_arithmetic(0., 1., 1., 0.));
|
let composite_final = filter.add_node(Node::composite_arithmetic(0., 1., 1., 0.));
|
||||||
|
|
||||||
filter.graph.extend_with_edges(&[
|
filter.graph.extend_with_edges(&[
|
||||||
(filter.source_graphic(), chan_r, Edge::new()),
|
(filter.source_graphic(), chan_r, Edge::unnamed()),
|
||||||
(filter.source_graphic(), chan_b, Edge::new()),
|
(filter.source_graphic(), chan_b, Edge::unnamed()),
|
||||||
(filter.source_graphic(), chan_g, Edge::new()),
|
(filter.source_graphic(), chan_g, Edge::unnamed()),
|
||||||
(chan_r, offset_r, Edge::new()),
|
(chan_r, offset_r, Edge::new("ro")),
|
||||||
(offset_r, blur_r, Edge::new()),
|
(offset_r, blur_r, Edge::new("rob")),
|
||||||
(chan_b, offset_b, Edge::new()),
|
(chan_b, offset_b, Edge::new("bo")),
|
||||||
(offset_b, blur_b, Edge::new()),
|
(offset_b, blur_b, Edge::new("bob")),
|
||||||
(blur_r, composite_rb, Edge::in_idx(0)),
|
(blur_r, composite_rb, Edge::new("robc").with_idx(0)),
|
||||||
(blur_b, composite_rb, Edge::in_idx(1)),
|
(blur_b, composite_rb, Edge::new("bobc").with_idx(1)),
|
||||||
(composite_rb, composite_final, Edge::in_idx(0)),
|
(composite_rb, composite_final, Edge::new("cf").with_idx(0)),
|
||||||
(chan_g, composite_final, Edge::in_idx(1)),
|
(chan_g, composite_final, Edge::new("gf").with_idx(1)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
println!("Result: {}", filter.to_svg())
|
println!("{}", filter.to_svg())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
use std::{borrow::Cow, collections::HashMap, fmt::Debug, io::BufWriter, primitive};
|
use core::panic;
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Debug,
|
||||||
|
io::BufWriter,
|
||||||
|
primitive,
|
||||||
|
};
|
||||||
|
|
||||||
use petgraph::{
|
use petgraph::{
|
||||||
adj::EdgeIndex,
|
adj::EdgeIndex,
|
||||||
|
@ -9,7 +16,7 @@ use petgraph::{
|
||||||
prelude::NodeIndex,
|
prelude::NodeIndex,
|
||||||
visit::NodeIndexable,
|
visit::NodeIndexable,
|
||||||
};
|
};
|
||||||
use quick_xml::{events::attributes::Attribute, name::QName, ElementWriter};
|
use quick_xml::{events::attributes::Attribute, name::QName, ElementWriter, Error};
|
||||||
|
|
||||||
pub mod length;
|
pub mod length;
|
||||||
pub mod nodes;
|
pub mod nodes;
|
||||||
|
@ -53,12 +60,18 @@ impl Filter<'_> {
|
||||||
|
|
||||||
pub fn to_svg(&self) -> String {
|
pub fn to_svg(&self) -> String {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut writer = quick_xml::Writer::new(&mut result);
|
// let mut doc_writer = quick_xml::Writer::new(&mut result);
|
||||||
|
let mut doc_writer = quick_xml::Writer::new_with_indent(&mut result, b' ', 2);
|
||||||
|
|
||||||
|
doc_writer
|
||||||
|
.create_element("filter")
|
||||||
|
.with_attribute(("id", "chromabb_gen"))
|
||||||
|
.write_inner_content(|writer| {
|
||||||
let mut dfs_space = DfsSpace::new(&self.graph);
|
let mut dfs_space = DfsSpace::new(&self.graph);
|
||||||
let sorted = toposort(&self.graph, Some(&mut dfs_space)).expect("No cycles! Bad user!");
|
let sorted =
|
||||||
|
toposort(&self.graph, Some(&mut dfs_space)).expect("No cycles! Bad user!");
|
||||||
|
|
||||||
sorted
|
let v = sorted
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|node_idx| {
|
.filter_map(|node_idx| {
|
||||||
let node = self
|
let node = self
|
||||||
|
@ -76,11 +89,9 @@ impl Filter<'_> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fold(
|
.try_fold(writer, |acc, (node_idx, primitive, common_attrs)| {
|
||||||
&mut writer,
|
let mut el_writer = primitive.element_writer(&mut *acc);
|
||||||
|writer, (node_idx, primitive, common_attrs)| {
|
el_writer = create_input_attr(
|
||||||
let el_writer = primitive.element_writer(writer);
|
|
||||||
create_input_elements(
|
|
||||||
&self.graph,
|
&self.graph,
|
||||||
el_writer,
|
el_writer,
|
||||||
node_idx,
|
node_idx,
|
||||||
|
@ -88,12 +99,14 @@ impl Filter<'_> {
|
||||||
.node_weight(node_idx)
|
.node_weight(node_idx)
|
||||||
.expect("cannot get invalid node_idx from toposort")
|
.expect("cannot get invalid node_idx from toposort")
|
||||||
.input_count(),
|
.input_count(),
|
||||||
)
|
|
||||||
.write_empty()
|
|
||||||
.expect("should write successfully")
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
create_output_attr(&self.graph, el_writer, node_idx).write_empty()
|
||||||
|
})
|
||||||
|
.map(|_| ());
|
||||||
|
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
})
|
||||||
|
.expect("shouldnt fail to write or something");
|
||||||
String::from_utf8_lossy(&result).to_string()
|
String::from_utf8_lossy(&result).to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,25 +125,31 @@ pub struct Edge<'a> {
|
||||||
pub in_idx: Option<u8>,
|
pub in_idx: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Edge<'_> {
|
impl<'a> Edge<'a> {
|
||||||
pub fn new() -> Self {
|
pub fn new(name: &'a str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
edge_type: EdgeType::Unnamed,
|
edge_type: EdgeType::Named(name),
|
||||||
in_idx: None,
|
in_idx: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn in_idx(idx: u8) -> Self {
|
#[must_use]
|
||||||
|
pub fn with_idx(mut self, idx: u8) -> Self {
|
||||||
|
self.in_idx = Some(idx);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unnamed() -> Self {
|
||||||
Self {
|
Self {
|
||||||
edge_type: EdgeType::Unnamed,
|
edge_type: EdgeType::Unnamed,
|
||||||
in_idx: Some(idx),
|
in_idx: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Edge<'_> {
|
impl Default for Edge<'_> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::unnamed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,12 +159,13 @@ pub enum EdgeType<'a> {
|
||||||
/// For standard inputs such as SourceGraphic etc., which we'll just be representing as nodes for simplicity
|
/// For standard inputs such as SourceGraphic etc., which we'll just be representing as nodes for simplicity
|
||||||
Unnamed,
|
Unnamed,
|
||||||
}
|
}
|
||||||
fn create_input_elements<'a>(
|
|
||||||
g: &'a DiGraph<Node, Edge<'_>>,
|
fn create_input_attr<'w, 'b>(
|
||||||
mut el_writer: ElementWriter<'a, &'a mut Vec<u8>>,
|
g: &'_ DiGraph<Node, Edge<'_>>,
|
||||||
|
mut el_writer: ElementWriter<'w, &'b mut Vec<u8>>,
|
||||||
node_idx: NodeIndex,
|
node_idx: NodeIndex,
|
||||||
input_count: u8,
|
input_count: u8,
|
||||||
) -> ElementWriter<'a, &'a mut Vec<u8>> {
|
) -> ElementWriter<'w, &'b mut Vec<u8>> {
|
||||||
let inputs = g
|
let inputs = g
|
||||||
.neighbors_directed(node_idx, petgraph::Direction::Incoming)
|
.neighbors_directed(node_idx, petgraph::Direction::Incoming)
|
||||||
.collect::<Vec<NodeIndex>>();
|
.collect::<Vec<NodeIndex>>();
|
||||||
|
@ -176,12 +196,55 @@ fn create_input_elements<'a>(
|
||||||
|
|
||||||
let v = match incoming_node {
|
let v = match incoming_node {
|
||||||
Node::StdInput(std_in) => format!("{std_in:?}"),
|
Node::StdInput(std_in) => format!("{std_in:?}"),
|
||||||
Node::Primitive {
|
Node::Primitive { .. } => {
|
||||||
primitive,
|
if let EdgeType::Named(name) = edge_type {
|
||||||
common_attrs,
|
(*name).to_owned()
|
||||||
} => todo!(),
|
} else {
|
||||||
|
panic!(
|
||||||
|
"unnamed edges should not be used for connections between primitives"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
el_writer.with_attribute((in_attr_name.as_str(), v.as_str()))
|
el_writer.with_attribute((in_attr_name.as_str(), v.as_str()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used, reason = "all unwraps are for finding on options")]
|
||||||
|
fn create_output_attr<'w, 'b>(
|
||||||
|
g: &'_ DiGraph<Node, Edge<'_>>,
|
||||||
|
mut el_writer: ElementWriter<'w, &'b mut Vec<u8>>,
|
||||||
|
node_idx: NodeIndex,
|
||||||
|
) -> ElementWriter<'w, &'b mut Vec<u8>> {
|
||||||
|
let output = g
|
||||||
|
.neighbors_directed(node_idx, petgraph::Direction::Outgoing)
|
||||||
|
.map(|neighbor_idx| {
|
||||||
|
let edge_idx = g.find_edge(node_idx, neighbor_idx).unwrap();
|
||||||
|
|
||||||
|
let Edge {
|
||||||
|
edge_type: EdgeType::Named(name),
|
||||||
|
..
|
||||||
|
} = g.edge_weight(edge_idx).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("Unnamed edge used for connection between primitives");
|
||||||
|
};
|
||||||
|
*name
|
||||||
|
})
|
||||||
|
.collect::<HashSet<&str>>();
|
||||||
|
|
||||||
|
if output.is_empty() {
|
||||||
|
el_writer
|
||||||
|
} else if output.len() == 1 {
|
||||||
|
el_writer.with_attribute(Attribute {
|
||||||
|
key: QName(b"result"),
|
||||||
|
value: Cow::from(
|
||||||
|
(*output.into_iter().collect::<Vec<&str>>().first().unwrap())
|
||||||
|
.to_string()
|
||||||
|
.into_bytes(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
panic!("Can't have more then one named output: {output:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,18 +62,18 @@ impl WriteElement for FePrimitive {
|
||||||
fn attrs(&self) -> std::vec::Vec<quick_xml::events::attributes::Attribute<'_>> {
|
fn attrs(&self) -> std::vec::Vec<quick_xml::events::attributes::Attribute<'_>> {
|
||||||
match self {
|
match self {
|
||||||
FePrimitive::Blend(_) => todo!(),
|
FePrimitive::Blend(_) => todo!(),
|
||||||
FePrimitive::ColorMatrix(cm) => cm.attrs(),
|
FePrimitive::ColorMatrix(el) => el.attrs(),
|
||||||
FePrimitive::ComponentTransfer(_) => todo!(),
|
FePrimitive::ComponentTransfer(_) => todo!(),
|
||||||
FePrimitive::Composite(_) => todo!(),
|
FePrimitive::Composite(el) => el.attrs(),
|
||||||
FePrimitive::ConvolveMatrix(_) => todo!(),
|
FePrimitive::ConvolveMatrix(_) => todo!(),
|
||||||
FePrimitive::DiffuseLighting(_) => todo!(),
|
FePrimitive::DiffuseLighting(_) => todo!(),
|
||||||
FePrimitive::DisplacementMap(_) => todo!(),
|
FePrimitive::DisplacementMap(_) => todo!(),
|
||||||
FePrimitive::Flood(_) => todo!(),
|
FePrimitive::Flood(_) => todo!(),
|
||||||
FePrimitive::GaussianBlur(_) => todo!(),
|
FePrimitive::GaussianBlur(el) => el.attrs(),
|
||||||
FePrimitive::Image(_) => todo!(),
|
FePrimitive::Image(_) => todo!(),
|
||||||
FePrimitive::Merge(_) => todo!(),
|
FePrimitive::Merge(_) => todo!(),
|
||||||
FePrimitive::Morphology(_) => todo!(),
|
FePrimitive::Morphology(_) => todo!(),
|
||||||
FePrimitive::Offset(_) => todo!(),
|
FePrimitive::Offset(el) => el.attrs(),
|
||||||
FePrimitive::SpecularLighting(_) => todo!(),
|
FePrimitive::SpecularLighting(_) => todo!(),
|
||||||
FePrimitive::Tile(_) => todo!(),
|
FePrimitive::Tile(_) => todo!(),
|
||||||
FePrimitive::Turbulence(_) => todo!(),
|
FePrimitive::Turbulence(_) => todo!(),
|
||||||
|
@ -83,18 +83,18 @@ impl WriteElement for FePrimitive {
|
||||||
fn tag_name(&self) -> &'static str {
|
fn tag_name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
FePrimitive::Blend(_) => todo!(),
|
FePrimitive::Blend(_) => todo!(),
|
||||||
FePrimitive::ColorMatrix(cm) => cm.tag_name(),
|
FePrimitive::ColorMatrix(el) => el.tag_name(),
|
||||||
FePrimitive::ComponentTransfer(_) => todo!(),
|
FePrimitive::ComponentTransfer(_) => todo!(),
|
||||||
FePrimitive::Composite(_) => todo!(),
|
FePrimitive::Composite(el) => el.tag_name(),
|
||||||
FePrimitive::ConvolveMatrix(_) => todo!(),
|
FePrimitive::ConvolveMatrix(_) => todo!(),
|
||||||
FePrimitive::DiffuseLighting(_) => todo!(),
|
FePrimitive::DiffuseLighting(_) => todo!(),
|
||||||
FePrimitive::DisplacementMap(_) => todo!(),
|
FePrimitive::DisplacementMap(_) => todo!(),
|
||||||
FePrimitive::Flood(_) => todo!(),
|
FePrimitive::Flood(_) => todo!(),
|
||||||
FePrimitive::GaussianBlur(_) => todo!(),
|
FePrimitive::GaussianBlur(el) => el.tag_name(),
|
||||||
FePrimitive::Image(_) => todo!(),
|
FePrimitive::Image(_) => todo!(),
|
||||||
FePrimitive::Merge(_) => todo!(),
|
FePrimitive::Merge(_) => todo!(),
|
||||||
FePrimitive::Morphology(_) => todo!(),
|
FePrimitive::Morphology(_) => todo!(),
|
||||||
FePrimitive::Offset(_) => todo!(),
|
FePrimitive::Offset(el) => el.tag_name(),
|
||||||
FePrimitive::SpecularLighting(_) => todo!(),
|
FePrimitive::SpecularLighting(_) => todo!(),
|
||||||
FePrimitive::Tile(_) => todo!(),
|
FePrimitive::Tile(_) => todo!(),
|
||||||
FePrimitive::Turbulence(_) => todo!(),
|
FePrimitive::Turbulence(_) => todo!(),
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use quick_xml::{events::attributes::Attribute, name::QName};
|
||||||
|
|
||||||
|
use super::WriteElement;
|
||||||
|
|
||||||
/// [feComposite](https://www.w3.org/TR/SVG11/filters.html#feCompositeElement)
|
/// [feComposite](https://www.w3.org/TR/SVG11/filters.html#feCompositeElement)
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Composite {
|
pub struct Composite {
|
||||||
|
@ -25,3 +31,50 @@ pub enum CompositeOperator {
|
||||||
Xor,
|
Xor,
|
||||||
Arithmetic { k1: f32, k2: f32, k3: f32, k4: f32 },
|
Arithmetic { k1: f32, k2: f32, k3: f32, k4: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WriteElement for Composite {
|
||||||
|
fn attrs(&self) -> Vec<quick_xml::events::attributes::Attribute> {
|
||||||
|
let (op_name, vals) = match self.operator {
|
||||||
|
CompositeOperator::Over => ("over", None),
|
||||||
|
CompositeOperator::In => ("in", None),
|
||||||
|
CompositeOperator::Out => ("out", None),
|
||||||
|
CompositeOperator::Atop => ("atop", None),
|
||||||
|
CompositeOperator::Xor => ("xor", None),
|
||||||
|
CompositeOperator::Arithmetic { k1, k2, k3, k4 } => {
|
||||||
|
("arithmetic", Some([k1, k2, k3, k4]))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r = vec![Attribute {
|
||||||
|
key: QName(b"operator"),
|
||||||
|
value: Cow::from(op_name.as_bytes()),
|
||||||
|
}];
|
||||||
|
|
||||||
|
if let Some([k1, k2, k3, k4]) = vals {
|
||||||
|
r.append(&mut vec![
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"k1"),
|
||||||
|
value: Cow::from(k1.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"k2"),
|
||||||
|
value: Cow::from(k2.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"k3"),
|
||||||
|
value: Cow::from(k3.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"k4"),
|
||||||
|
value: Cow::from(k4.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_name(&self) -> &'static str {
|
||||||
|
"feComposite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use quick_xml::{events::attributes::Attribute, name::QName};
|
||||||
|
|
||||||
|
use super::WriteElement;
|
||||||
|
|
||||||
/// [feGaussianBlur](https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement)
|
/// [feGaussianBlur](https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement)
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct GaussianBlur {
|
pub struct GaussianBlur {
|
||||||
|
@ -17,3 +23,18 @@ impl GaussianBlur {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WriteElement for GaussianBlur {
|
||||||
|
fn attrs(&self) -> Vec<quick_xml::events::attributes::Attribute> {
|
||||||
|
vec![Attribute {
|
||||||
|
key: QName(b"stdDeviation"),
|
||||||
|
value: Cow::from(
|
||||||
|
format!("{} {}", self.std_deviation.0, self.std_deviation.1).into_bytes(),
|
||||||
|
),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_name(&self) -> &'static str {
|
||||||
|
"feGaussianBlur"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use quick_xml::{events::attributes::Attribute, name::QName};
|
||||||
|
|
||||||
|
use super::WriteElement;
|
||||||
|
|
||||||
/// [feOffset](https://www.w3.org/TR/SVG11/filters.html#feOffsetElement)
|
/// [feOffset](https://www.w3.org/TR/SVG11/filters.html#feOffsetElement)
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Offset {
|
pub struct Offset {
|
||||||
|
@ -10,3 +16,22 @@ impl Offset {
|
||||||
Self { dx, dy }
|
Self { dx, dy }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WriteElement for Offset {
|
||||||
|
fn attrs(&self) -> Vec<quick_xml::events::attributes::Attribute> {
|
||||||
|
vec![
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"dx"),
|
||||||
|
value: Cow::from(self.dx.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
Attribute {
|
||||||
|
key: QName(b"dy"),
|
||||||
|
value: Cow::from(self.dy.to_string().into_bytes()),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_name(&self) -> &'static str {
|
||||||
|
"feOffset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue