Chris Collins

Lab · Playground

A harmonograph drawing machine

Four decaying pendulums steer a pen; frequencies near simple ratios draw the spirograph-like curves Victorian harmonographs were famous for.

Code blocks on this page are editable — change them and press Run.

A harmonograph is a Victorian parlour machine: pendulums swing a pen and a table in different directions, and the pen draws as everything slowly runs down. Mathematically each pendulum is a damped sine wave, and the pen’s position is the sum of two of them in each direction:

x(t) = A1·sin(f1·t + p1)·e^(−d1·t) + A2·sin(f2·t + p2)·e^(−d2·t)
y(t) = A3·sin(f3·t + p3)·e^(−d3·t) + A4·sin(f4·t + p4)·e^(−d4·t)

With no damping and one pendulum per axis this is a Lissajous figure — the shape you see on an oscilloscope fed two sine waves. When the frequencies are in a simple ratio (2 : 3, 3 : 4) the curve closes on itself; nudge one slightly off and the figure slowly rotates, which is what makes real harmonograph drawings look like woven ribbons. The damping pulls everything into the centre.

The machine draws a new pattern every few seconds with frequencies chosen near small whole-number ratios. Click to start a new one at once.

Damped pendulums
// Things to change: RATIOS, DETUNE, DAMP, and SPEED.
const RATIOS = [1, 2, 3, 4, 5];   // frequencies are picked from these
const DETUNE = 0.012;             // how far off the exact ratio — the source of the slow rotation
const DAMP = [0.004, 0.012];      // damping range per pendulum
const SPEED = 160;                // points drawn per frame
const DT = 0.02;                  // time step between points

let P, t, prev, hue;

function setup() {
  createCanvas(min(windowWidth, 640), 320);
  colorMode(HSB, 360, 100, 100, 100);
  newDrawing();
}

function pendulum(amp) {
  return { a: amp, f: random(RATIOS) + random(-DETUNE, DETUNE), p: random(TWO_PI), d: random(DAMP[0], DAMP[1]) };
}

function newDrawing() {
  const r = min(width, height) * 0.24;
  P = [pendulum(r), pendulum(r), pendulum(r), pendulum(r)];
  t = 0;
  prev = null;
  hue = random(360);
  background(35, 12, 97);
}

const swing = (q, t) => q.a * sin(q.f * t + q.p) * exp(-q.d * t);

function draw() {
  translate(width / 2, height / 2);
  strokeWeight(0.8);
  for (let i = 0; i < SPEED; i++) {
    const x = swing(P[0], t) + swing(P[1], t);
    const y = swing(P[2], t) + swing(P[3], t);
    if (prev) {
      // Colour drifts along the drawing; alpha keeps overlapping passes readable.
      stroke((hue + t * 0.4) % 360, 70, 55, 45);
      line(prev.x, prev.y, x, y);
    }
    prev = { x, y };
    t += DT;
  }
  // Start over once the pen has nearly stopped.
  const left = P.reduce((s, q) => s + q.a * exp(-q.d * t), 0);
  if (left < 6) newDrawing();
}

function mousePressed() {
  if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) newDrawing();
}

Things to change

  • RATIOS = [2, 3] restricts the machine to 2 : 3 and 3 : 2 figures — trefoils and pretzels.

  • DETUNE = 0 gives closed, perfectly repeating Lissajous figures; 0.05 turns them into fast spirals.

  • DAMP = [0, 0] stops the decay: a pure Lissajous figure that never fades (set a limit on t then, or it never starts over).

  • For a rotary harmonograph, add a fifth term that turns the paper: replace x, y with x*cos(0.01*t) - y*sin(0.01*t) and x*sin(0.01*t) + y*cos(0.01*t).

  • SPEED — 2000 draws each figure almost instantly; 20 lets you watch the pen move.