diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 65a5a7fc8..33b32fd57 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -13,6 +13,9 @@ [app.config :as cf] [app.render-wasm.helpers :as h] [app.util.functions :as fns] + [app.util.http :as http] + [app.util.webapi :as wapi] + [beicon.v2.core :as rx] [goog.object :as gobj] [promesa.core :as p])) @@ -109,17 +112,45 @@ (aget buffer 3)))) shape-ids)) +(defn- store-image + [id] + (let [buffer (uuid/get-u32 id) + url (cf/resolve-file-media {:id id})] + (->> (http/send! {:method :get + :uri url + :response-type :blob}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-array-buffer) + (rx/map (fn [image] + (let [image-size (.-byteLength image) + image-ptr (h/call internal-module "_alloc_bytes" image-size) + heap (gobj/get ^js internal-module "HEAPU8") + mem (js/Uint8Array. (.-buffer heap) image-ptr image-size)] + (.set mem (js/Uint8Array. image)) + (h/call internal-module "_store_image" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3) + image-ptr + image-size) + true)))))) + (defn set-shape-fills [fills] (h/call internal-module "_clear_shape_fills") - (run! (fn [fill] - (let [opacity (or (:fill-opacity fill) 1.0) - color (:fill-color fill) - gradient (:fill-color-gradient fill)] - (when ^boolean color + (keep (fn [fill] + (let [opacity (or (:fill-opacity fill) 1.0) + color (:fill-color fill) + gradient (:fill-color-gradient fill) + image (:fill-image fill)] + + (cond + (some? color) (let [rgba (rgba-from-hex color opacity)] - (h/call internal-module "_add_shape_solid_fill" rgba))) - (when (and (some? gradient) (= (:type gradient) :linear)) + (h/call internal-module "_add_shape_solid_fill" rgba)) + + (and (some? gradient) (= (:type gradient) :linear)) (let [stops (:stops gradient) n-stops (count stops) mem-size (* 5 n-stops) @@ -137,8 +168,22 @@ offset (:offset stop)] [r g b a (* 100 offset)])) stops))))) + (h/call internal-module "_add_shape_fill_stops" stops-ptr n-stops)) - (h/call internal-module "_add_shape_fill_stops" stops-ptr n-stops))))) + (some? image) + (let [id (dm/get-prop image :id) + buffer (uuid/get-u32 id) + cached-image? (h/call internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] + (h/call internal-module "_add_shape_image_fill" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3) + opacity + (dm/get-prop image :width) + (dm/get-prop image :height)) + (when (== cached-image? 0) + (store-image id)))))) fills)) (defn- translate-blend-mode @@ -183,28 +228,35 @@ (defn set-objects [objects] (let [shapes (into [] (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) - blend-mode (dm/get-prop shape :blend-mode) - opacity (dm/get-prop shape :opacity)] - (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) - (set-shape-opacity opacity) - (recur (inc index)))))) - (request-render)) + total-shapes (count shapes) + pending + (loop [index 0 pending []] + (if (< 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) + blend-mode (dm/get-prop shape :blend-mode) + opacity (dm/get-prop shape :opacity)] + (use-shape id) + (set-shape-selrect selrect) + (set-shape-rotation rotation) + (set-shape-transform transform) + (set-shape-blend-mode blend-mode) + (set-shape-children children) + (set-shape-opacity opacity) + (let [pending-fills (doall (set-shape-fills fills))] + (recur (inc index) (into pending pending-fills)))) + pending))] + (request-render) + (when-let [pending (seq pending)] + (->> (rx/from pending) + (rx/mapcat identity) + (rx/reduce conj []) + (rx/subs! request-render))))) (def ^:private canvas-options #js {:antialias false diff --git a/render-wasm/src/images.rs b/render-wasm/src/images.rs deleted file mode 100644 index bbf5d600c..000000000 --- a/render-wasm/src/images.rs +++ /dev/null @@ -1,3 +0,0 @@ -use skia_safe as skia; - -pub type Image = skia::Image; diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 80af60b9c..639607598 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,5 +1,4 @@ mod debug; -mod images; mod math; pub mod mem; mod render; @@ -10,6 +9,7 @@ mod view; use skia_safe as skia; +use crate::shapes::Image; use crate::state::State; use crate::utils::uuid_from_u32_quartet; @@ -208,6 +208,59 @@ pub extern "C" fn add_shape_fill_stops(ptr: *mut RawStopData, n_stops: i32) { } } +#[no_mangle] +pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32, ptr: *mut u8, size: u32) { + if ptr.is_null() || size == 0 { + panic!("Invalid data, null pointer or zero size"); + } + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let render_state = state.render_state(); + let id = uuid_from_u32_quartet(a, b, c, d); + unsafe { + let image_bytes = Vec::::from_raw_parts(ptr, size as usize, size as usize); + let image_data = skia::Data::new_copy(&*image_bytes); + match Image::from_encoded(image_data) { + Some(image) => { + render_state.images.insert(id.to_string(), image); + } + None => { + eprintln!("Error on image decoding"); + } + } + mem::free(ptr as *mut u8, size as usize * std::mem::size_of::()); + } +} + +#[no_mangle] +pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32) -> bool { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + let render_state = state.render_state(); + let id = uuid_from_u32_quartet(a, b, c, d); + render_state.images.contains_key(&id.to_string()) +} + +#[no_mangle] +pub extern "C" fn add_shape_image_fill( + a: u32, + b: u32, + c: u32, + d: u32, + alpha: f32, + width: f32, + height: f32, +) { + 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() { + shape.add_fill(shapes::Fill::new_image_fill( + id, + (alpha * 0xff as f32).floor() as u8, + height, + width, + )); + } +} + #[no_mangle] pub extern "C" fn clear_shape_fills() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 499922ae3..8ca2ac935 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -5,8 +5,9 @@ use std::collections::HashMap; use uuid::Uuid; use crate::debug; -use crate::images::Image; +use crate::shapes::Fill; use crate::shapes::Shape; +use crate::shapes::{draw_image_in_container, Image}; use crate::view::Viewbox; struct GpuState { @@ -97,6 +98,7 @@ pub(crate) struct RenderState { pub cached_surface_image: Option, options: RenderOptions, pub viewbox: Viewbox, + pub images: HashMap, } impl RenderState { @@ -119,6 +121,7 @@ impl RenderState { cached_surface_image: None, options: RenderOptions::default(), viewbox: Viewbox::new(width as f32, height as f32), + images: HashMap::with_capacity(2048), } } @@ -210,9 +213,22 @@ impl RenderState { self.drawing_surface.canvas().concat(&matrix); for fill in shape.fills().rev() { - self.drawing_surface - .canvas() - .draw_rect(shape.selrect, &fill.to_paint(&shape.selrect)); + if let Fill::Image(image_fill) = fill { + let image = self.images.get(&image_fill.id.to_string()); + if let Some(image) = image { + draw_image_in_container( + &self.drawing_surface.canvas(), + &image, + (image_fill.width, image_fill.height), + shape.selrect, + &fill.to_paint(&shape.selrect), + ); + } + } else { + self.drawing_surface + .canvas() + .draw_rect(shape.selrect, &fill.to_paint(&shape.selrect)); + } } let mut paint = skia::Paint::default(); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index dfe45266c..c967a4bfe 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -4,8 +4,10 @@ use uuid::Uuid; mod blend; mod fills; +mod images; pub use blend::*; pub use fills::*; +pub use images::*; #[derive(Debug, Clone, Copy)] pub enum Kind { diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 880e6194a..0641979e8 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -2,6 +2,7 @@ use skia_safe as skia; use super::Color; use crate::math; +use uuid::Uuid; #[derive(Debug, Clone, PartialEq)] pub struct Gradient { @@ -40,10 +41,19 @@ impl Gradient { } } +#[derive(Debug, Clone, PartialEq)] +pub struct ImageFill { + pub id: Uuid, + pub alpha: u8, + pub height: f32, + pub width: f32, +} + #[derive(Debug, Clone, PartialEq)] pub enum Fill { Solid(Color), LinearGradient(Gradient), + Image(ImageFill), } impl Fill { @@ -57,6 +67,15 @@ impl Fill { }) } + pub fn new_image_fill(id: Uuid, alpha: u8, height: f32, width: f32) -> Self { + Self::Image(ImageFill { + id, + alpha, + height, + width, + }) + } + pub fn to_paint(&self, rect: &math::Rect) -> skia::Paint { match self { Self::Solid(color) => { @@ -75,6 +94,14 @@ impl Fill { p.set_blend_mode(skia::BlendMode::SrcOver); p } + Self::Image(image_fill) => { + let mut p = skia::Paint::default(); + p.set_style(skia::PaintStyle::Fill); + p.set_anti_alias(true); + p.set_blend_mode(skia::BlendMode::SrcOver); + p.set_alpha(image_fill.alpha); + p + } } } } diff --git a/render-wasm/src/shapes/images.rs b/render-wasm/src/shapes/images.rs new file mode 100644 index 000000000..7ca2a7fc1 --- /dev/null +++ b/render-wasm/src/shapes/images.rs @@ -0,0 +1,52 @@ +use crate::math; +use skia_safe as skia; + +pub type Image = skia::Image; + +pub fn draw_image_in_container( + canvas: &skia::Canvas, + image: &Image, + size: (f32, f32), + container: skia::Rect, + paint: &skia::Paint, +) { + let width = size.0; + let height = size.1; + 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 widther, 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; + + // Calculate offset to center the image in the container + let offset_x = container.left + (container_width - scaled_width) / 2.0; + let offset_y = container.top + (container_height - scaled_height) / 2.0; + + let dest_rect = math::Rect::from_xywh(offset_x, offset_y, scaled_width, scaled_height); + + // Save the current canvas state + canvas.save(); + + // Set the clipping rectangle to the container bounds + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + + // Draw the image with the calculated destination rectangle + canvas.draw_image_rect(image, None, dest_rect, &paint); + + // Restore the canvas to remove the clipping + canvas.restore(); +}