Gage Rowden
  • Gage Rowden, M.S.
  • Useful Links
  • Projects
  • Publications
  • Utilities
    • ASCII Image Converter
    • L5R Dice Roller
    • Mandelbrot Viewer
    • Harmonograph
    • BMG Formatter
Harmonograph
function initSlider(label, min, max, step, value) {
  return Inputs.range([min, max], {
    label: label,
    value: value ?? Math.random() * (max - min) + min,
    step: step
  });
}
TWO_PI = 2 * Math.PI

// Equivalent of setup()/the slider "input" listener/renderHarmonograph().
function drawHarmonograph() {

  function initContext(cntnr, cnvs, ctxt, txFrac, tyFrac) {
    const width = cntnr.clientWidth;
    const height = cntnr.clientHeight;
    cnvs.width = width;
    cnvs.height = height;
    ctxt.setTransform(1, 0, 0, 1, 0, 0);
    ctxt.fillStyle = "black";
    ctxt.fillRect(0, 0, width, height);
    ctxt.translate(width * txFrac, height * tyFrac);
    ctxt.strokeStyle = `hsl(${hue}, ${sat}%, ${lit}%)`;
    ctxt.lineWidth = weight;
    ctxt.shadowBlur = glow;
    ctxt.shadowColor = `hsl(${hue}, ${sat}%, ${lit}%)`;
    ctxt.lineJoin = "round";
  }

  const container = document.getElementById("harmonograph-container");
  const canvas = document.getElementById("harmonograph-canvas");
  const ctx = canvas.getContext("2d");
  initContext(container, canvas, ctx, 0.5, 0.5);

  // Component-wave canvases plot amplitude against time, so time is
  // mapped linearly across the canvas rather than centered like the
  // amplitude axis.
  const x1Container = document.getElementById("x1-container");
  const x1Canvas = document.getElementById("x1-canvas");
  const x1ctx = x1Canvas.getContext("2d");
  initContext(x1Container, x1Canvas, x1ctx, 0, 0.5);

  const x2Container = document.getElementById("x2-container");
  const x2Canvas = document.getElementById("x2-canvas");
  const x2ctx = x2Canvas.getContext("2d");
  initContext(x2Container, x2Canvas, x2ctx, 0, 0.5);

  const y1Container = document.getElementById("y1-container");
  const y1Canvas = document.getElementById("y1-canvas");
  const y1ctx = y1Canvas.getContext("2d");
  initContext(y1Container, y1Canvas, y1ctx, 0.5, 0);

  const y2Container = document.getElementById("y2-container");
  const y2Canvas = document.getElementById("y2-canvas");
  const y2ctx = y2Canvas.getContext("2d");
  initContext(y2Container, y2Canvas, y2ctx, 0.5, 0);

  const freq1 = Math.pow(10, f1);
  const freq2 = Math.pow(10, f2);
  const freq3 = Math.pow(10, f3);
  const freq4 = Math.pow(10, f4);
  const phase1 = p1 * TWO_PI;
  const phase2 = p2 * TWO_PI;
  const phase3 = p3 * TWO_PI;
  const phase4 = p4 * TWO_PI;


  ctx.beginPath();
  x1ctx.beginPath();
  x2ctx.beginPath();
  y1ctx.beginPath();
  y2ctx.beginPath();
  const maxT = ITERATIONS * DT;
  let t = 0;
  for (let i = 0; i < ITERATIONS; i++) {
    const x1 = a1 * Math.sin(t * freq1 + phase1) * Math.exp(-d1 * t);
    const x2 = a2 * Math.sin(t * freq2 + phase2) * Math.exp(-d2 * t);
    const y1 = a3 * Math.sin(t * freq3 + phase3) * Math.exp(-d3 * t);
    const y2 = a4 * Math.sin(t * freq4 + phase4) * Math.exp(-d4 * t);
    const x = x1 + x2;
    const y = y1 + y2;
    const timeX = (t / maxT) * x1Canvas.width;
    const timeY = (t / maxT) * y1Canvas.height;
    if (i === 0) {
      ctx.moveTo(x, y);
      x1ctx.moveTo(timeX, x1);
      x2ctx.moveTo(timeX, x2);
      y1ctx.moveTo(y1, timeY);
      y2ctx.moveTo(y2, timeY);
    } else {
      ctx.lineTo(x, y);
      x1ctx.lineTo(timeX, x1 / 2);
      x2ctx.lineTo(timeX, x2 / 2);
      y1ctx.lineTo(y1 / 2, timeY);
      y2ctx.lineTo(y2 / 2, timeY);
    }
    t += DT;
  }
  ctx.stroke();
  x1ctx.stroke();
  x2ctx.stroke();
  y1ctx.stroke();
  y2ctx.stroke();

}
// Equivalent of p.windowResized(): redraw whenever the card is resized.
{
  const container = document.getElementById("harmonograph-container");
  const ro = new ResizeObserver(() => drawHarmonograph());
  ro.observe(container);
}
// Equivalent of the slider "input" listener: redraw whenever a control changes.
{
  f1; f2; f3; f4;
  a1; a2; a3; a4;
  d1; d2; d3; d4;
  p1; p2; p3; p4;
  weight; glow;
  drawHarmonograph();
}
Frequency
viewof f1 = initSlider("", 0, 3, 0.001);
viewof f2 = initSlider("", 0, 3, 0.001);
viewof f3 = initSlider("", 0, 3, 0.001);
viewof f4 = initSlider("", 0, 3, 0.001);
Amplitude
viewof a1 = initSlider("", 0, 200, 0.1);
viewof a2 = initSlider("", 0, 200, 0.1);
viewof a3 = initSlider("", 0, 200, 0.1);
viewof a4 = initSlider("", 0, 200, 0.1);
Damping
viewof d1 = initSlider("", 0, 1, 0.001);
viewof d2 = initSlider("", 0, 1, 0.001);
viewof d3 = initSlider("", 0, 1, 0.001);
viewof d4 = initSlider("", 0, 1, 0.001);
Phase
viewof p1 = initSlider("", 0, 1, 0.001);
viewof p2 = initSlider("", 0, 1, 0.001);
viewof p3 = initSlider("", 0, 1, 0.001);
viewof p4 = initSlider("", 0, 1, 0.001);
Style
viewof weight = initSlider("Line Weight", 0.5, 5, 0.1, 1.5);
viewof glow = initSlider("Glow", 0, 50, 1, 8);
viewof hue = initSlider("Hue", 0, 255, 1, 135);
viewof sat = initSlider("Saturation", 0, 100, 1, 100);
viewof lit = initSlider("Lightness", 0, 100, 1, 79);
viewof ITERATIONS = initSlider("Iterations", 100, 10_000, 10, 10_000);
viewof DT = initSlider("Δt", 0.001, 0.1, 0.001, 0.001);
viewof saveButton = Inputs.button("Save PNG", {
  value: null,
  reduce: () => {
    document.getElementById("harmonograph-canvas").toBlob((blob) => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "harmonograph.png";
      a.click();
      URL.revokeObjectURL(url);
    });
  }
});