Visualising Bézier Curves

Experimenting with Clojure

Posted by: Maarten Metz on 2017-07-01
ClojureFunctional

For reasons I might explain in another post, I was reading about Bézier curves on wikipedia the other day. Especially the paragraph on “Constructing Bézier curves” made me understand intuitively how these curves are constructed. I wondered if clojure could help me easily visualise some of these curves so I fired up a REPL (Read-Eval-Print-Loop) to investigate.

Setup

I assume you have a working leiningen setup with the lein-try plugin installed.

  • Leiningen will help you get started with clojure right away from your shell
  • Lein-try enables you to spin up a repl and try a library without any hassle.

Let’s first start a repl, and specify the library we want to try:

lein try incanter "1.5.7"

Incanter is a clojure-based, R-like platform for statistical computing and graphics. We’ll use it to visualise the Bézier curves.

Since the repl is already fired up, let’s immediately require the dependencies we’ll need:

(require '[incanter.core   :as incanter])
(require '[incanter.charts :as charts])

Nothing fancy going on here. Simply ‘import’ statements the clojure way.

Helper function

I decided to hand-roll my own pow function, because we’ll need it later in the creation of bézier curves:

(defn pow [base exponent]
  (reduce *' (repeat exponent base)))

This function basically says:

  • define a function named pow
  • let base and exponent be its arguments
  • make it repeat the base exponent times
  • and reduce that result with the *' multiply function
;; so:
(pow 2 3)
;; will lead to
(reduce *' [2 2 2])
;; which results in
8

I’m using the *' multiply function instead of the normal * multiply function, because according to the docs *' supports arbitrary precision. You can see for yourself by typing (doc *') in your repl, or study the code by typing (source *') in your repl.

Bézier

Bézier functions work with control points. The minimum number of control points is 2 and Bézier curves with 2 control points are straight lines (although you can also create straight lines with more than 2 control points).

For the moment I only need Bézier curves with 3 control points: a start point, an end point and one point controlling the curve of the line between start and end.

Quoting the Wikipedia page: “A quadratic Bézier curve is the path traced by the function B(t), given points P0, P1, and P2”:

bezier-formula

So given 3 points (P0, P1 and P2) I should be able to describe the curve with this math function. Conceptually a pen is drawing the curve by starting on P0 on moment t = 0 and following the curve described by this formula and the position of the 3 points until it hits the end point at t = 1. The only thing this resulting B(t) function needs is the moment t and it will calculate the X or Y coördinate at that particular moment.

Let’s convert the math function to clojure:

(defn bezier-3 [P0 P1 P2]
  (fn [t]
    (+
      (* (pow (- 1 t) 2) P0)
      (* 2 (- 1 t) t     P1)
      (* (pow t 2)       P2))))

As you can see, you’ll have to translate the infix notation to clojure’s prefix notation. The advantage is there are no precedence rules to remember anymore. These are all just lists where the first element of the list is interpreted as a function to call and the brackets are used to put them into context. (If you can’t live with that: you can also use infix notation in clojure. For instance incanter can also be fed with infix notation. However, you’ll lose the power of the 𝝺)

This function basically states:

  • define a function named bezier-3
  • let it have 3 arguments: P0, P1 and P2
  • make it return an anonymous function

This anonymous function:

  • takes t as an argument
  • has P0, P1 and P2 already ‘injected’
  • applies the Bézier math function

Excellent. Let’s try it:

(def test-b3 (bezier-3 1 1 0))

(test-b3 0)
=> 1

Here I’m defining a variable test-b3 which holds the anonymous function returned by the bezier-3 function call. The 3 points are either all x or all y coordinates of the points P0, P1 and P2.

With the (test-b3 0) function call I’m calling the anonymous function with a t value of 0. This nicely returns an answer representing the x coordinates at moment t if you provided the (P0, P1 and P2) x coordinates when calling the bezier-3 function or the y coordinate at moment t if you provided the (P0, P1 and P2) y coordinates.

Since this test didn’t blow the stack or throw a NullPointerException or anything, let’s map this function over a range of t’s [0 0.25 0.5 0.75 1]:

(map test-b3 [0 0.25 0.5 0.75 1])
=> (1 0.9375 0.75 0.4375 0)

Instead of typing these t’s to map over, we could also use the range function:

(range 0 10)
=> (0 1 2 3 4 5 6 7 8 9)

(range 0 10 2)
=> (0 2 4 6 8)

As you can see, range allows you to specify a start (inclusive), an end (exclusive) and a step size (optional).

(map test-b3 (range 0 1 0.1))
=> (1 0.9900000000000001 0.9600000000000002 0.9099999999999999 0.84 0.75 0.64 0.51 0.3600000000000001 0.19000000000000014 2.220446049250313E-16)

Although a certain pattern is already visible in these numbers, now might be the right time to start visualising the curves.

Visualise

Let’s be brave and go right to the essence:

(defn view-bezier-plot [[x1 y1] [x2 y2] [x3 y3] plot-title]
  (let [b3x         (bezier-3 x1 x2 x3)
        xs          (map b3x (range 0 1.0 0.01))
        b3y         (bezier-3 y1 y2 y3)
        ys          (map b3y (range 0 1.0 0.01))
        dataset     (incanter/conj-cols xs ys)
        xy-plot     (charts/xy-plot "col-0" "col-1" :data dataset :points true :title plot-title)]
    (incanter/view xy-plot)))

The let form is clojure’s way of defining local variables. So b3x, xs, b3y, etc. can be seen as local variables with their values specified in the functions directly after their declaration.

This function basically states:

  • define a function named view-bezier-plot
  • let it have 3 arguments which are destructured into their 2D x & y coordinates
  • let the fourth argument be the title of the plot
  • let it have some local variables:
    • b3x takes all x coordinates of the 3 points
    • b3y takes the y coordinates
    • xs are the all x values resulting from applying the b3x anonymous function with all the range values
    • ys are the all y values resulting from applying the b3y anonymous function with all the range values
    • incanter can work with columns similarly to spreadsheets. dataset is an incanter dataset where 2 columns are ‘brought together’, or in lisp terms conj[oined]. By default, these columns are called col-0, col-1, … , col-n
    • xy-plot contains an incanter chart where dataset provides the data, the x-axis - and y-axis values are found in columns col-0 and col-1 respectively and viewing the points is set to true
  • and make it return an xy-plot

I love it when a plan comes together


(view-bezier-plot [0 0] [1 0] [1 1] "increasing ascending")

(view-bezier-plot [0 0] [0 -1] [1 1] "swoosh")

(view-bezier-plot [-2 4] [0 -4] [2 4] "y = x^2 ?")

(view-bezier-plot [-1 0] [0 0] [1 0] "my pulse after a useless meeting")

incanter-plots

The generated charts will be opened in external java windows. Sometimes they stay hidden behind other windows on your screen, so you might have to bring your java windows to the foreground in order to see them.

Conclusion

In a relatively short repl session I was able to get a better understanding of (3 point) Bézier curves. Although the blogpost is long, not much code or time was needed to create the curves:

(require '[incanter.core   :as incanter])
(require '[incanter.charts :as charts])


(defn pow [base exponent]
  (reduce *' (repeat exponent base)))


(defn bezier-3 [P0 P1 P2]
  (fn [t]
    (+
      (* (pow (- 1 t) 2) P0)
      (* 2 (- 1 t) t     P1)
      (* (pow t 2)       P2))))


(defn view-bezier-plot [[x1 y1] [x2 y2] [x3 y3] plot-title]
  (let [b3x         (bezier-3 x1 x2 x3)
        xs          (map b3x (range 0 1.0 0.01))
        b3y         (bezier-3 y1 y2 y3)
        ys          (map b3y (range 0 1.0 0.01))
        dataset     (incanter/conj-cols xs ys)
        xy-plot     (charts/xy-plot "col-0" "col-1" :data dataset :points true :title plot-title)]
    (incanter/view xy-plot)))

Actually, the hand-rolled pow function isn’t really needed. I could have used clojure’s java interop: (Math/pow 2 3). In that case I would have been calling the pow method directly on java’s Math class. I weighted the option of hand-rolling the pow function, showing some functional code and explaining how functions are defined in clojure versus explaining java interop.

Explaining in text what the code does took me 5 times as long as writing the code itself. That’s a great thing about lisps in general and clojure in particular: no fluff, just stuff!

Thanks Niek for posting my guest blog. Please share your comments, suggestions and thoughts about this blog with me on twitter.com/mmz_. Thanks for reading and Happy Coding!