diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ee4ebe3bd0..b68c3fa82b 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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( diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 6965f16ed0..cc22e84fe1 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -178,6 +178,7 @@ pub fn render_text_shadows( blur_filter.as_ref(), 0.0, None, + None, )?; } else { text::render( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 4f2d521179..1d0a39f336 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -584,6 +584,7 @@ fn render_inner_stroke_on_canvas( fill_builders: &mut [Vec], blur: Option<&ImageFilter>, layer_opacity: Option, + fill_inset: Option, ) { 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, + fill_inset: Option, ) -> 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(()) diff --git a/render-wasm/src/render/vector.rs b/render-wasm/src/render/vector.rs index 721a02fbc5..2a5fa43d78 100644 --- a/render-wasm/src/render/vector.rs +++ b/render-wasm/src/render/vector.rs @@ -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(