How to get a white border around text, especially math formulas?

Hello,

my original issue is, that I am drawing a figure in cetz where text is above a line and I would like to improve the visual distinction between text and line.

Example:
image

I didn’t really find an out-of-the box solution.

Two things I considered:

  1. Just putting a white box on a layer between the text and line
    (I adjusted the box size to not “loose” too much of the line)
    image

    Code
    #import "@preview/cetz:0.3.4"
    
    #cetz.canvas({
      import cetz.draw: *
    
      on-layer(
        -2,
        line((0, -1), (0,1), stroke: blue)
      )
      content(
          (0,0),
          text()[$P_1$],
          name: "text",
      )
      
      on-layer(
        -1,
        rect(
          "text.north-east",
          (rel: (-1em, -0.7em)),
          stroke: white,
          fill: white,
        ),
      )  
    
      on-layer(
        -2,
        line((1, -1), (1,1), stroke: blue)
      )
      content(
          (1,0),
          text(fill: gray)[SPI],
          name: "text",
      )
      
      on-layer(
        -1,
        rect(
          "text.north-east",
          (rel: (-1em, -0.7em)),
          stroke: white,
          fill: white,
        ),
      ) 
    })
    
  2. Adding the text in slightly larger size and white behind the original text.
    (As you can see on the following figure, this doesn’t work on math text. This is a typst “problem” independently of cetz, as shown further below)

    image

    Code
    #import "@preview/cetz:0.3.4"
    
    #cetz.canvas({
      import cetz.draw: *
    
      on-layer(
        -2,
        line((0, -1), (0,1), stroke: blue)
      )
      content(
          (0,0),
          text()[$P_1$],
          name: "text",
      )
      
      on-layer(
        -1,
        content(
          (0,0),
          text(fill: white, stroke: white+1pt)[$P_1$],
        )
      )  
    
      on-layer(
        -2,
        line((1, -1), (1,1), stroke: blue)
      )
      content(
          (1,0),
          text(fill: gray)[SPI],
          name: "text",
      )
      
      on-layer(
        -1,
        content(
          (1,0),
          text(fill: white, stroke: white + 1pt)[SPI],
        )
      ) 
    })
    

I would prefer the second variant, as it looks better (in my personal opinion) in scenarios where the line is thicker.

image

Code
#import "@preview/cetz:0.3.4"

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

  on-layer(
    -2,
    line((1, -1), (1,1), stroke: blue + 5pt)
  )
  content(
      (1,0),
      text(fill: gray)[SPI],
      name: "text",
  )
  
  on-layer(
    -1,
    content(
      (1,0),
      text(fill: white, stroke: white + 1pt)[SPI],
    )
  ) 
})

Example of the problem in typst (without cetz stuff):

#text(fill: black, stroke: red + 0.3pt)[SPI]

#text(fill: black, stroke: red + 0.3pt)[$P_1$]

My two questions:

  1. Is there a possibility to get a stroke around text in math mode?
  2. Do you have any other solutions/ideas to get this working?

EDIT:
In latex there is the contour package

The used technique is quite simple. By default, in a circle around the original text position the same text is printed evenly distributed 16, 32, or a a given number times.

I think I could also implement the “copy” variant in typst, but I’m not too happy with it :sweat_smile:

Hi. The stroke not working in math is an issue.

As to the solution, I’ve looked at your code and this is what I was left with:

#import "@preview/cetz:0.3.4"

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

  let bg = box.with(fill: white, inset: 0.1em)

  line((0, -1), (0, 1), stroke: blue)
  content((0, 0), bg[$P_1$])

  line((1, -1), (1, 1), stroke: blue)
  content((1, 0), bg(text(fill: gray)[SPI]))

  set-origin((0, -2.1))

  let fake-stroke-content(pos, body, ..args) = {
    on-layer(1, content(pos, text(1.1em, white, {
      show text: text.with(white)
      body
    }), ..args))
    on-layer(2, content(pos, body, ..args))
  }

  line((0, -1), (0, 1), stroke: blue)
  fake-stroke-content((0, 0))[$P_1$]

  line((1, -1), (1, 1), stroke: blue)
  fake-stroke-content((1, 0), text(fill: gray)[SPI])
})
output

Thanks for the issue. Didn’t see it resp. was unsure if this would be an issue or something that doesn’t work by design. :sweat_smile:

Good idea with the larger letters but this also works only partially as the larger letters do not grow “equally” in all directions…
Therefore there is not really a “uniform” white-space around the letter. Mainly it helps on the top and lower side.