Skip to content

Commit 0ce06b9

Browse files
committed
feat: support spectrogram zoom in/out
1 parent 668907e commit 0ce06b9

2 files changed

Lines changed: 45 additions & 10 deletions

File tree

web/src/src/components/chart/DequeSpectrogram.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,18 @@ export const DequeSpectrogram = memo(
201201
<div className="relative h-full w-full">
202202
<canvas ref={canvasRef} className="block h-full w-full" />
203203

204-
<div className="absolute top-5 left-15 flex items-center space-x-1">
204+
<div className="absolute top-5 left-15 flex items-center gap-1">
205205
{title && (
206206
<div className="flex h-6 items-center rounded bg-black/50 px-3 text-sm font-bold text-white select-none">
207207
{title}
208208
</div>
209209
)}
210210

211211
<button
212-
className="flex size-6 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
212+
className="flex size-6 flex-shrink-0 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
213213
onClick={() => setShowSettings((v) => !v)}
214214
>
215-
<Icon path={mdiCog} size={0.6} />
215+
<Icon path={mdiCog} size={0.7} />
216216
</button>
217217
</div>
218218

web/src/src/components/chart/Spectrogram.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mdiClose, mdiCog } from '@mdi/js';
1+
import { mdiClose, mdiCog, mdiMagnifyMinus, mdiMagnifyPlus } from '@mdi/js';
22
import Icon from '@mdi/react';
33
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
44
import { useTranslation } from 'react-i18next';
@@ -16,6 +16,7 @@ interface ISpectrogram {
1616
readonly data: [number, number][];
1717
readonly fftExecutor?: FFTExecutor;
1818
readonly renderFPS?: number;
19+
readonly zoomStep?: number;
1920
readonly onSpectrogramUpdate?: (minDB: number, maxDB: number) => void;
2021
}
2122

@@ -32,13 +33,15 @@ export const Spectrogram = memo(
3233
data,
3334
fftExecutor,
3435
renderFPS = 2,
36+
zoomStep = 0.2,
3537
onSpectrogramUpdate
3638
}: ISpectrogram) => {
3739
const { t } = useTranslation();
3840

3941
const [showSettings, setShowSettings] = useState(false);
4042
const [minDBState, setMinDBState] = useState(minDB);
4143
const [maxDBState, setMaxDBState] = useState(maxDB);
44+
const [zoomDuration, setZoomDuration] = useState(duration);
4245

4346
const [initialized, setInitialized] = useState(false);
4447
const spectrogramRef = useRef<SpectrogramCore | null>(null);
@@ -116,7 +119,7 @@ export const Spectrogram = memo(
116119
}
117120

118121
const total = dataDuration ?? 0;
119-
const window = duration;
122+
const window = zoomDuration;
120123
const maxStart = Math.max(0, total - window);
121124
const start = maxStart * timePercent;
122125
const end = start + window;
@@ -130,7 +133,7 @@ export const Spectrogram = memo(
130133
};
131134
rafId = requestAnimationFrame(renderLoop);
132135
return () => cancelAnimationFrame(rafId);
133-
}, [dataDuration, duration, freqRange, initialized, renderFPS, timePercent]);
136+
}, [dataDuration, duration, freqRange, initialized, renderFPS, timePercent, zoomDuration]);
134137

135138
useEffect(() => {
136139
if (initialized) {
@@ -170,25 +173,57 @@ export const Spectrogram = memo(
170173
[minDBState, onSpectrogramUpdate, spectrogramRef]
171174
);
172175

176+
const handleZoomIn = useCallback(() => {
177+
if (dataDuration) {
178+
setZoomDuration((prev) => {
179+
const next = prev * (1 - zoomStep);
180+
return Math.max(next, dataDuration * 0.01);
181+
});
182+
}
183+
}, [dataDuration, zoomStep]);
184+
185+
const handleZoomOut = useCallback(() => {
186+
if (dataDuration) {
187+
setZoomDuration((prev) => {
188+
const next = prev * (1 + zoomStep);
189+
return Math.min(next, dataDuration);
190+
});
191+
}
192+
}, [dataDuration, zoomStep]);
193+
173194
return (
174195
<div className="relative flex h-full w-full flex-col">
175196
<canvas className="block" ref={canvasRef} />
176197

177-
<div className="absolute top-5 left-15 flex items-center space-x-1">
198+
<div className="absolute top-5 left-15 mr-15 flex flex-wrap items-center gap-1">
178199
{title && (
179200
<div className="flex h-6 items-center rounded bg-black/50 px-3 text-sm font-bold text-white select-none">
180201
{title}
181202
</div>
182203
)}
183204

184205
<button
185-
className="flex size-6 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
206+
className="flex size-6 flex-shrink-0 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
186207
onClick={() => setShowSettings((v) => !v)}
187208
>
188-
<Icon path={mdiCog} size={0.6} />
209+
<Icon path={mdiCog} size={0.7} />
210+
</button>
211+
212+
<button
213+
className="flex size-6 flex-shrink-0 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
214+
onClick={handleZoomIn}
215+
>
216+
<Icon path={mdiMagnifyPlus} size={0.7} />
217+
</button>
218+
219+
<button
220+
className="flex size-6 flex-shrink-0 cursor-pointer items-center justify-center rounded bg-black/50 text-white opacity-50 transition-all hover:opacity-100"
221+
onClick={handleZoomOut}
222+
>
223+
<Icon path={mdiMagnifyMinus} size={0.7} />
189224
</button>
190225

191-
<div className="flex h-6 items-center rounded bg-black/50 opacity-50 transition-all hover:opacity-100">
226+
<div className="flex h-6 max-w-36 items-center rounded bg-black/50 opacity-50 transition-all hover:opacity-100">
192227
<input
193228
type="range"
194229
min={0}

0 commit comments

Comments
 (0)