Skip to content

Commit

Permalink
bezier: Implement bezier marks
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-wolf committed Nov 5, 2023
1 parent 3b47529 commit b44078a
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 23 deletions.
67 changes: 59 additions & 8 deletions src/bezier.typ
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,61 @@
return d
}

/// Shorten curve by offsetting s and c1 or e and c2
/// by distance d.
///
/// - s (vector): Curve start
/// - e (vector): Curve end
/// - c1 (vector): Control point 1
/// - c2 (vector): Control point 2
/// - d (float): Distance to shorten by
/// -> (s, e, c1, c2) Shortened curve
#let cubic-shorten-linear(s, e, c1, c2, d) = {
if d == 0 { return (s, e, c1, c2) }

let t = if d < 0 { 1 } else { 0 }
let sign = if d < 0 { -1 } else { 1 }

let a = cubic-point(s, e, c1, c2, t)
let b = cubic-point(s, e, c1, c2, t + sign * 0.01)
let offset = vector.scale(vector.norm(vector.sub(b, a)),
calc.abs(d))
if d > 0 {
s = vector.add(s, offset)
c1 = vector.add(c1, offset)
} else {
e = vector.add(e, offset)
c2 = vector.add(c2, offset)
}
return (s, e, c1, c2)
}

/// Approximate t for a givend distance d.
/// If d is negative start from the curves end.
///
#let cubic-t-for-distance(s, e, c1, c2, d, samples: 10) = {
if d == 0 {
return 0
}

if d > 0 {
let travel = 0 // Distance traveled along the curve
let last = s
for t in range(1, samples + 1) {
let t = t / samples
let curr = cubic-point(s, e, c1, c2, t)
let dist = vector.dist(last, curr)
travel += dist
if travel >= d {
return t - 1/samples + d / (travel * samples)
}
last = curr
}
} else {
return 1 - cubic-t-for-distance(e, s, c2, c1, -d, samples: samples)
}
}

/// Shorten curve by length d. A negative length shortens from the end.
///
/// - s (vector): Curve start
Expand All @@ -263,8 +318,6 @@
/// - c2 (vector): Control point 2
/// - d (float): Distance to shorten by
/// - samples (int): Maximum of samples/steps to use
/// - reposition (bool): If true, correct the start or end
/// point, depending on
/// -> (s, e, c1, c2) Shortened curve
#let cubic-shorten(s, e, c1, c2, d, samples: 15) = {
if d == 0 {
Expand All @@ -288,14 +341,12 @@
last = curr
}
} else {
return cubic-shorten(e, s, c2, c1, -d, samples: samples)
let (e, s, c2, c1) = cubic-shorten(e, s, c2, c1, -d, samples: samples)
return (s, e, c1, c2)
}

let (left, right) = split(s, e, c1, c2, split-t)

// The right curve is aways reversed
let (e, s, c2, c1) = right
return (s, e, c1, c2)
let (_, right) = split(s, e, c1, c2, split-t)
return right
}

/// Align curve points pts to the line start-end
Expand Down
51 changes: 40 additions & 11 deletions src/draw/shapes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,16 @@

// Coordinates check
let t = coordinates.map(coordinate.resolve-system)

// Shorten curve by distance
let shorten-curve(s, e, c1, c2, distance, style) = {
let quick = style.shorten == "QUICK"
if quick {
return bezier_.cubic-shorten-linear(s, e, c1, c2, distance)
} else {
return bezier_.cubic-shorten(s, e, c1, c2, distance)
}
}

return (
ctx => {
Expand All @@ -763,33 +773,52 @@

let style = styles.resolve(ctx.style, style, root: "bezier")

let (start, end, c1, c2) = (start, end, ..ctrl)
if style.mark != none {
if style.mark.start != none {
let offset = mark_.calc-mark-offset(ctx, style.mark.start, style.mark)
(start, end, c1, c2) = shorten-curve(start, end, c1, c2, -offset, style)
}
if style.mark.end != none {
let offset = mark_.calc-mark-offset(ctx, style.mark.end, style.mark)
(start, end, c1, c2) = shorten-curve(start, end, c1, c2, -offset, style)
}
}

let drawables = (drawable.path(
path-util.cubic-segment(start, end, ctrl.at(0), ctrl.at(1)),
path-util.cubic-segment(start, end, c1, c2),
fill: style.fill,
stroke: style.stroke,
),)

if style.mark != none {
style = style.mark
let offset = 0.001
let flex = style.flex
if style.start != none {
let pt = if flex {
mark_.mark-base-on-curve(start, end, c1, c2, style.end, style, false)
} else {
vector.add(start, bezier_.cubic-derivative(start, end, c1, c2, 0))
}
drawables.push(drawable.mark(
pt,
start,
bezier_.cubic-point(start, end, ..ctrl, offset),
style.start,
style.size,
fill: style.fill,
stroke: style.stroke
style
))
}
if style.start != none {

if style.end != none {
let pt = if flex {
mark_.mark-base-on-curve(start, end, c1, c2, style.end, style, true)
} else {
vector.sub(end, bezier_.cubic-derivative(start, end, c1, c2, 1))
}
drawables.push(drawable.mark(
bezier_.cubic-point(start, end, ..ctrl, 1 - offset),
pt,
end,
style.end,
style.size,
fill: style.fill,
stroke: style.stroke
style
))
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/drawable.typ
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@
)
}

let circle() = {
let (x, y, z) = vector.add(t, vector.scale(vector.sub(base, t), .5))
ellipse(x, y, z, length / 2, length / 2, fill: fill, stroke: stroke)
}

if symbol == ">" {
triangle()
} else if symbol == "<" {
Expand All @@ -241,6 +246,8 @@
harpoon(side: "left")
} else if symbol == "right-harpoon" {
harpoon(side: "right")
} else if symbol == "o" {
circle()
} else {
panic("Invalid arrow head: " + symbol)
}
Expand Down
30 changes: 27 additions & 3 deletions src/mark.typ
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#import "path-util.typ"

// Calculate offset for a triangular mark (triangle, harpoon, ..)
#let _triangular-mark-offset(ctx, width, length, style) = {
#let _triangular-mark-offset(ctx, mark-width, mark-length, style) = {
let revert = symbol == "<"
let sign = if revert { 1 } else { -1 }

Expand All @@ -21,7 +21,7 @@
if type(width) == typst-length { width /= ctx.length }

if style.length == 0 { return 0 }
let angle = calc.atan(style.width / (2 * style.length)) * 2
let angle = calc.atan(mark-width / (2 * mark-length)) * 2
if join == "miter" {
let angle = calc.abs(angle)
let miter = if angle == 180deg {
Expand Down Expand Up @@ -54,11 +54,35 @@
// of the miter length or halt of the stroke
// thickness, depending on the joint style.
#let calc-mark-offset(ctx, symbol, style) = {
if symbol in ("<", ">", "left-harpoon", "right-harpoon") {
if symbol in ("<", ">") {
return _triangular-mark-offset(ctx, style.width, style.length, style)
} else if symbol in ("left-harpoon", "right-harpoon") {
return _triangular-mark-offset(ctx, style.width / 2, style.length, style)
} else if symbol == "<>" {
return _triangular-mark-offset(ctx, style.width, style.length / 2, style)
} else {
let width = line(stroke: style.stroke).stroke.thickness
if width == auto { width = 1pt }
if type(width) == length { width /= ctx.length }

return -width / 2
}

return 0
}

/// Calculate marks base point on a cubic bezier.
/// The base point depends on the mark symbol.
#let mark-base-on-curve(s, e, c1, c2, symbol, style, end) = {
import "bezier.typ"
let sign = if end { -1 } else { 1 }

let length = style.length
if symbol in ("<", ">", "left-harpoon", "right-harpoon") {
length -= style.inset
}
length *= style.scale

return bezier.cubic-point(s, e, c1, c2,
bezier.cubic-t-for-distance(s, e, c1, c2, sign * length))
}
12 changes: 11 additions & 1 deletion src/styles.typ
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@
mark: _default-mark,
),
bezier: (
mark: _default-mark,
mark: (
.._default-mark,

// If true, the mark points in the direction of the secant from
// its base to its tip. If false, the tanget at the marks tip is used.
flex: false,
),
// Bezier shortening mode:
// - "LINEAR" Moving the affected point and it's next control point (like TikZ "quick")
// - "CURVED" Preserving the bezier curve by calculating new control points
shorten: "LINEAR",
),
arc: (
// Supported values:
Expand Down
27 changes: 27 additions & 0 deletions tests/mark/auto-offset/test.typ
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,30 @@
}))
par([])
}

#box(stroke: 2pt + red, canvas({
import draw: *
rect((1,-1), (2,2))
rect((-2,-1), (-1,2))
bezier((-1,-.5), (1,1), (0,-.5), (0,1),
mark: (start: ">", end: ">", fill: red, stroke: blue, flex: false,
width: .2))
}))

#box(stroke: 2pt + red, canvas({
import draw: *
rect((1,-1), (2,2))
rect((-2,-1), (-1,2))
bezier((-1,-.5), (1,1), (0,-.5), (0,1),
mark: (start: ">", end: ">", fill: red, stroke: blue, flex: true,
width: .2))
}))

#box(stroke: 2pt + red, canvas({
import draw: *
rect((1,-1), (2,2))
rect((-2,-1), (-1,2))
bezier((-1,-.5), (1,1), (0,-.5), (0,1),
mark: (start: "|", end: "o", fill: red, stroke: blue,
width: .5))
}))

0 comments on commit b44078a

Please sign in to comment.