Fixes, added zoom
This commit is contained in:
parent
516bf02409
commit
319a052d48
@ -6,252 +6,392 @@
|
||||
<i class="fas fa-th-large text-blue-500"></i>
|
||||
<span>Spritesheet</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="relative overflow-auto bg-gray-700 rounded border border-gray-600">
|
||||
<canvas ref="canvasEl" class="block mx-auto"></canvas>
|
||||
<div class="text-sm text-gray-400">
|
||||
<span>Zoom: {{ Math.round(store.zoomLevel.value * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div ref="containerEl" class="relative overflow-auto bg-gray-700 rounded border border-gray-600 h-96"
|
||||
:class="{ 'cursor-grab': !isPanning, 'cursor-grabbing': isPanning }">
|
||||
<canvas
|
||||
ref="canvasEl"
|
||||
class="block"
|
||||
:style="{
|
||||
transform: `scale(${store.zoomLevel.value})`,
|
||||
transformOrigin: 'top left',
|
||||
width: zoomedWidth + 'px',
|
||||
height: zoomedHeight + 'px'
|
||||
}"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div v-if="isTooltipVisible" :style="tooltipStyle" class="absolute bg-gray-700 text-gray-200 px-3 py-2 rounded text-xs z-50 pointer-events-none shadow-md border border-gray-600">
|
||||
{{ tooltipText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
|
||||
const store = useSpritesheetStore();
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null);
|
||||
const store = useSpritesheetStore();
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null);
|
||||
const containerEl = ref<HTMLDivElement | null>(null);
|
||||
|
||||
// Tooltip state
|
||||
const isTooltipVisible = ref(false);
|
||||
const tooltipText = ref('');
|
||||
const tooltipPosition = ref({ x: 0, y: 0 });
|
||||
// Panning state
|
||||
const isPanning = ref(false);
|
||||
const isAltPressed = ref(false);
|
||||
const isMiddleMouseDown = ref(false);
|
||||
const lastPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
const tooltipStyle = computed(() => ({
|
||||
left: `${tooltipPosition.value.x + 15}px`,
|
||||
top: `${tooltipPosition.value.y + 15}px`,
|
||||
}));
|
||||
// Tooltip state
|
||||
const isTooltipVisible = ref(false);
|
||||
const tooltipText = ref('');
|
||||
const tooltipPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
const setupCheckerboardPattern = () => {
|
||||
if (!canvasEl.value) {
|
||||
console.error('MainContent: Canvas element not available for checkerboard pattern');
|
||||
return;
|
||||
}
|
||||
// Responsive canvas sizing
|
||||
const containerWidth = ref(0);
|
||||
const containerHeight = ref(0);
|
||||
const baseCanvasWidth = ref(0);
|
||||
const baseCanvasHeight = ref(0);
|
||||
|
||||
try {
|
||||
// This will be done with CSS using Tailwind's bg utilities
|
||||
canvasEl.value.style.backgroundImage = `
|
||||
// Computed properties for zoomed dimensions
|
||||
const zoomedWidth = computed(() => {
|
||||
return baseCanvasWidth.value * store.zoomLevel.value;
|
||||
});
|
||||
|
||||
const zoomedHeight = computed(() => {
|
||||
return baseCanvasHeight.value * store.zoomLevel.value;
|
||||
});
|
||||
|
||||
const tooltipStyle = computed(() => ({
|
||||
left: `${tooltipPosition.value.x + 15}px`,
|
||||
top: `${tooltipPosition.value.y + 15}px`,
|
||||
}));
|
||||
|
||||
// Watch for zoom changes to update the container scroll position
|
||||
watch(() => store.zoomLevel.value, (newZoom, oldZoom) => {
|
||||
if (!containerEl.value) return;
|
||||
|
||||
// Adjust scroll position to keep the center point consistent when zooming
|
||||
const centerX = containerEl.value.scrollLeft + containerEl.value.clientWidth / 2;
|
||||
const centerY = containerEl.value.scrollTop + containerEl.value.clientHeight / 2;
|
||||
|
||||
// Calculate new scroll position based on new zoom level
|
||||
const scaleChange = newZoom / oldZoom;
|
||||
containerEl.value.scrollLeft = centerX * scaleChange - containerEl.value.clientWidth / 2;
|
||||
containerEl.value.scrollTop = centerY * scaleChange - containerEl.value.clientHeight / 2;
|
||||
|
||||
// Re-render the canvas with the new zoom level
|
||||
updateCanvasSize();
|
||||
store.renderSpritesheetPreview();
|
||||
});
|
||||
|
||||
const setupCheckerboardPattern = () => {
|
||||
if (!canvasEl.value) return;
|
||||
|
||||
canvasEl.value.style.backgroundImage = `
|
||||
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
|
||||
`;
|
||||
canvasEl.value.style.backgroundSize = '20px 20px';
|
||||
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
|
||||
} catch (error) {
|
||||
console.error('MainContent: Error setting up checkerboard pattern:', error);
|
||||
canvasEl.value.style.backgroundSize = '20px 20px';
|
||||
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
|
||||
};
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
if (!canvasEl.value || !containerEl.value) return;
|
||||
|
||||
// Get the container dimensions
|
||||
containerWidth.value = containerEl.value.clientWidth;
|
||||
containerHeight.value = containerEl.value.clientHeight;
|
||||
|
||||
// Set the base canvas size to fill the container
|
||||
// These are the "unzoomed" dimensions
|
||||
baseCanvasWidth.value = Math.max(containerWidth.value,
|
||||
store.cellSize.width * Math.ceil(containerWidth.value / store.cellSize.width));
|
||||
baseCanvasHeight.value = Math.max(containerHeight.value,
|
||||
store.cellSize.height * Math.ceil(containerHeight.value / store.cellSize.height));
|
||||
|
||||
// Update the actual canvas element size
|
||||
canvasEl.value.width = baseCanvasWidth.value;
|
||||
canvasEl.value.height = baseCanvasHeight.value;
|
||||
|
||||
// Trigger a re-render
|
||||
store.renderSpritesheetPreview();
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (!canvasEl.value || store.sprites.value.length === 0) return;
|
||||
|
||||
const rect = canvasEl.value.getBoundingClientRect();
|
||||
// Adjust coordinates based on zoom level
|
||||
const x = (e.clientX - rect.left) / store.zoomLevel.value;
|
||||
const y = (e.clientY - rect.top) / store.zoomLevel.value;
|
||||
|
||||
// Find which sprite was clicked
|
||||
for (let i = store.sprites.value.length - 1; i >= 0; i--) {
|
||||
const sprite = store.sprites.value[i];
|
||||
if (x >= sprite.x && x <= sprite.x + store.cellSize.width && y >= sprite.y && y <= sprite.y + store.cellSize.height) {
|
||||
store.draggedSprite.value = sprite;
|
||||
store.dragOffset.x = x - sprite.x;
|
||||
store.dragOffset.y = y - sprite.y;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (!canvasEl.value || store.sprites.value.length === 0) return;
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
// Don't process sprite movement or tooltips while panning
|
||||
if (isPanning.value) return;
|
||||
|
||||
const rect = canvasEl.value.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
if (!canvasEl.value) return;
|
||||
|
||||
// Find which sprite was clicked
|
||||
for (let i = store.sprites.value.length - 1; i >= 0; i--) {
|
||||
const sprite = store.sprites.value[i];
|
||||
if (x >= sprite.x && x <= sprite.x + store.cellSize.width && y >= sprite.y && y <= sprite.y + store.cellSize.height) {
|
||||
store.draggedSprite.value = sprite;
|
||||
store.dragOffset.x = x - sprite.x;
|
||||
store.dragOffset.y = y - sprite.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const rect = canvasEl.value.getBoundingClientRect();
|
||||
// Adjust coordinates for zoom
|
||||
const x = (e.clientX - rect.left) / store.zoomLevel.value;
|
||||
const y = (e.clientY - rect.top) / store.zoomLevel.value;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!canvasEl.value) return;
|
||||
// Update tooltip
|
||||
const cellX = Math.floor(x / store.cellSize.width);
|
||||
const cellY = Math.floor(y / store.cellSize.height);
|
||||
|
||||
const rect = canvasEl.value.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Update tooltip
|
||||
const cellX = Math.floor(x / store.cellSize.width);
|
||||
const cellY = Math.floor(y / store.cellSize.height);
|
||||
|
||||
if (canvasEl.value && cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width && cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
|
||||
isTooltipVisible.value = true;
|
||||
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
|
||||
// Use pageX and pageY instead of clientX and clientY
|
||||
tooltipPosition.value.x = e.pageX;
|
||||
tooltipPosition.value.y = e.pageY;
|
||||
} else {
|
||||
isTooltipVisible.value = false;
|
||||
}
|
||||
|
||||
// Move the sprite if we're dragging one
|
||||
if (store.draggedSprite.value) {
|
||||
if (store.isShiftPressed.value) {
|
||||
// Free positioning within the cell bounds when shift is pressed
|
||||
// First determine which cell we're in
|
||||
const cellX = Math.floor(store.draggedSprite.value.x / store.cellSize.width);
|
||||
const cellY = Math.floor(store.draggedSprite.value.y / store.cellSize.height);
|
||||
|
||||
// Calculate new position with constraints to stay within the cell
|
||||
const newX = x - store.dragOffset.x;
|
||||
const newY = y - store.dragOffset.y;
|
||||
|
||||
// Calculate cell boundaries
|
||||
const cellLeft = cellX * store.cellSize.width;
|
||||
const cellTop = cellY * store.cellSize.height;
|
||||
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.img.width;
|
||||
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.img.height;
|
||||
|
||||
// Constrain position to stay within the cell
|
||||
store.draggedSprite.value.x = Math.max(cellLeft, Math.min(newX, cellRight));
|
||||
store.draggedSprite.value.y = Math.max(cellTop, Math.min(newY, cellBottom));
|
||||
|
||||
// Trigger a re-render
|
||||
store.renderSpritesheetPreview();
|
||||
} else {
|
||||
// Calculate new position based on grid cells (snap to grid)
|
||||
const newCellX = Math.floor((x - store.dragOffset.x) / store.cellSize.width);
|
||||
const newCellY = Math.floor((y - store.dragOffset.y) / store.cellSize.height);
|
||||
|
||||
// Make sure we stay within bounds
|
||||
if (canvasEl.value) {
|
||||
const maxCellX = Math.floor(canvasEl.value.width / store.cellSize.width) - 1;
|
||||
const maxCellY = Math.floor(canvasEl.value.height / store.cellSize.height) - 1;
|
||||
|
||||
const boundedCellX = Math.max(0, Math.min(newCellX, maxCellX));
|
||||
const boundedCellY = Math.max(0, Math.min(newCellY, maxCellY));
|
||||
|
||||
store.draggedSprite.value.x = boundedCellX * store.cellSize.width;
|
||||
store.draggedSprite.value.y = boundedCellY * store.cellSize.height;
|
||||
|
||||
// Trigger a re-render
|
||||
store.renderSpritesheetPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
store.draggedSprite.value = null;
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
if (canvasEl.value && cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width &&
|
||||
cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
|
||||
isTooltipVisible.value = true;
|
||||
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
|
||||
tooltipPosition.value.x = e.pageX;
|
||||
tooltipPosition.value.y = e.pageY;
|
||||
} else {
|
||||
isTooltipVisible.value = false;
|
||||
store.draggedSprite.value = null;
|
||||
};
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
store.isShiftPressed.value = true;
|
||||
}
|
||||
};
|
||||
// Move the sprite if we're dragging one
|
||||
if (store.draggedSprite.value) {
|
||||
if (store.isShiftPressed.value) {
|
||||
// Free positioning within the cell bounds when shift is pressed
|
||||
const cellX = Math.floor(store.draggedSprite.value.x / store.cellSize.width);
|
||||
const cellY = Math.floor(store.draggedSprite.value.y / store.cellSize.height);
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
store.isShiftPressed.value = false;
|
||||
}
|
||||
};
|
||||
// Calculate new position with constraints to stay within the cell
|
||||
const newX = x - store.dragOffset.x;
|
||||
const newY = y - store.dragOffset.y;
|
||||
|
||||
const setupCanvasEvents = () => {
|
||||
if (!canvasEl.value) {
|
||||
console.error('MainContent: Canvas element not available for event setup');
|
||||
return;
|
||||
// Calculate cell boundaries
|
||||
const cellLeft = cellX * store.cellSize.width;
|
||||
const cellTop = cellY * store.cellSize.height;
|
||||
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.img.width;
|
||||
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.img.height;
|
||||
|
||||
// Constrain position to stay within the cell
|
||||
store.draggedSprite.value.x = Math.max(cellLeft, Math.min(newX, cellRight));
|
||||
store.draggedSprite.value.y = Math.max(cellTop, Math.min(newY, cellBottom));
|
||||
} else {
|
||||
// Calculate new position based on grid cells (snap to grid)
|
||||
const newCellX = Math.floor((x - store.dragOffset.x) / store.cellSize.width);
|
||||
const newCellY = Math.floor((y - store.dragOffset.y) / store.cellSize.height);
|
||||
|
||||
// Make sure we stay within bounds
|
||||
if (canvasEl.value) {
|
||||
const maxCellX = Math.floor(canvasEl.value.width / store.cellSize.width) - 1;
|
||||
const maxCellY = Math.floor(canvasEl.value.height / store.cellSize.height) - 1;
|
||||
|
||||
const boundedCellX = Math.max(0, Math.min(newCellX, maxCellX));
|
||||
const boundedCellY = Math.max(0, Math.min(newCellY, maxCellY));
|
||||
|
||||
store.draggedSprite.value.x = boundedCellX * store.cellSize.width;
|
||||
store.draggedSprite.value.y = boundedCellY * store.cellSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
canvasEl.value.addEventListener('mousedown', handleMouseDown);
|
||||
canvasEl.value.addEventListener('mousemove', handleMouseMove);
|
||||
canvasEl.value.addEventListener('mouseup', handleMouseUp);
|
||||
canvasEl.value.addEventListener('mouseout', handleMouseOut);
|
||||
} catch (error) {
|
||||
console.error('MainContent: Error setting up canvas events:', error);
|
||||
// Trigger a re-render
|
||||
store.renderSpritesheetPreview();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
store.draggedSprite.value = null;
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
isTooltipVisible.value = false;
|
||||
store.draggedSprite.value = null;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
store.isShiftPressed.value = true;
|
||||
}
|
||||
|
||||
if (e.key === 'Alt') {
|
||||
e.preventDefault(); // Prevent browser from focusing address bar
|
||||
isAltPressed.value = true;
|
||||
}
|
||||
|
||||
// Add keyboard shortcuts for zooming
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === '=' || e.key === '+') {
|
||||
e.preventDefault();
|
||||
store.zoomIn();
|
||||
} else if (e.key === '-') {
|
||||
e.preventDefault();
|
||||
store.zoomOut();
|
||||
} else if (e.key === '0') {
|
||||
e.preventDefault();
|
||||
store.resetZoom();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize canvas immediately
|
||||
initializeCanvas();
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
store.isShiftPressed.value = false;
|
||||
}
|
||||
|
||||
// Also set up a MutationObserver to ensure the canvas is properly initialized
|
||||
// even if there are DOM changes
|
||||
const observer = new MutationObserver(mutations => {
|
||||
initializeCanvas();
|
||||
if (e.key === 'Alt') {
|
||||
isAltPressed.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasMouseDown = (e: MouseEvent) => {
|
||||
// Middle mouse button or Alt + left click for panning
|
||||
if (e.button === 1 || (e.button === 0 && isAltPressed.value)) {
|
||||
e.preventDefault();
|
||||
isPanning.value = true;
|
||||
lastPosition.value = { x: e.clientX, y: e.clientY };
|
||||
} else {
|
||||
// Regular sprite dragging
|
||||
handleMouseDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasMouseMove = (e: MouseEvent) => {
|
||||
if (isPanning.value && containerEl.value) {
|
||||
e.preventDefault();
|
||||
const dx = e.clientX - lastPosition.value.x;
|
||||
const dy = e.clientY - lastPosition.value.y;
|
||||
|
||||
// Scroll the container in the opposite direction of the mouse movement
|
||||
containerEl.value.scrollLeft -= dx;
|
||||
containerEl.value.scrollTop -= dy;
|
||||
|
||||
// Update the last position for the next movement
|
||||
lastPosition.value = { x: e.clientX, y: e.clientY };
|
||||
} else {
|
||||
// Handle regular mouse move for sprites and tooltip
|
||||
handleMouseMove(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasMouseUp = () => {
|
||||
isPanning.value = false;
|
||||
handleMouseUp();
|
||||
};
|
||||
|
||||
const handleCanvasMouseLeave = () => {
|
||||
isPanning.value = false;
|
||||
handleMouseOut();
|
||||
};
|
||||
|
||||
const preventContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const setupCanvasEvents = () => {
|
||||
if (!canvasEl.value) return;
|
||||
|
||||
// Set up mouse events for the canvas
|
||||
canvasEl.value.addEventListener('mousedown', handleCanvasMouseDown);
|
||||
canvasEl.value.addEventListener('mousemove', handleCanvasMouseMove);
|
||||
canvasEl.value.addEventListener('mouseup', handleCanvasMouseUp);
|
||||
canvasEl.value.addEventListener('mouseleave', handleCanvasMouseLeave);
|
||||
canvasEl.value.addEventListener('contextmenu', preventContextMenu);
|
||||
};
|
||||
|
||||
// Handle window resize to update canvas dimensions
|
||||
const handleResize = () => {
|
||||
updateCanvasSize();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Set up global event listeners
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Initialize the canvas
|
||||
await nextTick();
|
||||
initializeCanvas();
|
||||
|
||||
// Observe container size changes
|
||||
if ('ResizeObserver' in window) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize();
|
||||
});
|
||||
|
||||
// Start observing the document with the configured parameters
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
if (containerEl.value) {
|
||||
resizeObserver.observe(containerEl.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the observer after a short time
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
}, 2000);
|
||||
});
|
||||
const initializeCanvas = () => {
|
||||
if (!canvasEl.value || !containerEl.value) return;
|
||||
|
||||
const initializeCanvas = () => {
|
||||
if (!canvasEl.value) {
|
||||
console.error('MainContent: Canvas element not found');
|
||||
try {
|
||||
const context = canvasEl.value.getContext('2d');
|
||||
if (!context) {
|
||||
console.error('Failed to get 2D context from canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the 2D context
|
||||
const context = canvasEl.value.getContext('2d');
|
||||
if (!context) {
|
||||
console.error('MainContent: Failed to get 2D context from canvas');
|
||||
return;
|
||||
}
|
||||
// Set canvas and context in the store
|
||||
store.canvas.value = canvasEl.value;
|
||||
store.ctx.value = context;
|
||||
|
||||
// Set the store references
|
||||
store.canvas.value = canvasEl.value;
|
||||
store.ctx.value = context;
|
||||
// Set up the checkerboard pattern
|
||||
setupCheckerboardPattern();
|
||||
|
||||
// Initialize canvas size
|
||||
canvasEl.value.width = 400;
|
||||
canvasEl.value.height = 300;
|
||||
// Set up canvas mouse events
|
||||
setupCanvasEvents();
|
||||
|
||||
// Setup the canvas
|
||||
setupCheckerboardPattern();
|
||||
setupCanvasEvents();
|
||||
// Set the initial canvas size based on container
|
||||
updateCanvasSize();
|
||||
|
||||
// Setup keyboard events for modifiers
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
// Check if we have sprites that need rendering
|
||||
if (store.sprites.value.length > 0) {
|
||||
store.updateCellSize();
|
||||
store.autoArrangeSprites();
|
||||
store.renderSpritesheetPreview();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MainContent: Error initializing canvas:', error);
|
||||
// Update sprites if there are any loaded
|
||||
if (store.sprites.value.length > 0) {
|
||||
store.updateCellSize();
|
||||
store.autoArrangeSprites();
|
||||
store.renderSpritesheetPreview();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error initializing canvas:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
onBeforeUnmount(() => {
|
||||
// Remove global event listeners
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
|
||||
if (canvasEl.value) {
|
||||
canvasEl.value.removeEventListener('mousedown', handleMouseDown);
|
||||
canvasEl.value.removeEventListener('mousemove', handleMouseMove);
|
||||
canvasEl.value.removeEventListener('mouseup', handleMouseUp);
|
||||
canvasEl.value.removeEventListener('mouseout', handleMouseOut);
|
||||
}
|
||||
});
|
||||
// Remove canvas event listeners
|
||||
if (canvasEl.value) {
|
||||
canvasEl.value.removeEventListener('mousedown', handleCanvasMouseDown);
|
||||
canvasEl.value.removeEventListener('mousemove', handleCanvasMouseMove);
|
||||
canvasEl.value.removeEventListener('mouseup', handleCanvasMouseUp);
|
||||
canvasEl.value.removeEventListener('mouseleave', handleCanvasMouseLeave);
|
||||
canvasEl.value.removeEventListener('contextmenu', preventContextMenu);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cursor-grab {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.cursor-grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
@ -50,7 +50,22 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Zoom Controls -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<div class="text-sm font-medium text-gray-300 mb-2 w-full">Zoom Controls</div>
|
||||
<button @click="zoomIn" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500">
|
||||
<i class="fas fa-search-plus"></i> Zoom In
|
||||
</button>
|
||||
<button @click="zoomOut" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500">
|
||||
<i class="fas fa-search-minus"></i> Zoom Out
|
||||
</button>
|
||||
<button @click="resetZoom" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500">
|
||||
<i class="fas fa-sync-alt"></i> Reset Zoom
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
<div class="text-sm font-medium text-gray-300 mb-2 w-full">Keyboard Shortcuts</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Shift</kbd>
|
||||
<span>Fine-tune position</span>
|
||||
@ -63,6 +78,30 @@
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Esc</kbd>
|
||||
<span>Close preview</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<div class="flex items-center">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Ctrl</kbd>
|
||||
<span class="mx-1">+</span>
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">+</kbd>
|
||||
</div>
|
||||
<span>Zoom in</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<div class="flex items-center">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Ctrl</kbd>
|
||||
<span class="mx-1">+</span>
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">-</kbd>
|
||||
</div>
|
||||
<span>Zoom out</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<div class="flex items-center">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Ctrl</kbd>
|
||||
<span class="mx-1">+</span>
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">0</kbd>
|
||||
</div>
|
||||
<span>Reset zoom</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,5 +143,5 @@
|
||||
};
|
||||
|
||||
// Expose store methods directly
|
||||
const { autoArrangeSprites, downloadSpritesheet } = store;
|
||||
const { autoArrangeSprites, downloadSpritesheet, zoomIn, zoomOut, resetZoom } = store;
|
||||
</script>
|
||||
|
@ -37,6 +37,7 @@ const draggedSprite = ref<Sprite | null>(null);
|
||||
const dragOffset = reactive({ x: 0, y: 0 });
|
||||
const isShiftPressed = ref(false);
|
||||
const isModalOpen = ref(false);
|
||||
const zoomLevel = ref(1); // Default zoom level (1 = 100%)
|
||||
|
||||
export function useSpritesheetStore() {
|
||||
const animation = reactive<AnimationState>({
|
||||
@ -143,6 +144,11 @@ export function useSpritesheetStore() {
|
||||
|
||||
canvas.value.width = newWidth;
|
||||
canvas.value.height = newHeight;
|
||||
|
||||
// Emit an event to update the wrapper dimensions
|
||||
window.dispatchEvent(new CustomEvent('canvas-size-updated', {
|
||||
detail: { width: newWidth, height: newHeight }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Store: Error updating canvas size:', error);
|
||||
}
|
||||
@ -206,6 +212,10 @@ export function useSpritesheetStore() {
|
||||
// Clear the canvas
|
||||
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
|
||||
// Apply zoom transformation
|
||||
ctx.value.save();
|
||||
ctx.value.scale(zoomLevel.value, zoomLevel.value);
|
||||
|
||||
if (showGrid) {
|
||||
drawGrid();
|
||||
}
|
||||
@ -226,7 +236,10 @@ export function useSpritesheetStore() {
|
||||
// If image isn't loaded yet, set an onload handler
|
||||
sprite.img.onload = () => {
|
||||
if (ctx.value && canvas.value) {
|
||||
ctx.value.save();
|
||||
ctx.value.scale(zoomLevel.value, zoomLevel.value);
|
||||
ctx.value.drawImage(sprite.img, sprite.x, sprite.y);
|
||||
ctx.value.restore();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -234,6 +247,9 @@ export function useSpritesheetStore() {
|
||||
console.error(`Store: Error rendering sprite at index ${index}:`, spriteError);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore the canvas state
|
||||
ctx.value.restore();
|
||||
} catch (error) {
|
||||
console.error('Store: Error rendering spritesheet preview:', error);
|
||||
}
|
||||
@ -243,21 +259,25 @@ export function useSpritesheetStore() {
|
||||
if (!ctx.value || !canvas.value) return;
|
||||
|
||||
ctx.value.strokeStyle = '#333';
|
||||
ctx.value.lineWidth = 1;
|
||||
ctx.value.lineWidth = 1 / zoomLevel.value; // Adjust line width based on zoom level
|
||||
|
||||
// Calculate the visible area based on zoom level
|
||||
const visibleWidth = canvas.value.width / zoomLevel.value;
|
||||
const visibleHeight = canvas.value.height / zoomLevel.value;
|
||||
|
||||
// Draw vertical lines
|
||||
for (let x = 0; x <= canvas.value.width; x += cellSize.width) {
|
||||
for (let x = 0; x <= visibleWidth; x += cellSize.width) {
|
||||
ctx.value.beginPath();
|
||||
ctx.value.moveTo(x, 0);
|
||||
ctx.value.lineTo(x, canvas.value.height);
|
||||
ctx.value.lineTo(x, visibleHeight);
|
||||
ctx.value.stroke();
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (let y = 0; y <= canvas.value.height; y += cellSize.height) {
|
||||
for (let y = 0; y <= visibleHeight; y += cellSize.height) {
|
||||
ctx.value.beginPath();
|
||||
ctx.value.moveTo(0, y);
|
||||
ctx.value.lineTo(canvas.value.width, y);
|
||||
ctx.value.lineTo(visibleWidth, y);
|
||||
ctx.value.stroke();
|
||||
}
|
||||
}
|
||||
@ -411,6 +431,27 @@ export function useSpritesheetStore() {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
// Increase zoom level by 0.1, max 3.0 (300%)
|
||||
zoomLevel.value = Math.min(3.0, zoomLevel.value + 0.1);
|
||||
renderSpritesheetPreview();
|
||||
showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`);
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
// Decrease zoom level by 0.1, min 0.5 (50%)
|
||||
zoomLevel.value = Math.max(0.5, zoomLevel.value - 0.1);
|
||||
renderSpritesheetPreview();
|
||||
showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`);
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
// Reset to default zoom level (100%)
|
||||
zoomLevel.value = 1;
|
||||
renderSpritesheetPreview();
|
||||
showNotification('Zoom reset to 100%');
|
||||
}
|
||||
|
||||
return {
|
||||
sprites,
|
||||
canvas,
|
||||
@ -423,6 +464,7 @@ export function useSpritesheetStore() {
|
||||
isModalOpen,
|
||||
animation,
|
||||
notification,
|
||||
zoomLevel,
|
||||
addSprites,
|
||||
updateCellSize,
|
||||
updateCanvasSize,
|
||||
@ -436,5 +478,8 @@ export function useSpritesheetStore() {
|
||||
stopAnimation,
|
||||
renderAnimationFrame,
|
||||
showNotification,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
resetZoom,
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user