-
Notifications
You must be signed in to change notification settings - Fork 4
Animating Bezier curves in 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.
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)
wherep
,q
andr
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.
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.