Making recursive tangent circles inside circles and animate them
Some people find circles a fascinating shape. Circles are surrounded by mathematical paradigms. For example: did you know that even if circles are closed curves they still are ruled by one of the most interesting irrational numbers, Pi, which is a transcendental number?
And if you are a fan of circles, why not having several of them, recursively?
In fact, the image above was made by modifying the parameters of a small coding project that did just that. This is what Johan Karlsson, one of my favorites artist in Codepen, did for one of his series for the 2021 edition of Genuary: “Recursive Circles III”.
Questions that I had when looking at his project were:
- How did he organize his code?
- How did Johan Karlsson manage to get those circles inside other circles?
- How did he make the recursion?
- And how did he make them rotate in harmony?
In this post I will try to re-verse the code to get some answers to my questions. I will include quick visuals using a carousel and also the canvas API.
The Code
There are many other more advanced projects about recursivity and circles, but I chose Johan Karlsson’s one because I like his projects and the way he codes. He is usually on the search of striking patterns based on few geometric forms, all with very simple code using vanilla Javascript.
The whole implementation for this project was truly simple: A very simple css, a one-line html and a javascript of just 58 lines of prettified code, including empty lines.
("Simple", if we ignore all the code made by other coders to make all the many APIs, OS, etc. that allow his and our codes working, of course... :) )
Let’s put some attention to the JS code.
Code Composition and Organization The code consists of:
- A canvas instantiation function
- Canvas circles using the
arc
method - Some lines of code to control for the position and rotation of the circles
- Recursive calls of the drawing function, with a clear termination
- A
draw
function
Johan Karlsson started his code with an instantiation of the canvas:
let canvas;
let ctx;
let w, h;
let shrinkFactor;
function setup() {
canvas = document.querySelector("#canvas");
ctx = canvas.getContext("2d");
resize(); //<-- resize is called inside setup...
window.addEventListener("resize", resize);//<--... and linked to a global event.
}
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
}
The code above includes the declaration of some high-scope variables, the initialization of the canvas instance and the resize function. The code doesn’t have anything extraordinary: it is really standard, simple and organized.
The following is where the animation takes place:
function drawPattern(x, y, r, angle, iteration) {
if(iteration < 0) return;
let newR = r * shrinkFactor;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.stroke();
let r2 = r - newR;
let x1 = Math.cos(angle) * r2 + x;
let y1 = Math.sin(angle) * r2 + y;
drawPattern(x1, y1, newR, angle * 1.1, iteration - 1);//<-- Magic number: angle*1.1
let x2 = Math.cos(angle + Math.PI * 2 / 3) * r2 + x;
let y2 = Math.sin(angle + Math.PI * 2 / 3) * r2 + y;
drawPattern(x2, y2, newR, angle * 1.2, iteration - 1);//<-- Magic number: angle*1.2
let x3 = Math.cos(angle + Math.PI * 4 / 3) * r2 + x;
let y3 = Math.sin(angle + Math.PI * 4 / 3) * r2 + y;
drawPattern(x3, y3, newR, angle * 1.3, iteration - 1);//<-- Magic number: angle*1.3
}
A magic constant here is the argument related to iterations
, which control the depth of the recursions, and have a particular effect on performance (relevant!). He also make use of “magic numbers” to modify the values of the angles when recursively calling the function.
The last function he declared was the draw
function, accepting an argument (a time-based one):
function draw(now) {
requestAnimationFrame(draw);
ctx.fillStyle = "white";
ctx.fillRect(0, 0, w, h);
shrinkFactor = 0.463; //<-- Magic number: shrinkFactor = 0.463
let r = Math.min(w, h) * 0.475; //<-- Magic number: (...)*0.475
let angle = now / 2000; //<-- Magic number: now/2000
drawPattern(w / 2, h / 2, r, angle, 6);
}
Notice that in the draw function there are more configurations. In particular, there are some “magic numbers”:
- the
shrinkFactor
, - one associated to the value of
r
, and - another associated to the
angle
variable.
We will discuss them later.
I would like to finish this section of the post by highlighting that such a simple and small script shows some Johan Karlsson's good practices, particularly in terms of code organization and readability. His approach seems a minimalistic one, even in his choices for the naming conventions.
In general the code structure selected for this project, as well as other projects by the same author in Codepen, reminds me a bit the typical organization pattern I have found in many other canvas projects. In particular, it appears to be inspired very much on the typical P5.js pattern:
//variables for color change
let redVal = 0;
let greenVal = 0;
//variable for sun position
let sunHeight = 600; //point below horizon
function setup() {
createCanvas(600, 400);
noStroke(); //removes shape outlines
}
function draw() {
// call sky function
sky();
// a function for the sun;
// a function for the mountains;
// a function to update the variables
}
// A function to draw the sky
function sky() {
background(redVal, greenVal, 0);
}
(From P5.js website tutorials)
Building Steps
By evaluating the final product, you could almost discern from the code the problems that Johan Karlsson had to solve. We could separate the workflow in 3 different problems, or steps:
- The mathematical problem of inserting circles inside circles.
- The recursive function.
- The animation.
Maybe because the rush to get his daily project done for the creative coding month, the no-point-to-re-invent-the-wheel, and why not, Johan Karlsson left a few things unexplained in the code. Instead, he resourced to the use of magic shortcuts in the form of pre-calculated values.
But we could offer a more insightful explanations to how Johan Karlsson managed to solve his problems if we revealed the origins of such values.
Let's start by revealing the origin of what I think it was the most important of all of them: the shrinkFactor
.
shrinkFactor
or solving the “three circles inside a circle” problem
The project is based on a repetitive pattern consisting in inserting three circles inside a circle. But probably even before starting, Johan Karlsson might had to solve how to insert those circles in the first place.
To do that in a consistent way for all the recursions, Johan Karlsson found a relation between the radius of the outer circle and the radii fo the inner circles at every recurssion that stayed the same, and called it the shrinkFactor
. Now, instead of coding the solution to get the value at each recursion, Johan Karlsson solved the recursive pattern with a single constant.
But from where did he get that number? He might have used the result of a mathematical solution.
Well… it turns out that it is a very common secondary-school problem. You can find exactly the same problem and its geometric solution by following this link.
Let’s follow step by step the reasoning of the solution providing on the link:
For our specific case of three kissing circles, the solution to $$ \frac{R}{r} $$would be:
$$ \frac{R}{r} = \frac{(3 + 2 * \sqrt{3})}{3} $$
which gives the relation between the radius R of the containing circle against the radius r
of any of the three inserted circles. For r = 1
, the value of R
is (approx) 2.155.
Inverting the proportion gives:
$$ \frac{r}{R} = 0.464 $$
which is the value used by Johan Karlsson, probably adjusted to 0.463.
There is another way to get the same relationship by using the Descartes’ Circle Theorem, where, for this particular example, reduces to $$ (\frac{3}{r} - \frac{1}{R})^2 = 2*(\frac{3}{r^2} + \frac{1}{R^2}) $$
Now that he got the radii, he needed to get the position of the centers. The previous solution to the problem of the three circles inside a circle not only provides a solution to the relation between the radius of the outer circle and the radii of the inner circles. It also provides a solution to the proportional distance between the center of the outer circle and the centers of its inner circles:
$$ dist_{Cc} = R - r $$
(where C is the center of the outer circle and c the center of an inner circle)
With that distance is now possible to find the centers of the inner circles. Those are the coordinates calculated as (x1,y1)
, (x2,y2)
, and (x3,y3)
in his code. This is done with a code that is repeated for each center. Here is one of them:
let r2 = r - newR;
let x1 = Math.cos(angle) * r2 + x;
let y1 = Math.sin(angle) * r2 + y;
Where r2
is that distance, r
is actually the outer circle radius and newR
is the radius of one inner circle.
r2
might be a misleading naming though, as it is not a radius.
drawPattern
function: centers, radii, angles and a couple of other magic numbers
Once he got the constant relations necessary to calculate the inner circles, it should have been easier for Johan Karlsson to draw them.
Every time the function was used, the newR (the radius of the inner circles) was calculated.
Centers were found from the angles, which were 120 degrees (2*PI/3 radians) apart of each other, at a distance of r - newR
from the center.
The iteration
controlled the depth of the recursion (set to 6 in the original project). Notice that increasing the number of iterations will affect seriously the performance. Johan Karlsson might have found that six was the best number.
But then he also multiplied the angles by other magic numbers
at each self call of the drawPattern
function. Those numbers were made up and only controlled for different rotation speeds of the circles.
angle
is actually the value of the angular speed: the magnitude of the arc that the circle should travel at each frame. The larger that value, the faster it goes.
The “magic numbers” used by Johan Karlsson affected the rotation speed of the several different circles, helped by where he placed the value in the different recursive chains.
draw
function: setting up the size of the first outer circle and the initial speed of rotation
The “magic numbers” used in the draw
function are more mundane and don’t require a lot of discussion. One was used to fit the outest circle to an appropiate size on the screen (the “0.475”). The other one, the 2000 in let angle = now / 2000
, was more a way to initialize the angular speed of the first inner circles.
TADA...!
So. What did we learn from this code?
This was a very simple but entertaining project! And what’s more: it didn’t take much to prepare. So it is likely that there will be more posts like this one in the near future.
Particularly because, like this project by Johan Karlsson, there is a lot we can learn from just a few lines of code. And a good bunch of tools we can use to help us reveal their insides.
Something that it would be nice to explore further is Descartes's Circle Theorem is one of the many possible cases of circle packing in a circle, which can be a hard problem to solve.
Which other patterns can be built from the project by Johan Karlsson when trying other types of packings? Which other recursive patterns? Can we combine different types of patterns to generate other unexpected forms?
And if you are interested of the computational solution to the Descartes' Theorem for just 4 circles of different radii, here a link that might interest you (several implementations, including Python3 and Javascript): https://www.geeksforgeeks.org/descartes-circle-theorem-with-implementation/
I wish you happy coding!
The carousel used for this project is the same created by Jemima Abu for Envato TutsPlus in Codepen, with small modifications.
The rest of the code for this project is the same by Johan Karlsson’s, with added controllers using the dat-gui package.
I used Inkscape to create the svg's the images.
This post is an edited copy of one from personal blog on Github Pages, re-versing.
Subscribe to my newsletter
Read articles from evaristo.c directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by