Files
codetutor/frontend/src/lib/visualizations/use-visualization.ts
T

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,
};
}