Why I'm creating my own animation engine (again)

Tags: luamathtechnology

Some time ago I worked on a programmatic SVG animation engine. I was directly inspired by manim, so naturally I decided to develop it in Python.

I was really happy with how it turned out and even used it in some projects, but working with it was somewhat clumsy, specially with larger, more complex animations.

After learning about Motion Canvas, I was motivated to work on a new animation engine, hopefully one better than my previous attempt. Unsurprisingly, I chose Lua for this implementation, which turned out to be a really good decision.

Why am I making this? There are several reasons:

In a way, I consider this new engine to be a successor to ProSVG. The two biggest differences are:

Variables are special values that can be animated, but most importantly, their values get computed dynamically. This simplifies complex animations a lot, because by only updating a single variable, all other variables that depend on it get updated as well. This way, graphical elements that depend on these variables get animated.

Thanks to Lua's metatables it's possible to wrap the inner workings of variables in a simple and intuitive DSL.

As for the rendering part, Lua only generates SVG data which is piped to rsvg-convert to rasterize it. ffmpeg takes all these frames and produces a video.

Below are a few animations and the code needed to create them, using the default theme.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
A = point(-2, 0)
B = point(2, -2)
C = point(0, 2)

triangle = polygon(A, B, C)
plot(triangle) -- add triangle to the scene

draw(triangle, 2) -- animate it, 2 seconds

mA, mB, mC = midpoint(B, C), midpoint(A, C), midpoint(A, B)

all.draw { mA, mB, mC }

L1, L2, L3 = line(A, mA), line(B, mB), line(C, mC)

L1.style = "construction"
L2.style = "construction"
L3.style = "construction"

plotBack(L1, L2, L3)
all.draw { L1, L2, L3 }

pause(1)

tween(A, point(-4, -1), 2)

pause(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
square = function(x)
	return x^2 / 3 - 2
end

draw(curve(square, -3, 3))

t = var(-3) -- t is a variable that can be animated

p = point(t, square(t))
label = text(p + point(0, 0.25), "f([]) = []", t, square(t))

plot(p, label)

tween(t, 3, 5)
pause(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
local function construct(element)
	element.style = "construction"
	plotBack(element)
	draw(element)
end

A = point(2, 0)
B = point(1.8, 1.5)
C = point(-2, 1.2)

all.draw { A, B, C }

L1 = segment(A, B)
L2 = segment(B, C)

construct(L1)
construct(L2)

L3, L4 = bisect(A, B), bisect(B, C)

construct(circle(A, B))
construct(circle(B, A))

draw(L3)

construct(circle(B, C))
construct(circle(C, B))

draw(L4)

P = intersect(L3, L4)
draw(P)

c = circle(P, A)
plotBack(c)
draw(c, 2)
pause(1)

tween(B, point(0, 2.5), 2)

pause(1)

Note that the DSL is still in development, so some stuff might change in the future.