mirror of
https://github.com/penpot/penpot.git
synced 2026-05-27 02:43:42 +00:00
🎉 Improve text inner stroke rendering
This commit is contained in:
parent
cbe3a3f33e
commit
68760c8e26
@ -26,7 +26,7 @@ use crate::error::{Error, Result};
|
|||||||
use crate::performance;
|
use crate::performance;
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
|
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
|
||||||
Stroke, Type,
|
Stroke, StrokeKind, Type,
|
||||||
};
|
};
|
||||||
use crate::state::{ShapesPoolMutRef, ShapesPoolRef};
|
use crate::state::{ShapesPoolMutRef, ShapesPoolRef};
|
||||||
use crate::tiles::{self, PendingTiles, TileRect};
|
use crate::tiles::{self, PendingTiles, TileRect};
|
||||||
@ -1030,6 +1030,8 @@ impl RenderState {
|
|||||||
let text_stroke_blur_outset =
|
let text_stroke_blur_outset =
|
||||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||||
|
let stroke_kinds: Vec<StrokeKind> =
|
||||||
|
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
||||||
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
||||||
.visible_strokes()
|
.visible_strokes()
|
||||||
.rev()
|
.rev()
|
||||||
@ -1038,7 +1040,6 @@ impl RenderState {
|
|||||||
&text_content,
|
&text_content,
|
||||||
stroke,
|
stroke,
|
||||||
&shape.selrect(),
|
&shape.selrect(),
|
||||||
count_inner_strokes,
|
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1057,22 +1058,41 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(stroke_opacities.iter())
|
.zip(stroke_opacities.iter())
|
||||||
|
.enumerate()
|
||||||
{
|
{
|
||||||
text::render_with_bounds_outset(
|
if stroke_kinds[i] == StrokeKind::Inner {
|
||||||
Some(self),
|
let mut mask_builders = text_content.paragraph_builder_group_opaque();
|
||||||
None,
|
let mut fill_builders =
|
||||||
&shape,
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
stroke_paragraphs,
|
text::render_inner_stroke(
|
||||||
Some(strokes_surface_id),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
None,
|
&shape,
|
||||||
text_stroke_blur_outset,
|
&mut mask_builders,
|
||||||
None,
|
stroke_paragraphs,
|
||||||
*layer_opacity,
|
&mut fill_builders,
|
||||||
)?;
|
Some(strokes_surface_id),
|
||||||
|
None,
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
text::render_with_bounds_outset(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
None,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut drop_shadows = shape.drop_shadow_paints();
|
let mut drop_shadows = shape.drop_shadow_paints();
|
||||||
@ -1096,7 +1116,6 @@ impl RenderState {
|
|||||||
&text_content,
|
&text_content,
|
||||||
stroke,
|
stroke,
|
||||||
&shape.selrect(),
|
&shape.selrect(),
|
||||||
count_inner_strokes,
|
|
||||||
Some(true),
|
Some(true),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1126,6 +1145,8 @@ impl RenderState {
|
|||||||
text_drop_shadows_surface_id.into(),
|
text_drop_shadows_surface_id.into(),
|
||||||
&parent_shadows,
|
&parent_shadows,
|
||||||
&blur_filter,
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
&text_content,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1168,25 +1189,47 @@ impl RenderState {
|
|||||||
text_drop_shadows_surface_id.into(),
|
text_drop_shadows_surface_id.into(),
|
||||||
&drop_shadows,
|
&drop_shadows,
|
||||||
&blur_filter,
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
&text_content,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 4. Stroke fills
|
// 4. Stroke fills
|
||||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(stroke_opacities.iter())
|
.zip(stroke_opacities.iter())
|
||||||
|
.enumerate()
|
||||||
{
|
{
|
||||||
text::render_with_bounds_outset(
|
if stroke_kinds[i] == StrokeKind::Inner {
|
||||||
Some(self),
|
let mut mask_builders =
|
||||||
None,
|
text_content.paragraph_builder_group_opaque();
|
||||||
&shape,
|
let mut fill_builders =
|
||||||
stroke_paragraphs,
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
Some(strokes_surface_id),
|
text::render_inner_stroke(
|
||||||
None,
|
Some(self),
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
&shape,
|
||||||
None,
|
&mut mask_builders,
|
||||||
*layer_opacity,
|
stroke_paragraphs,
|
||||||
)?;
|
&mut fill_builders,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
text::render_with_bounds_outset(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
None,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Stroke inner shadows
|
// 5. Stroke inner shadows
|
||||||
@ -1198,6 +1241,8 @@ impl RenderState {
|
|||||||
Some(innershadows_surface_id),
|
Some(innershadows_surface_id),
|
||||||
&inner_shadows,
|
&inner_shadows,
|
||||||
&blur_filter,
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
&text_content,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 6. Fill Inner shadows
|
// 6. Fill Inner shadows
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use super::{RenderState, SurfaceId};
|
use super::{RenderState, SurfaceId};
|
||||||
use crate::render::strokes;
|
use crate::render::strokes;
|
||||||
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
|
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, StrokeKind, TextContent, Type};
|
||||||
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
|
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
@ -127,6 +127,7 @@ fn render_shadow_paint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_text_shadows(
|
pub fn render_text_shadows(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
@ -135,6 +136,8 @@ pub fn render_text_shadows(
|
|||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
shadows: &[Paint],
|
shadows: &[Paint],
|
||||||
blur_filter: &Option<skia_safe::ImageFilter>,
|
blur_filter: &Option<skia_safe::ImageFilter>,
|
||||||
|
stroke_kinds: &[StrokeKind],
|
||||||
|
text_content: &TextContent,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if stroke_paragraphs_group.is_empty() {
|
if stroke_paragraphs_group.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -160,18 +163,35 @@ pub fn render_text_shadows(
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
for (i, stroke_paragraphs) in stroke_paragraphs_group.iter_mut().enumerate() {
|
||||||
text::render(
|
if i < stroke_kinds.len() && stroke_kinds[i] == StrokeKind::Inner {
|
||||||
None,
|
let mut mask_builders = text_content.paragraph_builder_group_opaque();
|
||||||
Some(canvas),
|
let mut fill_builders = text_content.paragraph_builder_group_from_text(Some(true));
|
||||||
shape,
|
text::render_inner_stroke(
|
||||||
stroke_paragraphs,
|
None,
|
||||||
surface_id,
|
Some(canvas),
|
||||||
None,
|
shape,
|
||||||
blur_filter.as_ref(),
|
&mut mask_builders,
|
||||||
None,
|
stroke_paragraphs,
|
||||||
None,
|
&mut fill_builders,
|
||||||
)?;
|
surface_id,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
0.0,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
text::render(
|
||||||
|
None,
|
||||||
|
Some(canvas),
|
||||||
|
shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
surface_id,
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|||||||
@ -3,8 +3,8 @@ use crate::{
|
|||||||
error::Result,
|
error::Result,
|
||||||
math::Rect,
|
math::Rect,
|
||||||
shapes::{
|
shapes::{
|
||||||
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
|
calculate_position_data, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||||
ParagraphBuilderGroup, Stroke, StrokeKind, TextContent,
|
Stroke, StrokeKind, TextContent,
|
||||||
},
|
},
|
||||||
utils::{get_fallback_fonts, get_font_collection},
|
utils::{get_fallback_fonts, get_font_collection},
|
||||||
};
|
};
|
||||||
@ -19,7 +19,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
text_content: &TextContent,
|
text_content: &TextContent,
|
||||||
stroke: &Stroke,
|
stroke: &Stroke,
|
||||||
bounds: &Rect,
|
bounds: &Rect,
|
||||||
count_inner_strokes: usize,
|
|
||||||
use_shadow: Option<bool>,
|
use_shadow: Option<bool>,
|
||||||
) -> (Vec<ParagraphBuilderGroup>, Option<f32>) {
|
) -> (Vec<ParagraphBuilderGroup>, Option<f32>) {
|
||||||
let fallback_fonts = get_fallback_fonts();
|
let fallback_fonts = get_fallback_fonts();
|
||||||
@ -33,14 +32,8 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
for span in paragraph.children().iter() {
|
for span in paragraph.children().iter() {
|
||||||
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
|
let (stroke_paints, stroke_layer_opacity) =
|
||||||
let (stroke_paints, stroke_layer_opacity) = get_text_stroke_paints(
|
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||||
stroke,
|
|
||||||
bounds,
|
|
||||||
&text_paint,
|
|
||||||
count_inner_strokes,
|
|
||||||
remove_stroke_alpha,
|
|
||||||
);
|
|
||||||
|
|
||||||
if group_layer_opacity.is_none() {
|
if group_layer_opacity.is_none() {
|
||||||
group_layer_opacity = stroke_layer_opacity;
|
group_layer_opacity = stroke_layer_opacity;
|
||||||
@ -79,8 +72,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
fn get_text_stroke_paints(
|
fn get_text_stroke_paints(
|
||||||
stroke: &Stroke,
|
stroke: &Stroke,
|
||||||
bounds: &Rect,
|
bounds: &Rect,
|
||||||
text_paint: &Paint,
|
|
||||||
count_inner_strokes: usize,
|
|
||||||
remove_stroke_alpha: bool,
|
remove_stroke_alpha: bool,
|
||||||
) -> (Vec<Paint>, Option<f32>) {
|
) -> (Vec<Paint>, Option<f32>) {
|
||||||
let mut paints = Vec::new();
|
let mut paints = Vec::new();
|
||||||
@ -104,56 +95,19 @@ fn get_text_stroke_paints(
|
|||||||
|
|
||||||
match stroke.kind {
|
match stroke.kind {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
let shader = text_paint.shader();
|
// Just the stroke paint — mask+SrcIn+DstOver layering is handled
|
||||||
let mut is_opaque = true;
|
// by render_inner_stroke_on_canvas.
|
||||||
|
let mut paint = skia::Paint::default();
|
||||||
if let Some(shader) = shader {
|
paint.set_style(skia::PaintStyle::Stroke);
|
||||||
is_opaque = shader.is_opaque();
|
paint.set_anti_alias(true);
|
||||||
}
|
paint.set_stroke_width(stroke.width * 2.0);
|
||||||
|
if remove_stroke_alpha {
|
||||||
if is_opaque && count_inner_strokes == 1 {
|
|
||||||
let mut paint = text_paint.clone();
|
|
||||||
paint.set_style(skia::PaintStyle::Fill);
|
|
||||||
paint.set_anti_alias(true);
|
|
||||||
paints.push(paint);
|
|
||||||
|
|
||||||
let mut paint = skia::Paint::default();
|
|
||||||
paint.set_style(skia::PaintStyle::Stroke);
|
|
||||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
|
||||||
paint.set_anti_alias(true);
|
|
||||||
paint.set_stroke_width(stroke.width * 2.0);
|
|
||||||
fill_for_paint(&mut paint);
|
|
||||||
paints.push(paint);
|
|
||||||
} else {
|
|
||||||
let mut paint = skia::Paint::default();
|
|
||||||
if remove_stroke_alpha {
|
|
||||||
paint.set_color(skia::Color::BLACK);
|
|
||||||
paint.set_alpha(255);
|
|
||||||
} else {
|
|
||||||
paint = text_paint.clone();
|
|
||||||
if needs_opacity_layer {
|
|
||||||
let opaque_fill = stroke.fill.with_full_opacity();
|
|
||||||
set_paint_fill(&mut paint, &opaque_fill, bounds, false);
|
|
||||||
} else {
|
|
||||||
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paint.set_style(skia::PaintStyle::Fill);
|
|
||||||
paint.set_anti_alias(false);
|
|
||||||
paints.push(paint);
|
|
||||||
|
|
||||||
let mut paint = skia::Paint::default();
|
|
||||||
let image_filter =
|
|
||||||
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
|
|
||||||
|
|
||||||
paint.set_image_filter(image_filter);
|
|
||||||
paint.set_anti_alias(false);
|
|
||||||
paint.set_color(skia::Color::BLACK);
|
paint.set_color(skia::Color::BLACK);
|
||||||
paint.set_alpha(255);
|
paint.set_alpha(255);
|
||||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
} else {
|
||||||
paints.push(paint);
|
fill_for_paint(&mut paint);
|
||||||
}
|
}
|
||||||
|
paints.push(paint);
|
||||||
}
|
}
|
||||||
StrokeKind::Center => {
|
StrokeKind::Center => {
|
||||||
let mut paint = skia::Paint::default();
|
let mut paint = skia::Paint::default();
|
||||||
@ -330,25 +284,16 @@ fn render_text_on_canvas(
|
|||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text(
|
/// Lays out and paints paragraph builders without any layer management.
|
||||||
|
fn paint_text(
|
||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||||
layer_opacity: Option<f32>,
|
|
||||||
) {
|
) {
|
||||||
let text_content = shape.get_text_content();
|
let text_content = shape.get_text_content();
|
||||||
let layout_info =
|
let layout_info =
|
||||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||||
|
|
||||||
if let Some(opacity) = layer_opacity {
|
|
||||||
let mut opacity_paint = Paint::default();
|
|
||||||
opacity_paint.set_alpha_f(opacity);
|
|
||||||
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
|
||||||
canvas.save_layer(&layer_rec);
|
|
||||||
} else {
|
|
||||||
canvas.save_layer(&SaveLayerRec::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
for para in &layout_info.paragraphs {
|
for para in &layout_info.paragraphs {
|
||||||
para.paragraph.paint(canvas, (para.x, para.y));
|
para.paragraph.paint(canvas, (para.x, para.y));
|
||||||
for deco in ¶.decorations {
|
for deco in ¶.decorations {
|
||||||
@ -364,6 +309,178 @@ fn draw_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_text(
|
||||||
|
canvas: &Canvas,
|
||||||
|
shape: &Shape,
|
||||||
|
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
|
) {
|
||||||
|
if let Some(opacity) = layer_opacity {
|
||||||
|
let mut opacity_paint = Paint::default();
|
||||||
|
opacity_paint.set_alpha_f(opacity);
|
||||||
|
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||||
|
canvas.save_layer(&layer_rec);
|
||||||
|
} else {
|
||||||
|
canvas.save_layer(&SaveLayerRec::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
paint_text(canvas, shape, paragraph_builder_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders an inner stroke using mask + SrcIn + DstOver layer structure.
|
||||||
|
///
|
||||||
|
/// Layer structure:
|
||||||
|
/// saveLayer() — outer layer
|
||||||
|
/// saveLayer() — mask group (isolation)
|
||||||
|
/// paint mask — opaque fill as clip mask
|
||||||
|
/// saveLayer(SrcIn) — clips stroke to mask shape
|
||||||
|
/// paint stroke
|
||||||
|
/// restore
|
||||||
|
/// restore
|
||||||
|
/// saveLayer(DstOver) — fill behind the stroke
|
||||||
|
/// paint fill
|
||||||
|
/// restore
|
||||||
|
/// restore
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_inner_stroke_on_canvas(
|
||||||
|
canvas: &Canvas,
|
||||||
|
shape: &Shape,
|
||||||
|
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
blur: Option<&ImageFilter>,
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
|
) {
|
||||||
|
if let Some(blur_filter) = blur {
|
||||||
|
let mut blur_paint = Paint::default();
|
||||||
|
blur_paint.set_image_filter(blur_filter.clone());
|
||||||
|
canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opacity layer wraps the entire composition
|
||||||
|
if let Some(opacity) = layer_opacity {
|
||||||
|
let mut opacity_paint = Paint::default();
|
||||||
|
opacity_paint.set_alpha_f(opacity);
|
||||||
|
canvas.save_layer(&SaveLayerRec::default().paint(&opacity_paint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer layer
|
||||||
|
canvas.save_layer(&SaveLayerRec::default());
|
||||||
|
|
||||||
|
// Mask group layer (isolates mask from parent surface content)
|
||||||
|
canvas.save_layer(&SaveLayerRec::default());
|
||||||
|
|
||||||
|
// Draw opaque mask (full alpha text shape)
|
||||||
|
paint_text(canvas, shape, mask_builders);
|
||||||
|
|
||||||
|
// SrcIn layer — only keeps stroke pixels where mask has alpha
|
||||||
|
let mut src_in_paint = Paint::default();
|
||||||
|
src_in_paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||||
|
canvas.save_layer(&SaveLayerRec::default().paint(&src_in_paint));
|
||||||
|
|
||||||
|
// Draw stroke
|
||||||
|
paint_text(canvas, shape, stroke_builders);
|
||||||
|
|
||||||
|
canvas.restore(); // SrcIn layer
|
||||||
|
canvas.restore(); // mask group layer
|
||||||
|
|
||||||
|
// Fill with DstOver (behind the stroke result)
|
||||||
|
let mut dst_over_paint = Paint::default();
|
||||||
|
dst_over_paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||||
|
canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint));
|
||||||
|
|
||||||
|
paint_text(canvas, shape, fill_builders);
|
||||||
|
|
||||||
|
canvas.restore(); // DstOver layer
|
||||||
|
canvas.restore(); // outer layer
|
||||||
|
|
||||||
|
if layer_opacity.is_some() {
|
||||||
|
canvas.restore(); // opacity layer
|
||||||
|
}
|
||||||
|
|
||||||
|
if blur.is_some() {
|
||||||
|
canvas.restore(); // blur layer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public API for rendering inner strokes with mask+SrcIn+DstOver approach.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn render_inner_stroke(
|
||||||
|
render_state: Option<&mut RenderState>,
|
||||||
|
canvas: Option<&Canvas>,
|
||||||
|
shape: &Shape,
|
||||||
|
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||||
|
surface_id: Option<SurfaceId>,
|
||||||
|
blur: Option<&ImageFilter>,
|
||||||
|
stroke_bounds_outset: f32,
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(render_state) = render_state {
|
||||||
|
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||||
|
|
||||||
|
if let Some(blur_filter) = blur {
|
||||||
|
let mut text_bounds = shape
|
||||||
|
.get_text_content()
|
||||||
|
.calculate_bounds(shape, false)
|
||||||
|
.to_rect();
|
||||||
|
if stroke_bounds_outset > 0.0 {
|
||||||
|
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
|
||||||
|
}
|
||||||
|
let bounds = blur_filter.compute_fast_bounds(text_bounds);
|
||||||
|
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_inner_stroke_on_canvas(
|
||||||
|
temp_canvas,
|
||||||
|
shape,
|
||||||
|
mask_builders,
|
||||||
|
stroke_builders,
|
||||||
|
fill_builders,
|
||||||
|
Some(&blur_filter_clone),
|
||||||
|
layer_opacity,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||||
|
render_inner_stroke_on_canvas(
|
||||||
|
canvas,
|
||||||
|
shape,
|
||||||
|
mask_builders,
|
||||||
|
stroke_builders,
|
||||||
|
fill_builders,
|
||||||
|
blur,
|
||||||
|
layer_opacity,
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(canvas) = canvas {
|
||||||
|
render_inner_stroke_on_canvas(
|
||||||
|
canvas,
|
||||||
|
shape,
|
||||||
|
mask_builders,
|
||||||
|
stroke_builders,
|
||||||
|
fill_builders,
|
||||||
|
blur,
|
||||||
|
layer_opacity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_text_decorations(
|
fn draw_text_decorations(
|
||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
|
|||||||
@ -605,6 +605,40 @@ impl TextContent {
|
|||||||
paragraph_group
|
paragraph_group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates paragraph builders with always-opaque paint (BLACK @ alpha 255).
|
||||||
|
/// Used as a clip mask for inner stroke rendering.
|
||||||
|
pub fn paragraph_builder_group_opaque(&self) -> Vec<ParagraphBuilderGroup> {
|
||||||
|
let fonts = get_font_collection();
|
||||||
|
let fallback_fonts = get_fallback_fonts();
|
||||||
|
let mut paragraph_group = Vec::new();
|
||||||
|
|
||||||
|
for paragraph in self.paragraphs() {
|
||||||
|
let paragraph_style = paragraph.paragraph_to_style();
|
||||||
|
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||||
|
let mut has_text = false;
|
||||||
|
for span in paragraph.children() {
|
||||||
|
let text_style = span.to_style(
|
||||||
|
&self.bounds(),
|
||||||
|
fallback_fonts,
|
||||||
|
true, // always opaque
|
||||||
|
paragraph.line_height(),
|
||||||
|
);
|
||||||
|
let text: String = span.apply_text_transform();
|
||||||
|
if !text.is_empty() {
|
||||||
|
has_text = true;
|
||||||
|
}
|
||||||
|
builder.push_style(&text_style);
|
||||||
|
builder.add_text(&text);
|
||||||
|
}
|
||||||
|
if !has_text {
|
||||||
|
builder.add_text(" ");
|
||||||
|
}
|
||||||
|
paragraph_group.push(vec![builder]);
|
||||||
|
}
|
||||||
|
|
||||||
|
paragraph_group
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an Auto Width text layout.
|
/// Performs an Auto Width text layout.
|
||||||
fn text_layout_auto_width(&self) -> TextContentLayoutResult {
|
fn text_layout_auto_width(&self) -> TextContentLayoutResult {
|
||||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||||
@ -1094,6 +1128,7 @@ impl TextSpan {
|
|||||||
self.text = text;
|
self.text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn fills(&self) -> &[shapes::Fill] {
|
pub fn fills(&self) -> &[shapes::Fill] {
|
||||||
&self.fills
|
&self.fills
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user