How can I add a glow/embossing to text?

Hello,

I’m trying to recreate this effect in Typst. As you can see, there’s a “glow” around this. Maybe this is officially called differently, but I couldn’t find it. I’ve searched for glow, emboss, blur, gradient etc.

I’ve then also started attempts to create it myself, but it seems to me that this is currently not possible at all with Typst? I mean, stroke seems to be the thing to use, but there’s no “outward” mode for gradients that I could find. Yes, radial, but that’s from a single focus point, which text is not. I then also considered to create a “ghost” character at the background, but that so didn’t succeed and failed hard, that I didn’t even attempt to continue with it.

What I noticed is that whet you use stroke + gradient, it seems to apply over the “entire” text, and I haven’t been able to properly place it. It looks like across the entire container, not on a per letter basis.

Here are some examples of what I’ve tried

#text(size: 20pt, stroke: 5pt + gradient.radial(white, black, black.transparentize(100%), radius: 50%, relative: "self"))[Hello there!!!!] \
#text(size: 20pt, stroke: 5pt + gradient.radial(white, black, black.transparentize(100%), radius: 50%, relative: "parent"))[Hello there!!!!] \
#text(size: 20pt, stroke: 5pt + gradient.linear(white, black, black.transparentize(100%), relative: "self"))[Hello there!!!!] \
#text(size: 20pt, stroke: 5pt + gradient.linear(white, black, black.transparentize(100%), relative: "parent"))[Hello there!!!!] \

Am I really overlooking the obvious here on a Sunday? :grimacing:

Edit: Note that the behavior is wildly different when you don’t include the break (\) at the end. What I shared is just a title/header, but in the ideal world I can also apply a smaller glow on the regular text.

#text(size: 20pt, stroke: 5pt + gradient.radial(white, black, black.transparentize(100%), radius: 50%, relative: "self"))[Hello there!!!!]
#text(size: 20pt, stroke: 5pt + gradient.radial(white, black, black.transparentize(100%), radius: 50%, relative: "parent"))[Hello there!!!!]
#text(size: 20pt, stroke: 5pt + gradient.linear(white, black, black.transparentize(100%), relative: "self"))[Hello there!!!!]
#text(size: 20pt, stroke: 5pt + gradient.linear(white, black, black.transparentize(100%), relative: "parent"))[Hello there!!!!]

image

There are two ways to do so:

  1. Place a text with stroke, then put another stroke-less text on top of it.
  2. Create an SVG containing the text and apply SVG filter effect <feDropShadow>.

Full code can be found in 如何给文本增加阴影 | Typst 中文社区导航. Unfortunately that page is not available in English, but the code should mostly explains itself.

Hello,

Thanks again for helping me out so fast, but I think both are not really what I was aiming for, or at least with some caveats. I was quite busy so far this week, so I sadly didn’t have time yet to check out your suggestions, sorry for that…

The two texts doesn’t solve the issue of the shadow part let’s say. I could already make the stroke look OK, but not like a shadow.

The SVG is a nice hack perhaps, but it doesn’t work like it does in Typst. I mean, the font is rendered quite a bit different, and the custom fonts I have in a dedicated directory that Typst picks up via configuration, are not recognized.

I guess for now, the only ugly solution that works somewhat reliably, is by adding not two pieces of text, but a dozen… While this isn’t perfect either, because it doesn’t capture things like alignment properly, it’s sufficient for titles.

#let drop-shadow(color, layers, gap, content) = context {
  box[
  #for layer in range(layers, 1, step: -1, inclusive: true) {
    place[#text(fill: black.transparentize(100%), stroke: layer * gap + color, content)]
  }
  #content
  ]
}

#show heading: it => [#text(font: "Baguet Script", fill: white, size: 36pt)[#block[#drop-shadow(black.transparentize(99.8%), 50, 0.2pt, it.body)]]]

= Hello


Now, if I could apply this same technique on regular text that is affected with justify, it would be great. But I think due to the way place works, that won’t ever really happen easily or with a lot of dirty stuff…

Stacking content on top of each other can be achieved with place but as you point out, it’s limited.

Here’s another way to stack things which keeps the location of whatever is being stacked:

#let z-stack(..contents) = (
  context {
    let max-width = calc.max(
      ..contents.pos().map(content => {
        measure(content).width
      }),
    )

    box(
      grid(
        align: center + horizon,
        columns: contents.pos().len() * (max-width,),
        column-gutter: -max-width,
        rows: 1,
        ..contents
      ),
    )
  }
)

#let shadow(color, num-layers, gap, content) = {
  let layers = range(num-layers, 1, step: -1, inclusive: true).map(l => text(fill: black.transparentize(100%), stroke: l * gap + color, content))
  z-stack(..layers.rev(), content)
}

#show regex(`[^\s]+`.text): shadow.with(black.transparentize(99.8%), 50, 0.01em)

= Heading 1
#lorem(5)

$ y = m * x + b $

`let x = 1`

This is a combination of your idea, and a variation of the solution to this post:

Hello,

Thanks for some insightful “hacks” one can apply. I never would have thought that with “inline mode” you could have negative things like gutters and h. I started testing, debugging and essentially writing a transcript here, so it became quite lengthy. I came to the conclusion that probably what I want is just not nicely possible with Typst, and maybe also outside of its focus area at this moment. I’ll solve my WCAG chalenges in a different way, e.g., a filled box on the yellow background to have darker text on it etc.


I’ve tested your solution with z-stack, and for some reason the performance with it is really bad. But besides that, it also doesn’t render properly: it renders on a single line. Try it out with a lorem(300) and you’ll see there’s no newlines appearing with this construction. That’s quite obvious given the way the column width is calculated. Changing it to (100%, ) does give a better effect, but that won’t work for some reason, as it shows no text for me. Swapping the order around doesn’t do anything either: I’m not sure about this. With 100% only, it will show the content, but with a multiplied height by the amount of layers.

But then I looked at the linked suggestion: What is the opposite of text stroke? Here I found out that when I copy the example, it works just fine. At the moment I replace the content with #lorem(50) no text is shown no more for some – to me that is, and sorry for my language – bullshit reason. I mean, check this out

#let opposite-stroke(stroke: black + 1pt, body) = context {
  box(text(stroke: stroke, fill: rgb("#00000000"), body))
  let m = measure(body)
  h(- m.width)
  box(body)
}

#let background-red = image("../../_images/background-red.jpg")

#let content-red = [
  = Hello \
  #lorem(50)
  \
  -----
  \
  
  #opposite-stroke(stroke: blue+1pt)[#lorem(50)
    foo 
    #set text(fill: green)
    bar\
    #set text(fill: yellow)
    baz
  ]
]

#set page(
  background: box(width: 100%, height: 100%)[
    #rect(width: 100%, fill: tiling(background-red))[#content-red]
  ]
)

And when you remove the #lorem(50) inside the #opposite-stroke it becomes this. As they say in Dutch: “Just shoot me leak” (schiet mij maar lek).

Actually, after some more testing, I’m even more baffled… When you do something like this, it also gives the first effect! Even wrapped in [] it has the same effect. I’m totally incapable of understanding the underlying reasoning here.

#let some-text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porta in neque eget viverra. Maecenas dictum dolor eget ullamcorper sollicitudin. Cras fringilla ligula vitae pulvinar aliquam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Integer egestas consectetur feugiat. Vivamus ut efficitur dolor. Donec aliquet imperdiet feugiat."

#let content-red = [
  = Hello \
  #some-text
  \
  -----
  \
  
  #opposite-stroke(stroke: blue+1pt)[
    #some-text
  ]
]

I’ve done a bit more analysis here. Because of the negative h the content is moved up. But for some reason, it’s then no longer visible, like as if it’s hidden behind everything for some reason. When only showing the first box, it’s visible. As soon as the negative h comes in, everything is hidden from view. Also, giving it some thought: how would horizontal spacing work anyway when we’d need vertical spacing (reduction)?

I don’t like to leave threads hanging, but I also feel that there’s no satisfying answer that works properly the way one would expect. The SVG trick works well, if you don’t have paragraphs for example.

You’re right that the performance is really bad. I guess it’s because it’s laying out all parts of the shadow before stacking them on top of each other.

I’m not seeing a problem with lorem(300) only appearing on a single line:

But I guess the point is moot because of the poor performance. As you said, it’s possible that Typst doesn’t have a good (built-in) option for applying shadows like this. I haven’t looked into and I doubt it’s applicable but maybe the shadow-ing method from the shadowed package could be applied to text.

I tried to get a shadow affect with a gradient stroke and/or fill but didn’t get anything that looked correct. If this was an option the performance would no longer be a bottleneck.