feat(viz): two pointers narrative
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user