mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🐛 Fix shadows and blurs for high levels of zoom
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 360 KiB |
@@ -243,6 +243,10 @@ pub(crate) struct RenderState {
|
||||
pub show_grid: Option<Uuid>,
|
||||
pub focus_mode: FocusMode,
|
||||
pub touched_ids: HashSet<Uuid>,
|
||||
/// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.)
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -314,9 +318,70 @@ impl RenderState {
|
||||
show_grid: None,
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines every visible layer blur currently active (ancestors + shape)
|
||||
/// into a single equivalent blur. Layer blur radii compound by adding their
|
||||
/// variances (σ² = radius²), so we:
|
||||
/// 1. Convert each blur radius into variance via `blur_variance`.
|
||||
/// 2. Sum all variances.
|
||||
/// 3. Convert the total variance back to a radius with `blur_from_variance`.
|
||||
///
|
||||
/// This keeps blur math consistent everywhere we need to merge blur sources.
|
||||
fn combined_layer_blur(&self, shape_blur: Option<Blur>) -> Option<Blur> {
|
||||
let mut total = 0.;
|
||||
|
||||
for nested_blur in self.nested_blurs.iter().flatten() {
|
||||
total += Self::blur_variance(Some(*nested_blur));
|
||||
}
|
||||
|
||||
total += Self::blur_variance(shape_blur);
|
||||
|
||||
Self::blur_from_variance(total)
|
||||
}
|
||||
|
||||
/// Returns the variance (radius²) for a visible layer blur, or zero if the
|
||||
/// blur is hidden/absent. Working in variance space lets us add multiple
|
||||
/// blur radii correctly.
|
||||
fn blur_variance(blur: Option<Blur>) -> f32 {
|
||||
match blur {
|
||||
Some(blur) if !blur.hidden && blur.blur_type == BlurType::LayerBlur => {
|
||||
blur.value.powi(2)
|
||||
}
|
||||
_ => 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a blur from an accumulated variance value. If no variance was
|
||||
/// contributed, we return `None`; otherwise the equivalent single radius is
|
||||
/// `sqrt(total)`.
|
||||
fn blur_from_variance(total: f32) -> Option<Blur> {
|
||||
(total > 0.).then(|| Blur::new(BlurType::LayerBlur, false, total.sqrt()))
|
||||
}
|
||||
|
||||
/// Convenience helper to merge two optional layer blurs using the same
|
||||
/// variance math as `combined_layer_blur`.
|
||||
fn combine_blur_values(base: Option<Blur>, extra: Option<Blur>) -> Option<Blur> {
|
||||
let total = Self::blur_variance(base) + Self::blur_variance(extra);
|
||||
Self::blur_from_variance(total)
|
||||
}
|
||||
|
||||
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
|
||||
/// Certain off-screen passes (e.g. shadow masks) must render shapes without
|
||||
/// inheriting ancestor blur. This helper guarantees the flag is restored.
|
||||
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut RenderState) -> R,
|
||||
{
|
||||
let previous = self.ignore_nested_blurs;
|
||||
self.ignore_nested_blurs = true;
|
||||
let result = f(self);
|
||||
self.ignore_nested_blurs = previous;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn fonts(&self) -> &FontStore {
|
||||
&self.fonts
|
||||
}
|
||||
@@ -548,24 +613,12 @@ impl RenderState {
|
||||
// We don't want to change the value in the global state
|
||||
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
|
||||
|
||||
let mut nested_blur_value = 0.;
|
||||
for nested_blur in self.nested_blurs.iter().flatten() {
|
||||
if !nested_blur.hidden && nested_blur.blur_type == BlurType::LayerBlur {
|
||||
nested_blur_value += nested_blur.value.powf(2.);
|
||||
if !self.ignore_nested_blurs {
|
||||
if let Some(blur) = self.combined_layer_blur(shape.blur) {
|
||||
shape.to_mut().set_blur(Some(blur));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(blur) = shape.blur {
|
||||
if !blur.hidden {
|
||||
nested_blur_value += blur.value.powf(2.);
|
||||
}
|
||||
}
|
||||
|
||||
if nested_blur_value > 0. {
|
||||
let blur = Blur::new(BlurType::LayerBlur, false, nested_blur_value.sqrt());
|
||||
shape.to_mut().set_blur(Some(blur));
|
||||
}
|
||||
|
||||
let center = shape.center();
|
||||
let mut matrix = shape.transform;
|
||||
matrix.post_translate(center);
|
||||
@@ -1144,21 +1197,34 @@ impl RenderState {
|
||||
fn render_drop_black_shadow(
|
||||
&mut self,
|
||||
shape: &Shape,
|
||||
shape_bounds: &Rect,
|
||||
shadow: &Shadow,
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
extra_layer_blur: Option<Blur>,
|
||||
) {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
|
||||
// Scale blur to maintain consistent appearance across zoom levels
|
||||
// When canvas is scaled down (zoom out), blur should be scaled down too
|
||||
transformed_shadow.to_mut().blur = shadow.blur * scale;
|
||||
transformed_shadow.to_mut().spread = shadow.spread * scale;
|
||||
|
||||
let mut plain_shape = Cow::Borrowed(shape);
|
||||
let combined_blur =
|
||||
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
||||
let blur_filter = combined_blur
|
||||
.and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None));
|
||||
|
||||
let mut transform_matrix = shape.transform;
|
||||
let center = shape.center();
|
||||
// Re-center the matrix so rotations/scales happen around the shape center,
|
||||
// matching how the shape itself is rendered.
|
||||
transform_matrix.post_translate(center);
|
||||
transform_matrix.pre_translate(-center);
|
||||
|
||||
// Transform the local shadow offset into world coordinates so that rotations/scales
|
||||
// applied to the shape are respected when positioning the shadow.
|
||||
let mapped = transform_matrix.map_vector((shadow.offset.0, shadow.offset.1));
|
||||
let world_offset = (mapped.x, mapped.y);
|
||||
|
||||
// The opacity of fills and strokes shouldn't affect the shadow,
|
||||
// so we paint everything black with the same opacity
|
||||
@@ -1181,34 +1247,91 @@ impl RenderState {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
plain_shape.to_mut().clear_shadows();
|
||||
plain_shape.to_mut().blur = None;
|
||||
|
||||
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);
|
||||
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some((shadow.offset.0, shadow.offset.1)),
|
||||
None,
|
||||
);
|
||||
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
|
||||
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
|
||||
bounds.offset(world_offset);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
let filter_result =
|
||||
filters::render_into_filter_surface(self, bounds, |state, temp_surface| {
|
||||
{
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
shadow_paint.set_image_filter(drop_filter.clone());
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
);
|
||||
});
|
||||
|
||||
{
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
canvas.restore();
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((image, filter_scale)) = filter_result {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((scale, scale));
|
||||
drop_canvas.translate(translation);
|
||||
let mut drop_paint = skia::Paint::default();
|
||||
drop_paint.set_image_filter(blur_filter.clone());
|
||||
|
||||
// If we scaled down in the filter surface, we need to scale back up
|
||||
if filter_scale < 1.0 {
|
||||
let scaled_width = bounds.width() * filter_scale;
|
||||
let scaled_height = bounds.height() * filter_scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * filter_scale,
|
||||
bounds.top * filter_scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
);
|
||||
drop_canvas.restore();
|
||||
} else {
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
);
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_shape_tree_partial_uncached(
|
||||
@@ -1291,6 +1414,11 @@ impl RenderState {
|
||||
//
|
||||
// This approach is essential for complex shapes with transparency where
|
||||
// multiple shadow areas might overlap, ensuring visual consistency.
|
||||
let inherited_layer_blur = match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => element.blur,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
@@ -1302,10 +1430,12 @@ impl RenderState {
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
element,
|
||||
&element.extrect(tree, scale),
|
||||
shadow,
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
None,
|
||||
);
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
@@ -1322,10 +1452,12 @@ impl RenderState {
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
self.render_drop_black_shadow(
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
shadow,
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
inherited_layer_blur,
|
||||
);
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
@@ -1356,17 +1488,19 @@ impl RenderState {
|
||||
);
|
||||
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
self.render_shape(
|
||||
shadow_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
);
|
||||
self.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
shadow_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
);
|
||||
});
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use skia_safe::{self as skia, Paint, RRect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::render::get_source_rect;
|
||||
use crate::shapes::{Fill, Frame, ImageFill, Rect, Shape, Type};
|
||||
|
||||
@@ -100,34 +100,55 @@ pub fn render(
|
||||
) {
|
||||
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
paint.set_image_filter(image_filter);
|
||||
let bounds = image_filter.compute_fast_bounds(shape.selrect);
|
||||
if filters::render_with_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
surface_id,
|
||||
|state, temp_surface| {
|
||||
let mut filtered_paint = paint.clone();
|
||||
filtered_paint.set_image_filter(image_filter.clone());
|
||||
draw_fill_to_surface(state, shape, fill, antialias, temp_surface, &filtered_paint);
|
||||
},
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
paint.set_image_filter(image_filter);
|
||||
}
|
||||
}
|
||||
|
||||
draw_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint);
|
||||
}
|
||||
|
||||
fn draw_fill_to_surface(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
fill: &Fill,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
paint: &Paint,
|
||||
) {
|
||||
match (fill, &shape.shape_type) {
|
||||
(Fill::Image(image_fill), _) => {
|
||||
draw_image_fill(
|
||||
render_state,
|
||||
shape,
|
||||
image_fill,
|
||||
&paint,
|
||||
paint,
|
||||
antialias,
|
||||
surface_id,
|
||||
);
|
||||
}
|
||||
(_, Type::Rect(_) | Type::Frame(_)) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_rect_to(surface_id, shape, &paint);
|
||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
||||
}
|
||||
(_, Type::Circle) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_circle_to(surface_id, shape, &paint);
|
||||
.draw_circle_to(surface_id, shape, paint);
|
||||
}
|
||||
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_path_to(surface_id, shape, &paint);
|
||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
||||
}
|
||||
(_, Type::Group(_)) => {
|
||||
// Groups can have fills but they propagate them to their children
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use skia_safe::ImageFilter;
|
||||
use skia_safe::{self as skia, ImageFilter, Rect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
|
||||
/// Composes two image filters, returning a combined filter if both are present,
|
||||
/// or the individual filter if only one is present, or None if neither is present.
|
||||
@@ -21,3 +23,111 @@ pub fn compose_filters(
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders filtered content offscreen and composites it back into the target surface.
|
||||
///
|
||||
/// This helper is meant for shapes that rely on blur/filters that should be evaluated
|
||||
/// in document space, regardless of the zoom level currently applied on the main canvas.
|
||||
/// It draws the filtered content into `SurfaceId::Filter`, optionally downscales the
|
||||
/// offscreen canvas when the requested bounds exceed the filter surface dimensions, and
|
||||
/// then draws the resulting image into `target_surface`, scaling it back up if needed.
|
||||
pub fn render_with_filter_surface<F>(
|
||||
render_state: &mut RenderState,
|
||||
bounds: Rect,
|
||||
target_surface: SurfaceId,
|
||||
draw_fn: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
|
||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||
if scale < 1.0 {
|
||||
// The image was rendered at a smaller scale, so we need to scale it back up
|
||||
let scaled_width = bounds.width() * scale;
|
||||
let scaled_height = bounds.height() * scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
canvas.save();
|
||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * scale,
|
||||
bounds.top * scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.restore();
|
||||
} else {
|
||||
// No scaling needed, draw normally
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates/clears `SurfaceId::Filter`, prepares it for drawing the filtered content,
|
||||
/// and executes the provided `draw_fn`.
|
||||
///
|
||||
/// If the requested bounds are larger than the filter surface, the canvas is scaled
|
||||
/// down so that everything fits; the returned `scale` tells the caller how much the
|
||||
/// content was reduced so it can be re-scaled on compositing. The `draw_fn` should
|
||||
/// render the untransformed shape (i.e. in document coordinates) onto `SurfaceId::Filter`.
|
||||
pub fn render_into_filter_surface<F>(
|
||||
render_state: &mut RenderState,
|
||||
bounds: Rect,
|
||||
draw_fn: F,
|
||||
) -> Option<(skia::Image, f32)>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let filter_id = SurfaceId::Filter;
|
||||
let (filter_width, filter_height) = render_state.surfaces.filter_size();
|
||||
let bounds_width = bounds.width().ceil().max(1.0) as i32;
|
||||
let bounds_height = bounds.height().ceil().max(1.0) as i32;
|
||||
|
||||
// Calculate scale factor if bounds exceed filter surface size
|
||||
let scale = if bounds_width > filter_width || bounds_height > filter_height {
|
||||
let scale_x = filter_width as f32 / bounds_width as f32;
|
||||
let scale_y = filter_height as f32 / bounds_height as f32;
|
||||
// Use the smaller scale to ensure everything fits
|
||||
scale_x.min(scale_y).max(0.1) // Clamp to minimum 0.1 to avoid extreme scaling
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
{
|
||||
let canvas = render_state.surfaces.canvas(filter_id);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.save();
|
||||
// Apply scale first, then translate
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate((-bounds.left, -bounds.top));
|
||||
}
|
||||
|
||||
draw_fn(render_state, filter_id);
|
||||
|
||||
render_state.surfaces.canvas(filter_id).restore();
|
||||
|
||||
Some((render_state.surfaces.snapshot(filter_id), scale))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::shapes::{
|
||||
};
|
||||
use skia_safe::{self as skia, ImageFilter, RRect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::render::filters::compose_filters;
|
||||
use crate::render::{get_dest_rect, get_source_rect};
|
||||
|
||||
@@ -378,6 +378,7 @@ fn draw_image_stroke_in_container(
|
||||
stroke: &Stroke,
|
||||
image_fill: &ImageFill,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let scale = render_state.get_scale();
|
||||
let image = render_state.images.get(&image_fill.id());
|
||||
@@ -386,7 +387,7 @@ fn draw_image_stroke_in_container(
|
||||
}
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Strokes);
|
||||
let canvas = render_state.surfaces.canvas(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
@@ -523,10 +524,89 @@ pub fn render(
|
||||
shadow: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
) {
|
||||
render_internal(
|
||||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
surface_id,
|
||||
shadow,
|
||||
antialias,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal function to render a stroke with support for offscreen blur rendering.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `render_state`: The rendering state containing surfaces and context.
|
||||
/// - `shape`: The shape to render the stroke for.
|
||||
/// - `stroke`: The stroke configuration (width, fill, style, etc.).
|
||||
/// - `surface_id`: Optional target surface ID. Defaults to `SurfaceId::Strokes` if `None`.
|
||||
/// - `shadow`: Optional shadow filter to apply to the stroke.
|
||||
/// - `antialias`: Whether to use antialiasing for rendering.
|
||||
/// - `bypass_filter`:
|
||||
/// - If `false`, attempts to use offscreen filter surface for blur effects.
|
||||
/// - If `true`, renders directly to the target surface (used for recursive calls to avoid infinite loops when rendering into the filter surface).
|
||||
///
|
||||
/// # Behavior
|
||||
/// When `bypass_filter` is `false` and the shape has a blur filter:
|
||||
/// 1. Calculates bounds including stroke width and cap margins.
|
||||
/// 2. Attempts to render into an offscreen filter surface at unscaled coordinates.
|
||||
/// 3. If successful, composites the result back to the target surface and returns early.
|
||||
/// 4. If the offscreen render fails or `bypass_filter` is `true`, renders directly to the target
|
||||
/// surface using the appropriate drawing function for the shape type.
|
||||
///
|
||||
/// The recursive call with `bypass_filter=true` ensures that when rendering into the filter
|
||||
/// surface, we don't attempt to create another filter surface, avoiding infinite recursion.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_internal(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
bypass_filter: bool,
|
||||
) {
|
||||
if !bypass_filter {
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
// We have to calculate the bounds considering the stroke and the cap margins.
|
||||
let mut content_bounds = shape.selrect;
|
||||
let stroke_margin = stroke.bounds_width(shape.is_open());
|
||||
if stroke_margin > 0.0 {
|
||||
content_bounds.inset((-stroke_margin, -stroke_margin));
|
||||
}
|
||||
let cap_margin = stroke.cap_bounds_margin();
|
||||
if cap_margin > 0.0 {
|
||||
content_bounds.inset((-cap_margin, -cap_margin));
|
||||
}
|
||||
let bounds = image_filter.compute_fast_bounds(content_bounds);
|
||||
|
||||
let target = surface_id.unwrap_or(SurfaceId::Strokes);
|
||||
if filters::render_with_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
target,
|
||||
|state, temp_surface| {
|
||||
render_internal(
|
||||
state,
|
||||
shape,
|
||||
stroke,
|
||||
Some(temp_surface),
|
||||
shadow,
|
||||
antialias,
|
||||
true,
|
||||
);
|
||||
},
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let scale = render_state.get_scale();
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(surface_id.unwrap_or(SurfaceId::Strokes)));
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
let selrect = shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
@@ -536,7 +616,14 @@ pub fn render(
|
||||
&& matches!(stroke.fill, Fill::Image(_))
|
||||
{
|
||||
if let Fill::Image(image_fill) = &stroke.fill {
|
||||
draw_image_stroke_in_container(render_state, shape, stroke, image_fill, antialias);
|
||||
draw_image_stroke_in_container(
|
||||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
image_fill,
|
||||
antialias,
|
||||
target_surface,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
match &shape.shape_type {
|
||||
|
||||
@@ -18,20 +18,22 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SurfaceId {
|
||||
Target = 0b00_0000_0001,
|
||||
Cache = 0b00_0000_0010,
|
||||
Current = 0b00_0000_0100,
|
||||
Fills = 0b00_0000_1000,
|
||||
Strokes = 0b00_0001_0000,
|
||||
DropShadows = 0b00_0010_0000,
|
||||
InnerShadows = 0b00_0100_0000,
|
||||
TextDropShadows = 0b00_1000_0000,
|
||||
UI = 0b01_0000_0000,
|
||||
Filter = 0b00_0000_0010,
|
||||
Cache = 0b00_0000_0100,
|
||||
Current = 0b00_0000_1000,
|
||||
Fills = 0b00_0001_0000,
|
||||
Strokes = 0b00_0010_0000,
|
||||
DropShadows = 0b00_0100_0000,
|
||||
InnerShadows = 0b00_1000_0000,
|
||||
TextDropShadows = 0b01_0000_0000,
|
||||
UI = 0b10_0000_0000,
|
||||
Debug = 0b10_0000_0001,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
// is the final destination surface, the one that it is represented in the canvas element.
|
||||
target: skia::Surface,
|
||||
filter: skia::Surface,
|
||||
cache: skia::Surface,
|
||||
// keeps the current render
|
||||
current: skia::Surface,
|
||||
@@ -70,6 +72,7 @@ impl Surfaces {
|
||||
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
|
||||
|
||||
let target = gpu_state.create_target_surface(width, height);
|
||||
let filter = gpu_state.create_surface_with_dimensions("filter".to_string(), width, height);
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
|
||||
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
|
||||
let drop_shadows =
|
||||
@@ -89,6 +92,7 @@ impl Surfaces {
|
||||
let tiles = TileTextureCache::new();
|
||||
Surfaces {
|
||||
target,
|
||||
filter,
|
||||
cache,
|
||||
current,
|
||||
drop_shadows,
|
||||
@@ -113,6 +117,10 @@ impl Surfaces {
|
||||
surface.image_snapshot()
|
||||
}
|
||||
|
||||
pub fn filter_size(&self) -> (i32, i32) {
|
||||
(self.filter.width(), self.filter.height())
|
||||
}
|
||||
|
||||
pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
|
||||
let surface = self.get_mut(id);
|
||||
let image = surface.image_snapshot();
|
||||
@@ -157,6 +165,9 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Target as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Target));
|
||||
}
|
||||
if ids & SurfaceId::Filter as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Filter));
|
||||
}
|
||||
if ids & SurfaceId::Current as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Current));
|
||||
}
|
||||
@@ -215,6 +226,7 @@ impl Surfaces {
|
||||
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
SurfaceId::Cache => &mut self.cache,
|
||||
SurfaceId::Current => &mut self.current,
|
||||
SurfaceId::DropShadows => &mut self.drop_shadows,
|
||||
@@ -230,6 +242,7 @@ impl Surfaces {
|
||||
fn reset_from_target(&mut self, target: skia::Surface) {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
self.filter = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
self.ui = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
// The rest are tile size surfaces
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{RenderState, Shape, SurfaceId};
|
||||
use super::{filters, RenderState, Shape, SurfaceId};
|
||||
use crate::{
|
||||
math::Rect,
|
||||
shapes::{
|
||||
@@ -168,35 +168,71 @@ pub fn render(
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
) {
|
||||
let render_canvas = if let Some(rs) = render_state {
|
||||
rs.surfaces.canvas(surface_id.unwrap_or(SurfaceId::Fills))
|
||||
} else if let Some(c) = canvas {
|
||||
c
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
if let Some(render_state) = render_state {
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let bounds = blur_filter.compute_fast_bounds(shape.selrect);
|
||||
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
|
||||
let blur_filter_clone = blur_filter.clone();
|
||||
if filters::render_with_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
target_surface,
|
||||
|state, temp_surface| {
|
||||
let temp_canvas = state.surfaces.canvas(temp_surface);
|
||||
render_text_on_canvas(
|
||||
temp_canvas,
|
||||
shape,
|
||||
paragraph_builders,
|
||||
shadow,
|
||||
Some(&blur_filter_clone),
|
||||
);
|
||||
},
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(canvas) = canvas {
|
||||
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_text_on_canvas(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
) {
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
let blur_layer = SaveLayerRec::default().paint(&blur_paint);
|
||||
render_canvas.save_layer(&blur_layer);
|
||||
canvas.save_layer(&blur_layer);
|
||||
}
|
||||
|
||||
if let Some(shadow_paint) = shadow {
|
||||
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
||||
render_canvas.save_layer(&layer_rec);
|
||||
draw_text(render_canvas, shape, paragraph_builders);
|
||||
render_canvas.restore();
|
||||
canvas.save_layer(&layer_rec);
|
||||
draw_text(canvas, shape, paragraph_builders);
|
||||
canvas.restore();
|
||||
} else {
|
||||
draw_text(render_canvas, shape, paragraph_builders);
|
||||
draw_text(canvas, shape, paragraph_builders);
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
render_canvas.restore();
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
render_canvas.restore();
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
|
||||
Reference in New Issue
Block a user