0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-23 05:01:23 -05:00

🎉 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,21 +219,35 @@
:mixed 3
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
[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)
style (-> stroke :stroke-style translate-stroke-style)]
(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)
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
:inner (h/call internal-module "_add_shape_inner_stroke" width style)
:outer (h/call internal-module "_add_shape_outer_stroke" width style)
(h/call internal-module "_add_shape_center_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 cap-start cap-end)
(h/call internal-module "_add_shape_center_stroke" width style cap-start cap-end))
(cond
(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.
**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]
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");
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]
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");
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]
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");
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 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::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));
}
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(
canvas: &skia::Canvas,
stroke: &Stroke,
@ -177,6 +366,7 @@ fn draw_stroke_on_path(
// For center stroke we don't need to do anything extra
StrokeKind::CenterStroke => {
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
StrokeKind::OuterStroke => {
@ -295,6 +485,7 @@ pub fn draw_image_stroke_in_container(
}
let paint = stroke.to_stroked_paint(stroke_kind, &outer_rect);
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,
}
impl From<i32> for StrokeStyle {
fn from(value: i32) -> Self {
impl From<u8> for StrokeStyle {
fn from(value: u8) -> Self {
match value {
1 => StrokeStyle::Dotted,
2 => StrokeStyle::Dashed,
@ -21,15 +21,31 @@ impl From<i32> for StrokeStyle {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StrokeCap {
None,
// Line,
// Triangle,
// Circle,
// Diamond,
// Round,
// Square,
Line,
Triangle,
Rectangle,
Circle,
Diamond,
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)]
@ -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);
Stroke {
fill: Fill::Solid(transparent),
width: width,
style: StrokeStyle::from(style),
cap_end: StrokeCap::None,
cap_start: StrokeCap::None,
cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::from(cap_start),
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);
Stroke {
fill: Fill::Solid(transparent),
width: width,
style: StrokeStyle::from(style),
cap_end: StrokeCap::None,
cap_start: StrokeCap::None,
cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::from(cap_start),
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);
Stroke {
fill: Fill::Solid(transparent),
width: width,
style: StrokeStyle::from(style),
cap_end: StrokeCap::None,
cap_start: StrokeCap::None,
cap_end: StrokeCap::from(cap_end),
cap_start: StrokeCap::from(cap_start),
kind: StrokeKind::OuterStroke,
}
}