Files
spritesheet-generator/src/composables/useAnimationFrames.ts
2025-11-18 20:12:32 +01:00

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,
};
}