179 lines
4.7 KiB
TypeScript
179 lines
4.7 KiB
TypeScript
import { ref, computed, type Ref, onUnmounted, toRef, isRef } from 'vue';
|
|
import type { Sprite } from '@/types/sprites';
|
|
|
|
export interface AnimationFramesOptions {
|
|
sprites: Ref<Sprite[]> | 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<number[]>([]);
|
|
|
|
// Animation internals
|
|
const animationFrameId = ref<number | null>(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,
|
|
};
|
|
}
|