The right side of the homepage has a canvas that draws a different pattern on every visit. It’s about forty lines of JavaScript, no libraries. It is also, in a checkable sense, a picture of this repository.
what it draws
Five sine wave ribbons, each a color from the current theme palette: --accent, --secondary, --tertiary, --muted, and --outline. They’re stroked across the full canvas width, layered with some transparency. A scatter of small random dots fills in the gaps for texture.
Because the colors come from CSS variables already set on the page, the drawing always matches whatever theme is active. Switch to dark mode; the ribbons shift. The live scheme from my desktop pulls through the same way.
everything has a spectrum
The useful idea here is older than computers and stranger than it sounds: any sequence of numbers whatsoever can be written as a sum of sine waves. Not approximately, and not only for things that look wavy. Exactly, and for anything.
A pure tone is one wave. A square wave is a particular infinite stack of them. A photograph, a stock chart, a seismograph, a list of how many times someone touched a git repository — all sums of sine waves, differing only in which frequencies show up and how strongly.
This is worth sitting with, because the intuition runs the other way. It’s tempting to think a signal has to look periodic before you can talk about its frequencies. It doesn’t. Randomness isn’t the absence of frequency content; it’s the presence of all of it at once, in roughly equal measure and with unrelated phases. White noise is called white for exactly that reason — like white light, it’s every frequency together. There is no such thing as a signal without a spectrum. There are only signals whose spectra are boring.
So the question is never whether a pattern decomposes into frequencies. It’s what the decomposition looks like — whether a few components carry most of the weight, or whether it’s flat and every frequency is as good as every other.
where the frequencies come from
Sine waves are the one shape in mathematics that arrive with a ready-made theory of what they mean, which makes it a waste to draw them as decoration. These ones are measured.
At build time the site runs a Fourier transform over its own commit history and pulls out the strongest frequencies. Those become the ribbons.
To explain what that means, it’s easier to look at the raw material first.
the raw material
Every commit in this repo, one dot per day, height being how many commits landed that day. Zero days included — the gaps are part of the signal.
The curve through them is not a fit in the statistical sense. It’s a sum of sine waves, and the slider controls how many are allowed to participate. Drag it. Hover any dot for the date it stands for and how many commits landed that day.
At zero frequencies the curve is a flat line at the mean — the only thing one number can tell you. At one, a single slow swell across the whole history. Somewhere in the middle it starts tracking the shape of the record without committing to any particular day. At the far right it stops approximating and simply is the data: every dot pierced, to within floating-point noise.
The box underneath is the function actually being drawn, written out term by term. At the low end it’s short enough to read. Drag the slider up and it grows one cosine and one sine per frequency, until it’s the entire sum. Copy it into Desmos or a graphing calculator and you should get the curve above, dots and all — which is the point. You shouldn’t have to take my word for what the canvas is doing.
That last property — exact at the maximum — isn’t a coincidence or a matter of trying hard enough. It’s forced.
the counting argument
If the record is N days long, that’s N numbers. The transform turns them into N numbers of a different kind: about N/2 amplitudes and N/2 phases, plus the mean. No information is created and none is lost — it’s the same data in a different basis, the way a vector is the same vector whether you write it in x/y or in some rotated frame.
So of course the full set of frequencies reproduces the data exactly. You handed the transform N degrees of freedom and asked for N constraints back. The interesting question was never whether it fits. It’s what the partial sums look like on the way there.
the transform, written out
For each frequency k — meaning a wave that fits exactly k times into the record — we ask how much of that wave is present by multiplying the signal by it and adding up the result:
X_k = sum over n of x[n] * e^(-2*pi*i*k*n/N)
which, split into the parts a computer actually evaluates:
Re(X_k) = sum over n of x[n] * cos(2*pi*k*n/N)
Im(X_k) = sum over n of -x[n] * sin(2*pi*k*n/N)
If the signal wiggles in step with that wave, the products line up positive and the sum grows. If it doesn’t, they scatter either side of zero and cancel. That cancellation is the entire mechanism. The magnitude sqrt(Re² + Im²) is how much of that frequency is in there; atan2(Im, Re) is where its crests sit.
Going back the other way is the same operation with the sign of the exponent flipped, truncated at whatever K the slider is set to:
x̂(t) = X_0/N + (2/N) * sum from k=1 to K of
[ Re(X_k)*cos(2*pi*k*t/N) - Im(X_k)*sin(2*pi*k*t/N) ]
That expression is exactly what the copy box prints, with the coefficients evaluated and rounded.
Two details that bite if you skip them. The factor of two is there because a real-valued signal splits each frequency’s energy evenly between +k and -k; folding the negative half onto the positive one doubles it. But the very top frequency, k = N/2, is its own mirror image — no distinct partner to fold in — so it must not be doubled. Miss that and your reconstruction is subtly wrong only at maximum detail, which is the least convenient place to find a bug.
The other detail: t above is continuous, not an integer. Nothing stops you evaluating the sum at half past day forty. That’s what draws a smooth curve instead of connecting the dots, and it’s also where the honesty problem lives.
what happens between the dots
Push the slider to the maximum and watch the curve rather than the points. It passes through every sample perfectly and, in the gaps between them, dives below zero and overshoots well past the busiest day in the record.
Negative commits. The function is exactly right everywhere I measured and nonsense everywhere I didn’t.
This is ringing — the same phenomenon as Gibbs overshoot at a jump discontinuity. The data is spiky and mostly zero: a day with dozens of commits sits next to days with none. Reproducing a spike that sharp requires the high frequencies to arrive with large amplitudes and near-perfect cancellation between the samples, and that cancellation is only guaranteed at the sample points. Between them the individual waves are still swinging at full size, and they don’t politely agree to stay in range.
Which is a decent argument against reading too much into a perfect fit. The exact reconstruction is the least useful setting on the slider. Somewhere in the low double digits of frequencies you get a curve that’s wrong about every individual day and roughly right about the shape of the whole record — the burst when the site was first built, a long decay, a low hum of automated theme-sync commits after. That one actually tells you something.
back to the ribbons
The homepage takes the five strongest components and gives each one a color. A component that fits k times into the repo’s lifetime draws k cycles across the canvas, so the width of the drawing is the width of the history. Each ribbon’s height is that component’s amplitude relative to the strongest.
Worth being honest about how firm those five are. The first two dominate and reflect something real: the launch burst and its decay. Past that the spectrum is close to flat, and the ranking among the rest isn’t robust — the gaps between consecutive ranks are small enough that a few weeks of new commits can reshuffle the order. They are the strongest components, which isn’t the same claim as being meaningful periods, and I’d rather say so than let a picture imply otherwise.
What changes per visit is the phase. Each ribbon’s frequencies get a random offset between 0 and 2π on every draw:
const phase1 = Math.random() * Math.PI * 2;
const phase2 = Math.random() * Math.PI * 2;
for (let x = 0; x <= W; x++) {
const y = yBase
+ Math.sin(x * freq1 + phase1) * amp1
+ Math.sin(x * freq2 + phase2) * amp2;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
That split is deliberate. Phase is the part of a spectrum your eye is least attached to — randomising it while holding frequencies and amplitudes fixed produces a different picture every time with identical frequency content. Signal people call the result surrogate data. It means the drawing is never the same twice and is always this repository.
Each ribbon carries its own partial plus the next one at 45% amplitude, which gives the waves their slight pinching and spreading rather than uniform oscillation. Both of those frequencies are measured; only the offsets are chance.
Each ribbon is one long path: moveTo the first point, lineTo every subsequent pixel across the width, then stroke. No segments or beziers involved; just a point per pixel.
the dots
After the ribbons, forty dots are scattered at random positions. Each picks a random color from the same palette and a random radius between 0.5px and 2px, drawn at 18% opacity. They don’t do much individually. Together they add a little grain that keeps the flat sine lines from feeling too clean.
the trade-off
This is the one visual on the homepage that genuinely requires JavaScript. It runs via requestAnimationFrame rather than immediately, to ensure the theme CSS variables are already resolved before reading them. Without this delay, the canvas would draw in whatever the browser’s default color is.
If the script hasn’t run yet, the right side is blank. Once it does, the canvas fades in via a CSS transition on the ready class. It’s a small flicker of layout but not worth complicating the code to fix.
the data is live
The series is regenerated from git log on every build, so this plot includes the commit that published this post — and the ribbons on the homepage will quietly shift as the history grows. Which is also why there are no measurements written into this page: any number I quoted would be wrong by the next deploy. The plot and the function box are generated from the same data the canvas uses, so they can’t drift out of sync with it.