Making animations using generator functions
December 12, 2025
Have you ever seen one of those really pretty videos on YouTube with fancy visuals to show programming and mathematics concepts? Chances are they were made using Manim or a more recent alternative called Motion Canvas.
I’ve always been fascinated by how easy it was to understand concepts through visuals, so I wanted to learn how to do them myself!
I’m a huge 3 Blue 1 Brown fan, so the first thing I’ve done was look at Manim, but I immediately learned that it uses Python, which (unpopular opinion) I don’t really like using.
Luckily I’m also a huge aarthificial fan! (I’m starting to see a pattern), so I went on the Motion Canvas website to understand how to use it to make my own animations.
Motion Canvas
It’s a JavaScript editor and library to create videos procedurally using:
Fancy right? Just look at this:
yield* circle().scale(2, 0.3);
yield* all(
circle().scale(1, 0.3),
circle().position.y(200, 0.3),
);
yield* circle().fill('green', 0.3); But the first time I saw this code I thought “I have no idea what any of this means!” and that’s because generator functions are not that common in the JavaScript world, but they are really cool and really powerful.
We are now going to learn what generator functions are by creating our own very simple Motion Canvas. If you want to learn more about making proper procedural animations, look at this video.
Generator functions
At a high level, you can think of generator functions as functions that can “return” multiple times (we say that they yield values). More concretely, using the generator function syntax, we declare a Generator Function that, when called, returns a Generator.
Generators also implement the Iterable protocol, which is what you use in for...of loops.
An iterable is just an object that has a next() function that returns a { done: boolean, value: T }.
Another cool detail of generator functions is that they are lazily loaded, as in, they don’t actually execute until you call the .next() function.
JavaScript gives us some syntactic sugar both for defining a function and for yielding a value, which makes using generator functions really easy:
function* range(start, end){
for(let i = start; i < end; i++){
yield i;
}
}
for(const v of range(0, 10)){
console.log(v)
} You use the function* syntax to mark the function as being a generator, then inside of the body of the generator you can do three things, yield a value, return from the function, or yield* to delegate the generation to another generator.
Let’s look at all 3 of them at the same time:
function* subGenerator() {
yield 'A';
yield 'B';
}
function* mainGenerator() {
// 1. yield a value
yield 1;
// 2. yield* to delegate to another generator
yield* subGenerator();
yield 2;
// 3. return from the function (ends the generator)
return 'finish';
}
const gen = mainGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'A', done: false }
console.log(gen.next()); // { value: 'B', done: false }
console.log(gen.next()); // { value: 2, done: false }
// Careful! in iterators when `done === true` the value
// is discarded and not returned
console.log(gen.next()); // { value: 'finish', done: true } One insanely cool detail about generator functions is that when the code reaches a yield, the value is yielded but then execution is stopped until .next() is called again. This allows us to build resumable code, which will come very handy soon.
The intersection between generator functions and animations
When we create an animation we have objects in a “screen” that move, transform themselves, get added, removed, etc. But they do it while transitioning from one state to the other (for example, a square moving from the left to the right).
Let’s take as an example a square moving from left: 0px to right: 60px. We want the animation to last 1 second (at 60fps). What we would like to do is start from the initial state of left: 0px and then do a step (yield) every 16ms while moving the square to the right by 1px, and do this for a total of 60 times (1 second because we are at 60fps).
This concept of interpolation is called tweening, and we can build it using generator functions!
Tweening generator function
A tweening function takes as input an initial and final state, how many steps it should take, and an easing function (like linear, ease-in, ease-out, etc.):
function* tween(from, to, duration, easingFn) {
for (let i = 0; i <= duration; i++) {
const progress = easingFn(i / duration);
const newState = {};
for (const key in from) {
const start = from[key];
const end = to[key];
newState[key] = start + (end - start) * progress;
}
yield newState;
}
}
function linear(from, to, duration){
return tween(from, to, duration, (t) => t)
}
const tweenToTheRight = linear({ left: 0 }, { left: 60 }, 60)
for(const v of tweenToTheRight){
console.log(v.left) //0, 1, 2, 3...
} Now that we have the code to tween between two states, we just need to apply this style to an HTML element to animate it!
const wait1Fps = () => new Promise(res => setTimeout(res, 16))
const el = document.createElement('div')
el.style = 'position: fixed; width: 50px; height: 50px; background: red;'
document.body.appendChild(el)
for(const v of linear({ left: 0 }, { left: 60 }, 60)){
await wait1Fps()
el.style.left = `${v.left}px`
}
el.remove() Making it more complete
Let’s start building a small animation library! As you saw from the example before we need a few components:
- View: Which is the element where we “render” things into (in our previous example it was the body element). We want to be able to add and remove elements from the view.
- Elements: They are the individual elements that we add to the view and that we want to animate. We want to be able to create them, and transition their style.
- Tweening: The function that allows us to tween from initial to final state
- And a few other utilities which we will see afterwards
Here are some interfaces that we could implement:
export interface View {
element: HTMLElement;
add(toAdd: ViewElement): void;
remove(toRemove: ViewElement): void;
}
export interface ViewElement {
element: HTMLElement;
style: (style: string) => void;
to: (
style: Partial<NumericProps>,
duration: number,
easing?: ((t: number) => number),
) => Generator;
} And let’s now implement them!
function view(style?: string) {
const el = document.createElement('div')
if(style) el.style = style
return {
element: el,
add(e: ViewElement) {
el.appendChild(e.element);
},
remove(e: ViewElement) {
el.removeChild(e.element);
},
};
}
function el(view: View, style?: string): ViewElement {
const element = document.createElement("div");
if (style) element.style = style
const viewElement = {
element,
style: (p: string) => {
element.style = p;
},
to: function* (
target: Partial<NumericProps>,
duration: number,
easing: ((t: number) => number) = ((v) => v),
) {
const initial = numberOnly(window.getComputedStyle(element), target);
const tweenGen = tween(initial, target, duration, easing);
for (const state of tweenGen) {
Object.assign(element.style, toCssValue(state));
yield state;
}
},
};
view.add(viewElement);
return viewElement;
} You can ignore the numberOnly and toCssValue functions, they are the annoying parts to convert from CSS to numbers and the other way around.
With this boilerplate done, we have a standard way to make animations. Let’s pretend we have an animation function available. To play it, we simply do:
for(const el of animation()){
await wait1Fps()
} Let’s now make a very simple animation!
But what if we want to animate more than one element at a time? This is where the magic of generator functions really shines. Since they are resumable, we can create all sorts of utility functions to make animations easier to build.
Multiple elements at the same time
This allows us to animate multiple elements at once:
export function* all(...gens: Generator[]) {
while (true) {
const results = gens.map((g) => g.next());
if (results.every((r) => r.done)) {
return;
}
yield results.map((r) => r.value);
}
} Loop an animation
Using yield*, we can delegate to the animation generator and replay it multiple times. We pass a function that returns a generator because we want to be able to “rebuild” it as many times as needed:
export function* loop(
times: number,
generatorFactory: () => Generator,
) {
for (let i = 0; i < times; i++) {
yield* generatorFactory();
}
} Delay
Or we can delay for a few frames!
export function* delay(frames: number){
for (let i = 0; i < frames; i++) {
yield;
}
} With this we can make animations procedural, as in, make functions that return animated elements:
But we can notice that when a generator finishes executing, the element does not get removed from the view.
Using generators
This is another really cool feature in JavaScript that I wanted to experiment with: the using statement.
What it allows us to do is define an object as being Disposable, meaning an object that has a cleanup function with the key Symbol.dispose.
If you don’t know what Symbols are, you could see them as “values” that are guaranteed to be unique. The only way that a === b is true is if both a and b are the same reference to the Symbol.
You can create a new symbol like this:
const tag = Symbol("my_symbol")
console.log(Symbol("my_symbol") === Symbol("my_symbol")) // false
console.log(tag === tag) // true JavaScript also provides a few built-in Symbols that are used internally by the JavaScript engine.
One that we just implicitly used is Symbol.iterator, which is used internally by the for...of statement.
Once we have an object that is Disposable, we can use it with the using statement.
What the using statement does is cleanup at the end of the execution of the function. In more technical terms, the function located at the Symbol.dispose key is called when the object goes out of scope.
It is very similar to doing:
function test(){
const myObj = MyObject()
try{
someCode(myObj)
/*...*/
} finally {
myObj.dispose()
}
} And we implement and use it this way:
function MyObject(){
return {
[Symbol.dispose]: () => {
console.log('disposed')
}
}
}
function test2(){
using myObj = MyObject()
someCode(myObj)
/*...*/
} In our animation library, we would like to remove the element from the view once the function finishes executing. To do that, we can implement Disposable and let JavaScript handle it for us! Let’s edit the el function by adding:
function el(view: View, style?: string): ViewElement {
const element = document.createElement("div");
if (style) element.style = style
const viewElement = {
/*...*/
[Symbol.dispose]() {
view.remove(element);
element.remove();
},
};
view.add(viewElement);
return viewElement;
} And in our animation we just need to replace const with using and that’s it!
(if you are on Safari, using is not available yet so the animation wont work)
Recording animations to a video
Since our animation is resumable (we can step through it), we can also do other things between one frame and the other.
For example, we can capture the current frame and add it as a frame to a video. This way we can record the animation and download it!
For simplicity, we used HTML elements which are a bit tricky to convert to an image. Luckily, there are tools like html2canvas that allow us to do that.
If we were building a proper animation library, we’d use something like pixi.js or other 2d rendering libraries.
The end
I hope this post helped you understand how generators, using, and symbols work! If you want to create your own animations, you can try the animation editor or look at the complete source code here.