From e45e8158b00b571e60b80e61b928df0aaca3b920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 13 Nov 2024 15:00:20 +0100 Subject: [PATCH 1/6] :recycle: Refactor current_shape in rust_wasm --- render-wasm/src/main.rs | 58 ++++++++------------------------------- render-wasm/src/render.rs | 16 +++++++++++ render-wasm/src/state.rs | 14 ++++++++++ 3 files changed, 41 insertions(+), 47 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index cf4907a00..52e97c0e2 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -3,12 +3,9 @@ pub mod shapes; pub mod state; pub mod utils; -use std::collections::HashMap; - use skia_safe as skia; use uuid::Uuid; -use crate::shapes::Shape; use crate::state::State; use crate::utils::uuid_from_u32_quartet; @@ -37,34 +34,12 @@ pub unsafe extern "C" fn draw_all_shapes(zoom: f32, pan_x: f32, pan_y: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); reset_canvas(); - scale(zoom, zoom); - translate(pan_x, pan_y); + render::scale(state, zoom, zoom); + render::translate(state, pan_x, pan_y); render::render_shape_tree(state, Uuid::nil()); - flush(); -} - -#[no_mangle] -pub unsafe extern "C" fn flush() { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state - .render_state - .gpu_state - .context - .flush_and_submit_surface(&mut state.render_state.surface, None); -} - -#[no_mangle] -pub unsafe extern "C" fn translate(dx: f32, dy: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state.render_state.surface.canvas().translate((dx, dy)); -} - -#[no_mangle] -pub unsafe extern "C" fn scale(sx: f32, sy: f32) { - let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state.render_state.surface.canvas().scale((sx, sy)); + render::flush(state); } #[no_mangle] @@ -78,29 +53,18 @@ pub extern "C" fn reset_canvas() { state.render_state.surface.canvas().reset_matrix(); } -pub fn get_or_create_shape<'a>(shapes: &'a mut HashMap, id: Uuid) -> &'a mut Shape { - if !shapes.contains_key(&id) { - let new_shape = Shape::new(id); - shapes.insert(id, new_shape); - } - - shapes.get_mut(&id).unwrap() -} - #[no_mangle] pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); - state.current_id = Some(id); - let shapes = &mut state.shapes; - state.current_shape = Some(get_or_create_shape(shapes, id)); + state.use_shape(id); } #[no_mangle] pub unsafe extern "C" fn set_shape_selrect(x1: f32, y1: f32, x2: f32, y2: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.selrect.x1 = x1; shape.selrect.y1 = y1; shape.selrect.x2 = x2; @@ -111,7 +75,7 @@ pub unsafe extern "C" fn set_shape_selrect(x1: f32, y1: f32, x2: f32, y2: f32) { #[no_mangle] pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.rotation = rotation; } } @@ -119,7 +83,7 @@ pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { #[no_mangle] pub unsafe extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.transform.a = a; shape.transform.b = b; shape.transform.c = c; @@ -133,7 +97,7 @@ pub unsafe extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let id = uuid_from_u32_quartet(a, b, c, d); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.children.push(id); } } @@ -141,7 +105,7 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { #[no_mangle] pub extern "C" fn clear_shape_children() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.children.clear(); } } @@ -149,7 +113,7 @@ pub extern "C" fn clear_shape_children() { #[no_mangle] pub extern "C" fn add_shape_solid_fill(r: u8, g: u8, b: u8, a: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { let alpha: u8 = (a * 0xff as f32).floor() as u8; let color = skia::Color::from_argb(alpha, r, g, b); shape.add_fill(shapes::Fill::from(color)); @@ -159,7 +123,7 @@ pub extern "C" fn add_shape_solid_fill(r: u8, g: u8, b: u8, a: f32) { #[no_mangle] pub extern "C" fn clear_shape_fills() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - if let Some(shape) = state.current_shape.as_deref_mut() { + if let Some(shape) = state.current_shape() { shape.clear_fills(); } } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 473272bca..97fce0221 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -75,6 +75,22 @@ pub(crate) fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) .unwrap() } +pub(crate) fn flush(state: &mut State) { + state + .render_state + .gpu_state + .context + .flush_and_submit_surface(&mut state.render_state.surface, None); +} + +pub(crate) fn translate(state: &mut State, dx: f32, dy: f32) { + state.render_state.surface.canvas().translate((dx, dy)); +} + +pub(crate) fn scale(state: &mut State, sx: f32, sy: f32) { + state.render_state.surface.canvas().scale((sx, sy)); +} + pub(crate) fn render_shape_tree(state: &mut State, id: Uuid) { let shape = state.shapes.get(&id).unwrap(); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b52387f00..90f0b3656 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -30,4 +30,18 @@ impl<'a> State<'a> { pub fn set_surface(&mut self, surface: skia::Surface) { self.render_state.surface = surface; } + + pub fn use_shape(&'a mut self, id: Uuid) { + if !self.shapes.contains_key(&id) { + let new_shape = Shape::new(id); + self.shapes.insert(id, new_shape); + } + + self.current_id = Some(id); + self.current_shape = self.shapes.get_mut(&id); + } + + pub fn current_shape(&'a mut self) -> Option<&'a mut Shape> { + self.current_shape.as_deref_mut() + } } From 7458165e51c0eaddd50e6a4d28025bfcee84d8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 13 Nov 2024 15:07:16 +0100 Subject: [PATCH 2/6] :bug: Fix fill apply order --- render-wasm/src/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 97fce0221..ce79fda6a 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -141,7 +141,7 @@ fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { surface.canvas().concat(&matrix); - for fill in shape.fills() { + for fill in shape.fills().rev() { surface.canvas().draw_rect(r, &fill.to_paint()); } } From 966e942a7ffe0e4913637bf42f6992d81b06e75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 13 Nov 2024 16:39:26 +0100 Subject: [PATCH 3/6] :tada: Implement drawing with blend mode (single fill) --- common/src/app/common/types/shape/impl.cljc | 12 +++--- frontend/src/app/render_wasm.cljs | 41 +++++++++++++++++---- render-wasm/src/main.rs | 8 ++++ render-wasm/src/render.rs | 5 ++- render-wasm/src/shapes.rs | 36 +++++++++++++++++- 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/common/src/app/common/types/shape/impl.cljc b/common/src/app/common/types/shape/impl.cljc index a6ac5555f..16893e7f0 100644 --- a/common/src/app/common/types/shape/impl.cljc +++ b/common/src/app/common/types/shape/impl.cljc @@ -20,6 +20,7 @@ (defonce wasm-set-shape-transform (constantly nil)) (defonce wasm-set-shape-rotation (constantly nil)) (defonce wasm-set-shape-fills (constantly nil)) +(defonce wasm-set-shape-blend-mode (constantly nil)) (defonce wasm-set-shape-children (constantly nil)) (cr/defrecord Shape [id name type x y width height rotation selrect points @@ -115,11 +116,12 @@ (when *wasm-sync* (wasm-use-shape (:id coll)) (case k - :selrect (wasm-set-shape-selrect v) - :rotation (wasm-set-shape-rotation v) - :transform (wasm-set-shape-transform v) - :fills (wasm-set-shape-fills v) - :shapes (wasm-set-shape-children v) + :selrect (wasm-set-shape-selrect v) + :rotation (wasm-set-shape-rotation v) + :transform (wasm-set-shape-transform v) + :fills (wasm-set-shape-fills v) + :blend-mode (wasm-set-shape-blend-mode v) + :shapes (wasm-set-shape-children v) nil)) (let [delegate (.-delegate ^ShapeProxy coll) delegate' (assoc delegate k v)] diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index 3c662b50c..4560ecdb6 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -74,24 +74,50 @@ [r g b] (cc/hex->rgb (:fill-color fill))] (._add_shape_solid_fill ^js internal-module r g b a)))) +(defn set-shape-blend-mode + [blend-mode] + ;; These values correspond to skia::BlendMode representation + ;; https://rust-skia.github.io/doc/skia_safe/enum.BlendMode.html + (let [encoded-blend (case blend-mode + :normal 3 + :darken 16 + :multiply 24 + :color-burn 19 + :lighten 17 + :screen 14 + :color-dodge 18 + :overlay 15 + :soft-light 21 + :hard-light 20 + :difference 22 + :exclusion 23 + :hue 25 + :saturation 26 + :color 27 + :luminosity 28 + 3)] + (._set_shape_blend_mode ^js internal-module encoded-blend))) + (defn set-objects [objects] (let [shapes (into [] xform (vals objects)) total-shapes (count shapes)] (loop [index 0] (when (< index total-shapes) - (let [shape (nth shapes index) - id (dm/get-prop shape :id) - selrect (dm/get-prop shape :selrect) - rotation (dm/get-prop shape :rotation) - transform (dm/get-prop shape :transform) - fills (dm/get-prop shape :fills) - children (dm/get-prop shape :shapes)] + (let [shape (nth shapes index) + id (dm/get-prop shape :id) + selrect (dm/get-prop shape :selrect) + rotation (dm/get-prop shape :rotation) + transform (dm/get-prop shape :transform) + fills (dm/get-prop shape :fills) + children (dm/get-prop shape :shapes) + blend-mode (dm/get-prop shape :blend-mode)] (use-shape id) (set-shape-selrect selrect) (set-shape-rotation rotation) (set-shape-transform transform) (set-shape-fills fills) + (set-shape-blend-mode blend-mode) (set-shape-children children) (recur (inc index))))))) @@ -155,4 +181,5 @@ (set! app.common.types.shape.impl/wasm-set-shape-transform set-shape-transform) (set! app.common.types.shape.impl/wasm-set-shape-rotation set-shape-rotation) (set! app.common.types.shape.impl/wasm-set-shape-fills set-shape-fills) +(set! app.common.types.shape.impl/wasm-set-shape-blend-mode set-shape-blend-mode) (set! app.common.types.shape.impl/wasm-set-shape-children set-shape-children) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 52e97c0e2..ce202d1ea 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -128,6 +128,14 @@ pub extern "C" fn clear_shape_fills() { } } +#[no_mangle] +pub extern "C" fn set_shape_blend_mode(mode: i32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + shape.set_blend_mode(shapes::BlendMode::from(mode)); + } +} + fn main() { render::init_gl(); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ce79fda6a..9dc0b6bcf 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -141,7 +141,10 @@ fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { surface.canvas().concat(&matrix); + // TODO: use blend mode for the shape as a whole, not in each fill for fill in shape.fills().rev() { - surface.canvas().draw_rect(r, &fill.to_paint()); + let mut p = fill.to_paint(); + p.set_blend_mode(shape.blend_mode.into()); + surface.canvas().draw_rect(r, &p); } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index eb44322ce..d37ed8c19 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -72,7 +72,6 @@ impl Fill { p.set_color(*color); p.set_style(skia::PaintStyle::Fill); p.set_anti_alias(true); - // TODO: get proper blend mode. See https://tree.taiga.io/project/penpot/task/9275 p.set_blend_mode(skia::BlendMode::SrcOver); p } @@ -80,15 +79,43 @@ impl Fill { } } +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct BlendMode(skia::BlendMode); + +impl Default for BlendMode { + fn default() -> Self { + BlendMode(skia::BlendMode::SrcOver) + } +} + +impl From for BlendMode { + fn from(value: i32) -> Self { + if value <= skia::BlendMode::Luminosity as i32 { + unsafe { Self(std::mem::transmute(value)) } + } else { + Self::default() + } + } +} + +impl Into for BlendMode { + fn into(self) -> skia::BlendMode { + match self { + Self(skia_blend) => skia_blend, + } + } +} + #[derive(Debug, Clone)] pub struct Shape { pub id: Uuid, - pub children: Vec::, + pub children: Vec, pub kind: Kind, pub selrect: Rect, pub transform: Matrix, pub rotation: f32, fills: Vec, + pub blend_mode: BlendMode, } impl Shape { @@ -101,6 +128,7 @@ impl Shape { transform: Matrix::identity(), rotation: 0., fills: vec![], + blend_mode: BlendMode::default(), } } @@ -127,4 +155,8 @@ impl Shape { pub fn clear_fills(&mut self) { self.fills.clear(); } + + pub fn set_blend_mode(&mut self, mode: BlendMode) { + self.blend_mode = mode; + } } From 3e06c17a38732fa76d0e135d44cfd33cc39944e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 14 Nov 2024 11:47:10 +0100 Subject: [PATCH 4/6] :recycle: Refactor RenderState and GpuState --- render-wasm/src/main.rs | 19 +++++-- render-wasm/src/render.rs | 101 ++++++++++++++++++-------------------- render-wasm/src/state.rs | 4 -- 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index ce202d1ea..6244840b1 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -10,6 +10,20 @@ use crate::state::State; use crate::utils::uuid_from_u32_quartet; static mut STATE: Option> = None; +extern "C" { + fn emscripten_GetProcAddress( + name: *const ::std::os::raw::c_char, + ) -> *const ::std::os::raw::c_void; +} + +fn init_gl() { + unsafe { + gl::load_with(|addr| { + let addr = std::ffi::CString::new(addr).unwrap(); + emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _ + }); + } +} /// This is called from JS after the WebGL context has been created. #[no_mangle] @@ -25,8 +39,7 @@ pub extern "C" fn init(width: i32, height: i32) { #[no_mangle] pub unsafe extern "C" fn resize_surface(width: i32, height: i32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - let surface = render::create_surface(&mut state.render_state.gpu_state, width, height); - state.set_surface(surface); + state.render_state.resize(width, height); } #[no_mangle] @@ -137,5 +150,5 @@ pub extern "C" fn set_shape_blend_mode(mode: i32) { } fn main() { - render::init_gl(); + init_gl(); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9dc0b6bcf..fab6878a2 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -5,76 +5,69 @@ use uuid::Uuid; use crate::shapes::Shape; use crate::state::State; -extern "C" { - pub fn emscripten_GetProcAddress( - name: *const ::std::os::raw::c_char, - ) -> *const ::std::os::raw::c_void; -} - -pub(crate) struct GpuState { +struct GpuState { pub context: DirectContext, framebuffer_info: FramebufferInfo, } +impl GpuState { + fn new() -> Self { + let interface = skia_safe::gpu::gl::Interface::new_native().unwrap(); + let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap(); + let framebuffer_info = { + let mut fboid: gl::types::GLint = 0; + unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; + + FramebufferInfo { + fboid: fboid.try_into().unwrap(), + format: skia_safe::gpu::gl::Format::RGBA8.into(), + protected: skia_safe::gpu::Protected::No, + } + }; + + GpuState { + context, + framebuffer_info, + } + } + + /// Create a Skia surface that will be used for rendering. + fn create_surface(&mut self, width: i32, height: i32) -> skia::Surface { + let backend_render_target = + gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info); + + gpu::surfaces::wrap_backend_render_target( + &mut self.context, + &backend_render_target, + skia_safe::gpu::SurfaceOrigin::BottomLeft, + skia_safe::ColorType::RGBA8888, + None, + None, + ) + .unwrap() + } +} + pub(crate) struct RenderState { - pub gpu_state: GpuState, + gpu_state: GpuState, pub surface: skia::Surface, } impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { - let mut gpu_state = create_gpu_state(); - let surface = create_surface(&mut gpu_state, width, height); + // This needs to be done once per WebGL context. + let mut gpu_state = GpuState::new(); + let surface = gpu_state.create_surface(width, height); + RenderState { gpu_state, surface } } -} -pub(crate) fn init_gl() { - unsafe { - gl::load_with(|addr| { - let addr = std::ffi::CString::new(addr).unwrap(); - emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _ - }); + pub fn resize(&mut self, width: i32, height: i32) { + let surface = self.gpu_state.create_surface(width, height); + self.surface = surface; } } -/// This needs to be done once per WebGL context. -pub(crate) fn create_gpu_state() -> GpuState { - let interface = skia_safe::gpu::gl::Interface::new_native().unwrap(); - let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap(); - let framebuffer_info = { - let mut fboid: gl::types::GLint = 0; - unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; - - FramebufferInfo { - fboid: fboid.try_into().unwrap(), - format: skia_safe::gpu::gl::Format::RGBA8.into(), - protected: skia_safe::gpu::Protected::No, - } - }; - - GpuState { - context, - framebuffer_info, - } -} - -/// Create the Skia surface that will be used for rendering. -pub(crate) fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) -> skia::Surface { - let backend_render_target = - gpu::backend_render_targets::make_gl((width, height), 1, 8, gpu_state.framebuffer_info); - - gpu::surfaces::wrap_backend_render_target( - &mut gpu_state.context, - &backend_render_target, - skia_safe::gpu::SurfaceOrigin::BottomLeft, - skia_safe::ColorType::RGBA8888, - None, - None, - ) - .unwrap() -} - pub(crate) fn flush(state: &mut State) { state .render_state diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 90f0b3656..3672f40cf 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -27,10 +27,6 @@ impl<'a> State<'a> { } } - pub fn set_surface(&mut self, surface: skia::Surface) { - self.render_state.surface = surface; - } - pub fn use_shape(&'a mut self, id: Uuid) { if !self.shapes.contains_key(&id) { let new_shape = Shape::new(id); From 3eb24e7f5ff76b5ae212c81895d05af3f9934925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 14 Nov 2024 12:08:50 +0100 Subject: [PATCH 5/6] :recycle: Refactor State (wasm) --- render-wasm/src/main.rs | 18 ++------- render-wasm/src/render.rs | 85 ++++++--------------------------------- render-wasm/src/state.rs | 73 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 87 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6244840b1..a812e91bd 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -4,12 +4,12 @@ pub mod state; pub mod utils; use skia_safe as skia; -use uuid::Uuid; use crate::state::State; use crate::utils::uuid_from_u32_quartet; static mut STATE: Option> = None; + extern "C" { fn emscripten_GetProcAddress( name: *const ::std::os::raw::c_char, @@ -45,25 +45,13 @@ pub unsafe extern "C" fn resize_surface(width: i32, height: i32) { #[no_mangle] pub unsafe extern "C" fn draw_all_shapes(zoom: f32, pan_x: f32, pan_y: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - - reset_canvas(); - render::scale(state, zoom, zoom); - render::translate(state, pan_x, pan_y); - - render::render_shape_tree(state, Uuid::nil()); - - render::flush(state); + state.draw_all_shapes(zoom, pan_x, pan_y); } #[no_mangle] pub extern "C" fn reset_canvas() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state - .render_state - .surface - .canvas() - .clear(skia_safe::Color::TRANSPARENT); - state.render_state.surface.canvas().reset_matrix(); + state.render_state().reset_canvas(); } #[no_mangle] diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index fab6878a2..6a7426c9c 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,9 +1,5 @@ use skia_safe as skia; use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext}; -use uuid::Uuid; - -use crate::shapes::Shape; -use crate::state::State; struct GpuState { pub context: DirectContext, @@ -66,78 +62,23 @@ impl RenderState { let surface = self.gpu_state.create_surface(width, height); self.surface = surface; } -} -pub(crate) fn flush(state: &mut State) { - state - .render_state - .gpu_state - .context - .flush_and_submit_surface(&mut state.render_state.surface, None); -} - -pub(crate) fn translate(state: &mut State, dx: f32, dy: f32) { - state.render_state.surface.canvas().translate((dx, dy)); -} - -pub(crate) fn scale(state: &mut State, sx: f32, sy: f32) { - state.render_state.surface.canvas().scale((sx, sy)); -} - -pub(crate) fn render_shape_tree(state: &mut State, id: Uuid) { - let shape = state.shapes.get(&id).unwrap(); - - // This is needed so the next non-children shape does not carry this shape's transform - state.render_state.surface.canvas().save(); - - render_single_shape(&mut state.render_state.surface, shape); - - // draw all the children shapes - let shape_ids = shape.children.clone(); - for shape_id in shape_ids { - render_shape_tree(state, shape_id); + pub fn flush(&mut self) { + self.gpu_state + .context + .flush_and_submit_surface(&mut self.surface, None) } - state.render_state.surface.canvas().restore(); -} + pub fn translate(&mut self, dx: f32, dy: f32) { + self.surface.canvas().translate((dx, dy)); + } -fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { - let r = skia::Rect::new( - shape.selrect.x1, - shape.selrect.y1, - shape.selrect.x2, - shape.selrect.y2, - ); + pub fn scale(&mut self, sx: f32, sy: f32) { + self.surface.canvas().scale((sx, sy)); + } - // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc - let mut matrix = skia::Matrix::new_identity(); - let (translate_x, translate_y) = shape.translation(); - let (scale_x, scale_y) = shape.scale(); - let (skew_x, skew_y) = shape.skew(); - - matrix.set_all( - scale_x, - skew_x, - translate_x, - skew_y, - scale_y, - translate_y, - 0., - 0., - 1., - ); - - let mut center = r.center(); - matrix.post_translate(center); - center.negate(); - matrix.pre_translate(center); - - surface.canvas().concat(&matrix); - - // TODO: use blend mode for the shape as a whole, not in each fill - for fill in shape.fills().rev() { - let mut p = fill.to_paint(); - p.set_blend_mode(shape.blend_mode.into()); - surface.canvas().draw_rect(r, &p); + pub fn reset_canvas(&mut self) { + self.surface.canvas().clear(skia_safe::Color::TRANSPARENT); + self.surface.canvas().reset_matrix(); } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 3672f40cf..edbe561a0 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -27,6 +27,38 @@ impl<'a> State<'a> { } } + pub fn render_state(&'a mut self) -> &'a mut RenderState { + &mut self.render_state + } + + pub fn draw_all_shapes(&mut self, zoom: f32, pan_x: f32, pan_y: f32) { + self.render_state.reset_canvas(); + + self.render_state.scale(zoom, zoom); + self.render_state.translate(pan_x, pan_y); + + self.render_shape_tree(Uuid::nil()); + + self.render_state.flush(); + } + + fn render_shape_tree(&mut self, id: Uuid) { + let shape = self.shapes.get(&id).unwrap(); + + // This is needed so the next non-children shape does not carry this shape's transform + self.render_state.surface.canvas().save(); + + render_single_shape(&mut self.render_state.surface, shape); + + // draw all the children shapes + let shape_ids = shape.children.clone(); + for shape_id in shape_ids { + self.render_shape_tree(shape_id); + } + + self.render_state.surface.canvas().restore(); + } + pub fn use_shape(&'a mut self, id: Uuid) { if !self.shapes.contains_key(&id) { let new_shape = Shape::new(id); @@ -41,3 +73,44 @@ impl<'a> State<'a> { self.current_shape.as_deref_mut() } } + +fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { + let r = skia::Rect::new( + shape.selrect.x1, + shape.selrect.y1, + shape.selrect.x2, + shape.selrect.y2, + ); + + // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc + let mut matrix = skia::Matrix::new_identity(); + let (translate_x, translate_y) = shape.translation(); + let (scale_x, scale_y) = shape.scale(); + let (skew_x, skew_y) = shape.skew(); + + matrix.set_all( + scale_x, + skew_x, + translate_x, + skew_y, + scale_y, + translate_y, + 0., + 0., + 1., + ); + + let mut center = r.center(); + matrix.post_translate(center); + center.negate(); + matrix.pre_translate(center); + + surface.canvas().concat(&matrix); + + // TODO: use blend mode for the shape as a whole, not in each fill + for fill in shape.fills().rev() { + let mut p = fill.to_paint(); + p.set_blend_mode(shape.blend_mode.into()); + surface.canvas().draw_rect(r, &p); + } +} From 263d7eb313de6a34873f8fa91002482adce30c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 14 Nov 2024 16:21:51 +0100 Subject: [PATCH 6/6] :sparkles: Draw fills offscreen to support blend mode when multiple fills --- frontend/src/app/render_wasm.cljs | 34 ++++++++++++++--------------- render-wasm/src/render.rs | 36 +++++++++++++++++++++---------- render-wasm/src/state.rs | 32 ++++++++++++++++++--------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index 4560ecdb6..24f7d801b 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -79,23 +79,23 @@ ;; These values correspond to skia::BlendMode representation ;; https://rust-skia.github.io/doc/skia_safe/enum.BlendMode.html (let [encoded-blend (case blend-mode - :normal 3 - :darken 16 - :multiply 24 - :color-burn 19 - :lighten 17 - :screen 14 - :color-dodge 18 - :overlay 15 - :soft-light 21 - :hard-light 20 - :difference 22 - :exclusion 23 - :hue 25 - :saturation 26 - :color 27 - :luminosity 28 - 3)] + :normal 3 + :darken 16 + :multiply 24 + :color-burn 19 + :lighten 17 + :screen 14 + :color-dodge 18 + :overlay 15 + :soft-light 21 + :hard-light 20 + :difference 22 + :exclusion 23 + :hue 25 + :saturation 26 + :color 27 + :luminosity 28 + 3)] (._set_shape_blend_mode ^js internal-module encoded-blend))) (defn set-objects diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 6a7426c9c..80469da32 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -28,7 +28,7 @@ impl GpuState { } /// Create a Skia surface that will be used for rendering. - fn create_surface(&mut self, width: i32, height: i32) -> skia::Surface { + fn create_target_surface(&mut self, width: i32, height: i32) -> skia::Surface { let backend_render_target = gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info); @@ -46,39 +46,53 @@ impl GpuState { pub(crate) struct RenderState { gpu_state: GpuState, - pub surface: skia::Surface, + pub final_surface: skia::Surface, + pub drawing_surface: skia::Surface, } impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { // This needs to be done once per WebGL context. let mut gpu_state = GpuState::new(); - let surface = gpu_state.create_surface(width, height); + let mut final_surface = gpu_state.create_target_surface(width, height); + let drawing_surface = final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); - RenderState { gpu_state, surface } + RenderState { + gpu_state, + final_surface, + drawing_surface, + } } pub fn resize(&mut self, width: i32, height: i32) { - let surface = self.gpu_state.create_surface(width, height); - self.surface = surface; + let surface = self.gpu_state.create_target_surface(width, height); + self.final_surface = surface; + self.drawing_surface = self + .final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); } pub fn flush(&mut self) { self.gpu_state .context - .flush_and_submit_surface(&mut self.surface, None) + .flush_and_submit_surface(&mut self.final_surface, None) } pub fn translate(&mut self, dx: f32, dy: f32) { - self.surface.canvas().translate((dx, dy)); + self.drawing_surface.canvas().translate((dx, dy)); } pub fn scale(&mut self, sx: f32, sy: f32) { - self.surface.canvas().scale((sx, sy)); + self.drawing_surface.canvas().scale((sx, sy)); } pub fn reset_canvas(&mut self) { - self.surface.canvas().clear(skia_safe::Color::TRANSPARENT); - self.surface.canvas().reset_matrix(); + self.drawing_surface + .canvas() + .clear(skia_safe::Color::TRANSPARENT); + self.drawing_surface.canvas().reset_matrix(); } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index edbe561a0..88cf47dba 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,4 +1,4 @@ -use skia_safe as skia; +use skia_safe::{self as skia, SamplingOptions}; use std::collections::HashMap; use uuid::Uuid; @@ -46,9 +46,14 @@ impl<'a> State<'a> { let shape = self.shapes.get(&id).unwrap(); // This is needed so the next non-children shape does not carry this shape's transform - self.render_state.surface.canvas().save(); + self.render_state.final_surface.canvas().save(); + self.render_state.drawing_surface.canvas().save(); - render_single_shape(&mut self.render_state.surface, shape); + render_single_shape( + &mut self.render_state.final_surface, + &mut self.render_state.drawing_surface, + shape, + ); // draw all the children shapes let shape_ids = shape.children.clone(); @@ -56,7 +61,8 @@ impl<'a> State<'a> { self.render_shape_tree(shape_id); } - self.render_state.surface.canvas().restore(); + self.render_state.final_surface.canvas().restore(); + self.render_state.drawing_surface.canvas().restore(); } pub fn use_shape(&'a mut self, id: Uuid) { @@ -74,7 +80,7 @@ impl<'a> State<'a> { } } -fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { +fn render_single_shape(surface: &mut skia::Surface, offscreen: &mut skia::Surface, shape: &Shape) { let r = skia::Rect::new( shape.selrect.x1, shape.selrect.y1, @@ -105,12 +111,18 @@ fn render_single_shape(surface: &mut skia::Surface, shape: &Shape) { center.negate(); matrix.pre_translate(center); - surface.canvas().concat(&matrix); + offscreen.canvas().concat(&matrix); - // TODO: use blend mode for the shape as a whole, not in each fill for fill in shape.fills().rev() { - let mut p = fill.to_paint(); - p.set_blend_mode(shape.blend_mode.into()); - surface.canvas().draw_rect(r, &p); + offscreen.canvas().draw_rect(r, &fill.to_paint()); } + + let mut paint = skia::Paint::default(); + paint.set_blend_mode(shape.blend_mode.into()); + offscreen.draw( + &mut surface.canvas(), + (0.0, 0.0), + SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::None), + Some(&paint), + ); }