How do I draw parabolas?

I would really like to make a parabola. I am not sure if I have figured it out, yet.

Something that looks basically right is a path as follows:

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)

#let apex-x = w / 2
#let apex-y = w / 2

// Control point for quadratic Bezier curve.
#let control-x = w
#let control-y = w / 2

// Cubic control points
#let c1-x = w / 2
#let c1-y = w / 4
#let c2-x = w / 2
#let c2-y = 3 * w / 4

#let parabola = [
  #path(
    start,
    (
      (apex-x, apex-y),
      (c1-x - apex-x, c1-y - apex-y),
      (c2-x - apex-x, c2-y - apex-y),
    ),
    end,
  )
]

At the end of the document, please find a fuller program that creates the following diagram:

In blue, we have the control polygon of a quadratic Bezier curve. This is translated to a cubic Bezier curve, the control polygon of which is in green. (In Typst, there is a Bezier curve type – path – which supports only cubic curves.) It certainly does look like a parabola, but the control points that make it look like this don’t make a whole lot of sense to me.

A parabola can be easily represented with a quadratic Bezier curve (as far as I can tell, the control point is the point on the directrix opposite the focus) and there is a way to translate quadratic Bezier curves to cubic Bezier curves. Per “Degree Elevation” on Wikipedia, we learn that “A Bézier curve of degree n can be converted into a Bézier curve of degree n + 1 with the same shape.”.

According to the formula given by Wikipedia (and other sources), the cubic control points should be:

#let c1-x = (2 * control-x / 3) + (start.first() / 3)
#let c1-y = (2 * control-y / 3) + (start.last() / 3)
#let c2-x = (2 * control-x / 3) + (end.first() / 3)
#let c2-y = (2 * control-y / 3) + (end.last() / 3)

However, this results in a lobed curve:

Now I am just not sure if I really made a parabola or not, since I really just eyeballed it.

Here is the code for the full diagram:

#set page(paper: "a4")

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)

#let apex-x = w / 2
#let apex-y = w / 2

// Control point for quadratic Bezier curve.
#let control-x = w
#let control-y = w / 2

// Cubic control points
#let c1-x = w / 2
#let c1-y = w / 4
#let c2-x = w / 2
#let c2-y = 3 * w / 4
// #let c1-x = (2 * control-x / 3) + (start.first() / 3)
// #let c1-y = (2 * control-y / 3) + (start.last() / 3)
// #let c2-x = (2 * control-x / 3) + (end.first() / 3)
// #let c2-y = (2 * control-y / 3) + (end.last() / 3)

#let parabola = [
  #path(
    start,
    (
      (apex-x, apex-y),
      (c1-x - apex-x, c1-y - apex-y),
      (c2-x - apex-x, c2-y - apex-y),
    ),
    end,
    stroke: black + 1mm,
  )
]

#let grid = {
  let steps = 10
  let step = w / steps
  let cell = rect(width: step, height: step, stroke: gray + 0.1mm)

  for i in array.range(0, steps) {
    for j in array.range(0, steps) {
      place(dx: i * step, dy: j * step, cell)
    }
  }

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (control-x, control-y), end,
            stroke: blue.transparentize(25%) + 1mm)
  )

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (c1-x, c1-y), (c2-x, c2-y), end,
            stroke: green.transparentize(50%) + 0.5mm)
  )
}

#place(dx: 0mm, dy: 0mm, parabola)
#place(dx: 0mm, dy: 0mm, grid)

Unless you really want to use path directly, I’d suggest you use Cetz instead. It’s vastly more user friendly

1 Like

I would really like to understand path.

1 Like

@fenjalien or @jwolf might be the best ones to answer. At one point I did know this, but forgot the details.

I think it’s mostly just making sure you’re talking about the same control points.

Maybe it’s actually two quadratic curves, not cubic at all.

ok, I figured it out. The docs are maybe a bit hard to parse here… tldr: the first control point of the cubic bezier, which by Wikipedia’s formula results in a quadratic bezier, needs to be specified with the start point. In a Typst path, you specify control points for specific vertices, not for specific curves.

Let me put some code and an image first:

#let weight(a, (xa, ya), b, (xb, yb)) = {
  (a * xa + b * xb, a * ya + b * yb)
}

#let dot((x, y), stroke: black) = {
  place(path(
    stroke: (thickness: 3pt, paint: stroke, join: "round"),
    closed: true,
    (x - 1pt, y - 1pt),
    (x - 1pt, y + 1pt),
    (x + 1pt, y + 1pt),
    (x + 1pt, y - 1pt),
  ))
}

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)
#let control = (w, w/2)

#{
  let c1 = weight(1/3, start, 2/3, control)
  let c2 = weight(2/3, control, 1/3, end)

  dot(start)
  dot(c1, stroke: red)
  dot(control, stroke: blue)
  dot(c2, stroke: red)
  dot(end)

  place(path(
    start,
    end,
  ))
  // (1)
  place(path(
    stroke: blue,
    (
      start,
      weight(1, start, -1, control),
    ),
    (
      end,
      weight(1, control, -1, end),
    ),
  ))
  // (2)
  place(path(
    stroke: blue,
    start,
    (
      end,
      weight(1, control, -1, end),
    ),
  ))
  // (3)
  place(path(
    stroke: red,
    (
      start,
      (0mm, 0mm),
      weight(1, c1, -1, start),
    ),
    (
      end,
      weight(1, c2, -1, end),
      (0mm, 0mm),
    ),
  ))
}


For (1), let’s look what the docs say:

An array of two points, the first being the vertex and the second being the control point. The control point is expressed relative to the vertex and is mirrored to get the second control point. The given control point is the one that affects the curve coming into this vertex (even for the first point). The mirrored control point affects the curve going out of this vertex.

  place(path(
    stroke: blue,
    (
      start,
      weight(1, start, -1, control),
    ),
    (
      end,
      weight(1, control, -1, end),
    ),
  ))

We have two points with one control point each, and we are drawing a bezier going out of start and going into end. Going into start would be start + (start - control) (remember, relative to start), out is thus start - (start - control) = control. Going into end is end + (control - end) = control.
So we end up with a bezier curve with the points (start, control, control, end) - a symmentrical cubic bezier with coinciding control points, great.

(2) is a bit of a naive attempt:

  place(path(
    stroke: blue,
    start,
    (
      end,
      weight(1, control, -1, end),
    ),
  ))

With only a single point, the start control points are just the start point itself. Thus the resulting bezier is (start, start, control, end). I think that should be asymmetric, although it doesn’t seem like it… maybe I’m still missing something there.

(3) is what you were after

  // (3)
  place(path(
    stroke: red,
    (
      start,
      (0mm, 0mm),
      weight(1, c1, -1, start),
    ),
    (
      end,
      weight(1, c2, -1, end),
      (0mm, 0mm),
    ),
  ))

Here I used three coordinates per vertex of the path:

An array of three points, the first being the vertex and the next being the control points (control point for curves coming in and out, respectively).

We don’t need the control point going into start or out of end so I set these to the vertices themselves (in relative coordinates). The result is the cubic bezier (start, c1, c2, end)!


Let’s look at your path again:

#let parabola = [
  #path(
    start,
    (
      (apex-x, apex-y),
      (c1-x - apex-x, c1-y - apex-y),
      (c2-x - apex-x, c2-y - apex-y),
    ),
    end,
  )
]

This resulted in two curves: (start, start, c1, apex) and (apex, c2, end, end).

Here is a small update that makes it clear the sort-of-right shape is not the target parabola. The red dots are points on the parabola.

#set page(paper: "a4")

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)

#let apex-x = w / 2
#let apex-y = w / 2

// Control point for quadratic Bezier curve.
#let control-x = w
#let control-y = w / 2

// Cubic control points
#let c1-x = w / 2
#let c1-y = w / 4
#let c2-x = w / 2
#let c2-y = 3 * w / 4
// #let c1-x = (2 * control-x / 3) + (start.first() / 3)
// #let c1-y = (2 * control-y / 3) + (start.last() / 3)
// #let c2-x = (2 * control-x / 3) + (end.first() / 3)
// #let c2-y = (2 * control-y / 3) + (end.last() / 3)

#let parabola = [
  #path(
    start,
    (
      (apex-x, apex-y),
      (c1-x - apex-x, c1-y - apex-y),
      (c2-x - apex-x, c2-y - apex-y),
    ),
    end,
    stroke: black + 1mm,
  )
]

#let grid = {
  let steps = 10
  let step = w / steps
  let cell = rect(width: step, height: step, stroke: gray + 0.1mm)

  for i in array.range(0, steps) {
    for j in array.range(0, steps) {
      place(dx: i * step, dy: j * step, cell)
    }
  }

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (control-x, control-y), end,
            stroke: blue.transparentize(25%) + 1mm)
  )

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (c1-x, c1-y), (c2-x, c2-y), end,
            stroke: green.transparentize(50%) + 0.5mm)
  )
}

#let parabola-points = {
  let steps = 50
  let step = w / steps
  let r = 0.5mm
  let dot = circle(radius: r, fill: red, stroke: none)

  for i in array.range(0, steps) {
    let y = (step / 2) + (i * step)
    let t = (y - (w / 2)) / w
    let x = (w / 2) -  (2 * t * t * w)
    place(dx: x - r, dy: y - r, dot)
  }
}

#place(dx: 0mm, dy: 0mm, parabola)
#place(dx: 0mm, dy: 0mm, grid)
#place(dx: 0mm, dy: 0mm, parabola-points)

What is weight()? It doesn’t seem to be a standard library function.

Something very close to your code seems to be right.

Here we are using two control points, where one control point is “the point itself” (0, 0 in relative terms) and the other is one of the control points calculated using the calculation per “Degree Elevation” on Wikipedia.

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)

#let apex-x = w / 2
#let apex-y = w / 2

// Control point for quadratic Bezier curve.
#let control-x = w
#let control-y = w / 2

// Cubic control points
#let c1-x = (2 * control-x / 3) + (start.first() / 3)
#let c1-y = (2 * control-y / 3) + (start.last() / 3)
#let c2-x = (2 * control-x / 3) + (end.first() / 3)
#let c2-y = (2 * control-y / 3) + (end.last() / 3)

#let parabola = [
  #path(
    (start, (0mm, 0mm), (c1-x - start.first(), c1-y - start.last())),
    (end, (c2-x - end.first(), c2-y - end.last()), (0mm, 0mm)),
    stroke: black + 1mm,
  )
]

Here is the code for rendering the curve with the control polygons and grid:

#set page(paper: "a4")

#let w = 100mm
#let start = (0mm, 0mm)
#let end = (0mm, w)

#let apex-x = w / 2
#let apex-y = w / 2

// Control point for quadratic Bezier curve.
#let control-x = w
#let control-y = w / 2

// Cubic control points
#let c1-x = (2 * control-x / 3) + (start.first() / 3)
#let c1-y = (2 * control-y / 3) + (start.last() / 3)
#let c2-x = (2 * control-x / 3) + (end.first() / 3)
#let c2-y = (2 * control-y / 3) + (end.last() / 3)

#let parabola = [
  #path(
    (start, (0mm, 0mm), (c1-x - start.first(), c1-y - start.last())),
    (end, (c2-x - end.first(), c2-y - end.last()), (0mm, 0mm)),
    stroke: black + 1mm,
  )
]

#let grid = {
  let steps = 10
  let step = w / steps
  let cell = rect(width: step, height: step, stroke: gray + 0.1mm)

  for i in array.range(0, steps) {
    for j in array.range(0, steps) {
      place(dx: i * step, dy: j * step, cell)
    }
  }

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (control-x, control-y), end,
            stroke: blue.transparentize(25%) + 1mm)
  )

  place(
    dx: 0mm,
    dy: 0mm,
    polygon(start, (c1-x, c1-y), (c2-x, c2-y), end,
            stroke: green.transparentize(50%) + 0.5mm)
  )
}

#let parabola-points = {
  let steps = 50
  let step = w / steps
  let r = 0.5mm
  let dot = circle(radius: r, fill: red, stroke: none)

  for i in array.range(0, steps) {
    let y = (step / 2) + (i * step)
    let t = (y - (w / 2)) / w
    let x = (w / 2) -  (2 * t * t * w)
    place(dx: x - r, dy: y - r, dot)
  }
}

#place(dx: 0mm, dy: 0mm, parabola)
#place(dx: 0mm, dy: 0mm, grid)
#place(dx: 0mm, dy: 0mm, parabola-points)

Sorry, I missed it while copy pasting (dot as well). I’ll correct my post later, but it’s basically a weighted sum of 2D points/vectors: weight(a, v, b, w) = a * v + b * w.

(I edited the original reply)