simponic.xyz/ft-visualizer/js/script.js

295 lines
6.6 KiB
JavaScript
Raw Normal View History

2022-12-21 01:20:41 -05:00
const RENDER_TYPE = {
LATEX: 1,
FUNC: 2,
};
2022-12-21 02:23:50 -05:00
const THRESHOLD = 1e-12;
2022-12-21 01:20:41 -05:00
const FONT = "Courier New";
2022-12-21 02:23:50 -05:00
const FONT_HEIGHT_PX = 24;
const DX = 4;
2022-12-21 01:20:41 -05:00
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,
2022-12-21 02:23:50 -05:00
yLabelPadding: 12,
2022-12-21 01:20:41 -05:00
heights: Array(xLabels.length * DX - 1).fill(0),
};
};
2022-12-21 02:23:50 -05:00
const dft = (heights, render = RENDER_TYPE.LATEX, threshold = THRESHOLD) => {
2022-12-21 01:20:41 -05:00
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:
2022-12-21 02:23:50 -05:00
return `${amp}\\cos\\left(${w}\\frac{${
2 * DX
}\\pi}{${n}}x+${phase}\\right)`;
2022-12-21 01:20:41 -05:00
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 ||
2022-12-21 02:23:50 -05:00
state.diff.yLabels
2022-12-21 01:20:41 -05:00
) {
resizeCanvas(state.diff);
2022-12-21 02:23:50 -05:00
ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
2022-12-21 01:20:41 -05:00
state.maxYLabelWidth = state.yLabels.reduce(
(a, label) => Math.max(ctx.measureText(label).width, a),
-Infinity
);
2022-12-21 02:43:30 -05:00
2022-12-21 02:23:50 -05:00
state.gridBoxWidth =
state.width - state.maxYLabelWidth - state.yLabelPadding;
state.gridBoxHeight = state.height - 2.5 * FONT_HEIGHT_PX; // 2.5 to include bottom part of tall letters ("g", "y", etc.)
2022-12-21 01:20:41 -05:00
state.topLeftGridPos = {
2022-12-21 02:23:50 -05:00
x: state.maxYLabelWidth + state.yLabelPadding,
y: FONT_HEIGHT_PX,
2022-12-21 01:20:41 -05:00
};
state.bottomRightGridPos = {
x: state.topLeftGridPos.x + state.gridBoxWidth,
y: state.topLeftGridPos.y + state.gridBoxHeight,
};
}
if (state.diff.heights) drawDesmos();
2022-12-21 15:26:48 -05:00
draw(state);
2022-12-21 01:20:41 -05:00
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
) => {
2022-12-21 02:23:50 -05:00
ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
2022-12-21 01:20:41 -05:00
xDividers.forEach(({ label, position }) => {
2022-12-21 02:23:50 -05:00
ctx.fillText(label, position.x - ctx.measureText(label).width, position.y);
2022-12-21 01:20:41 -05:00
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);
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);
2022-12-21 02:23:50 -05:00
const dx = gridBoxWidth / (DX * (xLabels.length - 1));
2022-12-21 01:20:41 -05:00
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,
2022-12-21 02:23:50 -05:00
xLabels,
2022-12-21 01:20:41 -05:00
} = state;
2022-12-21 02:23:50 -05:00
const delta = gridBoxWidth / (DX * (xLabels.length - 1));
2022-12-21 01:20:41 -05:00
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);
})();