Continuation of separting logic into domain specific composables

This commit is contained in:
2025-11-18 20:11:36 +01:00
parent d571cb51cb
commit 404ca9ce88
7 changed files with 942 additions and 560 deletions

View File

@@ -157,10 +157,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from '@/composables/useSprites';
import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom';
import { useAnimationFrames } from '@/composables/useAnimationFrames';
const props = defineProps<{
sprites: Sprite[];
@@ -172,17 +175,43 @@
}>();
const previewCanvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
// Get settings from store
const settingsStore = useSettingsStore();
// Initialize composables
const canvas2D = useCanvas2D(previewCanvasRef);
const { zoom, increase: increaseZoom, decrease: decreaseZoom } = useZoom({
allowedValues: [0.5, 1, 2, 3, 4, 5],
initial: 1,
});
const {
currentFrameIndex,
isPlaying,
fps,
hiddenFrames,
visibleFrames,
visibleFramesCount,
visibleFrameIndex,
visibleFrameNumber,
togglePlayback,
nextFrame,
previousFrame,
handleSliderInput,
toggleHiddenFrame,
showAllFrames,
hideAllFrames,
stopAnimation,
} = useAnimationFrames({
sprites: props.sprites,
onDraw: drawPreviewCanvas,
});
// Preview state
const currentFrameIndex = ref(0);
const isPlaying = ref(false);
const fps = ref(12);
const zoom = ref(1);
const isDraggable = ref(false);
const showAllSprites = ref(false);
const animationFrameId = ref<number | null>(null);
const lastFrameTime = ref(0);
// Dragging state
const isDragging = ref(false);
@@ -191,128 +220,44 @@
const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Add this after other refs
const hiddenFrames = ref<number[]>([]);
// Get settings from store
const settingsStore = useSettingsStore();
// Add these computed properties
const visibleFrames = computed(() => props.sprites.filter((_, index) => !hiddenFrames.value.includes(index)));
const visibleFramesCount = computed(() => visibleFrames.value.length);
const visibleFrameIndex = computed(() => {
return visibleFrames.value.findIndex((_, idx) => idx === visibleFrames.value.findIndex(s => s === props.sprites[currentFrameIndex.value]));
});
const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1);
// Canvas drawing
const drawPreviewCanvas = () => {
if (!previewCanvasRef.value || !ctx.value || props.sprites.length === 0) return;
function drawPreviewCanvas() {
if (!previewCanvasRef.value || !canvas2D.ctx.value || props.sprites.length === 0) return;
const currentSprite = props.sprites[currentFrameIndex.value];
if (!currentSprite) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
// Apply pixel art optimization consistently from store
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Apply pixel art optimization
canvas2D.applySmoothing();
// Set canvas size to just fit one sprite cell
previewCanvasRef.value.width = maxWidth;
previewCanvasRef.value.height = maxHeight;
canvas2D.setCanvasSize(maxWidth, maxHeight);
// Clear canvas
ctx.value.clearRect(0, 0, previewCanvasRef.value.width, previewCanvasRef.value.height);
canvas2D.clear();
// Draw grid background (cell)
ctx.value.fillStyle = '#f9fafb';
ctx.value.fillRect(0, 0, maxWidth, maxHeight);
// Keep pixel art optimization consistent throughout all drawing operations
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
canvas2D.fillRect(0, 0, maxWidth, maxHeight, '#f9fafb');
// Draw all sprites with transparency if enabled
if (showAllSprites.value && props.sprites.length > 1) {
ctx.value.globalAlpha = 0.3;
props.sprites.forEach((sprite, index) => {
if (index !== currentFrameIndex.value && !hiddenFrames.value.includes(index)) {
// Use Math.floor for pixel-perfect positioning
ctx.value?.drawImage(sprite.img, Math.floor(sprite.x), Math.floor(sprite.y));
canvas2D.drawImage(sprite.img, sprite.x, sprite.y, 0.3);
}
});
ctx.value.globalAlpha = 1.0;
}
// Draw current sprite with integer positions for pixel-perfect rendering
ctx.value.drawImage(currentSprite.img, Math.floor(currentSprite.x), Math.floor(currentSprite.y));
// Draw current sprite
canvas2D.drawImage(currentSprite.img, currentSprite.x, currentSprite.y);
// Draw cell border
ctx.value.strokeStyle = '#e5e7eb';
ctx.value.lineWidth = 1;
ctx.value.strokeRect(0, 0, maxWidth, maxHeight);
};
canvas2D.strokeRect(0, 0, maxWidth, maxHeight, '#e5e7eb', 1);
}
// Animation control
const togglePlayback = () => {
isPlaying.value = !isPlaying.value;
if (isPlaying.value) {
startAnimation();
} else {
stopAnimation();
}
};
const startAnimation = () => {
lastFrameTime.value = performance.now();
animateFrame();
};
const stopAnimation = () => {
if (animationFrameId.value !== null) {
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
};
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 nextFrame = () => {
if (visibleFrames.value.length === 0) return;
const currentVisibleIndex = visibleFrameIndex.value;
const nextVisibleIndex = (currentVisibleIndex + 1) % visibleFrames.value.length;
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[nextVisibleIndex]);
drawPreviewCanvas();
};
const previousFrame = () => {
if (visibleFrames.value.length === 0) return;
const currentVisibleIndex = visibleFrameIndex.value;
const prevVisibleIndex = (currentVisibleIndex - 1 + visibleFrames.value.length) % visibleFrames.value.length;
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[prevVisibleIndex]);
drawPreviewCanvas();
};
// Add this method to handle slider input
const handleSliderInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const index = parseInt(target.value);
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[index]);
};
// Drag functionality
const startDrag = (event: MouseEvent) => {
@@ -372,22 +317,6 @@
activeSpriteId.value = null;
};
// Add helper methods for mobile zoom controls
const increaseZoom = () => {
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex < zoomValues.length - 1) {
zoom.value = zoomValues[currentIndex + 1];
}
};
const decreaseZoom = () => {
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex > 0) {
zoom.value = zoomValues[currentIndex - 1];
}
};
const handleTouchStart = (event: TouchEvent) => {
if (!isDraggable.value) return;
@@ -421,10 +350,8 @@
// Lifecycle hooks
onMounted(() => {
if (previewCanvasRef.value) {
ctx.value = previewCanvasRef.value.getContext('2d');
drawPreviewCanvas();
}
canvas2D.initContext();
drawPreviewCanvas();
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
@@ -437,17 +364,9 @@
// Handler for force redraw event
const handleForceRedraw = () => {
// Ensure we're using integer positions for pixel-perfect rendering
props.sprites.forEach(sprite => {
sprite.x = Math.floor(sprite.x);
sprite.y = Math.floor(sprite.y);
});
// Force a redraw with the correct image smoothing settings
if (ctx.value) {
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
drawPreviewCanvas();
}
canvas2D.ensureIntegerPositions(props.sprites);
canvas2D.applySmoothing();
drawPreviewCanvas();
};
// Watchers
@@ -464,41 +383,6 @@
drawPreviewCanvas();
}
const toggleHiddenFrame = (index: number) => {
const currentIndex = hiddenFrames.value.indexOf(index);
if (currentIndex === -1) {
// Adding to hidden frames
hiddenFrames.value.push(index);
// If we're hiding the current frame, switch to the next visible frame
if (index === currentFrameIndex.value) {
const nextVisibleSprite = props.sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i));
if (nextVisibleSprite !== -1) {
currentFrameIndex.value = nextVisibleSprite;
}
}
} else {
// Removing from hidden frames
hiddenFrames.value.splice(currentIndex, 1);
}
// Force a redraw
drawPreviewCanvas();
};
const showAllFrames = () => {
hiddenFrames.value = [];
drawPreviewCanvas();
};
const hideAllFrames = () => {
hiddenFrames.value = props.sprites.map((_, index) => index);
// Keep at least one frame visible
if (hiddenFrames.value.length > 0) {
hiddenFrames.value.splice(currentFrameIndex.value, 1);
}
drawPreviewCanvas();
};
</script>
<style scoped>