mirror of
https://github.com/penpot/penpot.git
synced 2025-01-22 14:39:45 -05:00
Merge pull request #5374 from penpot/superalex-render-wasm-fill-images
🎉 Render wasm fill images
This commit is contained in:
commit
5e9f533624
7 changed files with 237 additions and 38 deletions
|
@ -13,6 +13,9 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.render-wasm.helpers :as h]
|
[app.render-wasm.helpers :as h]
|
||||||
[app.util.functions :as fns]
|
[app.util.functions :as fns]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[app.util.webapi :as wapi]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
[goog.object :as gobj]
|
[goog.object :as gobj]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
@ -109,17 +112,45 @@
|
||||||
(aget buffer 3))))
|
(aget buffer 3))))
|
||||||
shape-ids))
|
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
|
(defn set-shape-fills
|
||||||
[fills]
|
[fills]
|
||||||
(h/call internal-module "_clear_shape_fills")
|
(h/call internal-module "_clear_shape_fills")
|
||||||
(run! (fn [fill]
|
(keep (fn [fill]
|
||||||
(let [opacity (or (:fill-opacity fill) 1.0)
|
(let [opacity (or (:fill-opacity fill) 1.0)
|
||||||
color (:fill-color fill)
|
color (:fill-color fill)
|
||||||
gradient (:fill-color-gradient fill)]
|
gradient (:fill-color-gradient fill)
|
||||||
(when ^boolean color
|
image (:fill-image fill)]
|
||||||
|
|
||||||
|
(cond
|
||||||
|
(some? color)
|
||||||
(let [rgba (rgba-from-hex color opacity)]
|
(let [rgba (rgba-from-hex color opacity)]
|
||||||
(h/call internal-module "_add_shape_solid_fill" rgba)))
|
(h/call internal-module "_add_shape_solid_fill" rgba))
|
||||||
(when (and (some? gradient) (= (:type gradient) :linear))
|
|
||||||
|
(and (some? gradient) (= (:type gradient) :linear))
|
||||||
(let [stops (:stops gradient)
|
(let [stops (:stops gradient)
|
||||||
n-stops (count stops)
|
n-stops (count stops)
|
||||||
mem-size (* 5 n-stops)
|
mem-size (* 5 n-stops)
|
||||||
|
@ -137,8 +168,22 @@
|
||||||
offset (:offset stop)]
|
offset (:offset stop)]
|
||||||
[r g b a (* 100 offset)]))
|
[r g b a (* 100 offset)]))
|
||||||
stops)))))
|
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))
|
fills))
|
||||||
|
|
||||||
(defn- translate-blend-mode
|
(defn- translate-blend-mode
|
||||||
|
@ -183,28 +228,35 @@
|
||||||
(defn set-objects
|
(defn set-objects
|
||||||
[objects]
|
[objects]
|
||||||
(let [shapes (into [] (vals objects))
|
(let [shapes (into [] (vals objects))
|
||||||
total-shapes (count shapes)]
|
total-shapes (count shapes)
|
||||||
(loop [index 0]
|
pending
|
||||||
(when (< index total-shapes)
|
(loop [index 0 pending []]
|
||||||
(let [shape (nth shapes index)
|
(if (< index total-shapes)
|
||||||
id (dm/get-prop shape :id)
|
(let [shape (nth shapes index)
|
||||||
selrect (dm/get-prop shape :selrect)
|
id (dm/get-prop shape :id)
|
||||||
rotation (dm/get-prop shape :rotation)
|
selrect (dm/get-prop shape :selrect)
|
||||||
transform (dm/get-prop shape :transform)
|
rotation (dm/get-prop shape :rotation)
|
||||||
fills (dm/get-prop shape :fills)
|
transform (dm/get-prop shape :transform)
|
||||||
children (dm/get-prop shape :shapes)
|
fills (dm/get-prop shape :fills)
|
||||||
blend-mode (dm/get-prop shape :blend-mode)
|
children (dm/get-prop shape :shapes)
|
||||||
opacity (dm/get-prop shape :opacity)]
|
blend-mode (dm/get-prop shape :blend-mode)
|
||||||
(use-shape id)
|
opacity (dm/get-prop shape :opacity)]
|
||||||
(set-shape-selrect selrect)
|
(use-shape id)
|
||||||
(set-shape-rotation rotation)
|
(set-shape-selrect selrect)
|
||||||
(set-shape-transform transform)
|
(set-shape-rotation rotation)
|
||||||
(set-shape-fills fills)
|
(set-shape-transform transform)
|
||||||
(set-shape-blend-mode blend-mode)
|
(set-shape-blend-mode blend-mode)
|
||||||
(set-shape-children children)
|
(set-shape-children children)
|
||||||
(set-shape-opacity opacity)
|
(set-shape-opacity opacity)
|
||||||
(recur (inc index))))))
|
(let [pending-fills (doall (set-shape-fills fills))]
|
||||||
(request-render))
|
(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
|
(def ^:private canvas-options
|
||||||
#js {:antialias false
|
#js {:antialias false
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
use skia_safe as skia;
|
|
||||||
|
|
||||||
pub type Image = skia::Image;
|
|
|
@ -1,5 +1,4 @@
|
||||||
mod debug;
|
mod debug;
|
||||||
mod images;
|
|
||||||
mod math;
|
mod math;
|
||||||
pub mod mem;
|
pub mod mem;
|
||||||
mod render;
|
mod render;
|
||||||
|
@ -10,6 +9,7 @@ mod view;
|
||||||
|
|
||||||
use skia_safe as skia;
|
use skia_safe as skia;
|
||||||
|
|
||||||
|
use crate::shapes::Image;
|
||||||
use crate::state::State;
|
use crate::state::State;
|
||||||
use crate::utils::uuid_from_u32_quartet;
|
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::<u8>::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::<u8>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[no_mangle]
|
||||||
pub extern "C" fn clear_shape_fills() {
|
pub extern "C" fn clear_shape_fills() {
|
||||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||||
|
|
|
@ -5,8 +5,9 @@ use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::debug;
|
use crate::debug;
|
||||||
use crate::images::Image;
|
use crate::shapes::Fill;
|
||||||
use crate::shapes::Shape;
|
use crate::shapes::Shape;
|
||||||
|
use crate::shapes::{draw_image_in_container, Image};
|
||||||
use crate::view::Viewbox;
|
use crate::view::Viewbox;
|
||||||
|
|
||||||
struct GpuState {
|
struct GpuState {
|
||||||
|
@ -97,6 +98,7 @@ pub(crate) struct RenderState {
|
||||||
pub cached_surface_image: Option<CachedSurfaceImage>,
|
pub cached_surface_image: Option<CachedSurfaceImage>,
|
||||||
options: RenderOptions,
|
options: RenderOptions,
|
||||||
pub viewbox: Viewbox,
|
pub viewbox: Viewbox,
|
||||||
|
pub images: HashMap<String, Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderState {
|
impl RenderState {
|
||||||
|
@ -119,6 +121,7 @@ impl RenderState {
|
||||||
cached_surface_image: None,
|
cached_surface_image: None,
|
||||||
options: RenderOptions::default(),
|
options: RenderOptions::default(),
|
||||||
viewbox: Viewbox::new(width as f32, height as f32),
|
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);
|
self.drawing_surface.canvas().concat(&matrix);
|
||||||
|
|
||||||
for fill in shape.fills().rev() {
|
for fill in shape.fills().rev() {
|
||||||
self.drawing_surface
|
if let Fill::Image(image_fill) = fill {
|
||||||
.canvas()
|
let image = self.images.get(&image_fill.id.to_string());
|
||||||
.draw_rect(shape.selrect, &fill.to_paint(&shape.selrect));
|
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();
|
let mut paint = skia::Paint::default();
|
||||||
|
|
|
@ -4,8 +4,10 @@ use uuid::Uuid;
|
||||||
|
|
||||||
mod blend;
|
mod blend;
|
||||||
mod fills;
|
mod fills;
|
||||||
|
mod images;
|
||||||
pub use blend::*;
|
pub use blend::*;
|
||||||
pub use fills::*;
|
pub use fills::*;
|
||||||
|
pub use images::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Kind {
|
pub enum Kind {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use skia_safe as skia;
|
||||||
|
|
||||||
use super::Color;
|
use super::Color;
|
||||||
use crate::math;
|
use crate::math;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Gradient {
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Fill {
|
pub enum Fill {
|
||||||
Solid(Color),
|
Solid(Color),
|
||||||
LinearGradient(Gradient),
|
LinearGradient(Gradient),
|
||||||
|
Image(ImageFill),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fill {
|
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 {
|
pub fn to_paint(&self, rect: &math::Rect) -> skia::Paint {
|
||||||
match self {
|
match self {
|
||||||
Self::Solid(color) => {
|
Self::Solid(color) => {
|
||||||
|
@ -75,6 +94,14 @@ impl Fill {
|
||||||
p.set_blend_mode(skia::BlendMode::SrcOver);
|
p.set_blend_mode(skia::BlendMode::SrcOver);
|
||||||
p
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
52
render-wasm/src/shapes/images.rs
Normal file
52
render-wasm/src/shapes/images.rs
Normal file
|
@ -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();
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue