Continuation of separting logic into domain specific composables
This commit is contained in:
167
src/composables/useAnimationFrames.ts
Normal file
167
src/composables/useAnimationFrames.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { ref, computed, type Ref, onUnmounted } 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;
|
||||
|
||||
// Helper to get sprites array
|
||||
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user