Why I'm creating my own animation engine (again)
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:
- Simple. I want it to have as few dependencies as possible. Right now only Lua, rsvg-convert, and ffmpeg are needed. It's not as feature rich as other engines, but it can do quite a lot of things, and overall it's a pretty lightweight engine.
- Easy to use. My goal is to make the process of making animations as frictionless as possible. It shouldn't feel like writing code.
- It's fun.
In a way, I consider this new engine to be a successor to ProSVG. The two biggest differences are:
- Graphics are constructed using mathematical elements (points, lines, circles, polygons) and not SVG elements
- Variables
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.