🔧 Render strokes using paths

This commit is contained in:
Elena Torro
2025-06-03 17:00:39 +02:00
parent ec29c4f5fe
commit b1ab7c7cc0
14 changed files with 661 additions and 319 deletions

View File

@@ -216,3 +216,86 @@ export function setupInteraction(canvas) {
canvas.addEventListener("mouseup", () => { isPanning = false; });
canvas.addEventListener("mouseout", () => { isPanning = false; });
}
export function addTextShape(x, y, fontSize, text) {
const numLeaves = 1; // Single text leaf for simplicity
const paragraphAttrSize = 48;
const leafAttrSize = 56;
const fillSize = 160;
const textBuffer = new TextEncoder().encode(text);
const textSize = textBuffer.byteLength;
// Calculate fills
const fills = [
{
type: "solid",
color: getRandomColor(),
opacity: getRandomFloat(0.5, 1.0),
},
];
const totalFills = fills.length;
const totalFillsSize = totalFills * fillSize;
// Calculate metadata and total buffer size
const metadataSize = paragraphAttrSize + leafAttrSize + totalFillsSize;
const totalSize = metadataSize + textSize;
// Allocate buffer
const bufferPtr = allocBytes(totalSize);
const heap = new Uint8Array(Module.HEAPU8.buffer, bufferPtr, totalSize);
const dview = new DataView(heap.buffer, bufferPtr, totalSize);
// Set number of leaves
dview.setUint32(0, numLeaves, true);
// Serialize paragraph attributes
dview.setUint8(4, 1); // text-align: left
dview.setUint8(5, 0); // text-direction: LTR
dview.setUint8(6, 0); // text-decoration: none
dview.setUint8(7, 0); // text-transform: none
dview.setFloat32(8, 1.2, true); // line-height
dview.setFloat32(12, 0, true); // letter-spacing
dview.setUint32(16, 0, true); // typography-ref-file (UUID part 1)
dview.setUint32(20, 0, true); // typography-ref-file (UUID part 2)
dview.setUint32(24, 0, true); // typography-ref-file (UUID part 3)
dview.setInt32(28, 0, true); // typography-ref-file (UUID part 4)
dview.setUint32(32, 0, true); // typography-ref-id (UUID part 1)
dview.setUint32(36, 0, true); // typography-ref-id (UUID part 2)
dview.setUint32(40, 0, true); // typography-ref-id (UUID part 3)
dview.setInt32(44, 0, true); // typography-ref-id (UUID part 4)
// Serialize leaf attributes
const leafOffset = paragraphAttrSize;
dview.setUint8(leafOffset, 0); // font-style: normal
dview.setFloat32(leafOffset + 4, fontSize, true); // font-size
dview.setUint32(leafOffset + 8, 400, true); // font-weight: normal
dview.setUint32(leafOffset + 12, 0, true); // font-id (UUID part 1)
dview.setUint32(leafOffset + 16, 0, true); // font-id (UUID part 2)
dview.setUint32(leafOffset + 20, 0, true); // font-id (UUID part 3)
dview.setInt32(leafOffset + 24, 0, true); // font-id (UUID part 4)
dview.setInt32(leafOffset + 28, 0, true); // font-family hash
dview.setUint32(leafOffset + 32, 0, true); // font-variant-id (UUID part 1)
dview.setUint32(leafOffset + 36, 0, true); // font-variant-id (UUID part 2)
dview.setUint32(leafOffset + 40, 0, true); // font-variant-id (UUID part 3)
dview.setInt32(leafOffset + 44, 0, true); // font-variant-id (UUID part 4)
dview.setInt32(leafOffset + 48, textSize, true); // text-length
dview.setInt32(leafOffset + 52, totalFills, true); // total fills count
// Serialize fills
let fillOffset = leafOffset + leafAttrSize;
fills.forEach((fill) => {
if (fill.type === "solid") {
const argb = hexToU32ARGB(fill.color, fill.opacity);
dview.setUint8(fillOffset, 0x00, true); // Fill type: solid
dview.setUint32(fillOffset + 4, argb, true);
fillOffset += fillSize; // Move to the next fill
}
});
// Add text content
const textOffset = metadataSize;
heap.set(textBuffer, textOffset);
// Call the WebAssembly function
Module._set_shape_text_content();
}

View File

@@ -0,0 +1 @@
// Placeholder for text-specific logic if needed in the future.

View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>WASM + WebGL2 Texts</title>
<style>
body {
margin: 0;
background: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render_wasm.js';
import {
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStroleFill
} from './js/lib.js';
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const MIN_LINES = 1;
const MAX_LINES = 5;
const MIN_WORDS = 1;
const MAX_WORDS = 10;
const params = new URLSearchParams(document.location.search);
const texts = params.get("texts") || 100;
function getRandomText() {
const words = ["Hello", "World", "Penpot", "Canvas", "Text", "Shape", "Random", "Line"];
const lines = Math.floor(Math.random() * MAX_LINES) + MIN_LINES;
let text = "";
for (let i = 0; i < lines; i++) {
const lineLength = Math.floor(Math.random() * MAX_WORDS) + MIN_WORDS;
const line = Array.from({ length: lineLength }, () => words[Math.floor(Math.random() * words.length)]).join(" ");
text += line;
if (i < lines - 1) text += "\n";
}
return text;
}
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
Module._init_shapes_pool(texts + 1);
setupInteraction(canvas);
const children = [];
for (let i = 0; i < texts; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(5);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 500);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const fontSize = Math.random() * 50 + 10;
const text = getRandomText();
addTextShape(x1, y1, fontSize, text);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
performance.mark('render:begin');
Module._render(Date.now());
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
console.log(`Render time: ${duration.toFixed(2)}ms`);
});
</script>
</body>
</html>

View File

@@ -1,3 +1,7 @@
use skia_safe::{self as skia, Matrix, RRect, Rect};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
mod blend;
mod debug;
mod fills;
@@ -10,10 +14,6 @@ mod strokes;
mod surfaces;
mod text;
use skia_safe::{self as skia, Matrix, RRect, Rect};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use gpu_state::GpuState;
use options::RenderOptions;
use surfaces::{SurfaceId, Surfaces};
@@ -416,34 +416,28 @@ impl RenderState {
}
Type::Text(text_content) => {
self.surfaces.apply_mut(&[SurfaceId::Fills], |s| {
self.surfaces
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
s.canvas().concat(&matrix);
});
let text_content = text_content.new_bounds(shape.selrect());
let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
let paths = text_content.get_paths(antialias);
shadows::render_text_drop_shadows(self, &shape, &paragraphs, antialias);
text::render(self, &shape, &paragraphs, None, None);
shadows::render_text_drop_shadows(self, &shape, &paths, antialias);
text::render(self, &paths, None, None);
for stroke in shape.strokes().rev() {
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
stroke,
&shape.selrect(),
self.fonts.font_collection(),
shadows::render_text_stroke_drop_shadows(
self, &shape, &paths, stroke, antialias,
);
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
text::render(
self,
&shape,
&stroke_paragraphs,
Some(SurfaceId::Strokes),
None,
strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
shadows::render_text_stroke_inner_shadows(
self, &shape, &paths, stroke, antialias,
);
shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias);
}
shadows::render_text_inner_shadows(self, &shape, &paragraphs, antialias);
shadows::render_text_inner_shadows(self, &shape, &paths, antialias);
}
_ => {
self.surfaces.apply_mut(
@@ -688,7 +682,7 @@ impl RenderState {
// scaled offset of the viewbox, this method snaps the origin to the nearest
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
/// consistent and predictable layout.
// consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
let scale = self.get_scale();

View File

@@ -87,6 +87,16 @@ impl FontStore {
let serialized = format!("{}", family);
self.font_provider.family_names().any(|x| x == serialized)
}
pub fn get_emoji_font(&self, size: f32) -> Option<Font> {
if let Some(typeface) = self
.font_provider
.match_family_style(DEFAULT_EMOJI_FONT, skia::FontStyle::default())
{
return Some(Font::from_typeface(typeface, size));
}
None
}
}
fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontProvider {

View File

@@ -2,7 +2,7 @@ use super::{RenderState, SurfaceId};
use crate::render::strokes;
use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type};
use skia_safe::{textlayout::Paragraph, Paint};
use skia_safe::{Paint, Path};
// Fill Shadows
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
@@ -86,56 +86,92 @@ pub fn render_stroke_inner_shadows(
pub fn render_text_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
render_text_drop_shadow(render_state, shape, shadow, paragraphs, antialias);
render_text_drop_shadow(render_state, shadow, paths, antialias);
}
}
pub fn render_text_drop_shadow(
render_state: &mut RenderState,
shape: &Shape,
shadow: &Shadow,
paragraphs: &[Vec<Paragraph>],
paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
let paint = shadow.get_drop_shadow_paint(antialias);
text::render(
render_state,
shape,
paragraphs,
paths,
Some(SurfaceId::DropShadows),
Some(paint),
);
}
pub fn render_text_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_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
render_text_inner_shadow(render_state, shape, shadow, paragraphs, antialias);
render_text_inner_shadow(render_state, shadow, paths, antialias);
}
}
pub fn render_text_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,
);
}
}
pub fn render_text_inner_shadow(
render_state: &mut RenderState,
shape: &Shape,
shadow: &Shadow,
paragraphs: &[Vec<Paragraph>],
paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
let paint = shadow.get_inner_shadow_paint(antialias);
text::render(
render_state,
shape,
paragraphs,
paths,
Some(SurfaceId::InnerShadows),
Some(paint),
);

View File

@@ -69,6 +69,39 @@ fn draw_stroke_on_circle(
canvas.draw_oval(stroke_rect, &paint);
}
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();
}
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();
}
// FIXME: See if we can simplify these arguments
#[allow(clippy::too_many_arguments)]
pub fn draw_stroke_on_path(
@@ -93,34 +126,15 @@ pub fn draw_stroke_on_path(
paint.set_image_filter(filter.clone());
}
// Draw the different kind of strokes for a path requires different strategies:
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 => {
canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface
canvas.clip_path(&skia_path, skia::ClipOp::Intersect, antialias);
canvas.draw_path(&skia_path, &paint);
canvas.restore();
draw_inner_stroke_path(canvas, &skia_path, &paint, antialias);
}
// For center stroke we don't need to do anything extra
StrokeKind::Center => {
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 => {
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(&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();
draw_outer_stroke_path(canvas, &skia_path, &paint, antialias);
}
}
@@ -543,3 +557,44 @@ pub fn render(
}
}
}
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);
}
}
}
}

View File

@@ -1,10 +1,9 @@
use super::{RenderState, Shape, SurfaceId};
use skia_safe::{self as skia, canvas::SaveLayerRec, textlayout::Paragraph};
use super::{RenderState, SurfaceId};
use skia_safe::{self as skia, canvas::SaveLayerRec};
pub fn render(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paths: &Vec<(skia::Path, skia::Paint)>,
surface_id: Option<SurfaceId>,
paint: Option<skia::Paint>,
) {
@@ -15,13 +14,12 @@ pub fn render(
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
canvas.save_layer(&mask);
for group in paragraphs {
let mut offset_y = 0.0;
for skia_paragraph in group {
let xy = (shape.selrect().x(), shape.selrect.y() + offset_y);
skia_paragraph.paint(canvas, xy);
offset_y += skia_paragraph.height();
for (path, paint) in paths {
if path.is_empty() {
eprintln!("Warning: Empty path detected");
}
canvas.draw_path(path, paint);
}
canvas.restore();
}

View File

@@ -602,6 +602,7 @@ impl Shape {
rect.join(shadow_rect);
}
if self.blur.blur_type != blurs::BlurType::None {
rect.left -= self.blur.value;
rect.top -= self.blur.value;
@@ -609,6 +610,18 @@ impl Shape {
rect.bottom += self.blur.value;
}
if let Some(max_stroke_width) = self
.strokes
.iter()
.map(|s| s.width)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
{
rect.left -= max_stroke_width / 2.0;
rect.top -= max_stroke_width / 2.0;
rect.right += max_stroke_width / 2.0;
rect.bottom += max_stroke_width / 2.0;
}
rect
}
@@ -704,6 +717,7 @@ impl Shape {
match self.shape_type {
Type::Text(ref mut text) => {
text.add_paragraph(paragraph);
text.new_bounds(self.selrect);
Ok(())
}
_ => Err("Shape is not a text".to_string()),

View File

@@ -1,4 +1,4 @@
use skia_safe::{self as skia, Paint, Rect};
use skia_safe::{self as skia, Rect};
pub use super::Color;
use crate::utils::get_image;
@@ -139,47 +139,19 @@ pub enum Fill {
impl Fill {
pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint {
match self {
Self::Solid(SolidColor(color)) => {
let mut p = skia::Paint::default();
p.set_color(*color);
p.set_style(skia::PaintStyle::Fill);
p.set_anti_alias(anti_alias);
p.set_blend_mode(skia::BlendMode::SrcOver);
p
}
Self::LinearGradient(gradient) => {
let mut p = skia::Paint::default();
p.set_shader(gradient.to_linear_shader(rect));
p.set_alpha(gradient.opacity);
p.set_style(skia::PaintStyle::Fill);
p.set_anti_alias(anti_alias);
p.set_blend_mode(skia::BlendMode::SrcOver);
p
}
Self::RadialGradient(gradient) => {
let mut p = skia::Paint::default();
p.set_shader(gradient.to_radial_shader(rect));
p.set_alpha(gradient.opacity);
p.set_style(skia::PaintStyle::Fill);
p.set_anti_alias(anti_alias);
p.set_blend_mode(skia::BlendMode::SrcOver);
p
}
Self::Image(image_fill) => {
let mut p = skia::Paint::default();
p.set_style(skia::PaintStyle::Fill);
p.set_anti_alias(anti_alias);
p.set_blend_mode(skia::BlendMode::SrcOver);
p.set_alpha(image_fill.opacity);
p
}
}
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Fill);
paint.set_anti_alias(anti_alias);
paint.set_blend_mode(skia::BlendMode::SrcOver);
let shader = self.get_fill_shader(rect);
if let Some(shader) = shader {
paint.set_shader(shader);
}
paint
}
pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader> {
match fill {
pub fn get_fill_shader(&self, bounding_box: &Rect) -> Option<skia::Shader> {
match self {
Fill::Solid(SolidColor(color)) => Some(skia::shaders::color(*color)),
Fill::LinearGradient(gradient) => gradient.to_linear_shader(bounding_box),
Fill::RadialGradient(gradient) => gradient.to_radial_shader(bounding_box),
@@ -187,8 +159,10 @@ pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader>
let mut image_shader = None;
let image = get_image(&image_fill.id);
if let Some(image) = image {
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let sampling_options = skia::SamplingOptions::new(
skia::FilterMode::Linear,
skia::MipmapMode::Nearest,
);
// FIXME no image ratio applied, centered to the current rect
let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp);
@@ -223,13 +197,14 @@ pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader>
}
}
}
}
pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
let mut combined_shader: Option<skia::Shader> = None;
let mut fills_paint = skia::Paint::default();
for fill in fills {
let shader = get_fill_shader(fill, &bounding_box);
let shader = fill.get_fill_shader(&bounding_box);
if let Some(shader) = shader {
combined_shader = match combined_shader {
@@ -246,10 +221,3 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
fills_paint.set_shader(combined_shader.clone());
fills_paint
}
pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect) {
let shader = get_fill_shader(fill, bounding_box);
if let Some(shader) = shader {
paint.set_shader(shader);
}
}

View File

@@ -108,8 +108,6 @@ pub fn propagate_modifiers(
modifiers: &[TransformEntry],
) -> (Vec<TransformEntry>, HashMap<Uuid, Bounds>) {
let shapes = &state.shapes;
let font_col = state.render_state.fonts.font_collection();
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| Modifier::Transform(entry.clone()))
@@ -147,9 +145,9 @@ pub fn propagate_modifiers(
if let Type::Text(content) = &shape.shape_type {
if content.grow_type() == GrowType::AutoHeight {
let mut paragraphs = content.get_skia_paragraphs(font_col);
let mut paragraphs = content.get_skia_paragraphs();
set_paragraphs_width(shape_bounds_after.width(), &mut paragraphs);
let height = auto_height(&paragraphs);
let height = auto_height(&mut paragraphs);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,

View File

@@ -224,6 +224,29 @@ impl Stroke {
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
}
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());

View File

@@ -1,15 +1,20 @@
use crate::with_state;
use crate::STATE;
use crate::{
math::Rect,
render::{default_font, DEFAULT_EMOJI_FONT},
};
use skia_safe::{
self as skia,
paint::Paint,
textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle},
textlayout::{Paragraph as SkiaParagraph, ParagraphBuilder, ParagraphStyle},
Point, TextBlob,
};
use crate::skia::FontMetrics;
use super::FontFamily;
use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind};
use crate::shapes::{self, merge_fills};
use crate::utils::uuid_from_u32;
use crate::wasm::fills::parse_fills_from_bytes;
use crate::Uuid;
@@ -39,17 +44,13 @@ pub struct TextContent {
grow_type: GrowType,
}
pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec<Vec<skia::textlayout::Paragraph>>) {
for group in paragraphs {
for paragraph in group {
// We first set max so we can get the min_intrinsic_width (this is the min word size)
// then after we set either the real with or the min.
// This is done this way so the words are not break into lines.
pub fn set_paragraphs_width(width: f32, paragraphs: &mut [ParagraphBuilder]) {
for paragraph_builder in paragraphs {
let mut paragraph = paragraph_builder.build();
paragraph.layout(f32::MAX);
paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
}
}
}
impl TextContent {
pub fn new(bounds: Rect, grow_type: GrowType) -> Self {
@@ -93,10 +94,11 @@ impl TextContent {
self.paragraphs.push(paragraph);
}
pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<Vec<skia::textlayout::Paragraph>> {
let mut paragraph_group = Vec::new();
let paragraphs = self
.paragraphs
pub fn to_paragraphs(&self) -> Vec<ParagraphBuilder> {
with_state!(state, {
let fonts = state.render_state.fonts().font_collection();
self.paragraphs
.iter()
.map(|p| {
let paragraph_style = p.paragraph_to_style();
@@ -108,78 +110,209 @@ impl TextContent {
builder.add_text(&text);
builder.pop();
}
builder.build()
builder
})
.collect()
})
.collect();
paragraph_group.push(paragraphs);
paragraph_group
}
pub fn to_stroke_paragraphs(
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
let mut paragraphs = self.to_paragraphs();
self.collect_paragraphs(&mut paragraphs);
paragraphs
}
pub fn grow_type(&self) -> GrowType {
self.grow_type
}
pub fn set_grow_type(&mut self, grow_type: GrowType) {
self.grow_type = grow_type;
}
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,
stroke: &Stroke,
bounds: &Rect,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
let mut paragraph_group = Vec::new();
let stroke_paints = get_text_stroke_paints(stroke, bounds);
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
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;
for stroke_paint in stroke_paints {
let mut stroke_paragraphs = Vec::new();
for paragraph in &self.paragraphs {
let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in &paragraph.children {
let stroke_style = leaf.to_stroke_style(paragraph, &stroke_paint);
let text: String = leaf.apply_text_transform(paragraph.text_transform);
builder.push_style(&stroke_style);
builder.add_text(&text);
builder.pop();
}
let p = builder.build();
stroke_paragraphs.push(p);
}
paragraph_group.push(stroke_paragraphs);
}
paragraph_group
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);
}
pub fn collect_paragraphs(
&self,
mut paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
if self.grow_type() == GrowType::AutoWidth {
set_paragraphs_width(f32::MAX, &mut paragraphs);
let max_width = auto_width(&paragraphs).ceil();
set_paragraphs_width(max_width, &mut paragraphs);
let mut paint = style_metric.text_style.foreground();
paint.set_anti_alias(antialias);
return Some((text_path, paint));
} else {
set_paragraphs_width(self.width(), &mut paragraphs);
eprintln!("Failed to generate path for text.");
}
None
}
fn collect_paragraphs<'a>(
&self,
paragraphs: &'a mut Vec<ParagraphBuilder>,
) -> &'a mut Vec<ParagraphBuilder> {
match self.grow_type() {
GrowType::AutoWidth => {
set_paragraphs_width(f32::MAX, paragraphs);
let max_width = auto_width(paragraphs).ceil();
set_paragraphs_width(max_width, paragraphs);
}
_ => {
set_paragraphs_width(self.width(), paragraphs);
}
}
paragraphs
}
pub fn get_skia_paragraphs(
fn calculate_text_decoration_rect(
&self,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
self.collect_paragraphs(self.to_paragraphs(fonts))
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,
}
}
pub fn get_skia_stroke_paragraphs(
&self,
stroke: &Stroke,
bounds: &Rect,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
self.collect_paragraphs(self.to_stroke_paragraphs(stroke, bounds, fonts))
}
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);
pub fn grow_type(&self) -> GrowType {
self.grow_type
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));
}
});
pub fn set_grow_type(&mut self, grow_type: GrowType) {
self.grow_type = grow_type;
eprintln!("Failed to create TextBlob for text.");
None
}
}
@@ -197,8 +330,8 @@ impl Default for TextContent {
pub struct Paragraph {
num_leaves: u32,
text_align: u8,
text_decoration: u8,
text_direction: u8,
text_decoration: u8,
text_transform: u8,
line_height: f32,
letter_spacing: f32,
@@ -212,8 +345,8 @@ impl Default for Paragraph {
Self {
num_leaves: 0,
text_align: 0,
text_decoration: 0,
text_direction: 0,
text_decoration: 0,
text_transform: 0,
line_height: 1.0,
letter_spacing: 0.0,
@@ -229,8 +362,8 @@ impl Paragraph {
pub fn new(
num_leaves: u32,
text_align: u8,
text_decoration: u8,
text_direction: u8,
text_decoration: u8,
text_transform: u8,
line_height: f32,
letter_spacing: f32,
@@ -241,8 +374,8 @@ impl Paragraph {
Self {
num_leaves,
text_align,
text_decoration,
text_direction,
text_decoration,
text_transform,
line_height,
letter_spacing,
@@ -345,7 +478,6 @@ impl TextLeaf {
3 => skia::textlayout::TextDecoration::OVERLINE,
_ => skia::textlayout::TextDecoration::NO_DECORATION,
});
style.set_font_families(&[
self.serialized_font_family(),
default_font(),
@@ -355,16 +487,6 @@ impl TextLeaf {
style
}
pub fn to_stroke_style(
&self,
paragraph: &Paragraph,
stroke_paint: &Paint,
) -> skia::textlayout::TextStyle {
let mut style = self.to_style(paragraph, &Rect::default());
style.set_foreground_paint(stroke_paint);
style
}
fn serialized_font_family(&self) -> String {
format!("{}", self.font_family)
}
@@ -445,7 +567,6 @@ impl From<&[u8]> for RawTextLeafData {
let text_leaf: RawTextLeaf = RawTextLeaf::try_from(bytes).unwrap();
let total_fills = text_leaf.total_fills as usize;
// Use checked_mul to prevent overflow
let fills_size = total_fills
.checked_mul(RAW_LEAF_FILLS_SIZE)
.expect("Overflow occurred while calculating fills size");
@@ -588,66 +709,18 @@ impl From<&Vec<u8>> for RawTextData {
}
}
pub fn auto_width(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> f32 {
paragraphs.iter().flatten().fold(0.0, |auto_width, p| {
f32::max(p.max_intrinsic_width(), auto_width)
pub fn auto_width(paragraphs: &mut [ParagraphBuilder]) -> f32 {
paragraphs.iter_mut().fold(0.0, |auto_width, p| {
let mut paragraph = p.build();
paragraph.layout(f32::MAX);
f32::max(paragraph.max_intrinsic_width(), auto_width)
})
}
pub fn auto_height(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> f32 {
paragraphs
.iter()
.flatten()
.fold(0.0, |auto_height, p| auto_height + p.height())
}
fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec<Paint> {
let mut paints = Vec::new();
match stroke.kind {
StrokeKind::Inner => {
let mut paint = skia::Paint::default();
paint.set_blend_mode(skia::BlendMode::DstOver);
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::SrcATop);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds);
paints.push(paint);
}
StrokeKind::Center => {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width);
set_paint_fill(&mut paint, &stroke.fill, bounds);
paints.push(paint);
}
StrokeKind::Outer => {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_blend_mode(skia::BlendMode::DstOver);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds);
paints.push(paint);
let mut paint = skia::Paint::default();
paint.set_blend_mode(skia::BlendMode::Clear);
paint.set_anti_alias(true);
paints.push(paint);
}
}
paints
pub fn auto_height(paragraphs: &mut [ParagraphBuilder]) -> f32 {
paragraphs.iter_mut().fold(0.0, |auto_height, p| {
let mut paragraph = p.build();
paragraph.layout(f32::MAX);
auto_height + paragraph.height()
})
}

View File

@@ -1,8 +1,8 @@
use crate::mem;
use crate::shapes::{auto_height, auto_width, GrowType, RawTextData, Type};
use crate::with_current_shape;
use crate::STATE;
use crate::{with_current_shape, with_state};
#[no_mangle]
pub extern "C" fn clear_shape_text() {
@@ -35,11 +35,6 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) {
#[no_mangle]
pub extern "C" fn get_text_dimensions() -> *mut u8 {
let font_col;
with_state!(state, {
font_col = state.render_state.fonts.font_collection();
});
let mut width = 0.01;
let mut height = 0.01;
with_current_shape!(state, |shape: &mut Shape| {
@@ -47,10 +42,10 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
height = shape.selrect.height();
if let Type::Text(content) = &shape.shape_type {
let paragraphs = content.get_skia_paragraphs(font_col);
height = auto_height(&paragraphs).ceil();
let mut paragraphs = content.to_paragraphs();
height = auto_height(&mut paragraphs).ceil();
if content.grow_type() == GrowType::AutoWidth {
width = auto_width(&paragraphs).ceil();
width = auto_width(&mut paragraphs).ceil();
}
}
});