diff --git a/.editorconfig b/.editorconfig index 1dec4b93c1..dc592e36cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,9 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[*.{rs}] +indent_size = 4 +indent_style = space + +end_of_line = lf diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index f0a9d14a08..b916688d07 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -128,11 +128,13 @@ (defn svg-raw-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) - svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] + svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] (if (and (map? (:content shape)) + ;; tspan shouldn't be contained in a group or have svg defs + (not= :tspan (get-in shape [:content :tag])) (or (= :svg (get-in shape [:content :tag])) (contains? shape :svg-attrs))) [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs index 1d6d009666..a1a451c8f8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs @@ -13,15 +13,25 @@ [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] + [app.util.functions :as uf] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] - (let [handle-change + (let [last-value (mf/use-state value) + + handle-change* (mf/use-fn - (mf/deps attr on-change) + (uf/debounce (fn [val] + (on-change attr val)) + 300)) + + handle-change + (mf/use-fn + (mf/deps attr on-change handle-change*) (fn [event] - (on-change attr (dom/get-target-val event)))) + (reset! last-value (dom/get-target-val event)) + (handle-change* (dom/get-target-val event)))) handle-delete (mf/use-fn @@ -35,7 +45,7 @@ [:div {:class (stl/css :attr-content)} [:span {:class (stl/css :attr-name)} label] [:div {:class (stl/css :attr-input)} - [:input {:value value + [:input {:value @last-value :on-change handle-change}]] [:div {:class (stl/css :attr-actions)} [:button {:class (stl/css :attr-action-btn) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 01c14a0ec4..de3a875d65 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -7,19 +7,24 @@ (ns app.render-wasm.api "A WASM based render API" (:require + ["react-dom/server" :as rds] [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.svg.path :as path] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.refs :as refs] + [app.main.render :as render] [app.render-wasm.helpers :as h] [app.util.debug :as dbg] [app.util.functions :as fns] [app.util.http :as http] [app.util.webapi :as wapi] [beicon.v2.core :as rx] + [cuerdas.core :as str] [goog.object :as gobj] - [promesa.core :as p])) + [promesa.core :as p] + [rumext.v2 :as mf])) (defonce internal-frame-id nil) (defonce internal-module #js {}) @@ -28,6 +33,27 @@ (def dpr (if use-dpr? js/window.devicePixelRatio 1.0)) +;; Based on app.main.render/object-svg +(mf/defc object-svg + {::mf/props :obj} + [{:keys [shape] :as props}] + (let [objects (mf/deref refs/workspace-page-objects) + shape-wrapper + (mf/with-memo [shape] + (render/shape-wrapper-factory objects))] + + [:svg {:version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + [:& shape-wrapper {:shape shape}]])) + +(defn get-static-markup + [shape] + (-> + (mf/element object-svg #js {:shape shape}) + (rds/renderToStaticMarkup))) + ;; This should never be called from the outside. ;; This function receives a "time" parameter that we're not using but maybe in the future could be useful (it is the time since ;; the window started rendering elements so it could be useful to measure time between frames). @@ -134,6 +160,36 @@ (aget buffer 3)))) shape-ids)) +(defn- get-string-length [string] (+ (count string) 1)) + +;; IMPORTANT: It should be noted that only TTF fonts can be stored. +;; Do not remove, this is going to be useful +;; when we implement text rendering. +#_(defn- store-font + [family-name font-array-buffer] + (let [family-name-size (get-string-length family-name) + font-array-buffer-size (.-byteLength font-array-buffer) + size (+ font-array-buffer-size family-name-size) + ptr (h/call internal-module "_alloc_bytes" size) + family-name-ptr (+ ptr font-array-buffer-size) + heap (gobj/get ^js internal-module "HEAPU8") + mem (js/Uint8Array. (.-buffer heap) ptr size)] + (.set mem (js/Uint8Array. font-array-buffer)) + (h/call internal-module "stringToUTF8" family-name family-name-ptr family-name-size) + (h/call internal-module "_store_font" family-name-size font-array-buffer-size))) + +;; This doesn't work +#_(store-font-url "roboto-thin-italic" "https://fonts.gstatic.com/s/roboto/v32/KFOiCnqEu92Fr1Mu51QrEzAdLw.woff2") +;; This does +#_(store-font-url "sourcesanspro-regular" "http://localhost:3449/fonts/sourcesanspro-regular.ttf") +;; Do not remove, this is going to be useful +;; when we implement text rendering. +#_(defn- store-font-url + [family-name font-url] + (-> (p/then (js/fetch font-url) + (fn [response] (.arrayBuffer response))) + (p/then (fn [array-buffer] (store-font family-name array-buffer))))) + (defn- store-image [id] (let [buffer (uuid/get-u32 id) @@ -302,16 +358,44 @@ (h/call internal-module "_add_shape_stroke_solid_fill" rgba))))) strokes)) +(defn serialize-path-attrs + [svg-attrs] + (reduce + (fn [acc [key value]] + (str/concat + acc + (str/kebab key) "\0" + value "\0")) "" svg-attrs)) + +(defn set-shape-path-attrs + [attrs] + (let [style (:style attrs) + attrs (-> attrs + (dissoc :style) + (merge style)) + str (serialize-path-attrs attrs) + size (count str) + ptr (h/call internal-module "_alloc_bytes" size)] + (h/call internal-module "stringToUTF8" str ptr size) + (h/call internal-module "_set_shape_path_attrs" (count attrs)))) + (defn set-shape-path-content [content] - (let [buffer (path/content->buffer content) - size (.-byteLength buffer) - ptr (h/call internal-module "_alloc_bytes" size) + (let [buffer (path/content->buffer content) + size (.-byteLength buffer) + ptr (h/call internal-module "_alloc_bytes" size) heap (gobj/get ^js internal-module "HEAPU8") mem (js/Uint8Array. (.-buffer heap) ptr size)] (.set mem (js/Uint8Array. buffer)) (h/call internal-module "_set_shape_path_content"))) +(defn set-shape-svg-raw-content + [content] + (let [size (get-string-length content) + ptr (h/call internal-module "_alloc_bytes" size)] + (h/call internal-module "stringToUTF8" content ptr size) + (h/call internal-module "_set_shape_svg_raw_content"))) + (defn- translate-blend-mode [blend-mode] (case blend-mode @@ -427,7 +511,8 @@ (dm/get-prop shape :r2) (dm/get-prop shape :r3) (dm/get-prop shape :r4)]) - bool-content (dm/get-prop shape :bool-content)] + bool-content (dm/get-prop shape :bool-content) + svg-attrs (dm/get-prop shape :svg-attrs)] (use-shape id) (set-shape-type type) @@ -436,12 +521,16 @@ (set-shape-rotation rotation) (set-shape-transform transform) (set-shape-blend-mode blend-mode) - (set-shape-children children) (set-shape-opacity opacity) (set-shape-hidden hidden) + (set-shape-children children) (when (some? blur) (set-shape-blur blur)) - (when (and (some? content) (= type :path)) (set-shape-path-content content)) + (when (and (some? content) (= type :path)) + (set-shape-path-attrs svg-attrs) + (set-shape-path-content content)) + (when (and (some? content) (= type :svg-raw)) + (set-shape-svg-raw-content (get-static-markup shape))) (when (some? bool-content) (set-shape-bool-content bool-content)) (when (some? corners) (set-shape-corners corners)) (let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 2c6a8ebfe2..0e76ddefcb 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -124,8 +124,15 @@ :opacity (api/set-shape-opacity v) :hidden (api/set-shape-hidden v) :shapes (api/set-shape-children v) - :content (when (= (:type self) :path) (api/set-shape-path-content v)) :blur (api/set-shape-blur v) + :svg-attrs (when (= (:type self) :path) + (api/set-shape-path-attrs v)) + :content (cond + (= (:type self) :path) + (api/set-shape-path-content v) + + (= (:type self) :svg-raw) + (api/set-shape-svg-raw-content (api/get-static-markup self))) nil) ;; when something synced with wasm ;; is modified, we need to request diff --git a/render-wasm/build b/render-wasm/build index 1f8c7c6e77..355d4e5841 100755 --- a/render-wasm/build +++ b/render-wasm/build @@ -16,7 +16,7 @@ export EMCC_CFLAGS="--no-entry \ -sMAX_WEBGL_VERSION=2 \ -sMODULARIZE=1 \ -sEXPORT_NAME=createRustSkiaModule \ - -sEXPORTED_RUNTIME_METHODS=GL \ + -sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8 \ -sEXPORT_ES6=1" EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh; diff --git a/render-wasm/src/fonts/RobotoMono-Regular.ttf b/render-wasm/src/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000000..6df2b25360 Binary files /dev/null and b/render-wasm/src/fonts/RobotoMono-Regular.ttf differ diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 461b22d198..972f608bc4 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -48,7 +48,7 @@ pub extern "C" fn clean_up() { #[no_mangle] pub extern "C" fn set_render_options(debug: u32, dpr: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let render_state = state.render_state(); render_state.set_debug_flags(debug); @@ -65,13 +65,13 @@ pub extern "C" fn set_canvas_background(raw_color: u32) { #[no_mangle] pub unsafe extern "C" fn render() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_all(true); } #[no_mangle] pub unsafe extern "C" fn render_without_cache() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_all(false); } @@ -90,44 +90,44 @@ pub unsafe extern "C" fn pan() { #[no_mangle] pub extern "C" fn reset_canvas() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_state().reset_canvas(); } #[no_mangle] pub extern "C" fn resize_viewbox(width: i32, height: i32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.resize(width, height); } #[no_mangle] pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_state().viewbox.set_all(zoom, x, y); } #[no_mangle] pub extern "C" fn set_view_zoom(zoom: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_state().viewbox.set_zoom(zoom); } #[no_mangle] pub extern "C" fn set_view_xy(x: f32, y: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_state().viewbox.set_pan_xy(x, y); } #[no_mangle] pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); state.use_shape(id); } #[no_mangle] pub unsafe extern "C" fn set_shape_kind_circle() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_kind(Kind::Circle(math::Rect::new_empty())); @@ -136,7 +136,7 @@ pub unsafe extern "C" fn set_shape_kind_circle() { #[no_mangle] pub unsafe extern "C" fn set_shape_kind_rect() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { match shape.kind() { @@ -148,7 +148,7 @@ pub unsafe extern "C" fn set_shape_kind_rect() { #[no_mangle] pub unsafe extern "C" fn set_shape_kind_path() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_kind(Kind::Path(Path::default())); } @@ -175,7 +175,7 @@ pub unsafe extern "C" fn set_shape_bool_type(raw_bool_type: u8) { #[no_mangle] pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_selrect(left, top, right, bottom); } @@ -183,7 +183,7 @@ pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32 #[no_mangle] pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_clip(clip_content); } @@ -191,7 +191,7 @@ pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { #[no_mangle] pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_rotation(rotation); } @@ -199,7 +199,7 @@ pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { #[no_mangle] pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_transform(a, b, c, d, e, f); } @@ -207,7 +207,7 @@ pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: #[no_mangle] pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); if let Some(shape) = state.current_shape() { shape.add_child(id); @@ -216,7 +216,7 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { #[no_mangle] pub extern "C" fn clear_shape_children() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.clear_children(); } @@ -224,7 +224,7 @@ pub extern "C" fn clear_shape_children() { #[no_mangle] pub extern "C" fn add_shape_solid_fill(raw_color: u32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { let color = skia::Color::new(raw_color); shape.add_fill(shapes::Fill::Solid(color)); @@ -239,7 +239,7 @@ pub extern "C" fn add_shape_linear_fill( end_y: f32, opacity: f32, ) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.add_fill(shapes::Fill::new_linear_gradient( (start_x, start_y), @@ -258,7 +258,7 @@ pub extern "C" fn add_shape_radial_fill( opacity: f32, width: f32, ) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.add_fill(shapes::Fill::new_radial_gradient( (start_x, start_y), @@ -271,7 +271,7 @@ pub extern "C" fn add_shape_radial_fill( #[no_mangle] pub extern "C" fn add_shape_fill_stops(ptr: *mut shapes::RawStopData, n_stops: u32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { let len = n_stops as usize; @@ -286,9 +286,30 @@ pub extern "C" fn add_shape_fill_stops(ptr: *mut shapes::RawStopData, n_stops: u } } +#[no_mangle] +pub extern "C" fn store_font(family_name_size: u32, font_size: u32) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + unsafe { + let font_bytes = + Vec::::from_raw_parts(mem::buffer_ptr(), font_size as usize, font_size as usize); + let family_name = String::from_raw_parts( + mem::buffer_ptr().add(font_size as usize), + family_name_size as usize, + family_name_size as usize, + ); + match state.render_state().add_font(family_name, &font_bytes) { + Err(msg) => { + eprintln!("{}", msg); + } + _ => {} + } + mem::free_bytes(); + } +} + #[no_mangle] pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32, size: u32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); unsafe { @@ -306,7 +327,7 @@ pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32, size: u32) { #[no_mangle] pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32) -> bool { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); state.render_state().has_image(&id) } @@ -321,7 +342,7 @@ pub extern "C" fn add_shape_image_fill( width: i32, height: i32, ) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); if let Some(shape) = state.current_shape() { shape.add_fill(shapes::Fill::new_image_fill( @@ -334,15 +355,30 @@ pub extern "C" fn add_shape_image_fill( #[no_mangle] pub extern "C" fn clear_shape_fills() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.clear_fills(); } } +#[no_mangle] +pub extern "C" fn set_shape_svg_raw_content() { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + let bytes = mem::bytes(); + let svg_raw_content = String::from_utf8(bytes) + .unwrap() + .trim_end_matches('\0') + .to_string(); + shape + .set_svg_raw_content(svg_raw_content) + .expect("Failed to set svg raw content"); + } +} + #[no_mangle] pub extern "C" fn set_shape_blend_mode(mode: i32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_blend_mode(render::BlendMode::from(mode)); } @@ -350,7 +386,7 @@ pub extern "C" fn set_shape_blend_mode(mode: i32) { #[no_mangle] pub extern "C" fn set_shape_opacity(opacity: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_opacity(opacity); } @@ -358,7 +394,7 @@ pub extern "C" fn set_shape_opacity(opacity: f32) { #[no_mangle] pub extern "C" fn set_shape_hidden(hidden: bool) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_hidden(hidden); } @@ -374,7 +410,7 @@ pub extern "C" fn set_shape_blur(blur_type: u8, hidden: bool, value: f32) { #[no_mangle] pub extern "C" fn set_shape_path_content() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { let bytes = mem::bytes(); @@ -488,6 +524,24 @@ pub extern "C" fn add_shape_stroke_stops(ptr: *mut shapes::RawStopData, n_stops: } } +// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`. +// Updates the `start` index to the end of the extracted string. +fn extract_string(start: &mut usize, bytes: &[u8]) -> String { + match bytes[*start..].iter().position(|&b| b == 0) { + Some(pos) => { + let end = *start + pos; + let slice = &bytes[*start..end]; + *start = end + 1; // Move the `start` pointer past the null byte + // Call to unsafe function within an unsafe block + unsafe { String::from_utf8_unchecked(slice.to_vec()) } + } + None => { + *start = bytes.len(); // Move `start` to the end if no null byte is found + String::new() + } + } +} + #[no_mangle] pub extern "C" fn add_shape_image_stroke( a: u32, @@ -523,7 +577,22 @@ pub extern "C" fn clear_shape_strokes() { pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { - shape.set_corners((r1, r2, r3, r4)) + shape.set_corners((r1, r2, r3, r4)); + } +} + +#[no_mangle] +pub extern "C" fn set_shape_path_attrs(num_attrs: u32) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + + if let Some(shape) = state.current_shape() { + let bytes = mem::bytes(); + let mut start = 0; + for _ in 0..num_attrs { + let name = extract_string(&mut start, &bytes); + let value = extract_string(&mut start, &bytes); + shape.set_path_attr(name, value); + } } } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index d242d9abb0..440056b876 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,65 +1,42 @@ -use std::collections::HashMap; - use skia::Contains; use skia_safe as skia; +use std::collections::HashMap; use uuid::Uuid; -use crate::math; use crate::view::Viewbox; mod blend; +mod cache; +mod debug; +mod fills; mod gpu_state; mod images; mod options; +mod strokes; +use crate::shapes::{Kind, Shape}; +use cache::CachedSurfaceImage; use gpu_state::GpuState; use options::RenderOptions; pub use blend::BlendMode; pub use images::*; -pub trait Renderable { - fn render( - &self, - surface: &mut skia::Surface, - images: &ImageStore, - scale: f32, - ) -> Result<(), String>; - fn blend_mode(&self) -> BlendMode; - fn opacity(&self) -> f32; - fn bounds(&self) -> math::Rect; - fn hidden(&self) -> bool; - fn clip(&self) -> bool; - fn children_ids(&self) -> Vec; - fn image_filter(&self, scale: f32) -> Option; -} - -pub(crate) struct CachedSurfaceImage { - pub image: Image, - pub viewbox: Viewbox, - has_all_shapes: bool, -} - -impl CachedSurfaceImage { - fn is_dirty_for_zooming(&mut self, viewbox: &Viewbox) -> bool { - !self.has_all_shapes && !self.viewbox.area.contains(viewbox.area) - } - - fn is_dirty_for_panning(&mut self, _viewbox: &Viewbox) -> bool { - !self.has_all_shapes - } -} - pub(crate) struct RenderState { gpu_state: GpuState, + options: RenderOptions, + + // TODO: Probably we're going to need + // a surface stack like the one used + // by SVG: https://www.w3.org/TR/SVG2/render.html pub final_surface: skia::Surface, pub drawing_surface: skia::Surface, pub debug_surface: skia::Surface, + pub font_provider: skia::textlayout::TypefaceFontProvider, pub cached_surface_image: Option, - options: RenderOptions, pub viewbox: Viewbox, - images: ImageStore, - background_color: skia::Color, + pub images: ImageStore, + pub background_color: skia::Color, } impl RenderState { @@ -74,12 +51,19 @@ impl RenderState { .new_surface_with_dimensions((width, height)) .unwrap(); + let mut font_provider = skia::textlayout::TypefaceFontProvider::new(); + let default_font = skia::FontMgr::default() + .new_from_data(include_bytes!("fonts/RobotoMono-Regular.ttf"), None) + .expect("Failed to load font"); + font_provider.register_typeface(default_font, "robotomono-regular"); + RenderState { gpu_state, final_surface, drawing_surface, debug_surface, cached_surface_image: None, + font_provider, options: RenderOptions::default(), viewbox: Viewbox::new(width as f32, height as f32), images: ImageStore::new(), @@ -87,6 +71,15 @@ impl RenderState { } } + pub fn add_font(&mut self, family_name: String, font_data: &[u8]) -> Result<(), String> { + let typeface = skia::FontMgr::default() + .new_from_data(font_data, None) + .expect("Failed to add font"); + self.font_provider + .register_typeface(typeface, family_name.as_ref()); + Ok(()) + } + pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { self.images.add(id, image_data) } @@ -161,11 +154,46 @@ impl RenderState { .reset_matrix(); } - pub fn render_single_element(&mut self, element: &impl Renderable) { - let scale = self.viewbox.zoom * self.options.dpr(); - element - .render(&mut self.drawing_surface, &self.images, scale) - .unwrap(); + pub fn render_shape(&mut self, shape: &mut Shape) { + let transform = shape.transform.to_skia_matrix(); + + // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc + let center = shape.bounds().center(); + let mut matrix = skia::Matrix::new_identity(); + matrix.pre_translate(center); + matrix.pre_concat(&transform); + matrix.pre_translate(-center); + + self.drawing_surface.canvas().concat(&matrix); + + match &shape.kind { + Kind::SVGRaw(sr) => { + if let Some(svg) = shape.svg.as_ref() { + svg.render(self.drawing_surface.canvas()) + } else { + let font_manager = skia::FontMgr::from(self.font_provider.clone()); + let dom_result = skia::svg::Dom::from_str(sr.content.to_string(), font_manager); + match dom_result { + Ok(dom) => { + dom.render(self.drawing_surface.canvas()); + shape.set_svg(dom); + } + Err(e) => { + eprintln!("Error parsing SVG. Error: {}", e); + } + } + } + } + _ => { + for fill in shape.fills().rev() { + fills::render(self, shape, fill); + } + + for stroke in shape.strokes().rev() { + strokes::render(self, shape, stroke); + } + } + }; self.drawing_surface.draw( &mut self.final_surface.canvas(), @@ -173,12 +201,13 @@ impl RenderState { skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), Some(&skia::Paint::default()), ); + self.drawing_surface .canvas() .clear(skia::Color::TRANSPARENT); } - pub fn zoom(&mut self, tree: &HashMap) -> Result<(), String> { + pub fn zoom(&mut self, tree: &HashMap) -> Result<(), String> { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { let is_dirty = cached_surface_image.is_dirty_for_zooming(&self.viewbox); if is_dirty { @@ -191,7 +220,7 @@ impl RenderState { Ok(()) } - pub fn pan(&mut self, tree: &HashMap) -> Result<(), String> { + pub fn pan(&mut self, tree: &HashMap) -> Result<(), String> { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { let is_dirty = cached_surface_image.is_dirty_for_panning(&self.viewbox); if is_dirty { @@ -204,11 +233,7 @@ impl RenderState { Ok(()) } - pub fn render_all( - &mut self, - tree: &HashMap, - generate_cached_surface_image: bool, - ) { + pub fn render_all(&mut self, tree: &HashMap, generate_cached_surface_image: bool) { self.reset_canvas(); self.scale( self.viewbox.zoom * self.options.dpr(), @@ -269,101 +294,64 @@ impl RenderState { Ok(()) } - fn render_debug_view(&mut self) { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); - paint.set_stroke_width(1.); - - let mut scaled_rect = self.viewbox.area.clone(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - - self.debug_surface.canvas().draw_rect(scaled_rect, &paint); - } - - fn render_debug_element(&mut self, element: &impl Renderable, intersected: bool) { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_color(if intersected { - skia::Color::from_argb(255, 255, 255, 0) - } else { - skia::Color::from_argb(255, 0, 255, 255) - }); - paint.set_stroke_width(1.); - - let mut scaled_rect = element.bounds(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - - self.debug_surface.canvas().draw_rect(scaled_rect, &paint); - } - fn render_debug(&mut self) { - let paint = skia::Paint::default(); - self.render_debug_view(); - self.debug_surface.draw( - &mut self.final_surface.canvas(), - (0.0, 0.0), - skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), - Some(&paint), - ); + debug::render(self); } // Returns a boolean indicating if the viewbox contains the rendered shapes - fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap) -> bool { - let element = tree.get(&root_id).unwrap(); - let mut is_complete = self.viewbox.area.contains(element.bounds()); + fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap) -> bool { + if let Some(element) = tree.get(&root_id) { + let mut is_complete = self.viewbox.area.contains(element.bounds()); - if !root_id.is_nil() { - if !element.bounds().intersects(self.viewbox.area) || element.hidden() { - self.render_debug_element(element, false); - // TODO: This means that not all the shapes are rendered so we - // need to call a render_all on the zoom out. - return is_complete; // TODO return is_complete or return false?? - } else { - self.render_debug_element(element, true); + if !root_id.is_nil() { + if !element.bounds().intersects(self.viewbox.area) || element.hidden() { + debug::render_debug_element(self, element, false); + // TODO: This means that not all the shapes are rendered so we + // need to call a render_all on the zoom out. + return is_complete; // TODO return is_complete or return false?? + } else { + debug::render_debug_element(self, element, true); + } } - } - let mut paint = skia::Paint::default(); - paint.set_blend_mode(element.blend_mode().into()); - paint.set_alpha_f(element.opacity()); - let filter = element.image_filter(self.viewbox.zoom * self.options.dpr()); - if let Some(image_filter) = filter { - paint.set_image_filter(image_filter); - } - - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - // This is needed so the next non-children shape does not carry this shape's transform - self.final_surface.canvas().save_layer(&layer_rec); - self.drawing_surface.canvas().save(); - - if !root_id.is_nil() { - self.render_single_element(element); - if element.clip() { - self.drawing_surface.canvas().clip_rect( - element.bounds(), - skia::ClipOp::Intersect, - true, - ); + let mut paint = skia::Paint::default(); + paint.set_blend_mode(element.blend_mode().into()); + paint.set_alpha_f(element.opacity()); + let filter = element.image_filter(self.viewbox.zoom * self.options.dpr()); + if let Some(image_filter) = filter { + paint.set_image_filter(image_filter); } + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + // This is needed so the next non-children shape does not carry this shape's transform + self.final_surface.canvas().save_layer(&layer_rec); + self.drawing_surface.canvas().save(); + + if !root_id.is_nil() { + self.render_shape(&mut element.clone()); + if element.clip() { + self.drawing_surface.canvas().clip_rect( + element.bounds(), + skia::ClipOp::Intersect, + true, + ); + } + } + + // draw all the children shapes + if element.is_recursive() { + for id in element.children_ids() { + is_complete = self.render_shape_tree(&id, tree) && is_complete; + } + } + + self.final_surface.canvas().restore(); + self.drawing_surface.canvas().restore(); + + return is_complete; + } else { + eprintln!("Error: Element with root_id {root_id} not found in the tree."); + return false; } - - // draw all the children shapes - for id in element.children_ids() { - is_complete = self.render_shape_tree(&id, tree) && is_complete; - } - - self.final_surface.canvas().restore(); - self.drawing_surface.canvas().restore(); - - return is_complete; } } diff --git a/render-wasm/src/render/cache.rs b/render-wasm/src/render/cache.rs new file mode 100644 index 0000000000..5cafe9db94 --- /dev/null +++ b/render-wasm/src/render/cache.rs @@ -0,0 +1,19 @@ +use super::{Image, Viewbox}; +use skia::Contains; +use skia_safe as skia; + +pub(crate) struct CachedSurfaceImage { + pub image: Image, + pub viewbox: Viewbox, + pub has_all_shapes: bool, +} + +impl CachedSurfaceImage { + pub fn is_dirty_for_zooming(&mut self, viewbox: &Viewbox) -> bool { + !self.has_all_shapes && !self.viewbox.area.contains(viewbox.area) + } + + pub fn is_dirty_for_panning(&mut self, _viewbox: &Viewbox) -> bool { + !self.has_all_shapes + } +} diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs new file mode 100644 index 0000000000..5e5363d491 --- /dev/null +++ b/render-wasm/src/render/debug.rs @@ -0,0 +1,57 @@ +use crate::shapes::Shape; +use skia_safe as skia; + +use super::RenderState; + +fn render_debug_view(render_state: &mut RenderState) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); + paint.set_stroke_width(1.); + + let mut scaled_rect = render_state.viewbox.area.clone(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + + render_state + .debug_surface + .canvas() + .draw_rect(scaled_rect, &paint); +} + +pub fn render_debug_element(render_state: &mut RenderState, element: &Shape, intersected: bool) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(if intersected { + skia::Color::from_argb(255, 255, 255, 0) + } else { + skia::Color::from_argb(255, 0, 255, 255) + }); + paint.set_stroke_width(1.); + + let mut scaled_rect = element.bounds(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + + render_state + .debug_surface + .canvas() + .draw_rect(scaled_rect, &paint); +} + +pub fn render(render_state: &mut RenderState) { + let paint = skia::Paint::default(); + render_debug_view(render_state); + render_state.debug_surface.draw( + &mut render_state.final_surface.canvas(), + (0.0, 0.0), + skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), + Some(&paint), + ); +} diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs new file mode 100644 index 0000000000..9beb474d7a --- /dev/null +++ b/render-wasm/src/render/fills.rs @@ -0,0 +1,121 @@ +use crate::{ + math, + shapes::{Fill, ImageFill, Kind, Shape}, +}; +use skia_safe::{self as skia, RRect}; + +use super::RenderState; + +fn draw_image_fill_in_container( + render_state: &mut RenderState, + shape: &Shape, + fill: &Fill, + image_fill: &ImageFill, +) { + let image = render_state.images.get(&image_fill.id()); + if image.is_none() { + return; + } + + let size = image_fill.size(); + let canvas = render_state.drawing_surface.canvas(); + let kind = &shape.kind; + let container = &shape.selrect; + let path_transform = shape.to_path_transform(); + let paint = fill.to_paint(container); + + let width = size.0 as f32; + let height = size.1 as f32; + let image_aspect_ratio = width / height; + + // Container size + let container_width = container.width(); + let container_height = container.height(); + let container_aspect_ratio = container_width / container_height; + + // Calculate scale to ensure the image covers the container + let scale = if image_aspect_ratio > container_aspect_ratio { + // Image is wider, scale based on height to cover container + container_height / height + } else { + // Image is taller, scale based on width to cover container + container_width / width + }; + + // Scaled size of the image + let scaled_width = width * scale; + let scaled_height = height * scale; + + let dest_rect = math::Rect::from_xywh( + container.left - (scaled_width - container_width) / 2.0, + container.top - (scaled_height - container_height) / 2.0, + scaled_width, + scaled_height, + ); + + // Save the current canvas state + canvas.save(); + + // Set the clipping rectangle to the container bounds + match kind { + Kind::Rect(_, _) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } + Kind::Circle(_) => { + let mut oval_path = skia::Path::new(); + oval_path.add_oval(container, None); + canvas.clip_path(&oval_path, skia::ClipOp::Intersect, true); + } + Kind::Path(path) | Kind::Bool(_, path) => { + canvas.clip_path( + &path.to_skia_path().transform(&path_transform.unwrap()), + skia::ClipOp::Intersect, + true, + ); + } + Kind::SVGRaw(_) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } + } + + // Draw the image with the calculated destination rectangle + canvas.draw_image_rect(image.unwrap(), None, dest_rect, &paint); + + // Restore the canvas to remove the clipping + canvas.restore(); +} + +/** + * This SHOULD be the only public function in this module. + */ +pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill) { + let canvas = render_state.drawing_surface.canvas(); + let selrect = shape.selrect; + let path_transform = shape.to_path_transform(); + let kind = &shape.kind; + match (fill, kind) { + (Fill::Image(image_fill), _) => { + draw_image_fill_in_container(render_state, shape, fill, image_fill); + } + (_, Kind::Rect(rect, None)) => { + canvas.draw_rect(rect, &fill.to_paint(&selrect)); + } + (_, Kind::Rect(rect, Some(corners))) => { + let rrect = RRect::new_rect_radii(rect, &corners); + canvas.draw_rrect(rrect, &fill.to_paint(&selrect)); + } + (_, Kind::Circle(rect)) => { + canvas.draw_oval(rect, &fill.to_paint(&selrect)); + } + (_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => { + let svg_attrs = &shape.svg_attrs; + let mut skia_path = &mut path.to_skia_path(); + skia_path = skia_path.transform(&path_transform.unwrap()); + if let Some("evenodd") = svg_attrs.get("fill-rule").map(String::as_str) { + skia_path.set_fill_type(skia::PathFillType::EvenOdd); + } + canvas.draw_path(&skia_path, &fill.to_paint(&selrect)); + } + (_, _) => todo!(), + } +} diff --git a/render-wasm/src/shapes/renderable.rs b/render-wasm/src/render/strokes.rs similarity index 53% rename from render-wasm/src/shapes/renderable.rs rename to render-wasm/src/render/strokes.rs index b3e1610221..76b83f5062 100644 --- a/render-wasm/src/shapes/renderable.rs +++ b/render-wasm/src/render/strokes.rs @@ -1,199 +1,26 @@ +use std::collections::HashMap; + +use crate::math::{self, Rect}; +use crate::shapes::{Corners, Fill, ImageFill, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; use skia_safe::{self as skia, RRect}; -use uuid::Uuid; -use super::{BlurType, Corners, Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; -use crate::math::Rect; -use crate::render::{ImageStore, Renderable}; - -impl Renderable for Shape { - fn render( - &self, - surface: &mut skia_safe::Surface, - images: &ImageStore, - scale: f32, - ) -> Result<(), String> { - let transform = self.transform.to_skia_matrix(); - - // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc - let center = self.bounds().center(); - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_translate(center); - matrix.pre_concat(&transform); - matrix.pre_translate(-center); - - surface.canvas().concat(&matrix); - - for fill in self.fills().rev() { - render_fill( - surface, - images, - fill, - self.selrect, - &self.kind, - self.to_path_transform().as_ref(), - ); - } - - for stroke in self.strokes().rev() { - render_stroke( - scale, - surface, - images, - stroke, - self.selrect, - &self.kind, - self.to_path_transform().as_ref(), - ); - } - - Ok(()) - } - - fn blend_mode(&self) -> crate::render::BlendMode { - self.blend_mode - } - - fn opacity(&self) -> f32 { - self.opacity - } - - fn hidden(&self) -> bool { - self.hidden - } - - fn bounds(&self) -> Rect { - self.selrect - } - - fn clip(&self) -> bool { - self.clip_content - } - - fn children_ids(&self) -> Vec { - if let Kind::Bool(_, _) = self.kind { - vec![] - } else { - self.children.clone() - } - } - - fn image_filter(&self, scale: f32) -> Option { - if !self.blur.hidden { - match self.blur.blur_type { - BlurType::None => None, - BlurType::Layer => skia::image_filters::blur( - (self.blur.value * scale, self.blur.value * scale), - None, - None, - None, - ), - } - } else { - None - } - } -} - -fn render_fill( - surface: &mut skia::Surface, - images: &ImageStore, - fill: &Fill, - selrect: Rect, - kind: &Kind, - path_transform: Option<&skia::Matrix>, -) { - match (fill, kind) { - (Fill::Image(image_fill), kind) => { - let image = images.get(&image_fill.id()); - if let Some(image) = image { - draw_image_fill_in_container( - surface.canvas(), - &image, - image_fill.size(), - kind, - &fill.to_paint(&selrect), - &selrect, - path_transform, - ); - } - } - (_, Kind::Rect(rect, None)) => { - surface.canvas().draw_rect(rect, &fill.to_paint(&selrect)); - } - (_, Kind::Rect(rect, Some(corners))) => { - let rrect = RRect::new_rect_radii(rect, corners); - surface.canvas().draw_rrect(rrect, &fill.to_paint(&selrect)); - } - (_, Kind::Circle(rect)) => { - surface.canvas().draw_oval(rect, &fill.to_paint(&selrect)); - } - (_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => { - surface.canvas().draw_path( - &path.to_skia_path().transform(path_transform.unwrap()), - &fill.to_paint(&selrect), - ); - } - } -} - -fn render_stroke( - scale: f32, - surface: &mut skia::Surface, - images: &ImageStore, - stroke: &Stroke, - selrect: Rect, - kind: &Kind, - path_transform: Option<&skia::Matrix>, -) { - if let Fill::Image(image_fill) = &stroke.fill { - if let Some(image) = images.get(&image_fill.id()) { - draw_image_stroke_in_container( - surface.canvas(), - scale, - &image, - stroke, - image_fill.size(), - kind, - &selrect, - path_transform, - ); - } - } else { - match kind { - Kind::Rect(rect, corners) => { - draw_stroke_on_rect(surface.canvas(), scale, stroke, rect, &selrect, corners); - } - Kind::Circle(rect) => { - draw_stroke_on_circle(surface.canvas(), scale, stroke, rect, &selrect); - } - Kind::Path(path) | Kind::Bool(_, path) => { - draw_stroke_on_path( - surface.canvas(), - scale, - stroke, - path, - &selrect, - path_transform, - ); - } - } - } -} +use super::RenderState; fn draw_stroke_on_rect( canvas: &skia::Canvas, - scale: f32, stroke: &Stroke, rect: &Rect, selrect: &Rect, corners: &Option, + svg_attrs: &HashMap, + scale: f32, ) { // Draw the different kind of strokes for a rect is straightforward, we just need apply a stroke to: // - The same rect if it's a center stroke // - A bigger rect if it's an outer stroke // - A smaller rect if it's an outer stroke let stroke_rect = stroke.outer_rect(rect); - let paint = stroke.to_paint(selrect, scale); + let paint = stroke.to_paint(selrect, svg_attrs, scale); match corners { Some(radii) => { @@ -209,17 +36,63 @@ fn draw_stroke_on_rect( fn draw_stroke_on_circle( canvas: &skia::Canvas, - scale: f32, stroke: &Stroke, rect: &Rect, selrect: &Rect, + svg_attrs: &HashMap, + scale: f32, ) { // Draw the different kind of strokes for an oval is straightforward, we just need apply a stroke to: // - The same oval if it's a center stroke // - A bigger oval if it's an outer stroke // - A smaller oval if it's an outer stroke let stroke_rect = stroke.outer_rect(rect); - canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect, scale)); + canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect, svg_attrs, scale)); +} + +fn draw_stroke_on_path( + canvas: &skia::Canvas, + stroke: &Stroke, + path: &Path, + selrect: &Rect, + path_transform: Option<&skia::Matrix>, + svg_attrs: &HashMap, + scale: f32, +) { + let mut skia_path = path.to_skia_path(); + skia_path.transform(path_transform.unwrap()); + + let is_open = path.is_open(); + let paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale); + // Draw the different kind of strokes for a path requires different strategies: + match stroke.kind { + // 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::InnerStroke => { + canvas.clip_path(&skia_path, skia::ClipOp::Intersect, true); + canvas.draw_path(&skia_path, &paint_stroke); + } + // For center stroke we don't need to do anything extra + StrokeKind::CenterStroke => { + canvas.draw_path(&skia_path, &paint_stroke); + } + // 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::OuterStroke => { + let mut paint = skia::Paint::default(); + paint.set_blend_mode(skia::BlendMode::SrcOver); + paint.set_anti_alias(true); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + canvas.save_layer(&layer_rec); + + canvas.draw_path(&skia_path, &paint_stroke); + + let mut clear_paint = skia::Paint::default(); + clear_paint.set_blend_mode(skia::BlendMode::Clear); + clear_paint.set_anti_alias(true); + canvas.draw_path(&skia_path, &clear_paint); + + canvas.restore(); + } + } } fn handle_stroke_cap( @@ -262,12 +135,13 @@ fn handle_stroke_cap( } fn handle_stroke_caps( - scale: f32, path: &mut skia::Path, stroke: &Stroke, selrect: &Rect, canvas: &skia::Canvas, is_open: bool, + svg_attrs: &HashMap, + dpr_scale: f32, ) { let points_count = path.count_points(); let mut points = vec![skia::Point::default(); points_count]; @@ -277,7 +151,9 @@ fn handle_stroke_caps( if c_points >= 2 && is_open { let first_point = points.first().unwrap(); let last_point = points.last().unwrap(); - let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, scale); + + // let kind = stroke.render_kind(is_open); + let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, dpr_scale); handle_stroke_cap( canvas, @@ -412,46 +288,7 @@ fn draw_triangle_cap( canvas.draw_path(&path, paint); } -fn draw_stroke_on_path( - canvas: &skia::Canvas, - scale: f32, - stroke: &Stroke, - path: &Path, - selrect: &Rect, - path_transform: Option<&skia::Matrix>, -) { - let mut skia_path = path.to_skia_path(); - skia_path.transform(path_transform.unwrap()); - - let is_open = path.is_open(); - let kind = stroke.render_kind(is_open); - let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, scale); - // Draw the different kind of strokes for a path requires different strategies: - match kind { - // 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::InnerStroke => { - canvas.clip_path(&skia_path, skia::ClipOp::Intersect, true); - canvas.draw_path(&skia_path, &paint_stroke); - } - // For center stroke we don't need to do anything extra - StrokeKind::CenterStroke => { - canvas.draw_path(&skia_path, &paint_stroke); - handle_stroke_caps(scale, &mut skia_path, stroke, selrect, canvas, is_open); - } - // For inner stroke we draw a center stroke (with double width) and clip to the original path removing the extra inner stroke - StrokeKind::OuterStroke => { - canvas.save(); - canvas.clip_path(&skia_path, skia::ClipOp::Difference, true); - // Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts - canvas.draw_path(&skia_path, &paint_stroke); - canvas.restore(); - paint_stroke.set_stroke_width(1. / scale); - canvas.draw_path(&skia_path, &paint_stroke); - } - } -} - -fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect { +fn calculate_scaled_rect(size: (i32, i32), container: &math::Rect, delta: f32) -> math::Rect { let (width, height) = (size.0 as f32, size.1 as f32); let image_aspect_ratio = width / height; @@ -469,7 +306,7 @@ fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect let scaled_width = width * scale; let scaled_height = height * scale; - Rect::from_xywh( + math::Rect::from_xywh( container.left - delta - (scaled_width - container_width) / 2.0, container.top - delta - (scaled_height - container_height) / 2.0, scaled_width + (2. * delta) + (scaled_width - container_width), @@ -477,103 +314,25 @@ fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect ) } -pub fn draw_image_fill_in_container( - canvas: &skia::Canvas, - image: &Image, - size: (i32, i32), - kind: &Kind, - paint: &skia::Paint, - container: &Rect, - path_transform: Option<&skia::Matrix>, -) { - // Compute scaled rect - let dest_rect = calculate_scaled_rect(size, container, 0.); - - // Save the current canvas state - canvas.save(); - - // Set the clipping rectangle to the container bounds - match kind { - Kind::Rect(_, None) => { - canvas.clip_rect(container, skia::ClipOp::Intersect, true); - } - Kind::Rect(_, Some(corners)) => { - let rrect = RRect::new_rect_radii(container, corners); - canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true); - } - Kind::Circle(_) => { - let mut oval_path = skia::Path::new(); - oval_path.add_oval(container, None); - canvas.clip_path(&oval_path, skia::ClipOp::Intersect, true); - } - Kind::Path(p) | Kind::Bool(_, p) => { - canvas.clip_path( - &p.to_skia_path().transform(path_transform.unwrap()), - skia::ClipOp::Intersect, - true, - ); - } - } - - canvas.draw_image_rect(image, None, dest_rect, &paint); - - // Restore the canvas to remove the clipping - canvas.restore(); -} - -pub fn draw_image_stroke_in_container( - canvas: &skia::Canvas, - scale: f32, - image: &Image, +fn draw_image_stroke_in_container( + render_state: &mut RenderState, + shape: &Shape, stroke: &Stroke, - size: (i32, i32), - kind: &Kind, - container: &Rect, - path_transform: Option<&skia::Matrix>, + image_fill: &ImageFill, ) { - // Helper to handle drawing based on kind - fn draw_kind( - canvas: &skia::Canvas, - scale: f32, - kind: &Kind, - stroke: &Stroke, - container: &Rect, - path_transform: Option<&skia::Matrix>, - ) { - let outer_rect = stroke.outer_rect(container); - match kind { - Kind::Rect(rect, corners) => { - draw_stroke_on_rect(canvas, 1., stroke, rect, &outer_rect, corners) - } - Kind::Circle(rect) => draw_stroke_on_circle(canvas, 1., stroke, rect, &outer_rect), - Kind::Path(p) | Kind::Bool(_, p) => { - canvas.save(); - let mut path = p.to_skia_path(); - path.transform(path_transform.unwrap()); - let stroke_kind = stroke.render_kind(p.is_open()); - match stroke_kind { - StrokeKind::InnerStroke => { - canvas.clip_path(&path, skia::ClipOp::Intersect, true); - } - StrokeKind::CenterStroke => {} - StrokeKind::OuterStroke => { - canvas.clip_path(&path, skia::ClipOp::Difference, true); - } - } - let is_open = p.is_open(); - let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, scale); - canvas.draw_path(&path, &paint); - canvas.restore(); - if stroke.render_kind(is_open) == StrokeKind::OuterStroke { - // Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts - paint.set_stroke_width(1. / scale); - canvas.draw_path(&path, &paint); - } - handle_stroke_caps(scale, &mut path, stroke, &outer_rect, canvas, p.is_open()); - } - } + let image = render_state.images.get(&image_fill.id()); + if image.is_none() { + return; } + let size = image_fill.size(); + let canvas = render_state.drawing_surface.canvas(); + let kind = &shape.kind; + let container = &shape.selrect; + let path_transform = shape.to_path_transform(); + let svg_attrs = &shape.svg_attrs; + let dpr_scale = render_state.viewbox.zoom * render_state.options.dpr(); + // Save canvas and layer state let mut pb = skia::Paint::default(); pb.set_blend_mode(skia::BlendMode::SrcOver); @@ -582,8 +341,55 @@ pub fn draw_image_stroke_in_container( canvas.save_layer(&layer_rec); // Draw the stroke based on the kind, we are using this stroke as a "selector" of the area of the image we want to show. - draw_kind(canvas, scale, kind, stroke, container, path_transform); - + let outer_rect = stroke.outer_rect(container); + match kind { + Kind::Rect(rect, corners) => draw_stroke_on_rect( + canvas, + stroke, + rect, + &outer_rect, + corners, + svg_attrs, + dpr_scale, + ), + Kind::Circle(rect) => { + draw_stroke_on_circle(canvas, stroke, rect, &outer_rect, svg_attrs, dpr_scale) + } + Kind::SVGRaw(_) => todo!(), + Kind::Path(p) | Kind::Bool(_, p) => { + canvas.save(); + let mut path = p.to_skia_path(); + path.transform(&path_transform.unwrap()); + let stroke_kind = stroke.render_kind(p.is_open()); + match stroke_kind { + StrokeKind::InnerStroke => { + canvas.clip_path(&path, skia::ClipOp::Intersect, true); + } + StrokeKind::CenterStroke => {} + StrokeKind::OuterStroke => { + canvas.clip_path(&path, skia::ClipOp::Difference, true); + } + } + let is_open = p.is_open(); + let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, dpr_scale); + canvas.draw_path(&path, &paint); + canvas.restore(); + if stroke.render_kind(is_open) == StrokeKind::OuterStroke { + // Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts + paint.set_stroke_width(1. / dpr_scale); + canvas.draw_path(&path, &paint); + } + handle_stroke_caps( + &mut path, + stroke, + &outer_rect, + canvas, + p.is_open(), + svg_attrs, + dpr_scale, + ); + } + } // Draw the image. We are using now the SrcIn blend mode, so the rendered piece of image will the area of the stroke over the image. let mut image_paint = skia::Paint::default(); image_paint.set_blend_mode(skia::BlendMode::SrcIn); @@ -592,8 +398,57 @@ pub fn draw_image_stroke_in_container( // Compute scaled rect and clip to it let dest_rect = calculate_scaled_rect(size, container, stroke.delta()); canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, true); - canvas.draw_image_rect(image, None, dest_rect, &image_paint); + canvas.draw_image_rect(image.unwrap(), None, dest_rect, &image_paint); + + // Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area. + if let Kind::Path(p) = kind { + if stroke.render_kind(p.is_open()) == StrokeKind::OuterStroke { + let mut path = p.to_skia_path(); + path.transform(&path_transform.unwrap()); + let mut clear_paint = skia::Paint::default(); + clear_paint.set_blend_mode(skia::BlendMode::Clear); + clear_paint.set_anti_alias(true); + canvas.draw_path(&path, &clear_paint); + } + } // Restore canvas state canvas.restore(); } + +/** + * This SHOULD be the only public function in this module. + */ +pub fn render(render_state: &mut RenderState, shape: &Shape, stroke: &Stroke) { + let canvas = render_state.drawing_surface.canvas(); + let dpr_scale = render_state.viewbox.zoom * render_state.options.dpr(); + let selrect = shape.selrect; + let path_transform = shape.to_path_transform(); + let kind = &shape.kind; + let svg_attrs = &shape.svg_attrs; + if let Fill::Image(image_fill) = &stroke.fill { + draw_image_stroke_in_container(render_state, shape, stroke, image_fill); + } else { + match kind { + Kind::Rect(rect, corners) => draw_stroke_on_rect( + canvas, stroke, rect, &selrect, corners, svg_attrs, dpr_scale, + ), + Kind::Circle(rect) => { + draw_stroke_on_circle(canvas, stroke, rect, &selrect, &svg_attrs, dpr_scale) + } + Kind::Path(path) | Kind::Bool(_, path) => { + let svg_attrs = &shape.svg_attrs; + draw_stroke_on_path( + canvas, + stroke, + path, + &selrect, + path_transform.as_ref(), + svg_attrs, + dpr_scale, + ); + } + Kind::SVGRaw(_) => todo!(), + } + } +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 2f29d1f1d4..84d01dc515 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,25 +1,25 @@ use crate::math; use skia_safe as skia; +use std::collections::HashMap; use uuid::Uuid; -use crate::render::{BlendMode, Renderable}; +use crate::render::BlendMode; mod blurs; mod bools; mod fills; -mod images; mod matrix; mod paths; -mod renderable; mod strokes; +mod svgraw; pub use blurs::*; pub use bools::*; pub use fills::*; -pub use images::*; use matrix::*; pub use paths::*; pub use strokes::*; +pub use svgraw::*; pub type CornerRadius = skia::Point; pub type Corners = [CornerRadius; 4]; @@ -30,6 +30,7 @@ pub enum Kind { Circle(math::Rect), Path(Path), Bool(BoolType, Path), + SVGRaw(SVGRaw), } pub type Color = skia::Color; @@ -37,19 +38,21 @@ pub type Color = skia::Color; #[derive(Debug, Clone)] #[allow(dead_code)] pub struct Shape { - id: Uuid, - children: Vec, - kind: Kind, - selrect: math::Rect, - transform: Matrix, - rotation: f32, - clip_content: bool, - fills: Vec, - strokes: Vec, - blend_mode: BlendMode, - blur: Blur, - opacity: f32, - hidden: bool, + pub id: Uuid, + pub children: Vec, + pub kind: Kind, + pub selrect: math::Rect, + pub transform: Matrix, + pub rotation: f32, + pub clip_content: bool, + pub fills: Vec, + pub strokes: Vec, + pub blend_mode: BlendMode, + pub blur: Blur, + pub opacity: f32, + pub hidden: bool, + pub svg: Option, + pub svg_attrs: HashMap, } impl Shape { @@ -68,6 +71,8 @@ impl Shape { opacity: 1., hidden: false, blur: Blur::default(), + svg: None, + svg_attrs: HashMap::new(), } } @@ -196,6 +201,20 @@ impl Shape { Ok(()) } + pub fn set_path_attr(&mut self, name: String, value: String) { + match &mut self.kind { + Kind::Path(_) => { + self.set_svg_attr(name, value); + } + Kind::Rect(_, _) | Kind::Circle(_) | Kind::SVGRaw(_) | Kind::Bool(_, _) => todo!(), + }; + } + + pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> { + self.kind = Kind::SVGRaw(SVGRaw::from_content(content)); + Ok(()) + } + pub fn set_blend_mode(&mut self, mode: BlendMode) { self.blend_mode = mode; } @@ -230,7 +249,63 @@ impl Shape { self.kind = Kind::Rect(self.selrect, corners); } - fn to_path_transform(&self) -> Option { + pub fn set_svg(&mut self, svg: skia::svg::Dom) { + self.svg = Some(svg); + } + + pub fn set_svg_attr(&mut self, name: String, value: String) { + self.svg_attrs.insert(name, value); + } + + pub fn blend_mode(&self) -> crate::render::BlendMode { + self.blend_mode + } + + pub fn opacity(&self) -> f32 { + self.opacity + } + + pub fn hidden(&self) -> bool { + self.hidden + } + + pub fn bounds(&self) -> math::Rect { + self.selrect + } + + pub fn clip(&self) -> bool { + self.clip_content + } + + pub fn children_ids(&self) -> Vec { + if let Kind::Bool(_, _) = self.kind { + vec![] + } else { + self.children.clone() + } + } + + pub fn image_filter(&self, scale: f32) -> Option { + if !self.blur.hidden { + match self.blur.blur_type { + BlurType::None => None, + BlurType::Layer => skia::image_filters::blur( + (self.blur.value * scale, self.blur.value * scale), + None, + None, + None, + ), + } + } else { + None + } + } + + pub fn is_recursive(&self) -> bool { + !matches!(self.kind, Kind::SVGRaw(_)) + } + + pub fn to_path_transform(&self) -> Option { match self.kind { Kind::Path(_) | Kind::Bool(_, _) => { let center = self.bounds().center(); diff --git a/render-wasm/src/shapes/images.rs b/render-wasm/src/shapes/images.rs deleted file mode 100644 index bbf5d600c3..0000000000 --- a/render-wasm/src/shapes/images.rs +++ /dev/null @@ -1,3 +0,0 @@ -use skia_safe as skia; - -pub type Image = skia::Image; diff --git a/render-wasm/src/shapes/rects.rs b/render-wasm/src/shapes/rects.rs deleted file mode 100644 index e9a6b3fd84..0000000000 --- a/render-wasm/src/shapes/rects.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::math::Point; - -#[derive(Debug, Clone, PartialEq)] -pub struct Rect { - -} diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 28562b2bd9..52d992096b 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,6 +1,7 @@ use crate::math; use crate::shapes::fills::Fill; use skia_safe as skia; +use std::collections::HashMap; use super::Corners; @@ -64,7 +65,7 @@ pub struct Stroke { pub style: StrokeStyle, pub cap_end: StrokeCap, pub cap_start: StrokeCap, - kind: StrokeKind, + pub kind: StrokeKind, } impl Stroke { @@ -155,7 +156,12 @@ impl Stroke { outer } - pub fn to_paint(&self, rect: &math::Rect, scale: f32) -> skia::Paint { + pub fn to_paint( + &self, + rect: &math::Rect, + svg_attrs: &HashMap, + scale: f32, + ) -> skia::Paint { let mut paint = self.fill.to_paint(rect); paint.set_style(skia::PaintStyle::Stroke); @@ -168,6 +174,14 @@ impl Stroke { paint.set_stroke_width(width); paint.set_anti_alias(true); + if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) { + paint.set_stroke_cap(skia::paint::Cap::Round); + } + + if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) { + paint.set_stroke_join(skia::paint::Join::Round); + } + if self.style != StrokeStyle::Solid { let path_effect = match self.style { StrokeStyle::Dotted => { @@ -206,8 +220,14 @@ impl Stroke { paint } - pub fn to_stroked_paint(&self, is_open: bool, rect: &math::Rect, scale: f32) -> skia::Paint { - let mut paint = self.to_paint(rect, scale); + pub fn to_stroked_paint( + &self, + is_open: bool, + rect: &math::Rect, + svg_attrs: &HashMap, + scale: f32, + ) -> skia::Paint { + let mut paint = self.to_paint(rect, svg_attrs, scale); match self.render_kind(is_open) { StrokeKind::InnerStroke => { paint.set_stroke_width(2. * paint.stroke_width()); diff --git a/render-wasm/src/shapes/svgraw.rs b/render-wasm/src/shapes/svgraw.rs new file mode 100644 index 0000000000..408a92d2a7 --- /dev/null +++ b/render-wasm/src/shapes/svgraw.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, PartialEq)] +pub struct SVGRaw { + pub content: String, +} + +impl SVGRaw { + pub fn from_content(svg: String) -> SVGRaw { + SVGRaw { content: svg } + } +}