import { ref, computed, type Ref, onUnmounted, toRef, isRef } from 'vue'; import type { Sprite } from '@/types/sprites'; export interface AnimationFramesOptions { sprites: Ref | Sprite[] | (() => Sprite[]); onDraw: () => void; } export function useAnimationFrames(options: AnimationFramesOptions) { const { onDraw } = options; // Convert sprites to a computed ref for reactivity const spritesRef = computed(() => { if (typeof options.sprites === 'function') { return options.sprites(); } if (isRef(options.sprites)) { return options.sprites.value; } return options.sprites; }); // Helper to get sprites array const getSprites = () => spritesRef.value; // State const currentFrameIndex = ref(0); const isPlaying = ref(false); const fps = ref(12); const hiddenFrames = ref([]); // Animation internals const animationFrameId = ref(null); const lastFrameTime = ref(0); // Computed properties for visible frames const visibleFrames = computed(() => getSprites().filter((_, index) => !hiddenFrames.value.includes(index))); const visibleFramesCount = computed(() => visibleFrames.value.length); const visibleFrameIndex = computed(() => { const sprites = getSprites(); const currentSprite = sprites[currentFrameIndex.value]; return visibleFrames.value.findIndex(s => s === currentSprite); }); const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1); // Animation control const animateFrame = () => { const now = performance.now(); const elapsed = now - lastFrameTime.value; const frameTime = 1000 / fps.value; if (elapsed >= frameTime) { lastFrameTime.value = now - (elapsed % frameTime); nextFrame(); } animationFrameId.value = requestAnimationFrame(animateFrame); }; const startAnimation = () => { lastFrameTime.value = performance.now(); animateFrame(); }; const stopAnimation = () => { if (animationFrameId.value !== null) { cancelAnimationFrame(animationFrameId.value); animationFrameId.value = null; } }; const togglePlayback = () => { isPlaying.value = !isPlaying.value; if (isPlaying.value) { startAnimation(); } else { stopAnimation(); } }; const nextFrame = () => { if (visibleFrames.value.length === 0) return; const sprites = getSprites(); const currentVisibleIndex = visibleFrameIndex.value; const nextVisibleIndex = (currentVisibleIndex + 1) % visibleFrames.value.length; currentFrameIndex.value = sprites.indexOf(visibleFrames.value[nextVisibleIndex]); onDraw(); }; const previousFrame = () => { if (visibleFrames.value.length === 0) return; const sprites = getSprites(); const currentVisibleIndex = visibleFrameIndex.value; const prevVisibleIndex = (currentVisibleIndex - 1 + visibleFrames.value.length) % visibleFrames.value.length; currentFrameIndex.value = sprites.indexOf(visibleFrames.value[prevVisibleIndex]); onDraw(); }; const handleSliderInput = (event: Event) => { const sprites = getSprites(); const target = event.target as HTMLInputElement; const index = parseInt(target.value); currentFrameIndex.value = sprites.indexOf(visibleFrames.value[index]); }; // Frame visibility management const toggleHiddenFrame = (index: number) => { const sprites = getSprites(); const currentIndex = hiddenFrames.value.indexOf(index); if (currentIndex === -1) { hiddenFrames.value.push(index); // If hiding current frame, switch to next visible if (index === currentFrameIndex.value) { const nextVisible = sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i)); if (nextVisible !== -1) { currentFrameIndex.value = nextVisible; } } } else { hiddenFrames.value.splice(currentIndex, 1); } onDraw(); }; const showAllFrames = () => { hiddenFrames.value = []; onDraw(); }; const hideAllFrames = () => { const sprites = getSprites(); hiddenFrames.value = sprites.map((_, index) => index); // Keep at least one frame visible if (hiddenFrames.value.length > 0) { hiddenFrames.value.splice(currentFrameIndex.value, 1); } onDraw(); }; // Cleanup on unmount onUnmounted(() => { stopAnimation(); }); return { // State currentFrameIndex, isPlaying, fps, hiddenFrames, // Computed visibleFrames, visibleFramesCount, visibleFrameIndex, visibleFrameNumber, // Methods togglePlayback, nextFrame, previousFrame, handleSliderInput, toggleHiddenFrame, showAllFrames, hideAllFrames, stopAnimation, }; }