多语言,EQ 频响曲线 Y 轴可调,滤波器类型新增 Bypass
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user