closability · integer ratio

Spiky Lissajous

Classic Lissajous is x = sin(A·θ), y = sin(B·θ). Swap sin for a spiky or sawtooth-ish p5.waves formula and the curve still closes - as long as θ sweeps one full period of that wave and A, B are integers. A pen traces the path over one cycle; watch it land back on the white start dot. Click to cycle the spiky wave.

Core snippet
// Periods measured via docs/periodicity.html
const SPIKY = [
  { name: 'sharp peaks',   period: Math.PI * 10 },
  { name: 'batman',        period: Math.PI * 20 },
  { name: 'zig-zag sine',  period: Math.PI * 10 },
  { name: 'up down pulse', period: Math.PI * 10 }
];
const RATIO_A = 3;
const RATIO_B = 5;

// Freeze phase for one cycle - one sweep draws a true closed loop.
const cycleId  = floor(millis() / CYCLE_MS);
const frozenMs = cycleId * CYCLE_MS;
const phaseX   = frozenMs * 0.00015;
const phaseY   = frozenMs * 0.00021;
const prog     = (millis() % CYCLE_MS) / CYCLE_MS;
const drawnTo  = floor(prog * SEGMENTS);

// Sample every vertex of the chosen wave, once per frame.
const xs = new Array(SEGMENTS + 1);
const ys = new Array(SEGMENTS + 1);
for (let i = 0; i <= SEGMENTS; i++) {
  const theta = (i / SEGMENTS) * choice.period;
  xs[i] = radius * Waves.wave(RATIO_A * theta + phaseX * choice.period, {
    wave: choice.name, amplitude: 1
  });
  ys[i] = radius * Waves.wave(RATIO_B * theta + phaseY * choice.period + choice.period * 0.25, {
    wave: choice.name, amplitude: 1
  });
}

// Bright pen trail up to the current progress.
stroke(choice.color);
beginShape();
for (let i = 0; i <= drawnTo; i++) vertex(xs[i], ys[i]);
endShape();

// Home dot + pen cursor: when drawnTo hits SEGMENTS they land on the same pixel.
fill(255); circle(xs[0], ys[0], 12);
fill(18);  circle(xs[0], ys[0], 6);
fill(255); circle(xs[drawnTo], ys[drawnTo], 8);