Enhance PEQ functionality: Update PeqBandForResponse type to allow numeric filter types, and introduce buildPeqSvgCurveData function for reusable SVG curve data generation. Refactor EQPage to utilize new curve data for improved frequency response visualization and animation handling.
This commit is contained in:
@@ -339,7 +339,7 @@ export type PeqBandForResponse = {
|
||||
gain: number;
|
||||
freq: number;
|
||||
q: number;
|
||||
type: string;
|
||||
type: string | number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -359,6 +359,60 @@ export function computePeqMagnitudeDb(bands: PeqBandForResponse[], fs: number):
|
||||
return getFreqznList(list, fs, f);
|
||||
}
|
||||
|
||||
export type PeqSvgCurveData = {
|
||||
points: Array<{ x: number; y: number; gainDb: number; freq: number }>;
|
||||
pathD: string;
|
||||
fillD: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build reusable SVG curve data (`points`, `pathD`, `fillD`) for PEQ response rendering.
|
||||
* Any page can call this and render with SVG `<path d={pathD} />`.
|
||||
*/
|
||||
export function buildPeqSvgCurveData(args: {
|
||||
bands: PeqBandForResponse[];
|
||||
width: number;
|
||||
height: number;
|
||||
fs?: number;
|
||||
yDbMax?: number;
|
||||
minFreq?: number;
|
||||
maxFreq?: number;
|
||||
paddingY?: number;
|
||||
}): PeqSvgCurveData {
|
||||
const {
|
||||
bands,
|
||||
width,
|
||||
height,
|
||||
fs = 48000,
|
||||
yDbMax = 20,
|
||||
minFreq = 20,
|
||||
maxFreq = 20000,
|
||||
paddingY = 10,
|
||||
} = args;
|
||||
|
||||
const safeW = Math.max(1, width);
|
||||
const safeH = Math.max(1, height);
|
||||
|
||||
const logMin = Math.log10(minFreq);
|
||||
const logMax = Math.log10(maxFreq);
|
||||
const freqToX = (f: number) =>
|
||||
((Math.log10(Math.max(minFreq, Math.min(maxFreq, f))) - logMin) / (logMax - logMin)) * safeW;
|
||||
const gainToY = (g: number) => safeH / 2 - (g / yDbMax) * (safeH / 2 - paddingY);
|
||||
|
||||
const freqs = getPeqLogSpacedFreqs();
|
||||
const db = computePeqMagnitudeDb(bands, fs);
|
||||
const points = freqs.map((f, i) => {
|
||||
const gainDb = db[i] ?? 0;
|
||||
return { x: freqToX(f), y: gainToY(gainDb), gainDb, freq: f };
|
||||
});
|
||||
|
||||
const pathD = points
|
||||
.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`)
|
||||
.join(" ");
|
||||
const fillD = pathD ? `${pathD} L ${safeW} ${safeH / 2} L 0 ${safeH / 2} Z` : "";
|
||||
return { points, pathD, fillD };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ECharts options for frequency response chart
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Reusable left-to-right SVG stroke draw animation.
|
||||
* Pass `enabled=false` to disable and clear dash styles (e.g. while dragging).
|
||||
*/
|
||||
export function useStrokeDrawAnimation(
|
||||
pathRef: React.RefObject<SVGPathElement | null>,
|
||||
pathD: string,
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
durationMs?: number;
|
||||
easing?: string;
|
||||
},
|
||||
) {
|
||||
const enabled = options?.enabled ?? true;
|
||||
const durationMs = options?.durationMs ?? 1350;
|
||||
const easing = options?.easing ?? "cubic-bezier(0.33, 1, 0.68, 1)";
|
||||
|
||||
useEffect(() => {
|
||||
const path = pathRef.current;
|
||||
if (!path || !pathD) return;
|
||||
|
||||
if (!enabled) {
|
||||
path.style.strokeDasharray = "";
|
||||
path.style.strokeDashoffset = "";
|
||||
path.style.transition = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const length = path.getTotalLength();
|
||||
if (!Number.isFinite(length) || length <= 0) return;
|
||||
|
||||
path.style.strokeDasharray = `${length}`;
|
||||
path.style.strokeDashoffset = `${length}`;
|
||||
path.style.transition = "none";
|
||||
|
||||
let cancelled = false;
|
||||
const id = requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) return;
|
||||
path.style.transition = `stroke-dashoffset ${durationMs}ms ${easing}`;
|
||||
path.style.strokeDashoffset = "0";
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(id);
|
||||
};
|
||||
}, [pathRef, pathD, enabled, durationMs, easing]);
|
||||
}
|
||||
+23
-48
@@ -15,6 +15,7 @@ import { ChevronLeft, ChevronDown, Minus, Plus, Edit3, Headphones, Search, X } f
|
||||
import { useLocation } from "wouter";
|
||||
import { useState, useMemo, useEffect, useRef, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useStrokeDrawAnimation } from "@/lib/useStrokeDrawAnimation";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -36,8 +37,7 @@ import {
|
||||
getChartOps,
|
||||
getFilterType,
|
||||
getFilterShortName,
|
||||
computePeqMagnitudeDb,
|
||||
getPeqLogSpacedFreqs,
|
||||
buildPeqSvgCurveData,
|
||||
} from "@/lib/peqAudio";
|
||||
|
||||
/* ── iOS Toggle ── */
|
||||
@@ -178,53 +178,28 @@ function FreqChart({
|
||||
setIsBandDragging(false);
|
||||
};
|
||||
|
||||
const FS = 48000;
|
||||
const curveData = useMemo(
|
||||
() =>
|
||||
buildPeqSvgCurveData({
|
||||
bands,
|
||||
width: W,
|
||||
height: H,
|
||||
fs: 48000,
|
||||
yDbMax: Y_DB_MAX,
|
||||
minFreq: 20,
|
||||
maxFreq: 20000,
|
||||
paddingY: 10,
|
||||
}),
|
||||
[bands, W],
|
||||
);
|
||||
const pathD = curveData.pathD;
|
||||
const fillD = curveData.fillD;
|
||||
|
||||
const curve = useMemo(() => {
|
||||
const freqs = getPeqLogSpacedFreqs();
|
||||
const db = computePeqMagnitudeDb(bands, FS);
|
||||
return freqs.map((f, i) => {
|
||||
const g = db[i] ?? 0;
|
||||
return { x: freqToX(f), y: gainToY(g), g };
|
||||
});
|
||||
}, [bands, W]);
|
||||
|
||||
const pathD = curve.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
||||
const fillD = `${pathD} L ${W} ${H / 2} L 0 ${H / 2} Z`;
|
||||
|
||||
// Left-to-right stroke draw animation (skip while dragging a band)
|
||||
useEffect(() => {
|
||||
const path = curvePathRef.current;
|
||||
if (!path || !pathD) return;
|
||||
|
||||
if (isBandDragging) {
|
||||
path.style.strokeDasharray = "";
|
||||
path.style.strokeDashoffset = "";
|
||||
path.style.transition = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const length = path.getTotalLength();
|
||||
if (!Number.isFinite(length) || length <= 0) return;
|
||||
|
||||
path.style.strokeDasharray = `${length}`;
|
||||
path.style.strokeDashoffset = `${length}`;
|
||||
path.style.transition = "none";
|
||||
|
||||
let cancelled = false;
|
||||
const id1 = requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) return;
|
||||
path.style.transition = "stroke-dashoffset 1.35s cubic-bezier(0.33, 1, 0.68, 1)";
|
||||
path.style.strokeDashoffset = "0";
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(id1);
|
||||
};
|
||||
}, [pathD, W, isBandDragging]);
|
||||
// Reusable left-to-right stroke draw animation (skip while dragging a band).
|
||||
useStrokeDrawAnimation(curvePathRef, pathD, {
|
||||
enabled: !isBandDragging,
|
||||
durationMs: 1350,
|
||||
});
|
||||
|
||||
const freqLabels = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
|
||||
const gainLabels = [20, 15, 10, 5, 0, -5, -10, -15, -20];
|
||||
|
||||
Reference in New Issue
Block a user