diff --git a/frontend/resources/wasm-playground/js/lib.js b/frontend/resources/wasm-playground/js/lib.js index 0f27cf83a1..156b55f2ef 100644 --- a/frontend/resources/wasm-playground/js/lib.js +++ b/frontend/resources/wasm-playground/js/lib.js @@ -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(); +} diff --git a/frontend/resources/wasm-playground/js/texts.js b/frontend/resources/wasm-playground/js/texts.js new file mode 100644 index 0000000000..aed1c48da3 --- /dev/null +++ b/frontend/resources/wasm-playground/js/texts.js @@ -0,0 +1 @@ +// Placeholder for text-specific logic if needed in the future. diff --git a/frontend/resources/wasm-playground/texts.html b/frontend/resources/wasm-playground/texts.html new file mode 100644 index 0000000000..77657be0e9 --- /dev/null +++ b/frontend/resources/wasm-playground/texts.html @@ -0,0 +1,94 @@ + + + + + WASM + WebGL2 Texts + + + + + + + diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 70ceea42dc..38a5df3d24 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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| { - s.canvas().concat(&matrix); - }); + 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, ¶graphs, antialias); - text::render(self, &shape, ¶graphs, 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, ¶graphs, 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(); diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index e21205f23d..9c0e8bbfb0 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -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 { + 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 { diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index d5819c8d7c..7708f6171b 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -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], + 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], + 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], + 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], + 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), ); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 36748a8dff..17a8c45980 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -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, + 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); + } + } + } +} diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 0bec6887d2..d460a7498a 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -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], + paths: &Vec<(skia::Path, skia::Paint)>, surface_id: Option, paint: Option, ) { @@ -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(); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ea328266d2..97299877ea 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -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()), diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index dd73f81ec8..93f2b053fa 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -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,87 +139,62 @@ 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 { - match fill { - 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), - Fill::Image(image_fill) => { - 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); + pub fn get_fill_shader(&self, bounding_box: &Rect) -> Option { + 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), + Fill::Image(image_fill) => { + 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, + ); - // FIXME no image ratio applied, centered to the current rect - let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp); - let image_width = image_fill.width as f32; - let image_height = image_fill.height as f32; - let scale_x = bounding_box.width() / image_width; - let scale_y = bounding_box.height() / image_height; - let scale = scale_x.max(scale_y); - let scaled_width = image_width * scale; - let scaled_height = image_height * scale; - let pos_x = bounding_box.left() - (scaled_width - bounding_box.width()) / 2.0; - let pos_y = bounding_box.top() - (scaled_height - bounding_box.height()) / 2.0; + // FIXME no image ratio applied, centered to the current rect + let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp); + let image_width = image_fill.width as f32; + let image_height = image_fill.height as f32; + let scale_x = bounding_box.width() / image_width; + let scale_y = bounding_box.height() / image_height; + let scale = scale_x.max(scale_y); + let scaled_width = image_width * scale; + let scaled_height = image_height * scale; + let pos_x = bounding_box.left() - (scaled_width - bounding_box.width()) / 2.0; + let pos_y = bounding_box.top() - (scaled_height - bounding_box.height()) / 2.0; - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_translate((pos_x, pos_y)); - matrix.pre_scale((scale, scale), None); + let mut matrix = skia::Matrix::new_identity(); + matrix.pre_translate((pos_x, pos_y)); + matrix.pre_scale((scale, scale), None); - let opacity = image_fill.opacity(); - let alpha_color = skia::Color4f::new(1.0, 1.0, 1.0, opacity as f32 / 255.0); - let alpha_shader = skia::shaders::color(alpha_color.to_color()); + let opacity = image_fill.opacity(); + let alpha_color = skia::Color4f::new(1.0, 1.0, 1.0, opacity as f32 / 255.0); + let alpha_shader = skia::shaders::color(alpha_color.to_color()); - image_shader = image.to_shader(tile_modes, sampling_options, &matrix); - if let Some(shader) = image_shader { - image_shader = Some(skia::shaders::blend( - skia::Blender::mode(skia::BlendMode::DstIn), - shader, - alpha_shader, - )); + image_shader = image.to_shader(tile_modes, sampling_options, &matrix); + if let Some(shader) = image_shader { + image_shader = Some(skia::shaders::blend( + skia::Blender::mode(skia::BlendMode::DstIn), + shader, + alpha_shader, + )); + } } + image_shader } - image_shader } } } @@ -229,7 +204,7 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint { 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); - } -} diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index a1d5cd00d2..c757d8c00d 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -108,8 +108,6 @@ pub fn propagate_modifiers( modifiers: &[TransformEntry], ) -> (Vec, HashMap) { 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(¶graphs); + let height = auto_height(&mut paragraphs); let resize_transform = math::resize_matrix( &shape_bounds_after, &shape_bounds_after, diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index e389b627cb..e93bb42d85 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -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, + 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()); diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 510406469e..764c999bef 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -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,15 +44,11 @@ pub struct TextContent { grow_type: GrowType, } -pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec>) { - 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. - paragraph.layout(f32::MAX); - paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil())); - } +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())); } } @@ -93,93 +94,225 @@ impl TextContent { self.paragraphs.push(paragraph); } - pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec> { - let mut paragraph_group = Vec::new(); - let paragraphs = self - .paragraphs - .iter() - .map(|p| { - let paragraph_style = p.paragraph_to_style(); - let mut builder = ParagraphBuilder::new(¶graph_style, fonts); - for leaf in &p.children { - let text_style = leaf.to_style(p, &self.bounds); // FIXME - let text = leaf.apply_text_transform(p.text_transform); - builder.push_style(&text_style); - builder.add_text(&text); - builder.pop(); - } - builder.build() - }) - .collect(); - paragraph_group.push(paragraphs); - paragraph_group + pub fn to_paragraphs(&self) -> Vec { + with_state!(state, { + let fonts = state.render_state.fonts().font_collection(); + + self.paragraphs + .iter() + .map(|p| { + let paragraph_style = p.paragraph_to_style(); + let mut builder = ParagraphBuilder::new(¶graph_style, fonts); + for leaf in &p.children { + let text_style = leaf.to_style(p, &self.bounds); // FIXME + let text = leaf.apply_text_transform(p.text_transform); + builder.push_style(&text_style); + builder.add_text(&text); + builder.pop(); + } + builder + }) + .collect() + }) } - pub fn to_stroke_paragraphs( - &self, - stroke: &Stroke, - bounds: &Rect, - fonts: &FontCollection, - ) -> Vec> { - let mut paragraph_group = Vec::new(); - let stroke_paints = get_text_stroke_paints(stroke, bounds); + pub fn get_skia_paragraphs(&self) -> Vec { + 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; + } - 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(¶graph_style, fonts); - for leaf in ¶graph.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(); + 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)); + } } - let p = builder.build(); - stroke_paragraphs.push(p); + line_offset_y = offset_y + line_baseline; } - paragraph_group.push(stroke_paragraphs); + offset_y += skia_paragraph.height(); } - paragraph_group + paths } - pub fn collect_paragraphs( + fn generate_text_path( &self, - mut paragraphs: Vec>, - ) -> Vec> { - if self.grow_type() == GrowType::AutoWidth { - set_paragraphs_width(f32::MAX, &mut paragraphs); - let max_width = auto_width(¶graphs).ceil(); - set_paragraphs_width(max_width, &mut paragraphs); + 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; + + 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)); } 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, + ) -> &'a mut Vec { + 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> { - 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 { + 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> { - 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::>(); + 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> for RawTextData { } } -pub fn auto_width(paragraphs: &[Vec]) -> 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]) -> f32 { - paragraphs - .iter() - .flatten() - .fold(0.0, |auto_height, p| auto_height + p.height()) -} - -fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec { - 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() + }) } diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 7642216daa..3e2822164e 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -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(¶graphs).ceil(); + let mut paragraphs = content.to_paragraphs(); + height = auto_height(&mut paragraphs).ceil(); if content.grow_type() == GrowType::AutoWidth { - width = auto_width(¶graphs).ceil(); + width = auto_width(&mut paragraphs).ceil(); } } });