Add support for more complex strokes (#505)

This commit is contained in:
Birk Tjelmeland 2023-04-13 16:05:56 +02:00 committed by GitHub
parent 46ce9c94e3
commit d1cd814ef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 526 additions and 33 deletions

View File

@ -135,6 +135,7 @@ fn layout(
Geometry::Line(Point::with_x(line_width)).stroked(Stroke {
paint: TextElem::fill_in(ctx.styles()),
thickness,
..Stroke::default()
}),
span,
),

View File

@ -121,8 +121,11 @@ fn layout(
frame.push(
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width()))
.stroked(Stroke { paint: TextElem::fill_in(ctx.styles()), thickness }),
Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke {
paint: TextElem::fill_in(ctx.styles()),
thickness,
..Stroke::default()
}),
span,
),
);

View File

@ -271,6 +271,7 @@ pub(super) fn decorate(
let stroke = deco.stroke.clone().unwrap_or(Stroke {
paint: text.fill.clone(),
thickness: metrics.thickness.at(text.size),
..Stroke::default()
});
let gap_padding = 0.08 * text.size;

View File

@ -40,9 +40,32 @@ pub struct LineElem {
/// to `{1pt}`.
/// - A stroke combined from color and thickness using the `+` operator as
/// in `{2pt + red}`.
/// - A stroke described by a dictionary with any of the following keys:
/// - `color`: the color to use for the stroke
/// - `thickness`: the stroke's thickness
/// - `cap`: one of `"butt"`, `"round"` or `"square"`, the line cap of the stroke
/// - `join`: one of `"miter"`, `"round"` or `"bevel"`, the line join of the stroke
/// - `miter-limit`: the miter limit to use if `join` is `"miter"`, defaults to 4.0
/// - `dash`: the dash pattern to use. Can be any of the following:
/// - One of the strings `"solid"`, `"dotted"`, `"densely-dotted"`, `"loosely-dotted"`,
/// `"dashed"`, `"densely-dashed"`, `"loosely-dashed"`, `"dashdotted"`,
/// `"densely-dashdotted"` or `"loosely-dashdotted"`
/// - An array with elements that specify the lengths of dashes and gaps, alternating.
/// Elements can also be the string `"dot"` for a length equal to the line thickness.
/// - A dict with the keys `array`, same as the array above, and `phase`, the offset to
/// the start of the first dash.
///
///
/// ```example
/// #line(length: 100%, stroke: 2pt + red)
/// #stack(
/// line(length: 100%, stroke: 2pt + red),
/// v(1em),
/// line(length: 100%, stroke: (color: blue, thickness: 4pt, cap: "round")),
/// v(1em),
/// line(length: 100%, stroke: (color: blue, thickness: 1pt, dash: "dashed")),
/// v(1em),
/// line(length: 100%, stroke: (color: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))),
/// )
/// ```
#[resolve]
#[fold]

View File

@ -47,8 +47,22 @@ pub struct RectElem {
/// to `{1pt}`.
/// - A stroke combined from color and thickness using the `+` operator as
/// in `{2pt + red}`.
/// - A dictionary: With a dictionary, the stroke for each side can be set
/// individually. The dictionary can contain the following keys in order
/// - A stroke described by a dictionary with any of the following keys:
/// - `color`: the color to use for the stroke
/// - `thickness`: the stroke's thickness
/// - `cap`: one of `"butt"`, `"round"` or `"square"`, the line cap of the stroke
/// - `join`: one of `"miter"`, `"round"` or `"bevel"`, the line join of the stroke
/// - `miter-limit`: the miter limit to use if `join` is `"miter"`, defaults to 4.0
/// - `dash`: the dash pattern to use. Can be any of the following:
/// - One of the strings `"solid"`, `"dotted"`, `"densely-dotted"`, `"loosely-dotted"`,
/// `"dashed"`, `"densely-dashed"`, `"loosely-dashed"`, `"dashdotted"`,
/// `"densely-dashdotted"` or `"loosely-dashdotted"`
/// - An array with elements that specify the lengths of dashes and gaps, alternating.
/// Elements can also be the string `"dot"` for a length equal to the line thickness.
/// - A dict with the keys `array`, same as the array above, and `phase`, the offset to
/// the start of the first dash.
/// - Another dictionary describing the stroke for each side inidvidually.
/// The dictionary can contain the following keys in order
/// of precedence:
/// - `top`: The top stroke.
/// - `right`: The right stroke.

View File

@ -359,6 +359,7 @@ impl Frame {
Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke {
paint: Color::RED.into(),
thickness: Abs::pt(1.0),
..Stroke::default()
}),
Span::detached(),
),
@ -386,6 +387,7 @@ impl Frame {
Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke {
paint: Color::GREEN.into(),
thickness: Abs::pt(1.0),
..Stroke::default()
}),
Span::detached(),
),

View File

@ -108,6 +108,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
Value::dynamic(PartialStroke {
paint: Smart::Custom(color.into()),
thickness: Smart::Custom(thickness),
..PartialStroke::default()
})
}

View File

@ -1,5 +1,7 @@
use ecow::eco_format;
use pdf_writer::types::{ActionType, AnnotationType, ColorSpaceOperand};
use pdf_writer::types::{
ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
};
use pdf_writer::writers::ColorSpace;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
@ -7,8 +9,8 @@ use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::font::Font;
use crate::geom::{
self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
Transform,
self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio,
Shape, Size, Stroke, Transform,
};
use crate::image::Image;
@ -250,8 +252,17 @@ impl PageContext<'_, '_> {
fn set_stroke(&mut self, stroke: &Stroke) {
if self.state.stroke.as_ref() != Some(stroke) {
let Stroke {
paint,
thickness,
line_cap,
line_join,
dash_pattern,
miter_limit,
} = stroke;
let f = |c| c as f32 / 255.0;
let Paint::Solid(color) = stroke.paint;
let Paint::Solid(color) = paint;
match color {
Color::Luma(c) => {
self.set_stroke_color_space(D65_GRAY);
@ -267,7 +278,26 @@ impl PageContext<'_, '_> {
}
}
self.content.set_line_width(stroke.thickness.to_f32());
self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
self.content.set_line_cap(line_cap.into());
}
if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) {
self.content.set_line_join(line_join.into());
}
if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) {
if let Some(pattern) = dash_pattern {
self.content.set_dash_pattern(
pattern.array.iter().map(|l| l.to_f32()),
pattern.phase.to_f32(),
);
} else {
self.content.set_dash_pattern([], 0.0);
}
}
if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) {
self.content.set_miter_limit(miter_limit.0 as f32);
}
self.state.stroke = Some(stroke.clone());
}
}
@ -486,3 +516,23 @@ fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size)
ctx.links.push((dest.clone(), rect));
}
impl From<&LineCap> for LineCapStyle {
fn from(line_cap: &LineCap) -> Self {
match line_cap {
LineCap::Butt => LineCapStyle::ButtCap,
LineCap::Round => LineCapStyle::RoundCap,
LineCap::Square => LineCapStyle::ProjectingSquareCap,
}
}
}
impl From<&LineJoin> for LineJoinStyle {
fn from(line_join: &LineJoin) -> Self {
match line_join {
LineJoin::Miter => LineJoinStyle::MiterJoin,
LineJoin::Round => LineJoinStyle::RoundJoin,
LineJoin::Bevel => LineJoinStyle::BevelJoin,
}
}
}

View File

@ -11,7 +11,8 @@ use usvg::{FitTo, NodeExt};
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::geom::{
self, Abs, Color, Geometry, Paint, PathItem, Shape, Size, Stroke, Transform,
self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke,
Transform,
};
use crate::image::{DecodedImage, Image};
@ -392,9 +393,36 @@ fn render_shape(
canvas.fill_path(&path, &paint, rule, ts, mask);
}
if let Some(Stroke { paint, thickness }) = &shape.stroke {
if let Some(Stroke {
paint,
thickness,
line_cap,
line_join,
dash_pattern,
miter_limit,
}) = &shape.stroke
{
let dash = dash_pattern.as_ref().and_then(|pattern| {
// tiny-skia only allows dash patterns with an even number of elements,
// while pdf allows any number.
let len = if pattern.array.len() % 2 == 1 {
pattern.array.len() * 2
} else {
pattern.array.len()
};
let dash_array =
pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
});
let paint = paint.into();
let stroke = sk::Stroke { width: thickness.to_f32(), ..Default::default() };
let stroke = sk::Stroke {
width: thickness.to_f32(),
line_cap: line_cap.into(),
line_join: line_join.into(),
dash,
miter_limit: miter_limit.0 as f32,
};
canvas.stroke_path(&path, &paint, &stroke, ts, mask);
}
@ -525,6 +553,26 @@ impl From<Color> for sk::Color {
}
}
impl From<&LineCap> for sk::LineCap {
fn from(line_cap: &LineCap) -> Self {
match line_cap {
LineCap::Butt => sk::LineCap::Butt,
LineCap::Round => sk::LineCap::Round,
LineCap::Square => sk::LineCap::Square,
}
}
}
impl From<&LineJoin> for sk::LineJoin {
fn from(line_join: &LineJoin) -> Self {
match line_join {
LineJoin::Miter => sk::LineJoin::Miter,
LineJoin::Round => sk::LineJoin::Round,
LineJoin::Bevel => sk::LineJoin::Bevel,
}
}
}
/// Allows to build tiny-skia paths from glyph outlines.
struct WrappedPathBuilder(sk::PathBuilder);

View File

@ -188,22 +188,30 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let mut try_cast = || -> StrResult<_> {
let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let x = take("x")?.or_else(|| rest.clone());
let y = take("y")?.or_else(|| rest.clone());
let sides = Sides {
left: take("left")?.or_else(|| x.clone()),
top: take("top")?.or_else(|| y.clone()),
right: take("right")?.or_else(|| x.clone()),
bottom: take("bottom")?.or_else(|| y.clone()),
let rest = take("rest")?;
let x = take("x")?.or_else(|| rest.clone());
let y = take("y")?.or_else(|| rest.clone());
let sides = Sides {
left: take("left")?.or_else(|| x.clone()),
top: take("top")?.or_else(|| y.clone()),
right: take("right")?.or_else(|| x.clone()),
bottom: take("bottom")?.or_else(|| y.clone()),
};
dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
Ok(sides)
};
dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
if let Ok(res) = try_cast() {
return Ok(res);
}
}
Ok(sides)
} else if T::is(&value) {
if T::is(&value) {
Ok(Self::splat(Some(T::cast(value)?)))
} else {
<Self as Cast>::error(value)

View File

@ -7,6 +7,14 @@ pub struct Stroke {
pub paint: Paint,
/// The stroke's thickness.
pub thickness: Abs,
/// The stroke's line cap.
pub line_cap: LineCap,
/// The stroke's line join.
pub line_join: LineJoin,
/// The stroke's line dash pattern.
pub dash_pattern: Option<DashPattern<Abs, Abs>>,
/// The miter limit. Defaults to 4.0, same as `tiny-skia`.
pub miter_limit: Scalar,
}
impl Default for Stroke {
@ -14,6 +22,10 @@ impl Default for Stroke {
Self {
paint: Paint::Solid(Color::BLACK),
thickness: Abs::pt(1.0),
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
dash_pattern: None,
miter_limit: Scalar(4.0),
}
}
}
@ -29,14 +41,41 @@ pub struct PartialStroke<T = Length> {
pub paint: Smart<Paint>,
/// The stroke's thickness.
pub thickness: Smart<T>,
/// The stroke's line cap.
pub line_cap: Smart<LineCap>,
/// The stroke's line join.
pub line_join: Smart<LineJoin>,
/// The stroke's line dash pattern.
pub dash_pattern: Smart<Option<DashPattern<T>>>,
/// The miter limit.
pub miter_limit: Smart<Scalar>,
}
impl PartialStroke<Abs> {
/// Unpack the stroke, filling missing fields from the `default`.
pub fn unwrap_or(self, default: Stroke) -> Stroke {
let thickness = self.thickness.unwrap_or(default.thickness);
let dash_pattern = self
.dash_pattern
.map(|pattern| {
pattern.map(|pattern| DashPattern {
array: pattern
.array
.into_iter()
.map(|l| l.finish(thickness))
.collect(),
phase: pattern.phase,
})
})
.unwrap_or(default.dash_pattern);
Stroke {
paint: self.paint.unwrap_or(default.paint),
thickness: self.thickness.unwrap_or(default.thickness),
thickness,
line_cap: self.line_cap.unwrap_or(default.line_cap),
line_join: self.line_join.unwrap_or(default.line_join),
dash_pattern,
miter_limit: self.miter_limit.unwrap_or(default.miter_limit),
}
}
@ -48,13 +87,205 @@ impl PartialStroke<Abs> {
impl<T: Debug> Debug for PartialStroke<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match (&self.paint, &self.thickness) {
(Smart::Custom(paint), Smart::Custom(thickness)) => {
write!(f, "{thickness:?} + {paint:?}")
let Self {
paint,
thickness,
line_cap,
line_join,
dash_pattern,
miter_limit,
} = &self;
if line_cap.is_auto()
&& line_join.is_auto()
&& dash_pattern.is_auto()
&& miter_limit.is_auto()
{
match (&self.paint, &self.thickness) {
(Smart::Custom(paint), Smart::Custom(thickness)) => {
write!(f, "{thickness:?} + {paint:?}")
}
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
}
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
} else {
write!(f, "(")?;
let mut sep = "";
if let Smart::Custom(paint) = &paint {
write!(f, "{}color: {:?}", sep, paint)?;
sep = ", ";
}
if let Smart::Custom(thickness) = &thickness {
write!(f, "{}thickness: {:?}", sep, thickness)?;
sep = ", ";
}
if let Smart::Custom(cap) = &line_cap {
write!(f, "{}cap: {:?}", sep, cap)?;
sep = ", ";
}
if let Smart::Custom(join) = &line_join {
write!(f, "{}join: {:?}", sep, join)?;
sep = ", ";
}
if let Smart::Custom(dash) = &dash_pattern {
write!(f, "{}dash: {:?}", sep, dash)?;
sep = ", ";
}
if let Smart::Custom(miter_limit) = &miter_limit {
write!(f, "{}miter-limit: {:?}", sep, miter_limit)?;
}
write!(f, ")")?;
Ok(())
}
}
}
/// The line cap of a stroke
#[derive(Cast, Clone, Eq, PartialEq, Hash)]
pub enum LineCap {
Butt,
Round,
Square,
}
impl Debug for LineCap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
LineCap::Butt => write!(f, "\"butt\""),
LineCap::Round => write!(f, "\"round\""),
LineCap::Square => write!(f, "\"square\""),
}
}
}
/// The line join of a stroke
#[derive(Cast, Clone, Eq, PartialEq, Hash)]
pub enum LineJoin {
Miter,
Round,
Bevel,
}
impl Debug for LineJoin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
LineJoin::Miter => write!(f, "\"miter\""),
LineJoin::Round => write!(f, "\"round\""),
LineJoin::Bevel => write!(f, "\"bevel\""),
}
}
}
/// A line dash pattern
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct DashPattern<T = Length, DT = DashLength<T>> {
/// The dash array.
pub array: Vec<DT>,
/// The dash phase.
pub phase: T,
}
impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "(array: (")?;
for (i, elem) in self.array.iter().enumerate() {
if i == 0 {
write!(f, "{:?}", elem)?;
} else {
write!(f, ", {:?}", elem)?;
}
}
write!(f, "), phase: {:?})", self.phase)?;
Ok(())
}
}
impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> {
fn from(array: Vec<DashLength<T>>) -> Self {
Self { array, phase: T::default() }
}
}
/// The length of a dash in a line dash pattern
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum DashLength<T = Length> {
LineWidth,
Length(T),
}
impl From<Abs> for DashLength {
fn from(l: Abs) -> Self {
DashLength::Length(l.into())
}
}
impl<T> DashLength<T> {
fn finish(self, line_width: T) -> T {
match self {
Self::LineWidth => line_width,
Self::Length(l) => l,
}
}
}
cast_from_value! {
DashLength: "dash length",
"dot" => Self::LineWidth,
l: Length => Self::Length(l),
}
impl Resolve for DashLength {
type Output = DashLength<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self {
Self::LineWidth => DashLength::LineWidth,
Self::Length(l) => DashLength::Length(l.resolve(styles)),
}
}
}
cast_from_value! {
DashPattern: "dash pattern",
// Use same names as tikz:
// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns
"solid" => Vec::new().into(),
"dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(),
"densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(),
"loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
"dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
"densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
"loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
"dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(),
"densely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(),
"loosely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(),
array: Vec<DashLength> => {
Self {
array,
phase: Length::zero(),
}
},
mut dict: Dict => {
let array: Vec<DashLength> = dict.take("array")?.cast()?;
let phase = dict.take("phase").ok().map(Length::cast)
.transpose()?.unwrap_or(Length::zero());
dict.finish(&["array", "phase"])?;
Self {
array,
phase,
}
},
}
impl Resolve for DashPattern {
type Output = DashPattern<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
DashPattern {
array: self.array.into_iter().map(|l| l.resolve(styles)).collect(),
phase: self.phase.resolve(styles),
}
}
}
@ -64,10 +295,42 @@ cast_from_value! {
thickness: Length => Self {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
line_cap: Smart::Auto,
line_join: Smart::Auto,
dash_pattern: Smart::Auto,
miter_limit: Smart::Auto,
},
color: Color => Self {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
line_cap: Smart::Auto,
line_join: Smart::Auto,
dash_pattern: Smart::Auto,
miter_limit: Smart::Auto,
},
mut dict: Dict => {
fn take<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {
Ok(dict.take(key).ok().map(T::cast)
.transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto))
}
let paint = take::<Paint>(&mut dict, "color")?;
let thickness = take::<Length>(&mut dict, "thickness")?;
let line_cap = take::<LineCap>(&mut dict, "cap")?;
let line_join = take::<LineJoin>(&mut dict, "join")?;
let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?;
let miter_limit = take::<f64>(&mut dict, "miter-limit")?;
dict.finish(&["color", "thickness", "cap", "join", "dash", "miter-limit"])?;
Self {
paint,
thickness,
line_cap,
line_join,
dash_pattern,
miter_limit: miter_limit.map(Scalar),
}
},
}
@ -78,6 +341,10 @@ impl Resolve for PartialStroke {
PartialStroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
line_cap: self.line_cap,
line_join: self.line_join,
dash_pattern: self.dash_pattern.resolve(styles),
miter_limit: self.miter_limit,
}
}
}
@ -89,6 +356,10 @@ impl Fold for PartialStroke<Abs> {
Self {
paint: self.paint.or(outer.paint),
thickness: self.thickness.or(outer.thickness),
line_cap: self.line_cap.or(outer.line_cap),
line_join: self.line_join.or(outer.line_join),
dash_pattern: self.dash_pattern.or(outer.dash_pattern),
miter_limit: self.miter_limit,
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -51,5 +51,5 @@
#rect(radius: (left: 10pt, cake: 5pt))
---
// Error: 15-21 expected length, color, stroke, none, dictionary, or auto, found array
// Error: 15-21 expected length, color, dictionary, stroke, none, or auto, found array
#rect(stroke: (1, 2))

View File

@ -0,0 +1,71 @@
// Test lines.
---
// Some simple test lines
#line(length: 60pt, stroke: red)
#v(3pt)
#line(length: 60pt, stroke: 2pt)
#v(3pt)
#line(length: 60pt, stroke: blue + 1.5pt)
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: "dashed"))
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 4pt, cap: "round"))
---
// Set rules with stroke
#set line(stroke: (color: red, thickness: 1pt, cap: "butt", dash: "dashdotted"))
#line(length: 60pt)
#v(3pt)
#line(length: 60pt, stroke: blue)
#v(3pt)
#line(length: 60pt, stroke: (dash: none))
---
// Rectangle strokes
#rect(width: 20pt, height: 20pt, stroke: red)
#v(3pt)
#rect(width: 20pt, height: 20pt, stroke: (rest: red, top: (color: blue, dash: "dashed")))
#v(3pt)
#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
---
// Dashing
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ("dot", 1pt)))
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ("dot", 1pt, 4pt, 2pt)))
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: (array: ("dot", 1pt, 4pt, 2pt), phase: 5pt)))
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ()))
#v(3pt)
#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: (1pt, 3pt, 9pt)))
---
// Line joins
#stack(dir: ltr,
polygon(stroke: (thickness: 4pt, color: blue, join: "round"),
(0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
h(1em),
polygon(stroke: (thickness: 4pt, color: blue, join: "bevel"),
(0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
h(1em),
polygon(stroke: (thickness: 4pt, color: blue, join: "miter"),
(0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
h(1em),
polygon(stroke: (thickness: 4pt, color: blue, join: "miter", miter-limit: 20.0),
(0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
)
---
// Error: 29-56 unexpected key "thicknes", valid keys are "color", "thickness", "cap", "join", "dash", and "miter-limit"
#line(length: 60pt, stroke: (color: red, thicknes: 1pt))
---
// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dashdotted", "densely-dashdotted", "loosely-dashdotted", array, dictionary, dash pattern, or none
#line(length: 60pt, stroke: (color: red, dash: "dash"))