223 lines
4.7 KiB
TypeScript
223 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import type {
|
|
SpeedPreset,
|
|
VisualizationData,
|
|
VisualizationStep,
|
|
} from "@/types/visualization";
|
|
|
|
const speedValues: Record<SpeedPreset, number> = {
|
|
slow: 2000,
|
|
normal: 1000,
|
|
fast: 500,
|
|
};
|
|
|
|
interface VisualizationContextValue {
|
|
// Data
|
|
data: VisualizationData | null;
|
|
currentStep: VisualizationStep | null;
|
|
stepIndex: number;
|
|
totalSteps: number;
|
|
|
|
// Playback state
|
|
isPlaying: boolean;
|
|
speed: SpeedPreset;
|
|
|
|
// Navigation actions
|
|
goToStep: (index: number) => void;
|
|
nextStep: () => void;
|
|
prevStep: () => void;
|
|
play: () => void;
|
|
pause: () => void;
|
|
togglePlayback: () => void;
|
|
setSpeed: (speed: SpeedPreset) => void;
|
|
|
|
// State queries
|
|
canGoNext: boolean;
|
|
canGoPrev: boolean;
|
|
}
|
|
|
|
const VisualizationContext = createContext<VisualizationContextValue | null>(
|
|
null
|
|
);
|
|
|
|
interface VisualizationProviderProps {
|
|
data: VisualizationData;
|
|
initialStep?: number;
|
|
autoPlay?: boolean;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function VisualizationProvider({
|
|
data,
|
|
initialStep = 0,
|
|
autoPlay = false,
|
|
children,
|
|
}: VisualizationProviderProps) {
|
|
const [stepIndex, setStepIndex] = useState(initialStep);
|
|
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
|
const [speed, setSpeed] = useState<SpeedPreset>("normal");
|
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
const totalSteps = data.steps.length;
|
|
const currentStep = data.steps[stepIndex] ?? null;
|
|
const canGoNext = stepIndex < totalSteps - 1;
|
|
const canGoPrev = stepIndex > 0;
|
|
|
|
const goToStep = useCallback(
|
|
(index: number) => {
|
|
const clampedIndex = Math.max(0, Math.min(index, totalSteps - 1));
|
|
setStepIndex(clampedIndex);
|
|
},
|
|
[totalSteps]
|
|
);
|
|
|
|
const nextStep = useCallback(() => {
|
|
if (canGoNext) {
|
|
setStepIndex((prev) => prev + 1);
|
|
} else {
|
|
setIsPlaying(false);
|
|
}
|
|
}, [canGoNext]);
|
|
|
|
const prevStep = useCallback(() => {
|
|
if (canGoPrev) {
|
|
setStepIndex((prev) => prev - 1);
|
|
}
|
|
}, [canGoPrev]);
|
|
|
|
const play = useCallback(() => {
|
|
if (stepIndex >= totalSteps - 1) {
|
|
setStepIndex(0);
|
|
}
|
|
setIsPlaying(true);
|
|
}, [stepIndex, totalSteps]);
|
|
|
|
const pause = useCallback(() => {
|
|
setIsPlaying(false);
|
|
}, []);
|
|
|
|
const togglePlayback = useCallback(() => {
|
|
if (isPlaying) {
|
|
pause();
|
|
} else {
|
|
play();
|
|
}
|
|
}, [isPlaying, play, pause]);
|
|
|
|
// Auto-advance when playing
|
|
useEffect(() => {
|
|
if (isPlaying) {
|
|
intervalRef.current = setInterval(() => {
|
|
setStepIndex((prev) => {
|
|
if (prev >= totalSteps - 1) {
|
|
setIsPlaying(false);
|
|
return prev;
|
|
}
|
|
return prev + 1;
|
|
});
|
|
}, speedValues[speed]);
|
|
}
|
|
|
|
return () => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
};
|
|
}, [isPlaying, speed, totalSteps]);
|
|
|
|
// Keyboard navigation
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Don't capture if user is typing in an input
|
|
if (
|
|
event.target instanceof HTMLInputElement ||
|
|
event.target instanceof HTMLTextAreaElement
|
|
) {
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
event.preventDefault();
|
|
prevStep();
|
|
break;
|
|
case "ArrowRight":
|
|
event.preventDefault();
|
|
nextStep();
|
|
break;
|
|
case " ":
|
|
event.preventDefault();
|
|
togglePlayback();
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [prevStep, nextStep, togglePlayback]);
|
|
|
|
const value = useMemo<VisualizationContextValue>(
|
|
() => ({
|
|
data,
|
|
currentStep,
|
|
stepIndex,
|
|
totalSteps,
|
|
isPlaying,
|
|
speed,
|
|
goToStep,
|
|
nextStep,
|
|
prevStep,
|
|
play,
|
|
pause,
|
|
togglePlayback,
|
|
setSpeed,
|
|
canGoNext,
|
|
canGoPrev,
|
|
}),
|
|
[
|
|
data,
|
|
currentStep,
|
|
stepIndex,
|
|
totalSteps,
|
|
isPlaying,
|
|
speed,
|
|
goToStep,
|
|
nextStep,
|
|
prevStep,
|
|
play,
|
|
pause,
|
|
togglePlayback,
|
|
canGoNext,
|
|
canGoPrev,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<VisualizationContext.Provider value={value}>
|
|
{children}
|
|
</VisualizationContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useVisualization(): VisualizationContextValue {
|
|
const context = useContext(VisualizationContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
"useVisualization must be used within a VisualizationProvider"
|
|
);
|
|
}
|
|
return context;
|
|
}
|