Draw My Canvas / studio

Spirograph

A spirograph is a toothed wheel rolling round a ring, with a pen through one of the wheel’s holes. This plate draws five such figures in turn. For each one the ring radius R, the wheel radius r and the pen’s distance from the wheel’s centre d are whole numbers, and the figure closes after exactly r ÷ gcd(R, r) trips of the wheel round the ring. With R 96 and r 60, which share a factor of 12, that is 5 trips and an 8‑lobed star. The coral dot is the pen.

Plate 42roulette

Waiting for the animation above to load…

What lands in the file, and what the width does

One click writes whatever the animation is drawing at that moment to a PNG, with the drawmycanvas.com mark drawn into the picture rather than laid over it. Leave the width box empty and you get the stage exactly as your browser rasterised it — your window’s width times its device pixel ratio, which is about 1,600 px across from a 1280‑px window on a HiDPI laptop and about 353 px from a 390‑px phone.

Type a width instead, or take a preset, and the frame is redrawn into a canvas that wide: the height follows the stage’s own shape and the mark scales with it. The stage is 16:9 on a wide window and 4:3 below 560 px, so a width of 1200 saves 1200×675 on a laptop and 1200×900 on a phone. 1200 px is the width Open Graph and X link cards are cut from — the canonical card is 1200×630, and a card crops the extra height rather than letterboxing it.

Honest limits. Asking for more pixels than the stage was drawn at resamples pixels that were never drawn: a 1920‑px file exported from a 353‑px phone stage is bigger, not sharper. For a big file that is sharp, use Save as wallpaper: it draws the plate again from scratch at exactly 1179×2556 or 1290×2796 (phones), 1920×1080, 2560×1440 or 3840×2160 (4K), so every line is rasterised at that size and the shape is the screen’s, never stretched. Because the plate restarts, a wallpaper is a fresh run of it rather than the exact frame on screen; it is given as long as the stage has been running, up to 20 seconds, to develop. A very large width is a real memory allocation and a browser is allowed to refuse it; when that happens the line above says so plainly and nothing else on the page changes. Stills are PNG only — no JPEG, no WebP. On browsers that can record video, Record a clip saves 5, 10 or 20 seconds of the running animation as MP4 or WebM (whichever this browser can encode) at the same width, with the mark in every frame. A clip is not a seamless loop, it has no audio, and a width bigger than the stage is resampled rather than sharper — only the wallpaper is redrawn at its size. Where a browser cannot record video the button never appears and a PNG is the only export. And nothing is uploaded: the frame or clip is assembled in your own browser, so no frame of this plate ever reaches us.

Live on an HTML canvas · vanilla JavaScript · no dependencies Open fullscreen

About this piece

The faint circle is the fixed ring. The translucent circle is the wheel, the thin line is the arm from the wheel’s centre to the pen, and the coral dot is the pen itself, drawn last so it is never hidden by the curve. The wheel’s centre takes 1.8 seconds per trip round the ring. When the pen is back where it started, the closed figure is held for 2.6 seconds and the next one begins, so all five take 58 seconds. The line along the bottom names the curve, its R, r and d, the trip in progress (“trip 3 of 5”) and the number of lobes it will have. With reduced motion switched on, you get one still frame: the first figure, closed.

The five figures

All three sizes are in the same unit, so the ring, the wheel and the arm on screen are to scale. “Trips” counts the trips of the wheel’s centre round the ring before the pen is back at its start. The gcd column is there because it decides the other two:

CurveRrdgcd(R, r)TripsLobes
Hypotrochoid9660451258
Hypotrochoid10563522135
Hypotrochoid9684561278
Hypotrochoid10540365821
Epitrochoid6024301225

The first figure is an 8‑pointed star of long loops. The second has the same R : r ratio as a 5‑tooth ring and a 3‑tooth wheel (105/63 = 5/3), so it is a 5‑lobed star that closes in 3 trips. In the third the wheel is nearly as big as the ring: its centre circles only 12 units from the middle, and the pen draws a slowly turning 8‑sided loop. The fourth is the dense one, 21 lobes in 8 trips. The fifth rolls the wheel round the outside of a ring and draws five large overlapping loops, with a small inner loop near the ring between each pair.

The formula

Let t be the angle of the wheel’s centre round the ring. Inside the ring the centre stays on a circle of radius R − r. Rolling without slipping turns the wheel the other way, and the pen’s arm turns through (R − r)/r radians for every radian the centre travels. Outside the ring the centre is at R + r and the arm turns (R + r)/r as fast:

hypotrochoid (wheel inside the ring)
  x = (R − r) cos t + d cos((R − r) t / r)
  y = (R − r) sin t − d sin((R − r) t / r)

epitrochoid (wheel outside the ring)
  x = (R + r) cos t − d cos((R + r) t / r)
  y = (R + r) sin t − d sin((R + r) t / r)

This is the form given on Wikipedia’s hypotrochoid page and its epitrochoid page. The shape depends only on the two ratios R/r and d/r, which is why R 105, r 63 draws the same star as R 5, r 3 would, just larger.

Why it closes after r / gcd(R, r) trips

After k whole trips (t = 2πk) the wheel’s centre is home. The pen is home only if the arm has also made a whole number of turns, and the arm has turned k(R − r)/r = kR/r − k times. So the figure closes at the first k for which kR/r is a whole number. Write g = gcd(R, r). Then R/r = (R/g)/(r/g) is a fraction in lowest terms, and kR/r is whole exactly when r/g divides k. The smallest such k is r/g.

Worked example, R 96, r 60. gcd(96, 60) = 12, so R/r = 8/5. Each trip turns the arm by 36/60 = 0.6 of a turn: 0.6, 1.2, 1.8, 2.4, then 3.0 after 5 trips, the first whole number. So the pen closes after 5 trips, not before.

The lobes are R/g. The pen’s distance from the centre satisfies |p|² = (R − r)² + d² + 2(R − r)d cos(Rt/r). That distance peaks R/r times per trip, so over r/g trips it peaks (R/r)(r/g) = R/g times. Each peak is the tip of a lobe. For the epitrochoid the cosine has a minus sign in front but the same Rt/r inside, so the count is the same. In toy terms: by the time the figure closes, the wheel’s rim has rolled its whole circumference along the ring R/g times, and the pen swings out towards the ring once per roll. For R 96, r 60 that is 5 trips of 96 teeth, 480 teeth in all, which is 8 turns of a 60‑tooth wheel.

Why these five avoid coprime sizes. With R 96 and r 35, which share no factor, the figure needs 35 trips and has 96 lobes. At 1.8 seconds a trip that is over a minute of drawing, and the result is a nearly solid disc. Sizes with a big common factor give the open figures.

Special cases. With d = r the pen sits on the rim and every lobe tip becomes a sharp cusp: that is a hypocycloid. With d = 0 the pen is at the centre and draws a plain circle of radius R − r. With R = 2r the figure closes in one trip and is an ellipse with semi-axes r + d and |r − d|. If d = r as well, that ellipse is flat and the pen runs back and forth along a straight line, the Tusi couple.

How it is computed

The picture is a pure function of the plate’s age. The age picks the figure and how far round it the pen has got, and the curve is redrawn from t = 0 every frame as straight segments, 720 per trip. That is at most 8 × 720 = 5,760 segments, for the 21‑lobed figure. The frame rate decides how often the picture is redrawn, never what is in it. The math lives in one block of code, used by the gallery tile, the embed and a node module that the test suite loads. The suite checks that the curve matches the formula above at 1,000 random parameter values. For 10 seeded (R, r) pairs, it checks that the pen is back within 10−9 after exactly r/gcd(R, r) trips and is not back after any earlier whole trip. It also counts the radius peaks over a closed figure and compares them with R/gcd(R, r), and checks the table above against the code. The first draft returned one trip too many. Nothing on screen showed it: the pen simply went round its closed figure once more. The closure test caught it.

A 30-line version

The snippet below draws the first figure, R 96, r 60, d 45, in the site’s palette, in 28 lines of HTML and plain JavaScript with no library. It adds 24 segments a frame, so it closes after 150 frames, about 2.5 seconds at 60 frames a second. With reduced motion it draws the closed figure at once. The test suite runs it against a stub canvas and checks that it stops exactly where it started, after 5 trips. It leaves out the ring, the wheel and the arm, the other four figures, sharp drawing on high-density screens (it draws in CSS pixels) and the reaction to theme changes.

Things worth changing: r = 35 shows why coprime sizes are dense (35 trips, 96 lobes). d = 60 puts the pen on the rim and turns the loops into cusps. R = 120, r = 60 gives an ellipse in one trip.

<canvas id="spiro" style="position:fixed; inset:0"></canvas>
<script>
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
const [ground, ink, pen] = dark ? ['#0B0D12', '150,180,255', '255,120,84']
                                : ['#E7E2D5', '40,72,205', '190,68,28'];  // the site's palette
const R = 96, r = 60, d = 45;  // ring radius, wheel radius, pen distance: whole numbers
const gcd = (a, b) => (b ? gcd(b, a % b) : a);
const TRIPS = r / gcd(R, r);  // 5: the pen is home after this many trips, and not before
const N = 720 * TRIPS, END = 2 * Math.PI * TRIPS;  // 720 straight segments per trip
const point = (t) => [(R - r) * Math.cos(t) + d * Math.cos((R - r) * t / r),
                      (R - r) * Math.sin(t) - d * Math.sin((R - r) * t / r)];
const cv = document.getElementById('spiro'), ctx = cv.getContext('2d');
const W = cv.width = innerWidth, H = cv.height = innerHeight;
const k = Math.min(W, H) * 0.45 / Math.max(R, R - r + d);  // the ring fills 90% of the short side
const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
let n = still ? N : 0;  // samples drawn so far
function frame() {
  n = Math.min(N, n + 24);
  ctx.fillStyle = ground; ctx.fillRect(0, 0, W, H);
  ctx.strokeStyle = `rgb(${ink})`; ctx.lineWidth = 1.5; ctx.beginPath();
  for (let i = 0; i <= n; i++) { const [x, y] = point(END * i / N); ctx.lineTo(W / 2 + x * k, H / 2 + y * k); }
  ctx.stroke();
  const [x, y] = point(END * n / N);  // the pen, on top
  ctx.fillStyle = `rgb(${pen})`; ctx.beginPath(); ctx.arc(W / 2 + x * k, H / 2 + y * k, 5, 0, 2 * Math.PI); ctx.fill();
  if (n < N) requestAnimationFrame(frame);
}
frame();
</script>
Paste it into an empty .html file.

The plates it sits nearest

  • Fourier Epicycles adds up many circles turning at different speeds. A hypotrochoid is the case with just two: one of radius R − r turning forwards, and one of radius d turning backwards (R − r)/r times as fast.
  • Lissajous Ribbon is another closed curve whose shape is set by a ratio of whole numbers, there 3 : 2 between two sine waves.
  • Cosmic Circles also moves things on circles, but leaves no trace of where they have been.

Honest limits

A toy cannot draw every figure here. On a real wheel the pen holes are inside the rim, so d is less than r. All four hypotrochoids keep to that. The epitrochoid does not: its d 30 is bigger than its r 24, which is what gives it the small loops, and a real wheel would need an arm sticking out to draw it.

Real gears are not ideal circles. Teeth have a pitch, the wheel wobbles in the ring and the pen line has width, so a hand-drawn spirograph rarely closes perfectly. The plate draws the ideal no-slip curve.

The curve is a polyline. At 720 segments per trip, the 21‑lobed figure still gets about 270 segments per lobe, and no corner shows at screen sizes. In floating point the pen comes back to within about 10−13 units of its start, far less than a pixel, and the plate stops each figure exactly at its closing value of t anyway.

Curious how the loop and canvas fit together? Read how it works →

More from the gallery

All 44 animations in the gallery →