/** * 항적조회 전용 애니메이션 스토어 * * - 재생/일시정지/정지 * - 배속 조절 (1x ~ 1000x) * - 반복 재생 * - requestAnimationFrame 기반 애니메이션 */ import { create } from 'zustand'; // 애니메이션 프레임 관리용 변수 (스토어 외부) let animationFrameId = null; let lastFrameTime = null; export const useTrackQueryAnimationStore = create((set, get) => { const animate = () => { const state = get(); if (!state.isPlaying) return; const now = performance.now(); if (lastFrameTime === null) { lastFrameTime = now; } const delta = now - lastFrameTime; lastFrameTime = now; const newTime = state.currentTime + delta * state.playbackSpeed; const effectiveEnd = state.loop ? state.loopEnd : state.endTime; const effectiveStart = state.loop ? state.loopStart : state.startTime; if (newTime >= effectiveEnd) { if (state.loop) { set({ currentTime: effectiveStart }); } else { set({ currentTime: state.endTime, isPlaying: false }); animationFrameId = null; lastFrameTime = null; return; } } else { set({ currentTime: newTime }); } animationFrameId = requestAnimationFrame(animate); }; return { isPlaying: false, currentTime: 0, startTime: 0, endTime: 0, playbackSpeed: 1, loop: false, loopStart: 0, loopEnd: 0, play: () => { const state = get(); if (state.endTime <= state.startTime) return; lastFrameTime = null; if (state.loop) { if (state.currentTime < state.loopStart || state.currentTime >= state.loopEnd) { set({ isPlaying: true, currentTime: state.loopStart }); } else { set({ isPlaying: true }); } } else { set({ isPlaying: true }); } animationFrameId = requestAnimationFrame(animate); }, pause: () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } lastFrameTime = null; set({ isPlaying: false }); }, stop: () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } lastFrameTime = null; set({ isPlaying: false, currentTime: get().startTime }); }, setCurrentTime: (time) => { const { startTime, endTime } = get(); const clampedTime = Math.max(startTime, Math.min(endTime, time)); set({ currentTime: clampedTime }); }, setPlaybackSpeed: (speed) => set({ playbackSpeed: speed }), toggleLoop: () => set({ loop: !get().loop }), setLoopSection: (start, end) => { const { startTime, endTime } = get(); const clampedStart = Math.max(startTime, Math.min(end, start)); const clampedEnd = Math.max(start, Math.min(endTime, end)); set({ loopStart: clampedStart, loopEnd: clampedEnd }); }, resetLoopSection: () => { const { startTime, endTime } = get(); set({ loopStart: startTime, loopEnd: endTime }); }, setTimeRange: (start, end) => { set({ startTime: start, endTime: end, currentTime: start, loopStart: start, loopEnd: end, }); }, getProgress: () => { const { currentTime, startTime, endTime } = get(); if (endTime <= startTime) return 0; return ((currentTime - startTime) / (endTime - startTime)) * 100; }, getLoopProgress: () => { const { startTime, endTime, loopStart, loopEnd } = get(); if (endTime <= startTime) return { start: 0, end: 100 }; const totalDuration = endTime - startTime; return { start: ((loopStart - startTime) / totalDuration) * 100, end: ((loopEnd - startTime) / totalDuration) * 100, }; }, reset: () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } lastFrameTime = null; set({ isPlaying: false, currentTime: 0, startTime: 0, endTime: 0, playbackSpeed: 1, loop: false, loopStart: 0, loopEnd: 0, }); }, }; }); /** 재생 가능한 배속 옵션 */ export const PLAYBACK_SPEED_OPTIONS = [1, 5, 10, 50, 100, 1000];