mirror of
https://github.com/penpot/penpot.git
synced 2025-01-21 06:02:32 -05:00
🎉 Basic strokes wasm support
This commit is contained in:
parent
b5e5c4b0dd
commit
beb9120b2b
7 changed files with 577 additions and 38 deletions
|
@ -211,6 +211,70 @@
|
|||
(store-image id))))))
|
||||
fills))
|
||||
|
||||
(defn set-shape-strokes
|
||||
[strokes]
|
||||
(h/call internal-module "_clear_shape_strokes")
|
||||
(keep (fn [stroke]
|
||||
(let [opacity (or (:stroke-opacity stroke) 1.0)
|
||||
color (:stroke-color stroke)
|
||||
gradient (:stroke-color-gradient stroke)
|
||||
image (:stroke-image stroke)
|
||||
width (:stroke-width stroke)
|
||||
align (:stroke-alignment stroke)]
|
||||
(case align
|
||||
:inner (h/call internal-module "_add_shape_inner_stroke" width)
|
||||
:outer (h/call internal-module "_add_shape_outer_stroke" width)
|
||||
(h/call internal-module "_add_shape_center_stroke" width))
|
||||
|
||||
(cond
|
||||
(some? gradient)
|
||||
(let [stops (:stops gradient)
|
||||
n-stops (count stops)
|
||||
mem-size (* 5 n-stops)
|
||||
stops-ptr (h/call internal-module "_alloc_bytes" mem-size)
|
||||
heap (gobj/get ^js internal-module "HEAPU8")
|
||||
mem (js/Uint8Array. (.-buffer heap) stops-ptr mem-size)]
|
||||
(if (= (:type gradient) :linear)
|
||||
(h/call internal-module "_add_shape_stroke_linear_fill"
|
||||
(:start-x gradient)
|
||||
(:start-y gradient)
|
||||
(:end-x gradient)
|
||||
(:end-y gradient)
|
||||
opacity)
|
||||
(h/call internal-module "_add_shape_stroke_radial_fill"
|
||||
(:start-x gradient)
|
||||
(:start-y gradient)
|
||||
(:end-x gradient)
|
||||
(:end-y gradient)
|
||||
opacity
|
||||
(:width gradient)))
|
||||
(.set mem (js/Uint8Array. (clj->js (flatten (map (fn [stop]
|
||||
(let [[r g b a] (rgba-bytes-from-hex (:color stop) (:opacity stop))
|
||||
offset (:offset stop)]
|
||||
[r g b a (* 100 offset)]))
|
||||
stops)))))
|
||||
(h/call internal-module "_add_shape_stroke_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_stroke"
|
||||
(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)))
|
||||
|
||||
(some? color)
|
||||
(let [rgba (rgba-from-hex color opacity)]
|
||||
(h/call internal-module "_add_shape_stroke_solid_fill" rgba)))))
|
||||
strokes))
|
||||
|
||||
(defn set-shape-path-content
|
||||
[content]
|
||||
(let [buffer (path/content->buffer content)
|
||||
|
@ -280,6 +344,8 @@
|
|||
transform (dm/get-prop shape :transform)
|
||||
fills (if (= type :group)
|
||||
[] (dm/get-prop shape :fills))
|
||||
strokes (if (= type :group)
|
||||
[] (dm/get-prop shape :strokes))
|
||||
children (dm/get-prop shape :shapes)
|
||||
blend-mode (dm/get-prop shape :blend-mode)
|
||||
opacity (dm/get-prop shape :opacity)
|
||||
|
@ -297,8 +363,8 @@
|
|||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
(when (and (some? content) (= type :path)) (set-shape-path-content content))
|
||||
(let [pending-fills (doall (set-shape-fills fills))]
|
||||
(recur (inc index) (into pending pending-fills))))
|
||||
(let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))]
|
||||
(recur (inc index) (into pending pending'))))
|
||||
pending))]
|
||||
(request-render)
|
||||
(when-let [pending (seq pending)]
|
||||
|
|
|
@ -117,6 +117,7 @@
|
|||
: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)
|
||||
|
|
|
@ -245,7 +245,7 @@ pub extern "C" fn add_shape_fill_stops(ptr: *mut shapes::RawStopData, n_stops: u
|
|||
unsafe {
|
||||
let buffer = Vec::<shapes::RawStopData>::from_raw_parts(ptr, len, len);
|
||||
shape
|
||||
.add_gradient_stops(buffer)
|
||||
.add_fill_gradient_stops(buffer)
|
||||
.expect("could not add gradient stops");
|
||||
mem::free_bytes();
|
||||
}
|
||||
|
@ -346,6 +346,131 @@ pub extern "C" fn set_shape_path_content() {
|
|||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_center_stroke(width: f32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape.add_stroke(shapes::Stroke::new_center_stroke(width))
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_inner_stroke(width: f32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape.add_stroke(shapes::Stroke::new_inner_stroke(width))
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_outer_stroke(width: f32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape.add_stroke(shapes::Stroke::new_outer_stroke(width))
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_stroke_solid_fill(raw_color: u32) {
|
||||
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);
|
||||
shape
|
||||
.set_stroke_fill(shapes::Fill::Solid(color))
|
||||
.expect("could not add stroke solid fill");
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_stroke_linear_fill(
|
||||
start_x: f32,
|
||||
start_y: f32,
|
||||
end_x: f32,
|
||||
end_y: f32,
|
||||
opacity: f32,
|
||||
) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape
|
||||
.set_stroke_fill(shapes::Fill::new_linear_gradient(
|
||||
(start_x, start_y),
|
||||
(end_x, end_y),
|
||||
opacity,
|
||||
))
|
||||
.expect("could not add stroke linear fill");
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_stroke_radial_fill(
|
||||
start_x: f32,
|
||||
start_y: f32,
|
||||
end_x: f32,
|
||||
end_y: f32,
|
||||
opacity: f32,
|
||||
width: f32,
|
||||
) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape
|
||||
.set_stroke_fill(shapes::Fill::new_radial_gradient(
|
||||
(start_x, start_y),
|
||||
(end_x, end_y),
|
||||
opacity,
|
||||
width,
|
||||
))
|
||||
.expect("could not add stroke radial fill");
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_stroke_stops(ptr: *mut shapes::RawStopData, n_stops: u32) {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
|
||||
if let Some(shape) = state.current_shape() {
|
||||
let len = n_stops as usize;
|
||||
|
||||
unsafe {
|
||||
let buffer = Vec::<shapes::RawStopData>::from_raw_parts(ptr, len, len);
|
||||
shape
|
||||
.add_stroke_gradient_stops(buffer)
|
||||
.expect("could not add gradient stops");
|
||||
mem::free_bytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_image_stroke(
|
||||
a: u32,
|
||||
b: u32,
|
||||
c: u32,
|
||||
d: u32,
|
||||
alpha: f32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) {
|
||||
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
|
||||
.set_stroke_fill(shapes::Fill::new_image_fill(
|
||||
id,
|
||||
(alpha * 0xff as f32).floor() as u8,
|
||||
(width, height),
|
||||
))
|
||||
.expect("could not add stroke image fill");
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn clear_shape_strokes() {
|
||||
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||
if let Some(shape) = state.current_shape() {
|
||||
shape.clear_strokes();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_gl();
|
||||
}
|
||||
|
|
|
@ -9,11 +9,13 @@ mod images;
|
|||
mod matrix;
|
||||
mod paths;
|
||||
mod renderable;
|
||||
mod strokes;
|
||||
|
||||
pub use fills::*;
|
||||
pub use images::*;
|
||||
use matrix::*;
|
||||
pub use paths::*;
|
||||
pub use strokes::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Kind {
|
||||
|
@ -35,6 +37,7 @@ pub struct Shape {
|
|||
rotation: f32,
|
||||
clip_content: bool,
|
||||
fills: Vec<Fill>,
|
||||
strokes: Vec<Stroke>,
|
||||
blend_mode: BlendMode,
|
||||
opacity: f32,
|
||||
hidden: bool,
|
||||
|
@ -51,6 +54,7 @@ impl Shape {
|
|||
rotation: 0.,
|
||||
clip_content: true,
|
||||
fills: vec![],
|
||||
strokes: vec![],
|
||||
blend_mode: BlendMode::default(),
|
||||
opacity: 1.,
|
||||
hidden: false,
|
||||
|
@ -114,7 +118,7 @@ impl Shape {
|
|||
self.fills.clear();
|
||||
}
|
||||
|
||||
pub fn add_gradient_stops(&mut self, buffer: Vec<RawStopData>) -> Result<(), String> {
|
||||
pub fn add_fill_gradient_stops(&mut self, buffer: Vec<RawStopData>) -> Result<(), String> {
|
||||
let fill = self.fills.last_mut().ok_or("Shape has no fills")?;
|
||||
let gradient = match fill {
|
||||
Fill::LinearGradient(g) => Ok(g),
|
||||
|
@ -129,6 +133,40 @@ impl Shape {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn strokes(&self) -> std::slice::Iter<Stroke> {
|
||||
self.strokes.iter()
|
||||
}
|
||||
|
||||
pub fn add_stroke(&mut self, s: Stroke) {
|
||||
self.strokes.push(s)
|
||||
}
|
||||
|
||||
pub fn set_stroke_fill(&mut self, f: Fill) -> Result<(), String> {
|
||||
let stroke = self.strokes.last_mut().ok_or("Shape has no strokes")?;
|
||||
stroke.fill = f;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_stroke_gradient_stops(&mut self, buffer: Vec<RawStopData>) -> Result<(), String> {
|
||||
let stroke = self.strokes.last_mut().ok_or("Shape has no strokes")?;
|
||||
let fill = &mut stroke.fill;
|
||||
let gradient = match fill {
|
||||
Fill::LinearGradient(g) => Ok(g),
|
||||
Fill::RadialGradient(g) => Ok(g),
|
||||
_ => Err("Active stroke is not a gradient"),
|
||||
}?;
|
||||
|
||||
for stop in buffer.into_iter() {
|
||||
gradient.add_stop(stop.color(), stop.offset());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_strokes(&mut self) {
|
||||
self.strokes.clear();
|
||||
}
|
||||
|
||||
pub fn set_path_segments(&mut self, buffer: Vec<RawPathData>) -> Result<(), String> {
|
||||
let p = Path::try_from(buffer)?;
|
||||
self.kind = Kind::Path(p);
|
||||
|
|
|
@ -75,6 +75,18 @@ pub struct Path {
|
|||
skia_path: skia::Path,
|
||||
}
|
||||
|
||||
fn starts_and_ends_at_same_point(path: &skia::Path) -> bool {
|
||||
if path.count_points() < 2 {
|
||||
return false; // A path with fewer than 2 points cannot be closed
|
||||
}
|
||||
|
||||
let start_point = path.get_point(0); // First point of the path
|
||||
let end_point = path.get_point(path.count_points() - 1); // Last point of the path
|
||||
|
||||
// Compare the start and end points
|
||||
start_point == end_point
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<RawPathData>> for Path {
|
||||
type Error = String;
|
||||
|
||||
|
@ -102,6 +114,10 @@ impl TryFrom<Vec<RawPathData>> for Path {
|
|||
}
|
||||
}
|
||||
|
||||
if !skia_path.is_last_contour_closed() && starts_and_ends_at_same_point(&skia_path) {
|
||||
skia_path.close();
|
||||
}
|
||||
|
||||
Ok(Path {
|
||||
segments,
|
||||
skia_path,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use skia_safe as skia;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{Fill, Image, Kind, Shape};
|
||||
use super::{Fill, Image, Kind, Path, Shape, Stroke, StrokeKind};
|
||||
use crate::math::Rect;
|
||||
use crate::render::{ImageStore, Renderable};
|
||||
|
||||
|
@ -29,9 +29,16 @@ impl Renderable for Shape {
|
|||
);
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(self.blend_mode.into());
|
||||
paint.set_alpha_f(self.opacity);
|
||||
for stroke in self.strokes().rev() {
|
||||
render_stroke(
|
||||
surface,
|
||||
images,
|
||||
stroke,
|
||||
self.selrect,
|
||||
&self.kind,
|
||||
self.to_path_transform().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -73,7 +80,7 @@ fn render_fill(
|
|||
(Fill::Image(image_fill), kind) => {
|
||||
let image = images.get(&image_fill.id());
|
||||
if let Some(image) = image {
|
||||
draw_image_in_container(
|
||||
draw_image_fill_in_container(
|
||||
surface.canvas(),
|
||||
&image,
|
||||
image_fill.size(),
|
||||
|
@ -99,7 +106,124 @@ fn render_fill(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn draw_image_in_container(
|
||||
fn render_stroke(
|
||||
surface: &mut skia::Surface,
|
||||
images: &ImageStore,
|
||||
stroke: &Stroke,
|
||||
selrect: Rect,
|
||||
kind: &Kind,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
) {
|
||||
if let Fill::Image(image_fill) = &stroke.fill {
|
||||
if let Some(image) = images.get(&image_fill.id()) {
|
||||
draw_image_stroke_in_container(
|
||||
surface.canvas(),
|
||||
&image,
|
||||
stroke,
|
||||
image_fill.size(),
|
||||
kind,
|
||||
&selrect,
|
||||
path_transform,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
match kind {
|
||||
Kind::Rect(rect) => draw_stroke_on_rect(surface.canvas(), stroke, rect, &selrect),
|
||||
Kind::Circle(rect) => draw_stroke_on_circle(surface.canvas(), stroke, rect, &selrect),
|
||||
Kind::Path(path) => {
|
||||
draw_stroke_on_path(surface.canvas(), stroke, path, &selrect, path_transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_stroke_on_rect(canvas: &skia::Canvas, stroke: &Stroke, rect: &Rect, selrect: &Rect) {
|
||||
// Draw the different kind of strokes for a rect is perry straightforward, we just need apply a stroke to:
|
||||
// - The same rect if it's a center stroke
|
||||
// - A bigger rect if it's an outer stroke
|
||||
// - A smaller rect if it's an outer stroke
|
||||
let stroke_rect = stroke.outer_rect(rect);
|
||||
canvas.draw_rect(&stroke_rect, &stroke.to_paint(selrect));
|
||||
}
|
||||
|
||||
fn draw_stroke_on_circle(canvas: &skia::Canvas, stroke: &Stroke, rect: &Rect, selrect: &Rect) {
|
||||
// Draw the different kind of strokes for an oval is perry straightforward, we just need apply a stroke to:
|
||||
// - The same oval if it's a center stroke
|
||||
// - A bigger oval if it's an outer stroke
|
||||
// - A smaller oval if it's an outer stroke
|
||||
let stroke_rect = stroke.outer_rect(rect);
|
||||
canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect));
|
||||
}
|
||||
|
||||
fn draw_stroke_on_path(
|
||||
canvas: &skia::Canvas,
|
||||
stroke: &Stroke,
|
||||
path: &Path,
|
||||
selrect: &Rect,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
) {
|
||||
let mut skia_path = path.to_skia_path();
|
||||
skia_path.transform(path_transform.unwrap());
|
||||
|
||||
let paint_stroke = stroke.to_stroked_paint(selrect);
|
||||
// Draw the different kind of strokes for a path requires different strategies:
|
||||
match stroke.kind {
|
||||
// For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
|
||||
StrokeKind::InnerStroke => {
|
||||
canvas.clip_path(&skia_path, skia::ClipOp::Intersect, true);
|
||||
canvas.draw_path(&skia_path, &paint_stroke);
|
||||
}
|
||||
// For center stroke we don't need to do anything extra
|
||||
StrokeKind::CenterStroke => {
|
||||
canvas.draw_path(&skia_path, &paint_stroke);
|
||||
}
|
||||
// For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
|
||||
StrokeKind::OuterStroke => {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
paint.set_anti_alias(true);
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
|
||||
canvas.draw_path(&skia_path, &paint_stroke);
|
||||
|
||||
let mut clear_paint = skia::Paint::default();
|
||||
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
||||
clear_paint.set_anti_alias(true);
|
||||
canvas.draw_path(&skia_path, &clear_paint);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_scaled_rect(size: (i32, i32), container: &Rect, delta: f32) -> Rect {
|
||||
let (width, height) = (size.0 as f32, size.1 as f32);
|
||||
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;
|
||||
|
||||
let scale = if image_aspect_ratio > container_aspect_ratio {
|
||||
container_height / height
|
||||
} else {
|
||||
container_width / width
|
||||
};
|
||||
|
||||
let scaled_width = width * scale;
|
||||
let scaled_height = height * scale;
|
||||
|
||||
Rect::from_xywh(
|
||||
container.left - delta - (scaled_width - container_width) / 2.0,
|
||||
container.top - delta - (scaled_height - container_height) / 2.0,
|
||||
scaled_width + (2. * delta) + (scaled_width - container_width),
|
||||
scaled_height + (2. * delta) + (scaled_width - container_width),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn draw_image_fill_in_container(
|
||||
canvas: &skia::Canvas,
|
||||
image: &Image,
|
||||
size: (i32, i32),
|
||||
|
@ -108,34 +232,8 @@ pub fn draw_image_in_container(
|
|||
container: &Rect,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
) {
|
||||
let width = size.0 as f32;
|
||||
let height = size.1 as f32;
|
||||
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 wider, 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;
|
||||
|
||||
let dest_rect = Rect::from_xywh(
|
||||
container.left - (scaled_width - container_width) / 2.0,
|
||||
container.top - (scaled_height - container_height) / 2.0,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
);
|
||||
// Compute scaled rect
|
||||
let dest_rect = calculate_scaled_rect(size, container, 0.);
|
||||
|
||||
// Save the current canvas state
|
||||
canvas.save();
|
||||
|
@ -165,3 +263,69 @@ pub fn draw_image_in_container(
|
|||
// Restore the canvas to remove the clipping
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn draw_image_stroke_in_container(
|
||||
canvas: &skia::Canvas,
|
||||
image: &Image,
|
||||
stroke: &Stroke,
|
||||
size: (i32, i32),
|
||||
kind: &Kind,
|
||||
container: &Rect,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
) {
|
||||
// Helper to handle drawing based on kind
|
||||
fn draw_kind(
|
||||
canvas: &skia::Canvas,
|
||||
kind: &Kind,
|
||||
stroke: &Stroke,
|
||||
container: &Rect,
|
||||
path_transform: Option<&skia::Matrix>,
|
||||
) {
|
||||
let outer_rect = stroke.outer_rect(container);
|
||||
match kind {
|
||||
Kind::Rect(rect) => draw_stroke_on_rect(canvas, stroke, rect, &outer_rect),
|
||||
Kind::Circle(rect) => draw_stroke_on_circle(canvas, stroke, rect, &outer_rect),
|
||||
Kind::Path(p) => {
|
||||
let mut path = p.to_skia_path();
|
||||
path.transform(path_transform.unwrap());
|
||||
if stroke.kind == StrokeKind::InnerStroke {
|
||||
canvas.clip_path(&path, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
let paint = stroke.to_stroked_paint(&outer_rect);
|
||||
canvas.draw_path(&path, &paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save canvas and layer state
|
||||
let mut pb = skia::Paint::default();
|
||||
pb.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
pb.set_anti_alias(true);
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&pb);
|
||||
canvas.save_layer(&layer_rec);
|
||||
|
||||
// Draw the stroke based on the kind, we are using this stroke as a "selector" of the area of the image we want to show.
|
||||
draw_kind(canvas, kind, stroke, container, path_transform);
|
||||
|
||||
// Draw the image. We are using now the SrcIn blend mode, so the rendered piece of image will the area of the stroke over the image.
|
||||
let mut image_paint = skia::Paint::default();
|
||||
image_paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
image_paint.set_anti_alias(true);
|
||||
// Compute scaled rect and clip to it
|
||||
let dest_rect = calculate_scaled_rect(size, container, stroke.delta());
|
||||
canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, true);
|
||||
canvas.draw_image_rect(image, None, dest_rect, &image_paint);
|
||||
|
||||
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
|
||||
if let (Kind::Path(p), StrokeKind::OuterStroke) = (kind, &stroke.kind) {
|
||||
let mut path = p.to_skia_path();
|
||||
path.transform(path_transform.unwrap());
|
||||
let mut clear_paint = skia::Paint::default();
|
||||
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
||||
clear_paint.set_anti_alias(true);
|
||||
canvas.draw_path(&path, &clear_paint);
|
||||
}
|
||||
|
||||
// Restore canvas state
|
||||
canvas.restore();
|
||||
}
|
||||
|
|
129
render-wasm/src/shapes/strokes.rs
Normal file
129
render-wasm/src/shapes/strokes.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use crate::math;
|
||||
use crate::shapes::fills::Fill;
|
||||
use skia_safe as skia;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StrokeStyle {
|
||||
Solid,
|
||||
// Dotted,
|
||||
// Dashed,
|
||||
// Mixed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StrokeCap {
|
||||
None,
|
||||
// Line,
|
||||
// Triangle,
|
||||
// Circle,
|
||||
// Diamond,
|
||||
// Round,
|
||||
// Square,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StrokeKind {
|
||||
InnerStroke,
|
||||
OuterStroke,
|
||||
CenterStroke,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Stroke {
|
||||
pub fill: Fill,
|
||||
pub width: f32,
|
||||
pub style: StrokeStyle,
|
||||
pub cap_end: StrokeCap,
|
||||
pub cap_start: StrokeCap,
|
||||
pub kind: StrokeKind,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub fn new_center_stroke(width: f32) -> Self {
|
||||
let transparent = skia::Color::from_argb(0, 0, 0, 0);
|
||||
Stroke {
|
||||
fill: Fill::Solid(transparent),
|
||||
width: width,
|
||||
style: StrokeStyle::Solid,
|
||||
cap_end: StrokeCap::None,
|
||||
cap_start: StrokeCap::None,
|
||||
kind: StrokeKind::CenterStroke,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_inner_stroke(width: f32) -> Self {
|
||||
let transparent = skia::Color::from_argb(0, 0, 0, 0);
|
||||
Stroke {
|
||||
fill: Fill::Solid(transparent),
|
||||
width: width,
|
||||
style: StrokeStyle::Solid,
|
||||
cap_end: StrokeCap::None,
|
||||
cap_start: StrokeCap::None,
|
||||
kind: StrokeKind::InnerStroke,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_outer_stroke(width: f32) -> Self {
|
||||
let transparent = skia::Color::from_argb(0, 0, 0, 0);
|
||||
Stroke {
|
||||
fill: Fill::Solid(transparent),
|
||||
width: width,
|
||||
style: StrokeStyle::Solid,
|
||||
cap_end: StrokeCap::None,
|
||||
cap_start: StrokeCap::None,
|
||||
kind: StrokeKind::OuterStroke,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delta(&self) -> f32 {
|
||||
match self.kind {
|
||||
StrokeKind::InnerStroke => 0.,
|
||||
StrokeKind::CenterStroke => self.width / 2.,
|
||||
StrokeKind::OuterStroke => self.width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outer_rect(&self, rect: &math::Rect) -> math::Rect {
|
||||
match self.kind {
|
||||
StrokeKind::InnerStroke => math::Rect::from_xywh(
|
||||
rect.left + (self.width / 2.),
|
||||
rect.top + (self.width / 2.),
|
||||
rect.width() - self.width,
|
||||
rect.height() - self.width,
|
||||
),
|
||||
StrokeKind::CenterStroke => {
|
||||
math::Rect::from_xywh(rect.left, rect.top, rect.width(), rect.height())
|
||||
}
|
||||
StrokeKind::OuterStroke => math::Rect::from_xywh(
|
||||
rect.left - (self.width / 2.),
|
||||
rect.top - (self.width / 2.),
|
||||
rect.width() + self.width,
|
||||
rect.height() + self.width,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_paint(&self, rect: &math::Rect) -> skia::Paint {
|
||||
let mut paint = self.fill.to_paint(rect);
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_stroke_width(self.width);
|
||||
paint.set_anti_alias(true);
|
||||
paint
|
||||
}
|
||||
|
||||
pub fn to_stroked_paint(&self, rect: &math::Rect) -> skia::Paint {
|
||||
let mut paint = self.to_paint(rect);
|
||||
match self.kind {
|
||||
StrokeKind::InnerStroke => {
|
||||
paint.set_stroke_width(2. * self.width);
|
||||
paint
|
||||
}
|
||||
|
||||
StrokeKind::CenterStroke => paint,
|
||||
StrokeKind::OuterStroke => {
|
||||
paint.set_stroke_width(2. * self.width);
|
||||
paint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue