Skip to content

Animating Bezier curves in ClojureScript

jonase edited this page Sep 1, 2011 · 4 revisions

Animating Bezier curves with ClojureScript

Lately, I've been working quite a bit with ClojureScript and the canvas element. As part of another project, I've been developing a thin wrapper for the HTML5 canvas API. It is neither feature complete nor robust yet but I thought it would be a good idea to use it in different settings to see how well it works. This blog post inspired me to write a similar animation in ClojureScript using my canvas library. The final result can be viewed here.

weft.canvas

The working name for the canvas library is weft.canvas. In its current shape and form weft.canvas is a very thin wrapper. There are only three things I've changed or added:

  • I use points instead of x and y coordinates in arguments to functions such as bezierCurveTo. This means that instead of writing (.bezierCurveTo ctx px py qx qy rx ry) one would write (bezier-curve-to ctx p q r) where p, q and r are points. Currently points are simply two element vectors, [x y].

  • I have tried to make the api work well with the doto macro. It is possible to write

(doto ctx
  save
  (set-fill-style "red")
  begin-path
  (move-to p)
  (line-to q)
  (line-to r)
  (line-to p)
  fill
  restore)

instead of

(doto ctx
  (. (save))
  (. (beginPath))
  (.moveTo px py)
  ...)

Setting .fillStyle and similar attributes doesn't work at all with doto without the wrapper.

  • I have added a few general functions as need has arisen, such as rounded-rect, circle etc.

Animating Bezier curves

The program is quite straight forward and easy to understand. Most of it involves figuring out which lines to draw between which points. There are a few interesting functions though. The first one is make-ticker which is defined as

(defn make-ticker [step]
  (let [t (atom 0)
        d (atom 1)]
    (fn []
      (when-not (< 0 @t 1)
        (swap! d * -1))
      (swap! t + (* step @d)))))

make-ticker returns a function which closes over two atoms, t and d. Every time the returned function is called the current value of t is returned after it has been either incremented or decremented by a small amount (depending on the value of the atom d). Two tickers are defined, cubic-tick and quad-tick, which are later used to animate the quadratic and the cubic bezier curves.

Another interesting function is lerp which I've found useful enough to add to weft.canvas. My instinct told me that such a function should be useful in many contexts and after a bit of searching I found a similarly named function in both the DirectX docs as well as Processing docs. The function takes two points (p and q) as arguments and returns a point somewhere between the two points interpolated by a parameter t. For example, with t = 0.33, the point located one third of the way between point p and q is returned. The implementation of lerp is as easy as

(defn lerp [[px py] [qx qy] t]
  [(+ px (* (- qx px) t))
   (+ py (* (- qy py) t))])

Next, the function that animates the quadratic bezier curve is shown below:

(defn render-quad []
  (let [t (quad-tick)
        p (c/lerp p1 p2 t)
        q (c/lerp p2 p3 t)
        x (c/lerp p q t)]
    (doto quad-ctx
      c/save
      c/clear-rect

      (c/set-line-width 2)
      (c/set-stroke-style "red")

      c/begin-path
      (c/move-to p1)
      (c/quadratic-curve-to p x)
      c/stroke

      (c/set-line-width 1)
      (c/set-stroke-style "black")
      
      c/begin-path
      (c/move-to p)
      (c/line-to q)
      c/stroke
      
      (draw-points [p q] 3 "black")
      (draw-points [x] 4 "red")

      c/restore))
  (c/request-animation-frame render-quad))

The function begins with a (quad-tick), yielding a number t between 0 and 1. Two points, p and q, are then calculated. p is a point on the straight line between p1 and p2 while q is located between p2 and p3. A third point, x, lie between p and q. The point x will always lie on the bezier curve defined by the points p1, p2 and p3.

The rest is drawing code. A background canvas is used to draw content that doesn't need to be animated (not shown here). Finally, (request-animation-frame render-quad) is called which makes the render function run again after a few milliseconds.

The render function for the cubic bezier curve is very similar to the quadratic case. Both animations can be viewed here.