Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to brace #237

Merged
merged 6 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions manual.typ
Original file line number Diff line number Diff line change
Expand Up @@ -1063,23 +1063,36 @@ Various pre-made shapes and lines.
#show-module-fn(decorations-module, "brace")
```example
import cetz.decorations: brace
brace((0, 0), (4, -.5), pointiness: 25deg, amplitude: .8, debug: true)
let text = text.with(size: 12pt, font: "Linux Libertine")

brace((0, 0), (4, -.5), pointiness: 25deg, outer-pointiness: auto, amplitude: .8, debug: true)
brace((0, -.5), (0, -3.5), name: "brace")
content("brace.content", [$P_1$])

// styling can be passed to the underlying `merge-path` call
brace((1.5, -2), (4.5, -2), amplitude: 1, pointiness: .5, stroke: orange + 2pt, fill: maroon, close: true, name: "saloon")
content((rel: (0, -.15), to: "saloon.center"), text(12pt, fill: orange, font: "Linux Libertine", smallcaps[*Saloon*]))
brace((1, -3), (4, -3), amplitude: 1, pointiness: .5, stroke: orange + 2pt, fill: maroon, close: true, name: "saloon")
content((rel: (0, -.15), to: "saloon.center"), text(fill: orange, smallcaps[*Saloon*]))

// as part of another path
set-origin((3, -3))
set-origin((2, -5))
merge-path({
brace((+1, .5), (+1, -.5), amplitude: .3, pointiness: .5)
brace((-1, .5), (-1, -.5), amplitude: .3, pointiness: .5, flip: true)
brace((-1, -.5), (-1, .5), amplitude: .3, pointiness: .5)
}, fill: white, close: true)
content((0, 0), text(.8em)[Hello, World!])
content((0, 0), text(size: 10pt)[Hello, World!])

brace((-1.5, -2.5), (2, -2.5), pointiness: 1, outer-pointiness: 1, stroke: olive, fill: green, name: "hill")
content((rel: (.3, .1), to: "hill.center"), text[*εїз*])
```

#STYLING

#def-arg("amplitude", `<number>`, default: .7, [Determines how much the brace rises above the base line.])
#def-arg("pointiness", `<number> or <angle>`, default: 15deg, [How pointy the spike should be. #0deg or `0` for maximum pointiness, #90deg or `1` for minimum.])
#def-arg("outer-pointiness", `<number> or <angle> or <auto>`, default: 0, [How pointy the outer edges should be. #0deg or `0` for maximum pointiness (allowing for a smooth transition to a straight line), #90deg or `1` for minimum. Setting this to #auto will use the value set for `pointiness`.])
#def-arg("content-offset", `<number>`, default: .3, [Offset of the `content` anchor from the spike.])
#def-arg("debug-text-size", `<length>`, default: 6pt, [Font size of displayed debug points when `debug` is #true.])

==== Default `brace` Style
#decorations.brace-default-style

Expand Down
75 changes: 49 additions & 26 deletions src/lib/decorations.typ
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,27 @@
#let brace-default-style = (
amplitude: .7,
pointiness: 15deg,
outer-pointiness: 0,
content-offset: .3,
debug-text-size: 6pt,
)

/// Draw a curly brace between two points.
///
/// *Style root:* `brace`.
///
/// *Additional styles keys:*
/// / amplitude (number): Determines how much the brace rises above the base line.
/// / pointiness (angle): How pointy the spike should be.
/// #0deg or #0 for maximum pointiness, #90deg or #1 for minimum.
/// / content-offset (number): Offset of the `content` anchor from the spike.
///
/// *Anchors:*
/// / start: Where the brace starts, same as the `start` parameter.
/// / end: Where the brace end, same as the `end` parameter.
/// / spike: Point of the spike, halfway between `start` and `end` and shifted
/// by `amplitude` towards the pointing direction.
/// / content: Point to place content/text at, in front of the spike.
/// / center: Center of the enclosing rectangle.
/// / (a-i): Debug points `a` through `i`.
/// / (a-k): Debug points `a` through `k`.
///
/// - start (coordinate): Start point
/// - end (coordinate): End point
/// - flip (bool): Flip the brace around, same as swapping the start and end points
/// - flip (bool): Flip the brace around
/// - debug (bool): Show debug lines and points
/// - name (string, none): Element name
/// - ..style (style): Style attributes
Expand All @@ -57,38 +53,57 @@
// validate coordinates
let t = (start, end).map(coordinate.resolve-system)

// flipping is achieved by swapping the start and end points, the parameter is just for convenience
if flip {
(start, end) = (end, start)
}

group(name: name, ctx => {
// get styles and validate types and values
let style = util.merge-dictionary(brace-default-style,
styles.resolve(ctx.style, style.named(), root: "brace"))

let amplitude = style.amplitude
assert(
type(amplitude) in (int, float),
message: "amplitude must be a number",
message: "amplitude must be a number, got " + repr(amplitude),
)

// get pointiness from styles
let pointiness = style.pointiness
assert(
(type(pointiness) in (int, float)
type(pointiness) in (int, float)
and pointiness >= 0 and pointiness <= 1
or type(pointiness) == angle
and pointiness >= 0deg and pointiness <= 90deg),
message: "pointiness must be a factor between 0 and 1 or an angle between 0deg and 90deg",
and pointiness >= 0deg and pointiness <= 90deg,
message: "pointiness must be a factor between 0 and 1 or an angle between 0deg and 90deg, got " + repr(pointiness),
)
let pointiness = if type(pointiness) == angle { pointiness } else { pointiness * 90deg }

let outer-pointiness = style.outer-pointiness
assert(
outer-pointiness == auto
or type(outer-pointiness) in (int, float)
and outer-pointiness >= 0 and outer-pointiness <= 1
or type(outer-pointiness) == angle
and outer-pointiness >= 0deg and outer-pointiness <= 90deg,
message: "outer-pointiness must be a factor between 0 and 1 or an angle between 0deg and 90deg or auto, got " + repr(outer-pointiness),
)
let outer-pointiness = if outer-pointiness == auto {
pointiness
} else if type(outer-pointiness) == angle {
outer-pointiness
} else {
outer-pointiness * 90deg
}

let content-offset = style.content-offset
assert(
type(content-offset) in (int, float),
message: "content-offset must be a number",
message: "content-offset must be a number, got " + repr(content-offset),
)

// we flip the brace by inverting the amplitude and pointiness values
if flip {
amplitude *= -1
pointiness *= -1
outer-pointiness *= -1
}

// 'abcd' is a rectangle with the base line 'ab' and the height 'amplitude'
let a = start
let b = end
Expand Down Expand Up @@ -116,23 +131,31 @@
line(f, h, stroke: orange)
}

// 'i' is the point where the content should be placed. It is offset from the spike (point 'f')
// 'i' and 'j' are the control points for the outer ends
let i = (_rotate-around.with(angle: -outer-pointiness), a, d)
let j = (_rotate-around.with(angle: +outer-pointiness), b, c)
if debug {
line(a, i, stroke: purple)
line(b, j, stroke: orange)
}

// 'k' is the point where the content should be placed. It is offset from the spike (point 'f')
// by 'content-offset' in the direction the spike is pointing
let i = ((a, b) => {
let k = ((a, b) => {
let rel = vector.sub(b, a)
let scaled = vector.scale(vector.norm(rel), vector.len(rel) + content-offset)
return vector.add(a, scaled)
}, e, f)

let points = (a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, i: i)
let points = (a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, i: i, j: j, k: k)
// combine the two bezier curves using 'merge-path' and apply styling
merge-path({
bezier(a, f, d, g)
bezier(f, b, h, c)
bezier(a, f, i, g)
bezier(f, b, h, j)
}, ..style)
// define some named anchors
anchor("spike", f)
anchor("content", i)
anchor("content", k)
anchor("start", a)
anchor("end", b)
anchor("center", (e, .5, f))
Expand All @@ -144,7 +167,7 @@
// label all points in debug mode
if debug {
for (name, point) in points {
content(point, box(fill: luma(240), inset: .5pt, text(6pt, raw(name))))
content(point, box(fill: luma(240), inset: .5pt, text(style.debug-text-size, raw(name))))
}
}
})
Expand Down