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 d03b6a586..3482e3f42 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -81,24 +81,50 @@ [r g b] (cc/hex->rgb (:fill-color fill))] (^function add-shape-fill 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))))))) @@ -164,4 +190,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 cf4907a00..a812e91bd 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -3,17 +3,28 @@ 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; 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] pub extern "C" fn init(width: i32, height: i32) { @@ -28,79 +39,33 @@ 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] 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::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)); + 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(); -} - -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() + state.render_state().reset_canvas(); } #[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 +76,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 +84,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 +98,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 +106,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 +114,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,11 +124,19 @@ 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(); } } -fn main() { - render::init_gl(); +#[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() { + init_gl(); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 473272bca..80469da32 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,131 +1,98 @@ use skia_safe as skia; use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext}; -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_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); + + 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, - pub surface: skia::Surface, + gpu_state: GpuState, + pub final_surface: skia::Surface, + pub drawing_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); - RenderState { gpu_state, surface } - } -} + // This needs to be done once per WebGL context. + let mut gpu_state = GpuState::new(); + let mut final_surface = gpu_state.create_target_surface(width, height); + let drawing_surface = final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); -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 _ - }); - } -} - -/// 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, + RenderState { + gpu_state, + final_surface, + drawing_surface, } - }; - - 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 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); } - state.render_state.surface.canvas().restore(); -} + pub fn resize(&mut self, width: i32, height: i32) { + 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(); + } -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 flush(&mut self) { + self.gpu_state + .context + .flush_and_submit_surface(&mut self.final_surface, None) + } - // 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(); + pub fn translate(&mut self, dx: f32, dy: f32) { + self.drawing_surface.canvas().translate((dx, dy)); + } - matrix.set_all( - scale_x, - skew_x, - translate_x, - skew_y, - scale_y, - translate_y, - 0., - 0., - 1., - ); + pub fn scale(&mut self, sx: f32, sy: f32) { + self.drawing_surface.canvas().scale((sx, sy)); + } - let mut center = r.center(); - matrix.post_translate(center); - center.negate(); - matrix.pre_translate(center); - - surface.canvas().concat(&matrix); - - for fill in shape.fills() { - surface.canvas().draw_rect(r, &fill.to_paint()); + pub fn reset_canvas(&mut self) { + self.drawing_surface + .canvas() + .clear(skia_safe::Color::TRANSPARENT); + self.drawing_surface.canvas().reset_matrix(); } } 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; + } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b52387f00..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; @@ -27,7 +27,102 @@ impl<'a> State<'a> { } } - pub fn set_surface(&mut self, surface: skia::Surface) { - self.render_state.surface = surface; + 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.final_surface.canvas().save(); + self.render_state.drawing_surface.canvas().save(); + + 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(); + for shape_id in shape_ids { + self.render_shape_tree(shape_id); + } + + self.render_state.final_surface.canvas().restore(); + self.render_state.drawing_surface.canvas().restore(); + } + + 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() } } + +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, + 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); + + offscreen.canvas().concat(&matrix); + + for fill in shape.fills().rev() { + 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), + ); +}