cetz icon indicating copy to clipboard operation
cetz copied to clipboard

Text along/on a path

Open Andrew15-5 opened this issue 2 years ago • 7 comments

The killer feature of tikz is being able to shape text to the path, so if the path is a wave, then the text will also be wavy. With Typst 0.10.0 this text also should be able to be filled with colorful gradient to achieve an absolute masterpiece!

This will greatly increase creativity/beautifulness of some custom papers like brochures, booklets, invitation/congratulation cards etc.

https://tikz.dev/tikz-decorations https://tex.stackexchange.com/a/640598 https://tex.stackexchange.com/a/22316 https://latexdraw.com/how-to-write-a-text-along-path-using-tikz-speedometer-case/

Andrew15-5 avatar Dec 11 '23 07:12 Andrew15-5

This isn't easily possible without extra support from the Typst compiler. You could try and break up content but it would be tricky to rotate each piece correctly while keeping styling correct and would break very easily.

fenjalien avatar Dec 11 '23 15:12 fenjalien

While it's not possible to split content at the moment, it is possible to split strings. Here's a minimal working example:

#import "@preview/cetz:0.2.2"

#context cetz.canvas({
  import cetz.draw: *

  let windows(arr, size) = {
    array.range(arr.len() - size + 1).map(i => {
      arr.slice(i, count: size)
    })
  }

  let text-along-path(text, path, start-percentage: 0%, end-percentage: 100%) = {
    let letters = text.clusters().map(cluster => [#cluster])
    let widths = letters.map(letter => {
      let width = measure(letter).width
      if width == 0pt {
        // Measuring a single space returns a width of 0pt.
        // This is a hack to get the width of a space.
        measure([X X]).width - measure([XX]).width
      } else {
        width
      }
    })
    let total_width = widths.sum()
    let total_percentage = end-percentage - start-percentage

    let distance_covered = 0
    let anchors = ()
    let anchors = for w in widths {
      let relative_width = w / total_width
      let percentage = start-percentage + total_percentage * distance_covered
      distance_covered += relative_width

      (percentage,)
    }
    anchors.push(end-percentage)

    for ((percentage_1, percentage_2), letter) in windows(anchors, 2).zip(letters) {
      let midpoint = (percentage_1 + percentage_2) / 2

      get-ctx(ctx => {
        let (ctx, percentage_1, percentage_2) = cetz.coordinate.resolve(
          ctx,
          (name: path, anchor: percentage_1),
          (name: path, anchor: percentage_2),
        )

        let angle = cetz.vector.angle2(percentage_1, percentage_2)
        
        content(
          (name: path, anchor: midpoint),
          anchor: "south",
          angle: angle,
          letter
        )
      })
    }
  }

  bezier(name: "line", (0, 0), (2, 0), (1, 1), stroke: 0.2mm + blue)

  let text = "Hello, World!"
  text-along-path(text, "line", start-percentage: 10%, end-percentage: 90%)
})

text-on-line

It should also be possible to determine start and end percentage based on the measured total width and the total length of the path, but I haven't implemented this yet.

rmburg avatar Jul 01 '24 22:07 rmburg

Nice solution, @rmburg! Thanks 🙂

Some things seem to have changed since cetz:0.2.2. If you change the first line to cetz:0.3.4 so it runs on Typst 0.13, the height of the comma is not calculated from the proper baseline (the bottom of the thick part) but from the bottom part of the whole glyph (the end of the tail). While this is relatively minor with a comma, it is catastrophic with letters descender, like p and g.

Change anchor: "south", to anchor: "base", to solve this.

rwmpelstilzchen avatar Mar 22 '25 10:03 rwmpelstilzchen

How do you change the text attributes of the letters (font, size, fill, etc…)? Changing letter within the context command to text(fill: red, letter), for example, doesn’t work. I must have missed something trivial…

rwmpelstilzchen avatar Mar 22 '25 10:03 rwmpelstilzchen

in this case, you have to use std.text, because one argument of the function is named text. Alternatively, rename the argument.

rmburg avatar Mar 22 '25 10:03 rmburg

Oh my… I knew I must have missed something 🤦‍♀️ Thanks! 🙂

rwmpelstilzchen avatar Mar 23 '25 16:03 rwmpelstilzchen

BTW there’s now a package for positioning text around an arc or circle, called Curvly.

rwmpelstilzchen avatar Apr 02 '25 22:04 rwmpelstilzchen