diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 376acfde5..39f52fc33 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -469,6 +469,32 @@ r4 (or (get corners 3) 0)] (h/call internal-module "_set_shape_corners" r1 r2 r3 r4))) + +(defn- translate-shadow-style + [style] + (case style + :drop-shadow 0 + :inner-shadow 1 + 0)) + +(defn set-shape-shadows + [shadows] + (h/call internal-module "_clear_shape_shadows") + (let [total-shadows (count shadows)] + (loop [index 0] + (when (< index total-shadows) + (let [shadow (nth shadows index) + color (dm/get-prop shadow :color) + blur (dm/get-prop shadow :blur) + rgba (rgba-from-hex (dm/get-prop color :color) (dm/get-prop color :opacity)) + hidden (dm/get-prop shadow :hidden) + x (dm/get-prop shadow :offset-x) + y (dm/get-prop shadow :offset-y) + spread (dm/get-prop shadow :spread) + style (dm/get-prop shadow :style)] + (h/call internal-module "_add_shape_shadow" rgba blur spread x y (translate-shadow-style style) hidden) + (recur (inc index))))))) + (def debounce-render-without-cache (fns/debounce render-without-cache 100)) (defn set-view-box @@ -514,7 +540,8 @@ (dm/get-prop shape :r3) (dm/get-prop shape :r4)]) bool-content (dm/get-prop shape :bool-content) - svg-attrs (dm/get-prop shape :svg-attrs)] + svg-attrs (dm/get-prop shape :svg-attrs) + shadows (dm/get-prop shape :shadow)] (use-shape id) (set-shape-type type) @@ -535,6 +562,7 @@ (set-shape-svg-raw-content (get-static-markup shape))) (when (some? bool-content) (set-shape-bool-content bool-content)) (when (some? corners) (set-shape-corners corners)) + (when (some? shadows) (set-shape-shadows shadows)) (let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))] (recur (inc index) (into pending pending')))) pending))] diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index fddfa274b..08ecb179e 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -82,7 +82,17 @@ Bool operations (`bool-type`) are serialized as `u8`: Blur types are serialized as `u8`: +| Value | Field | +| ----- | ----- | +| 1 | Layer | +| \_ | None | + +## Shadow Styles + +Shadow styles are serialized as `u8`: + | Value | Field | | ----- | ------------ | -| 1 | Layer | -| \_ | None | +| 0 | Drop Shadow | +| 1 | Inner Shadow | +| \_ | Drop Shadow | diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 972f608bc..20c0be3a9 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -596,6 +596,33 @@ pub extern "C" fn set_shape_path_attrs(num_attrs: u32) { } } +#[no_mangle] +pub extern "C" fn add_shape_shadow( + raw_color: u32, + blur: f32, + spread: f32, + x: f32, + y: f32, + raw_style: u8, + hidden: bool, +) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + let color = skia::Color::new(raw_color); + let style = shapes::ShadowStyle::from(raw_style); + let shadow = shapes::Shadow::new(color, blur, spread, (x, y), style, hidden); + shape.add_shadow(shadow); + } +} + +#[no_mangle] +pub extern "C" fn clear_shape_shadows() { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + shape.clear_shadows(); + } +} + fn main() { init_gl(); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index f1278bb38..79659f5eb 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -12,6 +12,7 @@ mod fills; mod gpu_state; mod images; mod options; +mod shadows; mod strokes; use crate::shapes::{Kind, Shape}; @@ -31,6 +32,7 @@ pub(crate) struct RenderState { // by SVG: https://www.w3.org/TR/SVG2/render.html pub final_surface: skia::Surface, pub drawing_surface: skia::Surface, + pub shadow_surface: skia::Surface, pub debug_surface: skia::Surface, pub font_provider: skia::textlayout::TypefaceFontProvider, pub cached_surface_image: Option, @@ -44,6 +46,9 @@ impl RenderState { // 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 shadow_surface = final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); let drawing_surface = final_surface .new_surface_with_dimensions((width, height)) .unwrap(); @@ -60,6 +65,7 @@ impl RenderState { RenderState { gpu_state, final_surface, + shadow_surface, drawing_surface, debug_surface, cached_surface_image: None, @@ -113,6 +119,10 @@ impl RenderState { let surface = self.gpu_state.create_target_surface(dpr_width, dpr_height); self.final_surface = surface; + self.shadow_surface = self + .final_surface + .new_surface_with_dimensions((dpr_width, dpr_height)) + .unwrap(); self.drawing_surface = self .final_surface .new_surface_with_dimensions((dpr_width, dpr_height)) @@ -144,6 +154,10 @@ impl RenderState { .canvas() .clear(self.background_color) .reset_matrix(); + self.shadow_surface + .canvas() + .clear(self.background_color) + .reset_matrix(); self.final_surface .canvas() .clear(self.background_color) @@ -162,6 +176,8 @@ impl RenderState { Some(&skia::Paint::default()), ); + self.shadow_surface.canvas().clear(skia::Color::TRANSPARENT); + self.drawing_surface .canvas() .clear(skia::Color::TRANSPARENT); @@ -214,6 +230,10 @@ impl RenderState { .clip_rect(shape.bounds(), skia::ClipOp::Intersect, true); } + for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) { + shadows::render_drop_shadow(self, shadow, self.viewbox.zoom * self.options.dpr()); + } + self.apply_drawing_to_final_canvas(); } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs new file mode 100644 index 000000000..f20532488 --- /dev/null +++ b/render-wasm/src/render/shadows.rs @@ -0,0 +1,25 @@ +use skia_safe::{self as skia}; + +use super::RenderState; +use crate::shapes::Shadow; + +pub fn render_drop_shadow(render_state: &mut RenderState, shadow: &Shadow, scale: f32) { + let shadow_paint = shadow.to_paint(true, scale); + render_state.drawing_surface.draw( + &mut render_state.shadow_surface.canvas(), + (0.0, 0.0), + skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), + Some(&shadow_paint), + ); + + render_state.shadow_surface.draw( + &mut render_state.final_surface.canvas(), + (0.0, 0.0), + skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), + Some(&skia::Paint::default()), + ); + render_state + .shadow_surface + .canvas() + .clear(skia::Color::TRANSPARENT); +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 84d01dc51..0ac1843ea 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -10,6 +10,7 @@ mod bools; mod fills; mod matrix; mod paths; +mod shadows; mod strokes; mod svgraw; @@ -18,6 +19,7 @@ pub use bools::*; pub use fills::*; use matrix::*; pub use paths::*; +pub use shadows::*; pub use strokes::*; pub use svgraw::*; @@ -53,6 +55,7 @@ pub struct Shape { pub hidden: bool, pub svg: Option, pub svg_attrs: HashMap, + shadows: Vec, } impl Shape { @@ -73,6 +76,7 @@ impl Shape { blur: Blur::default(), svg: None, svg_attrs: HashMap::new(), + shadows: vec![], } } @@ -305,6 +309,20 @@ impl Shape { !matches!(self.kind, Kind::SVGRaw(_)) } + pub fn add_shadow(&mut self, shadow: Shadow) { + self.shadows.push(shadow); + } + + pub fn clear_shadows(&mut self) { + self.shadows.clear(); + } + + pub fn drop_shadows(&self) -> impl DoubleEndedIterator { + self.shadows + .iter() + .filter(|shadow| shadow.style() == ShadowStyle::Drop) + } + pub fn to_path_transform(&self) -> Option { match self.kind { Kind::Path(_) | Kind::Bool(_, _) => { diff --git a/render-wasm/src/shapes/shadows.rs b/render-wasm/src/shapes/shadows.rs new file mode 100644 index 000000000..94f020979 --- /dev/null +++ b/render-wasm/src/shapes/shadows.rs @@ -0,0 +1,86 @@ +use skia_safe::{self as skia, image_filters}; + +use super::Color; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ShadowStyle { + Drop, + Inner, +} + +impl From for ShadowStyle { + fn from(value: u8) -> Self { + match value { + 0 => Self::Drop, + 1 => Self::Inner, + _ => Self::default(), + } + } +} + +impl Default for ShadowStyle { + fn default() -> Self { + Self::Drop + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Shadow { + color: Color, + blur: f32, + spread: f32, + offset: (f32, f32), + style: ShadowStyle, + hidden: bool, +} + +// TODO: create shadows out of a chunk of bytes +impl Shadow { + pub fn new( + color: Color, + blur: f32, + spread: f32, + offset: (f32, f32), + style: ShadowStyle, + hidden: bool, + ) -> Self { + Self { + color, + blur, + spread, + offset, + style, + hidden, + } + } + + pub fn style(&self) -> ShadowStyle { + self.style + } + + pub fn hidden(&self) -> bool { + self.hidden + } + + pub fn to_paint(&self, dilate: bool, scale: f32) -> skia::Paint { + let mut paint = skia::Paint::default(); + let mut filter = image_filters::drop_shadow_only( + (self.offset.0 * scale, self.offset.1 * scale), + (self.blur * scale, self.blur * scale), + self.color, + None, + None, + None, + ); + + if dilate { + filter = + image_filters::dilate((self.spread * scale, self.spread * scale), filter, None); + } + + paint.set_image_filter(filter); + paint.set_anti_alias(true); + + paint + } +}