This simulation uses a Discrete Fourier Transform (DFT) to decompose a hand-drawn path into a sum of rotating circles called an epicycle. It works in three steps: p5.js collects mouse coordinates, a resampling algorithm spaces them evenly along the path, and the DFT converts them into a bunch of spinning circles.
The backend of this project was coded in Javascript because I wanted to make it run on a website. The frontend uses HTML, p5.js, and really ugly CSS.
I use the p5.js canvas, the mouseDragged function, and the mouseX and mouseY variables. The rate of capture of mouse movements is determined by the frame rate (FPS).
Mouse inputs are unevenly spaced (for example, more are clustered in corners and less are around straight lines). To better reconstruct the shape, we fix that by resampling the points so they are evenly spaced along the path. Those coordinates are then fed into Step 3.
I learned this math from 3Blue1Brown's video. Check it out!
For every integer frequency f from 0 to N−1, the DFT computes a complex number coefficient X[f] that determines the amplitude and phase of the corresponding epicycle. It does so by first looping over all the points from Step 2 and representing them as complex numbers z[n] = x[n] + iy[n], where x[n] and y[n] are the x and y coordinates of the nth point. Then, for each frequency f, it computes the coefficient X[f] as:
X[f] = (1/N) Σ z[n] · e−2πifn/N
(This formula is due to the DFT math. It is explained in the 3B1B video much better than I could through a wall of text.)
Expanding via Euler's formula, eiθ = cosθ + i·sinθ, and multiplying out the complex product (x + iy)(cosθ + i·sinθ) gives the real and imaginary parts of X[f]:
re = Σ(x[n]·cosθ − y[n]·sinθ)/N
im = Σ(x[n]·sinθ + y[n]·cosθ)/N
where θ = -2πfn/N. (θ is negative because p5.js's y increases as it goes down, which flips the direction of rotation.)
The amplitude (radius) and phase (starting angle) of the epicycle for frequency f are then:
amplitude = √(re² + im²)
phase = arctan(im, re)
The DFT produces N frequencies. For frequencies above N/2, the standard convention is to interpret them as negative (the circle spins in reverse), so the simulator remaps any f > N/2 to f − N. The results are sorted by amplitude so the largest - most visually significant - circles are drawn first.
I do not render epicycles below a minimum amplitude threshold (0.5px), since they are too small to be seen. This does not affect the accuracy of the path drawn.
The "accuracy" filter represents the cumulative amplitude of the drawn epicycles divided by the total amplitude of all epicycles. For example, 99% accuracy means the epicycles we use to do the math and draw the path account for 99% of the total amplitude, and the remaining 1% is ignored.