"]
6 | license = "MIT"
7 | description = "Arrows and other marks for lines and paths."
8 | compiler = "0.13.0"
9 |
10 | repository = "https://github.com/Mc-Zen/tiptoe"
11 | keywords = ["arrows", "marks", "primitives", "line", "path", "tip", "arc"]
12 | categories = ["visualization", "layout"]
13 |
--------------------------------------------------------------------------------
/tests/ring/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ" as tiptoe: *
2 | #set page(width: 2cm, height: 2cm, margin: 0pt)
3 |
4 |
5 |
6 | #ring(origin: (1cm, 1cm), arc: 90deg)
7 |
8 | #pagebreak()
9 |
10 | #ring(origin: (1cm, 1cm), arc: 180deg, angle: -90deg, fill: blue)
11 |
12 | #pagebreak()
13 |
14 | #ring(origin: (1cm, 1cm), arc: 360deg, angle: -90deg, inner: .25cm, outer: .5cm)
15 |
16 |
17 |
18 | #pagebreak()
19 |
20 | #ring(origin: (2cm, 1cm), arc: 360deg, angle: -90deg, inner: .25cm, outer: .5cm)
21 |
22 |
--------------------------------------------------------------------------------
/tests/set-rule-integration/ring/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: 2cm, height: 1cm, margin: 2pt)
3 |
4 |
5 | #let test-curve = ring.with()
6 |
7 | #test-curve()
8 |
9 | #pagebreak()
10 |
11 | // Setting parameters on std.curve
12 | #{
13 | set std.curve(stroke: red, fill: gray)
14 | test-curve()
15 | }
16 |
17 | #pagebreak()
18 |
19 | // Overriding and folding (!) parameters
20 | #{
21 | set std.curve(stroke: green, fill: blue)
22 | test-curve(fill: gray, stroke: .5pt)
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/set-rule-integration/arc/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: 2cm, height: 1cm, margin: 2pt)
3 |
4 |
5 | #let test-curve = arc.with(
6 | tip: stealth,
7 | )
8 |
9 | #test-curve()
10 |
11 | #pagebreak()
12 |
13 | // Setting parameters on std.curve
14 | #{
15 | set std.curve(stroke: red, fill: gray)
16 | test-curve()
17 | }
18 |
19 | #pagebreak()
20 |
21 | // Overriding and folding (!) parameters
22 | #{
23 | set std.curve(stroke: green, fill: blue)
24 | test-curve(fill: gray, stroke: .5pt)
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/figures/intro-example.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure[
5 | #line(tip: stealth, toe: stealth.with(rev: true))
6 |
7 | #box(width: 20pt, height: 20pt, curve(
8 | tip: triangle,
9 | toe: bar,
10 | std.curve.cubic((10pt, 0pt), (20pt, 0pt), (20pt, 10pt)),
11 | std.curve.cubic(auto, none, (0pt, 20pt)),
12 | ))
13 |
14 | // #box(width: 20pt, height: 20pt, path(
15 | // tip: triangle, toe: bar,
16 | // ((0pt, 0pt), (-10pt, 0pt)),
17 | // ((20pt, 10pt), (0pt, -10pt)),
18 | // (0pt, 20pt)
19 | // ))
20 | ]
21 |
22 | #content
23 |
--------------------------------------------------------------------------------
/tests/ratio-length/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: 2cm, height: 2cm, margin: 6pt)
3 |
4 | #line(length: 100%, angle: 90deg, tip: stealth, toe: stealth)
5 |
6 | #pagebreak()
7 |
8 | #line(length: 100%, tip: stealth, toe: stealth)
9 |
10 |
11 | #pagebreak()
12 |
13 | // Full ratio
14 | #line(
15 | start: (50%, 0%),
16 | end: (100%, 100% - 1em),
17 | length: 2cm,
18 | angle: 20deg,
19 | toe: stealth,
20 | tip: bar,
21 | )
22 |
23 | #pagebreak()
24 |
25 | // Mixed coordinates
26 | #line(
27 | start: (1cm, 0%),
28 | end: (100%, 100% - 10pt),
29 | length: 2cm,
30 | angle: 20deg,
31 | toe: stealth,
32 | tip: bar,
33 | )
34 |
--------------------------------------------------------------------------------
/tests/set-rule-integration/line/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: 2cm, height: 1.3cm, margin: 2pt)
3 |
4 |
5 | #let test-curve = line.with(
6 | tip: stealth,
7 | start: (0pt, .2cm),
8 | end: (2cm - 4pt, .2cm),
9 | )
10 |
11 | #test-curve()
12 |
13 | #pagebreak()
14 |
15 | // Setting parameters on std.line
16 | #{
17 | set std.line(stroke: green)
18 | test-curve()
19 | test-curve(stroke: 2pt)
20 | }
21 |
22 | #pagebreak()
23 |
24 | // Overriding and folding (!) parameters
25 | #{
26 | set std.line(stroke: 2pt)
27 | test-curve()
28 | test-curve(stroke: blue)
29 | test-curve(stroke: (dash: "dashed", thickness: 1pt))
30 | }
31 |
--------------------------------------------------------------------------------
/tests/set-rule-integration/curve/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: 2cm, height: 1cm, margin: 2pt)
3 |
4 |
5 | #let test-curve = curve.with(
6 | tip: stealth,
7 | std.curve.line((2cm - 4pt, 6pt)),
8 | std.curve.line((0pt, 1cm - 4pt)),
9 | std.curve.line((1cm - 2pt, 0pt)),
10 | std.curve.line((2cm - 4pt, 1cm - 4pt)),
11 | )
12 |
13 | #test-curve()
14 |
15 | #pagebreak()
16 |
17 | // Setting parameters on std.curve
18 | #{
19 | set std.curve(stroke: red, fill: gray, fill-rule: "even-odd")
20 | test-curve()
21 | }
22 |
23 | #pagebreak()
24 |
25 | // Overriding and folding (!) parameters
26 | #{
27 | set std.curve(stroke: green, fill: blue, fill-rule: "even-odd")
28 | test-curve(fill: gray, stroke: 2pt, fill-rule: "non-zero")
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/assert.typ:
--------------------------------------------------------------------------------
1 | #let assert-mark(mark, kind: "tip") = {
2 | assert(
3 | type(mark) == function,
4 | message: "Invalid arrow "
5 | + kind
6 | + ", expected a function but got a "
7 | + str(type(mark))
8 | + ". Ensure that you use e.g. `stealth.with(..)` instead of directly writing `stealth(..)`",
9 | )
10 | }
11 |
12 | #let assert-dict-keys(dict, required: (), optional: ()) = {
13 | let keys = dict.keys()
14 |
15 | for key in required {
16 | if key not in keys {
17 | assert(false, message: "")
18 | }
19 | }
20 | let possible-keys = required + optional.dict
21 | for key in keys {
22 | if key not in possible-keys {
23 | assert(false, message: "Unexpected key \"" + key + "\"")
24 | }
25 | }
26 | assert(array)
27 | }
28 |
--------------------------------------------------------------------------------
/src/ring.typ:
--------------------------------------------------------------------------------
1 | #import "arc-impl.typ": bezier-arc2
2 | #import "curve.typ": curve
3 |
4 | #let ring(
5 | origin: (0pt, 0pt),
6 | angle: 0deg,
7 | arc: 45deg,
8 | inner: 0.5cm,
9 | outer: 1cm,
10 | stroke: auto,
11 | fill: auto,
12 | ) = {
13 | let inner-coords = bezier-arc2(
14 | origin: origin,
15 | angle: arc + angle,
16 | arc: -arc,
17 | radius: inner,
18 | )
19 | let outer-coords = bezier-arc2(
20 | origin: origin,
21 | angle: angle,
22 | arc: arc,
23 | radius: outer,
24 | move: arc == 360deg,
25 | )
26 |
27 | if arc == 360deg {
28 | inner-coords.push(std.curve.close(mode: "straight"))
29 | }
30 |
31 |
32 | curve(
33 | stroke: stroke,
34 | fill: fill,
35 | ..inner-coords,
36 | ..outer-coords,
37 | (std.curve.close(mode: "straight")),
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/run_tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on:
3 | push:
4 | branches: [ main ]
5 | paths:
6 | - src/**
7 | - tests/**
8 | - .github/**
9 | pull_request:
10 | branches: [ main ]
11 |
12 |
13 | jobs:
14 | tests:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Install tytanic (binary)
21 | uses: taiki-e/install-action@v2
22 | with:
23 | tool: tytanic@0.3.0
24 |
25 | - name: Run test suite
26 | run: tt run --no-fail-fast --font-path ./fonts
27 |
28 | - name: Archive artifacts
29 | uses: actions/upload-artifact@v4
30 | if: always()
31 | with:
32 | name: artifacts
33 | path: |
34 | tests/**/diff/*.png
35 | tests/**/out/*.png
36 | tests/**/ref/*.png
37 | retention-days: 5
38 |
--------------------------------------------------------------------------------
/docs/figures/ring.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure[
5 | #grid(
6 | columns: 3,
7 | column-gutter: 2em,
8 | row-gutter: 1em,
9 | box(height: 1.55cm, width: 1.1cm, ring(
10 | origin: (0pt, 30pt),
11 | angle: -90deg,
12 | arc: 90deg,
13 | )),
14 | box(height: 1.55cm, width: 1.7cm, ring(
15 | origin: (15pt, 30pt),
16 | angle: -180deg,
17 | arc: 180deg,
18 | fill: blue,
19 | )),
20 | box(height: 1.55cm, width: 2cm, ring(
21 | origin: (35pt, 25pt),
22 | angle: 60deg,
23 | arc: 360deg,
24 | )),
25 |
26 | ```typc
27 | ring(
28 | angle: -90deg,
29 | arc: 90deg
30 | )
31 | ```,
32 | ```typc
33 | ring(
34 | angle: 0deg,
35 | arc: 180deg,
36 | fill: blue
37 | )
38 | ```,
39 | ```typc
40 | ring(
41 | arc: 360deg,
42 | )
43 | ```,
44 | )
45 | ]
46 | #content
47 |
--------------------------------------------------------------------------------
/src/arc.typ:
--------------------------------------------------------------------------------
1 | #import "arc-impl.typ": bezier-arc2
2 | #import "curve.typ": curve
3 |
4 | #let arc(
5 | origin: (0pt, 0pt),
6 | angle: 0deg,
7 | arc: 45deg,
8 | radius: 1cm,
9 | width: auto,
10 | height: auto,
11 | stroke: auto,
12 | fill: auto,
13 | closed: false,
14 | tip: none,
15 | toe: none,
16 | shorten: 100%,
17 | ) = {
18 | let coords = bezier-arc2(
19 | origin: origin,
20 | angle: angle,
21 | arc: arc,
22 | radius: radius,
23 | width: width,
24 | height: height,
25 | )
26 |
27 | if closed == "sector" {
28 | coords.push(std.curve.line(origin))
29 | }
30 | if closed in ("sector", "segment") {
31 | coords.push(std.curve.close(mode: "straight"))
32 | closed = true
33 | } else if closed == true {
34 | coords.push(std.curve.close())
35 | }
36 |
37 | curve(
38 | stroke: stroke,
39 | tip: tip,
40 | toe: toe,
41 | shorten: shorten,
42 | fill: fill,
43 | ..coords,
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/docs/figures/sizing.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure(box(fill: none, width: 5cm, height: 3cm, {
5 | let (length, width) = (2cm, 1.5cm)
6 |
7 | place(horizon, line(
8 | tip: stealth.with(
9 | length: length,
10 | width: width,
11 | stroke: foreground,
12 | fill: none,
13 | ),
14 | stroke: 6pt + gray,
15 | length: 3cm,
16 | ))
17 | // line(tip: straight.with(length: length, width: width, stroke: black), stroke: 6pt + gray, length: 3cm))
18 | let m = (
19 | stroke: .3pt + foreground,
20 | tip: combine(bar, stealth),
21 | toe: combine(bar, stealth),
22 | )
23 | place(horizon, line(..m, start: (3.4cm, 0pt), angle: 90deg, length: width))
24 | place(horizon, dx: 3.6cm)[`width`]
25 | place(top, line(..m, start: (3cm - length, .6cm), length: length))
26 | place(dx: 1.5cm, dy: 0.1cm)[`length`]
27 | place(bottom, line(..m, start: (3cm - length, -.6cm), length: length * 40%))
28 | place(bottom, dx: .97cm, dy: -0.1cm)[`inset`]
29 | }))
30 |
31 | #content
32 |
--------------------------------------------------------------------------------
/docs/figures/template.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ" as tiptoe: *
2 |
3 | #let dark = "dark" in sys.inputs
4 | #let foreground = if dark { white } else { black }
5 | #let background = if dark { black } else { white }
6 |
7 |
8 |
9 | #let template(
10 | body,
11 | ) = {
12 | set std.curve(stroke: foreground)
13 | set std.line(stroke: foreground)
14 |
15 | let stroke-color = gray
16 | if dark {
17 | stroke-color = gray.darken(30%)
18 | }
19 |
20 | set page(width: auto, height: auto, margin: 2pt, fill: none)
21 | set grid.vline(stroke: stroke-color)
22 |
23 |
24 | show figure: box.with(stroke: .5pt + stroke-color, radius: .5em, inset: 1em)
25 |
26 | set text(foreground, font: "Liberation Sans")
27 |
28 |
29 | body
30 | }
31 |
32 | #let tline(..args) = box(
33 | height: 1cm,
34 | line(length: 1.3cm, stroke: 1.5pt + foreground, ..args),
35 | )
36 |
37 | #let code-and-tip(code) = {
38 | let j = eval(code, scope: dictionary(tiptoe))
39 | (
40 | tline(tip: j, length: 1cm),
41 | raw(code, lang: "typc"),
42 | [],
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-2025 Mc-Zen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/figures/combined.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 |
5 | #let content = figure({
6 | set text(.7em)
7 | grid(
8 | columns: (auto,) * 12, align: left + horizon,
9 | row-gutter: .8em, column-gutter: (10pt, 5pt, 5pt) * 4,
10 | ..range(3).map(i => grid.vline(x: 3 * i + 2, stroke: .4pt)),
11 | ..code-and-tip("combine(\n bar,\n stealth\n)"),
12 | ..code-and-tip(
13 | "combine(\n straight, 200%,\n straight, 200%,\n straight\n)",
14 | ),
15 | ..code-and-tip(
16 | "combine(\n square.with(fill: blue),\n circle.with(fill: red,
17 | align: end)\n)",
18 | ),
19 | ..code-and-tip("combine(\n bar, 200%, \n bar, 200%, \n bar\n)"),
20 | ..code-and-tip("combine(\n stealth,\n bar\n)"),
21 | ..code-and-tip(
22 | "combine(\n straight,\n end, 200%,\n straight, 200%,\n straight\n)",
23 | ),
24 | ..code-and-tip(
25 | "combine(\n bar.with(stroke: blue),\n 200%, bar, \n 200%, bar\n)",
26 | ),
27 | ..code-and-tip("combine(\n bar,\n end, 200%, \n bar, 200%,\n bar\n)"),
28 | ..code-and-tip("combine(\n stealth,\n stealth\n)"),
29 | )
30 | })
31 |
32 | #content
33 |
--------------------------------------------------------------------------------
/tests/arc/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ" as tiptoe: *
2 | #set page(width: auto, height: auto, margin: 10pt)
3 |
4 |
5 | #set std.curve(stroke: black)
6 |
7 | #box(height: 1.55cm, width: 1.1cm, arc(
8 | origin: (0pt, 30pt),
9 | angle: -90deg,
10 | arc: 120deg,
11 | fill: red,
12 | ))
13 |
14 | #pagebreak()
15 |
16 | #box(height: 1.55cm, width: 1.7cm, arc(
17 | origin: (15pt, 30pt),
18 | angle: -110deg,
19 | arc: 120deg,
20 | closed: "sector",
21 | fill: purple,
22 | ))
23 |
24 | #pagebreak()
25 |
26 | #box(height: 1.55cm, width: 2cm, arc(
27 | origin: (35pt, 10pt),
28 | angle: 60deg,
29 | arc: 130deg,
30 | closed: "segment",
31 | fill: green,
32 | ))
33 |
34 | #pagebreak()
35 |
36 | #box(height: 1.55cm, width: 2.6cm, arc(
37 | origin: (1.3cm, 20pt),
38 | angle: 60deg,
39 | arc: 300deg,
40 | width: 2cm,
41 | height: 1cm,
42 | ))
43 |
44 | #pagebreak()
45 |
46 | #box(height: 1.55cm, width: 2.6cm, arc(
47 | origin: (1.3cm, 20pt),
48 | angle: 60deg,
49 | arc: 300deg,
50 | width: 2cm,
51 | height: 1cm,
52 | fill: yellow,
53 | closed: "sector",
54 | ))
55 |
56 | #pagebreak()
57 |
58 | #box(height: 1.55cm, width: 2.6cm, arc(
59 | origin: (1.3cm, 20pt),
60 | angle: 60deg,
61 | arc: 300deg,
62 | width: 2cm,
63 | height: 1cm,
64 | fill: maroon,
65 | closed: "segment",
66 | ))
67 |
--------------------------------------------------------------------------------
/docs/figures/arc.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure[
5 | #grid(
6 | columns: 4,
7 | column-gutter: 2em,
8 | row-gutter: 1em,
9 | box(height: 1.55cm, width: 1.1cm, arc(
10 | origin: (0pt, 30pt),
11 | angle: -90deg,
12 | arc: 120deg,
13 | )),
14 | box(height: 1.55cm, width: 1.7cm, arc(
15 | origin: (15pt, 30pt),
16 | angle: -110deg,
17 | arc: 120deg,
18 | closed: "sector",
19 | )),
20 | box(height: 1.55cm, width: 2cm, arc(
21 | origin: (35pt, 10pt),
22 | angle: 60deg,
23 | arc: 130deg,
24 | closed: "segment",
25 | )),
26 | box(height: 1.55cm, width: 2.6cm, arc(
27 | origin: (1.3cm, 20pt),
28 | angle: 60deg,
29 | arc: 300deg,
30 | width: 2cm,
31 | height: 1cm,
32 | )),
33 |
34 | ```typc
35 | arc(
36 | angle: -90deg,
37 | arc: 120deg
38 | )
39 | ```,
40 | ```typc
41 | arc(
42 | angle: -110deg,
43 | arc: 120deg,
44 | closed: "sector"
45 | )
46 | ```,
47 | ```typc
48 | arc(
49 | angle: 60deg,
50 | arc: 130deg,
51 | closed: "segment"
52 | )
53 | ```,
54 | ```typc
55 | arc(
56 | angle: 60deg,
57 | arc: 300deg,
58 | width: 2cm,
59 | height: 1cm
60 | )
61 | ```,
62 | )
63 | ]
64 | #content
65 |
--------------------------------------------------------------------------------
/docs/figures/styling.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure({
5 | set text(.7em)
6 | grid(
7 | columns: (auto,) * 12, align: left + horizon,
8 | row-gutter: .8em, column-gutter: (10pt, 5pt, 5pt) * 4,
9 | ..range(3).map(i => grid.vline(x: 3 * i + 2, stroke: .4pt)),
10 | ..code-and-tip("square.with(\n fill: blue, \n width: 600%\n)"),
11 | ..code-and-tip("stealth.with(\n inset: 5%\n)"),
12 | ..code-and-tip("straight.with(\n stroke: 2pt + red\n)"),
13 | ..code-and-tip(
14 | "triangle.with(\n fill: green,\n length: 1200%,\n width: 500%\n)",
15 | ),
16 | ..code-and-tip(
17 | "diamond.with(\n fill: yellow,\n length: 1000%, \n width: 500%\n)",
18 | ),
19 | ..code-and-tip("stealth.with(\n inset: 60%\n)"),
20 | ..code-and-tip("stealth.with(\n fill: none,\n stroke: .8pt+blue\n)"),
21 | ..code-and-tip("rays.with(\n stroke: .7pt + red\n)"),
22 | ..code-and-tip("circle.with(\n fill: red\n)"),
23 | ..code-and-tip("stealth.with(\n stroke: 1pt+blue,\n fill: none\n)"),
24 | ..code-and-tip(
25 | "stealth.with(\n stroke: (dash: \"dashed\", \n paint: red, \n thickness: .3pt), \n fill: none,\n width: 1000%\n)",
26 | ),
27 | ..code-and-tip("bar.with(\n stroke: 5pt+yellow,\n width: 700%\n)"),
28 | ..code-and-tip("tikz.with(\n stroke: blue,\n width: 1000%\n)"),
29 | )
30 | })
31 |
32 | #content
33 |
--------------------------------------------------------------------------------
/docs/figures/marks.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 |
5 |
6 | #let content = figure(grid(
7 | columns: (auto, auto) * 2,
8 | align: horizon + right,
9 | row-gutter: 3pt,
10 | column-gutter: (1em, 3em, 1em),
11 | [`stealth`],
12 | tline(tip: stealth),
13 | [`stealth``.with(rev: true)`],
14 | tline(tip: stealth.with(rev: true)),
15 |
16 | [`triangle`],
17 | tline(tip: triangle),
18 | [`triangle``.with(rev: true)`],
19 | tline(tip: triangle.with(rev: true)),
20 |
21 | [`straight`],
22 | tline(tip: straight),
23 | [`straight``.with(rev: true)`],
24 | tline(tip: straight.with(rev: true)),
25 |
26 | [`round`],
27 | tline(tip: round),
28 | [`round``.with(rev: true)`],
29 | tline(tip: round.with(rev: true)),
30 |
31 | [`barb`],
32 | tline(tip: barb),
33 | [`barb.with(rev: true)`],
34 | tline(tip: barb.with(rev: true)),
35 |
36 | [`hooks`],
37 | tline(tip: hooks),
38 | [`hooks.with(rev: true)`],
39 | tline(tip: hooks.with(rev: true)),
40 |
41 | [`square`], tline(tip: square), [`diamond`], tline(tip: diamond),
42 | [`circle`], tline(tip: circle), [`bar`], tline(tip: bar),
43 | [`rays.with(n: 3)`], tline(tip: rays.with(n: 3)), [`rays`], tline(tip: rays),
44 | [`rays.with(n: 5)`],
45 | tline(tip: rays.with(n: 5)),
46 | [`rays.with(n: 6)`],
47 | tline(tip: rays.with(n: 6)),
48 |
49 | [`bracket`],
50 | tline(tip: bracket),
51 | [`bracket.with(rev: true)`],
52 | tline(tip: bracket.with(rev: true)),
53 |
54 | [`tikz`], tline(tip: tikz),
55 | ))
56 |
57 | #content
58 |
--------------------------------------------------------------------------------
/src/utility.typ:
--------------------------------------------------------------------------------
1 | #let if-auto(a, b) = if a == auto { b } else { a }
2 | #let if-none(a, b) = if a == none { b } else { a }
3 | #let chained-if-auto(..x) = x.pos().find(it => it != auto)
4 |
5 |
6 |
7 | #let extract-thickness-and-paint(stroke) = {
8 | if-auto(stroke.thickness, 1pt) + if-auto(stroke.paint, black)
9 | }
10 |
11 | #let inherit-thickness-and-paint(stroke, parent: 2) = {
12 | if stroke == none { return none } // this is always stronger
13 |
14 | // strokes might be length, color or dictionary:
15 | let parent = std.stroke(parent)
16 | let stroke = std.stroke(stroke)
17 | return std.stroke(
18 | paint: if-auto(stroke.paint, parent.paint),
19 | thickness: chained-if-auto(stroke.thickness, parent.thickness, 1pt),
20 | cap: stroke.cap,
21 | join: stroke.join,
22 | dash: stroke.dash,
23 | miter-limit: stroke.miter-limit,
24 | )
25 | }
26 |
27 |
28 | #let process-stroke(line, stroke) = {
29 | if stroke == auto {
30 | return extract-thickness-and-paint(line)
31 | }
32 | return inherit-thickness-and-paint(stroke, parent: line)
33 | }
34 |
35 |
36 | #let process-dims(
37 | line,
38 | length: none,
39 | width: none,
40 | default-ratio: .8,
41 | ) = {
42 | let result = (:)
43 | let linewidth = if-auto(line.thickness, 1pt)
44 |
45 | if length != none {
46 | result.length = if type(length) == ratio {
47 | linewidth * length
48 | } else if type(length) == relative {
49 | length.length + linewidth * length.ratio
50 | } else {
51 | length
52 | }
53 | }
54 |
55 | if width != none {
56 | result.width = if width == auto {
57 | result.length * default-ratio
58 | } else if type(width) == ratio {
59 | linewidth * width
60 | } else if type(width) == relative {
61 | width.length + linewidth * width.ratio
62 | } else {
63 | width
64 | }
65 | }
66 | return result
67 | }
68 |
--------------------------------------------------------------------------------
/docs/figures/shortening.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure(box(width: 8cm, height: 2.6cm, stroke: none, {
5 | let coords = (((0pt, 0pt), (-20pt, 0pt)), ((50pt, 20pt), (-20pt, 0pt)))
6 | let stroke = 2.4pt + foreground
7 | let red-stroke = 2.4pt + red.lighten(if dark { 10% } else { 50% })
8 | place([`shorten: 0%`])
9 | place(dx: 80pt, [`shorten: 100%`])
10 | place(dx: 160pt, [`shorten: 70%`])
11 | place(dx: 10pt, dy: 20pt, {
12 | place(path(
13 | ..coords,
14 | stroke: red-stroke,
15 | ))
16 | place(dy: 30pt, path(
17 | ..coords,
18 | stroke: stroke,
19 | tip: stealth,
20 | shorten: 0%,
21 | ))
22 | place(line(
23 | start: (25pt, 15pt),
24 | length: 20pt,
25 | angle: 90deg,
26 | tip: stealth,
27 | stroke: .5pt + foreground,
28 | ))
29 | place(line(
30 | start: (80pt + 25pt, 15pt),
31 | length: 20pt,
32 | angle: 90deg,
33 | tip: stealth,
34 | stroke: .5pt + foreground,
35 | ))
36 | place(line(
37 | start: (160pt + 25pt, 15pt),
38 | length: 20pt,
39 | angle: 90deg,
40 | tip: stealth,
41 | stroke: .5pt + foreground,
42 | ))
43 |
44 | place(dx: 80pt, path(
45 | ..coords,
46 | stroke: red-stroke,
47 | ))
48 | place(dx: 80pt, dy: 30pt, path(
49 | ..coords,
50 | stroke: red-stroke,
51 | ))
52 | place(dx: 80pt, dy: 30pt, path(
53 | ..coords,
54 | stroke: stroke,
55 | tip: stealth,
56 | ))
57 |
58 | place(dx: 160pt, path(
59 | ..coords,
60 | stroke: red-stroke,
61 | ))
62 | place(dx: 160pt, dy: 30pt, path(
63 | ..coords,
64 | stroke: red-stroke,
65 | ))
66 | place(dx: 160pt, dy: 30pt, path(
67 | ..coords,
68 | stroke: stroke,
69 | tip: stealth,
70 | shorten: 70%,
71 | ))
72 | })
73 | }))
74 |
75 | #content
76 |
--------------------------------------------------------------------------------
/docs/figures/alignment.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let content = figure(box(height: 7.3cm, width: 9.2cm, stroke: none, {
5 | set std.line(stroke: foreground)
6 | // set text(.8em)
7 | // show box: set align(center)
8 | // set box(stroke: red)
9 | show text: set align(center)
10 |
11 | let da = 85pt
12 | let dy = 17pt
13 | let y0 = 5em
14 | let len = 60pt
15 | let line = line.with(length: len, stroke: 1.4pt + foreground)
16 |
17 | place(dx: 0pt, dy: 4pt, box(width: da)[Always end-aligned tips])
18 | place(dx: da, dy: 4pt, box(width: 2 * da)[Tips with configurable alignment ])
19 | place(dy: 2.13em, dx: da, box(
20 | width: da,
21 | )[`align: center` #text(.8em)[(default)]])
22 | place(dy: 2.13em, dx: 2 * da, box(width: da)[`align: end`])
23 |
24 | let tips = (
25 | stealth,
26 | round,
27 | straight,
28 | stealth.with(rev: true),
29 | round.with(rev: true),
30 | straight.with(rev: true),
31 | tikz,
32 | barb,
33 | hooks,
34 | )
35 | place(dx: da, std.line(
36 | angle: 90deg,
37 | stroke: .3pt,
38 | length: y0 + dy * tips.len(),
39 | ))
40 | for (i, tip) in tips.enumerate() {
41 | place(dx: da - len, dy: y0 + i * dy, line(tip: tip))
42 | }
43 |
44 | let tips = (
45 | square,
46 | circle,
47 | diamond,
48 | bar,
49 | rays,
50 | )
51 | place(dy: 2em, dx: 2 * da, std.line(
52 | angle: 90deg,
53 | stroke: .3pt,
54 | length: y0 + dy * tips.len() - 2em,
55 | ))
56 | for (i, tip) in tips.enumerate() {
57 | place(dx: 2 * da - len, dy: y0 + i * dy, line(tip: tip))
58 | }
59 |
60 | let tips = (
61 | square.with(align: end),
62 | circle.with(align: end),
63 | diamond.with(align: end),
64 | bar.with(align: end),
65 | rays.with(align: end),
66 | )
67 | place(dy: 0pt, dx: 3 * da, std.line(
68 | angle: 90deg,
69 | stroke: .3pt,
70 | length: y0 + dy * tips.len(),
71 | ))
72 | for (i, tip) in tips.enumerate() {
73 | place(dx: 3 * da - len, dy: y0 + i * dy, line(tip: tip))
74 | }
75 | }))
76 |
77 | #content
78 |
--------------------------------------------------------------------------------
/docs/figures/logo.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 |
5 | #let figure1 = {
6 | place(path(
7 | tip: round,
8 | toe: square.with(length: 700%, fill: none),
9 | (20pt, 0pt),
10 | (10pt, 30pt),
11 | (12pt, 45pt),
12 | (5pt, 60pt),
13 | ))
14 | place(path(tip: round, (10pt, 30pt), (30pt, 40pt), (35pt, 55pt)))
15 | place(path(
16 | (14pt, 18pt),
17 | (25pt, 25pt),
18 | (26pt, 18pt),
19 | ))
20 | }
21 |
22 | #let figure2 = {
23 | place(path(
24 | tip: stealth,
25 | toe: circle.with(length: 700%, fill: none),
26 | (25pt, 0pt),
27 | (10pt, 30pt),
28 | ((15pt, 45pt), (0pt, -8pt)),
29 | (12pt, 60pt),
30 | ))
31 | place(path(
32 | tip: stealth,
33 | (10pt, 30pt),
34 | ((30pt, 40pt), (-4pt, -7pt)),
35 | (35pt, 55pt),
36 | ))
37 | place(dx: 2pt, path(
38 | (14pt, 18pt),
39 | (20pt, 24pt),
40 | (25pt, 15pt),
41 | ))
42 | }
43 |
44 | #let figure3 = {
45 | place(path(
46 | tip: square,
47 | toe: straight,
48 | ((22pt, 0pt), (1pt, -9pt)),
49 | ((17pt, 15pt), (1pt, -1pt)),
50 | ((10pt, 25pt), (1pt, -4pt)),
51 | (14pt, 45pt),
52 | (8pt, 57pt),
53 | ))
54 | place(path(
55 | tip: square,
56 | (10pt, 25pt),
57 | // ((25pt, 38pt), (-4pt, -7pt)),
58 | ((25pt, 52pt), (2pt, -16pt)),
59 | ))
60 | place(dx: 0pt, path(
61 | (14pt, 18pt),
62 | (20pt, 24pt),
63 | (25pt, 15pt),
64 | ))
65 | }
66 |
67 |
68 | #let figure4 = {
69 | place(path(
70 | tip: circle,
71 | toe: triangle.with(fill: none),
72 | (26pt, 0pt),
73 | ((17pt, 15pt), (1pt, -1pt)),
74 | ((10pt, 27pt), (1pt, -4pt)),
75 | (12pt, 45pt),
76 | (5pt, 60pt),
77 | ))
78 | place(path(tip: circle, (10pt, 27pt), (26pt, 40pt), (25pt, 55pt)))
79 | place(dx: 1pt, path(
80 | (14pt, 18pt),
81 | (23pt, 24pt),
82 | (25pt, 15pt),
83 | ))
84 | }
85 |
86 |
87 | #let logo = box(width: 260pt, height: 69pt, inset: 6pt, {
88 | set text(font: "Libertinus Serif")
89 | place(figure1)
90 | place(dx: 40pt, figure2)
91 | place(dx: 80pt, figure3)
92 | place(dx: 135pt, dy: 20pt, text(
93 | 1.8em,
94 | weight: "black",
95 | style: "italic",
96 | )[tiptoe])
97 | place(dx: 139pt, dy: 15pt, text(.4em)[_made with_])
98 | place(dx: 210pt, figure4)
99 | })
100 |
101 | #logo
102 |
--------------------------------------------------------------------------------
/src/path-to-curve.typ:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #let transform(a, b, mapper) = array.zip(a, b, exact: true).map(mapper)
5 | #let add(a, b) = transform(a, b, ((x, y)) => x + y)
6 | #let subtract(a, b) = transform(a, b, ((x, y)) => x - y)
7 | #let multiply(a, c) = a.map(x => x * c)
8 |
9 | #let path-to-curve(
10 | ..vertices,
11 | stroke: stroke(),
12 | fill: none,
13 | fill-rule: "even-odd",
14 | closed: false,
15 | ) = {
16 | // place(std.path(..vertices, stroke: stroke, fill: fill, closed: closed))
17 | vertices = vertices.pos()
18 | if vertices.len() == 0 { return }
19 |
20 | let is-vertex(v) = (
21 | (type(v) == array and type(v.first()) != array) or v.len() == 1
22 | )
23 | let extract-vertex(v) = {
24 | if is-vertex(v) {
25 | if v.len() == 2 { v } else { v.first() }
26 | } else {
27 | v.first()
28 | }
29 | }
30 |
31 | let curve-elements = ()
32 | let start-in = none
33 | let out = none
34 | for vertex in vertices {
35 | let v = extract-vertex(vertex)
36 |
37 | if is-vertex(vertex) {
38 | if out == none {
39 | curve-elements.push((v,))
40 | } else {
41 | curve-elements.push((out, none, v))
42 | out = none
43 | }
44 | } else {
45 | if vertex.len() == 2 {
46 | vertex.push(multiply(vertex.at(1), -1))
47 | // now its definitely a "cubic"!
48 | }
49 | if curve-elements.len() == 0 {
50 | curve-elements.push((v,))
51 | start-in = add(v, vertex.at(1))
52 | } else if out == none {
53 | curve-elements.push((auto, add(v, vertex.at(1)), v))
54 | out = auto
55 | } else {
56 | curve-elements.push((out, add(v, vertex.at(1)), v))
57 | }
58 | out = add(v, vertex.at(2))
59 | }
60 | }
61 |
62 | let to-curve-element(x) = {
63 | if x.len() == 1 { curve.line(..x) } else if x.len() == 2 {
64 | curve.quad(..x)
65 | } else if x.len() == 3 {
66 | curve.cubic(..x)
67 | }
68 | }
69 | curve-elements = curve-elements.map(to-curve-element)
70 | let start = extract-vertex(vertices.first())
71 | if closed {
72 | if out != none or start-in != none {
73 | curve-elements.push(curve.cubic(out, start-in, start))
74 | }
75 | curve-elements.push(curve.close(mode: "straight"))
76 | }
77 |
78 | std.curve(
79 | curve.move(start),
80 | fill: fill,
81 | stroke: stroke,
82 | ..curve-elements,
83 | fill-rule: fill-rule,
84 | // stroke: yellow + .5pt
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/tests/bezier-marks/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ" as tiptoe: *
2 | #import "/src/path-to-curve.typ": path-to-curve
3 | #set page(width: auto, height: auto, margin: 10pt)
4 |
5 |
6 | #let line = tiptoe.line.with(tip: tiptoe.stealth)
7 | #let path = tiptoe.path.with(tip: tiptoe.stealth)
8 |
9 | #line(stroke: yellow, tip: straight.with(stroke: orange))
10 | #pagebreak()
11 |
12 |
13 | #line(stroke: 10pt + yellow, length: 4cm, tip: straight.with(stroke: 3pt))
14 | #pagebreak()
15 |
16 |
17 | #set page(width: 3cm, height: 3cm)
18 |
19 | #arc(
20 | origin: (1cm, 1cm),
21 | angle: 0deg,
22 | arc: -360deg,
23 | stroke: red + 1mm,
24 | radius: 1.2cm,
25 | shorten: (start: 100%, end: 100%),
26 | tip: stealth,
27 | toe: bar.with(stroke: black),
28 | )
29 | #pagebreak()
30 |
31 |
32 |
33 | #path(
34 | tip: stealth,
35 | toe: stealth,
36 | (0pt, 0pt),
37 | ((50pt, 50pt), (-20pt, 0pt), (0pt, 0pt)),
38 | )
39 | #pagebreak()
40 | #path(
41 | tip: stealth,
42 | toe: stealth,
43 | ((0pt, 0pt), (20pt, -350pt), (0pt, 50pt)),
44 | ((50pt, 50pt), (-10pt, 0pt), (110pt, 110pt)),
45 | )
46 | #pagebreak()
47 | #path(
48 | tip: stealth,
49 | toe: stealth,
50 | ((0pt, 0pt), (20pt, -350pt), (0pt, 0pt)),
51 | ((25pt, 25pt), (0pt, -20pt), (20pt, 0pt)),
52 | ((50pt, 50pt), (0pt, 0pt), (110pt, 110pt)),
53 | )
54 | #pagebreak()
55 | #path(
56 | tip: stealth,
57 | toe: stealth,
58 | ((0pt, 0pt), (-20pt, 0pt)),
59 | ((25pt, 25pt), (0pt, 0pt), (0pt, 0pt)),
60 | ((50pt, 50pt), (-20pt, 0pt)),
61 | )
62 |
63 | #pagebreak()
64 |
65 | #set page(width: auto, height: auto)
66 |
67 | #let compare(..coords) = {
68 | place(path(..coords))
69 | path-to-curve(..coords, stroke: red)
70 | }
71 |
72 | #compare(
73 | (0pt, 0pt),
74 | (10pt, 0pt),
75 | (20pt, 20pt),
76 | )
77 |
78 | #pagebreak()
79 |
80 | #compare(
81 | // toe: none,
82 | // tip: none,
83 | ((0pt, 0pt), (-5pt, -5pt)),
84 | ((10pt, 0pt), (-10pt, 0pt)),
85 | ((20pt, 20pt), (5pt, -5pt)),
86 | )
87 | #pagebreak()
88 |
89 | #compare(
90 | (0pt, 0pt),
91 | ((10pt, 0pt), (-10pt, 0pt)),
92 | (20pt, 20pt),
93 | )
94 | #pagebreak()
95 |
96 | #compare(
97 | ((0pt, 0pt), (-5pt, -5pt)),
98 | ((10pt, 0pt),),
99 | ((20pt, 20pt), (5pt, -5pt)),
100 | )
101 | #pagebreak()
102 |
103 | #compare(
104 | ((0pt, 0pt), (-5pt, -5pt)),
105 | ((10pt, 0pt), (0pt, 5pt), (0pt, -15pt)),
106 | ((20pt, 20pt), (5pt, -5pt)),
107 | )
108 | #pagebreak()
109 |
110 | #compare(
111 | ((0pt, 0pt), (-20pt, 0pt)),
112 | ((50pt, 10pt), (-20pt, 0pt)),
113 | )
114 | #pagebreak()
115 |
116 | #compare(
117 | ((0pt, 0pt), (-20pt, 0pt), (20pt, 0pt)),
118 | ((50pt, 10pt), (-20pt, 0pt), (20pt, 0pt)),
119 | )
120 |
121 |
--------------------------------------------------------------------------------
/docs/figures/out/combine.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/figures/out/combine-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/line.typ:
--------------------------------------------------------------------------------
1 | #import "marks.typ": *
2 | #import "assert.typ": assert-mark
3 |
4 | #let resolve-relative(x, y, size) = {
5 | let resolve-coordinate(x, length) = {
6 | if type(x) == std.length {
7 | x
8 | } else if type(x) == ratio {
9 | length * x
10 | } else if type(x) == relative {
11 | x.length + length * x.ratio
12 | }
13 | }
14 |
15 | (
16 | resolve-coordinate(x, size.width),
17 | resolve-coordinate(y, size.height),
18 | )
19 | }
20 |
21 | #let line-impl(
22 | start: (0pt, 0pt),
23 | end: none,
24 | length: 30pt,
25 | angle: 0deg,
26 | tip: none,
27 | toe: none,
28 | ) = box({
29 | let stroke = std.stroke(line.stroke)
30 |
31 | if tip != none {
32 | assert-mark(tip, kind: "tip")
33 | tip = tip(line: stroke)
34 | }
35 | if toe != none {
36 | assert-mark(toe, kind: "toe")
37 | toe = toe(line: stroke)
38 | }
39 |
40 |
41 | if end == none {
42 | // using length and angle
43 | end = start
44 | .zip((length * calc.cos(angle), length * calc.sin(angle)))
45 | .map(array.sum)
46 | if type(length) == ratio {
47 | assert(
48 | angle in (0deg, 90deg),
49 | message: "When `length` is a ratio, the angle can only be 0deg or 90deg, found "
50 | + repr(angle),
51 | )
52 | } else if length.to-absolute() < 0pt {
53 | length *= -1
54 | angle += 180deg
55 | }
56 | } else {
57 | // using start and end
58 | let dx = (end.at(0) - start.at(0)).to-absolute() / 1pt
59 | let dy = (end.at(1) - start.at(1)).to-absolute() / 1pt
60 | angle = calc.atan2(dx, dy)
61 | length = 1pt * calc.sqrt(dx * dx + dy * dy)
62 | }
63 |
64 | let original-line = std.line(
65 | start: start,
66 | angle: angle,
67 | length: length,
68 | stroke: stroke,
69 | )
70 |
71 | // Apply path shortening
72 | let toe-pos = start
73 | if toe != none {
74 | start.at(0) += calc.cos(angle) * toe.end
75 | start.at(1) += calc.sin(angle) * toe.end
76 | length -= toe.end
77 | }
78 | if tip != none {
79 | length -= tip.end
80 | }
81 |
82 | place(std.line(start: start, angle: angle, length: length, stroke: stroke))
83 |
84 | if tip != none {
85 | place(
86 | dx: end.at(0),
87 | dy: end.at(1),
88 | rotate(angle, tip.mark, reflow: false),
89 | )
90 | }
91 | if toe != none {
92 | place(
93 | dx: toe-pos.at(0),
94 | dy: toe-pos.at(1),
95 | rotate(angle, scale(x: -100%, toe.mark), reflow: false),
96 | )
97 | }
98 | hide(original-line)
99 | })
100 |
101 |
102 |
103 | #let line(
104 | start: (0pt, 0pt),
105 | end: none,
106 | length: 30pt,
107 | angle: 0deg,
108 | stroke: auto,
109 | tip: none,
110 | toe: none,
111 | ) = {
112 | set place(left)
113 | set std.line(stroke: stroke) if stroke != auto
114 |
115 | let line-impl = line-impl.with(
116 | start: start,
117 | end: end,
118 | length: length,
119 | angle: angle,
120 | tip: tip,
121 | toe: toe,
122 | )
123 |
124 | if end != none and (start + end).map(type).any(x => x in (ratio, relative)) {
125 | context layout(size => line-impl(
126 | start: resolve-relative(..start, size),
127 | end: resolve-relative(..end, size),
128 | ))
129 | } else {
130 | context line-impl()
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/arc-impl.typ:
--------------------------------------------------------------------------------
1 | #let sub(p, q) = p.zip(q).map(((a, b)) => a - b)
2 | #import "assert.typ": assert-dict-keys
3 |
4 | #let bezier-arc(
5 | origin: (0pt, 0pt),
6 | angle: 0deg,
7 | arc: 45deg,
8 | radius: 1cm,
9 | width: auto,
10 | height: auto,
11 | ) = {
12 | if arc == 0deg { return () }
13 | if width == auto { width = 2 * radius }
14 | if height == auto { height = 2 * radius }
15 | if calc.abs(arc) > 360deg { arc = 360deg }
16 | let num-curves = int(calc.ceil(calc.abs(arc / 90deg)))
17 | let a = arc / num-curves
18 |
19 | let y0 = calc.sin(a / 2)
20 | let x0 = calc.cos(a / 2)
21 | let tx = (1 - x0) * 4 / 3
22 | let ty = y0 - tx * x0 / (y0 + 0.0001)
23 | let px = (x0, x0 + tx, x0 + tx, x0)
24 | let py = (-y0, -ty, ty, y0)
25 |
26 | let sn = calc.sin(angle + a / 2)
27 | let cs = calc.cos(angle + a / 2)
28 | let points = ()
29 | points.push((
30 | origin.at(0) + 0.5 * width * (px.at(0) * cs - py.at(0) * sn),
31 | origin.at(1) + 0.5 * height * (px.at(0) * sn + py.at(0) * cs),
32 | ))
33 |
34 | for i in range(num-curves) {
35 | let astart = angle + a * i
36 | let sn = calc.sin(astart + a / 2)
37 | let cs = calc.cos(astart + a / 2)
38 | for j in range(1, 4) {
39 | points.push((
40 | origin.at(0) + 0.5 * width * (px.at(j) * cs - py.at(j) * sn),
41 | origin.at(1) + 0.5 * height * (px.at(j) * sn + py.at(j) * cs),
42 | ))
43 | }
44 | }
45 | let coords = (
46 | (points.at(0), (0pt, 0pt), sub(points.at(1), points.at(0))),
47 | ..points
48 | .slice(1)
49 | .chunks(3)
50 | .map(
51 | ((a, b, c)) => {
52 | (c, sub(b, c))
53 | },
54 | ),
55 | )
56 | coords.last().push((0pt, 0pt))
57 | coords
58 | }
59 |
60 | #let bezier-arc2(
61 | origin: (0pt, 0pt),
62 | angle: 0deg,
63 | arc: 45deg,
64 | radius: 1cm,
65 | width: auto,
66 | height: auto,
67 | move: true,
68 | ) = {
69 | if arc == 0deg { return () }
70 | if width == auto { width = 2 * radius }
71 | if height == auto { height = 2 * radius }
72 | if calc.abs(arc) > 360deg { arc = 360deg }
73 | let num-curves = int(calc.ceil(calc.abs(arc / 90deg)))
74 | let a = arc / num-curves
75 |
76 | let y0 = calc.sin(a / 2)
77 | let x0 = calc.cos(a / 2)
78 | let tx = (1 - x0) * 4 / 3
79 | let ty = y0 - tx * x0 / (y0 + 0.0001)
80 | let px = (x0, x0 + tx, x0 + tx, x0)
81 | let py = (-y0, -ty, ty, y0)
82 |
83 | let sn = calc.sin(angle + a / 2)
84 | let cs = calc.cos(angle + a / 2)
85 | let points = ()
86 | points.push((
87 | origin.at(0) + 0.5 * width * (px.at(0) * cs - py.at(0) * sn),
88 | origin.at(1) + 0.5 * height * (px.at(0) * sn + py.at(0) * cs),
89 | ))
90 |
91 | for i in range(num-curves) {
92 | let astart = angle + a * i
93 | let sn = calc.sin(astart + a / 2)
94 | let cs = calc.cos(astart + a / 2)
95 | for j in range(1, 4) {
96 | points.push((
97 | origin.at(0) + 0.5 * width * (px.at(j) * cs - py.at(j) * sn),
98 | origin.at(1) + 0.5 * height * (px.at(j) * sn + py.at(j) * cs),
99 | ))
100 | }
101 | }
102 | let coords = if move {
103 | (curve.move(points.at(0)),)
104 | } else {
105 | (curve.line(points.at(0)),)
106 | }
107 | coords += points
108 | .slice(1)
109 | .chunks(3)
110 | .map(x => curve.cubic(..x, relative: false))
111 |
112 | coords
113 | }
114 |
115 |
116 | #let arc-impl(
117 | origin: (0pt, 0pt),
118 | angle: 0deg,
119 | arc: 6rad,
120 | radius: 1cm,
121 | stroke: 1pt + black,
122 | ) = {
123 | curve(
124 | stroke: stroke,
125 | ..bezier-arc2(
126 | origin: origin,
127 | angle: angle,
128 | arc: arc,
129 | radius: radius,
130 | ),
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/src/path.typ:
--------------------------------------------------------------------------------
1 | #import "marks.typ": *
2 | #import "assert.typ": assert-mark
3 | #import "path-to-curve.typ": path-to-curve
4 |
5 |
6 | #let add(p, q) = p.zip(q).map(array.sum)
7 | #let sub(p, q) = p.zip(q).map(((a, b)) => a - b)
8 | #let addorsub(p, q, f) = p.zip(q).map(((a, b)) => a + f * b)
9 |
10 | #let extract-bezier-polygon(vertex1, vertex2, rev: false) = {
11 | let polygon = ()
12 | if type(vertex1.at(0)) == array {
13 | let v = vertex1.at(0)
14 | if vertex1.len() == 1 {
15 | polygon.push(v)
16 | } else if vertex1.len() == 2 {
17 | polygon += (v, sub(v, vertex1.at(1))).dedup()
18 | } else if vertex1.len() == 3 {
19 | polygon += (v, add(v, vertex1.at(2))).dedup()
20 | }
21 | } else {
22 | polygon.push(vertex1)
23 | }
24 | if type(vertex2.at(0)) == array {
25 | let v = vertex2.at(0)
26 | if vertex2.len() == 1 {
27 | polygon.push(v)
28 | } else {
29 | polygon += (add(v, vertex2.at(1)), v).dedup()
30 | }
31 | } else {
32 | polygon.push(vertex2)
33 | }
34 |
35 | return polygon
36 | }
37 |
38 |
39 |
40 | #let path(
41 | ..args,
42 | fill: none,
43 | fill-rule: auto,
44 | stroke: 1pt,
45 | closed: false,
46 | tip: none,
47 | toe: none,
48 | shorten: 100%,
49 | ) = {
50 | if args.named().len() != 0 {
51 | assert(
52 | false,
53 | message: "Unexpected named argument \""
54 | + args.named().keys().first()
55 | + "\"",
56 | )
57 | }
58 |
59 | set place(left)
60 |
61 | stroke = std.stroke(stroke)
62 |
63 | assert(
64 | type(shorten) in (ratio, dictionary),
65 | message: "Expected ratio or dictionary for parameter `shorten`, found "
66 | + str(type(shorten)),
67 | )
68 | if type(shorten) == ratio {
69 | shorten = (start: shorten, end: shorten)
70 | } else if type(shorten) == dictionary {
71 | assert(
72 | shorten.keys().sorted() == ("end", "start"),
73 | message: "Unexpected key, valid keys are \"start\" and \"end\"",
74 | )
75 | }
76 |
77 |
78 | context {
79 | let points = args.pos()
80 | let original-points = points
81 |
82 | let treat-mark(mark, i1, i2, pos: start, shorten: 100%) = {
83 | mark = mark(line: stroke)
84 |
85 | let polygon = extract-bezier-polygon(points.at(i1), points.at(i2))
86 | if pos == end { polygon = polygon.rev() }
87 | let inner = polygon.at(1)
88 | let outer = polygon.at(0)
89 |
90 | let dx = (outer.at(0) - inner.at(0)).to-absolute() / 1pt
91 | let dy = (outer.at(1) - inner.at(1)).to-absolute() / 1pt
92 | let angle = calc.atan2(dx, dy)
93 |
94 | let mark-content = place(
95 | dx: outer.at(0),
96 | dy: outer.at(1),
97 | rotate(angle, mark.mark, reflow: false),
98 | )
99 | outer.at(0) -= calc.cos(angle) * mark.end * shorten
100 | outer.at(1) -= calc.sin(angle) * mark.end * shorten
101 | return (mark-content, outer)
102 | }
103 |
104 | let marks
105 | if toe != none and points.len() >= 2 {
106 | assert-mark(toe, kind: "toe")
107 | let (mark, end) = treat-mark(
108 | toe,
109 | 0,
110 | 1,
111 | pos: start,
112 | shorten: shorten.start,
113 | )
114 | if type(points.first().at(0)) == array {
115 | points.first().at(0) = end
116 | } else {
117 | points.first() = end
118 | }
119 | marks += mark
120 | }
121 | if tip != none and points.len() >= 2 {
122 | assert-mark(tip, kind: "tip")
123 | let (mark, end) = treat-mark(tip, -2, -1, pos: end, shorten: shorten.end)
124 | if type(points.last().at(0)) == array {
125 | points.last().at(0) = end
126 | } else {
127 | points.last() = end
128 | }
129 | marks += mark
130 | }
131 |
132 | let fill-rule = fill-rule
133 | if fill-rule == auto {
134 | fill-rule = std.curve.fill-rule
135 | }
136 |
137 | (
138 | place(path-to-curve(
139 | ..points,
140 | stroke: stroke,
141 | fill: fill,
142 | closed: closed,
143 | fill-rule: fill-rule,
144 | ))
145 | + marks
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/docs/figures/mark-parameters.typ:
--------------------------------------------------------------------------------
1 | #import "template.typ": *
2 | #show: template
3 |
4 | #let annotate-h(length, content, dx: 0pt, dy: 0pt) = {
5 | let mark = combine(bar, stealth)
6 | place(
7 | dx: dx,
8 | dy: dy,
9 | tiptoe.line(length: length, tip: mark, toe: mark, stroke: .4pt),
10 | )
11 | place(dx: length / 2 + dx, dy: dy - .1em, place(center + bottom, content))
12 | }
13 |
14 | #let describe-mark(
15 | mark,
16 | name: "",
17 | len: auto,
18 | wid: 1cm,
19 | width: auto,
20 | length: auto,
21 | ..annotations,
22 | ) = {
23 | let stroke = blue.lighten(60%) + 7pt
24 | let fill = none
25 |
26 |
27 | let args = (:)
28 | if width != auto {
29 | args.width = width
30 | wid = width
31 | }
32 | if length != auto {
33 | args.length = length
34 | if len == auto { len = length }
35 | } else {
36 | // length = 4cm
37 | }
38 |
39 | if type(mark) != array { mark = (mark,) }
40 |
41 | box(inset: (x: 1em, y: .5em, bottom: 1em), box(
42 | width: len,
43 | height: wid,
44 | stroke: .1pt + luma(if dark { 60% } else { 0% }),
45 | {
46 | set text(.8em)
47 | mark
48 | .map(mark => {
49 | mark = mark(line: stroke, ..args)
50 | set std.line(stroke: stroke)
51 | set std.line(stroke: luma(if dark { 30% } else { 90% }))
52 | place(dy: 50%, std.line(length: len - mark.end))
53 | place(
54 | dx: len,
55 | dy: wid / 2,
56 | mark.mark,
57 | )
58 | place(dy: 50%, dx: len - mark.end, place(horizon, line(
59 | angle: 90deg,
60 | stroke: .4pt + red,
61 | )))
62 | })
63 | .join()
64 | annotations.pos().join()
65 | if length != auto {
66 | place(dy: -.1em, dx: len / 2, place(center + bottom)[`length`])
67 | }
68 | place(dy: wid / 2, dx: len + .1em, place(horizon + left, rotate(
69 | 90deg,
70 | reflow: true,
71 | origin: left,
72 | )[`width`]))
73 |
74 | set text(gray, 1.2em)
75 | place(
76 | bottom + center,
77 | dy: 2pt,
78 | place(top + center, raw(name)),
79 | )
80 | },
81 | ))
82 | }
83 |
84 | #describe-mark(
85 | length: 3cm,
86 | width: 3cm,
87 | stealth.with(fill: none),
88 | annotate-h(40% * 4cm, dy: 50%)[`inset`],
89 | place(dx: 60%, dy: 17%, {
90 | place[`fill`]
91 | line(start: (1em, 1em), angle: 120deg, stroke: .5pt, length: 20pt)
92 | }),
93 | name: "stealth",
94 | )
95 | #describe-mark(
96 | length: 3cm,
97 | width: 3cm,
98 | round.with(fill: none),
99 | // annotate-h(40%*4cm, dy: 50%)[`inset`],
100 | place(dx: 60%, dy: 17%, {
101 | place[`fill`]
102 | line(start: (1em, 1em), angle: 120deg, stroke: .5pt, length: 20pt)
103 | }),
104 | name: "round",
105 | )
106 | #describe-mark(
107 | length: 3cm,
108 | width: 3cm,
109 | straight.with(rev: false),
110 | name: "straight",
111 | )
112 |
113 | #describe-mark(
114 | width: 3cm,
115 | len: 1.5cm,
116 | (barb.with(stroke: luma(85%)), barb.with(arc: 140deg)),
117 | place({
118 | set std.line(stroke: .5pt)
119 | place(std.line(start: (0pt, 50%), length: 1.4cm))
120 | place(std.line(start: (0pt, 50%), angle: -70deg, length: 1.4cm))
121 | place(arc(origin: (0pt, 50%), arc: -70deg, radius: 20pt, stroke: .5pt))
122 | place(dx: 3pt, dy: 1.15cm)[`arc`]
123 | }),
124 | name: "barb",
125 | )
126 | #describe-mark(
127 | width: 3cm,
128 | len: .8cm,
129 | (hooks.with(stroke: luma(85%)), hooks.with(arc: 140deg)),
130 | place({
131 | let y = 25% + 7pt / 4
132 | set std.line(stroke: .5pt)
133 | place(std.line(start: (0pt, y), angle: -50deg, length: .8cm))
134 | place(std.line(start: (0pt, y), angle: 90deg, length: .8cm))
135 | place(arc(
136 | origin: (0pt, y),
137 | angle: 90deg,
138 | arc: -140deg,
139 | radius: 20pt,
140 | stroke: .5pt,
141 | ))
142 | place(dx: 2pt, dy: .76cm)[`arc`]
143 | }),
144 | name: "hooks",
145 | )
146 | #describe-mark(
147 | width: 3cm,
148 | length: 1cm,
149 | bracket,
150 | name: "bracket",
151 | )
152 | #describe-mark(
153 | width: 3cm,
154 | len: .8cm,
155 | bar.with(align: end),
156 | name: "bar",
157 | )
158 | #describe-mark(
159 | wid: 3cm,
160 | length: 1.5cm,
161 | len: 3cm,
162 | rays.with(n: 6, align: end),
163 | name: "rays",
164 | )
165 |
166 | #describe-mark(
167 | width: 3cm,
168 | length: 2cm,
169 | square.with(fill: none, align: end),
170 | place(dx: 50%, dy: 50%, place(center + horizon)[`fill`]),
171 | name: "square",
172 | )
173 | #describe-mark(
174 | width: 3cm,
175 | length: 2cm,
176 | circle.with(fill: none, align: end),
177 | place(dx: 50%, dy: 50%, place(center + horizon)[`fill`]),
178 | name: "circle",
179 | )
180 | #describe-mark(
181 | width: 3cm,
182 | length: 2cm,
183 | diamond.with(fill: none, align: end),
184 | place(dx: 50%, dy: 50%, place(center + horizon)[`fill`]),
185 | name: "diamond",
186 | )
187 | #describe-mark(
188 | width: 3cm,
189 | len: 1.4cm,
190 | tikz,
191 | name: "tikz",
192 | )
193 |
--------------------------------------------------------------------------------
/tests/curve-tips/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: auto, height: auto, margin: 10pt)
3 |
4 | #set table(
5 | stroke: none,
6 | columns: (auto, 2cm),
7 | rows: 20pt,
8 | )
9 |
10 | #set align(right)
11 |
12 |
13 | == Move
14 |
15 | #table(
16 | [Normal],
17 | curve(
18 | std.curve.move((20pt, 20pt)),
19 | tip: stealth,
20 | ),
21 | )
22 |
23 | #pagebreak()
24 |
25 |
26 | == Line
27 |
28 | #table(
29 | [Normal],
30 | curve(
31 | std.curve.line((20pt, 10pt)),
32 | tip: stealth,
33 | ),
34 | [Move + line],
35 | curve(
36 | std.curve.move((40pt, 0pt)),
37 | std.curve.line((20pt, 10pt)),
38 | tip: stealth,
39 | ),
40 | [Line + Line],
41 | curve(
42 | std.curve.line((20pt, 0pt)),
43 | std.curve.line((20pt, 10pt)),
44 | tip: stealth,
45 | ),
46 | [Quad + Line],
47 | curve(
48 | std.curve.quad((0pt, 10pt), (20pt, 0pt)),
49 | std.curve.line((20pt, 10pt)),
50 | tip: stealth,
51 | ),
52 | )
53 |
54 | #pagebreak()
55 |
56 |
57 | == Quad
58 |
59 | #table(
60 | [Normal],
61 | curve(
62 | std.curve.quad((10pt, 0pt), (20pt, 10pt)),
63 | tip: stealth,
64 | ),
65 | [With `control: none`],
66 | curve(
67 | std.curve.quad(none, (20pt, 10pt)),
68 | tip: stealth,
69 | ),
70 | [With `control: none` and `curve.move` before],
71 | curve(
72 | std.curve.move((10pt, 0pt)),
73 | std.curve.quad(none, (30pt, 10pt)),
74 | tip: stealth,
75 | ),
76 | [With `control: none` and `curve.close` before],
77 | curve(
78 | std.curve.close(),
79 | std.curve.quad(none, (20pt, 10pt)),
80 | tip: stealth,
81 | ),
82 | [With `control: none` and `curve.line` before],
83 | curve(
84 | std.curve.line((10pt, 0pt)),
85 | std.curve.quad(none, (30pt, 10pt)),
86 | tip: stealth,
87 | ),
88 | [With `control: none` and `curve.quad` before],
89 | curve(
90 | std.curve.quad(none, (10pt, 0pt)),
91 | std.curve.quad(none, (30pt, 10pt)),
92 | tip: stealth,
93 | ),
94 | [With `control: auto` and nothing before],
95 | curve(
96 | std.curve.quad(auto, (20pt, 10pt)),
97 | tip: stealth,
98 | ),
99 | [With `control: auto` and `curve.line` before],
100 | curve(
101 | std.curve.line((10pt, 0pt)),
102 | std.curve.quad(auto, (20pt, 10pt)),
103 | tip: stealth,
104 | ),
105 | [With `control: auto` and `curve.quad` before],
106 | curve(
107 | std.curve.quad((10pt, 0pt), (20pt, 0pt)),
108 | std.curve.quad(auto, (20pt, 10pt)),
109 | tip: stealth,
110 | ),
111 | [With `control: auto` and `curve.quad` before but with `control: none`],
112 | curve(
113 | std.curve.quad(none, (20pt, 0pt)),
114 | std.curve.quad(auto, (20pt, 10pt)),
115 | tip: stealth,
116 | ),
117 | [With `control: auto` and `curve.cubic` before],
118 | curve(
119 | std.curve.cubic(none, (10pt, 0pt), (20pt, 0pt)),
120 | std.curve.quad(auto, (20pt, 10pt)),
121 | tip: stealth,
122 | ),
123 | [With `control: auto` and `curve.cubic` before but with `control: none`],
124 | curve(
125 | std.curve.cubic(none, none, (20pt, 0pt)),
126 | std.curve.quad(auto, (20pt, 10pt)),
127 | tip: stealth,
128 | ),
129 | )
130 |
131 |
132 | #pagebreak()
133 |
134 |
135 | == Cubic
136 |
137 | #table(
138 | [Normal],
139 | curve(
140 | std.curve.cubic(none, (10pt, 0pt), (20pt, 10pt)),
141 | tip: stealth,
142 | ),
143 | [With `control-end: none`],
144 | curve(
145 | std.curve.cubic((10pt, 0pt), none, (20pt, 10pt)),
146 | tip: stealth,
147 | ),
148 | [With `control-start: none` and `control-end: none`],
149 | curve(
150 | std.curve.cubic(none, none, (20pt, 10pt)),
151 | tip: stealth,
152 | ),
153 | [With `control-start: none` and `control-end: none` and `curve.move` before],
154 | curve(
155 | std.curve.move((40pt, 0pt)),
156 | std.curve.cubic(none, none, (20pt, 10pt)),
157 | tip: stealth,
158 | ),
159 | [With `control-start: none` and `control-end: none` and `curve.line` before],
160 | curve(
161 | std.curve.line((40pt, 0pt)),
162 | std.curve.cubic(none, none, (20pt, 10pt)),
163 | tip: stealth,
164 | ),
165 | [With `control-start: auto` and `control-end: none` and `curve.line` before],
166 | curve(
167 | tip: triangle,
168 | std.curve.cubic(none, (0pt, 10pt), (20pt, 0pt)),
169 | std.curve.cubic(auto, none, (20pt, 10pt)),
170 | ),
171 | )
172 |
173 |
174 | // = Relative
175 | // not quite done yet
176 | // == line
177 | // // #set std.curve.line(relative: true)
178 | // #curve(
179 | // std.curve.move((20pt, 0pt)),
180 | // std.curve.line((10pt, 10pt), relative: true),
181 | // std.curve.line((20pt, 20pt), relative: true),
182 | // tip: stealth
183 | // ),
184 | // #curve(
185 | // std.curve.move((20pt, 0pt)),
186 | // std.curve.line((10pt, 10pt)),
187 | // std.curve.line((20pt, 20pt), relative: true),
188 | // tip: stealth
189 | // ),
190 | // #curve(
191 | // std.curve.move((40pt, 0pt)),
192 | // std.curve.line((20pt, 20pt), relative: true),
193 | // tip: stealth
194 | // ),
195 |
196 |
197 | // #curve(
198 | // std.curve.quad((0pt, 10pt), (20pt, 0pt)),
199 | // std.curve.line((10pt, 20pt), relative: true),
200 | // tip: stealth
201 | // ),
202 |
203 | // #curve(
204 | // std.curve.quad((0pt, 10pt), (40pt, 0pt)),
205 | // // std.curve.quad(auto, (10pt, 20pt), relative: true),
206 | // tip: stealth
207 | // ),
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/tests/curve-toes/test.typ:
--------------------------------------------------------------------------------
1 | #import "/src/tiptoe.typ": *
2 | #set page(width: auto, height: auto, margin: 10pt)
3 |
4 | #set table(
5 | stroke: none,
6 | columns: (auto, 2cm),
7 | rows: 20pt,
8 | )
9 |
10 | #set align(right)
11 |
12 |
13 | // == Move + move
14 |
15 | // #table(
16 | // [normal],
17 | // std.curve(
18 | // std.curve.move((20pt, 0pt)),
19 | // std.curve.move((30pt, 0pt), relative: true),
20 | // std.curve.line((30pt, 0pt), relative: true),
21 | // toe: stealth.with(stroke: red)
22 | // ),
23 | // )
24 | // #highlight[todo]
25 |
26 | // #pagebreak()
27 |
28 |
29 | == Move + line
30 |
31 | #table(
32 | [normal],
33 | curve(
34 | std.curve.move((20pt, 0pt)),
35 | std.curve.line((30pt, 10pt)),
36 | toe: stealth.with(stroke: red),
37 | ),
38 | [`relative: true`],
39 | curve(
40 | std.curve.move((20pt, 0pt)),
41 | std.curve.line((10pt, 10pt), relative: true),
42 | toe: stealth.with(stroke: red),
43 | ),
44 | [`relative: true` in `set` rule],
45 | [
46 | #set std.curve.line(relative: true)
47 | #curve(
48 | std.curve.move((20pt, 0pt)),
49 | std.curve.line((10pt, 10pt)),
50 | toe: stealth.with(stroke: red),
51 | )
52 | ],
53 | )
54 |
55 | #pagebreak()
56 |
57 |
58 | == Move + quad
59 |
60 | #table(
61 | [Normal],
62 | curve(
63 | std.curve.move((20pt, 0pt)),
64 | std.curve.quad((30pt, 0pt), (30pt, 10pt)),
65 | toe: stealth.with(stroke: red),
66 | ),
67 | [With `control: none`],
68 | curve(
69 | std.curve.move((20pt, 0pt)),
70 | std.curve.quad(none, (30pt, 10pt)),
71 | toe: stealth.with(stroke: red),
72 | ),
73 | [With `relative: true` ],
74 | curve(
75 | std.curve.move((20pt, 0pt)),
76 | std.curve.quad((10pt, 0pt), (10pt, 10pt), relative: true),
77 | toe: stealth.with(stroke: red),
78 | ),
79 | [With `relative: true` in `set` rule],
80 | [
81 | #set std.curve.line(relative: true)
82 | #curve(
83 | std.curve.move((20pt, 0pt)),
84 | std.curve.line((10pt, 10pt)),
85 | toe: stealth.with(stroke: red),
86 | )
87 | ],
88 | )
89 |
90 | #pagebreak()
91 |
92 |
93 | == Move + cubic
94 | #table(
95 | [Normal],
96 | curve(
97 | std.curve.move((20pt, 0pt)),
98 | std.curve.cubic((20pt, 20pt), (30pt, 0pt), (30pt, 10pt)),
99 | toe: stealth.with(stroke: red),
100 | ),
101 | [With `control-start: none` ],
102 | curve(
103 | std.curve.move((20pt, 0pt)),
104 | std.curve.cubic(none, (30pt, 0pt), (30pt, 10pt)),
105 | toe: stealth.with(stroke: red),
106 | ),
107 | [With `control-start: auto`],
108 | curve(
109 | std.curve.move((20pt, 0pt)),
110 | std.curve.cubic(auto, (30pt, 0pt), (30pt, 10pt)),
111 | toe: stealth.with(stroke: red),
112 | ),
113 | [With `control-start: auto` and `control-end: none`],
114 | curve(
115 | std.curve.move((20pt, 0pt)),
116 | std.curve.cubic(auto, none, (30pt, 10pt)),
117 | toe: stealth.with(stroke: red),
118 | ),
119 | [With `relative: true` ],
120 | curve(
121 | std.curve.move((20pt, 0pt)),
122 | std.curve.cubic((0pt, 20pt), (10pt, 0pt), (10pt, 10pt), relative: true),
123 | toe: stealth.with(stroke: red),
124 | ),
125 | [With `relative: true` and `control-start: none`],
126 | curve(
127 | std.curve.move((20pt, 0pt)),
128 | std.curve.cubic(none, (10pt, 0pt), (10pt, 10pt), relative: true),
129 | toe: stealth.with(stroke: red),
130 | ),
131 | [With `relative: true` and `control-start: auto`],
132 | curve(
133 | std.curve.move((20pt, 0pt)),
134 | std.curve.cubic(auto, (10pt, 0pt), (10pt, 10pt), relative: true),
135 | toe: stealth.with(stroke: red),
136 | ),
137 | [With `relative: true` and `control-start: none` and `control-end: none`],
138 | curve(
139 | std.curve.move((20pt, 0pt)),
140 | std.curve.cubic(none, none, (10pt, 10pt), relative: true),
141 | toe: stealth.with(stroke: red),
142 | ),
143 | [With `relative: true` in `set` rule],
144 | [
145 | #set std.curve.line(relative: true)
146 | #curve(
147 | std.curve.move((20pt, 0pt)),
148 | std.curve.line((10pt, 10pt)),
149 | toe: stealth.with(stroke: red),
150 | )
151 | ],
152 | )
153 |
154 | #pagebreak()
155 |
156 |
157 | == Line
158 |
159 | #table(
160 | [Normal],
161 | curve(
162 | std.curve.line((20pt, 10pt)),
163 | toe: stealth.with(stroke: red),
164 | ),
165 | [With `relative: true` in `set` rule],
166 | [
167 | #set std.curve.line(relative: true)
168 | #curve(
169 | std.curve.line((20pt, 10pt)),
170 | toe: stealth.with(stroke: red),
171 | )
172 | ],
173 | )
174 |
175 | #pagebreak()
176 |
177 |
178 | == Quad
179 |
180 | #table(
181 | [Normal],
182 | curve(
183 | std.curve.quad((10pt, 0pt), (20pt, 10pt)),
184 | toe: stealth.with(stroke: red),
185 | ),
186 | [With `control: none`],
187 | curve(
188 | std.curve.quad(none, (20pt, 10pt)),
189 | toe: stealth.with(stroke: red),
190 | ),
191 | [With `control: auto`],
192 | curve(
193 | std.curve.quad(auto, (20pt, 10pt)),
194 | toe: stealth.with(stroke: red),
195 | ),
196 | [With `relative: true` in `set` rule],
197 | [
198 | #set std.curve.quad(relative: true)
199 | #curve(
200 | std.curve.quad((10pt, 0pt), (20pt, 10pt)),
201 | toe: stealth.with(stroke: red),
202 | )
203 | ],
204 | )
205 |
206 | #pagebreak()
207 |
208 |
209 | == Cubic
210 |
211 | #table(
212 | [Normal],
213 | curve(
214 | std.curve.cubic((0pt, 20pt), (10pt, 0pt), (20pt, 10pt)),
215 | toe: stealth.with(stroke: red),
216 | ),
217 | [With `control-start: none`],
218 | curve(
219 | std.curve.cubic(none, (0pt, 20pt), (20pt, 10pt)),
220 | toe: stealth.with(stroke: red),
221 | ),
222 | [With `control-start: auto`],
223 | curve(
224 | std.curve.cubic(auto, (0pt, 20pt), (20pt, 10pt)),
225 | toe: stealth.with(stroke: red),
226 | ),
227 | [With `control-start: none` and `control-end: none`],
228 | curve(
229 | std.curve.cubic(none, none, (20pt, 10pt)),
230 | toe: stealth.with(stroke: red),
231 | ),
232 | [With `relative: true` in `set` rule],
233 | [
234 | #set std.curve.cubic(relative: true)
235 | #curve(
236 | std.curve.cubic((0pt, 20pt), (10pt, 0pt), (20pt, 10pt)),
237 | toe: stealth.with(stroke: red),
238 | )
239 | ],
240 | )
241 |
--------------------------------------------------------------------------------
/docs/figures/out/intro-example.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/docs/figures/out/intro-example-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/marks.typ:
--------------------------------------------------------------------------------
1 | #import "arc-impl.typ": arc-impl
2 | #import "utility.typ"
3 |
4 |
5 |
6 | #let bar(
7 | width: 2.4pt + 360%,
8 | stroke: auto,
9 | align: center,
10 | line: stroke(),
11 | ) = {
12 | stroke = utility.process-stroke(line, stroke)
13 | let (width,) = utility.process-dims(
14 | line,
15 | width: width,
16 | )
17 |
18 | assert(align in (center, end))
19 | let offset = if align == end { stroke.thickness / 2 } else { 0pt }
20 |
21 | (
22 | mark: place(std.line(
23 | start: (-offset, -width / 2),
24 | end: (-offset, width / 2),
25 | stroke: stroke,
26 | )),
27 | end: offset,
28 | )
29 | }
30 |
31 |
32 | #let bracket(
33 | width: 2.4pt + 360%,
34 | length: auto,
35 | stroke: auto,
36 | rev: false,
37 | line: stroke(),
38 | ) = {
39 | stroke = utility.process-stroke(line, stroke)
40 | let (width, length) = utility.process-dims(
41 | line,
42 | length: width,
43 | width: length,
44 | default-ratio: .3,
45 | )
46 | (width, length) = (length, width)
47 | let s = stroke.thickness / 2
48 | let y = width / 2 - s
49 | let mark = place(curve(
50 | curve.move((-length, -y)),
51 | curve.line((-s, -y), relative: false),
52 | curve.line((-s, y), relative: false),
53 | curve.line((-length, y), relative: false),
54 | stroke: stroke,
55 | ))
56 | if rev {
57 | mark = scale(x: -100%, place(mark, dx: length))
58 | }
59 | (
60 | mark: mark,
61 | end: if rev { length } else { s },
62 | )
63 | }
64 |
65 |
66 | #let stealth(
67 | length: 3pt + 450%,
68 | width: auto,
69 | inset: 40%,
70 | fill: auto,
71 | rev: false,
72 | stroke: auto,
73 | line: stroke(),
74 | ) = {
75 | let is-auto-stroke = stroke == auto
76 | stroke = utility.process-stroke(line, stroke)
77 | let (width, length) = utility.process-dims(
78 | line,
79 | length: length,
80 | width: width,
81 | default-ratio: .8,
82 | )
83 |
84 | let linewidth = stroke.thickness
85 | let Δl = length * inset
86 | let dhalf = 0.5 * width
87 | let tip-length
88 |
89 | let path = if fill == auto and is-auto-stroke {
90 | // very common optimization for filled arrows
91 | polygon(
92 | (0pt, 0pt),
93 | (-length, dhalf),
94 | (-length + Δl, 0pt),
95 | (-length, -dhalf),
96 | stroke: none,
97 | fill: utility.if-auto(stroke.paint, black),
98 | )
99 | tip-length = length / 2
100 | } else {
101 | let tanα = dhalf / length
102 | let α = calc.atan(tanα)
103 | let sinα = calc.sin(α)
104 | let x3 = 0.5 * linewidth / sinα
105 |
106 | let (x, x4, y) = if inset == 0% {
107 | let x = length - linewidth / 2
108 | (x, x, tanα * (x - x3))
109 | } else {
110 | let tanβ = dhalf / Δl
111 | let β = calc.atan(tanβ)
112 | let x1 = 0.5 * linewidth / calc.sin(β)
113 | let x2 = length - Δl - x1 - x3
114 | let b = -x2 * tanα
115 | let x = b / (tanβ - tanα)
116 | let y = tanβ * x
117 | let x4 = length - Δl - x1
118 | (length - Δl - x1 - x, x4, y)
119 | }
120 | tip-length = x3
121 | polygon(
122 | (-x3, 0pt),
123 | (-x, y),
124 | (-x4, 0pt),
125 | (-x, -y),
126 | stroke: std.stroke(
127 | thickness: linewidth,
128 | paint: utility.if-auto(stroke.paint, black),
129 | dash: stroke.dash,
130 | miter-limit: 7,
131 | join: "miter",
132 | ),
133 | fill: utility.chained-if-auto(fill, stroke.paint, black),
134 | )
135 | }
136 |
137 |
138 | let mark = place(path)
139 | if rev {
140 | mark = scale(x: -100%, place(mark, dx: length))
141 | }
142 | (
143 | mark: mark,
144 | end: if rev { length - tip-length } else { length - Δl },
145 | )
146 | }
147 |
148 |
149 | #let triangle = stealth.with(inset: 0%)
150 |
151 | #let round(
152 | length: 3pt + 450%,
153 | width: auto,
154 | inset: 40%,
155 | rev: false,
156 | stroke: auto,
157 | fill: auto,
158 | line: stroke(),
159 | ) = {
160 | stroke = utility.process-stroke(line, stroke)
161 | let (width, length) = utility.process-dims(
162 | line,
163 | length: length,
164 | width: width,
165 | default-ratio: .8,
166 | )
167 |
168 | let s = 0.5 * stroke.thickness
169 |
170 | if inset >= 100% {
171 | inset = length - stroke.thickness
172 | } else {
173 | inset = length * inset
174 | }
175 |
176 | let mark = place(polygon(
177 | (-s, 0pt),
178 | (-length + s, 0.5 * width - s),
179 | (-length + inset + s, 0pt),
180 | (-length + s, -0.5 * width + s),
181 | fill: utility.chained-if-auto(fill, stroke.paint, black),
182 | stroke: std.stroke(
183 | thickness: stroke.thickness,
184 | paint: utility.if-auto(stroke.paint, black),
185 | dash: stroke.dash,
186 | miter-limit: 7,
187 | join: "round",
188 | ),
189 | ))
190 |
191 | let end = length - inset - s
192 |
193 | if rev {
194 | mark = scale(x: -100%, place(mark, dx: length))
195 | end = length - s
196 | }
197 |
198 | (
199 | mark: mark,
200 | end: end,
201 | )
202 | }
203 |
204 |
205 | #let straight(
206 | length: 3pt + 450%,
207 | width: auto,
208 | rev: false,
209 | stroke: auto,
210 | line: stroke(),
211 | ) = {
212 | stroke = utility.process-stroke(line, stroke)
213 | let (width, length) = utility.process-dims(
214 | line,
215 | length: length,
216 | width: width,
217 | default-ratio: .8,
218 | )
219 |
220 | let s = stroke.thickness / 2
221 | let α = calc.atan(0.5 * width / length)
222 | let tip-length = 0.5 * stroke.thickness / calc.sin(α)
223 |
224 | let mark = place(curve(
225 | curve.move((-length + s, 0.5 * width - s)),
226 | curve.line((-tip-length, 0pt), relative: false),
227 | curve.line((-length + s, -0.5 * width + s), relative: false),
228 | stroke: std.stroke(
229 | thickness: stroke.thickness,
230 | paint: utility.if-auto(stroke.paint, black),
231 | dash: stroke.dash,
232 | miter-limit: 7,
233 | cap: "round",
234 | ),
235 | ))
236 |
237 |
238 | if rev {
239 | mark = scale(x: -100%, place(mark, dx: length))
240 | }
241 |
242 | (
243 | mark: mark,
244 | end: if rev { length - tip-length } else { tip-length },
245 | )
246 | }
247 |
248 | #let diamond(
249 | length: 565.69%,
250 | width: auto,
251 | fill: auto,
252 | stroke: auto,
253 | align: center,
254 | line: stroke(),
255 | ) = {
256 | stroke = utility.process-stroke(line, stroke)
257 | let (width, length) = utility.process-dims(
258 | line,
259 | length: length,
260 | width: width,
261 | default-ratio: 1,
262 | )
263 | let dhalf = 0.5 * width
264 | let tip-length
265 |
266 | let tanα = dhalf / length * 2
267 | let α = calc.atan(tanα)
268 | let tip-length = 0.5 * stroke.thickness / calc.sin(α)
269 | let top-length = 0.5 * stroke.thickness / calc.cos(α)
270 |
271 | assert(align in (center, end))
272 | let offset = if align == center { length / 2 } else { 0pt }
273 |
274 | let mark = place(dx: offset, polygon(
275 | (-length / 2, dhalf - top-length),
276 | (-tip-length, 0pt),
277 | (-length / 2, -dhalf + top-length),
278 | (-length + tip-length, 0pt),
279 | stroke: stroke,
280 | fill: utility.chained-if-auto(fill, stroke.paint, black),
281 | ))
282 |
283 | (
284 | mark: mark,
285 | end: length - tip-length - offset,
286 | )
287 | }
288 |
289 | #let square(
290 | length: 400%,
291 | width: auto,
292 | fill: auto,
293 | stroke: auto,
294 | align: center,
295 | line: stroke(),
296 | ) = {
297 | stroke = utility.process-stroke(line, stroke)
298 | let (width, length) = utility.process-dims(
299 | line,
300 | length: length,
301 | width: width,
302 | default-ratio: 1,
303 | )
304 | assert(align in (center, end))
305 | let offset = if align == center { length / 2 } else { 0pt }
306 | let s = stroke.thickness / 2
307 | let y = width / 2 - s
308 | let mark = place(dx: offset, polygon(
309 | (-s, y),
310 | (-length + s, y),
311 | (-length + s, -y),
312 | (-s, -y),
313 | stroke: stroke,
314 | fill: utility.chained-if-auto(fill, stroke.paint, black),
315 | ))
316 |
317 | (
318 | mark: mark,
319 | end: length - s - offset,
320 | )
321 | }
322 |
323 |
324 |
325 | #let circle(
326 | length: 400%,
327 | width: auto,
328 | fill: auto,
329 | stroke: auto,
330 | align: center,
331 | line: stroke(),
332 | ) = {
333 | stroke = utility.process-stroke(line, stroke)
334 | let (width, length) = utility.process-dims(
335 | line,
336 | length: length,
337 | width: width,
338 | default-ratio: 1,
339 | )
340 | let s = stroke.thickness / 2
341 | let y = width / 2 - s
342 |
343 | assert(align in (center, end))
344 | let offset = if align == center { length / 2 } else { 0pt }
345 |
346 | let mark = place(dx: -length + s + offset, dy: -y, ellipse(
347 | width: length - 2 * s,
348 | height: width - 2 * s,
349 | stroke: stroke,
350 | fill: utility.chained-if-auto(fill, stroke.paint, black),
351 | ))
352 |
353 | (
354 | mark: mark,
355 | end: length - s - offset,
356 | )
357 | }
358 |
359 |
360 | #let rays(
361 | n: 4,
362 | length: 280%,
363 | phase: auto,
364 | stroke: auto,
365 | align: center,
366 | line: stroke(),
367 | ) = {
368 | stroke = utility.process-stroke(line, stroke)
369 | let (length,) = utility.process-dims(
370 | line,
371 | length: length,
372 | )
373 | if phase == auto {
374 | if n == 4 { phase = 45deg } else { phase = -90deg }
375 | }
376 |
377 | assert(align in (center, end))
378 | let offset = if align == end { -length } else { 0pt }
379 |
380 | let mark = for i in range(n) {
381 | place(dx: offset, std.line(
382 | length: length,
383 | angle: phase + 360deg * i / n,
384 | stroke: stroke,
385 | ))
386 | }
387 |
388 | (
389 | mark: mark,
390 | end: -offset,
391 | )
392 | }
393 |
394 |
395 |
396 | #let barb(
397 | width: 3pt + 450%,
398 | arc: 180deg,
399 | stroke: auto,
400 | rev: false,
401 | line: stroke(),
402 | ) = {
403 | stroke = utility.process-stroke(line, stroke)
404 | let (width,) = utility.process-dims(
405 | line,
406 | width: width,
407 | )
408 |
409 | let s = stroke.thickness / 2
410 | let radius = width / 2 - s
411 |
412 | let mark = place(
413 | arc-impl(
414 | origin: (-width / 2, 0pt),
415 | angle: -arc / 2,
416 | arc: arc,
417 | radius: radius,
418 | stroke: stroke,
419 | ),
420 | )
421 | let end = s
422 |
423 | if rev {
424 | mark = scale(x: -100%, place(mark, dx: width / 2))
425 | end = radius
426 | }
427 |
428 | (
429 | mark: mark,
430 | end: end,
431 | )
432 | }
433 |
434 |
435 | #let hooks(
436 | width: 3pt + 450%,
437 | arc: 180deg,
438 | stroke: auto,
439 | rev: false,
440 | line: stroke(),
441 | ) = {
442 | stroke = utility.process-stroke(line, stroke)
443 | let (width,) = utility.process-dims(
444 | line,
445 | width: width,
446 | )
447 |
448 | let s = stroke.thickness / 2
449 | let r = (width / 2 - s) / 2
450 |
451 | let mark = {
452 | place(
453 | arc-impl(
454 | origin: (-r - s, r),
455 | angle: -90deg,
456 | arc: arc,
457 | radius: r,
458 | stroke: stroke,
459 | ),
460 | )
461 | place(
462 | arc-impl(
463 | origin: (-r - s, -r),
464 | angle: 90deg,
465 | arc: -arc,
466 | radius: r,
467 | stroke: stroke,
468 | ),
469 | )
470 | }
471 | let end = r - s
472 |
473 | if rev {
474 | mark = scale(x: -100%, place(mark, dx: r + s))
475 | end = 0pt
476 | }
477 |
478 | (
479 | mark: mark,
480 | end: end + s,
481 | )
482 | }
483 |
484 |
485 |
486 | #let tikz(
487 | width: 3pt + 450%,
488 | arc: 180deg,
489 | stroke: auto,
490 | line: stroke(),
491 | ) = {
492 | let sharp = false
493 | stroke = utility.process-stroke(line, stroke)
494 | let (width,) = utility.process-dims(
495 | line,
496 | width: width,
497 | )
498 |
499 | let s = stroke.thickness / 2
500 | stroke = if sharp {
501 | (
502 | thickness: stroke.thickness,
503 | paint: stroke.paint,
504 | join: "bevel",
505 | cap: "butt",
506 | )
507 | } else {
508 | (
509 | thickness: stroke.thickness,
510 | paint: stroke.paint,
511 | join: "round",
512 | cap: "round",
513 | )
514 | }
515 | let x0 = if sharp { stroke.thickness / width * 10pt } else { -s }
516 | let mark = place(
517 | curve(
518 | curve.move((-width * 0.42, -width / 2 + s)),
519 | curve.cubic(
520 | (-width * 0.32, -width * 0.1 + s),
521 | none,
522 | (x0, 0pt),
523 | relative: false,
524 | ),
525 | curve.cubic(
526 | none,
527 | (-width * 0.32, width * 0.1 - s),
528 | (-width * 0.42, width / 2 - s),
529 | relative: false,
530 | ),
531 | stroke: stroke,
532 | ),
533 | )
534 |
535 | (
536 | mark: mark,
537 | end: stroke.thickness,
538 | )
539 | }
540 |
541 |
542 | #let combine(
543 | line: stroke(),
544 | ..marks,
545 | ) = (line: stroke()) => {
546 | let marks = marks.pos()
547 | let linewidth = utility.if-auto(line.thickness, 1pt)
548 |
549 | let combined-mark
550 | let pos = 0pt
551 | let end
552 |
553 | for mark in marks {
554 | if mark == std.end {
555 | end = pos
556 | } else if type(mark) == length {
557 | pos += mark
558 | } else if type(mark) == ratio {
559 | pos += mark * linewidth
560 | } else if type(mark) == function {
561 | mark = mark(line: line)
562 | combined-mark += place(dx: -pos, mark.mark)
563 | pos += mark.end
564 | }
565 | }
566 | if end == none {
567 | end = pos
568 | }
569 |
570 | (
571 | mark: combined-mark,
572 | end: end,
573 | )
574 | }
575 |
576 |
--------------------------------------------------------------------------------
/src/curve.typ:
--------------------------------------------------------------------------------
1 | #import "marks.typ": *
2 | #import "assert.typ": assert-mark
3 | #import "path-to-curve.typ": path-to-curve
4 |
5 |
6 | #let first-not-none-or-auto(..args) = (
7 | args.pos().find(x => x != none and x != auto)
8 | )
9 |
10 | #let add(p, q) = p.zip(q).map(array.sum)
11 | #let sub(p, q) = p.zip(q).map(((a, b)) => a - b)
12 | #let add-if-array(p, q) = if type(p) == array { add(p, q) } else { p }
13 |
14 | // Mirrors control around origin
15 | #let mirror(control, origin) = add(origin, sub(origin, control))
16 |
17 |
18 |
19 | // When encountering a final curve element with an `auto` control,
20 | // we can use this function to compute this control based on the
21 | // previous curve element.
22 | #let get-prev-mirrored-control(segments) = {
23 | let p = segments.at(-2, default: std.curve.move((0pt, 0pt)))
24 | if p.func() == std.curve.close {
25 | p = std.curve.move((0pt, 0pt))
26 | }
27 |
28 | if p.func() == std.curve.move {
29 | p.start
30 | } else if p.func() == std.curve.line {
31 | p.end
32 | } else if p.func() == std.curve.quad {
33 | if p.control == none {
34 | p.end
35 | } else {
36 | mirror(p.control, p.end)
37 | }
38 | } else if p.func() == std.curve.cubic {
39 | if p.control-end == none {
40 | p.end
41 | } else {
42 | mirror(p.control-end, p.end)
43 | }
44 | }
45 | }
46 |
47 | // Test whether a curve segment is relative (either explicitly or by a set rule)
48 | #let is-relative(segment) = {
49 | if segment.has("relative") { return segment.relative }
50 | return segment.func().relative
51 | }
52 |
53 |
54 | #let curve-to-absolute-polygon(segments) = {
55 | let base = (0pt, 0pt)
56 | let index-last-absolute-segment = -1
57 |
58 | for i in range(1, segments.len() + 1) {
59 | let index = segments.len() - i
60 | let segment = segments.at(index)
61 |
62 | if segment.func() == std.curve.move {
63 | index-last-absolute-segment = index
64 | base = segment.start
65 | break
66 | } else if not is-relative(segment) {
67 | index-last-absolute-segment = index
68 | base = segment.end
69 | break
70 | }
71 | }
72 | let polygon = (base,)
73 |
74 | for (i, segment) in segments
75 | .enumerate()
76 | .slice(index-last-absolute-segment + 1) {
77 | if segment.func() == std.curve.line {
78 | base = add(base, segment.end)
79 | polygon.push(base)
80 | } else if segment.func() == std.curve.quad {
81 | if i == segments.len() - 1 {
82 | polygon.push(add(base, segment.control))
83 | }
84 | base = add(base, segment.end)
85 | polygon.push(base)
86 | } else if segment.func() == std.curve.cubic {
87 | if i == segments.len() - 1 {
88 | polygon.push(add(base, segment.control-start))
89 | polygon.push(add(base, segment.control-end))
90 | }
91 | base = add(base, segment.end)
92 | polygon.push(base)
93 | }
94 | }
95 | polygon
96 | }
97 |
98 |
99 | #assert.eq(
100 | curve-to-absolute-polygon((
101 | std.curve.move((10pt, 10pt)),
102 | std.curve.line((10pt, 10pt), relative: true),
103 | )),
104 | ((10pt, 10pt), (20pt, 20pt)),
105 | )
106 | #assert.eq(
107 | curve-to-absolute-polygon((
108 | std.curve.move((10pt, 10pt)),
109 | std.curve.line((0pt, 10pt), relative: true),
110 | std.curve.line((10pt, 0pt), relative: true),
111 | )),
112 | ((10pt, 10pt), (10pt, 20pt), (20pt, 20pt)),
113 | )
114 | #assert.eq(
115 | curve-to-absolute-polygon((
116 | std.curve.move((10pt, 10pt)),
117 | std.curve.quad((0pt, 10pt), (10pt, 10pt), relative: true),
118 | std.curve.line((10pt, 0pt), relative: true),
119 | )),
120 | ((10pt, 10pt), (20pt, 20pt), (30pt, 20pt)),
121 | )
122 |
123 | #let resolve-relative(segments) = {
124 | let base = (0pt, 0pt)
125 | for segment in segments.rev() {
126 | if segment.func() == std.curve.move {
127 | base = add(base, segment.start)
128 | break
129 | } else {
130 | base = add(base, segment.end)
131 | if not is-relative(segment) {
132 | break
133 | }
134 | }
135 | }
136 | let prev
137 | if segments.last().func() == std.curve.move {
138 | prev = sub(base, segments.last().start)
139 | } else {
140 | prev = sub(base, segments.last().end)
141 | }
142 | (prev, base)
143 | }
144 |
145 |
146 | #assert.eq(
147 | resolve-relative((
148 | std.curve.move((10pt, 10pt)),
149 | std.curve.line((10pt, 10pt), relative: true),
150 | )),
151 | ((10pt, 10pt), (20pt, 20pt)),
152 | )
153 | #assert.eq(
154 | resolve-relative((
155 | std.curve.move((10pt, 10pt)),
156 | std.curve.line((0pt, 10pt), relative: true),
157 | std.curve.line((10pt, 0pt), relative: true),
158 | )),
159 | ((10pt, 20pt), (20pt, 20pt)),
160 | )
161 | #assert.eq(
162 | resolve-relative((
163 | std.curve.move((10pt, 10pt)),
164 | std.curve.line((0pt, 10pt), relative: true),
165 | std.curve.line((0pt, 10pt), relative: true),
166 | std.curve.line((0pt, 10pt), relative: false),
167 | std.curve.line((10pt, 0pt), relative: true),
168 | )),
169 | ((0pt, 10pt), (10pt, 10pt)),
170 | )
171 |
172 | #assert.eq(
173 | resolve-relative((
174 | std.curve.move((10pt, 10pt)),
175 | std.curve.line((10pt, 10pt), relative: true),
176 | )),
177 | ((10pt, 10pt), (20pt, 20pt)),
178 | )
179 |
180 |
181 | #let treat-tip(mark, segments, inner, shorten: 100%) = {
182 | let final-segment = segments.last()
183 | let args = (:)
184 | let end = final-segment.end
185 |
186 | if is-relative(final-segment) {
187 | if final-segment.has("relative") {
188 | args.relative = true
189 | }
190 | (inner, end) = resolve-relative(segments)
191 |
192 | (.., inner, end) = curve-to-absolute-polygon(segments)
193 | }
194 |
195 | let dx = (end.at(0) - inner.at(0)).length.to-absolute() / 1pt
196 | let dy = (end.at(1) - inner.at(1)).length.to-absolute() / 1pt
197 | let angle = calc.atan2(dx, dy)
198 | let mark-content = place(
199 | dx: end.at(0),
200 | dy: end.at(1),
201 | rotate(angle, mark.mark, reflow: false),
202 | )
203 | end.at(0) -= calc.cos(angle) * mark.end * shorten
204 | end.at(1) -= calc.sin(angle) * mark.end * shorten
205 | (mark-content, end, args)
206 | }
207 |
208 |
209 |
210 | #let treat-toe(mark, vertex0, vertex1, shorten: 100%) = {
211 | let dx = (vertex0.at(0) - vertex1.at(0)).length.to-absolute() / 1pt
212 | let dy = (vertex0.at(1) - vertex1.at(1)).length.to-absolute() / 1pt
213 | let angle = calc.atan2(dx, dy)
214 |
215 | let mark-content = place(
216 | dx: vertex0.at(0),
217 | dy: vertex0.at(1),
218 | rotate(angle, mark.mark, reflow: false),
219 | )
220 |
221 | vertex0.at(0) -= calc.cos(angle) * mark.end * shorten
222 | vertex0.at(1) -= calc.sin(angle) * mark.end * shorten
223 |
224 | (mark-content, vertex0)
225 | }
226 |
227 |
228 |
229 |
230 | #let curve(
231 | ..segments,
232 | fill: auto,
233 | fill-rule: auto,
234 | stroke: auto,
235 | tip: none,
236 | toe: none,
237 | shorten: 100%,
238 | ) = {
239 | if segments.named().len() != 0 {
240 | assert(
241 | false,
242 | message: "Unexpected named argument \""
243 | + segments.named().keys().first()
244 | + "\"",
245 | )
246 | }
247 |
248 | set place(left)
249 | set std.curve(fill: fill) if fill != auto
250 | set std.curve(stroke: stroke) if stroke != auto
251 | set std.curve(fill-rule: fill-rule) if fill-rule != auto
252 |
253 |
254 | assert(
255 | type(shorten) in (ratio, dictionary),
256 | message: "Expected ratio or dictionary for parameter `shorten`, found "
257 | + str(type(shorten)),
258 | )
259 | if type(shorten) == ratio {
260 | shorten = (start: shorten, end: shorten)
261 | } else if type(shorten) == dictionary {
262 | assert(
263 | shorten.keys().sorted() == ("end", "start"),
264 | message: "Unexpected key, valid keys are \"start\" and \"end\"",
265 | )
266 | }
267 |
268 |
269 | context {
270 | let stroke = std.curve.stroke
271 | if stroke == auto {
272 | stroke = std.stroke()
273 | } else {
274 | stroke = std.stroke(stroke)
275 | }
276 |
277 | let segments = segments.pos()
278 |
279 | let marks
280 | if toe != none and segments.len() >= 1 {
281 | assert-mark(toe, kind: "toe")
282 | let toe = toe(line: stroke)
283 | let first-segment = segments.first()
284 |
285 | if first-segment.func() == std.curve.move and segments.len() >= 2 {
286 | let vertex0 = first-segment.start
287 | let relative = is-relative(segments.at(1))
288 |
289 | // Obtain next vertex: either an end point or the next control point
290 | let vertex1 = {
291 | let se = segments.at(1)
292 | if se.func() == std.curve.line {
293 | if relative {
294 | // We will change the first curve.move, so this second segment cannot be relative
295 | segments.at(1) = std.curve.line(
296 | add(vertex0, se.end),
297 | relative: false,
298 | )
299 | }
300 | se.end
301 | } else if se.func() == std.curve.quad {
302 | if relative {
303 | // We will change the first curve.move, so this second segment cannot be relative
304 | segments.at(1) = std.curve.quad(
305 | add-if-array(se.control, vertex0),
306 | add(se.end, vertex0),
307 | relative: false,
308 | )
309 | }
310 | first-not-none-or-auto(se.control, se.end)
311 | } else if se.func() == std.curve.cubic {
312 | if relative {
313 | // We will change the first curve.move, so this second segment cannot be relative
314 | segments.at(1) = std.curve.cubic(
315 | add-if-array(se.control-start, vertex0),
316 | add-if-array(se.control-end, vertex0),
317 | add(se.end, vertex0),
318 | relative: false,
319 | )
320 | }
321 | first-not-none-or-auto(se.control-start, se.control-end, se.end)
322 | }
323 | }
324 |
325 | if relative {
326 | vertex1 = add(vertex0, vertex1)
327 | }
328 |
329 | let (mark, new-vertex0) = treat-toe(
330 | toe,
331 | vertex0,
332 | vertex1,
333 | shorten: shorten.start,
334 | )
335 | marks += mark
336 | segments.first() = std.curve.move(new-vertex0)
337 | } else if first-segment.func() == std.curve.line {
338 | let (mark, new-vertex0) = treat-toe(
339 | toe,
340 | (0pt, 0pt),
341 | first-segment.end,
342 | shorten: shorten.start,
343 | )
344 | marks += mark
345 | segments.first() = std.curve.line(first-segment.end, relative: false)
346 | segments.insert(0, std.curve.move(new-vertex0))
347 | } else if first-segment.func() == std.curve.quad {
348 | let vertex1 = first-not-none-or-auto(
349 | first-segment.control,
350 | first-segment.end,
351 | )
352 | let (mark, new-vertex0) = treat-toe(
353 | toe,
354 | (0pt, 0pt),
355 | vertex1,
356 | shorten: shorten.start,
357 | )
358 | marks += mark
359 | segments.first() = std.curve.quad(
360 | first-segment.control,
361 | first-segment.end,
362 | relative: false,
363 | )
364 | segments.insert(0, std.curve.move(new-vertex0))
365 | } else if first-segment.func() == std.curve.cubic {
366 | let vertex1 = first-not-none-or-auto(
367 | first-segment.control-start,
368 | first-segment.control-end,
369 | first-segment.end,
370 | )
371 | let (mark, new-vertex0) = treat-toe(
372 | toe,
373 | (0pt, 0pt),
374 | vertex1,
375 | shorten: shorten.start,
376 | )
377 | marks += mark
378 | segments.first() = std.curve.cubic(
379 | first-segment.control-start,
380 | first-segment.control-end,
381 | first-segment.end,
382 | relative: false,
383 | )
384 | segments.insert(0, std.curve.move(new-vertex0))
385 | }
386 | }
387 |
388 |
389 | if tip != none and segments.len() >= 1 {
390 | assert-mark(tip, kind: "tip")
391 | let tip = tip(line: stroke)
392 | let final-segment = segments.last()
393 |
394 | // Obtain previous vertex: either an end point or the last control point
395 | let vertex-n-1 = {
396 | let p = segments.at(-2, default: std.curve.move((0pt, 0pt)))
397 | if p.func() == std.curve.close { p = std.curve.move((0pt, 0pt)) }
398 | if p.func() == std.curve.move {
399 | p.start
400 | } else {
401 | p.end
402 | }
403 | }
404 |
405 | if final-segment.func() == std.curve.close {
406 | assert(
407 | false,
408 | message: "Tips are not supported on the `curve.close` element",
409 | )
410 | } else if final-segment.func() == std.curve.line {
411 | let (mark, new-vertex-n, args) = treat-tip(
412 | tip,
413 | segments,
414 | vertex-n-1,
415 | shorten: shorten.end,
416 | )
417 | marks += mark
418 | segments.last() = std.curve.line(new-vertex-n, relative: false)
419 | } else if final-segment.func() == std.curve.quad {
420 | if final-segment.control != none {
421 | if final-segment.control == auto {
422 | vertex-n-1 = get-prev-mirrored-control(segments)
423 | } else {
424 | vertex-n-1 = final-segment.control
425 | }
426 | }
427 |
428 | let (mark, new-vertex-n, args) = treat-tip(
429 | tip,
430 | segments,
431 | vertex-n-1,
432 | shorten: shorten.end,
433 | )
434 |
435 | marks += mark
436 | segments.last() = std.curve.quad(
437 | final-segment.control,
438 | new-vertex-n,
439 | relative: false,
440 | )
441 | } else if final-segment.func() == std.curve.cubic {
442 | if final-segment.control-end != none {
443 | vertex-n-1 = final-segment.control-end
444 | } else if final-segment.control-start == auto {
445 | vertex-n-1 = get-prev-mirrored-control(segments)
446 | } else if final-segment.control-start != none {
447 | vertex-n-1 = final-segment.control-start
448 | }
449 |
450 | let (mark, new-vertex-n, args) = treat-tip(
451 | tip,
452 | segments,
453 | vertex-n-1,
454 | shorten: shorten.end,
455 | )
456 |
457 | marks += mark
458 | segments.last() = std.curve.cubic(
459 | final-segment.control-start,
460 | final-segment.control-end,
461 | new-vertex-n,
462 | relative: false,
463 | )
464 | }
465 | }
466 |
467 | place(std.curve(..segments)) + marks
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | _Arrows for [Typst][typst] paths and other stories._
10 |
11 | [](https://typst.app/universe/package/tiptoe)
12 | [](https://github.com/Mc-Zen/tiptoe/actions/workflows/run_tests.yml)
13 | [](https://github.com/Mc-Zen/tiptoe/blob/main/LICENSE)
14 |
15 | ---
16 |
17 | *Tiptoe* adds configurable arrow tips (and toes) to the functions `line()` and `curve()`, and `path()`. Moreover, it adds the geometric primitives `arc()` and `ring()`.
18 |
19 | - [Tiptoe vs. Fletcher](#tiptoe-vs-fletcher)
20 | - [Available marks](#available-marks)
21 | - [Mark sizing and styling](#mark-sizing-and-styling)
22 | - [Mark alignment](#mark-alignment)
23 | - [Path shortening](#path-shortening)
24 | - [Combining marks](#combining-marks)
25 | - [Defining custom marks](#defining-custom-marks)
26 | - [Arc](#arc)
27 | - [Ring](#ring)
28 | - [Differences between `std.curve` and `tiptoe.curve`](#differences-between-stdcurve-and-tiptoecurve)
29 |
30 |
31 | The functions `tiptoe.line()`, `tiptoe.curve()`, and `tiptoe.path()` act as a drop-in replacement (except that [they are placed by default](#difference-between-built-in-and-tiptoe-path)) for their counterparts in the Typst standard library − but they are enhanced by additional `tip` and `toe` (you have read the title, what did you expect??) arguments.
32 |
33 | Let us consider a simple example to start off.
34 | ```typ
35 | #import "@preview/tiptoe:0.4.0": *
36 |
37 | #line(tip: stealth, toe: stealth.with(rev: true))
38 | #curve(
39 | tip: triangle, toe: bar,
40 | std.curve.cubic((10pt, 0pt), (20pt, 0pt), (20pt, 10pt)),
41 | std.curve.cubic(auto, none, (0pt, 20pt)),
42 | ))
43 | ```
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ## Tiptoe vs. Fletcher
55 |
56 | _Before going into the details:_ There exists another awesome package that provides great support for arrows and marks: [Fletcher][fletcher] by [Jollywatt][jollywatt]. If you wonder which package to use, the decision is easy because their use-cases are almost complementary.
57 | - Fletcher works with (and needs) [CeTZ][cetz] while
58 | - Tiptoe does not need (and does not really work with) CeTZ.
59 |
60 | So, if you want to create CeTZ graphics − use Fletcher! If you don't want to use CeTZ − maybe because you just need a single arrow, can't use a canvas, or develop a package that provides graphics utilities − stay here 😉.
61 |
62 | Also note, that the tip sizing and configuration mechanism works quite differently.
63 |
64 |
65 | ## Available marks
66 |
67 | Tiptoe comes with a collection of predefined marks, listed below. In [Defining custom marks](#defining-custom-marks), you can learn how to define your own marks.
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ## Mark sizing and styling
78 |
79 | All predefined marks can be configured through `.with()` calls. Some options (like the `inset` of a stealth arrow) are mark-specific.
80 |
81 | *Be Typst!* If you use a configured mark more than once, define an alias for it. Typst makes it incredibly easy to define variables:
82 | ```typ
83 | #let my-mark = stealth.with(rev: true, inset: 20%)
84 | #line(tip: my-mark)
85 | ```
86 |
87 | ### Sizing
88 |
89 | The size of most arrows is primarily defined by their length and secondarily by their width (exceptions are `bar`, `barb`, `hooks`, and `tikz` which only have a width). Both width and length can be set using
90 | - a `length` value, such as `10pt`,
91 | - or a ratio which is measured relative to the thickness of the line (e.g., `500%` corresponds to 5 times the line thickness),
92 | - or a combination of both, e.g., `3pt + 450%` (this is by the way the default for the `stealth` mark).
93 | This makes it possible to fine-tune the sizing behavior of a mark. By default (`width: auto`) and for most marks the width is defined in terms of the length via some predefined ratio.
94 |
95 |
96 | With the predefined marks, the length/width encompasses the *full* length/width of the mark, independent of the stroke thickness that is used. This is demonstrated below, where the fill is removed through `stealth.with(fill: none)`.
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | ### Colors!
107 |
108 | Usually, a mark inherits color and stroke thickness (but not other stroke attributes like `join` or `cap`) from the line that the mark is attached to. In order to override the color, the thickness or both, all marks feature a `stroke` parameter. Additionally, all solid marks feature a `fill` parameter that defaults (when set to `auto`) to the stroke color.
109 |
110 | Below are some examples for mark styling.
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ## More styling
121 |
122 | Apart from `length`, `width`, `fill`, and `stroke`, many marks possess additional styling parameters, such as
123 | - the `inset` for the arrows `stealth` and `round`,
124 | - an arc angle parameter for the marks `barb` and `hooks`,
125 |
126 | - and of course the `rev` parameter that allows for reversing all marks where this makes sense.
127 |
128 | The figure below shows the additional parameters that each mark supports. The red line indicates how much the underlying path is shortened (see [Path shortening](#path-shortening)).
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | ## Mark alignment
141 |
142 | Most marks are aligned such that they point _right onto the end_ (or start) of the path. However, for some marks it is more desirable to have them _centered_ at the end (or start). This is for example the case for the `square` and `circle` marker. All markers that are by default centered on the path end have an `align` parameter that can be set either to `center` or `end` to configure this behavior.
143 |
144 | The mark alignment for the built-in marks is summarized in the table below.
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | ## Path shortening
157 |
158 | In order to make room for the mark, the path needs to be shortened by some amount. This is trivial for straight segments but not for curved paths.
159 |
160 | The issue is demonstrated in the figure below. In all cases, the arrow is tangent to the curve at its end. In the left panel of the figure, the curve does not enter the arrow in the middle but rather from the side which definitely wouldn't make you look like a good designer when handing in professional work ;)
161 |
162 | To compensate this issue, the path is _transformed_, i.e., shortened by some amount to make it seem nicer. This happens at the cost of the path being not quite the same as before; but it yields a much prettier result.
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | Not always is it desirable to shorten the path all the way (hey, a little asymmetry simply belongs in life). For this purpose, `curve`, `path`, and `arc` have a parameter `shorten` which takes ratios between `0%` and `100%` (default).
176 |
177 |
178 |
179 | ## Combining marks
180 |
181 | The function `combine()` makes it easy to combine multiple marks into a single new one and acts as a mark itself. It accepts any number of marks and can even process combined marks recursively.
182 |
183 | ```typ
184 | #line(tip: combine(bar, stealth))
185 | ```
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | The combined marks are automatically lined up one after the other; always the next one where the previous one ended. In order to introduce or increase space between two marks, you may use `length` values (like `10pt`) or even better `ratio` values (which are measured relative to the line thickness of the curve). Negative values are also allowed!
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | By default, the path is shortened until the last mark. This behaviour can be overriden by adding an `end` element somewhere in the mark list. The position of the `end` element between the marks defines where the line or path should end.
209 |
210 |
211 | ## Defining custom marks
212 |
213 | A mark is just a function that accepts a named `line` argument and that returns a dictionary `(mark: .., end: ..)` where `mark` holds the rendered mark and `end` is a length that specifies the amount by which the line or path needs to be shortened.
214 |
215 | As an example, let us look at a simplified definition of the `bar` mark.
216 | ```typ
217 | #import tiptoe.utility
218 |
219 | #let bar(
220 | // mandatory, will be set by line(), path() and arc()
221 | line: stroke()
222 | // optional configuration parameters
223 | width: 2.4pt + 360%,
224 | stroke: auto,
225 | ) = {
226 | stroke = utility.process-stroke(line, stroke)
227 | let (width,) = utility.process-dims(
228 | line, width: width
229 | )
230 |
231 | (
232 | mark: place(path(
233 | (0pt, -width / 2),
234 | (0pt, width / 2),
235 | stroke: stroke
236 | )),
237 | end: 0pt
238 | )
239 | }
240 | ```
241 |
242 | Let us first explain the difference between the parameters `line` and `stroke`:
243 | - `line` is sort of a _private_ parameter. It is set by the functions `tiptoe.path()`, `tiptoe.line()` and `tiptoe.arc()`, when the mark is realized and contains the stroke used for drawing the path/line/arc. We use it often to make the mark inherit color and thickness.
244 | - `stroke` is totally optional for your mark, but all built-in marks have it. It allows the user of the mark to customize its stroke, overriding the stroke inherited from `line`.
245 |
246 | You can add an arbitrary number of other configuration parameters to your mark.
247 |
248 | The module `tiptoe.utility` provides two very useful helpers. The function `process-stroke()` takes the `line` and `stroke` parameter and returns a merged stroke to be used for drawing the mark. The merging obeys the following rules
249 | - Only `thickness` and `paint` are inherited from `line`.
250 | - Thickness and paint are both (independently) only inherited if they are set to `auto` in `stroke`. This makes it for example possible to configure only the color of a mark without changing the thickness.
251 | - If `stroke` is `none`, nothing is inherited and `none` is returned.
252 | - The merged stroke is guaranteed to have a `thickness` that is not `auto`.
253 |
254 |
255 |
256 |
257 |
258 | The other helper function `process-dims()` is useful for processing length and/or width of the mark which may be (see the [section about sizing](#sizing)) a `ratio` (in terms of the line thickness), a `length`, or a combination thereof. It takes the `line` and optional `width` and `length` parameters and returns a dictionary with the evaluated `width` and `length` (if they were given). In addition, it can process a width set to `auto` in terms of the length with a coefficient that can be specified with the parameter `default-ratio`. As an example, the `stealth` mark has an automatic width by default which is then 80% of the length.
259 |
260 | Hints for rendering the mark:
261 | - You only need to take care of the case where the mark is used as a `tip`. The `toe` case is handled automatically.
262 | - The path goes from left to right and ends at `(0pt, 0pt)`. The stealth arrow, e.g., points exactly to this coordinate.
263 | - The rotation of the mark is fully handled by tiptoe.
264 |
265 | ## Arc
266 |
267 | Many have noted that (as of now) Typst does not feature a function to draw arcs. This is sometimes unfortunate since circular arcs are not at all trivial to approximate with Bézier curves.
268 |
269 | Until a built-in arc function makes it into the core of Typst, enjoy this one:
270 |
271 | ```typ
272 | #let arc(
273 | origin: (0pt, 0pt), // Origin coordinates
274 | angle: 0deg, // Start angle
275 | arc: 45deg, // Arc angle
276 | radius: 1cm, // The radius of the full circle
277 | width: auto, // The width of the full ellipse
278 | height: auto, // The height of the full ellipse
279 | closed: false, // false, "segment" or "sector"
280 | tip: none, // Mark placed at the start
281 | toe: none, // Mark placed at the toe
282 | shorten: 100%, // Path shortening
283 | stroke: auto, // Folds with `std.curve.stroke`
284 | fill: auto, // Folds with `std.curve.fill`
285 | )
286 | ```
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 | ## Ring
301 |
302 | On top, Tiptoe provides a ring function.
303 |
304 | ```typ
305 | #let ring(
306 | origin: (0pt, 0pt), // Origin coordinates
307 | angle: 0deg, // Start angle
308 | arc: 45deg, // Arc angle
309 | inner: 0.5cm, // The inner radius of the full circle
310 | outer: 1cm, // The outer radius of the full circle
311 | stroke: auto, // Folds with `std.curve.stroke`
312 | fill: auto, // Folds with `std.curve.fill`
313 | )
314 | ```
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 | ## Differences between `std.curve` and `tiptoe.curve`
328 |
329 |
330 | While the built-in [`std.curve`][typst-curve] function returns a block-level element with a size that measures from `(0pt, 0pt)` to the largest (positive) coordinate, the corresponding Tiptoe function returns placed content (with zero-width and -height).
331 |
332 | The reasons are
333 | - It is hard to measure the bounding box properly including the marks.
334 | - The behavior of the built-in functions is not particularly useful since they measure only in the positive direction. I suspect that most packages using the drawing primitives wrap them with `place()` anyway.
335 |
336 | Currently, there are some additional limitations that might be lifted in a future release.
337 | - Values of type `relative` or `ratio` are not supported as coordinates in curve elements (to be precise, the `ratio` part is dismissed as of now).
338 | - When using coordinates with `relative: true`, some edge cases might not work as expected.
339 | - Tips are not supported on a `curve.close` element.
340 | - Toes don't work when the curve starts with multiple subsequent `curve.move` elements. You should just merge them into one.
341 |
342 |
343 | ## Changelog
344 |
345 | ### 0.4.0
346 | - Added a `ring()` function for drawing ring arcs.
347 | - Fixed positioning of marks when `set rotate(reflow: true)` has been set (thanks to @Andrew15-5).
348 | - The functions `line`, `curve`, `arc`, and `ring` now respond to set rules on `std.line` and `std.curve`.
349 |
350 | ### 0.3.2
351 | - Added support for `ratio` and `relative` values in the start and end coordinates.
352 |
353 | ### 0.3.1
354 | - Fixed tiptoe for right-to-left text flow.
355 | - Internally changed the implementation of `arc` to use `curve` instead of `path`. Consequently, the default of `arc.shorten` is now `100%` which looks better with the new implementation.
356 |
357 | ### 0.3.0
358 | - Added `curve` function in analogy to `std.curve`.
359 |
360 | ### 0.2.0
361 | - Added support for `relative` inputs for `line`.
362 |
363 | ### 0.1.0
364 | Initial release
365 |
366 |
367 | [fletcher]: https://github.com/Jollywatt/typst-fletcher
368 | [cetz]: https://github.com/cetz-package/cetz
369 | [jollywatt]: https://github.com/Jollywatt/
370 | [typst-curve]: https://typst.app/docs/reference/visualize/curve/
371 | [typst]: https://typst.app
372 |
--------------------------------------------------------------------------------
/docs/figures/out/sizing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
--------------------------------------------------------------------------------
/docs/figures/out/sizing-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
--------------------------------------------------------------------------------