mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🐛 Fix blur 0 artifacts
This commit is contained in:
parent
c7f644ab2a
commit
a7ab506c5c
1161
frontend/playwright/data/render-wasm/get-solid-shadows.json
Normal file
1161
frontend/playwright/data/render-wasm/get-solid-shadows.json
Normal file
File diff suppressed because it is too large
Load Diff
2826
frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json
Normal file
2826
frontend/playwright/data/render-wasm/get-solid-strokes-shadows.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -243,6 +243,46 @@ test("Renders a file with a closed path shape with multiple segments using strok
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders solid shadows after select all and zoom to selected", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-solid-shadows.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "93113137-fe66-80fb-8007-99ca9fd96841",
|
||||
pageId: "93113137-fe66-80fb-8007-99ca9fd96842",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await workspace.viewport.click();
|
||||
await page.keyboard.press("ControlOrMeta+A");
|
||||
const previousRenderCount = await workspace.getRenderCount();
|
||||
await page.keyboard.press("f");
|
||||
await workspace.waitForNextRender(previousRenderCount);
|
||||
|
||||
await workspace.hideUI();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders strokes with solid shadows", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-solid-strokes-shadows.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "93113137-fe66-80fb-8007-99cfd5cbf361",
|
||||
pageId: "93113137-fe66-80fb-8007-99cfd5cbf362",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await workspace.hideUI();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with paths and svg attrs", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 140 KiB |
@ -642,6 +642,7 @@ impl RenderState {
|
||||
apply_to_current_surface: bool,
|
||||
offset: Option<(f32, f32)>,
|
||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
let surface_ids = fills_surface_id as u32
|
||||
| strokes_surface_id as u32
|
||||
@ -700,7 +701,14 @@ impl RenderState {
|
||||
canvas.translate(translation);
|
||||
});
|
||||
|
||||
fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current);
|
||||
fills::render(
|
||||
self,
|
||||
shape,
|
||||
&shape.fills,
|
||||
antialias,
|
||||
SurfaceId::Current,
|
||||
None,
|
||||
);
|
||||
|
||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||
@ -710,6 +718,7 @@ impl RenderState {
|
||||
&visible_strokes,
|
||||
Some(SurfaceId::Current),
|
||||
antialias,
|
||||
spread,
|
||||
);
|
||||
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
@ -1055,10 +1064,24 @@ impl RenderState {
|
||||
{
|
||||
if let Some(fills_to_render) = self.nested_fills.last() {
|
||||
let fills_to_render = fills_to_render.clone();
|
||||
fills::render(self, shape, &fills_to_render, antialias, fills_surface_id);
|
||||
fills::render(
|
||||
self,
|
||||
shape,
|
||||
&fills_to_render,
|
||||
antialias,
|
||||
fills_surface_id,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fills::render(self, shape, &shape.fills, antialias, fills_surface_id);
|
||||
fills::render(
|
||||
self,
|
||||
shape,
|
||||
&shape.fills,
|
||||
antialias,
|
||||
fills_surface_id,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
|
||||
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
|
||||
@ -1073,6 +1096,7 @@ impl RenderState {
|
||||
&visible_strokes,
|
||||
Some(strokes_surface_id),
|
||||
antialias,
|
||||
spread,
|
||||
);
|
||||
if !fast_mode {
|
||||
for stroke in &visible_strokes {
|
||||
@ -1472,6 +1496,7 @@ impl RenderState {
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1626,14 +1651,10 @@ impl RenderState {
|
||||
return;
|
||||
}
|
||||
|
||||
if use_low_zoom_path {
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
shadow_paint.set_image_filter(drop_filter);
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
|
||||
if scale > 1.0 && shadow.blur <= 0.0 {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save_layer(&layer_rec);
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((scale, scale));
|
||||
drop_canvas.translate(translation);
|
||||
|
||||
@ -1648,6 +1669,53 @@ impl RenderState {
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
);
|
||||
});
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create filter with blur only (no offset, no spread - handled geometrically)
|
||||
let blur_only_filter = if transformed_shadow.blur > 0.0 {
|
||||
Some(skia::image_filters::blur(
|
||||
(transformed_shadow.blur, transformed_shadow.blur),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
if let Some(blur_filter) = blur_only_filter {
|
||||
shadow_paint.set_image_filter(blur_filter);
|
||||
}
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
|
||||
// Low zoom path: use blur filter but apply offset and spread geometrically
|
||||
if use_low_zoom_path {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save_layer(&layer_rec);
|
||||
drop_canvas.scale((scale, scale));
|
||||
drop_canvas.translate(translation);
|
||||
|
||||
self.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
);
|
||||
});
|
||||
|
||||
@ -1657,7 +1725,7 @@ impl RenderState {
|
||||
|
||||
// Adaptive downscale for large blur values (lossless GPU optimization).
|
||||
// Bounds above were computed from the original sigma so filter surface coverage is correct.
|
||||
// Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8×): beyond that the
|
||||
// Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8x): beyond that the
|
||||
// filter surface becomes too small and quality degrades noticeably.
|
||||
const MIN_BLUR_DOWNSCALE: f32 = 1.0 / BLUR_DOWNSCALE_THRESHOLD;
|
||||
let blur_downscale = if shadow.blur > BLUR_DOWNSCALE_THRESHOLD {
|
||||
@ -1666,23 +1734,18 @@ impl RenderState {
|
||||
1.0
|
||||
};
|
||||
|
||||
// High zoom with blur: use render_into_filter_surface to ensure blur has enough space
|
||||
// Apply spread geometrically to avoid dilate filter rounding issues
|
||||
let filter_result = filters::render_into_filter_surface(
|
||||
self,
|
||||
bounds,
|
||||
blur_downscale,
|
||||
|state, temp_surface| {
|
||||
{
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
shadow_paint.set_image_filter(drop_filter);
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
}
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
canvas.save_layer(&layer_rec);
|
||||
|
||||
state.with_nested_blurs_suppressed(|state| {
|
||||
// Apply offset and spread geometrically
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
@ -1691,15 +1754,13 @@ impl RenderState {
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
);
|
||||
});
|
||||
|
||||
{
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
canvas.restore();
|
||||
}
|
||||
state.surfaces.canvas(temp_surface).restore();
|
||||
},
|
||||
);
|
||||
|
||||
@ -1836,6 +1897,7 @@ impl RenderState {
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
);
|
||||
});
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
@ -2047,6 +2109,7 @@ impl RenderState {
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
self.surfaces
|
||||
|
||||
@ -97,6 +97,7 @@ pub fn render(
|
||||
fills: &[Fill],
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
if fills.is_empty() {
|
||||
return;
|
||||
@ -107,7 +108,7 @@ pub fn render(
|
||||
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
||||
if has_image_fills {
|
||||
for fill in fills.iter().rev() {
|
||||
render_single_fill(render_state, shape, fill, antialias, surface_id);
|
||||
render_single_fill(render_state, shape, fill, antialias, surface_id, spread);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -124,7 +125,7 @@ pub fn render(
|
||||
|state, temp_surface| {
|
||||
let mut filtered_paint = paint.clone();
|
||||
filtered_paint.set_image_filter(image_filter.clone());
|
||||
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint);
|
||||
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, spread);
|
||||
},
|
||||
) {
|
||||
return;
|
||||
@ -133,7 +134,7 @@ pub fn render(
|
||||
}
|
||||
}
|
||||
|
||||
draw_fill_to_surface(render_state, shape, surface_id, &paint);
|
||||
draw_fill_to_surface(render_state, shape, surface_id, &paint, spread);
|
||||
}
|
||||
|
||||
/// Draws a single paint (with a merged shader) to the appropriate surface
|
||||
@ -143,18 +144,23 @@ fn draw_fill_to_surface(
|
||||
shape: &Shape,
|
||||
surface_id: SurfaceId,
|
||||
paint: &Paint,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
match &shape.shape_type {
|
||||
Type::Rect(_) | Type::Frame(_) => {
|
||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_rect_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
Type::Circle => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_circle_to(surface_id, shape, paint);
|
||||
.draw_circle_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
Type::Path(_) | Type::Bool(_) => {
|
||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_path_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
Type::Group(_) => {}
|
||||
_ => unreachable!("This shape should not have fills"),
|
||||
@ -167,6 +173,7 @@ fn render_single_fill(
|
||||
fill: &Fill,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
@ -185,6 +192,7 @@ fn render_single_fill(
|
||||
antialias,
|
||||
temp_surface,
|
||||
&filtered_paint,
|
||||
spread,
|
||||
);
|
||||
},
|
||||
) {
|
||||
@ -194,7 +202,15 @@ fn render_single_fill(
|
||||
}
|
||||
}
|
||||
|
||||
draw_single_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint);
|
||||
draw_single_fill_to_surface(
|
||||
render_state,
|
||||
shape,
|
||||
fill,
|
||||
antialias,
|
||||
surface_id,
|
||||
&paint,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_single_fill_to_surface(
|
||||
@ -204,6 +220,7 @@ fn draw_single_fill_to_surface(
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
paint: &Paint,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
match (fill, &shape.shape_type) {
|
||||
(Fill::Image(image_fill), _) => {
|
||||
@ -217,15 +234,19 @@ fn draw_single_fill_to_surface(
|
||||
);
|
||||
}
|
||||
(_, Type::Rect(_) | Type::Frame(_)) => {
|
||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_rect_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
(_, Type::Circle) => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_circle_to(surface_id, shape, paint);
|
||||
.draw_circle_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_path_to(surface_id, shape, paint, spread);
|
||||
}
|
||||
(_, Type::Group(_)) => {
|
||||
// Groups can have fills but they propagate them to their children
|
||||
|
||||
@ -47,6 +47,7 @@ pub fn render_stroke_inner_shadows(
|
||||
Some(surface_id),
|
||||
filter.as_ref(),
|
||||
antialias,
|
||||
None, // Inner shadows don't use spread
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -106,15 +107,19 @@ fn render_shadow_paint(
|
||||
) {
|
||||
match &shape.shape_type {
|
||||
Type::Rect(_) | Type::Frame(_) => {
|
||||
render_state.surfaces.draw_rect_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_rect_to(surface_id, shape, paint, None);
|
||||
}
|
||||
Type::Circle => {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_circle_to(surface_id, shape, paint);
|
||||
.draw_circle_to(surface_id, shape, paint, None);
|
||||
}
|
||||
Type::Path(_) | Type::Bool(_) => {
|
||||
render_state.surfaces.draw_path_to(surface_id, shape, paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_path_to(surface_id, shape, paint, None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -526,6 +526,7 @@ pub fn render(
|
||||
strokes: &[&Stroke],
|
||||
surface_id: Option<SurfaceId>,
|
||||
antialias: bool,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
if strokes.is_empty() {
|
||||
return;
|
||||
@ -540,6 +541,10 @@ pub fn render(
|
||||
// edges semi-transparent and revealing strokes underneath.
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
let mut content_bounds = shape.selrect;
|
||||
// Expand for spread if provided
|
||||
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
content_bounds.outset((s, s));
|
||||
}
|
||||
let max_margin = strokes
|
||||
.iter()
|
||||
.map(|s| s.bounds_width(shape.is_open()))
|
||||
@ -583,6 +588,7 @@ pub fn render(
|
||||
antialias,
|
||||
true,
|
||||
true,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
|
||||
@ -595,12 +601,28 @@ pub fn render(
|
||||
|
||||
// No blur or filter surface unavailable — draw strokes individually.
|
||||
for stroke in strokes.iter().rev() {
|
||||
render_single(render_state, shape, stroke, surface_id, None, antialias);
|
||||
render_single(
|
||||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
surface_id,
|
||||
None,
|
||||
antialias,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
render_merged(render_state, shape, strokes, surface_id, antialias, false);
|
||||
render_merged(
|
||||
render_state,
|
||||
shape,
|
||||
strokes,
|
||||
surface_id,
|
||||
antialias,
|
||||
false,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
|
||||
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
|
||||
@ -620,6 +642,7 @@ fn render_merged(
|
||||
surface_id: Option<SurfaceId>,
|
||||
antialias: bool,
|
||||
bypass_filter: bool,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
let representative = *strokes
|
||||
.last()
|
||||
@ -635,6 +658,10 @@ fn render_merged(
|
||||
if !bypass_filter {
|
||||
if let Some(image_filter) = blur_filter.clone() {
|
||||
let mut content_bounds = shape.selrect;
|
||||
// Expand for spread if provided
|
||||
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
content_bounds.outset((s, s));
|
||||
}
|
||||
let stroke_margin = representative.bounds_width(shape.is_open());
|
||||
if stroke_margin > 0.0 {
|
||||
content_bounds.inset((-stroke_margin, -stroke_margin));
|
||||
@ -660,7 +687,15 @@ fn render_merged(
|
||||
canvas.save_layer(&layer_rec);
|
||||
});
|
||||
|
||||
render_merged(state, shape, strokes, Some(temp_surface), antialias, true);
|
||||
render_merged(
|
||||
state,
|
||||
shape,
|
||||
strokes,
|
||||
Some(temp_surface),
|
||||
antialias,
|
||||
true,
|
||||
spread,
|
||||
);
|
||||
|
||||
state.surfaces.apply_mut(temp_surface as u32, |surface| {
|
||||
surface.canvas().restore();
|
||||
@ -676,11 +711,19 @@ fn render_merged(
|
||||
// via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top).
|
||||
let fills: Vec<Fill> = strokes.iter().map(|s| s.fill.clone()).collect();
|
||||
|
||||
let merged = merge_fills(&fills, shape.selrect);
|
||||
// Expand selrect if spread is provided
|
||||
let selrect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
let mut r = shape.selrect;
|
||||
r.outset((s, s));
|
||||
r
|
||||
} else {
|
||||
shape.selrect
|
||||
};
|
||||
|
||||
let merged = merge_fills(&fills, selrect);
|
||||
let scale = render_state.get_scale();
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
let selrect = shape.selrect;
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
let path_transform = shape.to_path_transform();
|
||||
|
||||
@ -747,6 +790,7 @@ pub fn render_single(
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
render_single_internal(
|
||||
render_state,
|
||||
@ -757,6 +801,7 @@ pub fn render_single(
|
||||
antialias,
|
||||
false,
|
||||
false,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
|
||||
@ -770,10 +815,15 @@ fn render_single_internal(
|
||||
antialias: bool,
|
||||
bypass_filter: bool,
|
||||
skip_blur: bool,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
if !bypass_filter {
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
let mut content_bounds = shape.selrect;
|
||||
// Expand for spread if provided
|
||||
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
content_bounds.outset((s, s));
|
||||
}
|
||||
let stroke_margin = stroke.bounds_width(shape.is_open());
|
||||
if stroke_margin > 0.0 {
|
||||
content_bounds.inset((-stroke_margin, -stroke_margin));
|
||||
@ -799,6 +849,7 @@ fn render_single_internal(
|
||||
antialias,
|
||||
true,
|
||||
true,
|
||||
spread,
|
||||
);
|
||||
},
|
||||
) {
|
||||
@ -867,7 +918,21 @@ fn render_single_internal(
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
if let Some(path) = shape_type.path() {
|
||||
let is_open = path.is_open();
|
||||
let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
|
||||
let mut paint =
|
||||
stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias);
|
||||
// Apply spread by increasing stroke width
|
||||
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
let current_width = paint.stroke_width();
|
||||
// Path stroke kinds are built differently:
|
||||
// - Center uses the stroke width directly.
|
||||
// - Inner/Outer use a doubled width plus clipping/clearing logic.
|
||||
// Compensate spread so visual growth is comparable across kinds.
|
||||
let spread_growth = match stroke.render_kind(is_open) {
|
||||
StrokeKind::Center => s * 2.0,
|
||||
StrokeKind::Inner | StrokeKind::Outer => s * 4.0,
|
||||
};
|
||||
paint.set_stroke_width(current_width + spread_growth);
|
||||
}
|
||||
draw_stroke_on_path(
|
||||
canvas,
|
||||
stroke,
|
||||
|
||||
@ -74,7 +74,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 filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
|
||||
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 =
|
||||
@ -355,24 +355,62 @@ impl Surfaces {
|
||||
));
|
||||
}
|
||||
|
||||
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
pub fn draw_rect_to(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
shape: &Shape,
|
||||
paint: &Paint,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
let rect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
let mut r = shape.selrect;
|
||||
r.outset((s, s));
|
||||
r
|
||||
} else {
|
||||
shape.selrect
|
||||
};
|
||||
if let Some(corners) = shape.shape_type.corners() {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
|
||||
let rrect = RRect::new_rect_radii(rect, &corners);
|
||||
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
|
||||
} else {
|
||||
self.canvas_and_mark_dirty(id)
|
||||
.draw_rect(shape.selrect, paint);
|
||||
self.canvas_and_mark_dirty(id).draw_rect(rect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
self.canvas_and_mark_dirty(id)
|
||||
.draw_oval(shape.selrect, paint);
|
||||
pub fn draw_circle_to(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
shape: &Shape,
|
||||
paint: &Paint,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
let rect = if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
let mut r = shape.selrect;
|
||||
r.outset((s, s));
|
||||
r
|
||||
} else {
|
||||
shape.selrect
|
||||
};
|
||||
self.canvas_and_mark_dirty(id).draw_oval(rect, paint);
|
||||
}
|
||||
|
||||
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
pub fn draw_path_to(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
shape: &Shape,
|
||||
paint: &Paint,
|
||||
spread: Option<f32>,
|
||||
) {
|
||||
if let Some(path) = shape.get_skia_path() {
|
||||
self.canvas_and_mark_dirty(id).draw_path(&path, paint);
|
||||
let canvas = self.canvas_and_mark_dirty(id);
|
||||
if let Some(s) = spread.filter(|&s| s > 0.0) {
|
||||
// Draw path as a thick stroke to get outset (expanded) silhouette
|
||||
let mut stroke_paint = paint.clone();
|
||||
stroke_paint.set_stroke_width(s * 2.0);
|
||||
canvas.draw_path(&path, &stroke_paint);
|
||||
} else {
|
||||
canvas.draw_path(&path, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user