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();
}