diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 6e993228d..9432aafee 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -56,7 +56,7 @@ ([{:keys [id points selrect] :as shape} content] (wasm.api/use-shape id) - (wasm.api/set-shape-text-content content) + (wasm.api/set-shape-text content) (let [dimension (wasm.api/text-dimensions) resize-v (gpt/point diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index f5fffbd3e..c597ae063 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -188,7 +188,6 @@ (defn- store-image [id] - (let [buffer (uuid/get-u32 id) url (cf/resolve-file-media {:id id})] (->> (http/send! {:method :get @@ -209,6 +208,33 @@ (aget buffer 3)) true)))))) +(defn- get-fill-images + [leaf] + (filter :fill-image (:fills leaf))) + +(defn- process-fill-image + [fill] + (rx/from + (when-let [image (:fill-image fill)] + (let [id (dm/get-prop image :id) + buffer (uuid/get-u32 id) + cached-image? (h/call wasm/internal-module "_is_image_cached" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))] + (when (zero? cached-image?) + (store-image id)))))) + +(defn set-shape-text-images + [content] + (let [paragraph-set (first (get content :children)) + paragraphs (get paragraph-set :children)] + (->> paragraphs + (mapcat :children) + (mapcat get-fill-images) + (map process-fill-image)))) + (defn set-shape-fills [fills] (h/call wasm/internal-module "_clear_shape_fills") @@ -218,23 +244,24 @@ gradient (:fill-color-gradient fill) image (:fill-image fill) offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) - heap (mem/get-heap-u32)] + heap (mem/get-heap-u8) + dview (js/DataView. (.-buffer heap))] (cond (some? color) - (let [argb (sr-clr/hex->u32argb color opacity)] - (sr-fills/write-solid-fill! offset heap argb) + (do + (sr-fills/write-solid-fill! offset dview (sr-clr/hex->u32argb color opacity)) (h/call wasm/internal-module "_add_shape_fill")) (some? gradient) (do - (sr-fills/write-gradient-fill! offset heap gradient opacity) + (sr-fills/write-gradient-fill! offset dview gradient opacity) (h/call wasm/internal-module "_add_shape_fill")) (some? image) (let [id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (sr-fills/write-image-fill! offset heap id opacity (dm/get-prop image :width) (dm/get-prop image :height)) + (sr-fills/write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)) (h/call wasm/internal-module "_add_shape_fill") (when (== cached-image? 0) (store-image id)))))) @@ -254,7 +281,8 @@ cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap) cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap) offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) - heap (mem/get-heap-u32)] + heap (mem/get-heap-u8) + dview (js/DataView. (.-buffer heap))] (case align :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end) :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end) @@ -262,22 +290,22 @@ (cond (some? gradient) - (let [_ nil] - (sr-fills/write-gradient-fill! offset heap gradient opacity) + (do + (sr-fills/write-gradient-fill! offset dview gradient opacity) (h/call wasm/internal-module "_add_shape_stroke_fill")) (some? image) (let [id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (sr-fills/write-image-fill! offset heap id opacity (dm/get-prop image :width) (dm/get-prop image :height)) + (sr-fills/write-image-fill! offset dview id opacity (dm/get-prop image :width) (dm/get-prop image :height)) (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) (store-image id))) (some? color) - (let [argb (sr-clr/hex->u32argb color opacity)] - (sr-fills/write-solid-fill! offset heap argb) + (do + (sr-fills/write-solid-fill! offset dview (sr-clr/hex->u32argb color opacity)) (h/call wasm/internal-module "_add_shape_stroke_fill"))))) strokes)) @@ -608,6 +636,12 @@ fonts)] (f/store-fonts fonts)))) +(defn set-shape-text + [content] + (concat + (set-shape-text-images content) + (set-shape-text-content content))) + (defn set-shape-grow-type [grow-type] (h/call wasm/internal-module "_set_shape_grow_type" (sr/translate-grow-type grow-type))) @@ -697,7 +731,7 @@ (when (some? corners) (set-shape-corners corners)) (when (some? shadows) (set-shape-shadows shadows)) (when (and (= type :text) (some? content)) - (set-shape-text-content content)) + (set-shape-text content)) (when (= type :text) (set-shape-grow-type grow-type)) (when (or (ctl/any-layout? shape) @@ -711,9 +745,7 @@ (set-grid-layout shape)) (let [pending (into [] (concat - (if (and (= type :text) (some? content)) - (set-shape-text-content content) - []) + (set-shape-text content) (set-shape-fills fills) (set-shape-strokes strokes)))] (perf/end-measure "set-object") diff --git a/frontend/src/app/render_wasm/api/texts.cljs b/frontend/src/app/render_wasm/api/texts.cljs index e48cec362..2f44578a8 100644 --- a/frontend/src/app/render_wasm/api/texts.cljs +++ b/frontend/src/app/render_wasm/api/texts.cljs @@ -6,30 +6,65 @@ (ns app.render-wasm.api.texts (:require + [app.common.data.macros :as dm] [app.render-wasm.api.fonts :as f] [app.render-wasm.helpers :as h] [app.render-wasm.mem :as mem] [app.render-wasm.serializers :as sr] + [app.render-wasm.serializers.color :as sr-clr] + [app.render-wasm.serializers.fills :as sr-fills] [app.render-wasm.wasm :as wasm])) (defn utf8->buffer [text] (let [encoder (js/TextEncoder.)] (.encode encoder text))) +(defn set-text-leaf-fills + [fills current-offset dview] + (reduce (fn [offset fill] + (let [opacity (or (:fill-opacity fill) 1.0) + color (:fill-color fill) + gradient (:fill-color-gradient fill) + image (:fill-image fill)] + (cond + (some? color) + (sr-fills/write-solid-fill! offset dview (sr-clr/hex->u32argb color opacity)) + + (some? gradient) + (sr-fills/write-gradient-fill! offset dview gradient opacity) + + (some? image) + (sr-fills/write-image-fill! offset dview + (dm/get-prop image :id) + opacity + (dm/get-prop image :width) + (dm/get-prop image :height))) + + (+ offset sr-fills/FILL-BYTE-SIZE))) + current-offset + fills)) + +(defn total-fills-count + [leaves] + (reduce #(+ %1 (count (:fills %2))) 0 leaves)) + (defn write-shape-text ;; buffer has the following format: ;; [ ] [leaves paragraph text] - (let [num-leaves (count leaves) + (let [le? true + num-leaves (count leaves) paragraph-attr-size 48 - leaf-attr-size 52 - metadata-size (+ 1 paragraph-attr-size (* num-leaves leaf-attr-size)) + total-fills (total-fills-count leaves) + total-fills-size (* sr-fills/FILL-BYTE-SIZE total-fills) + leaf-attr-size 56 + metadata-size (+ paragraph-attr-size (* num-leaves leaf-attr-size) total-fills-size) text-buffer (utf8->buffer text) text-size (.-byteLength text-buffer) buffer (js/ArrayBuffer. (+ metadata-size text-size)) dview (js/DataView. buffer)] - (.setUint32 dview 0 num-leaves) + (.setUint32 dview 0 num-leaves le?) ;; Serialize paragraph attributes (let [text-align (sr/serialize-text-align (:text-align paragraph)) @@ -41,26 +76,26 @@ typography-ref-file (sr/serialize-uuid (:typography-ref-file paragraph)) typography-ref-id (sr/serialize-uuid (:typography-ref-id paragraph))] - (.setUint8 dview 4 text-align) - (.setUint8 dview 5 text-direction) - (.setUint8 dview 6 text-decoration) - (.setUint8 dview 7 text-transform) + (.setUint8 dview 4 text-align le?) + (.setUint8 dview 5 text-direction le?) + (.setUint8 dview 6 text-decoration le?) + (.setUint8 dview 7 text-transform le?) - (.setFloat32 dview 8 line-height) - (.setFloat32 dview 12 letter-spacing) + (.setFloat32 dview 8 line-height le?) + (.setFloat32 dview 12 letter-spacing le?) - (.setUint32 dview 16 (aget typography-ref-file 0)) - (.setUint32 dview 20 (aget typography-ref-file 1)) - (.setUint32 dview 24 (aget typography-ref-file 2)) - (.setInt32 dview 28 (aget typography-ref-file 3)) + (.setUint32 dview 16 (aget typography-ref-file 0) le?) + (.setUint32 dview 20 (aget typography-ref-file 1) le?) + (.setUint32 dview 24 (aget typography-ref-file 2) le?) + (.setInt32 dview 28 (aget typography-ref-file 3) le?) - (.setUint32 dview 32 (aget typography-ref-id 0)) - (.setUint32 dview 36 (aget typography-ref-id 1)) - (.setUint32 dview 40 (aget typography-ref-id 2)) - (.setInt32 dview 44 (aget typography-ref-id 3))) + (.setUint32 dview 32 (aget typography-ref-id 0) le?) + (.setUint32 dview 36 (aget typography-ref-id 1) le?) + (.setUint32 dview 40 (aget typography-ref-id 2) le?) + (.setInt32 dview 44 (aget typography-ref-id 3) le?)) ;; Serialize leaves attributes - (loop [index 0 offset (+ 1 paragraph-attr-size)] + (loop [index 0 offset paragraph-attr-size] (when (< index num-leaves) (let [leaf (nth leaves index) font-style (f/serialize-font-style (:font-style leaf)) @@ -70,26 +105,30 @@ font-family (hash (:font-family leaf)) font-variant-id (sr/serialize-uuid (:font-variant-id leaf)) text-buffer (utf8->buffer (:text leaf)) - text-length (.-byteLength text-buffer)] + text-length (.-byteLength text-buffer) + fills (:fills leaf) + total-fills (count fills)] - (.setUint8 dview offset font-style) - (.setFloat32 dview (+ offset 4) font-size) - (.setUint32 dview (+ offset 8) font-weight) - (.setUint32 dview (+ offset 12) (aget font-id 0)) - (.setUint32 dview (+ offset 16) (aget font-id 1)) - (.setUint32 dview (+ offset 20) (aget font-id 2)) - (.setInt32 dview (+ offset 24) (aget font-id 3)) + (.setUint8 dview offset font-style le?) + (.setFloat32 dview (+ offset 4) font-size le?) + (.setUint32 dview (+ offset 8) font-weight le?) + (.setUint32 dview (+ offset 12) (aget font-id 0) le?) + (.setUint32 dview (+ offset 16) (aget font-id 1) le?) + (.setUint32 dview (+ offset 20) (aget font-id 2) le?) + (.setInt32 dview (+ offset 24) (aget font-id 3) le?) - (.setInt32 dview (+ offset 28) font-family) + (.setInt32 dview (+ offset 28) font-family le?) - (.setUint32 dview (+ offset 32) (aget font-variant-id 0)) - (.setUint32 dview (+ offset 36) (aget font-variant-id 1)) - (.setUint32 dview (+ offset 40) (aget font-variant-id 2)) - (.setInt32 dview (+ offset 44) (aget font-variant-id 3)) + (.setUint32 dview (+ offset 32) (aget font-variant-id 0) le?) + (.setUint32 dview (+ offset 36) (aget font-variant-id 1) le?) + (.setUint32 dview (+ offset 40) (aget font-variant-id 2) le?) + (.setInt32 dview (+ offset 44) (aget font-variant-id 3) le?) - (.setInt32 dview (+ offset 48) text-length) + (.setInt32 dview (+ offset 48) text-length le?) + (.setInt32 dview (+ offset 52) total-fills le?) - (recur (inc index) (+ offset leaf-attr-size))))) + (let [new-offset (set-text-leaf-fills fills (+ offset leaf-attr-size) dview)] + (recur (inc index) new-offset))))) ;; Add text content to buffer (let [text-offset metadata-size diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 1dcf88711..4d8257e62 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -15,16 +15,14 @@ (def FILL-BYTE-SIZE (+ 4 (max GRADIENT-BYTE-SIZE IMAGE-BYTE-SIZE SOLID-BYTE-SIZE))) (defn write-solid-fill! - [offset heap-u32 argb] - (let [dview (js/DataView. (.-buffer heap-u32))] - (.setUint8 dview offset 0x00 true) - (.setUint32 dview (+ offset 4) argb true) - (+ offset FILL-BYTE-SIZE))) + [offset dview argb] + (.setUint8 dview offset 0x00 true) + (.setUint32 dview (+ offset 4) argb true) + (+ offset FILL-BYTE-SIZE)) (defn write-image-fill! - [offset heap-u32 id opacity width height] - (let [dview (js/DataView. (.-buffer heap-u32)) - uuid-buffer (uuid/get-u32 id)] + [offset dview id opacity width height] + (let [uuid-buffer (uuid/get-u32 id)] (.setUint8 dview offset 0x03 true) (.setUint32 dview (+ offset 4) (aget uuid-buffer 0) true) (.setUint32 dview (+ offset 8) (aget uuid-buffer 1) true) @@ -35,22 +33,19 @@ (.setInt32 dview (+ offset 28) height true) (+ offset FILL-BYTE-SIZE))) - - (defn write-gradient-fill! - [offset heap-u32 gradient opacity] - (let [dview (js/DataView. (.-buffer heap-u32)) - start-x (:start-x gradient) + [offset dview gradient opacity] + (let [start-x (:start-x gradient) start-y (:start-y gradient) end-x (:end-x gradient) - end-y (:end-y gradient) + end-y (:end-y gradient) width (or (:width gradient) 0) stops (take shp/MAX-GRADIENT-STOPS (:stops gradient)) type (if (= (:type gradient) :linear) 0x01 0x02)] (.setUint8 dview offset type true) (.setFloat32 dview (+ offset 4) start-x true) (.setFloat32 dview (+ offset 8) start-y true) - (.setFloat32 dview (+ offset 12) end-x true) + (.setFloat32 dview (+ offset 12) end-x true) (.setFloat32 dview (+ offset 16) end-y true) (.setFloat32 dview (+ offset 20) opacity true) (.setFloat32 dview (+ offset 24) width true) @@ -60,8 +55,8 @@ (+ offset FILL-BYTE-SIZE) (let [stop (first stops) hex-color (:color stop) - opacity (:opacity stop) - argb (clr/hex->u32argb hex-color opacity) + stop-opacity (:opacity stop) + argb (clr/hex->u32argb hex-color stop-opacity) stop-offset (:offset stop)] (.setUint32 dview loop-offset argb true) (.setFloat32 dview (+ loop-offset 4) stop-offset true) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 02525afc9..7c879b9c8 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -150,7 +150,7 @@ (api/set-shape-svg-raw-content (api/get-static-markup shape)) (= (:type shape) :text) - (into [] (api/set-shape-text-content v))) + (api/set-shape-text v)) :grow-type (api/set-shape-grow-type v) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 40112717d..70ceea42d 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -10,7 +10,7 @@ mod strokes; mod surfaces; mod text; -use skia_safe::{self as skia, image, Matrix, RRect, Rect}; +use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; @@ -19,7 +19,7 @@ use options::RenderOptions; use surfaces::{SurfaceId, Surfaces}; use crate::performance; -use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type}; +use crate::shapes::{modified_children_ids, Corners, Shape, StructureEntry, Type}; use crate::tiles::{self, TileRect, TileViewbox, TileWithDistance}; use crate::uuid::Uuid; use crate::view::Viewbox; @@ -427,13 +427,11 @@ impl RenderState { text::render(self, &shape, ¶graphs, None, None); for stroke in shape.strokes().rev() { - let mut image: Option = None; - if let Fill::Image(image_fill) = &stroke.fill { - image = self.images.get(&image_fill.id()).cloned(); - } - let stroke_paints = shape.get_text_stroke_paint(stroke, image.as_ref()); - let stroke_paragraphs = text_content - .get_skia_stroke_paragraphs(self.fonts.font_collection(), &stroke_paints); + let stroke_paragraphs = text_content.get_skia_stroke_paragraphs( + stroke, + &shape.selrect(), + self.fonts.font_collection(), + ); shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias); text::render( self, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 45ff3bb13..ea328266d 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,5 +1,4 @@ -use skia_safe::Image; -use skia_safe::{self as skia, paint::Paint}; +use skia_safe::{self as skia}; use crate::render::BlendMode; use crate::uuid::Uuid; @@ -806,87 +805,6 @@ impl Shape { pub fn has_fills(&self) -> bool { !self.fills.is_empty() } - - fn set_paint_fill(&self, paint: &mut Paint, fill: &Fill, image: Option) { - match fill { - Fill::Solid(SolidColor(color)) => { - paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - paint.set_shader(gradient.to_linear_shader(&self.selrect())); - } - Fill::RadialGradient(gradient) => { - paint.set_shader(gradient.to_radial_shader(&self.selrect())); - } - Fill::Image(image_fill) => { - if let Some(image) = image { - let position = (self.selrect().x(), self.selrect().y()); - let sampling_options = skia::SamplingOptions::new( - skia::FilterMode::Linear, - skia::MipmapMode::Nearest, - ); - let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp); - let mut matrix = skia::Matrix::default(); - matrix.set_translate(position); - - let shader = image.to_shader(tile_modes, sampling_options, &matrix); - paint.set_shader(shader); - paint.set_alpha(image_fill.opacity()); - } - } - } - } - - pub fn get_text_stroke_paint(&self, stroke: &Stroke, image: Option<&Image>) -> 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); - - self.set_paint_fill(&mut paint, &stroke.fill, image.cloned()); - - 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); - - self.set_paint_fill(&mut paint, &stroke.fill, image.cloned()); - - 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); - - self.set_paint_fill(&mut paint, &stroke.fill, image.cloned()); - - 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 - } } /* diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 27125f96f..dd73f81ec 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -1,6 +1,7 @@ -use skia_safe::{self as skia, Rect}; +use skia_safe::{self as skia, Paint, Rect}; pub use super::Color; +use crate::utils::get_image; use crate::uuid::Uuid; #[derive(Debug, Clone, PartialEq)] @@ -176,3 +177,79 @@ impl Fill { } } } + +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); + + // 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 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 + } + } +} + +pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint { + let mut combined_shader: Option = None; + let mut fills_paint = skia::Paint::default(); + + for fill in fills { + let shader = get_fill_shader(fill, &bounding_box); + + if let Some(shader) = shader { + combined_shader = match combined_shader { + Some(existing_shader) => Some(skia::shaders::blend( + skia::Blender::mode(skia::BlendMode::Overlay), + existing_shader, + shader, + )), + None => Some(shader), + }; + } + } + + 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/text.rs b/render-wasm/src/shapes/text.rs index d207eb616..510406469 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -9,7 +9,9 @@ use skia_safe::{ }; use super::FontFamily; +use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind}; use crate::utils::uuid_from_u32; +use crate::wasm::fills::parse_fills_from_bytes; use crate::Uuid; #[derive(Debug, PartialEq, Clone, Copy)] @@ -52,9 +54,9 @@ pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec Self { Self { + paragraphs: Vec::new(), bounds, grow_type, - ..Self::default() } } @@ -100,7 +102,7 @@ impl TextContent { 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); + 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); @@ -115,10 +117,12 @@ impl TextContent { pub fn to_stroke_paragraphs( &self, + stroke: &Stroke, + bounds: &Rect, fonts: &FontCollection, - stroke_paints: &Vec, ) -> Vec> { let mut paragraph_group = Vec::new(); + let stroke_paints = get_text_stroke_paints(stroke, bounds); for stroke_paint in stroke_paints { let mut stroke_paragraphs = Vec::new(); @@ -126,7 +130,7 @@ impl TextContent { 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 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); @@ -163,10 +167,11 @@ impl TextContent { pub fn get_skia_stroke_paragraphs( &self, + stroke: &Stroke, + bounds: &Rect, fonts: &FontCollection, - paints: &Vec, ) -> Vec> { - self.collect_paragraphs(self.to_stroke_paragraphs(fonts, paints)) + self.collect_paragraphs(self.to_stroke_paragraphs(stroke, bounds, fonts)) } pub fn grow_type(&self) -> GrowType { @@ -190,6 +195,7 @@ impl Default for TextContent { #[derive(Debug, PartialEq, Clone)] pub struct Paragraph { + num_leaves: u32, text_align: u8, text_decoration: u8, text_direction: u8, @@ -204,6 +210,7 @@ pub struct Paragraph { impl Default for Paragraph { fn default() -> Self { Self { + num_leaves: 0, text_align: 0, text_decoration: 0, text_direction: 0, @@ -218,9 +225,9 @@ impl Default for Paragraph { } impl Paragraph { - // FIXME: These arguments could be grouped or simplified #[allow(clippy::too_many_arguments)] pub fn new( + num_leaves: u32, text_align: u8, text_decoration: u8, text_direction: u8, @@ -232,6 +239,7 @@ impl Paragraph { children: Vec, ) -> Self { Self { + num_leaves, text_align, text_decoration, text_direction, @@ -286,6 +294,7 @@ pub struct TextLeaf { font_style: u8, font_weight: i32, font_variant_id: Uuid, + fills: Vec, } impl TextLeaf { @@ -296,6 +305,7 @@ impl TextLeaf { font_style: u8, font_weight: i32, font_variant_id: Uuid, + fills: Vec, ) -> Self { Self { text, @@ -304,12 +314,26 @@ impl TextLeaf { font_style, font_weight, font_variant_id, + fills, } } - pub fn to_style(&self, paragraph: &Paragraph) -> skia::textlayout::TextStyle { + pub fn to_style( + &self, + paragraph: &Paragraph, + content_bounds: &Rect, + ) -> skia::textlayout::TextStyle { let mut style = skia::textlayout::TextStyle::default(); - style.set_color(skia::Color::BLACK); + + let bounding_box = Rect::from_xywh( + content_bounds.x(), + content_bounds.y(), + self.font_size * self.text.len() as f32, + self.font_size, + ); + + let paint = merge_fills(&self.fills, bounding_box); + style.set_foreground_paint(&paint); style.set_font_size(self.font_size); style.set_letter_spacing(paragraph.letter_spacing); style.set_height(paragraph.line_height); @@ -336,7 +360,7 @@ impl TextLeaf { paragraph: &Paragraph, stroke_paint: &Paint, ) -> skia::textlayout::TextStyle { - let mut style = self.to_style(paragraph); + let mut style = self.to_style(paragraph, &Rect::default()); style.set_foreground_paint(stroke_paint); style } @@ -366,11 +390,44 @@ impl TextLeaf { } } -pub const RAW_PARAGRAPH_DATA_SIZE: usize = 48; -pub const RAW_LEAF_DATA_SIZE: usize = 52; +const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); +//const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::(); +// FIXME +pub const RAW_LEAF_DATA_SIZE: usize = 56; +pub const RAW_LEAF_FILLS_SIZE: usize = 160; #[repr(C)] -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] +pub struct RawTextLeaf { + font_style: u8, + font_size: f32, + font_weight: i32, + font_id: [u32; 4], + font_family: [u8; 4], + font_variant_id: [u32; 4], + text_length: u32, + total_fills: u32, +} + +impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeaf { + fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self { + unsafe { std::mem::transmute(bytes) } + } +} + +impl TryFrom<&[u8]> for RawTextLeaf { + type Error = String; + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_LEAF_DATA_SIZE] = bytes + .get(0..RAW_LEAF_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid text leaf data".to_string())?; + Ok(RawTextLeaf::from(data)) + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] pub struct RawTextLeafData { font_style: u8, font_size: f32, @@ -379,15 +436,48 @@ pub struct RawTextLeafData { font_family: [u8; 4], font_variant_id: [u32; 4], text_length: u32, + total_fills: u32, + fills: Vec, +} + +impl From<&[u8]> for RawTextLeafData { + fn from(bytes: &[u8]) -> Self { + 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"); + + let fills_start = RAW_LEAF_DATA_SIZE; + let fills_end = fills_start + fills_size; + let buffer = &bytes[fills_start..fills_end]; + let fills = parse_fills_from_bytes(buffer, total_fills); + + Self { + font_style: text_leaf.font_style, + font_size: text_leaf.font_size, + font_weight: text_leaf.font_weight, + font_id: text_leaf.font_id, + font_family: text_leaf.font_family, + font_variant_id: text_leaf.font_variant_id, + text_length: text_leaf.text_length, + total_fills: text_leaf.total_fills, + fills, + } + } } #[repr(C)] -#[derive(Debug)] +#[repr(align(4))] +#[derive(Debug, Clone, Copy)] pub struct RawParagraphData { + num_leaves: u32, text_align: u8, - text_transform: u8, - text_decoration: u8, text_direction: u8, + text_decoration: u8, + text_transform: u8, line_height: f32, letter_spacing: f32, typography_ref_file: [u32; 4], @@ -396,54 +486,22 @@ pub struct RawParagraphData { impl From<[u8; RAW_PARAGRAPH_DATA_SIZE]> for RawParagraphData { fn from(bytes: [u8; RAW_PARAGRAPH_DATA_SIZE]) -> Self { - Self { - text_align: bytes[4], - text_direction: bytes[5], - text_decoration: bytes[6], - text_transform: bytes[7], - line_height: f32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), - letter_spacing: f32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), - typography_ref_file: [ - u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), - u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - u32::from_be_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]), - u32::from_be_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]), - ], - typography_ref_id: [ - u32::from_be_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]), - u32::from_be_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]), - u32::from_be_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]), - u32::from_be_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]), - ], - } + unsafe { std::mem::transmute(bytes) } } } -pub struct RawTextData { - pub paragraph: Paragraph, +impl TryFrom<&[u8]> for RawParagraphData { + type Error = String; + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_PARAGRAPH_DATA_SIZE] = bytes + .get(0..RAW_PARAGRAPH_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid paragraph data".to_string())?; + Ok(RawParagraphData::from(data)) + } } impl RawTextData { - fn leaves_attrs_from_bytes(buffer: &[u8], num_leaves: usize) -> Vec { - let mut attrs = Vec::new(); - for i in 0..num_leaves { - let start = i * RAW_LEAF_DATA_SIZE; - let end = start + RAW_LEAF_DATA_SIZE; - let bytes = &buffer[start..end]; - let array: [u8; RAW_LEAF_DATA_SIZE] = bytes.try_into().expect("Slice length mismatch"); - let leaf_attrs = RawTextLeafData::from(array); - attrs.push(leaf_attrs); - } - attrs - } - - fn paragraph_attrs_from_bytes(buffer: &[u8]) -> RawParagraphData { - let bytes: [u8; RAW_PARAGRAPH_DATA_SIZE] = buffer[..RAW_PARAGRAPH_DATA_SIZE] - .try_into() - .expect("Slice length mismatch for paragraph attributes"); - RawParagraphData::from(bytes) - } - fn text_from_bytes(buffer: &[u8], offset: usize, text_length: u32) -> (String, usize) { let text_length = text_length as usize; let text_end = offset + text_length; @@ -467,75 +525,60 @@ impl RawTextData { } } -impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeafData { - fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self { - Self { - font_style: bytes[0], - font_size: f32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - font_weight: i32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), - font_id: [ - u32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), - u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), - u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - u32::from_be_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]), - ], - font_family: [bytes[28], bytes[29], bytes[30], bytes[31]], - font_variant_id: [ - u32::from_be_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]), - u32::from_be_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]), - u32::from_be_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]), - u32::from_be_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]), - ], - text_length: u32::from_be_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]), - } - } +pub struct RawTextData { + pub paragraph: Paragraph, } impl From<&Vec> for RawTextData { fn from(bytes: &Vec) -> Self { - let num_leaves = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; - - let paragraph_attrs = - RawTextData::paragraph_attrs_from_bytes(&bytes[..RAW_PARAGRAPH_DATA_SIZE]); - let leaves_attrs = - RawTextData::leaves_attrs_from_bytes(&bytes[1 + RAW_PARAGRAPH_DATA_SIZE..], num_leaves); - - let metadata_size = 1 + RAW_PARAGRAPH_DATA_SIZE + num_leaves * RAW_LEAF_DATA_SIZE; - let text_start = metadata_size; - let mut offset = text_start; + let paragraph = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE]).unwrap(); + let mut offset = RAW_PARAGRAPH_DATA_SIZE; + let mut raw_text_leaves: Vec = Vec::new(); let mut text_leaves: Vec = Vec::new(); - for attrs in leaves_attrs { - let (text, new_offset) = RawTextData::text_from_bytes(bytes, offset, attrs.text_length); - offset = new_offset; - - let font_id = uuid_from_u32(attrs.font_id); - let font_variant_id = uuid_from_u32(attrs.font_variant_id); - - let font_family = - FontFamily::new(font_id, attrs.font_weight as u32, attrs.font_style.into()); - - let text_leaf = TextLeaf::new( - text, - font_family, - attrs.font_size, - attrs.font_style, - attrs.font_weight, - font_variant_id, - ); - text_leaves.push(text_leaf); + for _ in 0..paragraph.num_leaves { + let text_leaf = RawTextLeafData::from(&bytes[offset..]); + raw_text_leaves.push(text_leaf.clone()); + offset += RAW_LEAF_DATA_SIZE + (text_leaf.total_fills as usize * RAW_LEAF_FILLS_SIZE); } - let typography_ref_file = uuid_from_u32(paragraph_attrs.typography_ref_file); - let typography_ref_id = uuid_from_u32(paragraph_attrs.typography_ref_id); + for text_leaf in raw_text_leaves.iter() { + let (text, new_offset) = + RawTextData::text_from_bytes(bytes, offset, text_leaf.text_length); + offset = new_offset; + + let font_id = uuid_from_u32(text_leaf.font_id); + let font_variant_id = uuid_from_u32(text_leaf.font_variant_id); + + let font_family = FontFamily::new( + font_id, + text_leaf.font_weight as u32, + text_leaf.font_style.into(), + ); + + let new_text_leaf = TextLeaf::new( + text, + font_family, + text_leaf.font_size, + text_leaf.font_style, + text_leaf.font_weight, + font_variant_id, + text_leaf.fills.clone(), + ); + text_leaves.push(new_text_leaf); + } + + let typography_ref_file = uuid_from_u32(paragraph.typography_ref_file); + let typography_ref_id = uuid_from_u32(paragraph.typography_ref_id); let paragraph = Paragraph::new( - paragraph_attrs.text_align, - paragraph_attrs.text_decoration, - paragraph_attrs.text_direction, - paragraph_attrs.text_transform, - paragraph_attrs.line_height, - paragraph_attrs.letter_spacing, + paragraph.num_leaves, + paragraph.text_align, + paragraph.text_direction, + paragraph.text_decoration, + paragraph.text_transform, + paragraph.line_height, + paragraph.letter_spacing, typography_ref_file, typography_ref_id, text_leaves.clone(), @@ -557,3 +600,54 @@ pub fn auto_height(paragraphs: &[Vec]) -> f32 { .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 +} diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs index 3b51efba0..17992f82a 100644 --- a/render-wasm/src/utils.rs +++ b/render-wasm/src/utils.rs @@ -1,4 +1,7 @@ +use crate::skia::Image; use crate::uuid::Uuid; +use crate::with_state; +use crate::STATE; pub fn uuid_from_u32_quartet(a: u32, b: u32, c: u32, d: u32) -> Uuid { let hi: u64 = ((a as u64) << 32) | b as u64; @@ -18,3 +21,7 @@ pub fn uuid_to_u32_quartet(id: &Uuid) -> (u32, u32, u32, u32) { pub fn uuid_from_u32(id: [u32; 4]) -> Uuid { uuid_from_u32_quartet(id[0], id[1], id[2], id[3]) } + +pub fn get_image(image_id: &Uuid) -> Option<&Image> { + with_state!(state, { state.render_state().images.get(image_id) }) +} diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 7be256d77..f1cef8eb1 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -53,6 +53,18 @@ impl TryFrom<&[u8]> for RawFillData { } } +pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec { + buffer + .chunks_exact(RAW_FILL_DATA_SIZE) + .take(num_fills) + .map(|bytes| { + RawFillData::try_from(bytes) + .expect("Invalid fill data") + .into() + }) + .collect() +} + #[no_mangle] pub extern "C" fn add_shape_fill() { with_current_shape!(state, |shape: &mut Shape| {