🐛 Fixing nested shadows

This commit is contained in:
Alejandro Alonso
2025-09-04 11:59:50 +02:00
parent 9c77296858
commit 7e52aadb98
10 changed files with 10505 additions and 211 deletions

View File

@@ -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);