Merge pull request #5447 from penpot/superalex-svg-render-wasm

🎉 SVG raw support and refactor render architecture
This commit is contained in:
Belén Albeza
2025-01-22 15:56:02 +01:00
committed by GitHub
18 changed files with 854 additions and 535 deletions

View File

@@ -11,3 +11,9 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{rs}]
indent_size = 4
indent_style = space
end_of_line = lf

View File

@@ -133,6 +133,8 @@
[{: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,24 @@
(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.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 +33,27 @@
(def dpr (def dpr
(if use-dpr? js/window.devicePixelRatio 1.0)) (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 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).
@@ -134,6 +160,36 @@
(aget buffer 3)))) (aget buffer 3))))
shape-ids)) 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 (defn- store-image
[id] [id]
(let [buffer (uuid/get-u32 id) (let [buffer (uuid/get-u32 id)
@@ -302,6 +358,27 @@
(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)
@@ -312,6 +389,13 @@
(.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
@@ -427,7 +511,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)
@@ -436,12 +521,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))]

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

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;

Binary file not shown.

View File

@@ -48,7 +48,7 @@ pub extern "C" fn clean_up() {
#[no_mangle] #[no_mangle]
pub extern "C" fn set_render_options(debug: u32, dpr: f32) { 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(); let render_state = state.render_state();
render_state.set_debug_flags(debug); render_state.set_debug_flags(debug);
@@ -65,13 +65,13 @@ pub extern "C" fn set_canvas_background(raw_color: u32) {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn render() { 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); state.render_all(true);
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn render_without_cache() { 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); state.render_all(false);
} }
@@ -90,44 +90,44 @@ pub unsafe extern "C" fn pan() {
#[no_mangle] #[no_mangle]
pub extern "C" fn reset_canvas() { 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(); state.render_state().reset_canvas();
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn resize_viewbox(width: i32, height: i32) { 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); state.resize(width, height);
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { 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); state.render_state().viewbox.set_all(zoom, x, y);
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn set_view_zoom(zoom: f32) { 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); state.render_state().viewbox.set_zoom(zoom);
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn set_view_xy(x: f32, y: f32) { 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); state.render_state().viewbox.set_pan_xy(x, y);
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { 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); let id = uuid_from_u32_quartet(a, b, c, d);
state.use_shape(id); state.use_shape(id);
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_circle() { 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() { if let Some(shape) = state.current_shape() {
shape.set_kind(Kind::Circle(math::Rect::new_empty())); shape.set_kind(Kind::Circle(math::Rect::new_empty()));
@@ -136,7 +136,7 @@ pub unsafe extern "C" fn set_shape_kind_circle() {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_rect() { 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() { if let Some(shape) = state.current_shape() {
match shape.kind() { match shape.kind() {
@@ -148,7 +148,7 @@ pub unsafe extern "C" fn set_shape_kind_rect() {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_path() { 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() { if let Some(shape) = state.current_shape() {
shape.set_kind(Kind::Path(Path::default())); 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] #[no_mangle]
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { 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() { if let Some(shape) = state.current_shape() {
shape.set_selrect(left, top, right, bottom); 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] #[no_mangle]
pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { 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() { if let Some(shape) = state.current_shape() {
shape.set_clip(clip_content); shape.set_clip(clip_content);
} }
@@ -191,7 +191,7 @@ pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { 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() { if let Some(shape) = state.current_shape() {
shape.set_rotation(rotation); shape.set_rotation(rotation);
} }
@@ -199,7 +199,7 @@ pub unsafe extern "C" fn set_shape_rotation(rotation: f32) {
#[no_mangle] #[no_mangle]
pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { 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() { if let Some(shape) = state.current_shape() {
shape.set_transform(a, b, c, d, e, f); 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] #[no_mangle]
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { 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); let id = uuid_from_u32_quartet(a, b, c, d);
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.add_child(id); 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] #[no_mangle]
pub extern "C" fn clear_shape_children() { 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() { if let Some(shape) = state.current_shape() {
shape.clear_children(); shape.clear_children();
} }
@@ -224,7 +224,7 @@ pub extern "C" fn clear_shape_children() {
#[no_mangle] #[no_mangle]
pub extern "C" fn add_shape_solid_fill(raw_color: u32) { 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() { if let Some(shape) = state.current_shape() {
let color = skia::Color::new(raw_color); let color = skia::Color::new(raw_color);
shape.add_fill(shapes::Fill::Solid(color)); shape.add_fill(shapes::Fill::Solid(color));
@@ -239,7 +239,7 @@ pub extern "C" fn add_shape_linear_fill(
end_y: f32, end_y: f32,
opacity: 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() { if let Some(shape) = state.current_shape() {
shape.add_fill(shapes::Fill::new_linear_gradient( shape.add_fill(shapes::Fill::new_linear_gradient(
(start_x, start_y), (start_x, start_y),
@@ -258,7 +258,7 @@ pub extern "C" fn add_shape_radial_fill(
opacity: f32, opacity: f32,
width: 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() { if let Some(shape) = state.current_shape() {
shape.add_fill(shapes::Fill::new_radial_gradient( shape.add_fill(shapes::Fill::new_radial_gradient(
(start_x, start_y), (start_x, start_y),
@@ -271,7 +271,7 @@ pub extern "C" fn add_shape_radial_fill(
#[no_mangle] #[no_mangle]
pub extern "C" fn add_shape_fill_stops(ptr: *mut shapes::RawStopData, n_stops: u32) { 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() { if let Some(shape) = state.current_shape() {
let len = n_stops as usize; 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::<u8>::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] #[no_mangle]
pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32, size: u32) { 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); let id = uuid_from_u32_quartet(a, b, c, d);
unsafe { unsafe {
@@ -306,7 +327,7 @@ pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32, size: u32) {
#[no_mangle] #[no_mangle]
pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32) -> bool { 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); let id = uuid_from_u32_quartet(a, b, c, d);
state.render_state().has_image(&id) state.render_state().has_image(&id)
} }
@@ -321,7 +342,7 @@ pub extern "C" fn add_shape_image_fill(
width: i32, width: i32,
height: 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); let id = uuid_from_u32_quartet(a, b, c, d);
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.add_fill(shapes::Fill::new_image_fill( shape.add_fill(shapes::Fill::new_image_fill(
@@ -334,15 +355,30 @@ pub extern "C" fn add_shape_image_fill(
#[no_mangle] #[no_mangle]
pub extern "C" fn clear_shape_fills() { 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() { if let Some(shape) = state.current_shape() {
shape.clear_fills(); 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] #[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");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.set_blend_mode(render::BlendMode::from(mode)); shape.set_blend_mode(render::BlendMode::from(mode));
} }
@@ -350,7 +386,7 @@ pub extern "C" fn set_shape_blend_mode(mode: i32) {
#[no_mangle] #[no_mangle]
pub extern "C" fn set_shape_opacity(opacity: f32) { 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() { if let Some(shape) = state.current_shape() {
shape.set_opacity(opacity); shape.set_opacity(opacity);
} }
@@ -358,7 +394,7 @@ pub extern "C" fn set_shape_opacity(opacity: f32) {
#[no_mangle] #[no_mangle]
pub extern "C" fn set_shape_hidden(hidden: bool) { 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() { if let Some(shape) = state.current_shape() {
shape.set_hidden(hidden); shape.set_hidden(hidden);
} }
@@ -374,7 +410,7 @@ pub extern "C" fn set_shape_blur(blur_type: u8, hidden: bool, value: f32) {
#[no_mangle] #[no_mangle]
pub extern "C" fn set_shape_path_content() { 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() { if let Some(shape) = state.current_shape() {
let bytes = mem::bytes(); 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] #[no_mangle]
pub extern "C" fn add_shape_image_stroke( pub extern "C" fn add_shape_image_stroke(
a: u32, 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) { 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,65 +1,42 @@
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::view::Viewbox; use crate::view::Viewbox;
mod blend; mod blend;
mod cache;
mod debug;
mod fills;
mod gpu_state; mod gpu_state;
mod images; mod images;
mod options; mod options;
mod strokes;
use crate::shapes::{Kind, Shape};
use cache::CachedSurfaceImage;
use gpu_state::GpuState; use gpu_state::GpuState;
use options::RenderOptions; use options::RenderOptions;
pub use blend::BlendMode; pub use blend::BlendMode;
pub use images::*; 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<Uuid>;
fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter>;
}
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 { pub(crate) struct RenderState {
gpu_state: GpuState, 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 final_surface: skia::Surface,
pub drawing_surface: skia::Surface, pub drawing_surface: skia::Surface,
pub debug_surface: skia::Surface, pub debug_surface: skia::Surface,
pub font_provider: skia::textlayout::TypefaceFontProvider,
pub cached_surface_image: Option<CachedSurfaceImage>, pub cached_surface_image: Option<CachedSurfaceImage>,
options: RenderOptions,
pub viewbox: Viewbox, pub viewbox: Viewbox,
images: ImageStore, pub images: ImageStore,
background_color: skia::Color, pub background_color: skia::Color,
} }
impl RenderState { impl RenderState {
@@ -74,12 +51,19 @@ impl RenderState {
.new_surface_with_dimensions((width, height)) .new_surface_with_dimensions((width, height))
.unwrap(); .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 { RenderState {
gpu_state, gpu_state,
final_surface, final_surface,
drawing_surface, drawing_surface,
debug_surface, debug_surface,
cached_surface_image: None, cached_surface_image: None,
font_provider,
options: RenderOptions::default(), options: RenderOptions::default(),
viewbox: Viewbox::new(width as f32, height as f32), viewbox: Viewbox::new(width as f32, height as f32),
images: ImageStore::new(), 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> { pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> {
self.images.add(id, image_data) self.images.add(id, image_data)
} }
@@ -161,11 +154,46 @@ impl RenderState {
.reset_matrix(); .reset_matrix();
} }
pub fn render_single_element(&mut self, element: &impl Renderable) { pub fn render_shape(&mut self, shape: &mut Shape) {
let scale = self.viewbox.zoom * self.options.dpr(); let transform = shape.transform.to_skia_matrix();
element
.render(&mut self.drawing_surface, &self.images, scale) // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc
.unwrap(); 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( self.drawing_surface.draw(
&mut self.final_surface.canvas(), &mut self.final_surface.canvas(),
@@ -173,12 +201,13 @@ 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);
} }
pub fn zoom(&mut self, tree: &HashMap<Uuid, impl Renderable>) -> Result<(), String> { pub fn zoom(&mut self, tree: &HashMap<Uuid, Shape>) -> Result<(), String> {
if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() {
let is_dirty = cached_surface_image.is_dirty_for_zooming(&self.viewbox); let is_dirty = cached_surface_image.is_dirty_for_zooming(&self.viewbox);
if is_dirty { if is_dirty {
@@ -191,7 +220,7 @@ impl RenderState {
Ok(()) Ok(())
} }
pub fn pan(&mut self, tree: &HashMap<Uuid, impl Renderable>) -> Result<(), String> { pub fn pan(&mut self, tree: &HashMap<Uuid, Shape>) -> Result<(), String> {
if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() {
let is_dirty = cached_surface_image.is_dirty_for_panning(&self.viewbox); let is_dirty = cached_surface_image.is_dirty_for_panning(&self.viewbox);
if is_dirty { if is_dirty {
@@ -204,11 +233,7 @@ impl RenderState {
Ok(()) Ok(())
} }
pub fn render_all( pub fn render_all(&mut self, tree: &HashMap<Uuid, Shape>, generate_cached_surface_image: bool) {
&mut self,
tree: &HashMap<Uuid, impl Renderable>,
generate_cached_surface_image: bool,
) {
self.reset_canvas(); self.reset_canvas();
self.scale( self.scale(
self.viewbox.zoom * self.options.dpr(), self.viewbox.zoom * self.options.dpr(),
@@ -269,66 +294,23 @@ impl RenderState {
Ok(()) 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) { fn render_debug(&mut self) {
let paint = skia::Paint::default(); debug::render(self);
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),
);
} }
// 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, Shape>) -> 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() {
self.render_debug_element(element, false); debug::render_debug_element(self, element, false);
// TODO: This means that not all the shapes are rendered so we // TODO: This means that not all the shapes are rendered so we
// need to call a render_all on the zoom out. // need to call a render_all on the zoom out.
return is_complete; // TODO return is_complete or return false?? return is_complete; // TODO return is_complete or return false??
} else { } else {
self.render_debug_element(element, true); debug::render_debug_element(self, element, true);
} }
} }
@@ -346,7 +328,7 @@ impl RenderState {
self.drawing_surface.canvas().save(); self.drawing_surface.canvas().save();
if !root_id.is_nil() { if !root_id.is_nil() {
self.render_single_element(element); self.render_shape(&mut element.clone());
if element.clip() { if element.clip() {
self.drawing_surface.canvas().clip_rect( self.drawing_surface.canvas().clip_rect(
element.bounds(), element.bounds(),
@@ -357,13 +339,19 @@ impl RenderState {
} }
// draw all the children shapes // draw all the children shapes
if element.is_recursive() {
for id in element.children_ids() { for id in element.children_ids() {
is_complete = self.render_shape_tree(&id, tree) && is_complete; is_complete = self.render_shape_tree(&id, tree) && is_complete;
} }
}
self.final_surface.canvas().restore(); self.final_surface.canvas().restore();
self.drawing_surface.canvas().restore(); self.drawing_surface.canvas().restore();
return is_complete; return is_complete;
} else {
eprintln!("Error: Element with root_id {root_id} not found in the tree.");
return false;
}
} }
} }

View File

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

View File

@@ -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),
);
}

View File

@@ -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!(),
}
}

View File

@@ -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 skia_safe::{self as skia, RRect};
use uuid::Uuid;
use super::{BlurType, Corners, Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind}; use super::RenderState;
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<Uuid> {
if let Kind::Bool(_, _) = self.kind {
vec![]
} else {
self.children.clone()
}
}
fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter> {
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,
);
}
}
}
}
fn draw_stroke_on_rect( fn draw_stroke_on_rect(
canvas: &skia::Canvas, canvas: &skia::Canvas,
scale: f32,
stroke: &Stroke, stroke: &Stroke,
rect: &Rect, rect: &Rect,
selrect: &Rect, selrect: &Rect,
corners: &Option<Corners>, corners: &Option<Corners>,
svg_attrs: &HashMap<String, String>,
scale: f32,
) { ) {
// Draw the different kind of strokes for a rect is straightforward, we just need apply a stroke to: // 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 // - The same rect if it's a center stroke
// - A bigger rect if it's an outer stroke // - A bigger rect if it's an outer stroke
// - A smaller rect if it's an outer stroke // - A smaller rect if it's an outer stroke
let stroke_rect = stroke.outer_rect(rect); 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 { match corners {
Some(radii) => { Some(radii) => {
@@ -209,17 +36,63 @@ fn draw_stroke_on_rect(
fn draw_stroke_on_circle( fn draw_stroke_on_circle(
canvas: &skia::Canvas, canvas: &skia::Canvas,
scale: f32,
stroke: &Stroke, stroke: &Stroke,
rect: &Rect, rect: &Rect,
selrect: &Rect, selrect: &Rect,
svg_attrs: &HashMap<String, String>,
scale: f32,
) { ) {
// Draw the different kind of strokes for an oval is straightforward, we just need apply a stroke to: // 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 // - The same oval if it's a center stroke
// - A bigger oval if it's an outer stroke // - A bigger oval if it's an outer stroke
// - A smaller oval if it's an outer stroke // - A smaller oval if it's an outer stroke
let stroke_rect = stroke.outer_rect(rect); 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<String, String>,
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( fn handle_stroke_cap(
@@ -262,12 +135,13 @@ fn handle_stroke_cap(
} }
fn handle_stroke_caps( fn handle_stroke_caps(
scale: f32,
path: &mut skia::Path, path: &mut skia::Path,
stroke: &Stroke, stroke: &Stroke,
selrect: &Rect, selrect: &Rect,
canvas: &skia::Canvas, canvas: &skia::Canvas,
is_open: bool, is_open: bool,
svg_attrs: &HashMap<String, String>,
dpr_scale: f32,
) { ) {
let points_count = path.count_points(); let points_count = path.count_points();
let mut points = vec![skia::Point::default(); points_count]; let mut points = vec![skia::Point::default(); points_count];
@@ -277,7 +151,9 @@ fn handle_stroke_caps(
if c_points >= 2 && is_open { if c_points >= 2 && is_open {
let first_point = points.first().unwrap(); let first_point = points.first().unwrap();
let last_point = points.last().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( handle_stroke_cap(
canvas, canvas,
@@ -412,46 +288,7 @@ fn draw_triangle_cap(
canvas.draw_path(&path, paint); canvas.draw_path(&path, paint);
} }
fn draw_stroke_on_path( fn calculate_scaled_rect(size: (i32, i32), container: &math::Rect, delta: f32) -> math::Rect {
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 {
let (width, height) = (size.0 as f32, size.1 as f32); let (width, height) = (size.0 as f32, size.1 as f32);
let image_aspect_ratio = width / height; 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_width = width * scale;
let scaled_height = height * scale; let scaled_height = height * scale;
Rect::from_xywh( math::Rect::from_xywh(
container.left - delta - (scaled_width - container_width) / 2.0, container.left - delta - (scaled_width - container_width) / 2.0,
container.top - delta - (scaled_height - container_height) / 2.0, container.top - delta - (scaled_height - container_height) / 2.0,
scaled_width + (2. * delta) + (scaled_width - container_width), scaled_width + (2. * delta) + (scaled_width - container_width),
@@ -477,79 +314,52 @@ fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect
) )
} }
pub fn draw_image_fill_in_container( fn draw_image_stroke_in_container(
canvas: &skia::Canvas, render_state: &mut RenderState,
image: &Image, shape: &Shape,
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,
stroke: &Stroke, stroke: &Stroke,
size: (i32, i32), image_fill: &ImageFill,
kind: &Kind,
container: &Rect,
path_transform: Option<&skia::Matrix>,
) {
// 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 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);
pb.set_anti_alias(true);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&pb);
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.
let outer_rect = stroke.outer_rect(container); let outer_rect = stroke.outer_rect(container);
match kind { match kind {
Kind::Rect(rect, corners) => { Kind::Rect(rect, corners) => draw_stroke_on_rect(
draw_stroke_on_rect(canvas, 1., stroke, rect, &outer_rect, corners) 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::Circle(rect) => draw_stroke_on_circle(canvas, 1., stroke, rect, &outer_rect), Kind::SVGRaw(_) => todo!(),
Kind::Path(p) | Kind::Bool(_, p) => { Kind::Path(p) | Kind::Bool(_, p) => {
canvas.save(); canvas.save();
let mut path = p.to_skia_path(); let mut path = p.to_skia_path();
path.transform(path_transform.unwrap()); path.transform(&path_transform.unwrap());
let stroke_kind = stroke.render_kind(p.is_open()); let stroke_kind = stroke.render_kind(p.is_open());
match stroke_kind { match stroke_kind {
StrokeKind::InnerStroke => { StrokeKind::InnerStroke => {
@@ -561,29 +371,25 @@ pub fn draw_image_stroke_in_container(
} }
} }
let is_open = p.is_open(); let is_open = p.is_open();
let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, scale); let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, dpr_scale);
canvas.draw_path(&path, &paint); canvas.draw_path(&path, &paint);
canvas.restore(); canvas.restore();
if stroke.render_kind(is_open) == StrokeKind::OuterStroke { if stroke.render_kind(is_open) == StrokeKind::OuterStroke {
// Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts // Small extra inner stroke to overlap with the fill and avoid unnecesary artifacts
paint.set_stroke_width(1. / scale); paint.set_stroke_width(1. / dpr_scale);
canvas.draw_path(&path, &paint); canvas.draw_path(&path, &paint);
} }
handle_stroke_caps(scale, &mut path, stroke, &outer_rect, canvas, p.is_open()); handle_stroke_caps(
&mut path,
stroke,
&outer_rect,
canvas,
p.is_open(),
svg_attrs,
dpr_scale,
);
} }
} }
}
// Save canvas and layer state
let mut pb = skia::Paint::default();
pb.set_blend_mode(skia::BlendMode::SrcOver);
pb.set_anti_alias(true);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&pb);
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);
// 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. // 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(); let mut image_paint = skia::Paint::default();
image_paint.set_blend_mode(skia::BlendMode::SrcIn); 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 // Compute scaled rect and clip to it
let dest_rect = calculate_scaled_rect(size, container, stroke.delta()); let dest_rect = calculate_scaled_rect(size, container, stroke.delta());
canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, true); 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 // Restore canvas state
canvas.restore(); 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!(),
}
}
}

View File

@@ -1,25 +1,25 @@
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;
mod blurs; mod blurs;
mod bools; mod bools;
mod fills; mod fills;
mod images;
mod matrix; mod matrix;
mod paths; mod paths;
mod renderable;
mod strokes; mod strokes;
mod svgraw;
pub use blurs::*; pub use blurs::*;
pub use bools::*; pub use bools::*;
pub use fills::*; pub use fills::*;
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 +30,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;
@@ -37,19 +38,21 @@ pub type Color = skia::Color;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Shape { pub struct Shape {
id: Uuid, pub id: Uuid,
children: Vec<Uuid>, pub children: Vec<Uuid>,
kind: Kind, pub kind: Kind,
selrect: math::Rect, pub selrect: math::Rect,
transform: Matrix, pub transform: Matrix,
rotation: f32, pub rotation: f32,
clip_content: bool, pub clip_content: bool,
fills: Vec<Fill>, pub fills: Vec<Fill>,
strokes: Vec<Stroke>, pub strokes: Vec<Stroke>,
blend_mode: BlendMode, pub blend_mode: BlendMode,
blur: Blur, pub blur: Blur,
opacity: f32, pub opacity: f32,
hidden: bool, pub hidden: bool,
pub svg: Option<skia::svg::Dom>,
pub svg_attrs: HashMap<String, String>,
} }
impl Shape { impl Shape {
@@ -68,6 +71,8 @@ impl Shape {
opacity: 1., opacity: 1.,
hidden: false, hidden: false,
blur: Blur::default(), blur: Blur::default(),
svg: None,
svg_attrs: HashMap::new(),
} }
} }
@@ -196,6 +201,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(_) | 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) { pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode; self.blend_mode = mode;
} }
@@ -230,7 +249,63 @@ impl Shape {
self.kind = Kind::Rect(self.selrect, corners); self.kind = Kind::Rect(self.selrect, corners);
} }
fn to_path_transform(&self) -> Option<skia::Matrix> { 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<Uuid> {
if let Kind::Bool(_, _) = self.kind {
vec![]
} else {
self.children.clone()
}
}
pub fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter> {
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<skia::Matrix> {
match self.kind { match self.kind {
Kind::Path(_) | Kind::Bool(_, _) => { Kind::Path(_) | Kind::Bool(_, _) => {
let center = self.bounds().center(); let center = self.bounds().center();

View File

@@ -1,3 +0,0 @@
use skia_safe as skia;
pub type Image = skia::Image;

View File

@@ -1,6 +0,0 @@
use crate::math::Point;
#[derive(Debug, Clone, PartialEq)]
pub struct Rect {
}

View File

@@ -1,6 +1,7 @@
use crate::math; use crate::math;
use crate::shapes::fills::Fill; use crate::shapes::fills::Fill;
use skia_safe as skia; use skia_safe as skia;
use std::collections::HashMap;
use super::Corners; use super::Corners;
@@ -64,7 +65,7 @@ pub struct Stroke {
pub style: StrokeStyle, pub style: StrokeStyle,
pub cap_end: StrokeCap, pub cap_end: StrokeCap,
pub cap_start: StrokeCap, pub cap_start: StrokeCap,
kind: StrokeKind, pub kind: StrokeKind,
} }
impl Stroke { impl Stroke {
@@ -155,7 +156,12 @@ impl Stroke {
outer outer
} }
pub fn to_paint(&self, rect: &math::Rect, scale: f32) -> skia::Paint { pub fn to_paint(
&self,
rect: &math::Rect,
svg_attrs: &HashMap<String, String>,
scale: f32,
) -> skia::Paint {
let mut paint = self.fill.to_paint(rect); let mut paint = self.fill.to_paint(rect);
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
@@ -168,6 +174,14 @@ impl Stroke {
paint.set_stroke_width(width); paint.set_stroke_width(width);
paint.set_anti_alias(true); 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 { if self.style != StrokeStyle::Solid {
let path_effect = match self.style { let path_effect = match self.style {
StrokeStyle::Dotted => { StrokeStyle::Dotted => {
@@ -206,8 +220,14 @@ impl Stroke {
paint paint
} }
pub fn to_stroked_paint(&self, is_open: bool, rect: &math::Rect, scale: f32) -> skia::Paint { pub fn to_stroked_paint(
let mut paint = self.to_paint(rect, scale); &self,
is_open: bool,
rect: &math::Rect,
svg_attrs: &HashMap<String, String>,
scale: f32,
) -> skia::Paint {
let mut paint = self.to_paint(rect, svg_attrs, scale);
match self.render_kind(is_open) { match self.render_kind(is_open) {
StrokeKind::InnerStroke => { StrokeKind::InnerStroke => {
paint.set_stroke_width(2. * paint.stroke_width()); paint.set_stroke_width(2. * paint.stroke_width());

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