diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index d89f9875d..de3a875d6 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -8,8 +8,6 @@ "A WASM based render API" (:require ["react-dom/server" :as rds] - [app.common.colors :as cc] - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.svg.path :as path] @@ -35,33 +33,6 @@ (def dpr (if use-dpr? js/window.devicePixelRatio 1.0)) -;; (mf/defc svg-raw-element -;; {::mf/props :obj} -;; [{:keys [tag attrs content] :as props}] -;; [:& (name tag) attrs -;; (for [child content] -;; (if (string? child) -;; child -;; [:& svg-raw-element child]))]) - -;; (mf/defc svg-raw -;; {::mf/props :obj} -;; [{:keys [shape] :as props}] -;; (let [content (:content shape)] -;; [:svg {:version "1.1" -;; :xmlns "http://www.w3.org/2000/svg" -;; :xmlnsXlink "http://www.w3.org/1999/xlink" -;; :fill "none"} -;; (if (string? content) -;; content -;; (let [svg-attrs (:svg-attrs shape) -;; content (-> -;; (:content shape) -;; (update :attrs merge svg-attrs))] -;; (println "content" content) -;; (println "svg-attrs" svg-attrs) -;; [:& svg-raw-element content]))])) - ;; Based on app.main.render/object-svg (mf/defc object-svg {::mf/props :obj} @@ -192,28 +163,32 @@ (defn- get-string-length [string] (+ (count string) 1)) ;; IMPORTANT: It should be noted that only TTF fonts can be stored. -(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))) +;; 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") -(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))))) +;; 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] @@ -574,11 +549,6 @@ :stencil true :alpha true}) -(defn clear-canvas - []) - ;; TODO: Perform the corresponding cleanup." - - (defn resize-viewbox [width height] (h/call internal-module "_resize_viewbox" width height)) diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index c2729e0d7..16afbba53 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -8,19 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -106,35 +93,12 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "edit-xml" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f955ed8607e62368a0ff8c4235d8a9b5ebb8d7dbf0139a457cf324ce5ed5d6a" -dependencies = [ - "ahash", - "encoding_rs", - "memchr", - "quick-xml", - "thiserror", - "tracing", -] - [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -329,24 +293,12 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project-lite" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" - [[package]] name = "prettyplease" version = "0.2.24" @@ -366,15 +318,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.37" @@ -426,7 +369,6 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "render" version = "0.1.0" dependencies = [ - "edit-xml", "gl", "skia-safe", "uuid", @@ -567,26 +509,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "thiserror" -version = "1.0.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "toml" version = "0.8.19" @@ -621,37 +543,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - [[package]] name = "unicode-ident" version = "1.0.13" @@ -667,12 +558,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -786,23 +671,3 @@ name = "xml-rs" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 076f7d184..97e8b4247 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -11,7 +11,6 @@ name = "render_wasm" path = "src/main.rs" [dependencies] -edit-xml = "0.1.0" gl = "0.14.0" skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]} uuid = { version = "1.11.0", features = ["v4"] } diff --git a/render-wasm/src/fonts/RobotoMono-Regular.ttf b/render-wasm/src/fonts/RobotoMono-Regular.ttf new file mode 100644 index 000000000..6df2b2536 Binary files /dev/null and b/render-wasm/src/fonts/RobotoMono-Regular.ttf differ diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 0ec79b0b5..440056b87 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -3,65 +3,40 @@ 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, - font_provider: &skia::textlayout::TypefaceFontProvider, - ) -> Result<(), String>; - fn blend_mode(&self) -> BlendMode; - fn opacity(&self) -> f32; - fn bounds(&self) -> math::Rect; - fn hidden(&self) -> bool; - fn clip(&self) -> bool; - fn children_ids(&self) -> Vec; - fn image_filter(&self, scale: f32) -> Option; - fn is_recursive(&self) -> bool; -} - -pub(crate) struct CachedSurfaceImage { - pub image: Image, - pub viewbox: Viewbox, - has_all_shapes: bool, -} - -impl CachedSurfaceImage { - fn is_dirty_for_zooming(&mut self, viewbox: &Viewbox) -> bool { - !self.has_all_shapes && !self.viewbox.area.contains(viewbox.area) - } - - fn is_dirty_for_panning(&mut self, _viewbox: &Viewbox) -> bool { - !self.has_all_shapes - } -} - pub(crate) struct RenderState { gpu_state: GpuState, + options: RenderOptions, + + // TODO: Probably we're going to need + // a surface stack like the one used + // by SVG: https://www.w3.org/TR/SVG2/render.html pub final_surface: skia::Surface, pub drawing_surface: skia::Surface, pub debug_surface: skia::Surface, pub font_provider: skia::textlayout::TypefaceFontProvider, pub cached_surface_image: Option, - options: RenderOptions, pub viewbox: Viewbox, - images: ImageStore, - background_color: skia::Color, + pub images: ImageStore, + pub background_color: skia::Color, } impl RenderState { @@ -179,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, &self.font_provider) - .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(), @@ -197,7 +207,7 @@ impl RenderState { .clear(skia::Color::TRANSPARENT); } - pub fn zoom(&mut self, tree: &HashMap) -> Result<(), String> { + pub fn zoom(&mut self, tree: &HashMap) -> Result<(), String> { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { let is_dirty = cached_surface_image.is_dirty_for_zooming(&self.viewbox); if is_dirty { @@ -210,7 +220,7 @@ impl RenderState { Ok(()) } - pub fn pan(&mut self, tree: &HashMap) -> Result<(), String> { + pub fn pan(&mut self, tree: &HashMap) -> Result<(), String> { if let Some(cached_surface_image) = self.cached_surface_image.as_mut() { let is_dirty = cached_surface_image.is_dirty_for_panning(&self.viewbox); if is_dirty { @@ -223,11 +233,7 @@ impl RenderState { Ok(()) } - pub fn render_all( - &mut self, - tree: &HashMap, - generate_cached_surface_image: bool, - ) { + pub fn render_all(&mut self, tree: &HashMap, generate_cached_surface_image: bool) { self.reset_canvas(); self.scale( self.viewbox.zoom * self.options.dpr(), @@ -288,66 +294,24 @@ impl RenderState { Ok(()) } - fn render_debug_view(&mut self) { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); - paint.set_stroke_width(1.); - - let mut scaled_rect = self.viewbox.area.clone(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - - self.debug_surface.canvas().draw_rect(scaled_rect, &paint); - } - - fn render_debug_element(&mut self, element: &impl Renderable, intersected: bool) { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_color(if intersected { - skia::Color::from_argb(255, 255, 255, 0) - } else { - skia::Color::from_argb(255, 0, 255, 255) - }); - paint.set_stroke_width(1.); - - let mut scaled_rect = element.bounds(); - let x = 100. + scaled_rect.x() * 0.2; - let y = 100. + scaled_rect.y() * 0.2; - let width = scaled_rect.width() * 0.2; - let height = scaled_rect.height() * 0.2; - scaled_rect.set_xywh(x, y, width, height); - - self.debug_surface.canvas().draw_rect(scaled_rect, &paint); - } - fn render_debug(&mut self) { - let paint = skia::Paint::default(); - self.render_debug_view(); - self.debug_surface.draw( - &mut self.final_surface.canvas(), - (0.0, 0.0), - skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), - Some(&paint), - ); + debug::render(self); } // Returns a boolean indicating if the viewbox contains the rendered shapes - fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap) -> bool { + fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap) -> bool { if let Some(element) = tree.get(&root_id) { let mut is_complete = self.viewbox.area.contains(element.bounds()); - if !root_id.is_nil() { - if !element.bounds().intersects(self.viewbox.area) || element.hidden() { - self.render_debug_element(element, false); - // TODO: This means that not all the shapes are rendered so we - // need to call a render_all on the zoom out. - return is_complete; // TODO return is_complete or return false?? - } else { - self.render_debug_element(element, true); + if !root_id.is_nil() { + if !element.bounds().intersects(self.viewbox.area) || element.hidden() { + debug::render_debug_element(self, element, false); + // TODO: This means that not all the shapes are rendered so we + // need to call a render_all on the zoom out. + return is_complete; // TODO return is_complete or return false?? + } else { + debug::render_debug_element(self, element, true); + } } let mut paint = skia::Paint::default(); @@ -364,7 +328,7 @@ impl RenderState { self.drawing_surface.canvas().save(); if !root_id.is_nil() { - self.render_single_element(element); + self.render_shape(&mut element.clone()); if element.clip() { self.drawing_surface.canvas().clip_rect( element.bounds(), diff --git a/render-wasm/src/render/cache.rs b/render-wasm/src/render/cache.rs new file mode 100644 index 000000000..5cafe9db9 --- /dev/null +++ b/render-wasm/src/render/cache.rs @@ -0,0 +1,19 @@ +use super::{Image, Viewbox}; +use skia::Contains; +use skia_safe as skia; + +pub(crate) struct CachedSurfaceImage { + pub image: Image, + pub viewbox: Viewbox, + pub has_all_shapes: bool, +} + +impl CachedSurfaceImage { + pub fn is_dirty_for_zooming(&mut self, viewbox: &Viewbox) -> bool { + !self.has_all_shapes && !self.viewbox.area.contains(viewbox.area) + } + + pub fn is_dirty_for_panning(&mut self, _viewbox: &Viewbox) -> bool { + !self.has_all_shapes + } +} diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs new file mode 100644 index 000000000..5e5363d49 --- /dev/null +++ b/render-wasm/src/render/debug.rs @@ -0,0 +1,57 @@ +use crate::shapes::Shape; +use skia_safe as skia; + +use super::RenderState; + +fn render_debug_view(render_state: &mut RenderState) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); + paint.set_stroke_width(1.); + + let mut scaled_rect = render_state.viewbox.area.clone(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + + render_state + .debug_surface + .canvas() + .draw_rect(scaled_rect, &paint); +} + +pub fn render_debug_element(render_state: &mut RenderState, element: &Shape, intersected: bool) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(if intersected { + skia::Color::from_argb(255, 255, 255, 0) + } else { + skia::Color::from_argb(255, 0, 255, 255) + }); + paint.set_stroke_width(1.); + + let mut scaled_rect = element.bounds(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + + render_state + .debug_surface + .canvas() + .draw_rect(scaled_rect, &paint); +} + +pub fn render(render_state: &mut RenderState) { + let paint = skia::Paint::default(); + render_debug_view(render_state); + render_state.debug_surface.draw( + &mut render_state.final_surface.canvas(), + (0.0, 0.0), + skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), + Some(&paint), + ); +} diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs new file mode 100644 index 000000000..9beb474d7 --- /dev/null +++ b/render-wasm/src/render/fills.rs @@ -0,0 +1,121 @@ +use crate::{ + math, + shapes::{Fill, ImageFill, Kind, Shape}, +}; +use skia_safe::{self as skia, RRect}; + +use super::RenderState; + +fn draw_image_fill_in_container( + render_state: &mut RenderState, + shape: &Shape, + fill: &Fill, + image_fill: &ImageFill, +) { + let image = render_state.images.get(&image_fill.id()); + if image.is_none() { + return; + } + + let size = image_fill.size(); + let canvas = render_state.drawing_surface.canvas(); + let kind = &shape.kind; + let container = &shape.selrect; + let path_transform = shape.to_path_transform(); + let paint = fill.to_paint(container); + + let width = size.0 as f32; + let height = size.1 as f32; + let image_aspect_ratio = width / height; + + // Container size + let container_width = container.width(); + let container_height = container.height(); + let container_aspect_ratio = container_width / container_height; + + // Calculate scale to ensure the image covers the container + let scale = if image_aspect_ratio > container_aspect_ratio { + // Image is wider, scale based on height to cover container + container_height / height + } else { + // Image is taller, scale based on width to cover container + container_width / width + }; + + // Scaled size of the image + let scaled_width = width * scale; + let scaled_height = height * scale; + + let dest_rect = math::Rect::from_xywh( + container.left - (scaled_width - container_width) / 2.0, + container.top - (scaled_height - container_height) / 2.0, + scaled_width, + scaled_height, + ); + + // Save the current canvas state + canvas.save(); + + // Set the clipping rectangle to the container bounds + match kind { + Kind::Rect(_, _) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } + Kind::Circle(_) => { + let mut oval_path = skia::Path::new(); + oval_path.add_oval(container, None); + canvas.clip_path(&oval_path, skia::ClipOp::Intersect, true); + } + Kind::Path(path) | Kind::Bool(_, path) => { + canvas.clip_path( + &path.to_skia_path().transform(&path_transform.unwrap()), + skia::ClipOp::Intersect, + true, + ); + } + Kind::SVGRaw(_) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } + } + + // Draw the image with the calculated destination rectangle + canvas.draw_image_rect(image.unwrap(), None, dest_rect, &paint); + + // Restore the canvas to remove the clipping + canvas.restore(); +} + +/** + * This SHOULD be the only public function in this module. + */ +pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill) { + let canvas = render_state.drawing_surface.canvas(); + let selrect = shape.selrect; + let path_transform = shape.to_path_transform(); + let kind = &shape.kind; + match (fill, kind) { + (Fill::Image(image_fill), _) => { + draw_image_fill_in_container(render_state, shape, fill, image_fill); + } + (_, Kind::Rect(rect, None)) => { + canvas.draw_rect(rect, &fill.to_paint(&selrect)); + } + (_, Kind::Rect(rect, Some(corners))) => { + let rrect = RRect::new_rect_radii(rect, &corners); + canvas.draw_rrect(rrect, &fill.to_paint(&selrect)); + } + (_, Kind::Circle(rect)) => { + canvas.draw_oval(rect, &fill.to_paint(&selrect)); + } + (_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => { + let svg_attrs = &shape.svg_attrs; + let mut skia_path = &mut path.to_skia_path(); + skia_path = skia_path.transform(&path_transform.unwrap()); + if let Some("evenodd") = svg_attrs.get("fill-rule").map(String::as_str) { + skia_path.set_fill_type(skia::PathFillType::EvenOdd); + } + canvas.draw_path(&skia_path, &fill.to_paint(&selrect)); + } + (_, _) => todo!(), + } +} diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs new file mode 100644 index 000000000..76b83f506 --- /dev/null +++ b/render-wasm/src/render/strokes.rs @@ -0,0 +1,454 @@ +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 super::RenderState; + +fn draw_stroke_on_rect( + canvas: &skia::Canvas, + stroke: &Stroke, + rect: &Rect, + selrect: &Rect, + corners: &Option, + svg_attrs: &HashMap, + scale: f32, +) { + // Draw the different kind of strokes for a rect is straightforward, we just need apply a stroke to: + // - The same rect if it's a center stroke + // - A bigger rect if it's an outer stroke + // - A smaller rect if it's an outer stroke + let stroke_rect = stroke.outer_rect(rect); + let paint = stroke.to_paint(selrect, svg_attrs, scale); + + match corners { + Some(radii) => { + let radii = stroke.outer_corners(radii); + let rrect = RRect::new_rect_radii(stroke_rect, &radii); + canvas.draw_rrect(rrect, &paint); + } + None => { + canvas.draw_rect(&stroke_rect, &paint); + } + } +} + +fn draw_stroke_on_circle( + canvas: &skia::Canvas, + stroke: &Stroke, + rect: &Rect, + selrect: &Rect, + svg_attrs: &HashMap, + scale: f32, +) { + // Draw the different kind of strokes for an oval is straightforward, we just need apply a stroke to: + // - The same oval if it's a center stroke + // - A bigger oval if it's an outer stroke + // - A smaller oval if it's an outer stroke + let stroke_rect = stroke.outer_rect(rect); + canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect, svg_attrs, scale)); +} + +fn draw_stroke_on_path( + canvas: &skia::Canvas, + stroke: &Stroke, + path: &Path, + selrect: &Rect, + path_transform: Option<&skia::Matrix>, + svg_attrs: &HashMap, + scale: f32, +) { + let mut skia_path = path.to_skia_path(); + skia_path.transform(path_transform.unwrap()); + + let is_open = path.is_open(); + let paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale); + // Draw the different kind of strokes for a path requires different strategies: + match stroke.kind { + // For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed) + StrokeKind::InnerStroke => { + canvas.clip_path(&skia_path, skia::ClipOp::Intersect, true); + canvas.draw_path(&skia_path, &paint_stroke); + } + // For center stroke we don't need to do anything extra + StrokeKind::CenterStroke => { + canvas.draw_path(&skia_path, &paint_stroke); + } + // For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added + StrokeKind::OuterStroke => { + let mut paint = skia::Paint::default(); + paint.set_blend_mode(skia::BlendMode::SrcOver); + paint.set_anti_alias(true); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + canvas.save_layer(&layer_rec); + + canvas.draw_path(&skia_path, &paint_stroke); + + let mut clear_paint = skia::Paint::default(); + clear_paint.set_blend_mode(skia::BlendMode::Clear); + clear_paint.set_anti_alias(true); + canvas.draw_path(&skia_path, &clear_paint); + + canvas.restore(); + } + } +} + +fn handle_stroke_cap( + canvas: &skia::Canvas, + cap: StrokeCap, + width: f32, + paint: &mut skia::Paint, + p1: &skia::Point, + p2: &skia::Point, +) { + paint.set_style(skia::PaintStyle::Fill); + paint.set_blend_mode(skia::BlendMode::Src); + match cap { + StrokeCap::None => {} + StrokeCap::Line => { + // We also draw this square cap to fill the gap between the path and the arrow + draw_square_cap(canvas, &paint, p1, p2, width, 0.); + paint.set_style(skia::PaintStyle::Stroke); + draw_arrow_cap(canvas, &paint, p1, p2, width * 4.); + } + StrokeCap::Triangle => { + draw_triangle_cap(canvas, &paint, p1, p2, width * 4.); + } + StrokeCap::Rectangle => { + draw_square_cap(canvas, &paint, p1, p2, width * 4., 0.); + } + StrokeCap::Circle => { + canvas.draw_circle((p1.x, p1.y), width * 2., &paint); + } + StrokeCap::Diamond => { + draw_square_cap(canvas, &paint, p1, p2, width * 4., 45.); + } + StrokeCap::Round => { + canvas.draw_circle((p1.x, p1.y), width / 2.0, &paint); + } + StrokeCap::Square => { + draw_square_cap(canvas, &paint, p1, p2, width, 0.); + } + } +} + +fn handle_stroke_caps( + path: &mut skia::Path, + stroke: &Stroke, + selrect: &Rect, + canvas: &skia::Canvas, + is_open: bool, + svg_attrs: &HashMap, + dpr_scale: f32, +) { + let points_count = path.count_points(); + let mut points = vec![skia::Point::default(); points_count]; + let c_points = path.get_points(&mut points); + + // Closed shapes don't have caps + if c_points >= 2 && is_open { + let first_point = points.first().unwrap(); + let last_point = points.last().unwrap(); + + // 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, + stroke.cap_start, + stroke.width, + &mut paint_stroke, + first_point, + &points[1], + ); + handle_stroke_cap( + canvas, + stroke.cap_end, + stroke.width, + &mut paint_stroke, + last_point, + &points[points_count - 2], + ); + } +} + +fn draw_square_cap( + canvas: &skia::Canvas, + paint: &skia::Paint, + center: &skia::Point, + direction: &skia::Point, + size: f32, + extra_rotation: f32, +) { + let dx = direction.x - center.x; + let dy = direction.y - center.y; + let angle = dy.atan2(dx); + + let mut matrix = skia::Matrix::new_identity(); + matrix.pre_rotate( + angle.to_degrees() + extra_rotation, + skia::Point::new(center.x, center.y), + ); + + let half_size = size / 2.0; + let rect = skia::Rect::from_xywh(center.x - half_size, center.y - half_size, size, size); + + let points = [ + skia::Point::new(rect.left(), rect.top()), + skia::Point::new(rect.right(), rect.top()), + skia::Point::new(rect.right(), rect.bottom()), + skia::Point::new(rect.left(), rect.bottom()), + ]; + + let mut transformed_points = points.clone(); + matrix.map_points(&mut transformed_points, &points); + + let mut path = skia::Path::new(); + path.move_to(skia::Point::new(center.x, center.y)); + path.move_to(transformed_points[0]); + path.line_to(transformed_points[1]); + path.line_to(transformed_points[2]); + path.line_to(transformed_points[3]); + path.close(); + canvas.draw_path(&path, paint); +} + +fn draw_arrow_cap( + canvas: &skia::Canvas, + paint: &skia::Paint, + center: &skia::Point, + direction: &skia::Point, + size: f32, +) { + let dx = direction.x - center.x; + let dy = direction.y - center.y; + let angle = dy.atan2(dx); + + let mut matrix = skia::Matrix::new_identity(); + matrix.pre_rotate( + angle.to_degrees() - 90., + skia::Point::new(center.x, center.y), + ); + + let half_height = size / 2.; + let points = [ + skia::Point::new(center.x, center.y - half_height), + skia::Point::new(center.x - size, center.y + half_height), + skia::Point::new(center.x + size, center.y + half_height), + ]; + + let mut transformed_points = points.clone(); + matrix.map_points(&mut transformed_points, &points); + + let mut path = skia::Path::new(); + path.move_to(transformed_points[1]); + path.line_to(transformed_points[0]); + path.line_to(transformed_points[2]); + path.move_to(skia::Point::new(center.x, center.y)); + path.line_to(transformed_points[0]); + + canvas.draw_path(&path, paint); +} + +fn draw_triangle_cap( + canvas: &skia::Canvas, + paint: &skia::Paint, + center: &skia::Point, + direction: &skia::Point, + size: f32, +) { + let dx = direction.x - center.x; + let dy = direction.y - center.y; + let angle = dy.atan2(dx); + + let mut matrix = skia::Matrix::new_identity(); + matrix.pre_rotate( + angle.to_degrees() - 90., + skia::Point::new(center.x, center.y), + ); + + let half_height = size / 2.; + let points = [ + skia::Point::new(center.x, center.y - half_height), + skia::Point::new(center.x - size, center.y + half_height), + skia::Point::new(center.x + size, center.y + half_height), + ]; + + let mut transformed_points = points.clone(); + matrix.map_points(&mut transformed_points, &points); + + let mut path = skia::Path::new(); + path.move_to(transformed_points[0]); + path.line_to(transformed_points[1]); + path.line_to(transformed_points[2]); + path.close(); + + canvas.draw_path(&path, paint); +} + +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; + + // Container size + let container_width = container.width(); + let container_height = container.height(); + let container_aspect_ratio = container_width / container_height; + + let scale = if image_aspect_ratio > container_aspect_ratio { + container_height / height + } else { + container_width / width + }; + + let scaled_width = width * scale; + let scaled_height = height * scale; + + 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), + scaled_height + (2. * delta) + (scaled_width - container_width), + ) +} + +fn draw_image_stroke_in_container( + render_state: &mut RenderState, + shape: &Shape, + stroke: &Stroke, + 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 svg_attrs = &shape.svg_attrs; + let dpr_scale = render_state.viewbox.zoom * render_state.options.dpr(); + + // Save canvas and layer state + let mut pb = skia::Paint::default(); + pb.set_blend_mode(skia::BlendMode::SrcOver); + pb.set_anti_alias(true); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&pb); + canvas.save_layer(&layer_rec); + + // Draw the stroke based on the kind, we are using this stroke as a "selector" of the area of the image we want to show. + let outer_rect = stroke.outer_rect(container); + 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); + image_paint.set_anti_alias(true); + + // 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.unwrap(), None, dest_rect, &image_paint); + + // Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area. + if let Kind::Path(p) = kind { + if stroke.render_kind(p.is_open()) == StrokeKind::OuterStroke { + let mut path = p.to_skia_path(); + path.transform(&path_transform.unwrap()); + let mut clear_paint = skia::Paint::default(); + clear_paint.set_blend_mode(skia::BlendMode::Clear); + clear_paint.set_anti_alias(true); + canvas.draw_path(&path, &clear_paint); + } + } + + // Restore canvas state + canvas.restore(); +} + +/** + * This SHOULD be the only public function in this module. + */ +pub fn render(render_state: &mut RenderState, shape: &Shape, stroke: &Stroke) { + let canvas = render_state.drawing_surface.canvas(); + let dpr_scale = render_state.viewbox.zoom * render_state.options.dpr(); + let selrect = shape.selrect; + let path_transform = shape.to_path_transform(); + let kind = &shape.kind; + let svg_attrs = &shape.svg_attrs; + if let Fill::Image(image_fill) = &stroke.fill { + draw_image_stroke_in_container(render_state, shape, stroke, image_fill); + } else { + match kind { + Kind::Rect(rect, corners) => draw_stroke_on_rect( + canvas, stroke, rect, &selrect, corners, svg_attrs, dpr_scale, + ), + Kind::Circle(rect) => { + draw_stroke_on_circle(canvas, stroke, rect, &selrect, &svg_attrs, dpr_scale) + } + Kind::Path(path) | Kind::Bool(_, path) => { + let svg_attrs = &shape.svg_attrs; + draw_stroke_on_path( + canvas, + stroke, + path, + &selrect, + path_transform.as_ref(), + svg_attrs, + dpr_scale, + ); + } + Kind::SVGRaw(_) => todo!(), + } + } +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ed3897074..84d01dc51 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -3,22 +3,19 @@ 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::*; @@ -41,20 +38,21 @@ pub type Color = skia::Color; #[derive(Debug, Clone)] #[allow(dead_code)] pub struct Shape { - id: Uuid, - children: Vec, - kind: Kind, - selrect: math::Rect, - transform: Matrix, - rotation: f32, - clip_content: bool, - fills: Vec, - strokes: Vec, - blend_mode: BlendMode, - blur: Blur, - opacity: f32, - hidden: bool, - svg_attrs: HashMap, + pub id: Uuid, + pub children: Vec, + pub kind: Kind, + pub selrect: math::Rect, + pub transform: Matrix, + pub rotation: f32, + pub clip_content: bool, + pub fills: Vec, + pub strokes: Vec, + pub blend_mode: BlendMode, + pub blur: Blur, + pub opacity: f32, + pub hidden: bool, + pub svg: Option, + pub svg_attrs: HashMap, } impl Shape { @@ -73,6 +71,7 @@ impl Shape { opacity: 1., hidden: false, blur: Blur::default(), + svg: None, svg_attrs: HashMap::new(), } } @@ -207,7 +206,7 @@ impl Shape { Kind::Path(_) => { self.set_svg_attr(name, value); } - Kind::Rect(_) | Kind::Circle(_) | Kind::SVGRaw(_) => todo!(), + Kind::Rect(_, _) | Kind::Circle(_) | Kind::SVGRaw(_) | Kind::Bool(_, _) => todo!(), }; } @@ -250,11 +249,63 @@ impl Shape { self.kind = Kind::Rect(self.selrect, corners); } + 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); } - fn to_path_transform(&self) -> Option { + pub fn blend_mode(&self) -> crate::render::BlendMode { + self.blend_mode + } + + pub fn opacity(&self) -> f32 { + self.opacity + } + + pub fn hidden(&self) -> bool { + self.hidden + } + + pub fn bounds(&self) -> math::Rect { + self.selrect + } + + pub fn clip(&self) -> bool { + self.clip_content + } + + pub fn children_ids(&self) -> Vec { + if let Kind::Bool(_, _) = self.kind { + vec![] + } else { + self.children.clone() + } + } + + pub fn image_filter(&self, scale: f32) -> Option { + if !self.blur.hidden { + match self.blur.blur_type { + BlurType::None => None, + BlurType::Layer => skia::image_filters::blur( + (self.blur.value * scale, self.blur.value * scale), + None, + None, + None, + ), + } + } else { + None + } + } + + pub fn is_recursive(&self) -> bool { + !matches!(self.kind, Kind::SVGRaw(_)) + } + + pub fn to_path_transform(&self) -> Option { match self.kind { Kind::Path(_) | Kind::Bool(_, _) => { let center = self.bounds().center(); diff --git a/render-wasm/src/shapes/images.rs b/render-wasm/src/shapes/images.rs deleted file mode 100644 index bbf5d600c..000000000 --- a/render-wasm/src/shapes/images.rs +++ /dev/null @@ -1,3 +0,0 @@ -use skia_safe as skia; - -pub type Image = skia::Image; diff --git a/render-wasm/src/shapes/rects.rs b/render-wasm/src/shapes/rects.rs deleted file mode 100644 index e9a6b3fd8..000000000 --- a/render-wasm/src/shapes/rects.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::math::Point; - -#[derive(Debug, Clone, PartialEq)] -pub struct Rect { - -} diff --git a/render-wasm/src/shapes/renderable.rs b/render-wasm/src/shapes/renderable.rs deleted file mode 100644 index e5801161c..000000000 --- a/render-wasm/src/shapes/renderable.rs +++ /dev/null @@ -1,656 +0,0 @@ -use skia_safe::{self as skia, RRect}; -use std::collections::HashMap; -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, - font_provider: &skia::textlayout::TypefaceFontProvider, - ) -> Result<(), String> { - let transform = self.transform.to_skia_matrix(); - - // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc - let center = self.bounds().center(); - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_translate(center); - matrix.pre_concat(&transform); - matrix.pre_translate(-center); - - surface.canvas().concat(&matrix); - - for fill in self.fills().rev() { - render_fill( - surface, - images, - fill, - self.selrect, - &self.kind, - self.to_path_transform().as_ref(), - ); - } - - for stroke in self.strokes().rev() { - render_stroke( - scale, - surface, - images, - stroke, - self.selrect, - &self.kind, - self.to_path_transform().as_ref(), - ); - } - - Ok(()) - } - - fn blend_mode(&self) -> crate::render::BlendMode { - self.blend_mode - } - - fn opacity(&self) -> f32 { - self.opacity - } - - fn hidden(&self) -> bool { - self.hidden - } - - fn bounds(&self) -> Rect { - self.selrect - } - - fn clip(&self) -> bool { - self.clip_content - } - - fn children_ids(&self) -> Vec { - if let Kind::Bool(_, _) = self.kind { - vec![] - } else { - self.children.clone() - } - } - - fn image_filter(&self, scale: f32) -> Option { - if !self.blur.hidden { - match self.blur.blur_type { - BlurType::None => None, - BlurType::Layer => skia::image_filters::blur( - (self.blur.value * scale, self.blur.value * scale), - None, - None, - None, - ), - } - } else { - None - } - } - - fn is_recursive(&self) -> bool { - !matches!(self.kind, Kind::SVGRaw(_)) - } -} - -fn render_fills_for_kind( - shape: &Shape, - canvas: &skia::Canvas, - images: &ImageStore, - path_transform: Option<&skia::Matrix>, - svg_attrs: &HashMap, -) { - for fill in shape.fills().rev() { - render_fill( - canvas, - images, - fill, - shape.selrect, - &shape.kind, - path_transform, - svg_attrs, - ); - } - - //TODO: remove when strokes are implemented, this is just for testing paths with no fills - if shape.fills().len() == 0 { - if let Kind::Path(ref path) = shape.kind { - let mut p = skia::Paint::default(); - p.set_style(skia_safe::PaintStyle::Stroke); - p.set_stroke_width(2.0); - p.set_anti_alias(true); - p.set_blend_mode(skia::BlendMode::SrcOver); - - if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) { - p.set_stroke_cap(skia::paint::Cap::Round); - } - if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) { - p.set_stroke_join(skia::paint::Join::Round); - } - let mut skia_path = &mut path.to_skia_path(); - skia_path = skia_path.transform(path_transform.unwrap()); - canvas.draw_path(&skia_path, &p); - } - } -} - -fn render_fill( - canvas: &skia::Canvas, - images: &ImageStore, - fill: &Fill, - selrect: Rect, - kind: &Kind, - path_transform: Option<&skia::Matrix>, - svg_attrs: &HashMap, -) { - 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( - canvas, - &image, - image_fill.size(), - kind, - &fill.to_paint(&selrect), - &selrect, - path_transform, - ); - } - } - (_, 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 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)); - } - (_, Kind::SVGRaw(_sr)) => { - // NOOP - } - } -} - -fn render_stroke( - scale: f32, - surface: &mut skia::Surface, - images: &ImageStore, - stroke: &Stroke, - selrect: Rect, - kind: &Kind, - path_transform: Option<&skia::Matrix>, -) { - if let Fill::Image(image_fill) = &stroke.fill { - if let Some(image) = images.get(&image_fill.id()) { - draw_image_stroke_in_container( - surface.canvas(), - scale, - &image, - stroke, - image_fill.size(), - kind, - &selrect, - path_transform, - ); - } - } else { - match kind { - Kind::Rect(rect, corners) => { - draw_stroke_on_rect(surface.canvas(), scale, stroke, rect, &selrect, corners); - } - Kind::Circle(rect) => { - draw_stroke_on_circle(surface.canvas(), scale, stroke, rect, &selrect); - } - Kind::Path(path) | Kind::Bool(_, path) => { - draw_stroke_on_path( - surface.canvas(), - scale, - stroke, - path, - &selrect, - path_transform, - ); - } - } - } -} - -fn draw_stroke_on_rect( - canvas: &skia::Canvas, - scale: f32, - stroke: &Stroke, - rect: &Rect, - selrect: &Rect, - corners: &Option, -) { - // 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); - - match corners { - Some(radii) => { - let radii = stroke.outer_corners(radii); - let rrect = RRect::new_rect_radii(stroke_rect, &radii); - canvas.draw_rrect(rrect, &paint); - } - None => { - canvas.draw_rect(&stroke_rect, &paint); - } - } -} - -fn draw_stroke_on_circle( - canvas: &skia::Canvas, - scale: f32, - stroke: &Stroke, - rect: &Rect, - selrect: &Rect, -) { - // 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)); -} - -fn handle_stroke_cap( - canvas: &skia::Canvas, - cap: StrokeCap, - width: f32, - paint: &mut skia::Paint, - p1: &skia::Point, - p2: &skia::Point, -) { - paint.set_style(skia::PaintStyle::Fill); - paint.set_blend_mode(skia::BlendMode::Src); - match cap { - StrokeCap::None => {} - StrokeCap::Line => { - // We also draw this square cap to fill the gap between the path and the arrow - draw_square_cap(canvas, &paint, p1, p2, width, 0.); - paint.set_style(skia::PaintStyle::Stroke); - draw_arrow_cap(canvas, &paint, p1, p2, width * 4.); - } - StrokeCap::Triangle => { - draw_triangle_cap(canvas, &paint, p1, p2, width * 4.); - } - StrokeCap::Rectangle => { - draw_square_cap(canvas, &paint, p1, p2, width * 4., 0.); - } - StrokeCap::Circle => { - canvas.draw_circle((p1.x, p1.y), width * 2., &paint); - } - StrokeCap::Diamond => { - draw_square_cap(canvas, &paint, p1, p2, width * 4., 45.); - } - StrokeCap::Round => { - canvas.draw_circle((p1.x, p1.y), width / 2.0, &paint); - } - StrokeCap::Square => { - draw_square_cap(canvas, &paint, p1, p2, width, 0.); - } - } -} - -fn handle_stroke_caps( - scale: f32, - path: &mut skia::Path, - stroke: &Stroke, - selrect: &Rect, - canvas: &skia::Canvas, - is_open: bool, -) { - let points_count = path.count_points(); - let mut points = vec![skia::Point::default(); points_count]; - let c_points = path.get_points(&mut points); - - // Closed shapes don't have 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); - - handle_stroke_cap( - canvas, - stroke.cap_start, - stroke.width, - &mut paint_stroke, - first_point, - &points[1], - ); - handle_stroke_cap( - canvas, - stroke.cap_end, - stroke.width, - &mut paint_stroke, - last_point, - &points[points_count - 2], - ); - } -} - -fn draw_square_cap( - canvas: &skia::Canvas, - paint: &skia::Paint, - center: &skia::Point, - direction: &skia::Point, - size: f32, - extra_rotation: f32, -) { - let dx = direction.x - center.x; - let dy = direction.y - center.y; - let angle = dy.atan2(dx); - - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_rotate( - angle.to_degrees() + extra_rotation, - skia::Point::new(center.x, center.y), - ); - - let half_size = size / 2.0; - let rect = skia::Rect::from_xywh(center.x - half_size, center.y - half_size, size, size); - - let points = [ - skia::Point::new(rect.left(), rect.top()), - skia::Point::new(rect.right(), rect.top()), - skia::Point::new(rect.right(), rect.bottom()), - skia::Point::new(rect.left(), rect.bottom()), - ]; - - let mut transformed_points = points.clone(); - matrix.map_points(&mut transformed_points, &points); - - let mut path = skia::Path::new(); - path.move_to(skia::Point::new(center.x, center.y)); - path.move_to(transformed_points[0]); - path.line_to(transformed_points[1]); - path.line_to(transformed_points[2]); - path.line_to(transformed_points[3]); - path.close(); - canvas.draw_path(&path, paint); -} - -fn draw_arrow_cap( - canvas: &skia::Canvas, - paint: &skia::Paint, - center: &skia::Point, - direction: &skia::Point, - size: f32, -) { - let dx = direction.x - center.x; - let dy = direction.y - center.y; - let angle = dy.atan2(dx); - - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_rotate( - angle.to_degrees() - 90., - skia::Point::new(center.x, center.y), - ); - - let half_height = size / 2.; - let points = [ - skia::Point::new(center.x, center.y - half_height), - skia::Point::new(center.x - size, center.y + half_height), - skia::Point::new(center.x + size, center.y + half_height), - ]; - - let mut transformed_points = points.clone(); - matrix.map_points(&mut transformed_points, &points); - - let mut path = skia::Path::new(); - path.move_to(transformed_points[1]); - path.line_to(transformed_points[0]); - path.line_to(transformed_points[2]); - path.move_to(skia::Point::new(center.x, center.y)); - path.line_to(transformed_points[0]); - - canvas.draw_path(&path, paint); -} - -fn draw_triangle_cap( - canvas: &skia::Canvas, - paint: &skia::Paint, - center: &skia::Point, - direction: &skia::Point, - size: f32, -) { - let dx = direction.x - center.x; - let dy = direction.y - center.y; - let angle = dy.atan2(dx); - - let mut matrix = skia::Matrix::new_identity(); - matrix.pre_rotate( - angle.to_degrees() - 90., - skia::Point::new(center.x, center.y), - ); - - let half_height = size / 2.; - let points = [ - skia::Point::new(center.x, center.y - half_height), - skia::Point::new(center.x - size, center.y + half_height), - skia::Point::new(center.x + size, center.y + half_height), - ]; - - let mut transformed_points = points.clone(); - matrix.map_points(&mut transformed_points, &points); - - let mut path = skia::Path::new(); - path.move_to(transformed_points[0]); - path.line_to(transformed_points[1]); - path.line_to(transformed_points[2]); - path.close(); - - 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 { - let (width, height) = (size.0 as f32, 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; - - let scale = if image_aspect_ratio > container_aspect_ratio { - container_height / height - } else { - container_width / width - }; - - let scaled_width = width * scale; - let scaled_height = height * scale; - - 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), - scaled_height + (2. * delta) + (scaled_width - container_width), - ) -} - -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, - ); - } - Kind::SVGRaw(_) => { - canvas.clip_rect(container, skia::ClipOp::Intersect, true); - } - } - - canvas.draw_image_rect(image, None, dest_rect, &paint); - - // Restore the canvas to remove the clipping - canvas.restore(); -} - -pub fn draw_image_stroke_in_container( - canvas: &skia::Canvas, - scale: f32, - image: &Image, - stroke: &Stroke, - size: (i32, i32), - kind: &Kind, - container: &Rect, - path_transform: Option<&skia::Matrix>, -) { - // Helper to handle drawing based on kind - fn draw_kind( - canvas: &skia::Canvas, - scale: f32, - kind: &Kind, - stroke: &Stroke, - container: &Rect, - path_transform: Option<&skia::Matrix>, - ) { - let 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()); - } - } - } - - // Save canvas and layer state - let mut pb = skia::Paint::default(); - pb.set_blend_mode(skia::BlendMode::SrcOver); - pb.set_anti_alias(true); - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&pb); - canvas.save_layer(&layer_rec); - - // Draw the stroke based on the kind, we are using this stroke as a "selector" of the area of the image we want to show. - draw_kind(canvas, scale, kind, stroke, container, path_transform); - - // Draw the image. We are using now the SrcIn blend mode, so the rendered piece of image will the area of the stroke over the image. - let mut image_paint = skia::Paint::default(); - image_paint.set_blend_mode(skia::BlendMode::SrcIn); - image_paint.set_anti_alias(true); - - // 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); - - // Restore canvas state - canvas.restore(); -} diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 28562b2bd..52d992096 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,6 +1,7 @@ use crate::math; use crate::shapes::fills::Fill; use skia_safe as skia; +use std::collections::HashMap; use super::Corners; @@ -64,7 +65,7 @@ pub struct Stroke { pub style: StrokeStyle, pub cap_end: StrokeCap, pub cap_start: StrokeCap, - kind: StrokeKind, + pub kind: StrokeKind, } impl Stroke { @@ -155,7 +156,12 @@ impl Stroke { outer } - pub fn to_paint(&self, rect: &math::Rect, scale: f32) -> skia::Paint { + pub fn to_paint( + &self, + rect: &math::Rect, + svg_attrs: &HashMap, + scale: f32, + ) -> skia::Paint { let mut paint = self.fill.to_paint(rect); paint.set_style(skia::PaintStyle::Stroke); @@ -168,6 +174,14 @@ impl Stroke { paint.set_stroke_width(width); paint.set_anti_alias(true); + if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) { + paint.set_stroke_cap(skia::paint::Cap::Round); + } + + if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) { + paint.set_stroke_join(skia::paint::Join::Round); + } + if self.style != StrokeStyle::Solid { let path_effect = match self.style { StrokeStyle::Dotted => { @@ -206,8 +220,14 @@ impl Stroke { paint } - pub fn to_stroked_paint(&self, is_open: bool, rect: &math::Rect, scale: f32) -> skia::Paint { - let mut paint = self.to_paint(rect, scale); + pub fn to_stroked_paint( + &self, + is_open: bool, + rect: &math::Rect, + svg_attrs: &HashMap, + scale: f32, + ) -> skia::Paint { + let mut paint = self.to_paint(rect, svg_attrs, scale); match self.render_kind(is_open) { StrokeKind::InnerStroke => { paint.set_stroke_width(2. * paint.stroke_width());