0
Fork 0
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:
Belén Albeza 2025-01-22 15:56:02 +01:00 committed by GitHub
commit 69cc72de61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 854 additions and 535 deletions

View file

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

View file

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

View file

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

View file

@ -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))]

View file

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

View file

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

Binary file not shown.

View file

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

View file

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

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

View file

@ -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();

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::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());

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