mirror of
https://github.com/penpot/penpot.git
synced 2025-06-01 23:51:39 +02:00
🎉 Basic wasm support for svg attrs and svg defs
This commit is contained in:
parent
751df46dc9
commit
79df616108
12 changed files with 452 additions and 60 deletions
|
@ -361,6 +361,21 @@ pub extern "C" fn clear_shape_fills() {
|
|||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_svg_raw_content() {
|
||||
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
let bytes = mem::bytes();
|
||||
let svg_raw_content = String::from_utf8(bytes)
|
||||
.unwrap()
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
shape
|
||||
.set_svg_raw_content(svg_raw_content)
|
||||
.expect("Failed to set svg raw content");
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_blend_mode(mode: i32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
|
||||
|
@ -509,6 +524,24 @@ pub extern "C" fn add_shape_stroke_stops(ptr: *mut shapes::RawStopData, n_stops:
|
|||
}
|
||||
}
|
||||
|
||||
// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`.
|
||||
// Updates the `start` index to the end of the extracted string.
|
||||
fn extract_string(start: &mut usize, bytes: &[u8]) -> String {
|
||||
match bytes[*start..].iter().position(|&b| b == 0) {
|
||||
Some(pos) => {
|
||||
let end = *start + pos;
|
||||
let slice = &bytes[*start..end];
|
||||
*start = end + 1; // Move the `start` pointer past the null byte
|
||||
// Call to unsafe function within an unsafe block
|
||||
unsafe { String::from_utf8_unchecked(slice.to_vec()) }
|
||||
}
|
||||
None => {
|
||||
*start = bytes.len(); // Move `start` to the end if no null byte is found
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_image_stroke(
|
||||
a: u32,
|
||||
|
@ -544,7 +577,22 @@ pub extern "C" fn clear_shape_strokes() {
|
|||
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape.set_corners((r1, r2, r3, r4))
|
||||
shape.set_corners((r1, r2, r3, r4));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_path_attrs(num_attrs: u32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
|
||||
|
||||
if let Some(shape) = state.current_shape() {
|
||||
let bytes = mem::bytes();
|
||||
let mut start = 0;
|
||||
for _ in 0..num_attrs {
|
||||
let name = extract_string(&mut start, &bytes);
|
||||
let value = extract_string(&mut start, &bytes);
|
||||
shape.set_path_attr(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use skia::Contains;
|
||||
use skia_safe as skia;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::math;
|
||||
|
@ -33,6 +32,7 @@ pub trait Renderable {
|
|||
fn clip(&self) -> bool;
|
||||
fn children_ids(&self) -> Vec<Uuid>;
|
||||
fn image_filter(&self, scale: f32) -> Option<skia::ImageFilter>;
|
||||
fn is_recursive(&self) -> bool;
|
||||
}
|
||||
|
||||
pub(crate) struct CachedSurfaceImage {
|
||||
|
@ -191,6 +191,7 @@ impl RenderState {
|
|||
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest),
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
|
||||
self.drawing_surface
|
||||
.canvas()
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
@ -336,8 +337,8 @@ impl RenderState {
|
|||
|
||||
// Returns a boolean indicating if the viewbox contains the rendered shapes
|
||||
fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool {
|
||||
let element = tree.get(&root_id).unwrap();
|
||||
let mut is_complete = self.viewbox.area.contains(element.bounds());
|
||||
if let Some(element) = tree.get(&root_id) {
|
||||
let mut is_complete = self.viewbox.area.contains(element.bounds());
|
||||
|
||||
if !root_id.is_nil() {
|
||||
if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
|
||||
|
@ -348,40 +349,45 @@ impl RenderState {
|
|||
} else {
|
||||
self.render_debug_element(element, true);
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
let filter = element.image_filter(self.viewbox.zoom * self.options.dpr());
|
||||
if let Some(image_filter) = filter {
|
||||
paint.set_image_filter(image_filter);
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
// This is needed so the next non-children shape does not carry this shape's transform
|
||||
self.final_surface.canvas().save_layer(&layer_rec);
|
||||
self.drawing_surface.canvas().save();
|
||||
|
||||
if !root_id.is_nil() {
|
||||
self.render_single_element(element);
|
||||
if element.clip() {
|
||||
self.drawing_surface.canvas().clip_rect(
|
||||
element.bounds(),
|
||||
skia::ClipOp::Intersect,
|
||||
true,
|
||||
);
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
let filter = element.image_filter(self.viewbox.zoom * self.options.dpr());
|
||||
if let Some(image_filter) = filter {
|
||||
paint.set_image_filter(image_filter);
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
// This is needed so the next non-children shape does not carry this shape's transform
|
||||
self.final_surface.canvas().save_layer(&layer_rec);
|
||||
self.drawing_surface.canvas().save();
|
||||
|
||||
if !root_id.is_nil() {
|
||||
self.render_single_element(element);
|
||||
if element.clip() {
|
||||
self.drawing_surface.canvas().clip_rect(
|
||||
element.bounds(),
|
||||
skia::ClipOp::Intersect,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// draw all the children shapes
|
||||
if element.is_recursive() {
|
||||
for id in element.children_ids() {
|
||||
is_complete = self.render_shape_tree(&id, tree) && is_complete;
|
||||
}
|
||||
}
|
||||
|
||||
self.final_surface.canvas().restore();
|
||||
self.drawing_surface.canvas().restore();
|
||||
|
||||
return is_complete;
|
||||
} else {
|
||||
eprintln!("Error: Element with root_id {root_id} not found in the tree.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// draw all the children shapes
|
||||
for id in element.children_ids() {
|
||||
is_complete = self.render_shape_tree(&id, tree) && is_complete;
|
||||
}
|
||||
|
||||
self.final_surface.canvas().restore();
|
||||
self.drawing_surface.canvas().restore();
|
||||
|
||||
return is_complete;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::math;
|
||||
use skia_safe as skia;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::render::{BlendMode, Renderable};
|
||||
|
@ -12,6 +13,7 @@ mod matrix;
|
|||
mod paths;
|
||||
mod renderable;
|
||||
mod strokes;
|
||||
mod svgraw;
|
||||
|
||||
pub use blurs::*;
|
||||
pub use bools::*;
|
||||
|
@ -20,6 +22,7 @@ pub use images::*;
|
|||
use matrix::*;
|
||||
pub use paths::*;
|
||||
pub use strokes::*;
|
||||
pub use svgraw::*;
|
||||
|
||||
pub type CornerRadius = skia::Point;
|
||||
pub type Corners = [CornerRadius; 4];
|
||||
|
@ -30,6 +33,7 @@ pub enum Kind {
|
|||
Circle(math::Rect),
|
||||
Path(Path),
|
||||
Bool(BoolType, Path),
|
||||
SVGRaw(SVGRaw),
|
||||
}
|
||||
|
||||
pub type Color = skia::Color;
|
||||
|
@ -50,6 +54,7 @@ pub struct Shape {
|
|||
blur: Blur,
|
||||
opacity: f32,
|
||||
hidden: bool,
|
||||
svg_attrs: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
|
@ -68,6 +73,7 @@ impl Shape {
|
|||
opacity: 1.,
|
||||
hidden: false,
|
||||
blur: Blur::default(),
|
||||
svg_attrs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,6 +202,20 @@ impl Shape {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_path_attr(&mut self, name: String, value: String) {
|
||||
match &mut self.kind {
|
||||
Kind::Path(_) => {
|
||||
self.set_svg_attr(name, value);
|
||||
}
|
||||
Kind::Rect(_) | Kind::Circle(_) | Kind::SVGRaw(_) => todo!(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
|
||||
self.kind = Kind::SVGRaw(SVGRaw::from_content(content));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_blend_mode(&mut self, mode: BlendMode) {
|
||||
self.blend_mode = mode;
|
||||
}
|
||||
|
@ -230,6 +250,10 @@ impl Shape {
|
|||
self.kind = Kind::Rect(self.selrect, corners);
|
||||
}
|
||||
|
||||
pub fn set_svg_attr(&mut self, name: String, value: String) {
|
||||
self.svg_attrs.insert(name, value);
|
||||
}
|
||||
|
||||
fn to_path_transform(&self) -> Option<skia::Matrix> {
|
||||
match self.kind {
|
||||
Kind::Path(_) | Kind::Bool(_, _) => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use skia_safe::{self as skia, RRect};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{BlurType, Corners, Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind};
|
||||
|
@ -93,22 +94,68 @@ impl Renderable for Shape {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_recursive(&self) -> bool {
|
||||
!matches!(self.kind, Kind::SVGRaw(_))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_fills_for_kind(
|
||||
shape: &Shape,
|
||||
canvas: &skia::Canvas,
|
||||
images: &ImageStore,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
svg_attrs: &HashMap<String, String>,
|
||||
) {
|
||||
for fill in shape.fills().rev() {
|
||||
render_fill(
|
||||
canvas,
|
||||
images,
|
||||
fill,
|
||||
shape.selrect,
|
||||
&shape.kind,
|
||||
path_transform,
|
||||
svg_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: remove when strokes are implemented, this is just for testing paths with no fills
|
||||
if shape.fills().len() == 0 {
|
||||
if let Kind::Path(ref path) = shape.kind {
|
||||
let mut p = skia::Paint::default();
|
||||
p.set_style(skia_safe::PaintStyle::Stroke);
|
||||
p.set_stroke_width(2.0);
|
||||
p.set_anti_alias(true);
|
||||
p.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) {
|
||||
p.set_stroke_cap(skia::paint::Cap::Round);
|
||||
}
|
||||
if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) {
|
||||
p.set_stroke_join(skia::paint::Join::Round);
|
||||
}
|
||||
let mut skia_path = &mut path.to_skia_path();
|
||||
skia_path = skia_path.transform(path_transform.unwrap());
|
||||
canvas.draw_path(&skia_path, &p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_fill(
|
||||
surface: &mut skia::Surface,
|
||||
canvas: &skia::Canvas,
|
||||
images: &ImageStore,
|
||||
fill: &Fill,
|
||||
selrect: Rect,
|
||||
kind: &Kind,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
svg_attrs: &HashMap<String, String>,
|
||||
) {
|
||||
match (fill, kind) {
|
||||
(Fill::Image(image_fill), kind) => {
|
||||
let image = images.get(&image_fill.id());
|
||||
if let Some(image) = image {
|
||||
draw_image_fill_in_container(
|
||||
surface.canvas(),
|
||||
canvas,
|
||||
&image,
|
||||
image_fill.size(),
|
||||
kind,
|
||||
|
@ -119,20 +166,26 @@ fn render_fill(
|
|||
}
|
||||
}
|
||||
(_, Kind::Rect(rect, None)) => {
|
||||
surface.canvas().draw_rect(rect, &fill.to_paint(&selrect));
|
||||
canvas.draw_rect(rect, &fill.to_paint(&selrect));
|
||||
}
|
||||
(_, Kind::Rect(rect, Some(corners))) => {
|
||||
let rrect = RRect::new_rect_radii(rect, corners);
|
||||
surface.canvas().draw_rrect(rrect, &fill.to_paint(&selrect));
|
||||
canvas.draw_rrect(rrect, &fill.to_paint(&selrect));
|
||||
}
|
||||
(_, Kind::Circle(rect)) => {
|
||||
surface.canvas().draw_oval(rect, &fill.to_paint(&selrect));
|
||||
canvas.draw_oval(rect, &fill.to_paint(&selrect));
|
||||
}
|
||||
(_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => {
|
||||
surface.canvas().draw_path(
|
||||
&path.to_skia_path().transform(path_transform.unwrap()),
|
||||
&fill.to_paint(&selrect),
|
||||
);
|
||||
let mut skia_path = &mut path.to_skia_path();
|
||||
skia_path = skia_path.transform(path_transform.unwrap());
|
||||
if let Some("evenodd") = svg_attrs.get("fill-rule").map(String::as_str) {
|
||||
skia_path.set_fill_type(skia::PathFillType::EvenOdd);
|
||||
}
|
||||
|
||||
canvas.draw_path(&skia_path, &fill.to_paint(&selrect));
|
||||
}
|
||||
(_, Kind::SVGRaw(_sr)) => {
|
||||
// NOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -514,6 +567,9 @@ pub fn draw_image_fill_in_container(
|
|||
true,
|
||||
);
|
||||
}
|
||||
Kind::SVGRaw(_) => {
|
||||
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.draw_image_rect(image, None, dest_rect, &paint);
|
||||
|
|
10
render-wasm/src/shapes/svgraw.rs
Normal file
10
render-wasm/src/shapes/svgraw.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SVGRaw {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl SVGRaw {
|
||||
pub fn from_content(svg: String) -> SVGRaw {
|
||||
SVGRaw { content: svg }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue