0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-05 19:41:27 -05:00

♻️ Refactor to make it more ECS friendly

This commit is contained in:
AzazelN28 2025-01-02 15:49:43 +01:00
parent 79df616108
commit fb4e92d0e8
14 changed files with 835 additions and 980 deletions

View file

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

135
render-wasm/Cargo.lock generated
View file

@ -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",
]

View file

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

Binary file not shown.

View file

@ -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<Uuid>;
fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter>;
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<CachedSurfaceImage>,
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<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 {
@ -210,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 {
@ -223,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(),
@ -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<Uuid, impl Renderable>) -> bool {
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();
@ -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(),

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

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

View file

@ -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<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,
svg_attrs: HashMap<String, String>,
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 {
@ -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<skia::Matrix> {
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,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<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 is_recursive(&self) -> bool {
!matches!(self.kind, Kind::SVGRaw(_))
}
}
fn render_fills_for_kind(
shape: &Shape,
canvas: &skia::Canvas,
images: &ImageStore,
path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) {
for fill in shape.fills().rev() {
render_fill(
canvas,
images,
fill,
shape.selrect,
&shape.kind,
path_transform,
svg_attrs,
);
}
//TODO: remove when strokes are implemented, this is just for testing paths with no fills
if shape.fills().len() == 0 {
if let Kind::Path(ref path) = shape.kind {
let mut p = skia::Paint::default();
p.set_style(skia_safe::PaintStyle::Stroke);
p.set_stroke_width(2.0);
p.set_anti_alias(true);
p.set_blend_mode(skia::BlendMode::SrcOver);
if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) {
p.set_stroke_cap(skia::paint::Cap::Round);
}
if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) {
p.set_stroke_join(skia::paint::Join::Round);
}
let mut skia_path = &mut path.to_skia_path();
skia_path = skia_path.transform(path_transform.unwrap());
canvas.draw_path(&skia_path, &p);
}
}
}
fn render_fill(
canvas: &skia::Canvas,
images: &ImageStore,
fill: &Fill,
selrect: Rect,
kind: &Kind,
path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) {
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<Corners>,
) {
// 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();
}

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