Published
Edited
Jul 11, 2019
5 forks
80 stars
Insert cell
Insert cell
viewof m = {
const context = DOM.context2d(width, height);
const scale = width / viewbox.width * 1.8;
context.lineCap = "round";
context.lineJoin = "round";
for (let m = 0; true; m = (m + 1) % M) {
context.save();
context.fillStyle = "rgba(255, 255, 255, 0.04)";
context.fillRect(0, 0, width, height);
context.translate(width / 2, height / 2);
context.scale(scale, scale);
context.translate(-viewbox.width / 2, -viewbox.height / 2);
context.beginPath();
for (let t = 0; t < q; ++t) {
const a = t * 2 / q * Math.PI;
let p = [0, 0];
for (let i = 0; i < m; ++i) {
p = add(p, mul(DFT[i], expim(a * K[i])));
}
if (t === 0) context.moveTo(...p);
else context.lineTo(...p);
}
context.closePath();
context.lineWidth = 1.5 / scale;
context.stroke();
context.restore();
context.canvas.value = m;
yield context.canvas;
}
}
Insert cell
Insert cell
svg = fetch("https://gist.githubusercontent.com/mbostock/a4fd7a68925d4039c22996cc1d4862ce/raw/d813a42956d311d73fee336e1b5aac899c835883/fourier.svg")
.then(response => response.text())
.then(text => (new DOMParser).parseFromString(text, "image/svg+xml"))
.then(svg => svg.documentElement)
Insert cell
P = {
const path = svg.querySelector("path");
const pathSampler = new PathSampler(path.getAttribute("d"));
return Array.from({length: N}, (_, i) => pathSampler.pointAt(i / N));
}
Insert cell
K = Int16Array.from({length: M}, (_, i) => (1 + i >> 1) * (i & 1 ? -1 : 1))
Insert cell
DFT = Array.from(K, k => {
let x = [0, 0];
for (let i = 0; i < N; ++i) {
x = add(x, mul(P[i], expim(k * i * 2 / N * -Math.PI)));
}
return [x[0] / N, x[1] / N];
})
Insert cell
N = 2000 // number of input samples
Insert cell
M = 350 // maximum number of epicycles
Insert cell
q = 2000 // number of output samples
Insert cell
function abs([re, im]) {
return Math.hypot(re, im);
}
Insert cell
function expim(im) {
return [Math.cos(im), Math.sin(im)];
}
Insert cell
function add([rea, ima], [reb, imb]) {
return [rea + reb, ima + imb];
}
Insert cell
function mul([rea, ima], [reb, imb]) {
return [rea * reb - ima * imb, rea * imb + ima * reb];
}
Insert cell
viewbox = svg.viewBox.baseVal
Insert cell
height = width
Insert cell
class PathSampler {
constructor(source) {
const data = PathData.parse(source, {normalize: true});
const d0 = data[0];
const d1 = data[data.length - 1];
if (d0.type !== "M") throw new Error("expected M");
if (d1.type !== "Z") throw new Error("expected Z");
const segments = Array.from({length: data.length - 2}, (_, i) => {
const {type, values} = data[i + 1];
switch (type) {
case "C": return new C(data[i].values.slice(-2).concat(values));
case "L": return new L(data[i].values.slice(-2).concat(values));
}
});
const start = d0.values.slice(0, 2);
const end = data[data.length - 2].values.slice(-2);
if (start[0] !== end[0] || start[1] !== end[1]) {
segments.push(new L(end.concat(start)));
}
this.segments = segments;
}
pointAt(t) {
const n = this.segments.length;
if (!((t *= n) >= t)) return;
const i = Math.max(0, Math.min(n - 1, Math.floor(t)));
return this.segments[i].pointAt(t % 1);
}
}
Insert cell
class L {
constructor(values) {
this.values = values;
}
pointAt(t) {
const [x0, y0, x1, y1] = this.values;
const a = t;
const b = 1 - t;
return [
a * x0 + b * x1,
a * y0 + b * y1
];
}
}
Insert cell
class C {
constructor(values) {
this.values = values;
}
pointAt(t) {
const [x0, y0, x1, y1, x2, y2, x3, y3] = this.values;
const a = (1 - t) ** 3;
const b = 3 * (1 - t) ** 2 * t;
const c = 3 * (1 - t) * t ** 2;
const d = t ** 3;
return [
a * x0 + b * x1 + c * x2 + d * x3,
a * y0 + b * y1 + c * y2 + d * y3
];
}
}
Insert cell
PathData = require("path-data@0.0.2")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more