From f8d58cb74e09c09dce52c52b88e3d0ec3d35875a Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Wed, 22 Jan 2025 16:41:25 +0100 Subject: [PATCH] :tada: Feat masks --- frontend/src/app/render_wasm/api.cljs | 8 +- frontend/src/app/render_wasm/shape.cljs | 55 +++--- render-wasm/src/main.rs | 31 ++-- render-wasm/src/render.rs | 219 ++++++++++++++++++------ render-wasm/src/render/debug.rs | 2 +- render-wasm/src/render/fills.rs | 3 +- render-wasm/src/render/strokes.rs | 4 +- render-wasm/src/shapes.rs | 19 +- render-wasm/src/shapes/groups.rs | 10 ++ 9 files changed, 249 insertions(+), 102 deletions(-) create mode 100644 render-wasm/src/shapes/groups.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index c80fd1d01..2e22109c8 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -108,7 +108,7 @@ (h/call internal-module "_set_shape_clip_content" clip-content)) (defn set-shape-type - [type] + [type {:keys [masked]}] (cond (= type :circle) (h/call internal-module "_set_shape_kind_circle") @@ -119,6 +119,9 @@ (= type :bool) (h/call internal-module "_set_shape_kind_bool") + (= type :group) + (h/call internal-module "_set_shape_kind_group" masked) + :else (h/call internal-module "_set_shape_kind_rect"))) @@ -537,6 +540,7 @@ (let [shape (nth shapes index) id (dm/get-prop shape :id) type (dm/get-prop shape :type) + masked (dm/get-prop shape :masked-group) selrect (dm/get-prop shape :selrect) clip-content (if (= type :frame) (not (dm/get-prop shape :show-content)) @@ -563,7 +567,7 @@ shadows (dm/get-prop shape :shadow)] (use-shape id) - (set-shape-type type) + (set-shape-type type {:masked masked}) (set-shape-clip-content clip-content) (set-shape-selrect selrect) (set-shape-rotation rotation) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index f44ca2390..e4117d366 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -110,34 +110,35 @@ [self k v] (when ^boolean shape/*wasm-sync* (api/use-shape (:id self)) - (case k - :type (api/set-shape-type v) - :bool-type (api/set-shape-bool-type v) - :bool-content (api/set-shape-bool-content v) - :selrect (api/set-shape-selrect v) - :show-content (if (= (:type self) :frame) - (api/set-shape-clip-content (not v)) - (api/set-shape-clip-content false)) - :rotation (api/set-shape-rotation v) - :transform (api/set-shape-transform v) - :fills (api/set-shape-fills v) - :strokes (api/set-shape-strokes v) - :blend-mode (api/set-shape-blend-mode v) - :opacity (api/set-shape-opacity v) - :hidden (api/set-shape-hidden v) - :shapes (api/set-shape-children v) - :blur (api/set-shape-blur v) - :svg-attrs (when (= (:type self) :path) - (api/set-shape-path-attrs v)) - :constraints-h (api/set-constraints-h v) - :constraints-v (api/set-constraints-v v) - :content (cond - (= (:type self) :path) - (api/set-shape-path-content v) + (let [masked (:masked-group self)] + (case k + :type (api/set-shape-type v {:masked masked}) + :bool-type (api/set-shape-bool-type v) + :bool-content (api/set-shape-bool-content v) + :selrect (api/set-shape-selrect v) + :show-content (if (= (:type self) :frame) + (api/set-shape-clip-content (not v)) + (api/set-shape-clip-content false)) + :rotation (api/set-shape-rotation v) + :transform (api/set-shape-transform v) + :fills (api/set-shape-fills v) + :strokes (api/set-shape-strokes v) + :blend-mode (api/set-shape-blend-mode v) + :opacity (api/set-shape-opacity v) + :hidden (api/set-shape-hidden v) + :shapes (api/set-shape-children v) + :blur (api/set-shape-blur v) + :svg-attrs (when (= (:type self) :path) + (api/set-shape-path-attrs v)) + :constraints-h (api/set-constraints-h v) + :constraints-v (api/set-constraints-v v) + :content (cond + (= (:type self) :path) + (api/set-shape-path-content v) - (= (:type self) :svg-raw) - (api/set-shape-svg-raw-content (api/get-static-markup self))) - nil) + (= (:type self) :svg-raw) + (api/set-shape-svg-raw-content (api/get-static-markup self))) + nil)) ;; when something synced with wasm ;; is modified, we need to request ;; a new render. diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index d6c84a2be..20e44f2f5 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -10,7 +10,8 @@ mod utils; mod view; use crate::mem::SerializableResult; -use crate::shapes::{BoolType, ConstraintH, ConstraintV, Kind, Path, TransformEntry}; +use crate::shapes::{BoolType, ConstraintH, ConstraintV, Group, Kind, Path, TransformEntry}; + use crate::state::State; use crate::utils::uuid_from_u32_quartet; @@ -63,19 +64,19 @@ pub extern "C" fn set_canvas_background(raw_color: u32) { } #[no_mangle] -pub unsafe extern "C" fn render(timestamp: i32) { +pub extern "C" fn render(timestamp: i32) { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.start_render_loop(timestamp).expect("Error rendering"); } #[no_mangle] -pub unsafe extern "C" fn render_from_cache() { +pub extern "C" fn render_from_cache() { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state.render_from_cache(); } #[no_mangle] -pub unsafe extern "C" fn process_animation_frame(timestamp: i32) { +pub extern "C" fn process_animation_frame(timestamp: i32) { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); state .process_animation_frame(timestamp) @@ -120,7 +121,15 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { } #[no_mangle] -pub unsafe extern "C" fn set_shape_kind_circle() { +pub extern "C" fn set_shape_kind_group(masked: bool) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + shape.set_kind(Kind::Group(Group::new(masked))); + } +} + +#[no_mangle] +pub extern "C" fn set_shape_kind_circle() { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { @@ -129,7 +138,7 @@ pub unsafe extern "C" fn set_shape_kind_circle() { } #[no_mangle] -pub unsafe extern "C" fn set_shape_kind_rect() { +pub extern "C" fn set_shape_kind_rect() { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { @@ -141,7 +150,7 @@ pub unsafe extern "C" fn set_shape_kind_rect() { } #[no_mangle] -pub unsafe extern "C" fn set_shape_kind_path() { +pub extern "C" fn set_shape_kind_path() { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_kind(Kind::Path(Path::default())); @@ -149,7 +158,7 @@ pub unsafe extern "C" fn set_shape_kind_path() { } #[no_mangle] -pub unsafe extern "C" fn set_shape_kind_bool() { +pub extern "C" fn set_shape_kind_bool() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { match shape.kind() { @@ -160,7 +169,7 @@ pub unsafe extern "C" fn set_shape_kind_bool() { } #[no_mangle] -pub unsafe extern "C" fn set_shape_bool_type(raw_bool_type: u8) { +pub extern "C" fn set_shape_bool_type(raw_bool_type: u8) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_bool_type(BoolType::from(raw_bool_type)) @@ -176,7 +185,7 @@ pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32 } #[no_mangle] -pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { +pub extern "C" fn set_shape_clip_content(clip_content: bool) { let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.set_clip(clip_content); @@ -184,7 +193,7 @@ pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { } #[no_mangle] -pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { +pub 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() { shape.set_rotation(rotation); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2cf7c3c30..0ee39e67b 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -40,6 +40,18 @@ fn get_time() -> i32 { unsafe { emscripten_run_script_int(script.as_ptr()) } } +pub struct NodeRenderState { + pub id: Uuid, + // We use this bool to keep that we've traversed all the children inside this node. + pub visited_children: bool, + // This is used to clip the content of frames. + pub clip_bounds: Option, + // This is a flag to indicate that we've already drawn the mask of a masked group. + pub visited_mask: bool, + // This bool indicates that we're drawing the mask shape. + pub mask: bool, +} + pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, @@ -57,8 +69,8 @@ pub(crate) struct RenderState { pub render_request_id: Option, // Indicates whether the rendering process has pending frames. pub render_in_progress: bool, - // Stack of nodes pending to be rendered. The boolean flag indicates if the node has already been visited. The rect the optional bounds to clip. - pub pending_nodes: Vec<(Uuid, bool, Option)>, + // Stack of nodes pending to be rendered. + pub pending_nodes: Vec, } impl RenderState { @@ -78,7 +90,6 @@ impl RenderState { let debug_surface = final_surface .new_surface_with_dimensions((width, height)) .unwrap(); - let mut font_provider = skia::textlayout::TypefaceFontProvider::new(); let default_font = skia::FontMgr::default() .new_from_data(DEFAULT_FONT_BYTES, None) @@ -172,14 +183,6 @@ impl RenderState { .flush_and_submit_surface(&mut self.final_surface, None); } - pub fn translate(&mut self, dx: f32, dy: f32) { - self.drawing_surface.canvas().translate((dx, dy)); - } - - pub fn scale(&mut self, sx: f32, sy: f32) { - self.drawing_surface.canvas().scale((sx, sy)); - } - pub fn reset_canvas(&mut self) { self.drawing_surface.canvas().restore_to_count(1); self.render_surface.canvas().restore_to_count(1); @@ -306,12 +309,20 @@ impl RenderState { } } self.reset_canvas(); - self.scale( + self.drawing_surface.canvas().scale(( self.viewbox.zoom * self.options.dpr(), self.viewbox.zoom * self.options.dpr(), - ); - self.translate(self.viewbox.pan_x, self.viewbox.pan_y); - self.pending_nodes = vec![(Uuid::nil(), false, None)]; + )); + self.drawing_surface + .canvas() + .translate((self.viewbox.pan_x, self.viewbox.pan_y)); + self.pending_nodes = vec![NodeRenderState { + id: Uuid::nil(), + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: false, + }]; self.render_in_progress = true; self.process_animation_frame(tree, modifiers, timestamp)?; Ok(()) @@ -413,60 +424,154 @@ impl RenderState { } let mut i = 0; - while let Some((node_id, visited_children, clip_bounds)) = self.pending_nodes.pop() { - let element = tree.get_mut(&node_id).ok_or( - "Error: Element with root_id {node_id} not found in the tree.".to_string(), + while let Some(node_render_state) = self.pending_nodes.pop() { + let NodeRenderState { + id: node_id, + visited_children, + clip_bounds, + visited_mask, + mask, + } = node_render_state; + let element = tree.get(&node_render_state.id).ok_or( + "Error: Element with root_id {node_render_state.id} not found in the tree." + .to_string(), )?; - if !visited_children { - if !node_id.is_nil() { - if !element.bounds().intersects(self.viewbox.area) || element.hidden() { - debug::render_debug_element(self, &element, false); - continue; - } else { - debug::render_debug_element(self, &element, true); + if visited_children { + if !visited_mask { + match element.kind { + Kind::Group(group) => { + // When we're dealing with masked groups we need to + // do a separate extra step to draw the mask (the last + // element of a masked group) and blend (using + // the blend mode 'destination-in') the content + // of the group and the mask. + if group.masked { + self.pending_nodes.push(NodeRenderState { + id: node_id, + visited_children: true, + clip_bounds: None, + visited_mask: true, + mask: false, + }); + if let Some(&mask_id) = element.mask_id() { + self.pending_nodes.push(NodeRenderState { + id: mask_id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: true, + }); + } + } + } + _ => {} } - } - - let mut paint = skia::Paint::default(); - paint.set_blend_mode(element.blend_mode().into()); - paint.set_alpha_f(element.opacity()); - - if let Some(image_filter) = - element.image_filter(self.viewbox.zoom * self.options.dpr()) - { - paint.set_image_filter(image_filter); - } - - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); - self.render_surface.canvas().save_layer(&layer_rec); - - self.drawing_surface.canvas().save(); - if !node_id.is_nil() { - self.render_shape(element, modifiers.get(&element.id), clip_bounds); } else { - self.apply_drawing_to_render_canvas(); - } - self.drawing_surface.canvas().restore(); - - // Set the node as visited before processing children - self.pending_nodes.push((node_id, true, None)); - if element.is_recursive() { - let children_clip_bounds = - (!node_id.is_nil() & element.clip()).then(|| element.bounds()); - - for child_id in element.children_ids().iter().rev() { - self.pending_nodes - .push((*child_id, false, children_clip_bounds)); + // Because masked groups needs two rendering passes (first drawing + // the content and then drawing the mask), we need to do an + // extra restore. + match element.kind { + Kind::Group(group) => { + if group.masked { + self.render_surface.canvas().restore(); + } + } + _ => {} } } - } else { self.render_surface.canvas().restore(); + continue; } + + // If we didn't visited_children this shape, then we need to do + if !node_render_state.id.is_nil() { + if !element.bounds().intersects(self.viewbox.area) || element.hidden() { + debug::render_debug_shape(self, element, false); + continue; + } else { + debug::render_debug_shape(self, element, true); + } + } + + // Masked groups needs two rendering passes, the first one rendering + // the content and the second one rendering the mask so we need to do + // an extra save_layer to keep all the masked group separate from other + // already drawn elements. + match element.kind { + Kind::Group(group) => { + if group.masked { + let paint = skia::Paint::default(); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + self.render_surface.canvas().save_layer(&layer_rec); + } + } + _ => {} + } + + let mut paint = skia::Paint::default(); + paint.set_blend_mode(element.blend_mode().into()); + paint.set_alpha_f(element.opacity()); + + // When we're rendering the mask shape we need to set a special blend mode + // called 'destination-in' that keeps the drawn content within the mask. + // @see https://skia.org/docs/user/api/skblendmode_overview/ + if mask { + let mut mask_paint = skia::Paint::default(); + mask_paint.set_blend_mode(skia::BlendMode::DstIn); + let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint); + self.render_surface.canvas().save_layer(&mask_rec); + } + + if let Some(image_filter) = element.image_filter(self.viewbox.zoom * self.options.dpr()) + { + paint.set_image_filter(image_filter); + } + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + self.render_surface.canvas().save_layer(&layer_rec); + + self.drawing_surface.canvas().save(); + if !node_render_state.id.is_nil() { + self.render_shape( + &mut element.clone(), + modifiers.get(&element.id), + clip_bounds, + ); + } else { + self.apply_drawing_to_render_canvas(); + } + self.drawing_surface.canvas().restore(); + + // Set the node as visited_children before processing children + self.pending_nodes.push(NodeRenderState { + id: node_id, + visited_children: true, + clip_bounds: None, + visited_mask: false, + mask: mask, + }); + + if element.is_recursive() { + let children_clip_bounds = + (!node_render_state.id.is_nil() & element.clip()).then(|| element.bounds()); + + for child_id in element.children_ids().iter().rev() { + self.pending_nodes.push(NodeRenderState { + id: *child_id, + visited_children: false, + clip_bounds: children_clip_bounds, + visited_mask: false, + mask: false, + }); + } + } + // We try to avoid doing too many calls to get_time if i % NODE_BATCH_THRESHOLD == 0 && get_time() - timestamp > MAX_BLOCKING_TIME_MS { return Ok(()); } + i += 1; } diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 0e23fb137..ada9d8b66 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -39,7 +39,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) { canvas.draw_str("WASM RENDERER", p, &font, &paint); } -pub fn render_debug_element(render_state: &mut RenderState, element: &Shape, intersected: bool) { +pub fn render_debug_shape(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 { diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 6061b85c5..152f93bbc 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -78,6 +78,7 @@ fn draw_image_fill_in_container( Kind::SVGRaw(_) => { canvas.clip_rect(container, skia::ClipOp::Intersect, true); } + Kind::Group(_) => unreachable!("A group should not have fills"), } // Draw the image with the calculated destination rectangle @@ -123,6 +124,6 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill) { canvas.draw_path(&skia_path, &fill.to_paint(&selrect)); } } - (_, _) => todo!(), + (_, _) => unreachable!("This shape should not have fills"), } } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index ce42178be..2a1996878 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -364,7 +364,7 @@ fn draw_image_stroke_in_container( Kind::Circle(rect) => { draw_stroke_on_circle(canvas, stroke, rect, &outer_rect, svg_attrs, dpr_scale) } - Kind::SVGRaw(_) => todo!(), + Kind::SVGRaw(_) | Kind::Group(_) => unreachable!("This shape should not have strokes"), Kind::Path(p) | Kind::Bool(_, p) => { canvas.save(); let mut path = p.to_skia_path(); @@ -457,7 +457,7 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, stroke: &Stroke) { dpr_scale, ); } - Kind::SVGRaw(_) => todo!(), + Kind::SVGRaw(_) | Kind::Group(_) => unreachable!("This shape should not have strokes"), } } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ce1f32bc9..95755b31c 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -9,6 +9,7 @@ use skia::Matrix; mod blurs; mod bools; mod fills; +mod groups; mod modifiers; mod paths; mod shadows; @@ -19,6 +20,7 @@ mod transform; pub use blurs::*; pub use bools::*; pub use fills::*; +pub use groups::*; pub use modifiers::*; pub use paths::*; pub use shadows::*; @@ -36,6 +38,7 @@ pub enum Kind { Path(Path), Bool(BoolType, Path), SVGRaw(SVGRaw), + Group(Group), } #[derive(Debug, Clone, PartialEq)] @@ -269,7 +272,11 @@ impl Shape { Kind::Path(_) => { self.set_svg_attr(name, value); } - Kind::Rect(_, _) | Kind::Circle(_) | Kind::SVGRaw(_) | Kind::Bool(_, _) => todo!(), + Kind::Rect(_, _) + | Kind::Circle(_) + | Kind::SVGRaw(_) + | Kind::Bool(_, _) + | Kind::Group(_) => unreachable!("This shape should have path attrs"), }; } @@ -340,9 +347,19 @@ impl Shape { self.clip_content } + pub fn mask_id(&self) -> Option<&Uuid> { + self.children.first() + } + pub fn children_ids(&self) -> Vec { if let Kind::Bool(_, _) = self.kind { vec![] + } else if let Kind::Group(group) = self.kind { + if group.masked { + self.children[1..self.children.len()].to_vec() + } else { + self.children.clone() + } } else { self.children.clone() } diff --git a/render-wasm/src/shapes/groups.rs b/render-wasm/src/shapes/groups.rs new file mode 100644 index 000000000..4f8e4266d --- /dev/null +++ b/render-wasm/src/shapes/groups.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Group { + pub masked: bool, +} + +impl Group { + pub fn new(masked: bool) -> Self { + Group { masked } + } +}