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

Spirograph Generator: 9 Gear-Ratio Curve Patterns

When the two gear tooth counts share no common factor the curve closes, and the moment the ratio slips the sheet wanders forever — 03 computes the

See All Nine Gear-Curve Patterns Move Right on This Page

Scroll through the grid below and each of the nine canvases is already drawing its own curve — nothing waits for a click, and every one of them starts from two plain integers, a ring tooth count and a rolling-gear tooth count.

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

Spirograph Patterns: 9 Gear-Ratio Drawings That Close Only at the Right Ratio

The set is a small spirograph pattern generator written in plain canvas code: a gear rolling inside a ring, the same gear rolling outside it, symmetry counted from the tooth ratio, several pens on one gear, a fading trail, a guide overlay with the gear drawn in, a pointer that changes the ratio, a turntable, and a gallery that puts four ratios on one wall. If you want the mirror-based cousin of this idea, Kaleidoscope Drawing: 9 Canvas Symmetry Tools covers reflection instead of gears. Open any item below to read the exact lines that place the pen.

01Basic Spirograph Curve Tracing a Gear Inside a Ring

A small gear rolls around the inside of a large ring while the pen position is computed from angles around the ring's center. The curve curls inward and stacks up as one connected line, and TURNS is the gear's tooth count divided by the greatest common divisor, which is 3 for 72 and 27.

Basic Spirograph Curve Tracing a Gear Inside a Ring
Open in a new tab

01 · Basic Spirograph Curve Tracing a Gear Inside a Ring — Measured area 65.706% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GEAR_RING_TEETHSEGMENTS_PER_TURNshadowBlur
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURNS * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  function point(t) {
    var k = (R - r) / r;
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(k * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

Item 01 repainted 65.7% of the frame at an average intensity of 22.7 across 23/23 measured frames.

02Outer Curve Drawn by a Gear Rolling Around the Outside

The same gear now rolls around the outside of the ring, so in point(t) the radius sum replaces the radius difference, the rotation factor changes with it, and the pen term's sign flips, which sends the line outward in petals. With 96 and 36 teeth the divisor is 12, which gives 3 turns to close and 8 petals.

Outer Curve Drawn by a Gear Rolling Around the Outside
Open in a new tab

02 · Outer Curve Drawn by a Gear Rolling Around the Outside — Measured area 66.216% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GEAR_TEETH_OUTERTOOTH_UNITdrawFull
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_TEETH_OUTER * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var g = gcd(GEAR_TEETH_OUTER, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURNS * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  function point(t) {
    var k = (R + r) / r;
    return {
      x: (R + r) * Math.cos(t) - d * Math.cos(k * t),
      y: (R + r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

Item 02 repainted 66.2% of the frame at an average intensity of 26.3 across 23/23 measured frames.

03Pattern Whose Symmetry-Axis Count Comes From the Tooth Ratio

Dividing the ring's tooth count by the greatest common divisor gives AXIS_COUNT, and dividing the rolling gear's count gives TURN_COUNT. For 90 and 24 teeth that is 15 axes and 4 turns, and a badge on the canvas prints both numbers from those two variables.

Pattern Whose Symmetry-Axis Count Comes From the Tooth Ratio
Open in a new tab

03 · Pattern Whose Symmetry-Axis Count Comes From the Tooth Ratio — Measured area 59.435% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PEN_RADIUS_RATIOAXIS_COUNTTURN_COUNT
  function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = b; b = a % b; a = t; } return a; }

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_RADIUS_RATIO;
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var AXIS_COUNT = GEAR_RING_TEETH / g;
  var TURN_COUNT = GEAR_ROLL_TEETH / g;
  var TOTAL_T = TURN_COUNT * Math.PI * 2;
  var CYCLE_SECONDS = 6;
  var SPEED = TOTAL_T / CYCLE_SECONDS;

  badge.textContent = 'Symmetry axes: ' + AXIS_COUNT + ' · closes in ' + TURN_COUNT + ' turns';

  function point(t) {
    var k = (R - r) / r;
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(k * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(k * t)
    };
  }

Item 03 repainted 59.4% of the frame at an average intensity of 19.5 across 23/23 measured frames.

04Rainbow Gear Pattern Drawn by Several Pen Holes at Once

One gear carries four pen holes at the ratios in PEN_RATIOS, and drawPen() computes each hole's coordinates separately with its own color and phase. Because the phases come from a seeded generator, the layered picture is the same on every load, and with 78 and 21 teeth it closes after 7 turns.

Rainbow Gear Pattern Drawn by Several Pen Holes at Once
Open in a new tab

04 · Rainbow Gear Pattern Drawn by Several Pen Holes at Once — Measured area 10.149% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PEN_RATIOSPEN_COLORSPEN_PHASE
  function drawPen(idx, tMax) {
    var d = r * PEN_RATIOS[idx];
    var phase = PEN_PHASE[idx];
    ctx.strokeStyle = PEN_COLORS[idx];
    ctx.lineWidth = Math.max(2.4, scale * 0.032);
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    var steps = Math.max(2, Math.round((tMax / (Math.PI * 2)) * SEGMENTS_PER_TURN));
    ctx.beginPath();
    for (var i = 0; i <= steps; i += 1) {
      var t = (i / steps) * tMax;
      var s = toScreen(point(t + phase, d));
      if (i === 0) ctx.moveTo(s.x, s.y); else ctx.lineTo(s.x, s.y);
    }
    ctx.stroke();
    ctx.save();
    ctx.fillStyle = PEN_COLORS[idx].replace('.85', '.13');
    ctx.fill('evenodd');
    ctx.restore();

Item 04 repainted 10.1% of the frame at an average intensity of 38.2 across 23/23 measured frames.

05Scene That Keeps Drawing While the Trail Slowly Fades

Every tick paints a translucent rectangle over the canvas and then adds one short line segment from the previous pen point to the new one. FADE_ALPHA is 3 divided by TRAIL_FADE_FRAMES, so with 240 frames each stroke dims by about 1.25% per step, and this scene never stops drawing.

Scene That Keeps Drawing While the Trail Slowly Fades
Open in a new tab

05 · Scene That Keeps Drawing While the Trail Slowly Fades — Measured area 49.931% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TRAIL_FADE_FRAMESFADE_ALPHAsimStep
  function simStep() {
    ctx.fillStyle = 'rgba(23,20,26,' + FADE_ALPHA + ')';
    ctx.fillRect(0, 0, cw, ch);
    t += ANGLE_SPEED * (STEP_MS / 1000);
    var p = toScreen(point(t));
    ctx.strokeStyle = 'rgba(255,247,230,.95)';
    ctx.shadowColor = 'rgba(255,214,200,.7)';
    ctx.shadowBlur = Math.max(8, scale * 0.14);
    ctx.lineWidth = Math.max(3, scale * 0.045);
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(prev.x, prev.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    ctx.shadowBlur = 0;
    var halo = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, Math.max(cw, ch) * 0.34);
    halo.addColorStop(0, 'rgba(255,200,215,.075)');
    halo.addColorStop(0.5, 'rgba(190,225,255,.04)');
    halo.addColorStop(1, 'rgba(190,225,255,0)');
    ctx.fillStyle = halo;
    ctx.fillRect(0, 0, cw, ch);
    prev = p;
  }

Item 05 repainted 49.9% of the frame at an average intensity of 18.8 across 23/23 measured frames.

06Guide Overlay Showing the Gear and Pen on the Curve

The rolling gear's circle, its center from rollCenterAt(), and the pen tip are drawn over the growing curve, so the current angle stays visible. The curve finishes at HOLD_FRACTION of each cycle and holds there, and with 60 and 24 teeth the badge reports the contact angle and 5 axes.

Guide Overlay Showing the Gear and Pen on the Curve
Open in a new tab

06 · Guide Overlay Showing the Gear and Pen on the Curve — Measured area 59.176% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HOLD_FRACTIONAXIS_COUNTrollCenterAt
  function gcd(a, b) { while (b) { var t = b; b = a % b; a = t; } return a; }
  var g = gcd(GEAR_RING_TEETH, GEAR_ROLL_TEETH);
  var TURNS = GEAR_ROLL_TEETH / g;
  var AXIS_COUNT = GEAR_RING_TEETH / g;

  var R = GEAR_RING_TEETH * TOOTH_UNIT;
  var r = GEAR_ROLL_TEETH * TOOTH_UNIT;
  var d = r * PEN_TIP_RATIO;
  var K = (R - r) / r;

  function penAt(t) {
    return {
      x: (R - r) * Math.cos(t) + d * Math.cos(K * t),
      y: (R - r) * Math.sin(t) - d * Math.sin(K * t)
    };
  }
  function rollCenterAt(t) {
    return { x: (R - r) * Math.cos(t), y: (R - r) * Math.sin(t) };
  }

Item 06 repainted 59.2% of the frame at an average intensity of 62.8 across 23/23 measured frames.

07Pattern Morphing Its Tooth Ratio by Pointer Distance

The distance from the pointer to the center, divided by POINTER_MAX_DIST, is mapped onto a rolling-gear count between ROLL_TEETH_MIN and ROLL_TEETH_MAX. The path is recomputed only when the rounded tooth count actually changes, and the left and right arrow keys move the same value in KEY_STEP increments.

Pattern Morphing Its Tooth Ratio by Pointer Distance
Open in a new tab

07 · Pattern Morphing Its Tooth Ratio by Pointer Distance — Measured area 11.26% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

POINTER_MAX_DISTROLL_TEETH_MINROLL_TEETH_MAX
  function rebuildForRatio(ratio) {
    ratioParam = Math.max(0, Math.min(1, ratio));
    var newRoll = Math.round(ROLL_TEETH_MIN + ratioParam * (ROLL_TEETH_MAX - ROLL_TEETH_MIN));
    if (newRoll !== currentRoll) {
      currentRoll = newRoll;
      pathData = computePath(currentRoll);
    }
  }

  function handlePointer(px, py) {
    var dist = Math.hypot(px, py);
    markerAngle = Math.atan2(py, px);
    markerDist = Math.min(POINTER_MAX_DIST, dist);
    rebuildForRatio(dist / POINTER_MAX_DIST);
  }

Item 07 repainted 11.3% of the frame at an average intensity of 103.6 across 23/23 measured frames.

08Turntable Scene Spinning the Finished Pattern

The completed 48-and-18-tooth curve is closed into one path and the whole canvas is rotated with a transform at ROTATE_SPEED, one full turn every 9 seconds. Because the divisor is 6 there are 8 axes, so the outline looks the same after every eighth of a turn.

Turntable Scene Spinning the Finished Pattern
Open in a new tab

08 · Turntable Scene Spinning the Finished Pattern — Measured area 14.838% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DISC_RATIOGLOSS_SWEEP_SECONDSAXIS_COUNT
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(rot);
    ctx.beginPath();
    for (var i = 0; i < pathData.length; i += 1) {
      var p = pathData[i];
      var sx = p.x * scale, sy = p.y * scale;
      if (i === 0) { ctx.moveTo(sx, sy); } else { ctx.lineTo(sx, sy); }
    }
    ctx.closePath();
    ctx.strokeStyle = 'rgba(10,79,232,.82)';
    ctx.lineWidth = Math.max(1.6, scale * 0.022);
    ctx.lineJoin = 'round';
    ctx.stroke();
    ctx.restore();

Item 08 repainted 14.8% of the frame at an average intensity of 96.2 across 23/23 measured frames.

09Hero Gallery Wall Combining Ratios, Pens, and Trails

makeCurve() builds four curves from the ring counts 60, 54, 48 and 66 and the rolling counts 23, 19, 17 and 25, and pointOn() places the pen on each. Every pair is coprime, so the cells need 23, 19, 17 and 25 turns to close, and each draws at its own HERO_SPEED.

Hero Gallery Wall Combining Ratios, Pens, and Trails
Open in a new tab

09 · Hero Gallery Wall Combining Ratios, Pens, and Trails — Measured area 48.697% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_RING_TEETHHERO_PEN_RATIOSHERO_TRAIL_FADE_FRAMES
  function makeCurve(ringTeeth, rollTeeth, penRatio) {
    var R = ringTeeth * HERO_TOOTH_UNIT;
    var r = rollTeeth * HERO_TOOTH_UNIT;
    var d = r * penRatio;
    var K = (R - r) / r;
    var g = gcd(ringTeeth, rollTeeth);
    var turns = rollTeeth / g;
    return { R: R, r: r, d: d, K: K, turns: turns, maxT: 2 * Math.PI * turns, patternRadius: R + d };
  }
  function pointOn(curve, t) {
    return {
      x: (curve.R - curve.r) * Math.cos(t) + curve.d * Math.cos(curve.K * t),
      y: (curve.R - curve.r) * Math.sin(t) - curve.d * Math.sin(curve.K * t)
    };
  }

Item 09 repainted 48.7% of the frame at an average intensity of 21.7 across 23/23 measured frames.

Code for Each Gear-Curve Technique

Reading the nine rows together, the repaint column has two groups: items 01 to 03, 05, 06 and 09 cover roughly half to two thirds of the frame, while 04, 07 and 08 cover about a tenth. The wide group draws long continuous strokes or keeps a trail that never clears, so almost every pixel gets touched over the loop, whereas the narrow group either draws thin pen lines on a plain wash or rotates an outline that stays in place. Average intensity runs the other way: 07 (103.6) and 08 (96.2) are the highest, which fits an outline whose few moving pixels change a lot from frame to frame, while the trail scene 05 (18.8) shows the opposite: many pixels that each shift only slightly.

Item Technique Repaint area Avg. intensity Moved frames
01 Basic gear-in-ring curve 65.7% 22.7 23/23
02 Outer rolling curve 66.2% 26.3 23/23
03 Tooth-ratio symmetry 59.4% 19.5 23/23
04 Multi-pen rainbow 10.1% 38.2 23/23
05 Fading trail drawing 49.9% 18.8 23/23
06 Gear and pen guide 59.2% 62.8 23/23
07 Pointer ratio morph 11.3% 103.6 23/23
08 Pattern turntable 14.8% 96.2 23/23
09 Hero gallery wall 48.7% 21.7 23/23

Where the Values Change the Pattern — Tooth Counts, Pen Offset, Spin Speed

Three constants decide most of what you see. The tooth counts set petals and turns through the divisor, the pen ratio (PEN_TIP_RATIO or PEN_RADIUS_RATIO) sets how far the pen sits from the small gear's center, and the cycle or speed constant sets how fast the line is drawn. None of them touches the loop structure, because each only scales a number that point(t) already reads, the same way Fourier Epicycles, 9 Ways (No Library) treats its circle radii as plain inputs. The variant table further down changes one value per row so you can watch the pattern shift on its own.

Where It Breaks — Curves That Never Close, Narrow Screens, Overlapping Lines

With whole tooth counts the curve always closes after GEAR_ROLL_TEETH / gcd turns, so it only fails to close if you feed in an irrational ratio such as Math.PI * 10 teeth, and then the line never returns to its start and the loops keep piling up. A coprime pair like 23 and 25 is the other trap: it closes, but only after 23 turns, and because SPEED = TOTAL_T / CYCLE_SECONDS the pen then sweeps nearly eight times faster than a 3-turn curve in the same six-second cycle. On a narrow screen fit() shrinks the canvas and the line width follows scale, but a dense curve such as item 09's 25-turn cell packs 25 loops into one small cell, so the lines crowd. The nine index.html files behind every item above come zipped together with password 899p5t2j so you can change the tooth counts yourself and see where your own ratio breaks.

Three Variants and Accessibility

Variant Value Effect
Adjust the gear tooth-count ratio 02 GEAR_TEETH_OUTER 96 → 60 Moving it from 96 to 60 keeps the greatest common divisor with the 36-tooth rolling gear at 12, so the curve still closes after 3 turns, but the petal count drops from 8 to 5 and the pattern opens up.
Adjust the pen-hole position ratio 03 PEN_RADIUS_RATIO 0.6 → 0.95 A low value keeps the pen near the small gear's center for round, gentle loops, while raising it to 0.95 moves the pen out to the rim, so the curve folds sharply and reaches far outward.
Adjust the trail afterglow length 05 TRAIL_FADE_FRAMES 240 → 60 Dropping it to 60 fades the drawn line quickly, so only the pen's most recent path stays visible, while a larger value keeps the pattern lingering so several turns of the trace stack up on one screen.

Every one of the nine Canvas API scenes reads prefers-reduced-motion once through matchMedia before it starts its requestAnimationFrame loop. With reduced motion on, items 01 to 04 draw the finished curve once, 05 and 09 run their trail steps ahead of time and show a settled frame, 06 holds at its 84% frame, 07 renders once, and 08 shows one still view of the disc. All nine roots carry tabindex="0", but item 07 is the only one that listens for input: pointer moves and presses, plus the left and right arrow keys, change the ratio even when the animation is off.

What Does Spirograph Math Actually Calculate in the Code?

Each frame asks one question: where is the pen when the rolling gear has turned by angle t? Inside the ring the answer is (R - r) times cosine and sine of t plus a pen term that spins at k = (R - r) / r, and outside it is the same shape with the radii added, which is all that separates items 01 and 02. The background gradients and the faint fill inside the curve in 04 are a presentation layer, while the coordinates, the divisor, and TURNS = GEAR_ROLL_TEETH / gcd are the calculation, and the same computed-not-recorded idea drives HTML5 Canvas Animation: 9 Warp & Droplet Effects and Wave Interference Simulation: 9 Canvas Scenes.

Frequently Asked Questions Before You Use These Spirograph Patterns

Can this spirograph art generator draw with any tooth counts I type?

Yes, as long as they are whole numbers: change the two constants at the top of any item and the divisor, the axis count and the closing turn count follow, because they are computed from those two values.

Why does item 09 use pairs like 23 and 25 instead of round numbers?

A coprime pair has a greatest common divisor of 1, so the curve takes the longest possible number of turns to close and the cell fills with many loops. Round numbers share a divisor and close early into a sparse flower.

Can I use one of these as a spirograph gear generator on my own page?

Yes. Each item's canvas, its constants and its drawing code live together in its own index.html, so copying one item does not pull in code from the other eight.

Enter the archive password

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