cetz-package / cetz

CeTZ: ein Typst Zeichenpaket - A library for drawing stuff with Typst.
https://cetz-package.github.io
GNU Lesser General Public License v3.0
731 stars 34 forks source link

Text along/on a path #395

Open Andrew15-5 opened 6 months ago

Andrew15-5 commented 6 months ago

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/

fenjalien commented 6 months ago

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.

Narcha commented 3 days ago

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.