Add fourier visualizer
This commit is contained in:
parent
0008269b8d
commit
bc02eec44d
44
ft-visualizer/index.html
Normal file
44
ft-visualizer/index.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Simponic's FT Visualizer</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#calculator {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canvas-holder {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1300px;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="calculator"></div>
|
||||
<div class="canvas-holder">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://www.desmos.com/api/v1.7/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6"></script>
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
301
ft-visualizer/js/script.js
Normal file
301
ft-visualizer/js/script.js
Normal file
@ -0,0 +1,301 @@
|
||||
const RENDER_TYPE = {
|
||||
LATEX: 1,
|
||||
FUNC: 2,
|
||||
};
|
||||
const THRESHOLD = 1e-10;
|
||||
const LATEX_SIGFIGS = 8;
|
||||
const FONT = "Courier New";
|
||||
const FONT_HEIGHT_PX = 16;
|
||||
const DX = 3;
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
let state = {};
|
||||
const initializeState = () => {
|
||||
const xLabels = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sept",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
"Jan",
|
||||
];
|
||||
const yLabels = ["Great", "Good", "Meh", "Bad", "Horrible"];
|
||||
return {
|
||||
width: canvas.parentElement.clientWidth,
|
||||
height: canvas.parentElement.clientHeight,
|
||||
xLabels,
|
||||
yLabels,
|
||||
gridBox: { width: 0.9, height: 0.9 },
|
||||
heights: Array(xLabels.length * DX - 1).fill(0),
|
||||
};
|
||||
};
|
||||
|
||||
const dft = (
|
||||
heights,
|
||||
render = RENDER_TYPE.LATEX,
|
||||
threshold = THRESHOLD,
|
||||
sigfigs = LATEX_SIGFIGS
|
||||
) => {
|
||||
const n = heights.length;
|
||||
return Array(n)
|
||||
.fill()
|
||||
.map((x, w) => {
|
||||
const rate = -2 * Math.PI * w;
|
||||
const s = heights.reduce(
|
||||
(a, x, i) => ({
|
||||
re: a.re + x * Math.cos((rate * i) / n),
|
||||
im: a.im + x * Math.sin((rate * i) / n),
|
||||
}),
|
||||
{ re: 0, im: 0 }
|
||||
);
|
||||
|
||||
Object.entries(s).forEach(
|
||||
([key, value]) =>
|
||||
(s[key] = ((Math.abs(value) < threshold ? 0 : 1) * value) / n)
|
||||
);
|
||||
|
||||
const amp = Math.sqrt(s.re * s.re + s.im * s.im);
|
||||
const phase = Math.atan2(s.im, s.re);
|
||||
|
||||
switch (render) {
|
||||
case RENDER_TYPE.LATEX:
|
||||
return `${amp.toPrecision(sigfigs)}\\cos\\left(${w}x\\frac{2}{${
|
||||
n / DX
|
||||
}}\\pi+${phase.toPrecision(sigfigs)}\\right)`;
|
||||
case RENDER_TYPE.FUNC:
|
||||
return (t) => amp * Math.cos(w * t + phase);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resizeCanvas = ({ width, height }) => {
|
||||
canvas.width = width;
|
||||
canvas.style.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.height = height;
|
||||
};
|
||||
|
||||
const loop = () => {
|
||||
const stateChanges = Object.keys(state.diff);
|
||||
if (stateChanges.length > 0) {
|
||||
state = { ...state, ...state.diff };
|
||||
if (
|
||||
state.diff.width ||
|
||||
state.diff.height ||
|
||||
state.diff.xLabels ||
|
||||
state.diff.yLabels ||
|
||||
state.diff.gridBox
|
||||
) {
|
||||
resizeCanvas(state.diff);
|
||||
state.gridBoxWidth = state.gridBox.width * state.width;
|
||||
state.gridBoxHeight = state.gridBox.height * state.height;
|
||||
|
||||
state.maxYLabelWidth = state.yLabels.reduce(
|
||||
(a, label) => Math.max(ctx.measureText(label).width, a),
|
||||
-Infinity
|
||||
);
|
||||
|
||||
state.topLeftGridPos = {
|
||||
x: (state.width - state.gridBoxWidth) / 2 + state.maxYLabelWidth,
|
||||
y: (state.height - state.gridBoxHeight) / 2,
|
||||
};
|
||||
|
||||
state.bottomRightGridPos = {
|
||||
x: state.topLeftGridPos.x + state.gridBoxWidth,
|
||||
y: state.topLeftGridPos.y + state.gridBoxHeight,
|
||||
};
|
||||
}
|
||||
if (Object.keys(state.diff).length) draw(state);
|
||||
if (state.diff.heights) drawDesmos();
|
||||
state.diff = {};
|
||||
}
|
||||
|
||||
requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
const drawLine = (pos1, pos2) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pos1.x, pos1.y);
|
||||
ctx.lineTo(pos2.x, pos2.y);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const drawDividers = (
|
||||
xDividers,
|
||||
yDividers,
|
||||
topLeftGridPos,
|
||||
bottomRightGridPos,
|
||||
yLabelPadding
|
||||
) => {
|
||||
xDividers.forEach(({ label, position }) => {
|
||||
ctx.fillText(
|
||||
label,
|
||||
position.x - ctx.measureText(label).width / 2,
|
||||
position.y
|
||||
);
|
||||
drawLine(
|
||||
{ ...position, y: topLeftGridPos.y },
|
||||
{ ...position, y: bottomRightGridPos.y }
|
||||
);
|
||||
});
|
||||
yDividers.forEach(({ label, position }) => {
|
||||
ctx.fillText(
|
||||
label,
|
||||
topLeftGridPos.x - yLabelPadding - ctx.measureText(label).width,
|
||||
position.y
|
||||
);
|
||||
drawLine(
|
||||
{ ...position, x: topLeftGridPos.x },
|
||||
{ ...position, x: bottomRightGridPos.x }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const draw = ({
|
||||
heights,
|
||||
gridBoxWidth,
|
||||
gridBoxHeight,
|
||||
topLeftGridPos,
|
||||
bottomRightGridPos,
|
||||
maxYLabelWidth,
|
||||
xLabels,
|
||||
yLabels,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
|
||||
|
||||
const xDividers = xLabels.map((label, i) => ({
|
||||
label,
|
||||
position: {
|
||||
x: topLeftGridPos.x + (gridBoxWidth / (xLabels.length - 1)) * i,
|
||||
y: bottomRightGridPos.y + FONT_HEIGHT_PX,
|
||||
},
|
||||
}));
|
||||
|
||||
const yDividers = yLabels.map((label, i) => ({
|
||||
label,
|
||||
position: {
|
||||
x: 0,
|
||||
y: topLeftGridPos.y + (gridBoxHeight / (yLabels.length - 1)) * i,
|
||||
},
|
||||
}));
|
||||
|
||||
drawDividers(xDividers, yDividers, topLeftGridPos, bottomRightGridPos, 12);
|
||||
let dx = (bottomRightGridPos.x - topLeftGridPos.x) / (heights.length - 1);
|
||||
const prevStrokeStyle = ctx.strokeStyle;
|
||||
ctx.strokeStyle = "red";
|
||||
for (let i = 0; i < heights.length; ++i) {
|
||||
const x = dx * i + topLeftGridPos.x;
|
||||
drawLine(
|
||||
{ x, y: (gridBoxHeight / 2) * (1 - heights[i]) + topLeftGridPos.y },
|
||||
{
|
||||
x: x + dx,
|
||||
y: (gridBoxHeight / 2) * (1 - heights[i + 1]) + topLeftGridPos.y,
|
||||
}
|
||||
);
|
||||
}
|
||||
ctx.strokeStyle = prevStrokeStyle;
|
||||
};
|
||||
|
||||
const calculator = Desmos.GraphingCalculator(
|
||||
document.getElementById("calculator"),
|
||||
{
|
||||
expressionsCollapsed: true,
|
||||
autosize: true,
|
||||
}
|
||||
);
|
||||
calculator.setMathBounds({
|
||||
left: -0.8,
|
||||
right: 12,
|
||||
bottom: -3,
|
||||
top: 3,
|
||||
});
|
||||
|
||||
const drawDesmos = () => {
|
||||
const equations = dft(state.heights);
|
||||
|
||||
calculator.setExpression({
|
||||
id: `graph-total`,
|
||||
latex: equations.map((_x, i) => `y_{${i}}`).join(" + "),
|
||||
});
|
||||
equations.forEach((x, i) =>
|
||||
calculator.setExpression({
|
||||
id: `graph${i}`,
|
||||
latex: `y_{${i}}=${x}`,
|
||||
hidden: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
let isDown = false;
|
||||
canvas.addEventListener(
|
||||
"mousedown",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
isDown = true;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
canvas.addEventListener(
|
||||
"mouseup",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
isDown = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
canvas.addEventListener(
|
||||
"mousemove",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
if (isDown) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const [x, y] = [e.clientX - rect.left, e.clientY - rect.top];
|
||||
|
||||
const {
|
||||
topLeftGridPos,
|
||||
bottomRightGridPos,
|
||||
gridBoxWidth,
|
||||
gridBoxHeight,
|
||||
heights,
|
||||
} = state;
|
||||
const delta = Math.ceil(gridBoxWidth / heights.length);
|
||||
const bin = Math.min(
|
||||
Math.round(Math.max(x - topLeftGridPos.x, 0) / delta),
|
||||
heights.length - 1
|
||||
);
|
||||
heights[bin] = Math.min(
|
||||
Math.max(1 - (2 * (y - topLeftGridPos.y)) / gridBoxHeight, -1),
|
||||
1
|
||||
);
|
||||
state.diff.heights = heights;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
state.diff = {
|
||||
...state.diff,
|
||||
width: canvas.parentElement.clientWidth,
|
||||
height: canvas.parentElement.clientHeight,
|
||||
};
|
||||
});
|
||||
|
||||
(() => {
|
||||
state.diff = initializeState();
|
||||
window.requestAnimationFrame(loop);
|
||||
})();
|
16
index.html
16
index.html
@ -11,18 +11,17 @@
|
||||
<script src="https://kit.fontawesome.com/d7e97ed48f.js" crossorigin="anonymous"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-container animate__animated animate__fadeIn">
|
||||
<img src="images/profile.png" class="profile-picture">
|
||||
<p>
|
||||
<h3>
|
||||
👋 Hello, I'm Simponic!
|
||||
<br>
|
||||
📖 This page hosts strictly static content.
|
||||
<br>
|
||||
🔔 My "real website" is at <a href="https://simponic.xyz">simponic.xyz</a>.
|
||||
</p>
|
||||
</h3>
|
||||
<div class="projects-grid">
|
||||
<div class="project" onclick="window.location='dvd-logo/index.html'">
|
||||
<div class="project-logo-container">
|
||||
@ -67,6 +66,17 @@
|
||||
<p>Zoom, pan, and "c" complex changes in this fun GPU-accelerated playground!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project" onclick="window.location='ft-visualizer/index.html'">
|
||||
<div class="project-logo-container">
|
||||
<i class="fa-solid fa-wave-square"></i>
|
||||
</div>
|
||||
|
||||
<div class="project-body">
|
||||
<h1>Discrete Fourier Visualizer</h1>
|
||||
<p>Draw how your year has gone and view a reactive graph containing its DFT by dragging your mouse over the canvas!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
Loading…
Reference in New Issue
Block a user