ok so that animation took way longer to finish than i was expecting, not just because of details i kept missing, but also because of some technical limitations that make it a miracle that this thing even works.
like, the animation does not do justice to the amount of Crime™ lurking within. so, i figured i'd write these
confessions of a computer criminal
to share some of the Fun™ i had making this.prologue: what am i even looking at here
If you've never seen an animation like this before, it's a visualization of a complex Fourier series that traces out a loopy curve. There are some great videos by 3Blue1Brown and Mathologer that dive into the math with some nice visualizations, or if you just want an executive summary:Furry-what?
You know how sound is a wave? You can have multiple sounds going on at the same time, like playing a chord on an instrument. They combine into a single wave, whose strength at a given time is the sum of the strengths of the waves you're combining.But if you start with a wave that's already a combination of others (like most sounds, unless you listen to sine waves for fun), can you pull them apart? yes, you can!
That's what the Fourier transform is all about. You can take a wave and a particular frequency, and it'll tell you how much a pure sine wave of that frequency contributes to the overall one.
The snappy way of putting this you'll sometimes hear is that a Fourier transform converts a signal from the time domain to the frequency domain.
I know what a Fourier series is, but where's the wave?
If you think of the points on the outline of the drawing as complex numbers, then you've got a continuous, periodic function—just like a wave!Do Fourier transforms work on complex-valued functions? They sure do! In fact, they work really nicely! If you learned Fourier transforms in the context of real-valued functions, you might have seen the output be given as a sum of both sines and cosines of each frequency, since each pure frequency can be out of phase with the others. In the complex world, this isn't necessary. Thanks to Euler's formula, eiθ = cos(θ) + isin(θ) which traces a circle, and the coëfficients also being complex numbers, which encode rotation and thus the phase shift, the transform is just a sum of these circles rotating at various frequencies.
One caveat here is that with complex valued functions, you do now have to worry about negative frequencies—you can see in the animation that some of the circles spin in opposite directions.
The plan is to take some lines, attach them tip to tail, and rotate them at various speeds. By sampling some points along eggbug's outline and doing a Fourier transform on them, we can figure out how long each of the lines need to be and what angle they start at so that the tip of the last one traces out this drawing of eggbug.
And then animate it with just HTML, CSS, and SVG.
Simple, right? How hard can that be?
1. right, simple!
Using html does actually make some parts of this simpler. Since divs inherit positioning properties from their parent, we can just nest them and join the lines tip to tail, and as one line rotates, it brings the next one along for the ride. No extra calculation is needed from us to figure out where the start of each line should be at any given time, which would have gotten very overwhelming—we'd have needed to follow the whole path up to that point.Here's all we have to do: notice how the inner circle only needs to be positioned so that its centre is at the end of the line. Its movement is taken care of automatically.
Great, the proof-of-concept works! The real thing is just nesting a whole bunch of these, plus scaling, rotating, and setting them to spin at the right speed. Yep, it's smooth sailing from here!
2. i can be ur angle...or yuor devil
The Fourier transform gives us a coëfficient to apply to each of the rotating circles. Since this is a complex number, it tells us not only the size of the circle, but its starting angle as well. CSS has a function to rotate things, so all we have to do is add it to each circle's styling.Oh. Oh no.
Right ok we have to talk about CSS animations.
CSS doesn't have a built-in "make this things spin around" function, its animation system is much more general. You start out by defining keyframes. These define what a property's value should be at that point in the animation, and the browser will interpolate the values in between. The duration of the animation isn't part of the declaration, that's set when it's applied to an object.
Cohost's CSS provides a spin animation, and it's defined like this:
@keyframes spin {
to {
transform: rotate(360deg);
}
}What this means is that the object's angle will move to 360°, and finish there, from wherever angle it happens to start at. If the animation is looped, it'll reset to its starting angle, causing that jump in the example above.
And just to point this out, whatever duration we give an animation is how long it takes to reach 360°, not the duration of a full turn. If we give the same duration to objects starting at different angles, they rotate at different speeds to finish at the same time.
Bonus: I said the object's angle moves to 360°, not increases to 360°. How come? :)
Alas, if we can't animate a complete rotation from an arbitrary starting angle, the spin animation isn't gonna work out. In theory, we could generate an animation for each circle that starts and ends at the correct angle, or...
As I alluded to in one of those dropdowns in the prologue that you could've skipped if you know your maths, these starting angles represent the phase shift of the circles.
When you apply a CSS animation to an object, you can also specify a delay before the animation starts. Even better, this delay can be negative, and the animation will start partway through! So all we have to do is figure out how far into the animation each circle has to be at to reach its starting angle, and shift their phases that way.
Perfect! Both these circles make a full turn, but from different starting angles.
3. honey i shrunk the divs
To make the animation scale with window size, the circles' sizes are expressed as percentages of the size of the thing containing them. You can see in the example wayyy back in Part 1 that both circles have their sizes set to 50%, but one is smaller than the other. That one's actually half the size *of the first circle*, which is itself half the size of the image, so it ends up being 25% of the size of the image.Getting all the circles the right size is easy to do if you just keep this in mind. If we want a chain of 15 circles, each 5% of the image size, all but the first need to be 100% of the previous one's 5% of the image.
oh darn it.
By the way, this here is a testament to the power of minimal reproducible examples. When the positioning in the animation was way off and it was missing all the points, I spend way too long checking over the math instead of making sure the sizing worked the way I thought it did. The effect shown here is hard to notice with a couple of large circles, or a bunch with effectively random sizes. But a very simple test case like "these should all be the same size" makes it very easy to notice that something has gone wrong.
Thankfully, with the problem identified, browser dev tools make it easy to see what's going on. My browser tells me that the displayed size of the first circle is just shy of 30 pixels across. The second is 28px, the third is 26px, and so on. Each one loses 2 pixels compared to the last. It's also helpful enough to show exactly why: the borders. Each circle has a 1px thick border, and that eats into the space that the next one considers to be 100%.
Knowing what the problem is makes it easier to solve. Extracting the border from the elements that nest means there's no need to fiddle with its sizing and alignment.
At the small cost of an extra div per circle, we've got them at the size we want.
4. maybe i'll be tracer
That settles the problems with the spinning circles, but there's still another part to this animation: tracing out eggbug's outline.The way to do this is actually hilarious to me.
After creating an SVG of eggbug's outline, it can be given a dashed border, like the circles I've been showing so far. SVG gives you a lot of control over how the dashes work, most importantly setting the lengths of the dashes and gaps between them, and choosing where in that pattern to start from.
If the dash length is equal to the outline's, you'll see a fully solid border. Add a bit of offset, and a gap will appear. Larger offsets will blank out more of the outline, and since the gap length is also the same as the outline's, you can eventually erase the whole thing.
If you were to do this backwards and gradually change the offset from the outline's length down to 0, it'd look like the shape is being drawn. Keep going into the negatives, and it'll start being erased.
We just need a way to change the value of this property over time, and we just so happen to already have one: CSS animations.
Yep, SVG properties can be controlled and animated with CSS. I love that.
There's a catch though. Of course there's a catch.
Notice how the line drawing speed is constant, respecting the physical distance between the points. The second line segment is twice as long as the first, and takes twice as much time to be drawn.
Recall, I started with some sample points from eggbug's outline and did a Fourier transform on them. As far as that math is concerned, those points come from reading a signal at times with constant gaps. If two points are close together in space, then the signal must change slowly between them. If they're far apart, it must change quickly. Regardless of how far apart the points are, it takes the same amount of time to go between them.
Because of this, even if the animations on the circles and the outline start and end at the same time, there's no guarantee that the Fourier series' value (indicated by that blue tracer dot) will hit the reference points at the same time as the outline does.
Once again, knowing is half the battle: If we need each line between points to be drawn in the same amount of time, we can just split the path up into individual lines and animate them individually.
This does introduce some new problems, though: each line needs to start being drawn only when the other one is done. I wanted a looping animation where the outline gets erased, so each line needs to stay drawn until its turn to disappear.
The second is easy enough to solve. Taking a page from the path-drawing trick, the dash length can be the length of the whole shape rather than just the line. The line will end up filled in for a whole cycle, then blank for the next.
The second can in principle be solved with animation delays again, but I went with a different approach.
For these dashed outlines, the dashes and gaps don't have to be the same length and in fact you can have an arbitrary sequence of lengths that it loops through. What I did amounted to chopping off some fraction of the blank space corresponding to how long a line needed to wait before being drawn, then moved that in front of the filled line. Later lines would still be animated right from the start, but they wouldn't reach the filled portion of their dashes before their turn.
5. i have a line, i have a circle... unh! 'puter crime!
With both animations' issues all sorted out, all that's left to do is throw both of them into a post—well, almost. Cohost doesn't actually allow you to embed SVG tags into a post, but there is a workaround (which Prechoster makes use of): you can make it a background image by passing it to CSS's url function.
There is a cost, though. Remember how CSS can be used to style SVG elements? This workaround breaks that, since the SVG isn't part of the DOM anymore. Fortunately, SVGs can have their own style tags, and the line-drawing animations can just be moved over there.
And at this point, tragedy strikes.
Try hitting that rerun button in the bottom-right a few times. This might depend on your browser, but for me at least, the circles reset to their original positions while the rectangle just keeps chugging right along.
I have no idea why this happens, my best guess is that the SVG background gets cached and its animation timer is relative to when it was first loaded, while the circles' timer starts on page load.
But regardless of why it happens, it just breaks the animation. You wouldn't even need to refresh the page for it to get desynced, sometimes it would just start that way!
Maybe there's a way out of this. There's another way to animate SVG elements, and that's the in-deprecation-limbo Synchronized Multimedia Integration Language (SMIL), made up of XML-esque tags that go into the also XML-esque SVG elements. SMIL elements can have an attribute that controls when the animation starts, and it's more flexible than just an offset. It can be set relative to the start or end of another element's animation, to be triggered by keypress from the user, to a timestamp, or triggered by events on other elements, like mousing over something or scrolling a text box.
If I can get the animation to start when the page loads, that should get it to sync up with the circles' CSS animations, right? I spent a lot of time looking for ways to make this work, but it just wasn't happening. Much like CSS animations can't be used on this SVG because it's a separate document and not part of the DOM, there wasn't really any way to get the SVG to wait for events from the page.
And so here I was, after grappling with advanced mathematics, wrangling vector graphics, and getting document layouting pixel-perfect. Defeated by that most ancient and unassailable of foes, Time.
After the struggle to forge these two parts, the Line and the Circle, all was undone at the right at the end—all that had been left to do was just put them overtop each other.
Perhaps I had flown too close to the Sun.
Perhaps such an abomination as this was never meant to be.
6. a new hope
And then I got an idea.
A wicked, terrible idea.
If you can have CSS inside of an SVG document...
could you have HTML too?
If I could just stick the circles in the SVG... wouldn't that fix the timing? They'd be part of the same document, wouldn't they?
Turns out, you totally can. And the cherry on top? If you do, your HTML needs an XML attribute to reference the XHTML namespace.
After all this, that is such a beautiful little bit of jank.
I think this calls for a bit of maniacal laughter.
conclusion: it's alive!
Despite all the troubles I ran into that took this from a moderate-effort shitpost to consuming my vacation, it was a fun learning experience. All the bits involved—Fourier series, CSS animations, div positioning—are things I thought I had a good grasp on, but this project really pushed the limits of my understanding.I'm pleased with how this turned out, and I look forward to doing more computer crimes in the future.
If you read all the way here, thank you! Writing this followup post was also pretty fun, and I hope you got something from my pain. I've cleaned up the Jupyter notebook I was using for the math and code generation, so if you want to know even more about how this animation was put together, go ahead and have a look!
epilogue: revenge of the sync
If you saw the animation when I first posted it, you might have caught it still falling out of sync occasionally. It was hard to reproduce the issue, so fortunately it didn't ruin it that badly.
Here's what I think happened: after experimenting with (and giving up on) using SIML to sync up the animations, I had left it there when I deçode to just move the HTML in. This got them to start at the same time, but presumably CSS and SIML animations run off of different clocks, that can fall out of sync.
I realized a while after posting that I could just go back to animating the lines with CSS again, so I went ahead and did that, which seems to have solved the issue.
Mercifully, this second boss phase was much easier :)
