Visualizing Process with ES6 Generators
Countless hours are poured into designing functions that run in the fraction of a second. When functions execute so quickly, their ingenious implementations are not easily appreciated. Let’s slow them down, and take the necessary time to watch them work.
In this article, I use generators to visualize a function’s process and show how using process as a creative tool can have striking results.
What's a Generator Function?
Generator functions are new to JavaScript, and many people have been struggling to find real-world practical uses for them. I’m excited to show you a cool way to use them, but let’s go over the basics first. Here’s a simple Generator function:
function * myGeneratorFunction(arg) {
yield 1;
yield arg;
}
It looks a lot like a normal function except for two differences: an asterisk (*) after function, and the use of the yield statement.
Below is how myGeneratorFunction is used:
const generator = myGeneratorFunction('hello world');
console.log(generator.next().value)
// logs out 1
console.log(generator.next().value)
// logs out 'hello world'
Calling a Generator Function does not execute it right away, instead it returns a Generator object. Calling .next() on the Generator object causes myGeneratorFunction to execute up to the first yield statement, returning the value appearing after the yield keyword. Generators allow us to stop and start the execution of a function. Check out the the MDN page on Generators for more information.
Why?
Visualizing a function’s process helps when trying to understand the implementation, and can result in fascinating animations and impressive effects. Take this video visualizing various sorting algorithms for example:
- Watching the process of sorting is strangely captivating.
- The differences in how each sorting algorithm works are instantly obvious.
- What better way to interest someone in how something works, than to make how it works look cool?
Let’s Visualize!
Computers nowadays run ridiculously, faster-than-Usain-Bolt, mind-bogglingly fast. That means that functions run just as fast. To slow down the process of a function we will use a Generator. With a Generator we slow down the process so that it operates at 60 steps per second. At this speed we can watch a function do what it does best, in real time as it does it. It will be like watching the world’s fastest sprinter in slow motion, seeing individual muscles contract and relax in a single step.
For our example we shamelessly copy the youtube video above and visualize the insertion sort algorithm with a bar graph.
This is the insertion sort implementation:
function insertionSort(inputArray) {
for (let i = 0; i < inputArray.length; i++) {
const value = inputArray[i];
let j = i - 1;
while (j >= 0 && value < inputArray[j]) {
inputArray[j+1] = inputArray[j];
j -= 1;
}
inputArray[j+1] = value
}
return inputArray;
}
Below we have a function that draws an array as a bar graph on a canvas. I use the 2d Canvas API:
const c = document.getElementById('canvasEl');
const ctx = c.getContext('2d');
function drawGraphFromArray(array) {
ctx.clearRect(0,0,c.width,c.height);
const barWidth = c.width / array.length;
const barHeightScale = c.height / Math.max(...array);
array.forEach((value, i) => ctx.fillRect(
i * barWidth,
0,
barWidth,
barHeightScale * value
));
}
Now back to our regular programming. In order to slow down our insertion sort function we are going to rewrite it as a Generator function. Sounds tricky, right? It’s actually the opposite of tricky, it’s SUPER EASY. This is the rewritten insertion sort:
function * insertionSort(inputArray) {
for (let i = 0; i < inputArray.length; i++) {
const value = inputArray[i];
let j = i - 1;
while (j >= 0 && value < inputArray[j]) {
inputArray[j+1] = inputArray[j];
j -= 1;
yield inputArray;
}
inputArray[j+1] = value
}
return inputArray;
}
There are only two changes. We add an asterisk after the function keyword and add a yield statement whenever we want to draw a frame in the animation, yielding the array being sorted. With those simple changes, we’ve converted a function into a Generator function that is executed one step at a time, and yields the data we need to visualize its process. This rewrite is great because it’s unintrusive - there’s almost no chance the conversion will affect the logic of the function.
Let’s put drawGraphFromArray and our new insertionSort Generator function together in a requestAnimationFrame render loop, resulting in our finished animation:
// code from above ...
const randomArr = Array(50).fill(0).map(Math.random);
const sortGenerator = insertionSort(randomArr);
function renderLoop() {
const yieldedArray = sortGenerator.next().value;
drawGraphFromArray(yieldedArr);
requestAnimationFrame(renderLoop);
}
renderLoop();
In the final animation above, we see the bar graph go from a jagged mess to a beautiful staircase. To achieve this, we ask our insertion sort to operate at one step per render loop with .next(). requestAnimationFrame makes sure our render loop runs at 60 frames per second, the perfect speed for a smooth animation.
Insertion sort is a simple example of what we can do. Let’s see if we can’t get a bit more crazy with Generators.
Process Solves Problems and is Creatively Cool
Seeing the series of steps taken toward a final result can add something special to a piece of work. When creating something, I think about the parts of the process that would be interesting to someone looking at the final product, and I show those parts off. Below are two interactive canvases that show off the process of rendering in order to make the user feel good.
I made a program a while ago that allows a user to explore into the Mandelbrot set. The problem with the program is that it takes at least a full second to render. As a result, user input feels jerky and unresponsive. To solve this problem, I modified the program to render at a low resolution (which renders way faster) while the user is interacting. As the user refrains from interacting the program takes the opportunity to render at progressively higher and higher resolutions.
A Generator is used to help with progressively rendering at increasing resolutions. The Generator yields squares, starting with 4 consecutively yielded large squares covering the area of the image. Followed by 8 smaller squares covering the area, then 16 even smaller squares, then 32, and so on. The program takes these squares and fills them in with color. I limit the program to filling in 700 squares with color per frame. This way rendering takes less than 1/60th of a second, resulting in user input that feels responsive and fluid. As an added benefit, the Mandelbrot set being rendered at increasing resolutions is a cool effect.
Here’s another example that uses a similar idea. With an image instead of the Mandelbrot set, and triangles instead of squares:
Rendering an image is easily possible at 60fps, so there was no need for the fancy triangle animation. Exaggerate the process of rendering just because it looks awesome.
The End of the Process
Process is going on all the time, but it happens so fast that it can be hard to work with. Generators make process malleable, giving you access to something that’s always around, but rarely seen. Hopefully I’ve got you thinking about process and Generators in a new way. Thanks for reading.
If you are still interested in Generators, check out this other article: JavaScript Generators for People Who Don't Give a Shit About GettingStuffDone™