200 lines
4.7 KiB
TypeScript
200 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import type {
|
|
AlgorithmDefinition,
|
|
PlaybackSpeed,
|
|
PlaybackState,
|
|
UseVisualizationReturn,
|
|
VisualizationControls,
|
|
} from "./types";
|
|
|
|
const SPEED_MULTIPLIERS: Record<PlaybackSpeed, number> = {
|
|
0.5: 2,
|
|
1: 1,
|
|
2: 0.5,
|
|
};
|
|
|
|
const BASE_STEP_DURATION = 1500;
|
|
|
|
export function useVisualization(
|
|
algorithm: AlgorithmDefinition
|
|
): UseVisualizationReturn {
|
|
const [playback, setPlayback] = useState<PlaybackState>({
|
|
isPlaying: false,
|
|
currentStepIndex: 0,
|
|
speed: 1,
|
|
});
|
|
|
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
const totalSteps = algorithm.steps.length;
|
|
|
|
const clearAutoAdvance = useCallback(() => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const stepForward = useCallback(() => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
currentStepIndex: Math.min(prev.currentStepIndex + 1, totalSteps - 1),
|
|
}));
|
|
}, [totalSteps]);
|
|
|
|
const stepBackward = useCallback(() => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
currentStepIndex: Math.max(prev.currentStepIndex - 1, 0),
|
|
}));
|
|
}, []);
|
|
|
|
const goToStep = useCallback(
|
|
(index: number) => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
currentStepIndex: Math.max(0, Math.min(index, totalSteps - 1)),
|
|
}));
|
|
},
|
|
[totalSteps]
|
|
);
|
|
|
|
const goToFirst = useCallback(() => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
currentStepIndex: 0,
|
|
}));
|
|
}, []);
|
|
|
|
const goToLast = useCallback(() => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
currentStepIndex: totalSteps - 1,
|
|
}));
|
|
}, [totalSteps]);
|
|
|
|
const pause = useCallback(() => {
|
|
clearAutoAdvance();
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
isPlaying: false,
|
|
}));
|
|
}, [clearAutoAdvance]);
|
|
|
|
const play = useCallback(() => {
|
|
setPlayback((prev) => {
|
|
if (prev.currentStepIndex >= totalSteps - 1) {
|
|
return { ...prev, isPlaying: true, currentStepIndex: 0 };
|
|
}
|
|
return { ...prev, isPlaying: true };
|
|
});
|
|
}, [totalSteps]);
|
|
|
|
const togglePlay = useCallback(() => {
|
|
setPlayback((prev) => {
|
|
if (prev.isPlaying) {
|
|
clearAutoAdvance();
|
|
return { ...prev, isPlaying: false };
|
|
}
|
|
if (prev.currentStepIndex >= totalSteps - 1) {
|
|
return { ...prev, isPlaying: true, currentStepIndex: 0 };
|
|
}
|
|
return { ...prev, isPlaying: true };
|
|
});
|
|
}, [clearAutoAdvance, totalSteps]);
|
|
|
|
const setSpeed = useCallback((speed: PlaybackSpeed) => {
|
|
setPlayback((prev) => ({
|
|
...prev,
|
|
speed,
|
|
}));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (playback.isPlaying) {
|
|
const interval =
|
|
BASE_STEP_DURATION * SPEED_MULTIPLIERS[playback.speed];
|
|
|
|
intervalRef.current = setInterval(() => {
|
|
setPlayback((prev) => {
|
|
if (prev.currentStepIndex >= totalSteps - 1) {
|
|
clearAutoAdvance();
|
|
return { ...prev, isPlaying: false };
|
|
}
|
|
return { ...prev, currentStepIndex: prev.currentStepIndex + 1 };
|
|
});
|
|
}, interval);
|
|
} else {
|
|
clearAutoAdvance();
|
|
}
|
|
|
|
return clearAutoAdvance;
|
|
}, [playback.isPlaying, playback.speed, totalSteps, clearAutoAdvance]);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (
|
|
e.target instanceof HTMLInputElement ||
|
|
e.target instanceof HTMLTextAreaElement
|
|
) {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case " ":
|
|
e.preventDefault();
|
|
togglePlay();
|
|
break;
|
|
case "ArrowLeft":
|
|
e.preventDefault();
|
|
stepBackward();
|
|
break;
|
|
case "ArrowRight":
|
|
e.preventDefault();
|
|
stepForward();
|
|
break;
|
|
case "Home":
|
|
e.preventDefault();
|
|
goToFirst();
|
|
break;
|
|
case "End":
|
|
e.preventDefault();
|
|
goToLast();
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [togglePlay, stepBackward, stepForward, goToFirst, goToLast]);
|
|
|
|
const currentStep = algorithm.steps[playback.currentStepIndex];
|
|
const progress =
|
|
totalSteps > 1
|
|
? (playback.currentStepIndex / (totalSteps - 1)) * 100
|
|
: 100;
|
|
|
|
const controls: VisualizationControls = {
|
|
play,
|
|
pause,
|
|
togglePlay,
|
|
stepForward,
|
|
stepBackward,
|
|
goToStep,
|
|
goToFirst,
|
|
goToLast,
|
|
setSpeed,
|
|
};
|
|
|
|
return {
|
|
currentStep,
|
|
currentStepIndex: playback.currentStepIndex,
|
|
totalSteps,
|
|
playback,
|
|
controls,
|
|
currentPhase: currentStep.phase,
|
|
progress,
|
|
};
|
|
}
|