├── .vscode ├── settings.json ├── ltex.disabledRules.en-US.txt └── ltex.dictionary.en-US.txt ├── tests ├── .gitignore ├── .ignore ├── arc │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── test.typ ├── marks │ ├── ref │ │ └── 1.png │ └── test.typ ├── ring │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── test.typ ├── combine │ ├── ref │ │ └── 1.png │ └── test.typ ├── styling │ ├── ref │ │ └── 1.png │ └── test.typ ├── curve-tips │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── test.typ ├── curve-toes │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── test.typ ├── bezier-marks │ ├── ref │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── test.typ ├── ratio-length │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── test.typ └── set-rule-integration │ ├── arc │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── test.typ │ ├── curve │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── test.typ │ ├── line │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── test.typ │ └── ring │ ├── ref │ ├── 1.png │ ├── 2.png │ └── 3.png │ └── test.typ ├── fonts └── Liberation Sans │ └── LiberationSans-Regular.ttf ├── docs └── figures │ ├── combine.typ │ ├── generate-images.sh │ ├── out │ ├── template.svg │ ├── template-dark.svg │ ├── combine.svg │ ├── combine-dark.svg │ ├── intro-example.svg │ ├── intro-example-dark.svg │ ├── sizing.svg │ └── sizing-dark.svg │ ├── intro-example.typ │ ├── ring.typ │ ├── sizing.typ │ ├── template.typ │ ├── combined.typ │ ├── arc.typ │ ├── styling.typ │ ├── marks.typ │ ├── shortening.typ │ ├── alignment.typ │ ├── logo.typ │ └── mark-parameters.typ ├── src ├── tiptoe.typ ├── assert.typ ├── ring.typ ├── arc.typ ├── utility.typ ├── path-to-curve.typ ├── line.typ ├── arc-impl.typ ├── path.typ ├── marks.typ └── curve.typ ├── typst.toml ├── .github └── workflows │ └── run_tests.yml ├── LICENSE.txt └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /.vscode/ltex.disabledRules.en-US.txt: -------------------------------------------------------------------------------- 1 | PASSIVE_VOICE 2 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /.vscode/ltex.dictionary.en-US.txt: -------------------------------------------------------------------------------- 1 | Typst 2 | Jollywatt 3 | CeTZ 4 | btw 5 | -------------------------------------------------------------------------------- /tests/arc/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/1.png -------------------------------------------------------------------------------- /tests/arc/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/2.png -------------------------------------------------------------------------------- /tests/arc/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/3.png -------------------------------------------------------------------------------- /tests/arc/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/4.png -------------------------------------------------------------------------------- /tests/arc/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/5.png -------------------------------------------------------------------------------- /tests/arc/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/arc/ref/6.png -------------------------------------------------------------------------------- /tests/marks/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/marks/ref/1.png -------------------------------------------------------------------------------- /tests/ring/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ring/ref/1.png -------------------------------------------------------------------------------- /tests/ring/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ring/ref/2.png -------------------------------------------------------------------------------- /tests/ring/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ring/ref/3.png -------------------------------------------------------------------------------- /tests/ring/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ring/ref/4.png -------------------------------------------------------------------------------- /tests/combine/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/combine/ref/1.png -------------------------------------------------------------------------------- /tests/styling/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/styling/ref/1.png -------------------------------------------------------------------------------- /tests/curve-tips/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-tips/ref/1.png -------------------------------------------------------------------------------- /tests/curve-tips/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-tips/ref/2.png -------------------------------------------------------------------------------- /tests/curve-tips/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-tips/ref/3.png -------------------------------------------------------------------------------- /tests/curve-tips/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-tips/ref/4.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/1.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/2.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/3.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/4.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/5.png -------------------------------------------------------------------------------- /tests/curve-toes/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/curve-toes/ref/6.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/1.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/10.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/11.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/12.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/13.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/14.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/2.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/3.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/4.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/5.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/6.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/7.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/8.png -------------------------------------------------------------------------------- /tests/bezier-marks/ref/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/bezier-marks/ref/9.png -------------------------------------------------------------------------------- /tests/ratio-length/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ratio-length/ref/1.png -------------------------------------------------------------------------------- /tests/ratio-length/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ratio-length/ref/2.png -------------------------------------------------------------------------------- /tests/ratio-length/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ratio-length/ref/3.png -------------------------------------------------------------------------------- /tests/ratio-length/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/ratio-length/ref/4.png -------------------------------------------------------------------------------- /tests/set-rule-integration/arc/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/arc/ref/1.png -------------------------------------------------------------------------------- /tests/set-rule-integration/arc/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/arc/ref/2.png -------------------------------------------------------------------------------- /tests/set-rule-integration/arc/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/arc/ref/3.png -------------------------------------------------------------------------------- /tests/set-rule-integration/curve/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/curve/ref/1.png -------------------------------------------------------------------------------- /tests/set-rule-integration/curve/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/curve/ref/2.png -------------------------------------------------------------------------------- /tests/set-rule-integration/curve/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/curve/ref/3.png -------------------------------------------------------------------------------- /tests/set-rule-integration/line/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/line/ref/1.png -------------------------------------------------------------------------------- /tests/set-rule-integration/line/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/line/ref/2.png -------------------------------------------------------------------------------- /tests/set-rule-integration/line/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/line/ref/3.png -------------------------------------------------------------------------------- /tests/set-rule-integration/ring/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/ring/ref/1.png -------------------------------------------------------------------------------- /tests/set-rule-integration/ring/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/ring/ref/2.png -------------------------------------------------------------------------------- /tests/set-rule-integration/ring/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/tests/set-rule-integration/ring/ref/3.png -------------------------------------------------------------------------------- /fonts/Liberation Sans/LiberationSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tiptoe/HEAD/fonts/Liberation Sans/LiberationSans-Regular.ttf -------------------------------------------------------------------------------- /docs/figures/combine.typ: -------------------------------------------------------------------------------- 1 | #import "template.typ": * 2 | #show: template 3 | 4 | #let content = figure(line(tip: combine(bar, stealth))) 5 | 6 | #content 7 | -------------------------------------------------------------------------------- /tests/marks/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tiptoe.typ" as tiptoe: * 2 | #set page(width: auto, height: auto, margin: 10pt) 3 | 4 | #show text: none 5 | #import "/docs/figures/marks.typ" 6 | #marks.content 7 | -------------------------------------------------------------------------------- /tests/styling/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tiptoe.typ" as tiptoe: * 2 | #set page(width: auto, height: auto, margin: 10pt) 3 | 4 | #show text: none 5 | #import "/docs/figures/styling.typ" 6 | #styling.content 7 | -------------------------------------------------------------------------------- /src/tiptoe.typ: -------------------------------------------------------------------------------- 1 | #import "line.typ": line 2 | #import "path.typ": path 3 | #import "curve.typ": curve 4 | #import "arc.typ": arc 5 | #import "ring.typ": ring 6 | #import "utility.typ" 7 | #import "marks.typ": * 8 | -------------------------------------------------------------------------------- /tests/combine/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tiptoe.typ" as tiptoe: * 2 | #set page(width: auto, height: auto, margin: 10pt) 3 | 4 | #show text: none 5 | #import "/docs/figures/combined.typ" 6 | #combined.content 7 | -------------------------------------------------------------------------------- /docs/figures/generate-images.sh: -------------------------------------------------------------------------------- 1 | path=docs/figures 2 | for entry in "$path"/*.typ 3 | do 4 | name=${entry#*figures/} 5 | name=${name%.*} 6 | out="$path/out/$name" 7 | echo "$name" 8 | typst c "$entry" "$out.svg" --format svg --root . --font-path ./fonts 9 | typst c "$entry" "$out-dark.svg" --format svg --root . --input dark=true --font-path ./fonts 10 | done 11 | -------------------------------------------------------------------------------- /docs/figures/out/template.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/figures/out/template-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tiptoe" 3 | version = "0.4.0" 4 | entrypoint = "src/tiptoe.typ" 5 | authors = ["Mc-Zen "] 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 | Logo 6 | 7 |

8 | 9 | _Arrows for [Typst][typst] paths and other stories._ 10 | 11 | [![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FMc-Zen%2Ftiptoe%2Fv0.4.0%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://typst.app/universe/package/tiptoe) 12 | [![Test Status](https://github.com/Mc-Zen/tiptoe/actions/workflows/run_tests.yml/badge.svg)](https://github.com/Mc-Zen/tiptoe/actions/workflows/run_tests.yml) 13 | [![MIT License](https://img.shields.io/badge/license-MIT-blue)](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 | Basic introductory example 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 | Available predefined marks 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 | Arrow sizing 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 | Examples for styling marks 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 | Parameters of the predefined marks 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 | Alignment of marks 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 | Path shortening 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 | How to combine marks 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 | Examples for combined arrows and marks 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 | Usage of the arc() primitive 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 | Usage of the arc() primitive 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 | --------------------------------------------------------------------------------