A Spirograph is a geometric drawing toy that was first sold in 1965. They can create an amazing variety of elaborate spiral drawings. The technical name for the shapes created are hypotrochoids and epitrochoids. I based this code on how a spirograph works which essentially comes down to 2 circles rotating in conjunction.
Here’s the actual dweet running live. Because the designs are so small and numerous it looks best in fullscreen mode. You can play around with the code if you like while we examine how it works. The maximum limit for Dwitter programs is 140 bytes and this program uses all of them.
Like many dweets, this is a one liner, so our first step is to format the code so it’s easier to read. I just added some new lines and moved the fillRect to the end.
for( i=144; q=(8+t>>3)*i--; ) t%24 < 1? c.width |= r=n=> S(q**n)*9|0: T=f=> 5*r(4)*f(r(3)*t) + 9*f(r(2)*t) + 60, x.fillRect( i%16*120+T(S), (i>>4)*120+T(C), 2, 2 )
for( i=144; q=(8+t>>3)*i--; )
This for loop header is iterating through every Spirograph design on screen, there are 144 of them (9*16). The loop condition statement (q=(8+t>>3)*i–) decrements i while setting up q to be a hashed value which will be used as a seed for random number generation.
We need q to be an integer that changes every 8 seconds (8+t>>3) and is different for each value of i. Dwitter passes t into our function as the current elapsed time in seconds. The >>3 is a bit shifting operation used here to divide by 8 while also converting to an integer. The 8+ part is because otherwise q would be 0 and draw nothing for the first 8 seconds. Notice that q will only be 0 when i reaches 0, otherwise this technique would not work.
In the original dweet the fillRect is stuffed in the 3rd part of the loop header to save a comma. That’s the last part of the loop to execute so it can be moved to the end to make the code a little more legible.
t%24 < 1?
The ternary conditional operator aka ? is often used in place of an if statement because it uses less space and is more versatile. In this case it’s really just an if/else block. This conditional will be true for the first second out of every 24 seconds. I chose 24 because the random seed changes every 8 seconds so 3 different spirographs will overlap before the canvas is cleared.
c.width |= r=n=> S(q**n)*9|0:
The first half of our conditional is to clear the canvas which is stored in the variable c. Any operation on the width or height will clear the canvas. Doing c.width|=0 is a common method of clearing it without changing the size. In this case we are oring with a function, which seems odd but it has the same effect as oring with 0. The canvas will be cleared and at the same time we can define the arrow function r.
The function r is a mini random number generator. It takes a single parameter n which is hashed with the seed q we created earlier by taking it to the power of n. We then get the sine (defined by dwitter as S) of this which will be a randomish number between -1 and 1.
Using the chaotic properties of an integer taken to an exponent and passed to a trig function is a good way to make seeded random numbers using only a tiny bit of code. To help demonstrate this, I generated the small image to the right where each pixel brightness is controlled by S(i**n) where n is 3. I wouldn’t use it for bank transactions, but as you can see, it is random enough for creative purposes.
Taking the result of sine we multiply by 9 to bring it in the range -9 to 9. Then we apply |0 which is a tiny way to convert floats to integers. We want the result of our random number generator to be an integer because the Spirograph designs look best when the rotation parameters have a low common multiple. Without that the spirograph will take too long to connect back to it’s starting point. In other words, we want the period of the spirograph to be fairly short and using single digit integers is a good way to accomplish that.
T=f=> 5*r(4)*f(r(3)*t) + 9*f(r(2)*t) + 60,
Finally, we have reached the actual spirograph equation. A spirograph is basically just one circle rotating inside another circle and though it may not look it at first, that’s what this function does. It’s another arrow function that again takes only 1 parameter. Single parameter functions are ideal for minified code because the definition doesn’t require parenthesis around the parameters or commas between them.
The parameter f is used in an unusual way, it is going to be passed in as a function. We will be calling this once for each axis and passing in either sine S or cosine C depending if it is for the X or Y axis. We also add 60 to center the spirographs in their 120×120 grid cells.
The mini random number generator r is called 3 times to randomize the spirograph. Each time a different value is passed in for n, so the random seed will be different. The random values are used to create 2 circles with random radii and rotation rates. One of the circles has a fixed radius to eliminate the need to call r a fourth time.
Here is a more generalized form of the spirograph equation we are using that may help show how it works…
X = radius1*sin(turnRate1*time) + radius2*sin(turnRate2*time) Y = radius1*cos(turnRate1*time) + radius2*cos(turnRate2*time)
There is another little trick going on with this function you may not have noticed yet. Remember from earlier that the conditional will be true during the first second every 24 seconds. Unfortunately that also applies to the start of the program. So, this function will not be defined for the first second!
To solve this problem T was chosen specifically chosen because dwitter already defines T to be Math.tan on startup. So there is no error because T is always defined. For the first second the code will call Math.tan instead of our new T function, but it’s not a problem since nothing draws than anyway. I try to avoid doing this kind of obfuscation but sometimes it is necessary to save a few bytes and make the dweet fit.
x.fillRect( i%16*120+T(S), (i>>4)*120+T(C), 2, 2 )
Now that we’ve set everything up, this is the easy part. We are going to call fillRect on the canvas context (x) using our spirograph function T.
Here is where the loop variable i is split into X and Y grid values. We could loop through each axis separately, but combining them into a single loop saves space. To get X we simply mod by 16 because there are 16 spirographs per row. For Y we need to divide by 16 and cast to an integer. Conveniently >>4 does both of those things in less space. Because the grid cells are 120×120 we multiply X and Y by 120.
The T function has already done the heavy lifting for us. We can just call T(S) and T(C) for the X and Y components respectively to generate the spirograph. The fillRect draws a square of size 2 because I found 1 to be a bit too small and 3 too big.
In fact all of the “magic numbers” used in this dweet were carefully chosen. For example dwitter sets up the canvas to be 1920×1080 so 120 is evenly divisible by both. Also, 1920/120 is 16 and because that is a power of 2 we were able to use bit shifting operations to save space.
That brings us to the end of the program. Let’s take one final look at the original dweet…
It’s a tiny bit smaller then the version we analyzed, just enough to fit in the 140 byte limit imposed by dwitter. Everything has been packed into a single line to eliminate commas and semicolons. So, I ask you, which is more beautiful, the images this produces or the code that makes them?
I made a animation to help show how the spirograph equation works. It shows both circles rotating in tandem to create a 5 sided star. The intersection point marked in yellow is where the line is drawn from.
There’s much more fun to be had with these Spirograph designs. To celebrate getting to 500 twitter followers I generated this epic image, with one spirograph icon for each follower. The math is basically the same except I used a black background and a random color for each spirograph layer. Thankfully no one actually counted because I screwed it up and cropped out the top and bottom rows so it’s only 468 glyphs… oops!
That’s all I have for today. I hope you learned something new. Feel free to build on my code and create your own Spirograph experiments. Thanks for reading!