"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { AlgorithmDefinition, PlaybackSpeed, PlaybackState, UseVisualizationReturn, VisualizationControls, } from "./types"; const SPEED_MULTIPLIERS: Record = { 0.5: 2, 1: 1, 2: 0.5, }; const BASE_STEP_DURATION = 1500; export function useVisualization( algorithm: AlgorithmDefinition ): UseVisualizationReturn { const [playback, setPlayback] = useState({ isPlaying: false, currentStepIndex: 0, speed: 1, }); const intervalRef = useRef | 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, }; }