🎉 Basic wasm support for svg attrs and svg defs

This commit is contained in:
Alejandro Alonso 2024-12-26 13:16:37 +01:00 committed by AzazelN28
parent 751df46dc9
commit 79df616108
12 changed files with 452 additions and 60 deletions

View file

@ -128,11 +128,13 @@
(defn svg-raw-wrapper-factory (defn svg-raw-wrapper-factory
[objects] [objects]
(let [shape-wrapper (shape-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 (mf/fnc svg-raw-wrapper
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))] (let [childs (mapv #(get objects %) (:shapes shape))]
(if (and (map? (:content 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])) (or (= :svg (get-in shape [:content :tag]))
(contains? shape :svg-attrs))) (contains? shape :svg-attrs)))
[:> shape-container {:shape shape} [:> shape-container {:shape shape}

View file

@ -13,15 +13,25 @@
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] (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/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] (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 handle-delete
(mf/use-fn (mf/use-fn
@ -35,7 +45,7 @@
[:div {:class (stl/css :attr-content)} [:div {:class (stl/css :attr-content)}
[:span {:class (stl/css :attr-name)} label] [:span {:class (stl/css :attr-name)} label]
[:div {:class (stl/css :attr-input)} [:div {:class (stl/css :attr-input)}
[:input {:value value [:input {:value @last-value
:on-change handle-change}]] :on-change handle-change}]]
[:div {:class (stl/css :attr-actions)} [:div {:class (stl/css :attr-actions)}
[:button {:class (stl/css :attr-action-btn) [:button {:class (stl/css :attr-action-btn)

View file

@ -7,19 +7,26 @@
(ns app.render-wasm.api (ns app.render-wasm.api
"A WASM based render API" "A WASM based render API"
(:require (:require
["react-dom/server" :as rds]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.svg.path :as path] [app.common.svg.path :as path]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.refs :as refs]
[app.main.render :as render]
[app.render-wasm.helpers :as h] [app.render-wasm.helpers :as h]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.functions :as fns] [app.util.functions :as fns]
[app.util.http :as http] [app.util.http :as http]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.object :as gobj] [goog.object :as gobj]
[promesa.core :as p])) [promesa.core :as p]
[rumext.v2 :as mf]))
(defonce internal-frame-id nil) (defonce internal-frame-id nil)
(defonce internal-module #js {}) (defonce internal-module #js {})
@ -28,6 +35,54 @@
(def dpr (def dpr
(if use-dpr? js/window.devicePixelRatio 1.0)) (if use-dpr? js/window.devicePixelRatio 1.0))
;; (mf/defc svg-raw-element
;; {::mf/props :obj}
;; [{:keys [tag attrs content] :as props}]
;; [:& (name tag) attrs
;; (for [child content]
;; (if (string? child)
;; child
;; [:& svg-raw-element child]))])
;; (mf/defc svg-raw
;; {::mf/props :obj}
;; [{:keys [shape] :as props}]
;; (let [content (:content shape)]
;; [:svg {:version "1.1"
;; :xmlns "http://www.w3.org/2000/svg"
;; :xmlnsXlink "http://www.w3.org/1999/xlink"
;; :fill "none"}
;; (if (string? content)
;; content
;; (let [svg-attrs (:svg-attrs shape)
;; content (->
;; (:content shape)
;; (update :attrs merge svg-attrs))]
;; (println "content" content)
;; (println "svg-attrs" svg-attrs)
;; [:& svg-raw-element content]))]))
;; 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 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 ;; 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). ;; the window started rendering elements so it could be useful to measure time between frames).
@ -328,16 +383,44 @@
(h/call internal-module "_add_shape_stroke_solid_fill" rgba))))) (h/call internal-module "_add_shape_stroke_solid_fill" rgba)))))
strokes)) 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 (defn set-shape-path-content
[content] [content]
(let [buffer (path/content->buffer content) (let [buffer (path/content->buffer content)
size (.-byteLength buffer) size (.-byteLength buffer)
ptr (h/call internal-module "_alloc_bytes" size) ptr (h/call internal-module "_alloc_bytes" size)
heap (gobj/get ^js internal-module "HEAPU8") heap (gobj/get ^js internal-module "HEAPU8")
mem (js/Uint8Array. (.-buffer heap) ptr size)] mem (js/Uint8Array. (.-buffer heap) ptr size)]
(.set mem (js/Uint8Array. buffer)) (.set mem (js/Uint8Array. buffer))
(h/call internal-module "_set_shape_path_content"))) (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 (defn- translate-blend-mode
[blend-mode] [blend-mode]
(case blend-mode (case blend-mode
@ -453,7 +536,8 @@
(dm/get-prop shape :r2) (dm/get-prop shape :r2)
(dm/get-prop shape :r3) (dm/get-prop shape :r3)
(dm/get-prop shape :r4)]) (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) (use-shape id)
(set-shape-type type) (set-shape-type type)
@ -462,12 +546,16 @@
(set-shape-rotation rotation) (set-shape-rotation rotation)
(set-shape-transform transform) (set-shape-transform transform)
(set-shape-blend-mode blend-mode) (set-shape-blend-mode blend-mode)
(set-shape-children children)
(set-shape-opacity opacity) (set-shape-opacity opacity)
(set-shape-hidden hidden) (set-shape-hidden hidden)
(set-shape-children children)
(when (some? blur) (when (some? blur)
(set-shape-blur 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? bool-content) (set-shape-bool-content bool-content))
(when (some? corners) (set-shape-corners corners)) (when (some? corners) (set-shape-corners corners))
(let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))] (let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))]
@ -486,6 +574,11 @@
:stencil true :stencil true
:alpha true}) :alpha true})
(defn clear-canvas
[])
;; TODO: Perform the corresponding cleanup."
(defn resize-viewbox (defn resize-viewbox
[width height] [width height]
(h/call internal-module "_resize_viewbox" width height)) (h/call internal-module "_resize_viewbox" width height))

View file

@ -124,8 +124,15 @@
:opacity (api/set-shape-opacity v) :opacity (api/set-shape-opacity v)
:hidden (api/set-shape-hidden v) :hidden (api/set-shape-hidden v)
:shapes (api/set-shape-children v) :shapes (api/set-shape-children v)
:content (when (= (:type self) :path) (api/set-shape-path-content v))
:blur (api/set-shape-blur 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) nil)
;; when something synced with wasm ;; when something synced with wasm
;; is modified, we need to request ;; is modified, we need to request

135
render-wasm/Cargo.lock generated
View file

@ -8,6 +8,19 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -93,12 +106,35 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "edit-xml"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f955ed8607e62368a0ff8c4235d8a9b5ebb8d7dbf0139a457cf324ce5ed5d6a"
dependencies = [
"ahash",
"encoding_rs",
"memchr",
"quick-xml",
"thiserror",
"tracing",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -293,12 +329,24 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.24" version = "0.2.24"
@ -318,6 +366,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.37"
@ -369,6 +426,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
name = "render" name = "render"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"edit-xml",
"gl", "gl",
"skia-safe", "skia-safe",
"uuid", "uuid",
@ -509,6 +567,26 @@ dependencies = [
"xattr", "xattr",
] ]
[[package]]
name = "thiserror"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -543,6 +621,37 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
@ -558,6 +667,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -671,3 +786,23 @@ name = "xml-rs"
version = "0.8.22" version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -11,6 +11,7 @@ name = "render_wasm"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
edit-xml = "0.1.0"
gl = "0.14.0" gl = "0.14.0"
skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]} skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]}
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4"] }

View file

@ -16,7 +16,7 @@ export EMCC_CFLAGS="--no-entry \
-sMAX_WEBGL_VERSION=2 \ -sMAX_WEBGL_VERSION=2 \
-sMODULARIZE=1 \ -sMODULARIZE=1 \
-sEXPORT_NAME=createRustSkiaModule \ -sEXPORT_NAME=createRustSkiaModule \
-sEXPORTED_RUNTIME_METHODS=GL \ -sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8 \
-sEXPORT_ES6=1" -sEXPORT_ES6=1"
EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh; EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh;

View file

@ -361,6 +361,21 @@ pub extern "C" fn clear_shape_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] #[no_mangle]
pub extern "C" fn set_shape_blend_mode(mode: i32) { 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");
@ -509,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] #[no_mangle]
pub extern "C" fn add_shape_image_stroke( pub extern "C" fn add_shape_image_stroke(
a: u32, a: u32,
@ -544,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) { 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"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { 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);
}
} }
} }

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use skia::Contains; use skia::Contains;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::math; use crate::math;
@ -33,6 +32,7 @@ pub trait Renderable {
fn clip(&self) -> bool; fn clip(&self) -> bool;
fn children_ids(&self) -> Vec<Uuid>; fn children_ids(&self) -> Vec<Uuid>;
fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter>; fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter>;
fn is_recursive(&self) -> bool;
} }
pub(crate) struct CachedSurfaceImage { pub(crate) struct CachedSurfaceImage {
@ -191,6 +191,7 @@ impl RenderState {
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest),
Some(&skia::Paint::default()), Some(&skia::Paint::default()),
); );
self.drawing_surface self.drawing_surface
.canvas() .canvas()
.clear(skia::Color::TRANSPARENT); .clear(skia::Color::TRANSPARENT);
@ -336,8 +337,8 @@ impl RenderState {
// Returns a boolean indicating if the viewbox contains the rendered shapes // Returns a boolean indicating if the viewbox contains the rendered shapes
fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool { fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool {
let element = tree.get(&root_id).unwrap(); if let Some(element) = tree.get(&root_id) {
let mut is_complete = self.viewbox.area.contains(element.bounds()); let mut is_complete = self.viewbox.area.contains(element.bounds());
if !root_id.is_nil() { if !root_id.is_nil() {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() { if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
@ -348,40 +349,45 @@ impl RenderState {
} else { } else {
self.render_debug_element(element, true); self.render_debug_element(element, true);
} }
}
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into()); paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity()); paint.set_alpha_f(element.opacity());
let filter = element.image_filter(self.viewbox.zoom * self.options.dpr()); let filter = element.image_filter(self.viewbox.zoom * self.options.dpr());
if let Some(image_filter) = filter { if let Some(image_filter) = filter {
paint.set_image_filter(image_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 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,
);
}
}
// 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;
} }
} }

View file

@ -1,5 +1,6 @@
use crate::math; use crate::math;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::render::{BlendMode, Renderable}; use crate::render::{BlendMode, Renderable};
@ -12,6 +13,7 @@ mod matrix;
mod paths; mod paths;
mod renderable; mod renderable;
mod strokes; mod strokes;
mod svgraw;
pub use blurs::*; pub use blurs::*;
pub use bools::*; pub use bools::*;
@ -20,6 +22,7 @@ pub use images::*;
use matrix::*; use matrix::*;
pub use paths::*; pub use paths::*;
pub use strokes::*; pub use strokes::*;
pub use svgraw::*;
pub type CornerRadius = skia::Point; pub type CornerRadius = skia::Point;
pub type Corners = [CornerRadius; 4]; pub type Corners = [CornerRadius; 4];
@ -30,6 +33,7 @@ pub enum Kind {
Circle(math::Rect), Circle(math::Rect),
Path(Path), Path(Path),
Bool(BoolType, Path), Bool(BoolType, Path),
SVGRaw(SVGRaw),
} }
pub type Color = skia::Color; pub type Color = skia::Color;
@ -50,6 +54,7 @@ pub struct Shape {
blur: Blur, blur: Blur,
opacity: f32, opacity: f32,
hidden: bool, hidden: bool,
svg_attrs: HashMap<String, String>,
} }
impl Shape { impl Shape {
@ -68,6 +73,7 @@ impl Shape {
opacity: 1., opacity: 1.,
hidden: false, hidden: false,
blur: Blur::default(), blur: Blur::default(),
svg_attrs: HashMap::new(),
} }
} }
@ -196,6 +202,20 @@ impl Shape {
Ok(()) 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(_) => 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) { pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode; self.blend_mode = mode;
} }
@ -230,6 +250,10 @@ impl Shape {
self.kind = Kind::Rect(self.selrect, corners); self.kind = Kind::Rect(self.selrect, corners);
} }
pub fn set_svg_attr(&mut self, name: String, value: String) {
self.svg_attrs.insert(name, value);
}
fn to_path_transform(&self) -> Option<skia::Matrix> { fn to_path_transform(&self) -> Option<skia::Matrix> {
match self.kind { match self.kind {
Kind::Path(_) | Kind::Bool(_, _) => { Kind::Path(_) | Kind::Bool(_, _) => {

View file

@ -1,4 +1,5 @@
use skia_safe::{self as skia, RRect}; use skia_safe::{self as skia, RRect};
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use super::{BlurType, Corners, Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; use super::{BlurType, Corners, Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind};
@ -93,22 +94,68 @@ impl Renderable for Shape {
None None
} }
} }
fn is_recursive(&self) -> bool {
!matches!(self.kind, Kind::SVGRaw(_))
}
}
fn render_fills_for_kind(
shape: &Shape,
canvas: &skia::Canvas,
images: &ImageStore,
path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) {
for fill in shape.fills().rev() {
render_fill(
canvas,
images,
fill,
shape.selrect,
&shape.kind,
path_transform,
svg_attrs,
);
}
//TODO: remove when strokes are implemented, this is just for testing paths with no fills
if shape.fills().len() == 0 {
if let Kind::Path(ref path) = shape.kind {
let mut p = skia::Paint::default();
p.set_style(skia_safe::PaintStyle::Stroke);
p.set_stroke_width(2.0);
p.set_anti_alias(true);
p.set_blend_mode(skia::BlendMode::SrcOver);
if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) {
p.set_stroke_cap(skia::paint::Cap::Round);
}
if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) {
p.set_stroke_join(skia::paint::Join::Round);
}
let mut skia_path = &mut path.to_skia_path();
skia_path = skia_path.transform(path_transform.unwrap());
canvas.draw_path(&skia_path, &p);
}
}
} }
fn render_fill( fn render_fill(
surface: &mut skia::Surface, canvas: &skia::Canvas,
images: &ImageStore, images: &ImageStore,
fill: &Fill, fill: &Fill,
selrect: Rect, selrect: Rect,
kind: &Kind, kind: &Kind,
path_transform: Option<&skia::Matrix>, path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) { ) {
match (fill, kind) { match (fill, kind) {
(Fill::Image(image_fill), kind) => { (Fill::Image(image_fill), kind) => {
let image = images.get(&image_fill.id()); let image = images.get(&image_fill.id());
if let Some(image) = image { if let Some(image) = image {
draw_image_fill_in_container( draw_image_fill_in_container(
surface.canvas(), canvas,
&image, &image,
image_fill.size(), image_fill.size(),
kind, kind,
@ -119,20 +166,26 @@ fn render_fill(
} }
} }
(_, Kind::Rect(rect, None)) => { (_, Kind::Rect(rect, None)) => {
surface.canvas().draw_rect(rect, &fill.to_paint(&selrect)); canvas.draw_rect(rect, &fill.to_paint(&selrect));
} }
(_, Kind::Rect(rect, Some(corners))) => { (_, Kind::Rect(rect, Some(corners))) => {
let rrect = RRect::new_rect_radii(rect, corners); let rrect = RRect::new_rect_radii(rect, corners);
surface.canvas().draw_rrect(rrect, &fill.to_paint(&selrect)); canvas.draw_rrect(rrect, &fill.to_paint(&selrect));
} }
(_, Kind::Circle(rect)) => { (_, Kind::Circle(rect)) => {
surface.canvas().draw_oval(rect, &fill.to_paint(&selrect)); canvas.draw_oval(rect, &fill.to_paint(&selrect));
} }
(_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => { (_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => {
surface.canvas().draw_path( let mut skia_path = &mut path.to_skia_path();
&path.to_skia_path().transform(path_transform.unwrap()), skia_path = skia_path.transform(path_transform.unwrap());
&fill.to_paint(&selrect), 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));
}
(_, Kind::SVGRaw(_sr)) => {
// NOOP
} }
} }
} }
@ -514,6 +567,9 @@ pub fn draw_image_fill_in_container(
true, true,
); );
} }
Kind::SVGRaw(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
}
} }
canvas.draw_image_rect(image, None, dest_rect, &paint); canvas.draw_image_rect(image, None, dest_rect, &paint);

View file

@ -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 }
}
}