🎉 Basic strokes wasm support

This commit is contained in:
Alejandro Alonso 2024-12-27 13:59:35 +01:00
parent b5e5c4b0dd
commit beb9120b2b
7 changed files with 577 additions and 38 deletions

View file

@ -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,

View file

@ -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();
}

View 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
}
}
}
}