GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Fourier Epicycles, 9 Ways (No Library)

No library, no WebGL — put a path's points on the complex plane, run one sine/cosine integral, and circles stacked on circles start to spin; the pen at the

Auto-plays · click a tile to jump to its section · all nine in one zip

From Hand-Picked Coefficients to the Discrete Fourier Transform, Nine Steps

The nine scenes climb a single axis: how the coefficients get into the chain. 01 places five of them by hand, so you can see that a chain is nothing but radius, frequency, and phase. 02 swaps them for the odd-harmonic series of a square wave — still no transform, just circles building a sharp edge out of smooth spins. 03 is the door everything else walks through: a real star outline is sampled at 256 points and transformed, and the pen draws the star back. 04 throws most of the coefficients away and the heart survives, 05 lets your pointer supply the path and recomputes on every stroke, 06 erects the coefficients as bars and morphs between two drawings' fingerprints beside the still-spinning chain, 07 freezes magnitudes and rotates only the phases, 08 hands the throw-away decision to a slider, and 09 stages the same resynthesis as a one-stroke logo reveal in the dark. It shares nothing with a slime mold network grown from deposits and evaporation — there nothing is computed ahead, here everything is arithmetic on a fixed path — and a fourier epicycles drawing is readable in both directions: path to coefficients, or coefficients back to path.

Code per item

Each snippet below is lifted from that item's own script tag, constant names and all — comments translated, identifiers untouched. Drop one into a console next to a canvas and it grows the same chain the panel above it draws.

01Basic epicycle chain — circles stacked on circles, orbiting and plotting a point

Five coefficients — frequency k, radius r, starting phase p — are chained tip-to-center, and the endpoint orbits into a petal-like symmetric figure: the minimal unit of resynthesis, with no transform anywhere, backed by a polar grid of sixteen rotating spokes and four breathing rings redrawn every frame. Every circle's angle is k * t + p, each next center sits one cos/sin step from the previous endpoint, and this exact loop — fed by hand-set numbers here, by transform output in items 03 through 09 — paints 36.176% of the frame cumulatively, peaks at 4.983%, and shines at 33.1 average intensity, tied for the brightest of the nine.

Basic epicycle chain — circles stacked on circles, orbiting and plotting a point
Open in a new tab

01 · Basic epicycle chain — circles stacked on circles, orbiting and plotting a point — Measured area 36.176% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DarcrequestAnimationFrame

The five coefficients are the whole drawing

  var COEF = [
    { k: 2, r: 0.34, p: 0.0 },
    { k: 3, r: 0.18, p: 1.1 },
    { k: 5, r: 0.12, p: 2.4 },
    { k: 7, r: 0.07, p: 0.6 },
    { k: 9, r: 0.05, p: 3.6 }
  ];

02Square-wave synthesis — a few odd-harmonic circles building a sharp wave

A square wave decomposes into odd harmonics only — f(t) = (4/π)·Σ sin(k t)/k — and each term is a circle of radius 1/k spinning k times faster; the fourth circle's radius is a seventh of the first, but it spins seven times as fast, and that trade is what sharpens the corners. Stack those circles vertically on the left, plot the endpoint's height along the t axis on the right, and smooth circular motion sums into a sharp staircase before your eyes — the classic oscilloscope scene for science content, painted at 36.880% cumulative area with a 9.004% peak at 27.4 intensity, the classical series acting as a coefficient table before any transform runs.

Square-wave synthesis — a few odd-harmonic circles building a sharp wave
Open in a new tab

02 · Square-wave synthesis — a few odd-harmonic circles building a sharp wave — Measured area 36.88% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DlineTorequestAnimationFrame

Radius one over k, angular speed k

  function computeChain(t) {
    // Vertical stack: circle k has radius AMP/k and angular speed k
    var pts = [];
    var cx = W * 0.24, cy = H * 0.5;
    var base = H * 0.36;
    pts.push({ x: cx, y: cy });
    for (var k = 1; k <= TERMS * 2; k += 2) {
      var y = pts[pts.length - 1].y + Math.sin(k * t) * (AMP / k) * base;
      pts.push({ x: cx, y: y });
    }
    return pts;
  }

03Drawing a star — coefficients sorted by size, one stroke of the pen

A five-point star — outer radius 1, inner 0.42, closed through ten vertices — is sampled at 256 points and run through the discrete Fourier transform, where each output coefficient is the average of every point multiplied by a spinning unit complex number — e^(−i·2πkn/N) split into cos and sin parts — with indices past the halfway point folded into negative frequencies so circles spin both directions. The final magnitude sort puts big circles first, which keeps the chain from swinging off-screen, and the pen then rounds the star's outline in a single revolution — the chain carries the top 14 coefficients (SHOW=14), so the edges come out very slightly softened but the star is essentially complete with them, HOLD=0.45 dwell — painting 45.551% cumulatively at 6.447% peak and 26.4 intensity over a radar sweep of 22 stepped-alpha wedges: the one-stroke reveal for logos and splash screens.

Drawing a star — coefficients sorted by size, one stroke of the pen
Open in a new tab

03 · Drawing a star — coefficients sorted by size, one stroke of the pen — Measured area 45.551% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DlineTorequestAnimationFrame

The transform is one nested loop with a frequency fold

  function dft(path) {
    var M = path.length;
    var out = [];
    for (var k = 0; k < M; k++) {
      var freq = (k <= M / 2) ? k : k - M;   // fold into negative frequencies
      var re = 0, im = 0;
      for (var n = 0; n < M; n++) {
        var a = -TAU * k * n / M;
        var c = Math.cos(a), s = Math.sin(a);
        re += path[n].x * c - path[n].y * s;
        im += path[n].x * s + path[n].y * c;
      }
      re /= M; im /= M;
      out.push({ freq: freq, re: re, im: im, amp: Math.sqrt(re * re + im * im) });
    }
    out.sort(function (a, b) { return b.amp - a.amp; });   // sort by magnitude
    return out;
  }

04Drawing a heart — closed-curve coefficients bringing a heart back

A heart parametric curve sampled at 256 points goes through the same transform, and only the top 60 circles are drawn (SHOW=60, HOLD=0.4) — the chain reads each coefficient's phase with Math.atan2(c.im, c.re) and places the next center one cos/sin step from the last, so nothing about the heart is stored beyond the coefficient list and the drawing you watch is nothing but the sum. Starting at the concave dip between the lobes it retraces the heart exactly, tail included — throwing coefficients away while the shape survives is the trailer for compression that item 08's slider finishes, painted at 46.641% cumulative area, 5.647% peak, 27.7 intensity: the campaign-hero one-stroke scene.

Drawing a heart — closed-curve coefficients bringing a heart back
Open in a new tab

04 · Drawing a heart — closed-curve coefficients bringing a heart back — Measured area 46.641% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DlineTorequestAnimationFrame

One formula defines the path, resynthesis walks it back

  function heartPath(u) {
    var t = u * TAU;
    var x = 16 * Math.pow(Math.sin(t), 3);
    var y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
    return { x: x / 17, y: y / 17 };
  }

  function chainAt(u, show) {
    var base = Math.min(W, H) * 0.42;
    var cx = W * 0.5, cy = H * 0.48;
    var pts = [{ x: cx, y: cy }];
    var n = show || COEF.length;
    for (var i = 0; i < n && i < COEF.length; i++) {
      var c = COEF[i];
      var a = c.freq * u * TAU;
      var ph = Math.atan2(c.im, c.re);
      var ang = a + ph;
      var px = pts[i].x + Math.cos(ang) * c.amp * base;
      var py = pts[i].y + Math.sin(ang) * c.amp * base;
      pts.push({ x: px, y: py });
    }
    return pts;
  }

05Draw your own line — sketch it, and circles redraw it live

Draw any line with the pointer — every added point re-normalizes the path and re-runs the transform, so the epicycles re-form around your just-drawn signature or doodle live, stroke by stroke. The O(N²) transform over up to MAXPTS=512 points finishes instantly — no debounce, no worker — and normalizing to center and unit span means the SHOW=48-circle chain fits the stage whatever size you draw at, painting 42.997% cumulatively at 7.037% peak and 23.7 intensity — until you draw, the epicycles keep redrawing a sample wavy signature and leave a faint afterimage of it: the interactive easter egg of the set.

Draw your own line — sketch it, and circles redraw it live
Open in a new tab

05 · Draw your own line — sketch it, and circles redraw it live — Measured area 42.997% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdownpointermoverequestAnimationFrame

Every stroke redraws the coefficients from scratch

  function normalize() {
    // Normalize the drawn points to stage center and unit radius
    var minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
    for (var i = 0; i < PATH.length; i++) {
      if (PATH[i].x < minx) minx = PATH[i].x;
      if (PATH[i].x > maxx) maxx = PATH[i].x;
      if (PATH[i].y < miny) miny = PATH[i].y;
      if (PATH[i].y > maxy) maxy = PATH[i].y;
    }
    var cx = (minx + maxx) / 2, cy = (miny + maxy) / 2;
    var span = Math.max(maxx - minx, maxy - miny) || 1;
    normPath = [];
    for (var j = 0; j < PATH.length; j++) {
      normPath.push({ x: (PATH[j].x - cx) / span, y: (PATH[j].y - cy) / span });
    }
    COEF = dft(normPath);
  }

06Spectrum bars — coefficient magnitudes showing which circles do the work

Two drawings — the same star and an "even spectrum" doodle mixing many frequencies at similar size — are transformed at N=128 and erected as bars across frequency slots −12 through +12 (25 slots), a slot joining the interpolated chain only when both drawings have a coefficient there (if (!s || !d) continue;). Amplitude slides linearly while phase takes the angular road between the two atan2 values, and the bars and the chain read the same interpolated array — what you watch slide is literally what the pen draws — as over MORPH=2.6s the fingerprint morphs from a few tall low-frequency bars into an even comb, painting 36.266% cumulatively with the smallest peak frame of the nine at 2.672% — the bars live in a narrow band — at 33.1 intensity, tied brightest.

Spectrum bars — coefficient magnitudes showing which circles do the work
Open in a new tab

06 · Spectrum bars — coefficient magnitudes showing which circles do the work — Measured area 36.266% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DfillRectrequestAnimationFrame

Morphing is amplitude lerp plus phase blend

  function coefAt(mix) {
    var used = [];
    for (var fi = -NBARS / 2; fi <= NBARS / 2; fi++) {
      if (fi === 0 && mix < 0.5) continue;
      var s = slotStar[fi], d = slotDoodle[fi];
      if (!s || !d) continue;
      used.push({
        freq: fi,
        amp: s.amp + (d.amp - s.amp) * mix,
        ph: s.ph !== undefined ? s.ph + (d.ph - s.ph) * mix : 0
      });
    }
    // Blend the phases in a second pass
    for (var i = 0; i < used.length; i++) {
      var s2 = slotStar[used[i].freq], d2 = slotDoodle[used[i].freq];
      used[i].ph = Math.atan2(s2.im, s2.re) + (Math.atan2(d2.im, d2.re) - Math.atan2(s2.im, s2.re)) * mix;
    }
    return used;
  }

07Phase morph — same coefficients, rotated phases, a pattern that shape-shifts

The star's amplitudes stay frozen; only the phases receive an offset proportional to frequency, φ_k = d·k, with d swinging between −0.8 and 0.8 over SWEEP=2.4s — the one added term + c.freq * d inside the chain's phase gives each circle an extra head start proportional to its frequency, so slow circles barely move while fast circles swing wide. The pen's trajectory morphs continuously from star to petal to swirl, the accumulating trail overlapping the whole family as afterglow (SHOW=16) — magnitude answers "how much", phase answers "where from" — painting 40.690% cumulatively with the largest single-frame change of the nine at 15.098% peak and 24.6 intensity: a generative-art background that keeps shape-shifting through its whole cycle.

Phase morph — same coefficients, rotated phases, a pattern that shape-shifts
Open in a new tab

07 · Phase morph — same coefficients, rotated phases, a pattern that shape-shifts — Measured area 40.69% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

One added term rotates every phase

  function chainAt(u, d, show) {
    var base = Math.min(W, H) * 0.36;
    var cx = W * 0.32, cy = H * 0.5;
    var pts = [{ x: cx, y: cy }];
    for (var i = 0; i < show && i < COEF.length; i++) {
      var c = COEF[i];
      var ph = Math.atan2(c.im, c.re) + c.freq * d;
      var ang = c.freq * u * TAU + ph;
      pts.push({
        x: pts[i].x + Math.cos(ang) * c.amp * base,
        y: pts[i].y + Math.sin(ang) * c.amp * base
      });
    }
    return pts;
  }

08Harmonic slider — how the drawing blurs as you remove circles

Item 04's heart coefficients, magnitude-sorted, sit behind an input[type=range] from 1 to 64 circles — the auto demo sweeps the count on a smooth 0.5 - 0.5 * Math.cos(lt * TAU / SWEEP) triangle over SWEEP=3.2s, and grabbing the slider (pointerdown, input, or keydown) stops auto mode, leaving the count in your hand. At show=1 the heart is a droplet blur, somewhere around a dozen circles the lobes commit, and by 64 it is crisp — the same "how much can you throw away and keep the essence" question that image and audio compression ask of a signal, painted at 41.044% cumulative area, 12.360% peak, 31.8 intensity: the teaching toy of the set.

Harmonic slider — how the drawing blurs as you remove circles
Open in a new tab

08 · Harmonic slider — how the drawing blurs as you remove circles — Measured area 41.044% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

input[type=range]CanvasRenderingContext2DrequestAnimationFrame

A cosine triangle walks the slider until you grab it

  function frame(now) {
    if (t0 < 0) t0 = now;
    var lt = (now - t0) / 1000;
    var u = (lt * 0.55) % 1;
    var pulse = 0.5 + 0.5 * Math.sin(lt * 8.0);

    var show;
    if (autoMode) {
      // Auto round trip 1 -> 64 -> 1; a grab stops it
      var tri = 0.5 - 0.5 * Math.cos(lt * TAU / SWEEP);
      show = 1 + Math.round(tri * (MAXSHOW - 1));
      if (slider && String(slider.value) !== String(show)) slider.value = String(show);
    } else {
      show = slider ? parseInt(slider.value, 10) : MAXSHOW;
    }

    decayTrail();
    var pen = draw(u, pulse, show, lt * TAU);
    stampTrail(pen);
    if (nOut) nOut.textContent = String(show);
    raf = requestAnimationFrame(frame);
  }

09Logo-trace scene — a one-stroke logo reveal on a dark stage

A one-stroke letter shape — our own path, not any real brand's — is a 300-degree arc bending inward into a horizontal bar, sampled at 256 points and redrawn through the same transform and chain (SHOW=36, HOLD=0.35), the first 72% of the parameter walking the arc and the rest interpolating a straight run into the bar's end. Two segments and nothing else, yet after the transform it is just another coefficient list drawn by the identical chain that drew the star and the heart, on a dark stage breathing a central spotlight of concentric rings with stepping alpha and the slowest trail decay of the nine at α=0.012 — the finished stroke lingers as afterglow even after the circles pause, at 47.314% cumulative area, the largest of the nine, 8.119% peak and 24.2 intensity: the brand-splash closer.

Logo-trace scene — a one-stroke logo reveal on a dark stage
Open in a new tab

09 · Logo-trace scene — a one-stroke logo reveal on a dark stage — Measured area 47.314% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientCanvasRenderingContext2DrequestAnimationFrame

The logo is a 300-degree arc plus a straight bar

  function logoPath(u) {
    // 0-0.72: arc (starting at -60 degrees, sweeping 300), 0.72-1: horizontal bar
    var ARC = 0.72;
    var R = 1;
    if (u < ARC) {
      var ang = (-Math.PI / 6) + (u / ARC) * (TAU * 300 / 360) * -1;
      return { x: Math.cos(ang) * R * 0.62, y: Math.sin(ang) * R * 0.62 };
    }
    var f = (u - ARC) / (1 - ARC);
    var sx = Math.cos(-Math.PI / 6) * 0.62, sy = Math.sin(-Math.PI / 6) * 0.62;
    var ex = 0.42, ey = 0.02;
    var cx2 = sx + (ex - sx) * f, cy2 = sy + (ey - sy) * f;
    return { x: cx2, y: cy2 };
  }

Sorting coefficients by frequency instead of size is where the chain breaks loose

Frequencies left as positives break the closure

Skip the fold (k <= M / 2) ? k : k - M and every circle spins the same direction, the chain drifts, and the pen never returns to its start — the star smears into an open spiral instead of closing. Negative frequencies are not a nicety; they are half the answer, and without them no closed path comes back to life.

Sorting by frequency swings the chain off-screen

Magnitude-first stacking means each new circle is smaller than the last, so the chain converges toward its pen. Frequency order puts some of the biggest circles late in the chain, the intermediate endpoints leave the stage entirely, and the drawing survives only as off-screen arithmetic. The truncation games of items 04 and 08 also silently assume magnitude order — "top k circles" is meaningless otherwise.

Dropping the phase loses the drawing

Keep every amplitude and zero every phase, and the circles still spin — but the pen wanders a blob and never finds the star. Magnitude answers "how much" and phase answers "where from"; lose either and the sum stops being the drawing. Item 07 is the controlled experiment: it slides a frequency-proportional offset through every phase at once, and you watch the star dissolve into petals and swirls as the offset deepens.

clearRect kills the comet tail

Erasing the main canvas every frame and redrawing the trace from a stored point array throws away both the decay feel and the painted-area numbers — the trail becomes a polyline, not ink fading at α between 0.012 and 0.03 per frame with destination-out. The separate transparent trail canvas is not an optimization; it is the look. Item 09's 47.314% cumulative area exists only because its α=0.012 erasure is the gentlest of the nine.

Why do spinning circles draw a picture? — complex coefficients and frequency decomposition

A path is a list of complex numbers

Treat every sampled point (x, y) as z = x + iy. Multiplying two complex numbers adds their angles and multiplies their lengths, which is exactly the arithmetic a spinning circle needs: a coefficient times e^(iθ) is a circle of that radius at that angle. Once the path is a list of z_n, one formula reads all the circles out of it — c_k = (1/N)·Σ z_n · e^(−i·2πkn/N) — and the chain is just that sum, term by term, drawn as it evaluates.

The transform asks every frequency how much it contributes

Each c_k correlates the whole path against one rotation speed: a frequency present in the path adds up coherently and lands a large amplitude, amp = √(re² + im²), while an absent one cancels toward zero. The phase, atan2(im, re), records where that frequency's circle must start if the sum is to retrace the path exactly. Frequencies above N/2 fold into negatives so loops can close — circles spinning in both directions is what lets a pen retrace a closed drawing. It is the same stacked-sines idea that moves an ocean wave scene whose surface is nothing but summed traveling sines — there the sines are chosen by hand, here the transform recovers them from a finished drawing. A fourier transform circle animation stays compelling precisely because the arithmetic never hides: every circle on screen is one row of the coefficient table.

Resynthesis walks the chain, and the pen is the last point

At time u each coefficient contributes radius amp at angle freq·u·2π + phase; the next center sits on the previous endpoint, and the drawing is the locus of the final point. HOLD pauses u after the stroke completes so the finished figure gets its moment on stage. Nothing is pre-drawn or overlaid — delete the coefficient list and the drawing deletes with it.

Three variants that swap rotation speed and sort order, tuned for reduced-motion

One constant, three different drawings

Name Change Feel
Circles in use 03 SHOW all → top 5 Vertices round off into a soft star
Time speed 01 SPEED 1× → 3× The trail stretches into long hatching streaks
Sort order 03 magnitude → frequency Same pen trajectory, a completely different circle arrangement

None of these touch the transform — the drawing lives entirely in how the coefficient list is sampled and read back.

Reduced motion still shows the finished drawing

Every item checks matchMedia('(prefers-reduced-motion: reduce)') before the requestAnimationFrame loop starts, and for those visitors the loop never runs. Instead, one full cycle of the trace is pre-stamped and the finished scene freezes with an .is-still class — the star, the heart, and the letter sit there complete and legible, just not spinning.

One slider to grab, eight panels that run themselves

Only item 05 wants a pointer and only item 08 wants a grab — every other panel runs on requestAnimationFrame alone, so the page stays alive unattended. Nothing above is retyped or trimmed: tjy7w4m5 unlocks the same nine source files the snippets came from, HTML, CSS, and JS together, inside this page's zip.

Questions before you wire an epicycle chain into a page

Do all nine items run the same Fourier transform

No — and the spread is deliberate. 01 hand-defines five coefficients and 02 reads its circles straight off the odd-harmonic series of a square wave, no transform at all; items 03 through 09 all run the full discrete transform on a sampled path (06 runs it twice, once per drawing, then interpolates between the two coefficient sets). The epicycles fourier series pipeline — path in, coefficients out, chain redraws — is one idea that each item enters by a different door. The hand-set chain of 01 and the series table of 02 exist so you can verify that claim: the chain code never changes, only where its numbers come from.

Can I draw my own shape and watch the circles redraw it

Yes — item 05 is the fourier epicycles online playground of this page: press, draw, release, and up to 512 of your points become coefficients on the spot, then the chain redraws your line stroke by stroke. The recompute is O(N²) and still instantaneous at that size, so the circles re-form while you are still drawing. Every other item is self-running and needs no input at all. Closed shapes work best — the transform assumes the path wraps around, so a signature with a clear start and end reproduces more faithfully than a scribble that doubles back on itself.

Why does item 09 paint the largest area of the nine

Because it erases the slowest. Its trail decay α=0.012 is the gentlest of the nine, so old ink survives longest and the cumulative painted area climbs to 47.314% — ahead of the heart's 46.641% and the star's 45.551%. Its HOLD=0.35 dwell keeps the finished letter on stage after the circles stop, which is the whole point of a reveal scene: the stroke stays lit after the machine that drew it goes quiet. The same arithmetic explains why item 06 posts the smallest peak frame at 2.672% — its bars live in a narrow band of the screen, so even a big morph moves few pixels per frame.

Sources: MDN — CanvasRenderingContext2D · MDN — Window.requestAnimationFrame() · MDN — prefers-reduced-motion · Wikipedia — Fourier series · Wikipedia — Fourier transform.

Logo reveals and hand-drawn demos — where these nine actually land

A brand intro or a loading screen reaches for item 09 first — the one-stroke logo reveal on a dark stage is already staged for exactly that moment, so swapping in a logo path is the only change needed. An event page or a guestbook built for visitors who want to leave a mark does better with item 05: a line sketched with the mouse comes back as an epicycle chain on the spot, turning a demo into something to play with.

For content that explains audio or signal processing, item 06's spectrum bars are the fit — coefficient magnitude shown as a bar turns an abstract Fourier idea into a chart a reader can follow. All three ship as one self-contained file, so none of them need a build step to sit inside a static section.

Enter the archive password

The password is inside this article. You will find it as you read.