The blue circle's animation is quite complex. It consists of multiple
stages. (1) The circle grows in size. (2) It continues to grow in size
at a faster rate, as it shoots off to the right. (3) It pauses. (4) It
moves to the middle. (5) It pauses again. (6) It shrinks to nothing.
All of this is captured by a single object anim_circle (written using minanim.js)
which declares what the animation is doing:

anim_circle is a function, which can be invoked as val = anim_circle(t).
It returns an object val. val.cx and val.cr have values as the animation dictates.
That's it. It does not modify the DOM. It does not edit thecircle tag.
Given a time t0, it computes cx and cr at time t0. Keep it simple, stupid!

Here is a plot of the values of val.cx and val.cr for different values of t.
This plotting code calls anim_circle at different times to plot the
results. The function anim_circleis these plots,
since it doesn't compute anything else.

Fancy ways of saying that anim_circle doesn't change anything else is to say that it is side-effect-free, or refrentially transparent.

You can explore different definitions anim_circles. Feel free to
play around. Try evaluating anim_circle(0), anim_circle(anim_circle.duration),
anim_circle(anim_circle.duration/2.0) in the console to get a feel for what
anim_circle returns.

As hinted above, since our specification of the animation was entirely declarative,
it can't really "do anything else" like manipulate the DOM. This gives us
fantastic debugging and editing capabilities. As it's "just" a mathematical
function:

1:2: anim_circle: (t:Time) -> (cx: float, cr: float)
3:

We can easily swap it (by pasting the code above), poke it (by calling anim_circle(0.5)),
and in general deal with is as a unit of thought. It has no unpleasant
interactions with the rest of the world.

Our framework is composable, because we can build larger objects from smaller
objects in a natural way. As an example, a staggered animation is a nice
way to make the entry of multiple objects feel less monotonous.

The code to achieve this creates a list of animations called as which
has the animations of the ball rising up. Each element as[i] has
the animation of the ball rising up for the same amount of time. This is
visualized here:

Next, each element as[i] is modified by creating a new animation xs[i].
xs[i] runs as[i] after a delay of delta*i.
We then compose all the xs[i] in parallel to create a single animation x.
This animation has the balls rising from the bottom in a staggered fashion.

Notice that the final animation network is quite complex. It's hopeless
to build it "manually". In code, we write special helpers
called anim_stagger that allow us to stagger animation, and then use
it, along with .seq() and .par() to build the full animation:

As hinted above, since our specification of the animation was entirely declarative,
it can't really "do anything else" like manipulate the DOM. This gives us
fantastic debugging and editing capabilities. As it's "just" a mathematical
function:

1:2: anim_circle: (t:Time) -> (cx: float, cr: float)
3:

so we can play with it on the console, edit it interactively, and plot it.
It's behaviour can be studied on a piece of paper, since it's entirely
decoupled from the real world.

So far, we have been using the same easing parameter everywhere:
easing_cubic. This parameter is a way to warp time. We only tell the
library what the final value is supposed to be. It's our library's job
to figure out how to get from the current value to the final value. However,
there are many ways to get from the initial value to the final value. We
could:

Change the value in constant increments. This is what easing_linear does.

Change the value so that it changes slowly in the beginning, and much
faster later. This is what easing_cubic does.

Change the value so that it changes quickly, overshoots, and then
comes back to the final value. This is what ease_out_back does.

There are many easing functions. Indeed, infinitely many, since we can write
any function we want. A quick example of the three mentioned above, with
a slide to notice the difference:

Both d3.js and anime.js are libraries that intertwine
computing with animation. On the other hand, our implementation describes
only how values change. It's up to us to render this using
SVG/canvas/what-have-you.
Building a layer like anime.js on top of this is not hard. On the other hand,
using anime.js purely is impossible.

The entire "library", which is written very defensively and sprinkled with
asserts fits in exactly 100 lines of code
. It can be golfed further
at the expense of either asserts, clarity, or by adding some higher-order
functions that factor out some common work. I was loath to do any of these.
So here's the full source code, explained as we go on.

We write assert_precondition(t, out, tstart) to check that t
and tstart are numbers such that t >= tstart, and that out is an object.
If tstart is uninitialized, we initialize tstart to 0. If
out is uninitialized, we initialize out to {}.

anim_delay(duration) creates a function f. On being invoked, it returns
whatever value of out has been given to it. That is, it doesn't
modify anything. It has three fields, duration, par, and seq.
duration. duration is how long the animation runs for. par, seq
are methods for chaining, that allows us to compose this delay animation
in parallel and in sequence with other animations.

const(field, v) creates a function f. On being invoked, it sets
out[field] = v. It takes zero time to run such an animation, hence it's
duration is 0. Useful for instaneously setting the value at the
start of an animation.

We implement two easing functions, which takes a parameter
tlin such that 0 <= tlin <= 1, and two parameters vstart and vend.
The functions allow us to animate a change from vstart to vend smoothly.
We are to imagine tlin as a time. When tlin=0, we are at vstart.
When tlin=1, we will be at tend.
In between, we want values between vstart and vend. To animate values,
we often want the change from vstart to vend to happen a certain way.
For example, we often want the change to start slowly, and then for the
change to happen faster towards the end. A good reference for this is
easings.net
. Our animation library can use
any easing function we see fit.

anim_interpolated(duration) creates a function f. On being invoked,
it figures out if its animation is running or has already ended.
We have a preconditiont >= tstart, which is checked by
assert_precondition, and is maintained by the library.
So, we only need to care whether the animation is currently running
or has ended. If the animation is currently running, we find the
current value using fease. If the animation has ended, we set
the value to the end value.

anim_sequence(anim1, anim2) sets up anim2 to begin
running once anim1 has completed. When it is invoked, t >= tstart. So
it can run anim1 immediately. If it learns that
anim1 has completed, it then invokes anim2. The total time taken for
this animation is its duration. This is the sum of durations of anim1
and anim2.

anim_parallel(anim1, anim2) sets up anim1 and
anim2 to run in parallel. When it is invoked, t >= tstart. So it can
launch anim1, anim2 both immediately. The duration of this animation
is the maximum time taken by anim1, anim2.

We saw how to write a tiny, declarative, composable animation library that
does one thing: compose functions that manipulate values over time,
and does it well.
If you like this content, check out the repo at
bollu/mathemagic