How to draw a decorative frame (rect with concave corners) around text?

I’m trying to replicate in Typst an existing book design (created in InDesign) that has decorative frames around certain text (quotes), with concave rounded corners and double-stroked borders (bold outer frame, and with a small padding, a less bold inner frame). It looks as in the image below (text blurred out to redact it):

How can this be done in Typst? The frames are always the full width of the text block, but height should resize to that of the text contents (as #rect does by default).

Hello.

IMO, if you are trying to do something, then it means you have some progress or something that you’ve tried, therefore this should be present in the post. So far you’ve only described and showed how it should look like and how can this be done. This sends conflicting signals, as if you are trying, but actually you didn’t try anything, which is not great. Perhaps a wrong verb was chosen.

Here is something that looks like what you want, not sure about corner specifics:

#let frame-shape(width: 10cm, height: 5cm, radius: 15pt, a: 0pt, ..args) = {
  set curve.quad(relative: true)
  set curve.line(relative: true)
  curve(
    ..args,
    curve.move((0pt, radius)),
    curve.line((0pt, height - radius * 2)), // Left
    curve.quad((-a + radius, a), (radius, radius)), // Bottom-left
    curve.line((width - radius * 2, 0pt)), // Bottom
    curve.quad((a, a - radius), (radius, 0pt - radius)), // Bottom-right
    curve.line((0pt, -height + radius * 2)), // Right
    curve.quad((a - radius, -a), (-radius, -radius)), // Top-right
    curve.line((-width + radius * 2, 0pt)), // Top edge
    curve.quad((-a, -a + radius), (-radius, radius)), // Top-left
  )
}

#let frame(
  width: 10cm,
  height: auto,
  radius: 15pt,
  inner: 2pt,
  stroke: 1pt,
  inner-stroke: 0.5pt,
  body,
) = {
  layout(size => {
    let body-block = block.with(width: width, inset: radius + inner)
    let (height, body) = if height != auto {
      (height, body-block(height: height, body))
    } else {
      let body = body-block(body)
      (measure(body, ..size).height, body)
    }
    place(frame-shape(
      width: width,
      height: height,
      radius: radius,
      stroke: stroke,
    ))
    place(dx: inner, dy: inner, frame-shape(
      width: width - inner * 2,
      height: height - inner * 2,
      radius: radius,
      stroke: inner-stroke,
      a: inner / 2,
    ))
    body
  })
}

#set par(justify: true)

#frame(lorem(25))

#frame(width: 13cm)[
  #align(center, lorem(5))

  #align(center, lorem(7))

  #lorem(59)

  #lorem(6)
]


About redacting. I don’t see why you can’t just remove the text and only leave the clean empty frame. Why going half-way and bother blurring if it’s not important anyway and can be stripped completely. A rule of thumb is to always avoid blurring, if you really don’t want to share the content, or it’s not important/relevant (it’s shape, etc).

2 Likes

Thank you; that’s very helpful and informative!

The only thing I’d like to be different is for the frame to have the same full width as other text blocks, without having to manually specifying a width. Here’s an example of what I mean (screenshot of output from this project: Typst ):

As you can see, on the left the frame starts at the same x-coordinate as the text, but on the right the width is different — I guess I could measure the earlier blocks’ width and specify that, but wondering if there’s a way to specify the inner width (of the text) by subtracting the relevant amounts from the “current” width, then measure the height, then draw the frame with the relevant width and height.

(About the earlier question: true, I said “trying” but included no code; I should have said I’m exploring whether it’s feasible to use Typst for this use-case; I’m still currently on page 3, and all I had for this specific issue was a #rect with rounded corners, which I thought wasn’t worth sharing. There are other issues I’m encountering like Paragraph indentation seems to be buggy which may mean I have to give up on Typst, so it’s still in the early stages of exploration. Also the screenshot tool I used to take a screenshot of the original PDF didn’t allow redacting with black bars which would have been better, but only allowed blurring, which I agree is a weird choice! There is actually something relevant about the shape, namely placing the last line indented a bit to fit into the frame, but I’m willing to forgo that.)

#let frame-shape(width: 10cm, height: 5cm, radius: 15pt, a: 0pt, ..args) = {
  set curve.quad(relative: true)
  set curve.line(relative: true)
  curve(
    ..args,
    curve.move((0pt, radius)),
    curve.line((0pt, height - radius * 2)), // Left
    curve.quad((-a + radius, a), (radius, radius)), // Bottom-left
    curve.line((width - radius * 2, 0pt)), // Bottom
    curve.quad((a, a - radius), (radius, 0pt - radius)), // Bottom-right
    curve.line((0pt, -height + radius * 2)), // Right
    curve.quad((a - radius, -a), (-radius, -radius)), // Top-right
    curve.line((-width + radius * 2, 0pt)), // Top edge
    curve.quad((-a, -a + radius), (-radius, radius)), // Top-left
  )
}

#let frame(
  width: 100%,
  height: auto,
  radius: 15pt,
  inner: 2pt,
  stroke: 1pt,
  inner-stroke: 0.5pt,
  body,
) = {
  layout(size => {
    let body-block = block.with(width: width, inset: radius + inner)
    let (height, body) = if height != auto {
      (height, body-block(height: height, body))
    } else {
      let body = body-block(body)
      (measure(body, ..size).height, body)
    }
    place(frame-shape(
      width: width,
      height: height,
      radius: radius,
      stroke: stroke,
    ))
    place(dx: inner, dy: inner, frame-shape(
      width: width - inner * 2,
      height: height - inner * 2,
      radius: radius,
      stroke: inner-stroke,
      a: inner / 2,
    ))
    body
  })
}

#set par(justify: true)

#place(rect(width: 100%, height: 100%))
#frame(lorem(50))
#lorem(50)

1 Like

Ah width: 100% was what I was missing; thank you!

(Now I recall seeing example of such relative widths in the tutorial e.g. Formatting – Typst Documentation but I’m not sure where it’s documented more formally what 100% means in a given context: does it always mean the width of the text block on the page? I guess it’s the current width being used to break lines in the current place, kind of like TeX’s current value of \hsize.)

1 Like

(Apologies if this is getting offtopic; I can start a new question or ask elsewhere if suggested…)

Thank you; I’d read the documentation on relative lengths, but my question is more about: relative to what? For example, the source code link above suggests that often 25%, 100% etc are relative to “the width of the page”, but it also seems to make a distinction between that and “the page’s full width” (so the former apparently doesn’t include the margins). Is this documented anywhere? Also, as it says, in general these relative lengths are “relative to the container”, but how to figure out what the container is, i.e. what is the layout model? For example, in TeX: the page is a vbox made of a vertical list of hboxes (and glue), each of which was typically formed by breaking a horizontal list of boxes-and-glue (the paragraph). In Typst it appears there are different block-level elements: inline elements are collected into paragraphs but there are also block, rect, place, etc — is there a full list of these elements somewhere? It would be useful for building a correct mental model of what’s going on, on a typical page.

Doesn’t the documentation answers this?

Anything that creates a new region: rect, block, box, pad, etc. And default page/header/footer body.

No, but you can request it.

No, it just says “A length in relation to some known length” when the crucial question is what the known length is. Is there some other page I should be looking at?

Ah thanks, “region” is another term new to me that seems important to understand; what does it mean / where can I learn more about it? And what’s the difference between “page” and “body”, given that (for widths) “100% of the page” also seems to mean “100% of the body”?

Yes thanks that would be useful; where can I request this?

This is a quote from the very first paragraph. There are 2 distinct sections that describe what the known length is.

From reading the Typst source code.

I think the new docs should be enough for practical use. If not, then create a new issue.

“Body” is a contextual term, page isn’t. Page’s body/footer/header have the same width of page.width - page.margin.left - page.margin.right.

https://github.com/typst/typst/issues/new?template=3-docs.yml