多语言,EQ 频响曲线 Y 轴可调,滤波器类型新增 Bypass

This commit is contained in:
eafonyang
2026-11-03 11:22:56 +08:00
parent 45197947cf
commit d9e05d34ad
11 changed files with 2428 additions and 93 deletions
+125 -46
View File
@@ -9,6 +9,24 @@ import type { PeqEqUi } from "../constants";
import { eqInterp } from "../utils";
import { PeqUndoButton } from "./PeqUndoButton";
/* ── Y 轴刻度范围(±dB)可选项与持久化 ── */
const Y_RANGE_OPTIONS = [6, 12, 15, 20] as const;
type YRangeDb = (typeof Y_RANGE_OPTIONS)[number];
const DEFAULT_Y_RANGE_DB: YRangeDb = 12;
const Y_RANGE_STORAGE_KEY = "x9-peq-chart-y-db-max";
/** 每档范围的网格整数步长(均可整除边界,确保标签为整数) */
const Y_RANGE_STEP: Record<YRangeDb, number> = { 6: 2, 12: 3, 15: 5, 20: 5 };
function loadYRangeDb(): YRangeDb {
try {
const raw = Number(localStorage.getItem(Y_RANGE_STORAGE_KEY));
if (Y_RANGE_OPTIONS.includes(raw as YRangeDb)) return raw as YRangeDb;
} catch {
/* localStorage 不可用时回退默认值 */
}
return DEFAULT_Y_RANGE_DB;
}
/* ── Legend indicator ── */
function LegendCurveIndicator({
loading,
@@ -48,7 +66,15 @@ export function FreqChart({
onBandSelect,
eqUi,
}: {
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
bands: Array<{
freq: number;
gain: number;
q: number;
type: string;
enabled: boolean;
/** 旁通的带贡献 0 dB,曲线与手柄纵坐标自动排除它 */
bypassed?: boolean;
}>;
rawCurve: number[] | null;
rawCurveLoading?: boolean;
selectedBand: number;
@@ -76,6 +102,16 @@ export function FreqChart({
const [chartWidth, setChartWidth] = useState(340);
const W = chartWidth;
const [isBandDragging, setIsBandDragging] = useState(false);
const [yDbMax, setYDbMax] = useState<YRangeDb>(loadYRangeDb);
const handleYRangeChange = (range: YRangeDb) => {
setYDbMax(range);
try {
localStorage.setItem(Y_RANGE_STORAGE_KEY, String(range));
} catch {
/* 忽略持久化失败,仅影响下次进入页面的默认值 */
}
};
useEffect(() => {
const node = chartContainerRef.current;
@@ -92,7 +128,7 @@ export function FreqChart({
return () => observer.disconnect();
}, []);
const Y_DB_MAX = 20;
const Y_DB_MAX = yDbMax;
const freqToX = (f: number) => {
const logMin = Math.log10(20), logMax = Math.log10(20000);
@@ -158,7 +194,7 @@ export function FreqChart({
maxFreq: 20000,
paddingY: 10,
}),
[bands, W, H],
[bands, W, H, Y_DB_MAX],
);
const pathD = curveData.pathD;
const fillD = curveData.fillD;
@@ -226,7 +262,20 @@ export function FreqChart({
const copyButtonText = eqInterp(eqUi.chartCopyTo, { mode: targetMode });
const freqLabels = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
const gainLabels = [20, 15, 10, 5, 0, -5, -10, -15, -20];
// 按当前范围的整数步长生成网格标签(如 ±12 → 12,9,6,3,0,-3,-6,-9,-12)
const gainLabels = useMemo(() => {
const step = Y_RANGE_STEP[Y_DB_MAX];
const intervals = Math.round((2 * Y_DB_MAX) / step);
const labels: number[] = [];
for (let i = 0; i <= intervals; i++) {
labels.push(Number((Y_DB_MAX - i * step).toFixed(2)));
}
return labels;
}, [Y_DB_MAX]);
const formatGainLabel = (g: number) => {
const v = Number(g.toFixed(2));
return v > 0 ? `+${v}` : String(v);
};
return (
<div className="ios-list-group p-3 mb-3">
@@ -268,6 +317,27 @@ export function FreqChart({
Equalized
</span>
</button>
{/* Y 轴刻度范围切换 */}
<div
className="ml-auto flex shrink-0 items-center overflow-hidden rounded-[8px]"
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}
>
{Y_RANGE_OPTIONS.map((r) => (
<button
key={r}
type="button"
className="px-2 py-1 text-[10px] transition-all duration-150"
style={
yDbMax === r
? { background: "#00FFF6", color: "#000", fontWeight: 600 }
: { color: "rgba(255,255,255,0.4)" }
}
onClick={() => handleYRangeChange(r)}
>
±{r}
</button>
))}
</div>
</div>
{/* A/B + DIFF controls */}
@@ -342,13 +412,19 @@ export function FreqChart({
onPointerCancel={stopDragging}
onPointerLeave={stopDragging}
>
<defs>
{/* 绘图区裁剪:超出当前 Y 轴刻度范围的曲线部分直接截断不显示 */}
<clipPath id="freq-chart-plot-clip">
<rect x="0" y="10" width={W} height={H - 20} />
</clipPath>
</defs>
{/* dB grid */}
{gainLabels.map((g) => (
<g key={`gain-${g}`}>
<line x1="0" y1={gainToY(g)} x2={W} y2={gainToY(g)}
stroke="rgba(255,255,255,0.05)" strokeWidth="1" />
<text x="3" y={gainToY(g) - 2} fontSize="7" fill="rgba(255,255,255,0.2)">
{g > 0 ? `+${g}` : g}
{formatGainLabel(g)}
</text>
</g>
))}
@@ -359,46 +435,48 @@ export function FreqChart({
))}
{/* Zero line */}
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="rgba(255,255,255,0.15)" strokeWidth="1" />
{/* Fill */}
{showEq && <path d={fillD} fill="rgba(255, 237, 0, 0.08)" />}
{/* EQ curve */}
{showEq && (
<path
ref={curvePathRef}
d={pathD}
fill="none"
stroke="#FFED00"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
{/* Raw */}
{rawPathD && showRaw && (
<path
ref={rawPathRef}
d={rawPathD}
fill="none"
stroke="#ffffff"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.9"
/>
)}
{/* Equalized = EQ + Raw */}
{equalizedPathD && showEqualized && (
<path
ref={equalizedPathRef}
d={equalizedPathD}
fill="none"
stroke="#23d2fe"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.95"
/>
)}
<g clipPath="url(#freq-chart-plot-clip)">
{/* Fill */}
{showEq && <path d={fillD} fill="rgba(255, 237, 0, 0.08)" />}
{/* EQ curve */}
{showEq && (
<path
ref={curvePathRef}
d={pathD}
fill="none"
stroke="#FFED00"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
{/* Raw */}
{rawPathD && showRaw && (
<path
ref={rawPathRef}
d={rawPathD}
fill="none"
stroke="#ffffff"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.9"
/>
)}
{/* Equalized = EQ + Raw */}
{equalizedPathD && showEqualized && (
<path
ref={equalizedPathRef}
d={equalizedPathD}
fill="none"
stroke="#23d2fe"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.95"
/>
)}
</g>
{/* Band nodes with index */}
{showEq && bands.map((band, i) => (
<g
@@ -412,7 +490,8 @@ export function FreqChart({
const fillColor = isSelected ? "#FFED00" : "rgba(0,0,0,0.5)";
const strokeColor = isSelected ? "#FFED00" : "#FFED00";
const textColor = isSelected ? "#000000" : "#FFED00";
const handleY = gainToY(combinedMagDbAtHandles[i] ?? 0);
// 级联响应超出当前刻度范围时,手柄钳制在绘图区边缘保持可见可拖
const handleY = Math.max(10, Math.min(H - 10, gainToY(combinedMagDbAtHandles[i] ?? 0)));
return (
<>
<circle