feat: add geq graph + bypass warning
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
export const GEQ_BAND_HZ = [
|
||||
20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600,
|
||||
2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000,
|
||||
] as const;
|
||||
|
||||
/** Peaking-filter magnitude response (dB) of a single band at a given frequency. */
|
||||
export function geqPeakResponseDb(
|
||||
freq: number,
|
||||
bandFreq: number,
|
||||
gainDb: number,
|
||||
q: number
|
||||
): number {
|
||||
if (gainDb === 0) return 0;
|
||||
const ratio = freq / bandFreq - bandFreq / freq;
|
||||
return gainDb / (1 + (q * ratio) ** 2);
|
||||
}
|
||||
|
||||
/** Sum of all bands' contributions at a given frequency. */
|
||||
export function geqTotalCurveDb(
|
||||
freq: number,
|
||||
values: number[],
|
||||
bandHz: readonly number[] = GEQ_BAND_HZ,
|
||||
q: number
|
||||
): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
sum += geqPeakResponseDb(freq, bandHz[i], values[i], q);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** Log-spaced sample points from xMin to xMax, for a pixel-smooth curve on a log x-axis. */
|
||||
export function logSpace(xMin: number, xMax: number, steps: number): number[] {
|
||||
const logMin = Math.log10(xMin);
|
||||
const logMax = Math.log10(xMax);
|
||||
const out: number[] = [];
|
||||
for (let s = 0; s <= steps; s++) {
|
||||
out.push(Math.pow(10, logMin + ((logMax - logMin) * s) / steps));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type GeqCurveColors = {
|
||||
strokeColor: string;
|
||||
fillAbove: string;
|
||||
fillAboveFade: string;
|
||||
fillBelowFade: string;
|
||||
fillBelow: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_GEQ_CURVE_COLORS: GeqCurveColors = {
|
||||
strokeColor: '#e5e7eb',
|
||||
fillAbove: 'rgba(74, 222, 128, 0.35)',
|
||||
fillAboveFade: 'rgba(74, 222, 128, 0.08)',
|
||||
fillBelowFade: 'rgba(248, 113, 113, 0.08)',
|
||||
fillBelow: 'rgba(248, 113, 113, 0.35)',
|
||||
};
|
||||
|
||||
export type GeqCurveOptions = {
|
||||
values: number[];
|
||||
bandHz?: readonly number[];
|
||||
q: number;
|
||||
xMin: number;
|
||||
xMax: number;
|
||||
yMin: number;
|
||||
yMax: number;
|
||||
steps?: number;
|
||||
colors?: GeqCurveColors;
|
||||
lineWidth?: number;
|
||||
dimmed?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a `draw` callback compatible with LinearGraph's `draw` prop:
|
||||
* ({ ctx, toPx }) => void
|
||||
*
|
||||
* Usage:
|
||||
* <LinearGraph ... draw={buildGeqCurveDraw({ values, q: 8.65, xMin, xMax, yMin, yMax })} />
|
||||
*/
|
||||
export function buildGeqCurveDraw({
|
||||
values,
|
||||
bandHz = GEQ_BAND_HZ,
|
||||
q,
|
||||
xMin,
|
||||
xMax,
|
||||
yMin,
|
||||
yMax,
|
||||
steps = 500,
|
||||
colors = DEFAULT_GEQ_CURVE_COLORS,
|
||||
lineWidth = 2,
|
||||
dimmed = false,
|
||||
}: GeqCurveOptions) {
|
||||
return ({
|
||||
ctx,
|
||||
toPx,
|
||||
}: {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
toPx: (x: number, y: number) => [number, number];
|
||||
}) => {
|
||||
const freqs = logSpace(xMin, xMax, steps);
|
||||
const points = freqs.map((freq) => {
|
||||
const gain = geqTotalCurveDb(freq, values, bandHz, q);
|
||||
return toPx(freq, gain);
|
||||
});
|
||||
|
||||
ctx.save();
|
||||
if (dimmed) ctx.globalAlpha = 0.25; // NEW: fade everything drawn below
|
||||
|
||||
// Fill area under the curve down to 0 dB
|
||||
ctx.beginPath();
|
||||
points.forEach(([xPx, yPx], i) => {
|
||||
if (i === 0) ctx.moveTo(xPx, yPx);
|
||||
else ctx.lineTo(xPx, yPx);
|
||||
});
|
||||
const [xEndPx, zeroYPx] = toPx(xMax, 0);
|
||||
const [xStartPx] = toPx(xMin, 0);
|
||||
ctx.lineTo(xEndPx, zeroYPx);
|
||||
ctx.lineTo(xStartPx, zeroYPx);
|
||||
ctx.closePath();
|
||||
|
||||
const [, topPx] = toPx(xMin, yMax);
|
||||
const [, bottomPx] = toPx(xMin, yMin);
|
||||
const gradient = ctx.createLinearGradient(0, topPx, 0, bottomPx);
|
||||
gradient.addColorStop(0, colors.fillAbove);
|
||||
gradient.addColorStop(0.5, colors.fillAboveFade);
|
||||
gradient.addColorStop(0.5, colors.fillBelowFade);
|
||||
gradient.addColorStop(1, colors.fillBelow);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Stroke the curve on top
|
||||
ctx.beginPath();
|
||||
points.forEach(([xPx, yPx], i) => {
|
||||
if (i === 0) ctx.moveTo(xPx, yPx);
|
||||
else ctx.lineTo(xPx, yPx);
|
||||
});
|
||||
ctx.strokeStyle = colors.strokeColor;
|
||||
ctx.lineWidth = lineWidth;
|
||||
if (dimmed) ctx.setLineDash([6, 4]); // dashed = "not actually applied"
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user