mirror of
https://github.com/penpot/penpot.git
synced 2026-08-05 20:38:38 +00:00
🐛 Fix text inner stroke fill artifacts
This commit is contained in:
parent
ca81776d04
commit
a726733b9e
@ -1432,6 +1432,10 @@ impl RenderState {
|
||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||
// Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge.
|
||||
let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale());
|
||||
let only_inner_strokes = count_inner_strokes > 0
|
||||
&& shape
|
||||
.visible_strokes()
|
||||
.all(|stroke| stroke.kind == StrokeKind::Inner);
|
||||
let text_stroke_blur_outset =
|
||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
@ -1451,17 +1455,19 @@ impl RenderState {
|
||||
.unzip();
|
||||
if skip_effects {
|
||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
None,
|
||||
text_fill_inset,
|
||||
None,
|
||||
)?;
|
||||
if !only_inner_strokes {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
None,
|
||||
text_fill_inset,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||
.iter_mut()
|
||||
@ -1483,6 +1489,7 @@ impl RenderState {
|
||||
None,
|
||||
text_stroke_blur_outset,
|
||||
*layer_opacity,
|
||||
text_fill_inset,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset(
|
||||
@ -1573,17 +1580,19 @@ impl RenderState {
|
||||
}
|
||||
|
||||
// 2. Text fills
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
text_fill_inset,
|
||||
None,
|
||||
)?;
|
||||
if !only_inner_strokes {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
text_fill_inset,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
// 3. Stroke drop shadows
|
||||
shadows::render_text_shadows(
|
||||
@ -1620,6 +1629,7 @@ impl RenderState {
|
||||
blur_filter.as_ref(),
|
||||
text_stroke_blur_outset,
|
||||
*layer_opacity,
|
||||
text_fill_inset,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset(
|
||||
|
||||
@ -178,6 +178,7 @@ pub fn render_text_shadows(
|
||||
blur_filter.as_ref(),
|
||||
0.0,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
} else {
|
||||
text::render(
|
||||
|
||||
@ -584,6 +584,7 @@ fn render_inner_stroke_on_canvas(
|
||||
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||
blur: Option<&ImageFilter>,
|
||||
layer_opacity: Option<f32>,
|
||||
fill_inset: Option<f32>,
|
||||
) {
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
@ -623,7 +624,19 @@ fn render_inner_stroke_on_canvas(
|
||||
dst_over_paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint));
|
||||
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
||||
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
||||
let mut layer_paint = Paint::default();
|
||||
layer_paint.set_image_filter(erode);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&layer_paint));
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
canvas.restore();
|
||||
} else {
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
}
|
||||
} else {
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
}
|
||||
|
||||
canvas.restore(); // DstOver layer
|
||||
canvas.restore(); // outer layer
|
||||
@ -650,6 +663,7 @@ pub fn render_inner_stroke(
|
||||
blur: Option<&ImageFilter>,
|
||||
stroke_bounds_outset: f32,
|
||||
layer_opacity: Option<f32>,
|
||||
fill_inset: Option<f32>,
|
||||
) -> Result<()> {
|
||||
if let Some(render_state) = render_state {
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||
@ -679,6 +693,7 @@ pub fn render_inner_stroke(
|
||||
fill_builders,
|
||||
Some(&blur_filter_clone),
|
||||
layer_opacity,
|
||||
fill_inset,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
@ -697,6 +712,7 @@ pub fn render_inner_stroke(
|
||||
fill_builders,
|
||||
blur,
|
||||
layer_opacity,
|
||||
fill_inset,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@ -710,6 +726,7 @@ pub fn render_inner_stroke(
|
||||
fill_builders,
|
||||
blur,
|
||||
layer_opacity,
|
||||
fill_inset,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -153,6 +153,10 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
let text_content = text_content.new_bounds(shape.selrect());
|
||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let only_inner_strokes = shape.count_visible_inner_strokes() > 0
|
||||
&& shape
|
||||
.visible_strokes()
|
||||
.all(|stroke| stroke.kind == StrokeKind::Inner);
|
||||
|
||||
// Text drop shadows: one filter layer per shadow over fill + stroke
|
||||
// silhouettes (mirrors GPU `render_text_shadows`).
|
||||
@ -208,6 +212,7 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
blur_filter.as_ref(),
|
||||
0.0,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset_overlay_emoji(
|
||||
@ -227,15 +232,17 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
text::render_overlay_emoji(
|
||||
self.canvas,
|
||||
shape,
|
||||
&mut paragraph_builders,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
if !only_inner_strokes {
|
||||
text::render_overlay_emoji(
|
||||
self.canvas,
|
||||
shape,
|
||||
&mut paragraph_builders,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Strokes for text
|
||||
let stroke_blur_outset = Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||
@ -250,6 +257,11 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
);
|
||||
if stroke.render_kind(false) == StrokeKind::Inner {
|
||||
// Inner text stroke: clip to the glyph fill, else it bleeds out.
|
||||
let fill_inset = (shape.count_visible_inner_strokes() > 0).then(|| {
|
||||
let ctm = self.canvas.local_to_device_as_3x3();
|
||||
let scale = (ctm.scale_x().powi(2) + ctm.skew_y().powi(2)).sqrt().max(1.0);
|
||||
1.0 / scale
|
||||
});
|
||||
let mut mask_builders = text_content.paragraph_builder_group_opaque();
|
||||
let mut fill_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
text::render_inner_stroke(
|
||||
@ -263,6 +275,7 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
blur_filter.as_ref(),
|
||||
stroke_blur_outset,
|
||||
layer_opacity,
|
||||
fill_inset,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset_overlay_emoji(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user