🎉 Stroke caps support for wasm render

This commit is contained in:
Alejandro Alonso 2025-01-08 14:14:26 +01:00
parent 4bd1e32462
commit 13ec04dd65
5 changed files with 288 additions and 35 deletions

View file

@ -219,6 +219,18 @@
:mixed 3 :mixed 3
0)) 0))
(defn- translate-stroke-cap
[stroke-cap]
(case stroke-cap
:line-arrow 1
:triangle-arrow 2
:square-marker 3
:circle-marker 4
:diamond-marker 5
:round 6
:square 7
0))
(defn set-shape-strokes (defn set-shape-strokes
[strokes] [strokes]
(h/call internal-module "_clear_shape_strokes") (h/call internal-module "_clear_shape_strokes")
@ -229,11 +241,13 @@
image (:stroke-image stroke) image (:stroke-image stroke)
width (:stroke-width stroke) width (:stroke-width stroke)
align (:stroke-alignment stroke) align (:stroke-alignment stroke)
style (-> stroke :stroke-style translate-stroke-style)] style (-> stroke :stroke-style translate-stroke-style)
cap-start (-> stroke :stroke-cap-start translate-stroke-cap)
cap-end (-> stroke :stroke-cap-end translate-stroke-cap)]
(case align (case align
:inner (h/call internal-module "_add_shape_inner_stroke" width style) :inner (h/call internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
:outer (h/call internal-module "_add_shape_outer_stroke" width style) :outer (h/call internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
(h/call internal-module "_add_shape_center_stroke" width style)) (h/call internal-module "_add_shape_center_stroke" width style cap-start cap-end))
(cond (cond
(some? gradient) (some? gradient)

View file

@ -39,3 +39,29 @@ Gradient stops are serialized in a `Uint8Array`, each stop taking **5 bytes**.
**Red**, **Green**, **Blue** and **Alpha** are the RGBA components of the stop. **Red**, **Green**, **Blue** and **Alpha** are the RGBA components of the stop.
**Stop offset** is the offset, being integer values ranging from `0` to `100` (both inclusive). **Stop offset** is the offset, being integer values ranging from `0` to `100` (both inclusive).
## StrokeCap
Stroke caps are serialized as `u8`:
| Value | Field |
| ----- | --------- |
| 1 | Line |
| 2 | Triangle |
| 3 | Rectangle |
| 4 | Circle |
| 5 | Diamond |
| 6 | Round |
| 7 | Square |
| _ | None |
## StrokeStyle
Stroke styles are serialized as `u8`:
| Value | Field |
| ----- | ------ |
| 1 | Dotted |
| 2 | Dashed |
| 3 | Mixed |
| _ | Solid |

View file

@ -347,26 +347,32 @@ pub extern "C" fn set_shape_path_content() {
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn add_shape_center_stroke(width: f32, style: i32) { pub extern "C" fn add_shape_center_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.add_stroke(shapes::Stroke::new_center_stroke(width, style)); shape.add_stroke(shapes::Stroke::new_center_stroke(
width, style, cap_start, cap_end,
));
} }
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn add_shape_inner_stroke(width: f32, style: i32) { pub extern "C" fn add_shape_inner_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.add_stroke(shapes::Stroke::new_inner_stroke(width, style)) shape.add_stroke(shapes::Stroke::new_inner_stroke(
width, style, cap_start, cap_end,
))
} }
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn add_shape_outer_stroke(width: f32, style: i32) { pub extern "C" fn add_shape_outer_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.add_stroke(shapes::Stroke::new_outer_stroke(width, style)) shape.add_stroke(shapes::Stroke::new_outer_stroke(
width, style, cap_start, cap_end,
))
} }
} }

View file

@ -1,7 +1,7 @@
use skia_safe as skia; use skia_safe as skia;
use uuid::Uuid; use uuid::Uuid;
use super::{Fill, Image, Kind, Path, Shape, Stroke, StrokeKind}; use super::{Fill, Image, Kind, Path, Shape, Stroke, StrokeCap, StrokeKind};
use crate::math::Rect; use crate::math::Rect;
use crate::render::{ImageStore, Renderable}; use crate::render::{ImageStore, Renderable};
@ -155,6 +155,195 @@ fn draw_stroke_on_circle(canvas: &skia::Canvas, stroke: &Stroke, rect: &Rect, se
canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect)); canvas.draw_oval(&stroke_rect, &stroke.to_paint(selrect));
} }
fn handle_stroke_cap(
canvas: &skia::Canvas,
cap: StrokeCap,
width: f32,
paint: &mut skia::Paint,
p1: &skia::Point,
p2: &skia::Point,
) {
paint.set_style(skia::PaintStyle::Fill);
paint.set_blend_mode(skia::BlendMode::Src);
match cap {
StrokeCap::None => {}
StrokeCap::Line => {
paint.set_style(skia::PaintStyle::Stroke);
draw_arrow_cap(canvas, &paint, p1, p2, width * 4.);
}
StrokeCap::Triangle => {
draw_triangle_cap(canvas, &paint, p1, p2, width * 4.);
}
StrokeCap::Rectangle => {
draw_square_cap(canvas, &paint, p1, p2, width * 4., 0.);
}
StrokeCap::Circle => {
canvas.draw_circle((p1.x, p1.y), width * 2., &paint);
}
StrokeCap::Diamond => {
draw_square_cap(canvas, &paint, p1, p2, width * 4., 45.);
}
StrokeCap::Round => {
canvas.draw_circle((p1.x, p1.y), width / 2.0, &paint);
}
StrokeCap::Square => {
draw_square_cap(canvas, &paint, p1, p2, width, 0.);
}
}
}
fn handle_stroke_caps(
path: &mut skia::Path,
stroke: &Stroke,
selrect: &Rect,
canvas: &skia::Canvas,
is_open: bool,
) {
let points_count = path.count_points();
let mut points = vec![skia::Point::default(); points_count];
let c_points = path.get_points(&mut points);
// Closed shapes don't have caps
if c_points >= 2 && is_open {
let first_point = points.first().unwrap();
let last_point = points.last().unwrap();
let kind = stroke.render_kind(is_open);
let mut paint_stroke = stroke.to_stroked_paint(kind.clone(), selrect);
handle_stroke_cap(
canvas,
stroke.cap_start,
stroke.width,
&mut paint_stroke,
first_point,
&points[1],
);
handle_stroke_cap(
canvas,
stroke.cap_end,
stroke.width,
&mut paint_stroke,
last_point,
&points[points_count - 2],
);
}
}
fn draw_square_cap(
canvas: &skia::Canvas,
paint: &skia::Paint,
center: &skia::Point,
direction: &skia::Point,
size: f32,
extra_rotation: f32,
) {
let dx = direction.x - center.x;
let dy = direction.y - center.y;
let angle = dy.atan2(dx);
let mut matrix = skia::Matrix::new_identity();
matrix.pre_rotate(
angle.to_degrees() + extra_rotation,
skia::Point::new(center.x, center.y),
);
let half_size = size / 2.0;
let rect = skia::Rect::from_xywh(center.x - half_size, center.y - half_size, size, size);
let points = [
skia::Point::new(rect.left(), rect.top()),
skia::Point::new(rect.right(), rect.top()),
skia::Point::new(rect.right(), rect.bottom()),
skia::Point::new(rect.left(), rect.bottom()),
];
let mut transformed_points = points.clone();
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(skia::Point::new(center.x, center.y));
path.move_to(transformed_points[0]);
path.line_to(transformed_points[1]);
path.line_to(transformed_points[2]);
path.line_to(transformed_points[3]);
path.close();
canvas.draw_path(&path, paint);
}
fn draw_arrow_cap(
canvas: &skia::Canvas,
paint: &skia::Paint,
center: &skia::Point,
direction: &skia::Point,
size: f32,
) {
let dx = direction.x - center.x;
let dy = direction.y - center.y;
let angle = dy.atan2(dx);
let mut matrix = skia::Matrix::new_identity();
matrix.pre_rotate(
angle.to_degrees() - 90.,
skia::Point::new(center.x, center.y),
);
let half_height = size / 2.;
let points = [
skia::Point::new(center.x, center.y - half_height),
skia::Point::new(center.x - size, center.y + half_height),
skia::Point::new(center.x + size, center.y + half_height),
];
let mut transformed_points = points.clone();
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(transformed_points[1]);
path.line_to(transformed_points[0]);
path.line_to(transformed_points[2]);
path.move_to(skia::Point::new(center.x, center.y));
path.line_to(transformed_points[0]);
canvas.draw_path(&path, paint);
}
fn draw_triangle_cap(
canvas: &skia::Canvas,
paint: &skia::Paint,
center: &skia::Point,
direction: &skia::Point,
size: f32,
) {
let dx = direction.x - center.x;
let dy = direction.y - center.y;
let angle = dy.atan2(dx);
let mut matrix = skia::Matrix::new_identity();
matrix.pre_rotate(
angle.to_degrees() - 90.,
skia::Point::new(center.x, center.y),
);
let half_height = size / 2.;
let points = [
skia::Point::new(center.x, center.y - half_height),
skia::Point::new(center.x - size, center.y + half_height),
skia::Point::new(center.x + size, center.y + half_height),
];
let mut transformed_points = points.clone();
matrix.map_points(&mut transformed_points, &points);
let mut path = skia::Path::new();
path.move_to(transformed_points[0]);
path.line_to(transformed_points[1]);
path.line_to(transformed_points[2]);
path.close();
canvas.draw_path(&path, paint);
}
fn draw_stroke_on_path( fn draw_stroke_on_path(
canvas: &skia::Canvas, canvas: &skia::Canvas,
stroke: &Stroke, stroke: &Stroke,
@ -177,6 +366,7 @@ fn draw_stroke_on_path(
// For center stroke we don't need to do anything extra // For center stroke we don't need to do anything extra
StrokeKind::CenterStroke => { StrokeKind::CenterStroke => {
canvas.draw_path(&skia_path, &paint_stroke); canvas.draw_path(&skia_path, &paint_stroke);
handle_stroke_caps(&mut skia_path, stroke, selrect, canvas, path.is_open());
} }
// 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 // 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 => { StrokeKind::OuterStroke => {
@ -295,6 +485,7 @@ pub fn draw_image_stroke_in_container(
} }
let paint = stroke.to_stroked_paint(stroke_kind, &outer_rect); let paint = stroke.to_stroked_paint(stroke_kind, &outer_rect);
canvas.draw_path(&path, &paint); canvas.draw_path(&path, &paint);
handle_stroke_caps(&mut path, stroke, &outer_rect, canvas, p.is_open());
} }
} }
} }

View file

@ -10,8 +10,8 @@ pub enum StrokeStyle {
Mixed, Mixed,
} }
impl From<i32> for StrokeStyle { impl From<u8> for StrokeStyle {
fn from(value: i32) -> Self { fn from(value: u8) -> Self {
match value { match value {
1 => StrokeStyle::Dotted, 1 => StrokeStyle::Dotted,
2 => StrokeStyle::Dashed, 2 => StrokeStyle::Dashed,
@ -21,15 +21,31 @@ impl From<i32> for StrokeStyle {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum StrokeCap { pub enum StrokeCap {
None, None,
// Line, Line,
// Triangle, Triangle,
// Circle, Rectangle,
// Diamond, Circle,
// Round, Diamond,
// Square, Round,
Square,
}
impl From<u8> for StrokeCap {
fn from(value: u8) -> Self {
match value {
1 => StrokeCap::Line,
2 => StrokeCap::Triangle,
3 => StrokeCap::Rectangle,
4 => StrokeCap::Circle,
5 => StrokeCap::Diamond,
6 => StrokeCap::Round,
7 => StrokeCap::Square,
_ => StrokeCap::None,
}
}
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@ -59,38 +75,38 @@ impl Stroke {
} }
} }
pub fn new_center_stroke(width: f32, style: i32) -> Self { pub fn new_center_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self {
let transparent = skia::Color::from_argb(0, 0, 0, 0); let transparent = skia::Color::from_argb(0, 0, 0, 0);
Stroke { Stroke {
fill: Fill::Solid(transparent), fill: Fill::Solid(transparent),
width: width, width: width,
style: StrokeStyle::from(style), style: StrokeStyle::from(style),
cap_end: StrokeCap::None, cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::None, cap_start: StrokeCap::from(cap_start),
kind: StrokeKind::CenterStroke, kind: StrokeKind::CenterStroke,
} }
} }
pub fn new_inner_stroke(width: f32, style: i32) -> Self { pub fn new_inner_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self {
let transparent = skia::Color::from_argb(0, 0, 0, 0); let transparent = skia::Color::from_argb(0, 0, 0, 0);
Stroke { Stroke {
fill: Fill::Solid(transparent), fill: Fill::Solid(transparent),
width: width, width: width,
style: StrokeStyle::from(style), style: StrokeStyle::from(style),
cap_end: StrokeCap::None, cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::None, cap_start: StrokeCap::from(cap_start),
kind: StrokeKind::InnerStroke, kind: StrokeKind::InnerStroke,
} }
} }
pub fn new_outer_stroke(width: f32, style: i32) -> Self { pub fn new_outer_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self {
let transparent = skia::Color::from_argb(0, 0, 0, 0); let transparent = skia::Color::from_argb(0, 0, 0, 0);
Stroke { Stroke {
fill: Fill::Solid(transparent), fill: Fill::Solid(transparent),
width: width, width: width,
style: StrokeStyle::from(style), style: StrokeStyle::from(style),
cap_end: StrokeCap::None, cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::None, cap_start: StrokeCap::from(cap_start),
kind: StrokeKind::OuterStroke, kind: StrokeKind::OuterStroke,
} }
} }