Last active
May 31, 2023 05:54
-
-
Save bellbind/23fdb64e7fa41fe96440a67508b876bb to your computer and use it in GitHub Desktop.
[JavaScript][browser] Epicycles of a single-stroke by DFT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
demo: https://gist.githack.com/bellbind/23fdb64e7fa41fe96440a67508b876bb/raw/index.html