mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(waveform-seek): H3 — extract renderers + 2 hooks + SeekbarPreview component (#665)
* refactor(waveform-seek): H3.1 — extract helpers + constants + types * refactor(waveform-seek): H3.2 — extract drawSeekbar + style renderers to utils/waveformSeekRenderers.ts * refactor(waveform-seek): H3.3 — split renderers into static + animated * refactor(waveform-seek): H3.4 — extract SeekbarPreview to WaveformSeekPreview.tsx * refactor(waveform-seek): H3.5 — extract useWaveformHeights hook + hoist constants * refactor(waveform-seek): H3.6 — extract useWaveformInterpolation hook
This commit is contained in:
committed by
GitHub
parent
b8a9fe860e
commit
dc5c64a109
@@ -0,0 +1,113 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import type { SeekbarStyle } from '../store/authStoreTypes';
|
||||
import { getPlaybackProgressSnapshot } from '../store/playbackProgress';
|
||||
import {
|
||||
ANIMATED_STYLES, AnimState, INTERPOLATION_PAINT_MIN_MS,
|
||||
isBarQuantizedSeekStyle, quantizeProgressByBars,
|
||||
} from '../utils/waveformSeekHelpers';
|
||||
import { drawSeekbar } from '../utils/waveformSeekRenderers';
|
||||
|
||||
interface Args {
|
||||
duration: number;
|
||||
isPlaying: boolean;
|
||||
previewFreezesMainSeekbar: boolean;
|
||||
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||
heightsRef: React.MutableRefObject<Float32Array | null>;
|
||||
styleRef: React.MutableRefObject<SeekbarStyle>;
|
||||
progressRef: React.MutableRefObject<number>;
|
||||
bufferedRef: React.MutableRefObject<number>;
|
||||
visualProgressRef: React.MutableRefObject<number>;
|
||||
visualTargetProgressRef: React.MutableRefObject<number>;
|
||||
progressAnchorRef: React.MutableRefObject<{ progress: number; atMs: number }>;
|
||||
animStateRef: React.MutableRefObject<AnimState>;
|
||||
isDraggingRef: React.MutableRefObject<boolean>;
|
||||
wheelPreviewFractionRef: React.MutableRefObject<number | null>;
|
||||
wheelPreviewUntilRef: React.MutableRefObject<number>;
|
||||
pendingCommittedSeekRef: React.MutableRefObject<{ fraction: number; setAtMs: number } | null>;
|
||||
}
|
||||
|
||||
/** Smooth interpolation rAF loop — predicts playhead position between sparse
|
||||
* transport heartbeats, with proper anchor reset on resume. Inactive while
|
||||
* paused, dragging, wheel-preview, or pending-committed-seek. */
|
||||
export function useWaveformInterpolation({
|
||||
duration, isPlaying, previewFreezesMainSeekbar,
|
||||
canvasRef, heightsRef, styleRef,
|
||||
progressRef, bufferedRef, visualProgressRef, visualTargetProgressRef,
|
||||
progressAnchorRef, animStateRef, isDraggingRef,
|
||||
wheelPreviewFractionRef, wheelPreviewUntilRef, pendingCommittedSeekRef,
|
||||
}: Args) {
|
||||
useEffect(() => {
|
||||
if (!isPlaying || previewFreezesMainSeekbar || duration <= 0 || !isFinite(duration)) return;
|
||||
// This effect is torn down while paused, so `progressAnchorRef.atMs` is not refreshed.
|
||||
// On resume the first `tick` would add the entire pause duration to `elapsedSec` and
|
||||
// overshoot the playhead until the next transport heartbeat corrects it.
|
||||
const snap = getPlaybackProgressSnapshot();
|
||||
const raw = snap.progress;
|
||||
progressRef.current = raw;
|
||||
progressAnchorRef.current = {
|
||||
progress: raw,
|
||||
atMs: performance.now(),
|
||||
};
|
||||
const resumeVisual = isBarQuantizedSeekStyle(styleRef.current)
|
||||
? quantizeProgressByBars(raw)
|
||||
: raw;
|
||||
visualTargetProgressRef.current = resumeVisual;
|
||||
visualProgressRef.current = resumeVisual;
|
||||
|
||||
let rafId: number | null = null;
|
||||
let lastPaintAt = 0;
|
||||
const tick = (now: number) => {
|
||||
if (document.hidden || window.__psyHidden) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (isDraggingRef.current) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
const wheelPreviewFraction = wheelPreviewFractionRef.current;
|
||||
if (wheelPreviewFraction != null && Date.now() < wheelPreviewUntilRef.current) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (pendingCommittedSeekRef.current) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
const anchor = progressAnchorRef.current;
|
||||
const elapsedSec = Math.max(0, (now - anchor.atMs) / 1000);
|
||||
const predicted = Math.max(0, Math.min(1, anchor.progress + elapsedSec / duration));
|
||||
const nextTargetProgress = isBarQuantizedSeekStyle(styleRef.current)
|
||||
? quantizeProgressByBars(predicted)
|
||||
: predicted;
|
||||
if (Math.abs(nextTargetProgress - visualTargetProgressRef.current) > 0.000001) {
|
||||
visualTargetProgressRef.current = nextTargetProgress;
|
||||
}
|
||||
const currentVisual = visualProgressRef.current;
|
||||
const targetVisual = visualTargetProgressRef.current;
|
||||
const delta = targetVisual - currentVisual;
|
||||
if (Math.abs(delta) > 0.000001) {
|
||||
const smoothing = isBarQuantizedSeekStyle(styleRef.current) ? 0.22 : 0.28;
|
||||
const nextVisualProgress = Math.abs(delta) < 0.002
|
||||
? targetVisual
|
||||
: currentVisual + delta * smoothing;
|
||||
visualProgressRef.current = nextVisualProgress;
|
||||
progressRef.current = nextVisualProgress;
|
||||
const needsDirectDraw = !ANIMATED_STYLES.has(styleRef.current);
|
||||
if (needsDirectDraw && now - lastPaintAt >= INTERPOLATION_PAINT_MIN_MS) {
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
drawSeekbar(canvas, styleRef.current, heightsRef.current, nextVisualProgress, bufferedRef.current, animStateRef.current);
|
||||
lastPaintAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return () => {
|
||||
if (rafId != null) cancelAnimationFrame(rafId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [duration, isPlaying, previewFreezesMainSeekbar]);
|
||||
}
|
||||
Reference in New Issue
Block a user