mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🐛 Fixing nested shadows
This commit is contained in:
10098
frontend/playwright/data/render-wasm/get-file-shadows.json
Normal file
10098
frontend/playwright/data/render-wasm/get-file-shadows.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -179,3 +179,19 @@ test("Renders a file with blurs applied to any kind of shape", async ({
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with shadows applied to any kind of shape", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-shadows.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "9502081a-e1a4-80bc-8006-c2b968723199",
|
||||
pageId: "9502081a-e1a4-80bc-8006-c2b96872319a",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
@@ -22,7 +22,7 @@ use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::performance;
|
||||
use crate::shapes::{Blur, BlurType, Corners, Fill, Shape, StructureEntry, Type};
|
||||
use crate::shapes::{Blur, BlurType, Corners, Fill, Shadow, Shape, StructureEntry, Type};
|
||||
use crate::state::ShapesPool;
|
||||
use crate::textlayout::{
|
||||
paragraph_builder_group_from_text, stroke_paragraph_builder_group_from_text,
|
||||
@@ -61,16 +61,39 @@ impl NodeRenderState {
|
||||
self.id.is_nil()
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for child elements of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied to child elements
|
||||
/// when rendering. It takes into account the element's selection rectangle, transform,
|
||||
/// and any additional modifiers.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate clip bounds
|
||||
/// * `modifiers` - Optional transformation matrix to apply to the bounds
|
||||
/// * `offset` - Optional offset (x, y) to adjust the bounds position. When provided,
|
||||
/// the bounds are translated by the negative of this offset, effectively moving
|
||||
/// the clipping region to compensate for coordinate system transformations.
|
||||
/// This is useful for nested coordinate systems or when elements are grouped
|
||||
/// and need relative positioning adjustments.
|
||||
pub fn get_children_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
modifiers: Option<&Matrix>,
|
||||
offset: Option<(f32, f32)>,
|
||||
) -> Option<(Rect, Option<Corners>, Matrix)> {
|
||||
if self.id.is_nil() || !element.clip() {
|
||||
return self.clip_bounds;
|
||||
}
|
||||
|
||||
let bounds = element.selrect();
|
||||
let mut bounds = element.selrect();
|
||||
if let Some(offset) = offset {
|
||||
let x = bounds.x() - offset.0;
|
||||
let y = bounds.y() - offset.1;
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
bounds.set_xywh(x, y, width, height);
|
||||
}
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(bounds.center());
|
||||
transform.pre_translate(-bounds.center());
|
||||
@@ -87,6 +110,45 @@ impl NodeRenderState {
|
||||
|
||||
Some((bounds, corners, transform))
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for shadow rendering of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied when rendering a
|
||||
/// shadow for a shape element. It uses the shadow bounds but calculates the
|
||||
/// transformation center based on the original shape, not the shadow bounds.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate shadow clip bounds
|
||||
/// * `modifiers` - Optional transformation matrix to apply to the bounds
|
||||
/// * `shadow` - The shadow configuration containing blur, offset, and other properties
|
||||
pub fn get_shadow_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
modifiers: Option<&Matrix>,
|
||||
shadow: &Shadow,
|
||||
) -> Option<(Rect, Option<Corners>, Matrix)> {
|
||||
if self.id.is_nil() {
|
||||
return self.clip_bounds;
|
||||
}
|
||||
|
||||
let bounds = element.get_frame_shadow_bounds(shadow);
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(element.center());
|
||||
transform.pre_translate(-element.center());
|
||||
|
||||
if let Some(modifier) = modifiers {
|
||||
transform.post_concat(modifier);
|
||||
}
|
||||
|
||||
let corners = match &element.shape_type {
|
||||
Type::Rect(data) => data.corners,
|
||||
Type::Frame(data) => data.corners,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some((bounds, corners, transform))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the "focus mode" state used during rendering.
|
||||
@@ -372,9 +434,6 @@ impl RenderState {
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, Some(&paint));
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
|
||||
@@ -396,10 +455,8 @@ impl RenderState {
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
let surface_ids =
|
||||
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
@@ -414,6 +471,7 @@ impl RenderState {
|
||||
self.focus_mode.set_shapes(shapes);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_shape(
|
||||
&mut self,
|
||||
shapes: &ShapesPool,
|
||||
@@ -422,6 +480,11 @@ impl RenderState {
|
||||
shape: &Shape,
|
||||
scale_content: Option<&f32>,
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
fills_surface_id: SurfaceId,
|
||||
strokes_surface_id: SurfaceId,
|
||||
innershadows_surface_id: SurfaceId,
|
||||
apply_to_current_surface: bool,
|
||||
offset: Option<(f32, f32)>,
|
||||
) {
|
||||
let shape = if let Some(scale_content) = scale_content {
|
||||
&shape.scale_content(*scale_content)
|
||||
@@ -429,10 +492,8 @@ impl RenderState {
|
||||
shape
|
||||
};
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
let surface_ids =
|
||||
fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
@@ -466,7 +527,7 @@ impl RenderState {
|
||||
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
||||
paint.set_stroke_width(4.);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Fills)
|
||||
.canvas(fills_surface_id)
|
||||
.draw_rect(bounds, &paint);
|
||||
}
|
||||
|
||||
@@ -505,22 +566,27 @@ impl RenderState {
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
|
||||
// Apply the additional transformation matrix if exists
|
||||
if let Some(offset) = offset {
|
||||
matrix.pre_translate(offset);
|
||||
}
|
||||
|
||||
match &shape.shape_type {
|
||||
Type::SVGRaw(sr) => {
|
||||
if let Some(shape_modifiers) = modifiers.get(&shape.id) {
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Fills)
|
||||
.canvas(fills_surface_id)
|
||||
.concat(shape_modifiers);
|
||||
}
|
||||
self.surfaces.canvas(SurfaceId::Fills).concat(&matrix);
|
||||
self.surfaces.canvas(fills_surface_id).concat(&matrix);
|
||||
if let Some(svg) = shape.svg.as_ref() {
|
||||
svg.render(self.surfaces.canvas(SurfaceId::Fills))
|
||||
svg.render(self.surfaces.canvas(fills_surface_id))
|
||||
} else {
|
||||
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
|
||||
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
|
||||
match dom_result {
|
||||
Ok(dom) => {
|
||||
dom.render(self.surfaces.canvas(SurfaceId::Fills));
|
||||
dom.render(self.surfaces.canvas(fills_surface_id));
|
||||
shape.to_mut().set_svg(dom);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -531,16 +597,11 @@ impl RenderState {
|
||||
}
|
||||
|
||||
Type::Text(text_content) => {
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().concat(&matrix);
|
||||
});
|
||||
|
||||
let text_content = text_content.new_bounds(shape.selrect());
|
||||
let drop_shadows = shape.drop_shadow_paints();
|
||||
let inner_shadows = shape.inner_shadow_paints();
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let blur_mask = shape.mask_filter(1.);
|
||||
@@ -551,44 +612,9 @@ impl RenderState {
|
||||
None,
|
||||
);
|
||||
|
||||
// Render all drop shadows if there are no visible strokes
|
||||
if !shape.has_visible_strokes() && !drop_shadows.is_empty() {
|
||||
for drop_shadow in &drop_shadows {
|
||||
let mut paragraphs_with_drop_shadows = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(drop_shadow),
|
||||
);
|
||||
shadows::render_text_drop_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_drop_shadows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||
text::render(self, &shape, &mut paragraphs, None);
|
||||
text::render(self, &shape, &mut paragraphs, Some(fills_surface_id));
|
||||
for stroke in shape.visible_strokes().rev() {
|
||||
for drop_shadow in &drop_shadows {
|
||||
let mut stroke_paragraphs_with_drop_shadows =
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(drop_shadow),
|
||||
count_inner_strokes,
|
||||
);
|
||||
shadows::render_text_drop_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut stroke_paragraphs_with_drop_shadows,
|
||||
);
|
||||
}
|
||||
|
||||
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
@@ -603,7 +629,7 @@ impl RenderState {
|
||||
self,
|
||||
&shape,
|
||||
stroke,
|
||||
None,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
Some(&mut stroke_paragraphs),
|
||||
antialias,
|
||||
@@ -624,6 +650,7 @@ impl RenderState {
|
||||
self,
|
||||
&shape,
|
||||
&mut stroke_paragraphs_with_inner_shadows,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -639,14 +666,11 @@ impl RenderState {
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_inner_shadows,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().concat(&matrix);
|
||||
});
|
||||
@@ -676,34 +700,41 @@ impl RenderState {
|
||||
if let Some(fills_to_render) = self.nested_fills.last() {
|
||||
let fills_to_render = fills_to_render.clone();
|
||||
for fill in fills_to_render.iter() {
|
||||
fills::render(self, shape, fill, antialias);
|
||||
fills::render(self, shape, fill, antialias, fills_surface_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for fill in shape.fills().rev() {
|
||||
fills::render(self, shape, fill, antialias);
|
||||
fills::render(self, shape, fill, antialias, fills_surface_id);
|
||||
}
|
||||
}
|
||||
|
||||
for stroke in shape.visible_strokes().rev() {
|
||||
shadows::render_stroke_drop_shadows(self, shape, stroke, antialias);
|
||||
//In clipped content strokes are drawn over the contained elements in a subsequent step
|
||||
if !shape.clip() {
|
||||
strokes::render(self, shape, stroke, None, None, None, antialias);
|
||||
}
|
||||
shadows::render_stroke_inner_shadows(self, shape, stroke, antialias);
|
||||
strokes::render(
|
||||
self,
|
||||
shape,
|
||||
stroke,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
None,
|
||||
antialias,
|
||||
);
|
||||
shadows::render_stroke_inner_shadows(
|
||||
self,
|
||||
shape,
|
||||
stroke,
|
||||
antialias,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
}
|
||||
|
||||
shadows::render_fill_inner_shadows(self, shape, antialias);
|
||||
shadows::render_fill_drop_shadows(self, shape, antialias);
|
||||
shadows::render_fill_inner_shadows(self, shape, antialias, innershadows_surface_id);
|
||||
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
|
||||
}
|
||||
};
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
}
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
@@ -782,10 +813,8 @@ impl RenderState {
|
||||
performance::begin_measure!("start_render_loop");
|
||||
|
||||
self.reset_canvas();
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32;
|
||||
let surface_ids =
|
||||
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
@@ -868,13 +897,6 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(Some(element.blur));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
@@ -970,6 +992,11 @@ impl RenderState {
|
||||
&element_strokes,
|
||||
scale_content,
|
||||
None,
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
true,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1016,6 +1043,61 @@ impl RenderState {
|
||||
self.get_aligned_tile_bounds(self.current_tile.unwrap())
|
||||
}
|
||||
|
||||
/// Renders a drop shadow effect for the given shape.
|
||||
///
|
||||
/// Creates a black shadow by converting the original shadow color to black,
|
||||
/// scaling the blur radius, and rendering the shape with the shadow offset applied.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_drop_black_shadow(
|
||||
&mut self,
|
||||
shapes: &ShapesPool,
|
||||
modifiers: &HashMap<Uuid, Matrix>,
|
||||
structure: &HashMap<Uuid, Vec<StructureEntry>>,
|
||||
shape: &Shape,
|
||||
shadow: &Shadow,
|
||||
scale_content: Option<&f32>,
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
) {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0., 0.);
|
||||
transformed_shadow.to_mut().color = skia::Color::from_argb(255, 0, 0, 0);
|
||||
transformed_shadow.to_mut().blur = transformed_shadow.blur * scale;
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter());
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.scale((scale, scale));
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.translate(translation);
|
||||
|
||||
self.render_shape(
|
||||
shapes,
|
||||
modifiers,
|
||||
structure,
|
||||
shape,
|
||||
scale_content,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some((shadow.offset.0, shadow.offset.1)),
|
||||
);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
|
||||
pub fn render_shape_tree_partial_uncached(
|
||||
&mut self,
|
||||
tree: &ShapesPool,
|
||||
@@ -1093,6 +1175,88 @@ impl RenderState {
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale = self.get_scale();
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
// Shadow rendering technique: Two-pass approach for proper opacity handling
|
||||
//
|
||||
// The shadow rendering uses a two-pass technique to ensure that overlapping
|
||||
// shadow areas maintain correct opacity without unwanted darkening:
|
||||
//
|
||||
// 1. First pass: Render shadow shape in pure black (alpha channel preserved)
|
||||
// - This creates the shadow silhouette with proper alpha gradients
|
||||
// - The black color acts as a mask for the final shadow color
|
||||
//
|
||||
// 2. Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// - SrcIn preserves the alpha channel from the black shadow
|
||||
// - Only the color channels are replaced, maintaining transparency
|
||||
// - This prevents overlapping shadows from accumulating opacity
|
||||
//
|
||||
// This approach is essential for complex shapes with transparency where
|
||||
// multiple shadow areas might overlap, ensuring visual consistency.
|
||||
for shadow in element.drop_shadows().rev().filter(|s| !s.hidden()) {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
element,
|
||||
shadow,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
);
|
||||
|
||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||
for shadow_shape_id in element.children.iter() {
|
||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||
let clip_bounds = node_render_state.get_shadow_clip_bounds(
|
||||
element,
|
||||
modifiers.get(&element.id),
|
||||
shadow,
|
||||
);
|
||||
self.render_drop_black_shadow(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
shadow_shape,
|
||||
shadow,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
);
|
||||
}
|
||||
|
||||
// Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// This preserves the alpha channel from the black shadow while
|
||||
// replacing only the color channels, preventing opacity accumulation
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(shadow.color);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.draw_paint(&paint);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
self.render_shape(
|
||||
tree,
|
||||
modifiers,
|
||||
@@ -1100,11 +1264,27 @@ impl RenderState {
|
||||
element,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
true,
|
||||
None,
|
||||
);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
} else if visited_children {
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(Some(element.blur));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Set the node as visited_children before processing children
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: node_id,
|
||||
@@ -1115,8 +1295,11 @@ impl RenderState {
|
||||
});
|
||||
|
||||
if element.is_recursive() {
|
||||
let children_clip_bounds =
|
||||
node_render_state.get_children_clip_bounds(element, modifiers.get(&element.id));
|
||||
let children_clip_bounds = node_render_state.get_children_clip_bounds(
|
||||
element,
|
||||
modifiers.get(&element.id),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut children_ids =
|
||||
element.modified_children_ids(structure.get(&element.id), false);
|
||||
|
||||
@@ -10,6 +10,7 @@ fn draw_image_fill(
|
||||
image_fill: &ImageFill,
|
||||
paint: &Paint,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let image = render_state.images.get(&image_fill.id());
|
||||
if image.is_none() {
|
||||
@@ -17,7 +18,7 @@ fn draw_image_fill(
|
||||
}
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Fills);
|
||||
let canvas = render_state.surfaces.canvas(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
|
||||
@@ -90,7 +91,13 @@ fn draw_image_fill(
|
||||
/**
|
||||
* This SHOULD be the only public function in this module.
|
||||
*/
|
||||
pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill, antialias: bool) {
|
||||
pub fn render(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
fill: &Fill,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
paint.set_image_filter(image_filter);
|
||||
@@ -98,22 +105,29 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill, antial
|
||||
|
||||
match (fill, &shape.shape_type) {
|
||||
(Fill::Image(image_fill), _) => {
|
||||
draw_image_fill(render_state, shape, image_fill, &paint, antialias);
|
||||
draw_image_fill(
|
||||
render_state,
|
||||
shape,
|
||||
image_fill,
|
||||
&paint,
|
||||
antialias,
|
||||
surface_id,
|
||||
);
|
||||
}
|
||||
(_, Type::Rect(_) | Type::Frame(_)) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_rect_to(SurfaceId::Fills, shape, &paint);
|
||||
.draw_rect_to(surface_id, shape, &paint);
|
||||
}
|
||||
(_, Type::Circle) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_circle_to(SurfaceId::Fills, shape, &paint);
|
||||
.draw_circle_to(surface_id, shape, &paint);
|
||||
}
|
||||
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_path_to(SurfaceId::Fills, shape, &paint);
|
||||
.draw_path_to(surface_id, shape, &paint);
|
||||
}
|
||||
(_, Type::Group(_)) => {
|
||||
// Groups can have fills but they propagate them to their children
|
||||
|
||||
@@ -6,28 +6,15 @@ use skia_safe::textlayout::ParagraphBuilder;
|
||||
use skia_safe::{Paint, Path};
|
||||
|
||||
// Fill Shadows
|
||||
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
|
||||
if shape.has_fills() {
|
||||
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
|
||||
render_fill_drop_shadow(render_state, shape, shadow, antialias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_fill_drop_shadow(
|
||||
pub fn render_fill_inner_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
shadow: &Shadow,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let paint = &shadow.get_drop_shadow_paint(antialias, shape.image_filter(1.).as_ref());
|
||||
render_shadow_paint(render_state, shape, paint, SurfaceId::DropShadows);
|
||||
}
|
||||
|
||||
pub fn render_fill_inner_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
|
||||
if shape.has_fills() {
|
||||
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
|
||||
render_fill_inner_shadow(render_state, shape, shadow, antialias);
|
||||
render_fill_inner_shadow(render_state, shape, shadow, antialias, surface_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,31 +24,10 @@ fn render_fill_inner_shadow(
|
||||
shape: &Shape,
|
||||
shadow: &Shadow,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let paint = &shadow.get_inner_shadow_paint(antialias, shape.image_filter(1.).as_ref());
|
||||
render_shadow_paint(render_state, shape, paint, SurfaceId::InnerShadows);
|
||||
}
|
||||
|
||||
pub fn render_stroke_drop_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
antialias: bool,
|
||||
) {
|
||||
if !shape.has_fills() {
|
||||
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
|
||||
let filter = shadow.get_drop_shadow_filter();
|
||||
strokes::render(
|
||||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
None,
|
||||
filter.as_ref(),
|
||||
None,
|
||||
antialias,
|
||||
)
|
||||
}
|
||||
}
|
||||
render_shadow_paint(render_state, shape, paint, surface_id);
|
||||
}
|
||||
|
||||
pub fn render_stroke_inner_shadows(
|
||||
@@ -69,6 +35,7 @@ pub fn render_stroke_inner_shadows(
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
if !shape.has_fills() {
|
||||
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
|
||||
@@ -77,7 +44,7 @@ pub fn render_stroke_inner_shadows(
|
||||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
None,
|
||||
Some(surface_id),
|
||||
filter.as_ref(),
|
||||
None,
|
||||
antialias,
|
||||
@@ -86,19 +53,6 @@ pub fn render_stroke_inner_shadows(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_text_drop_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
paragraphs: &mut [Vec<ParagraphBuilder>],
|
||||
) {
|
||||
text::render(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs,
|
||||
Some(SurfaceId::DropShadows),
|
||||
);
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
#[allow(dead_code)]
|
||||
pub fn render_text_path_stroke_drop_shadows(
|
||||
@@ -126,13 +80,9 @@ pub fn render_text_inner_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
paragraphs: &mut [Vec<ParagraphBuilder>],
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
text::render(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs,
|
||||
Some(SurfaceId::InnerShadows),
|
||||
);
|
||||
text::render(render_state, shape, paragraphs, Some(surface_id));
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
|
||||
@@ -525,7 +525,7 @@ pub fn render(
|
||||
let scale = render_state.get_scale();
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||
.canvas(surface_id.unwrap_or(surface_id.unwrap_or(SurfaceId::Strokes)));
|
||||
let selrect = shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
let svg_attrs = &shape.svg_attrs;
|
||||
@@ -569,7 +569,7 @@ pub fn render(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs.expect("Text shapes should have paragraphs"),
|
||||
Some(SurfaceId::Strokes),
|
||||
surface_id,
|
||||
);
|
||||
}
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
|
||||
@@ -175,16 +175,21 @@ impl Surfaces {
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
let translation = (
|
||||
pub fn get_render_context_translation(
|
||||
&mut self,
|
||||
render_area: skia::Rect,
|
||||
scale: f32,
|
||||
) -> (f32, f32) {
|
||||
(
|
||||
-render_area.left() + self.margins.width as f32 / scale,
|
||||
-render_area.top() + self.margins.height as f32 / scale,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
let translation = self.get_render_context_translation(render_area, scale);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32,
|
||||
SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::InnerShadows as u32,
|
||||
|s| {
|
||||
s.canvas().restore();
|
||||
s.canvas().save();
|
||||
@@ -251,7 +256,6 @@ impl Surfaces {
|
||||
|
||||
pub fn reset(&mut self, color: skia::Color) {
|
||||
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
||||
self.canvas(SurfaceId::DropShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Current).restore_to_count(1);
|
||||
@@ -259,7 +263,6 @@ impl Surfaces {
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::Current as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::InnerShadows as u32,
|
||||
|s| {
|
||||
s.canvas().clear(color).reset_matrix();
|
||||
|
||||
@@ -719,6 +719,65 @@ impl Shape {
|
||||
.get_or_init(|| self.calculate_extrect(shapes_pool, modifiers))
|
||||
}
|
||||
|
||||
/// Calculates the bounding rectangle for a frame shape's shadow, taking into account
|
||||
/// stroke widths and shadow properties.
|
||||
///
|
||||
/// This method computes the expanded bounds that would be needed to fully render
|
||||
/// the shadow effect for a frame shape. It considers:
|
||||
/// - The base frame bounds (selection rectangle)
|
||||
/// - Maximum stroke width across all strokes, accounting for stroke rendering kind
|
||||
/// - Shadow offset (x, y displacement)
|
||||
/// - Shadow blur radius (expands bounds outward)
|
||||
/// - Whether the shadow is hidden
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `shadow` - The shadow configuration containing offset, blur, and visibility
|
||||
///
|
||||
/// # Returns
|
||||
/// A `math::Rect` representing the bounding rectangle that encompasses the shadow.
|
||||
/// Returns an empty rectangle if the shadow is hidden.
|
||||
pub fn get_frame_shadow_bounds(&self, shadow: &Shadow) -> math::Rect {
|
||||
assert!(
|
||||
self.is_frame(),
|
||||
"This method can only be called on frame shapes"
|
||||
);
|
||||
|
||||
let base_bounds = self.selrect();
|
||||
let mut rect = skia::Rect::new_empty();
|
||||
|
||||
let mut max_stroke: Option<f32> = None;
|
||||
for stroke in self.strokes.iter() {
|
||||
let width = match stroke.render_kind(false) {
|
||||
StrokeKind::Inner => -stroke.width / 2.,
|
||||
StrokeKind::Center => 0.,
|
||||
StrokeKind::Outer => stroke.width,
|
||||
};
|
||||
max_stroke = Some(max_stroke.unwrap_or(f32::MIN).max(width));
|
||||
}
|
||||
if !shadow.hidden() {
|
||||
let (x, y) = shadow.offset;
|
||||
let mut shadow_rect = base_bounds;
|
||||
shadow_rect.left += x;
|
||||
shadow_rect.right += x;
|
||||
shadow_rect.top += y;
|
||||
shadow_rect.bottom += y;
|
||||
|
||||
shadow_rect.left += shadow.blur;
|
||||
shadow_rect.top += shadow.blur;
|
||||
shadow_rect.right -= shadow.blur;
|
||||
shadow_rect.bottom -= shadow.blur;
|
||||
|
||||
if let Some(max_stroke) = max_stroke {
|
||||
shadow_rect.left -= max_stroke;
|
||||
shadow_rect.right += max_stroke;
|
||||
shadow_rect.top -= max_stroke;
|
||||
shadow_rect.bottom += max_stroke;
|
||||
}
|
||||
rect.join(shadow_rect);
|
||||
}
|
||||
rect
|
||||
}
|
||||
|
||||
pub fn calculate_extrect(
|
||||
&self,
|
||||
shapes_pool: &ShapesPool,
|
||||
@@ -762,22 +821,24 @@ impl Shape {
|
||||
}
|
||||
|
||||
for shadow in self.shadows.iter() {
|
||||
let (x, y) = shadow.offset;
|
||||
let mut shadow_rect = rect;
|
||||
shadow_rect.left += x;
|
||||
shadow_rect.right += x;
|
||||
shadow_rect.top += y;
|
||||
shadow_rect.bottom += y;
|
||||
if !shadow.hidden() {
|
||||
let (x, y) = shadow.offset;
|
||||
let mut shadow_rect = rect;
|
||||
shadow_rect.left += x;
|
||||
shadow_rect.right += x;
|
||||
shadow_rect.top += y;
|
||||
shadow_rect.bottom += y;
|
||||
|
||||
shadow_rect.left -= shadow.blur;
|
||||
shadow_rect.top -= shadow.blur;
|
||||
shadow_rect.right += shadow.blur;
|
||||
shadow_rect.bottom += shadow.blur;
|
||||
shadow_rect.left -= shadow.blur;
|
||||
shadow_rect.top -= shadow.blur;
|
||||
shadow_rect.right += shadow.blur;
|
||||
shadow_rect.bottom += shadow.blur;
|
||||
|
||||
rect.join(shadow_rect);
|
||||
rect.join(shadow_rect);
|
||||
}
|
||||
}
|
||||
|
||||
if self.blur.blur_type != blurs::BlurType::None {
|
||||
if self.blur.blur_type != blurs::BlurType::None && !self.blur.hidden {
|
||||
rect.left -= self.blur.value;
|
||||
rect.top -= self.blur.value;
|
||||
rect.right += self.blur.value;
|
||||
@@ -1101,10 +1162,6 @@ impl Shape {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
pub fn has_visible_strokes(&self) -> bool {
|
||||
self.visible_strokes().next().is_some()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn has_visible_inner_strokes(&self) -> bool {
|
||||
self.visible_strokes().any(|s| s.kind == StrokeKind::Inner)
|
||||
@@ -1153,20 +1210,6 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_shadow_paints(&self) -> Vec<skia_safe::Paint> {
|
||||
let drop_shadows: Vec<&crate::shapes::shadows::Shadow> =
|
||||
self.drop_shadows().filter(|s| !s.hidden()).collect();
|
||||
drop_shadows
|
||||
.into_iter()
|
||||
.map(|shadow| {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
let filter = shadow.get_drop_shadow_filter();
|
||||
paint.set_image_filter(filter);
|
||||
paint
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn inner_shadow_paints(&self) -> Vec<skia_safe::Paint> {
|
||||
let inner_shadows: Vec<&crate::shapes::shadows::Shadow> =
|
||||
self.inner_shadows().filter(|s| !s.hidden()).collect();
|
||||
|
||||
@@ -63,19 +63,6 @@ impl Shadow {
|
||||
self.hidden
|
||||
}
|
||||
|
||||
pub fn get_drop_shadow_paint(
|
||||
&self,
|
||||
antialias: bool,
|
||||
blur_filter: Option<&ImageFilter>,
|
||||
) -> Paint {
|
||||
let mut paint = Paint::default();
|
||||
let shadow_filter = self.get_drop_shadow_filter();
|
||||
let filter = compose_filters(blur_filter, shadow_filter.as_ref());
|
||||
paint.set_image_filter(filter);
|
||||
paint.set_anti_alias(antialias);
|
||||
paint
|
||||
}
|
||||
|
||||
pub fn get_drop_shadow_filter(&self) -> Option<ImageFilter> {
|
||||
let mut filter = image_filters::drop_shadow_only(
|
||||
(self.offset.0, self.offset.1),
|
||||
|
||||
Reference in New Issue
Block a user