Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active May 31, 2023 05:54
Show Gist options
  • Save bellbind/23fdb64e7fa41fe96440a67508b876bb to your computer and use it in GitHub Desktop.
Save bellbind/23fdb64e7fa41fe96440a67508b876bb to your computer and use it in GitHub Desktop.
[JavaScript][browser] Epicycles of a single-stroke by DFT
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Epicycles of a single-stroke by DFT</title>
<script type="module" src="./main.js"></script>
</head>
<body style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
<h1>Draw a line</h1>
<canvas id="canvas" style="border: solid; width: 75svmin; height: 75svmin; "></canvas>
<div><label>ordered by size<input id="by-size" type="checkbox" /></label></div>
</body>
</html>
// complex number and DFT
const cabs = c => Math.hypot(c.re, c.im);
const carg = c => Math.atan2(c.im, c.re);
const cadd = (a, b) => ({re: a.re + b.re, im: a.im + b.im});
const cmul = (a, b) => ({re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re});
const ndiv = (n, c) => ({re: c.re / n, im: c.im / n});
const expi = th => ({re: Math.cos(th), im: Math.sin(th)});
const csum = cs => cs.reduce((s, c) => cadd(s, c), {re: 0, im: 0});
const range = n => [...Array(n).keys()];
const dft = cs => {
const n = cs.length, t = -2 * Math.PI / n;
return range(n).map(k => csum(cs.map((c, i) => cmul(c, expi(t * i * k)))));
};
// canvas events
const canvas = document.querySelector("#canvas");
const c2d = canvas.getContext("2d");
let dragging = false;
let points = [];
const toPoint = ev => {
const {x, y, width, height} = canvas.getBoundingClientRect();
const re = (ev.clientX - x)- width / 2;
const im = height / 2 - (ev.clientY - y);
return {re, im};
};
canvas.addEventListener("pointerdown", ev => {
dragging = true;
points = [toPoint(ev)];
});
canvas.addEventListener("pointermove", ev => {
if (dragging) {
points.push(toPoint(ev));
const {width, height} = canvas.getBoundingClientRect();
[canvas.width, canvas.height] = [width, height];
c2d.clearRect(0, 0, canvas.width, canvas.height);
drawLine(points, "hsla(240, 75%, 25%, 0.5)", 3);
}
});
const run = ev => {
if (dragging) {
points.push(toPoint(ev));
dragging = false;
startEpicycles();
}
};
canvas.addEventListener("pointerup", run);
canvas.addEventListener("pointerout", run);
// rendering
const startEpicycles = () => {
// DFT then pre divide with size: divide norm, keep angle
const cycles = dft(points).map(c => ndiv(points.length, c));
const loop = (k, history) => {
if (dragging) return;
if (k === 0) history = [];
const {width, height} = canvas.getBoundingClientRect();
[canvas.width, canvas.height] = [width, height];
c2d.clearRect(0, 0, canvas.width, canvas.height);
drawLine(points, "hsla(240, 75%, 25%, 0.25)", 3);
const cur = drawEpicycles(cycles, k);
const current = [...history, cur];
drawLine(current, "hsla(240, 75%, 25%, 1)", 5);
setTimeout(loop, 100, (k + 1) % cycles.length, current);
};
setTimeout(loop, 100, 0, []);
};
const drawLine = (points, style, w) => {
c2d.save();
c2d.translate(canvas.width / 2, canvas.height / 2);
c2d.scale(1, -1);
c2d.beginPath();
const [head, ...rest] = points;
c2d.moveTo(head.re, head.im);
for (const {re, im} of rest) c2d.lineTo(re, im);
c2d.strokeStyle = style;
c2d.lineWidth = w;
c2d.stroke();
c2d.restore();
};
const argsort = (arr, cmp = (a, b) => a - b) =>
arr.map((e, i) => [e, i]).sort(([a], [b]) => cmp(a, b)).map(e => e[1]);
const drawEpicycles = (cycles, k) => {
const n = cycles.length, t = 2 * Math.PI / n;
const elems = cycles.map((c, i) => cmul(c, expi(t * i * k))); // sum(elems) == point[k]: iDFT before summarized
const order = document.getElementById("by-size").checked ?
argsort(cycles.slice(1).map(cabs), (a, b) => b - a).map(i => i + 1) : range(n).slice(1);
c2d.save();
c2d.translate(canvas.width / 2, canvas.height / 2);
c2d.scale(1, -1);
let p = elems[0]; // freq-0 cycle as offset from center
for (const i of order) {
const elem = elems[i], cycle = cycles[i];
const abs = cabs(elem); // = cabs(cycle)
// orbit
c2d.beginPath();
c2d.arc(p.re, p.im, abs, 0, 2 * Math.PI);
c2d.strokeStyle = "hsla(0, 75%, 25%, 0.25)";
c2d.lineWidth = 1;
c2d.stroke();
// track
const start = carg(cycle), current = start + t * i * k; // = carg(elem)
const cw = i > cycles.length / 2; //NOTE: cw as higher freq cycles
c2d.beginPath();
c2d.arc(p.re, p.im, abs, start, current, cw); //NOTE: c2d.arc()'s ccw option become cw when reversed y-axis
c2d.strokeStyle = "hsla(0, 75%, 25%, 0.5)";
c2d.lineWidth = 2;
c2d.stroke();
// bone
c2d.beginPath();
c2d.moveTo(p.re, p.im);
p = cadd(p, elem);
c2d.lineTo(p.re, p.im);
c2d.strokeStyle = "hsla(120, 75%, 25%, 0.5)";
c2d.lineWidth = 2;
c2d.stroke();
}
c2d.restore();
return p;
};
@bellbind
Copy link
Author

bellbind commented May 23, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment