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