mirror of
https://github.com/penpot/penpot.git
synced 2025-07-14 18:27:18 +02:00
🔧 Add methods to render text as path (#6624)
* 🔧 Refactor text strokes drawing * 🔧 Add text to path methods for future usage * 📚 Add text as paths internal documentation
This commit is contained in:
parent
2d36a1f3e0
commit
4869373a43
8 changed files with 462 additions and 63 deletions
|
@ -28,6 +28,18 @@ Penpot offers font handling for both Google Fonts and custom fonts, using Skia
|
||||||
- **Fallback Mechanism:** Skia requires explicit font data for proper Unicode rendering, so we cannot rely on browser fallback as with SVG. We detect the language used and automatically add the appropriate Noto Sans font as a fallback. If the user’s selected fonts cannot render the text, Skia’s fallback mechanism will try the next available font in the list.
|
- **Fallback Mechanism:** Skia requires explicit font data for proper Unicode rendering, so we cannot rely on browser fallback as with SVG. We detect the language used and automatically add the appropriate Noto Sans font as a fallback. If the user’s selected fonts cannot render the text, Skia’s fallback mechanism will try the next available font in the list.
|
||||||
- **Emoji Support:** For emoji characters, we use Noto Color Emoji by default. Ideally in the future, users will be able to select custom emoji fonts instead of Noto Sans as default, as the code is ready for this scenario.
|
- **Emoji Support:** For emoji characters, we use Noto Color Emoji by default. Ideally in the future, users will be able to select custom emoji fonts instead of Noto Sans as default, as the code is ready for this scenario.
|
||||||
|
|
||||||
|
## Texts as Paths
|
||||||
|
|
||||||
|
In Skia, it's possible to render text as paths in different ways. However, to preserve paragraph properties, we need to convert text nodes to `TextBlob`, then convert them to `Path`, and finally render them. This is necessary to ensure each piece of text is rendered in its correct position within the paragraph layout. We achieve this by using **Line Metrics** and **Style Metrics**, which allow us to iterate through each text element and read its dimensions, position, and style properties.
|
||||||
|
|
||||||
|
This feature is not currently activated, but it is explained step by step in [render-wasm/src/shapes/text_paths.rs](/render-wasm/src/shapes/text_paths.rs).
|
||||||
|
|
||||||
|
1. Get the paragraph and set the layout width. This is important because the rest of the metrics depend on setting the layout correctly.
|
||||||
|
2. Iterate through each line in the paragraph. Paragraph text style is retrieved through `LineMetrics`, which is why we go line by line.
|
||||||
|
3. Get the styles present in the line for each text leaf. `StyleMetrics` contain the style for individual text leaves, which we convert to `TextBlob` and then to `Path`.
|
||||||
|
4. Finally, `text::render` should paint all the paths on the canvas.
|
||||||
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Noto: A typeface for the world](https://fonts.google.com/noto)
|
- [Noto: A typeface for the world](https://fonts.google.com/noto)
|
||||||
|
|
|
@ -19,9 +19,7 @@ use options::RenderOptions;
|
||||||
use surfaces::{SurfaceId, Surfaces};
|
use surfaces::{SurfaceId, Surfaces};
|
||||||
|
|
||||||
use crate::performance;
|
use crate::performance;
|
||||||
use crate::shapes::{
|
use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type};
|
||||||
modified_children_ids, Corners, Fill, Shape, StrokeKind, StructureEntry, Type,
|
|
||||||
};
|
|
||||||
use crate::tiles::{self, PendingTiles, TileRect};
|
use crate::tiles::{self, PendingTiles, TileRect};
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
use crate::view::Viewbox;
|
use crate::view::Viewbox;
|
||||||
|
@ -225,6 +223,14 @@ impl RenderState {
|
||||||
self.surfaces.reset(self.background_color);
|
self.surfaces.reset(self.background_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas {
|
||||||
|
self.surfaces.canvas(surface_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_canvas(&mut self, surface_id: SurfaceId) {
|
||||||
|
self.surfaces.canvas(surface_id).restore();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
|
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
|
||||||
let tile_rect = self.get_current_aligned_tile_bounds();
|
let tile_rect = self.get_current_aligned_tile_bounds();
|
||||||
self.surfaces.cache_current_tile_texture(
|
self.surfaces.cache_current_tile_texture(
|
||||||
|
@ -405,7 +411,13 @@ impl RenderState {
|
||||||
let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
|
let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
|
||||||
|
|
||||||
shadows::render_text_drop_shadows(self, &shape, ¶graphs, antialias);
|
shadows::render_text_drop_shadows(self, &shape, ¶graphs, antialias);
|
||||||
text::render(self, &shape, ¶graphs, None, None);
|
text::render(self, &shape, ¶graphs, None);
|
||||||
|
|
||||||
|
if shape.has_inner_strokes() {
|
||||||
|
// Inner strokes paints need the text fill to apply correctly their blend modes
|
||||||
|
// (e.g., SrcATop, DstOver)
|
||||||
|
text::render(self, &shape, ¶graphs, Some(SurfaceId::Strokes));
|
||||||
|
}
|
||||||
|
|
||||||
for stroke in shape.strokes().rev() {
|
for stroke in shape.strokes().rev() {
|
||||||
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
|
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
|
||||||
|
@ -414,21 +426,15 @@ impl RenderState {
|
||||||
self.fonts.font_collection(),
|
self.fonts.font_collection(),
|
||||||
);
|
);
|
||||||
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
|
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
|
||||||
if stroke.kind == StrokeKind::Inner {
|
strokes::render(
|
||||||
// Inner strokes must be rendered on the Fills surface because their blend modes
|
self,
|
||||||
// (e.g., SrcATop, DstOver) rely on the text fill already being present underneath.
|
&shape,
|
||||||
// Rendering them on a separate surface would break this blending and result in incorrect visuals as
|
stroke,
|
||||||
// black color background.
|
None,
|
||||||
text::render(self, &shape, &stroke_paragraphs, None, None);
|
None,
|
||||||
} else {
|
Some(&stroke_paragraphs),
|
||||||
text::render(
|
antialias,
|
||||||
self,
|
);
|
||||||
&shape,
|
|
||||||
&stroke_paragraphs,
|
|
||||||
Some(SurfaceId::Strokes),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias);
|
shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +464,7 @@ impl RenderState {
|
||||||
|
|
||||||
for stroke in shape.strokes().rev() {
|
for stroke in shape.strokes().rev() {
|
||||||
shadows::render_stroke_drop_shadows(self, &shape, stroke, antialias);
|
shadows::render_stroke_drop_shadows(self, &shape, stroke, antialias);
|
||||||
strokes::render(self, &shape, stroke, None, None, antialias);
|
strokes::render(self, &shape, stroke, None, None, None, antialias);
|
||||||
shadows::render_stroke_inner_shadows(self, &shape, stroke, antialias);
|
shadows::render_stroke_inner_shadows(self, &shape, stroke, antialias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use super::{RenderState, SurfaceId};
|
||||||
use crate::render::strokes;
|
use crate::render::strokes;
|
||||||
use crate::render::text::{self};
|
use crate::render::text::{self};
|
||||||
use crate::shapes::{Shadow, Shape, Stroke, Type};
|
use crate::shapes::{Shadow, Shape, Stroke, Type};
|
||||||
use skia_safe::{textlayout::Paragraph, Paint};
|
use skia_safe::{canvas::SaveLayerRec, textlayout::Paragraph, Paint, Path};
|
||||||
|
|
||||||
// Fill Shadows
|
// Fill Shadows
|
||||||
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
|
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
|
||||||
|
@ -54,8 +54,9 @@ pub fn render_stroke_drop_shadows(
|
||||||
render_state,
|
render_state,
|
||||||
shape,
|
shape,
|
||||||
stroke,
|
stroke,
|
||||||
Some(SurfaceId::Strokes), // FIXME
|
None,
|
||||||
filter.as_ref(),
|
filter.as_ref(),
|
||||||
|
None,
|
||||||
antialias,
|
antialias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,8 +76,9 @@ pub fn render_stroke_inner_shadows(
|
||||||
render_state,
|
render_state,
|
||||||
shape,
|
shape,
|
||||||
stroke,
|
stroke,
|
||||||
Some(SurfaceId::Strokes), // FIXME
|
None,
|
||||||
filter.as_ref(),
|
filter.as_ref(),
|
||||||
|
None,
|
||||||
antialias,
|
antialias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -94,6 +96,29 @@ pub fn render_text_drop_shadows(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render text paths (unused)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn render_text_path_stroke_drop_shadows(
|
||||||
|
render_state: &mut RenderState,
|
||||||
|
shape: &Shape,
|
||||||
|
paths: &Vec<(Path, Paint)>,
|
||||||
|
stroke: &Stroke,
|
||||||
|
antialias: bool,
|
||||||
|
) {
|
||||||
|
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
|
||||||
|
let stroke_shadow = shadow.get_drop_shadow_filter();
|
||||||
|
strokes::render_text_paths(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
stroke,
|
||||||
|
paths,
|
||||||
|
Some(SurfaceId::DropShadows),
|
||||||
|
stroke_shadow.as_ref(),
|
||||||
|
antialias,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_text_drop_shadow(
|
pub fn render_text_drop_shadow(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
|
@ -102,14 +127,19 @@ pub fn render_text_drop_shadow(
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let paint = shadow.get_drop_shadow_paint(antialias);
|
let paint = shadow.get_drop_shadow_paint(antialias);
|
||||||
|
let mask_paint = paint.clone();
|
||||||
|
let mask = SaveLayerRec::default().paint(&mask_paint);
|
||||||
|
let canvas = render_state.get_canvas_at(SurfaceId::DropShadows);
|
||||||
|
canvas.save_layer(&mask);
|
||||||
|
|
||||||
text::render(
|
text::render(
|
||||||
render_state,
|
render_state,
|
||||||
shape,
|
shape,
|
||||||
paragraphs,
|
paragraphs,
|
||||||
Some(SurfaceId::DropShadows),
|
Some(SurfaceId::DropShadows),
|
||||||
Some(paint),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
render_state.restore_canvas(SurfaceId::DropShadows);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_text_inner_shadows(
|
pub fn render_text_inner_shadows(
|
||||||
|
@ -131,14 +161,43 @@ pub fn render_text_inner_shadow(
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let paint = shadow.get_inner_shadow_paint(antialias);
|
let paint = shadow.get_inner_shadow_paint(antialias);
|
||||||
|
let mask_paint = paint.clone();
|
||||||
|
let mask = SaveLayerRec::default().paint(&mask_paint);
|
||||||
|
let canvas = render_state.get_canvas_at(SurfaceId::InnerShadows);
|
||||||
|
|
||||||
|
canvas.save_layer(&mask);
|
||||||
|
|
||||||
text::render(
|
text::render(
|
||||||
render_state,
|
render_state,
|
||||||
shape,
|
shape,
|
||||||
paragraphs,
|
paragraphs,
|
||||||
Some(SurfaceId::InnerShadows),
|
Some(SurfaceId::InnerShadows),
|
||||||
Some(paint),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
render_state.restore_canvas(SurfaceId::InnerShadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render text paths (unused)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn render_text_path_stroke_inner_shadows(
|
||||||
|
render_state: &mut RenderState,
|
||||||
|
shape: &Shape,
|
||||||
|
paths: &Vec<(Path, Paint)>,
|
||||||
|
stroke: &Stroke,
|
||||||
|
antialias: bool,
|
||||||
|
) {
|
||||||
|
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
|
||||||
|
let stroke_shadow = shadow.get_inner_shadow_filter();
|
||||||
|
strokes::render_text_paths(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
stroke,
|
||||||
|
paths,
|
||||||
|
Some(SurfaceId::InnerShadows),
|
||||||
|
stroke_shadow.as_ref(),
|
||||||
|
antialias,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_shadow_paint(
|
fn render_shadow_paint(
|
||||||
|
|
|
@ -3,9 +3,10 @@ use std::collections::HashMap;
|
||||||
use crate::math::{Matrix, Point, Rect};
|
use crate::math::{Matrix, Point, Rect};
|
||||||
|
|
||||||
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
|
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
|
||||||
use skia_safe::{self as skia, ImageFilter, RRect};
|
use skia_safe::{self as skia, textlayout::Paragraph, ImageFilter, RRect};
|
||||||
|
|
||||||
use super::{RenderState, SurfaceId};
|
use super::{RenderState, SurfaceId};
|
||||||
|
use crate::render::text::{self};
|
||||||
|
|
||||||
// FIXME: See if we can simplify these arguments
|
// FIXME: See if we can simplify these arguments
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -69,6 +70,41 @@ fn draw_stroke_on_circle(
|
||||||
canvas.draw_oval(stroke_rect, &paint);
|
canvas.draw_oval(stroke_rect, &paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_outer_stroke_path(
|
||||||
|
canvas: &skia::Canvas,
|
||||||
|
path: &skia::Path,
|
||||||
|
paint: &skia::Paint,
|
||||||
|
antialias: bool,
|
||||||
|
) {
|
||||||
|
let mut outer_paint = skia::Paint::default();
|
||||||
|
outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||||
|
outer_paint.set_anti_alias(antialias);
|
||||||
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
|
||||||
|
canvas.save_layer(&layer_rec);
|
||||||
|
canvas.draw_path(path, paint);
|
||||||
|
|
||||||
|
let mut clear_paint = skia::Paint::default();
|
||||||
|
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
||||||
|
clear_paint.set_anti_alias(antialias);
|
||||||
|
canvas.draw_path(path, &clear_paint);
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
|
||||||
|
fn draw_inner_stroke_path(
|
||||||
|
canvas: &skia::Canvas,
|
||||||
|
path: &skia::Path,
|
||||||
|
paint: &skia::Paint,
|
||||||
|
antialias: bool,
|
||||||
|
) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.clip_path(path, skia::ClipOp::Intersect, antialias);
|
||||||
|
canvas.draw_path(path, paint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
|
||||||
// FIXME: See if we can simplify these arguments
|
// FIXME: See if we can simplify these arguments
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn draw_stroke_on_path(
|
pub fn draw_stroke_on_path(
|
||||||
|
@ -93,34 +129,15 @@ pub fn draw_stroke_on_path(
|
||||||
paint.set_image_filter(filter.clone());
|
paint.set_image_filter(filter.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the different kind of strokes for a path requires different strategies:
|
|
||||||
match stroke.render_kind(is_open) {
|
match stroke.render_kind(is_open) {
|
||||||
// For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
|
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface
|
draw_inner_stroke_path(canvas, &skia_path, &paint, antialias);
|
||||||
canvas.clip_path(&skia_path, skia::ClipOp::Intersect, antialias);
|
|
||||||
canvas.draw_path(&skia_path, &paint);
|
|
||||||
canvas.restore();
|
|
||||||
}
|
}
|
||||||
// For center stroke we don't need to do anything extra
|
|
||||||
StrokeKind::Center => {
|
StrokeKind::Center => {
|
||||||
canvas.draw_path(&skia_path, &paint);
|
canvas.draw_path(&skia_path, &paint);
|
||||||
}
|
}
|
||||||
// For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
|
|
||||||
StrokeKind::Outer => {
|
StrokeKind::Outer => {
|
||||||
let mut outer_paint = skia::Paint::default();
|
draw_outer_stroke_path(canvas, &skia_path, &paint, antialias);
|
||||||
outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
|
||||||
outer_paint.set_anti_alias(antialias);
|
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
|
|
||||||
canvas.save_layer(&layer_rec);
|
|
||||||
canvas.draw_path(&skia_path, &paint);
|
|
||||||
|
|
||||||
let mut clear_paint = skia::Paint::default();
|
|
||||||
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
|
||||||
clear_paint.set_anti_alias(antialias);
|
|
||||||
canvas.draw_path(&skia_path, &clear_paint);
|
|
||||||
|
|
||||||
canvas.restore();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,15 +517,13 @@ fn draw_image_stroke_in_container(
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This SHOULD be the only public function in this module.
|
|
||||||
*/
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
stroke: &Stroke,
|
stroke: &Stroke,
|
||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
shadow: Option<&ImageFilter>,
|
shadow: Option<&ImageFilter>,
|
||||||
|
paragraphs: Option<&[Vec<Paragraph>]>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let scale = render_state.get_scale();
|
let scale = render_state.get_scale();
|
||||||
|
@ -541,6 +556,14 @@ pub fn render(
|
||||||
Type::Circle => draw_stroke_on_circle(
|
Type::Circle => draw_stroke_on_circle(
|
||||||
canvas, stroke, &selrect, &selrect, svg_attrs, scale, shadow, antialias,
|
canvas, stroke, &selrect, &selrect, svg_attrs, scale, shadow, antialias,
|
||||||
),
|
),
|
||||||
|
Type::Text(_) => {
|
||||||
|
text::render(
|
||||||
|
render_state,
|
||||||
|
shape,
|
||||||
|
paragraphs.expect("Text shapes should have paragraphs"),
|
||||||
|
Some(SurfaceId::Strokes),
|
||||||
|
);
|
||||||
|
}
|
||||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||||
if let Some(path) = shape_type.path() {
|
if let Some(path) = shape_type.path() {
|
||||||
draw_stroke_on_path(
|
draw_stroke_on_path(
|
||||||
|
@ -560,3 +583,46 @@ pub fn render(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render text paths (unused)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn render_text_paths(
|
||||||
|
render_state: &mut RenderState,
|
||||||
|
shape: &Shape,
|
||||||
|
stroke: &Stroke,
|
||||||
|
paths: &Vec<(skia::Path, skia::Paint)>,
|
||||||
|
surface_id: Option<SurfaceId>,
|
||||||
|
shadow: Option<&ImageFilter>,
|
||||||
|
antialias: bool,
|
||||||
|
) {
|
||||||
|
let scale = render_state.get_scale();
|
||||||
|
let canvas = render_state
|
||||||
|
.surfaces
|
||||||
|
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||||
|
let selrect = &shape.selrect;
|
||||||
|
let svg_attrs = &shape.svg_attrs;
|
||||||
|
let mut paint: skia_safe::Handle<_> =
|
||||||
|
stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
|
||||||
|
|
||||||
|
if let Some(filter) = shadow {
|
||||||
|
paint.set_image_filter(filter.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
match stroke.render_kind(false) {
|
||||||
|
StrokeKind::Inner => {
|
||||||
|
for (path, _) in paths {
|
||||||
|
draw_inner_stroke_path(canvas, path, &paint, antialias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StrokeKind::Center => {
|
||||||
|
for (path, _) in paths {
|
||||||
|
canvas.draw_path(path, &paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StrokeKind::Outer => {
|
||||||
|
for (path, _) in paths {
|
||||||
|
draw_outer_stroke_path(canvas, path, &paint, antialias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,26 +1,16 @@
|
||||||
use super::{RenderState, Shape, SurfaceId};
|
use super::{RenderState, Shape, SurfaceId};
|
||||||
use skia_safe::{self as skia, canvas::SaveLayerRec, textlayout::Paragraph};
|
use skia_safe::{textlayout::Paragraph, Paint, Path};
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
paragraphs: &[Vec<Paragraph>],
|
paragraphs: &[Vec<Paragraph>],
|
||||||
surface_id: Option<SurfaceId>,
|
surface_id: Option<SurfaceId>,
|
||||||
paint: Option<skia::Paint>,
|
|
||||||
) {
|
) {
|
||||||
let use_save_layer = paint.is_some();
|
|
||||||
let mask_paint = paint.unwrap_or_default();
|
|
||||||
let mask = SaveLayerRec::default().paint(&mask_paint);
|
|
||||||
let canvas = render_state
|
let canvas = render_state
|
||||||
.surfaces
|
.surfaces
|
||||||
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
|
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
|
||||||
|
|
||||||
// Skip save_layer when no custom paint is provided to avoid isolating content unnecessarily.
|
|
||||||
// This ensures inner strokes and fills can blend correctly on the same surface.
|
|
||||||
if use_save_layer {
|
|
||||||
canvas.save_layer(&mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
for group in paragraphs {
|
for group in paragraphs {
|
||||||
let mut offset_y = 0.0;
|
let mut offset_y = 0.0;
|
||||||
for skia_paragraph in group {
|
for skia_paragraph in group {
|
||||||
|
@ -29,7 +19,47 @@ pub fn render(
|
||||||
offset_y += skia_paragraph.height();
|
offset_y += skia_paragraph.height();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if use_save_layer {
|
}
|
||||||
canvas.restore();
|
|
||||||
|
// Render text paths (unused)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn render_as_path(
|
||||||
|
render_state: &mut RenderState,
|
||||||
|
paths: &Vec<(Path, Paint)>,
|
||||||
|
surface_id: Option<SurfaceId>,
|
||||||
|
) {
|
||||||
|
let canvas = render_state
|
||||||
|
.surfaces
|
||||||
|
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
|
||||||
|
|
||||||
|
for (path, paint) in paths {
|
||||||
|
// Note: path can be empty
|
||||||
|
canvas.draw_path(path, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How to use it?
|
||||||
|
// Type::Text(text_content) => {
|
||||||
|
// self.surfaces
|
||||||
|
// .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
|
||||||
|
// s.canvas().concat(&matrix);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let text_content = text_content.new_bounds(shape.selrect());
|
||||||
|
// let paths = text_content.get_paths(antialias);
|
||||||
|
|
||||||
|
// shadows::render_text_drop_shadows(self, &shape, &paths, antialias);
|
||||||
|
// text::render(self, &paths, None, None);
|
||||||
|
|
||||||
|
// for stroke in shape.strokes().rev() {
|
||||||
|
// shadows::render_text_path_stroke_drop_shadows(
|
||||||
|
// self, &shape, &paths, stroke, antialias,
|
||||||
|
// );
|
||||||
|
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
|
||||||
|
// shadows::render_text_path_stroke_inner_shadows(
|
||||||
|
// self, &shape, &paths, stroke, antialias,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// shadows::render_text_inner_shadows(self, &shape, &paths, antialias);
|
||||||
|
// }
|
||||||
|
|
|
@ -910,6 +910,10 @@ impl Shape {
|
||||||
pub fn has_fills(&self) -> bool {
|
pub fn has_fills(&self) -> bool {
|
||||||
!self.fills.is_empty()
|
!self.fills.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_inner_strokes(&self) -> bool {
|
||||||
|
self.strokes.iter().any(|s| s.kind == StrokeKind::Inner)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -240,4 +240,28 @@ impl Stroke {
|
||||||
|
|
||||||
paint
|
paint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render text paths (unused)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn to_text_stroked_paint(
|
||||||
|
&self,
|
||||||
|
is_open: bool,
|
||||||
|
rect: &Rect,
|
||||||
|
svg_attrs: &HashMap<String, String>,
|
||||||
|
scale: f32,
|
||||||
|
antialias: bool,
|
||||||
|
) -> skia::Paint {
|
||||||
|
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
|
||||||
|
match self.render_kind(is_open) {
|
||||||
|
StrokeKind::Inner => {
|
||||||
|
paint.set_stroke_width(2. * paint.stroke_width());
|
||||||
|
}
|
||||||
|
StrokeKind::Center => {}
|
||||||
|
StrokeKind::Outer => {
|
||||||
|
paint.set_stroke_width(2. * paint.stroke_width());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
198
render-wasm/src/shapes/text_paths.rs
Normal file
198
render-wasm/src/shapes/text_paths.rs
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
use crate::shapes::text::TextContent;
|
||||||
|
use skia_safe::{self as skia, textlayout::ParagraphBuilder, Path, Paint};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
pub struct TextPaths(TextContent);
|
||||||
|
|
||||||
|
// Note: This class is not being currently used.
|
||||||
|
// It's an example of how to convert texts to paths
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl TextPaths {
|
||||||
|
pub fn new(content: TextContent) -> Self {
|
||||||
|
Self(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
|
||||||
|
let mut paragraphs = self.to_paragraphs();
|
||||||
|
self.collect_paragraphs(&mut paragraphs);
|
||||||
|
paragraphs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
|
||||||
|
let mut offset_y = self.bounds.y();
|
||||||
|
let mut paragraphs = self.get_skia_paragraphs();
|
||||||
|
for paragraph_builder in paragraphs.iter_mut() {
|
||||||
|
// 1. Get paragraph and set the width layout
|
||||||
|
let mut skia_paragraph = paragraph_builder.build();
|
||||||
|
let text = paragraph_builder.get_text();
|
||||||
|
let paragraph_width = self.bounds.width();
|
||||||
|
skia_paragraph.layout(paragraph_width);
|
||||||
|
|
||||||
|
let mut line_offset_y = offset_y;
|
||||||
|
|
||||||
|
// 2. Iterate through each line in the paragraph
|
||||||
|
for line_metrics in skia_paragraph.get_line_metrics() {
|
||||||
|
let line_baseline = line_metrics.baseline as f32;
|
||||||
|
let start = line_metrics.start_index;
|
||||||
|
let end = line_metrics.end_index;
|
||||||
|
|
||||||
|
// 3. Get styles present in line for each text leaf
|
||||||
|
let style_metrics = line_metrics.get_style_metrics(start..end);
|
||||||
|
|
||||||
|
let mut offset_x = 0.0;
|
||||||
|
|
||||||
|
for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
|
||||||
|
let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
|
||||||
|
|
||||||
|
let start_byte = text
|
||||||
|
.char_indices()
|
||||||
|
.nth(*start_index)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let end_byte = text
|
||||||
|
.char_indices()
|
||||||
|
.nth(end_index)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
|
||||||
|
let leaf_text = &text[start_byte..end_byte];
|
||||||
|
|
||||||
|
let font = skia_paragraph.get_font_at(*start_index);
|
||||||
|
|
||||||
|
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
|
||||||
|
let blob_offset_y = line_offset_y;
|
||||||
|
|
||||||
|
// 4. Get the path for each text leaf
|
||||||
|
if let Some((text_path, paint)) = self.generate_text_path(
|
||||||
|
leaf_text,
|
||||||
|
&font,
|
||||||
|
blob_offset_x,
|
||||||
|
blob_offset_y,
|
||||||
|
style_metric,
|
||||||
|
antialias,
|
||||||
|
) {
|
||||||
|
let text_width = font.measure_text(leaf_text, None).0;
|
||||||
|
offset_x += text_width;
|
||||||
|
paths.push((text_path, paint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line_offset_y = offset_y + line_baseline;
|
||||||
|
}
|
||||||
|
offset_y += skia_paragraph.height();
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_text_path(
|
||||||
|
&self,
|
||||||
|
leaf_text: &str,
|
||||||
|
font: &skia::Font,
|
||||||
|
blob_offset_x: f32,
|
||||||
|
blob_offset_y: f32,
|
||||||
|
style_metric: &skia::textlayout::StyleMetrics,
|
||||||
|
antialias: bool,
|
||||||
|
) -> Option<(skia::Path, skia::Paint)> {
|
||||||
|
// Convert text to path, including text decoration
|
||||||
|
// TextBlob might be empty and, in this case, we return None
|
||||||
|
// This is used to avoid rendering empty paths, but we can
|
||||||
|
// revisit this logic later
|
||||||
|
if let Some((text_blob_path, text_blob_bounds)) =
|
||||||
|
Self::get_text_blob_path(leaf_text, font, blob_offset_x, blob_offset_y)
|
||||||
|
{
|
||||||
|
let mut text_path = text_blob_path.clone();
|
||||||
|
let text_width = font.measure_text(leaf_text, None).0;
|
||||||
|
|
||||||
|
let decoration = style_metric.text_style.decoration();
|
||||||
|
let font_metrics = style_metric.font_metrics;
|
||||||
|
|
||||||
|
let blob_left = blob_offset_x;
|
||||||
|
let blob_top = blob_offset_y;
|
||||||
|
let blob_height = text_blob_bounds.height();
|
||||||
|
|
||||||
|
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
|
||||||
|
decoration.ty,
|
||||||
|
font_metrics,
|
||||||
|
blob_left,
|
||||||
|
blob_top,
|
||||||
|
text_width,
|
||||||
|
blob_height,
|
||||||
|
) {
|
||||||
|
text_path.add_rect(decoration_rect, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut paint = style_metric.text_style.foreground();
|
||||||
|
paint.set_anti_alias(antialias);
|
||||||
|
|
||||||
|
return Some((text_path, paint));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_text_decoration_rect(
|
||||||
|
&self,
|
||||||
|
decoration: skia::textlayout::TextDecoration,
|
||||||
|
font_metrics: FontMetrics,
|
||||||
|
blob_left: f32,
|
||||||
|
blob_offset_y: f32,
|
||||||
|
text_width: f32,
|
||||||
|
blob_height: f32,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
match decoration {
|
||||||
|
skia::textlayout::TextDecoration::LINE_THROUGH => {
|
||||||
|
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
|
||||||
|
let underline_position = blob_height / 2.0;
|
||||||
|
Some(Rect::new(
|
||||||
|
blob_left,
|
||||||
|
blob_offset_y + underline_position - underline_thickness / 2.0,
|
||||||
|
blob_left + text_width,
|
||||||
|
blob_offset_y + underline_position + underline_thickness / 2.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
skia::textlayout::TextDecoration::UNDERLINE => {
|
||||||
|
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
|
||||||
|
let underline_position = blob_height - underline_thickness;
|
||||||
|
Some(Rect::new(
|
||||||
|
blob_left,
|
||||||
|
blob_offset_y + underline_position - underline_thickness / 2.0,
|
||||||
|
blob_left + text_width,
|
||||||
|
blob_offset_y + underline_position + underline_thickness / 2.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_text_blob_path(
|
||||||
|
leaf_text: &str,
|
||||||
|
font: &skia::Font,
|
||||||
|
blob_offset_x: f32,
|
||||||
|
blob_offset_y: f32,
|
||||||
|
) -> Option<(skia::Path, skia::Rect)> {
|
||||||
|
with_state!(state, {
|
||||||
|
let utf16_text = leaf_text.encode_utf16().collect::<Vec<u16>>();
|
||||||
|
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
||||||
|
let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
|
||||||
|
let use_font = emoji_font.as_ref().unwrap_or(font);
|
||||||
|
|
||||||
|
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
|
||||||
|
let path = SkiaParagraph::get_path(&mut text_blob);
|
||||||
|
let d = Point::new(blob_offset_x, blob_offset_y);
|
||||||
|
let offset_path = path.with_offset(d);
|
||||||
|
let bounds = text_blob.bounds();
|
||||||
|
return Some((offset_path, *bounds));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TextPaths {
|
||||||
|
type Target = TextContent;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue