mirror of
https://github.com/penpot/penpot.git
synced 2026-09-11 22:49:30 +00:00
✨ Export layer blur to WASM SVG (#11580)
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a native feGaussianBlur filter on the composite <g>. Match canvas sigma via radius_to_sigma(value * scale), and skip Skia blur filters on the SVG VectorRenderer path so shapes do not vanish. Closes #11380
This commit is contained in:
parent
30849babcc
commit
dbe5941a23
@ -1,6 +1,6 @@
|
|||||||
use skia_safe::{self as skia, Paint};
|
use skia_safe::{self as skia, Paint};
|
||||||
|
|
||||||
use crate::shapes::{Shape, Type};
|
use crate::shapes::{radius_to_sigma, Shape, Type};
|
||||||
use crate::state::ShapesPoolRef;
|
use crate::state::ShapesPoolRef;
|
||||||
|
|
||||||
use crate::render::vector::draw_shape_geometry;
|
use crate::render::vector::draw_shape_geometry;
|
||||||
@ -151,6 +151,25 @@ impl SvgLayerCanvas {
|
|||||||
"<clipPath id=\"{id}\" clipPathUnits=\"userSpaceOnUse\">{geometry}</clipPath>"
|
"<clipPath id=\"{id}\" clipPathUnits=\"userSpaceOnUse\">{geometry}</clipPath>"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Registers a layer-blur `<filter>` and returns its id.
|
||||||
|
///
|
||||||
|
/// `sigma` is Skia/canvas stdDeviation (`radius_to_sigma(value * scale)`).
|
||||||
|
/// Padding (±50%) avoids the default 10% objectBoundingBox clip on large blurs.
|
||||||
|
pub(super) fn push_layer_blur_filter(&mut self, sigma: f32) -> String {
|
||||||
|
let id = self.unique("blur");
|
||||||
|
self.defs.push_str(&format!(
|
||||||
|
concat!(
|
||||||
|
"<filter id=\"{id}\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" ",
|
||||||
|
"color-interpolation-filters=\"sRGB\">",
|
||||||
|
"<feGaussianBlur stdDeviation=\"{sigma}\"/>",
|
||||||
|
"</filter>"
|
||||||
|
),
|
||||||
|
id = id,
|
||||||
|
sigma = sigma
|
||||||
|
));
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a clip geometry into `cv` (already set up with the page transform).
|
/// Draws a clip geometry into `cv` (already set up with the page transform).
|
||||||
@ -171,11 +190,11 @@ fn draw_clip_geometry(cv: &skia::Canvas, shape: &Shape, tree: ShapesPoolRef, pai
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the `<g>` attribute string for a shape's composite effects (opacity,
|
/// Builds the `<g>` attribute string for a shape's composite effects (opacity,
|
||||||
/// blend mode). Returns `None` when the shape needs no wrapper.
|
/// blend mode, layer blur). Returns `None` when the shape needs no wrapper.
|
||||||
///
|
///
|
||||||
/// Layer blur / shadows are intentionally omitted here — they need native SVG
|
/// Layer blur is a native SVG `<filter>` (SkSVGDevice drops paint image-filters).
|
||||||
/// filter re-emission to survive `SkSVGDevice` and land in later PRs.
|
/// Shadows still need dedicated re-emission in a later PR.
|
||||||
pub(super) fn effect_attrs(element: &Shape) -> Option<String> {
|
pub(super) fn effect_attrs(builder: &mut SvgLayerCanvas, element: &Shape) -> Option<String> {
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
|
|
||||||
let opacity = element.opacity();
|
let opacity = element.opacity();
|
||||||
@ -187,6 +206,13 @@ pub(super) fn effect_attrs(element: &Shape) -> Option<String> {
|
|||||||
parts.push(format!("style=\"mix-blend-mode:{css}\""));
|
parts.push(format!("style=\"mix-blend-mode:{css}\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(blur) = element.visible_layer_blur() {
|
||||||
|
// Match canvas `Shape::image_filter`: sigma from radius × export scale.
|
||||||
|
let sigma = radius_to_sigma(blur.value * builder.scale);
|
||||||
|
let id = builder.push_layer_blur_filter(sigma);
|
||||||
|
parts.push(format!("filter=\"url(#{id})\""));
|
||||||
|
}
|
||||||
|
|
||||||
if parts.is_empty() {
|
if parts.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ pub(super) fn render_frame(
|
|||||||
tree: ShapesPoolRef,
|
tree: ShapesPoolRef,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let effects = effect_attrs(element);
|
let effects = effect_attrs(builder, element);
|
||||||
if let Some(attrs) = &effects {
|
if let Some(attrs) = &effects {
|
||||||
builder.open_group(attrs);
|
builder.open_group(attrs);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ pub(super) fn render_group(
|
|||||||
tree: ShapesPoolRef,
|
tree: ShapesPoolRef,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let effects = effect_attrs(element);
|
let effects = effect_attrs(builder, element);
|
||||||
if let Some(attrs) = &effects {
|
if let Some(attrs) = &effects {
|
||||||
builder.open_group(attrs);
|
builder.open_group(attrs);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,9 +60,10 @@ fn svg_page_bounds(shape: &Shape, tree: ShapesPoolRef, scale: f32) -> skia::Rect
|
|||||||
/// composed as native SVG `<g>` wrappers. Frame `clip content` uses a native
|
/// composed as native SVG `<g>` wrappers. Frame `clip content` uses a native
|
||||||
/// `<clipPath>`.
|
/// `<clipPath>`.
|
||||||
///
|
///
|
||||||
/// Shadows, layer blur, masks, and text strokes still need dedicated SVG
|
/// Layer blur is re-emitted as a native SVG `feGaussianBlur` filter wrapper.
|
||||||
/// re-emission. Solid Inner/Outer and dotted/dashed strokes go out as filled
|
/// Shadows, masks, and text strokes still need dedicated SVG re-emission.
|
||||||
/// outlines; image-filled strokes use a linked `<image>` clipped to the stroke.
|
/// Solid Inner/Outer and dotted/dashed strokes go out as filled outlines;
|
||||||
|
/// image-filled strokes use a linked `<image>` clipped to the stroke.
|
||||||
pub fn render_to_svg(
|
pub fn render_to_svg(
|
||||||
shared: &mut RenderResources,
|
shared: &mut RenderResources,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
@ -186,7 +187,7 @@ fn render_leaf(
|
|||||||
tree: ShapesPoolRef,
|
tree: ShapesPoolRef,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let effects = effect_attrs(element);
|
let effects = effect_attrs(builder, element);
|
||||||
if let Some(attrs) = &effects {
|
if let Some(attrs) = &effects {
|
||||||
builder.open_group(attrs);
|
builder.open_group(attrs);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="223.7846" height="123.78461" viewBox="0 0 223.7846 123.78461"><defs><filter id="blur0" x="-50%" y="-50%" width="200%" height="200%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="3.9641016"/></filter></defs><g filter="url(#blur0)">
|
||||||
|
<rect fill="blue" transform="translate(11.8923 11.8923)" width="90" height="100"/>
|
||||||
|
<rect fill="#00C800" transform="translate(11.8923 11.8923)" x="110" width="90" height="100"/>
|
||||||
|
</g></svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="137.64102" height="117.64102" viewBox="0 0 137.64102 117.64102"><defs><filter id="blur0" x="-50%" y="-50%" width="200%" height="200%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="6.2735023"/></filter></defs><g filter="url(#blur0)">
|
||||||
|
<rect fill="red" transform="translate(18.8205 18.8205)" width="100" height="80"/>
|
||||||
|
</g></svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<rect fill="red" width="100" height="80"/>
|
||||||
|
</svg>
|
||||||
@ -1,7 +1,8 @@
|
|||||||
use super::fixtures::*;
|
use super::fixtures::*;
|
||||||
|
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
BlendMode, Fill, ImageFill, ImageFillTransform, SolidColor, StrokeCap, StrokeKind,
|
radius_to_sigma, BlendMode, Blur, BlurType, Fill, ImageFill, ImageFillTransform, SolidColor,
|
||||||
|
StrokeCap, StrokeKind,
|
||||||
};
|
};
|
||||||
use crate::state::ShapesPool;
|
use crate::state::ShapesPool;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
@ -85,6 +86,116 @@ fn exports_leaf_opacity_and_blend_mode_as_group_wrappers() {
|
|||||||
insta::assert_snapshot!(svg);
|
insta::assert_snapshot!(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_leaf_layer_blur_as_fe_gaussian_blur() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_solid_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
skia::Color::from_rgb(255, 0, 0),
|
||||||
|
);
|
||||||
|
let blur_value = 10.0;
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
shape.set_blur(Some(Blur::new(BlurType::LayerBlur, false, blur_value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
let expected_sigma = radius_to_sigma(blur_value * 1.0);
|
||||||
|
assert!(
|
||||||
|
svg.contains("<filter") && svg.contains("feGaussianBlur"),
|
||||||
|
"layer blur must emit an SVG filter: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains(&format!("stdDeviation=\"{expected_sigma}\"")),
|
||||||
|
"stdDeviation must match canvas radius_to_sigma(value * scale): {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("filter=\"url(#blur"),
|
||||||
|
"shape group must reference the blur filter: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skips_hidden_layer_blur() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_solid_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
skia::Color::from_rgb(255, 0, 0),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
shape.set_blur(Some(Blur::new(BlurType::LayerBlur, true, 10.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("feGaussianBlur") && !svg.contains("filter=\"url(#blur"),
|
||||||
|
"hidden layer blur must not emit a filter: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_group_layer_blur_wrapping_children() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let group_id = uid(1);
|
||||||
|
let a = uid(2);
|
||||||
|
let b = uid(3);
|
||||||
|
|
||||||
|
add_group(
|
||||||
|
&mut pool,
|
||||||
|
group_id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 200.0, 100.0),
|
||||||
|
&[a, b],
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let group = pool.get_mut(&group_id).unwrap();
|
||||||
|
group.set_blur(Some(Blur::new(BlurType::LayerBlur, false, 6.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
add_solid_rect(
|
||||||
|
&mut pool,
|
||||||
|
a,
|
||||||
|
group_id,
|
||||||
|
(0.0, 0.0, 90.0, 100.0),
|
||||||
|
skia::Color::from_rgb(0, 0, 255),
|
||||||
|
);
|
||||||
|
add_solid_rect(
|
||||||
|
&mut pool,
|
||||||
|
b,
|
||||||
|
group_id,
|
||||||
|
(110.0, 0.0, 200.0, 100.0),
|
||||||
|
skia::Color::from_rgb(0, 200, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, group_id);
|
||||||
|
let expected_sigma = radius_to_sigma(6.0);
|
||||||
|
assert!(
|
||||||
|
svg.contains(&format!("stdDeviation=\"{expected_sigma}\"")),
|
||||||
|
"group layer blur stdDeviation: {svg}"
|
||||||
|
);
|
||||||
|
// Filter wrapper must open before child geometry.
|
||||||
|
let filter_pos = svg
|
||||||
|
.find("filter=\"url(#blur")
|
||||||
|
.expect("group filter wrapper");
|
||||||
|
let child_pos = svg.find("fill=\"#").expect("child fill");
|
||||||
|
assert!(
|
||||||
|
filter_pos < child_pos,
|
||||||
|
"group blur must wrap children: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exports_a_group_with_two_rects_and_group_opacity() {
|
fn exports_a_group_with_two_rects_and_group_opacity() {
|
||||||
let mut pool = ShapesPool::new();
|
let mut pool = ShapesPool::new();
|
||||||
|
|||||||
@ -44,6 +44,18 @@ impl<'a> VectorRenderer<'a> {
|
|||||||
compose_fills,
|
compose_fills,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layer-blur paint filter for this backend.
|
||||||
|
///
|
||||||
|
/// SVG export (`compose_fills == false`) returns `None`: `SkSVGDevice` drops
|
||||||
|
/// paint image-filters (the shape would vanish). Layer blur is re-emitted as
|
||||||
|
/// a native SVG `<filter>` wrapper instead.
|
||||||
|
fn layer_blur_filter(&self, shape: &Shape) -> Option<skia::ImageFilter> {
|
||||||
|
if !self.compose_fills {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
shape.image_filter(1.)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShapeRenderer for VectorRenderer<'_> {
|
impl ShapeRenderer for VectorRenderer<'_> {
|
||||||
@ -52,17 +64,24 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let blur_filter = self.layer_blur_filter(shape);
|
||||||
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
||||||
if !self.compose_fills || has_image_fills {
|
if !self.compose_fills || has_image_fills {
|
||||||
// fills[0] is the topmost layer; draw bottom → top (matches GPU + classic SVG).
|
// fills[0] is the topmost layer; draw bottom → top (matches GPU + classic SVG).
|
||||||
for fill in fills.iter().rev() {
|
for fill in fills.iter().rev() {
|
||||||
match fill {
|
match fill {
|
||||||
Fill::Image(image_fill) => {
|
Fill::Image(image_fill) => {
|
||||||
draw_image_fill(self.shared, self.canvas, shape, image_fill)?;
|
draw_image_fill(
|
||||||
|
self.shared,
|
||||||
|
self.canvas,
|
||||||
|
shape,
|
||||||
|
image_fill,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let mut paint = fill.to_paint(&shape.selrect, true);
|
let mut paint = fill.to_paint(&shape.selrect, true);
|
||||||
if let Some(filter) = shape.image_filter(1.) {
|
if let Some(filter) = blur_filter.clone() {
|
||||||
paint.set_image_filter(filter);
|
paint.set_image_filter(filter);
|
||||||
}
|
}
|
||||||
draw_shape_geometry(self.canvas, shape, &paint);
|
draw_shape_geometry(self.canvas, shape, &paint);
|
||||||
@ -75,7 +94,7 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
let mut paint = merge_fills(fills, shape.selrect);
|
let mut paint = merge_fills(fills, shape.selrect);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
|
|
||||||
if let Some(filter) = shape.image_filter(1.) {
|
if let Some(filter) = blur_filter {
|
||||||
paint.set_image_filter(filter);
|
paint.set_image_filter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +143,7 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
}
|
}
|
||||||
let layer_bounds = shape.layer_bounds();
|
let layer_bounds = shape.layer_bounds();
|
||||||
for shadow in shape.inner_shadows_visible() {
|
for shadow in shape.inner_shadows_visible() {
|
||||||
let paint = shadow.get_inner_shadow_paint(true, shape.image_filter(1.).as_ref());
|
let paint = shadow.get_inner_shadow_paint(true, self.layer_blur_filter(shape).as_ref());
|
||||||
self.canvas.save_layer(
|
self.canvas.save_layer(
|
||||||
&skia::canvas::SaveLayerRec::default()
|
&skia::canvas::SaveLayerRec::default()
|
||||||
.bounds(&layer_bounds)
|
.bounds(&layer_bounds)
|
||||||
@ -163,7 +182,7 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
|
|
||||||
let text_content = text_content.new_bounds(shape.selrect());
|
let text_content = text_content.new_bounds(shape.selrect());
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||||
let blur_filter = shape.image_filter(1.);
|
let blur_filter = self.layer_blur_filter(shape);
|
||||||
|
|
||||||
// Text drop shadows: one filter layer per shadow over fill + stroke
|
// Text drop shadows: one filter layer per shadow over fill + stroke
|
||||||
// silhouettes (mirrors GPU `render_text_shadows`).
|
// silhouettes (mirrors GPU `render_text_shadows`).
|
||||||
@ -353,6 +372,9 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_blur_layer(&mut self, shape: &Shape) -> bool {
|
fn apply_blur_layer(&mut self, shape: &Shape) -> bool {
|
||||||
|
if !self.compose_fills {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let blur = match shape.blur {
|
let blur = match shape.blur {
|
||||||
Some(b) if !b.hidden && b.blur_type == BlurType::LayerBlur && b.value > 0.0 => b,
|
Some(b) if !b.hidden && b.blur_type == BlurType::LayerBlur && b.value > 0.0 => b,
|
||||||
_ => return false,
|
_ => return false,
|
||||||
@ -1033,6 +1055,7 @@ fn draw_image_fill(
|
|||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
image_fill: &crate::shapes::ImageFill,
|
image_fill: &crate::shapes::ImageFill,
|
||||||
|
blur_filter: Option<&skia::ImageFilter>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Use a CPU-backed image copy — GPU-backed images can't be drawn
|
// Use a CPU-backed image copy — GPU-backed images can't be drawn
|
||||||
// on the PDF canvas which has no GPU context.
|
// on the PDF canvas which has no GPU context.
|
||||||
@ -1053,8 +1076,8 @@ fn draw_image_fill(
|
|||||||
|
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
if let Some(filter) = shape.image_filter(1.) {
|
if let Some(filter) = blur_filter {
|
||||||
paint.set_image_filter(filter);
|
paint.set_image_filter(filter.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.draw_image_rect_with_sampling_options(
|
canvas.draw_image_rect_with_sampling_options(
|
||||||
|
|||||||
@ -652,6 +652,13 @@ impl Shape {
|
|||||||
self.background_blur.filter(|blur| !blur.hidden)
|
self.background_blur.filter(|blur| !blur.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visible layer blur (`!hidden`, `LayerBlur`, `value > 0`).
|
||||||
|
pub fn visible_layer_blur(&self) -> Option<Blur> {
|
||||||
|
self.blur.filter(|blur| {
|
||||||
|
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn add_child(&mut self, id: Uuid) {
|
pub fn add_child(&mut self, id: Uuid) {
|
||||||
self.children.push(id);
|
self.children.push(id);
|
||||||
@ -2059,6 +2066,51 @@ mod tests {
|
|||||||
assert_eq!(shape.visible_background_blur(), None);
|
assert_eq!(shape.visible_background_blur(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cases mirrored from Penpot MCP board `layer-blur-cases` (file "blur"):
|
||||||
|
/// leaf-visible-blur, leaf-hidden-blur, leaf-zero-blur, leaf-background-blur,
|
||||||
|
/// group-with-blur.
|
||||||
|
#[test]
|
||||||
|
fn visible_layer_blur_requires_non_hidden_positive_layer_blur() {
|
||||||
|
let mut shape = any_shape();
|
||||||
|
|
||||||
|
// leaf-visible-blur / group-with-blur
|
||||||
|
let visible = Blur::new(BlurType::LayerBlur, false, 10.0);
|
||||||
|
shape.set_blur(Some(visible));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), Some(visible));
|
||||||
|
|
||||||
|
let group_blur = Blur::new(BlurType::LayerBlur, false, 6.0);
|
||||||
|
shape.set_blur(Some(group_blur));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), Some(group_blur));
|
||||||
|
|
||||||
|
// leaf-hidden-blur
|
||||||
|
shape.set_blur(Some(Blur::new(BlurType::LayerBlur, true, 10.0)));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), None);
|
||||||
|
|
||||||
|
// leaf-zero-blur
|
||||||
|
shape.set_blur(Some(Blur::new(BlurType::LayerBlur, false, 0.0)));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), None);
|
||||||
|
|
||||||
|
// no blur
|
||||||
|
shape.set_blur(None);
|
||||||
|
assert_eq!(shape.visible_layer_blur(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visible_layer_blur_ignores_background_blur() {
|
||||||
|
let mut shape = any_shape();
|
||||||
|
// leaf-background-blur: Plugin API uses `backgroundBlur`, not `blur`.
|
||||||
|
shape.set_background_blur(Some(Blur::new(BlurType::BackgroundBlur, false, 8.0)));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), None);
|
||||||
|
assert_eq!(
|
||||||
|
shape.visible_background_blur(),
|
||||||
|
Some(Blur::new(BlurType::BackgroundBlur, false, 8.0))
|
||||||
|
);
|
||||||
|
|
||||||
|
let layer = Blur::new(BlurType::LayerBlur, false, 4.0);
|
||||||
|
shape.set_blur(Some(layer));
|
||||||
|
assert_eq!(shape.visible_layer_blur(), Some(layer));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_corners() {
|
fn test_set_corners() {
|
||||||
let mut shape = any_shape();
|
let mut shape = any_shape();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user