-
-
-
+
@@ -26,6 +32,12 @@
y: number;
}
+ interface CellPosition {
+ col: number;
+ row: number;
+ index: number;
+ }
+
const props = defineProps<{
sprites: Sprite[];
columns: number;
@@ -33,6 +45,7 @@
const emit = defineEmits<{
(e: 'updateSprite', id: string, x: number, y: number): void;
+ (e: 'updateSpriteCell', id: string, newIndex: number): void;
}>();
// Get settings from store
@@ -41,12 +54,20 @@
const canvasRef = ref
(null);
const ctx = ref(null);
- // Dragging state
+ // State for tracking drag operations
const isDragging = ref(false);
const activeSpriteId = ref(null);
+ const activeSpriteCellIndex = ref(null);
const dragStartX = ref(0);
const dragStartY = ref(0);
- const spritePosBeforeDrag = ref({ x: 0, y: 0 });
+ const dragOffsetX = ref(0);
+ const dragOffsetY = ref(0);
+ const allowCellSwap = ref(false);
+ const currentHoverCell = ref(null);
+
+ // Visual feedback refs
+ const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
+ const highlightCell = ref(null);
const spritePositions = computed(() => {
if (!canvasRef.value) return [];
@@ -67,6 +88,9 @@
height: sprite.height,
maxWidth,
maxHeight,
+ col,
+ row,
+ index,
};
});
});
@@ -80,7 +104,8 @@
maxHeight = Math.max(maxHeight, sprite.height);
});
- return { maxWidth, maxHeight };
+ // Add some padding to ensure sprites have room to move
+ return { maxWidth: maxWidth, maxHeight: maxHeight };
};
const startDrag = (event: MouseEvent) => {
@@ -94,23 +119,47 @@
const mouseY = (event.clientY - rect.top) * scaleY;
const clickedSprite = findSpriteAtPosition(mouseX, mouseY);
-
if (clickedSprite) {
isDragging.value = true;
activeSpriteId.value = clickedSprite.id;
dragStartX.value = mouseX;
dragStartY.value = mouseY;
- // Find current sprite position
- const sprite = props.sprites.find(s => s.id === clickedSprite.id);
- if (sprite) {
- spritePosBeforeDrag.value = { x: sprite.x, y: sprite.y };
+ // Find the sprite's position to calculate offset from mouse to sprite origin
+ const spritePosition = spritePositions.value.find(pos => pos.id === clickedSprite.id);
+ if (spritePosition) {
+ dragOffsetX.value = mouseX - spritePosition.canvasX;
+ dragOffsetY.value = mouseY - spritePosition.canvasY;
+ activeSpriteCellIndex.value = spritePosition.index;
+
+ // Store the starting cell position
+ const startCell = findCellAtPosition(mouseX, mouseY);
+ if (startCell) {
+ currentHoverCell.value = startCell;
+ highlightCell.value = null; // No highlight at the start
+ }
}
}
};
+ const findCellAtPosition = (x: number, y: number): CellPosition | null => {
+ const { maxWidth, maxHeight } = calculateMaxDimensions();
+ const col = Math.floor(x / maxWidth);
+ const row = Math.floor(y / maxHeight);
+
+ // Check if the cell position is valid
+ const totalRows = Math.ceil(props.sprites.length / props.columns);
+ if (col >= 0 && col < props.columns && row >= 0 && row < totalRows) {
+ const index = row * props.columns + col;
+ if (index < props.sprites.length) {
+ return { col, row, index };
+ }
+ }
+ return null;
+ };
+
const drag = (event: MouseEvent) => {
- if (!isDragging.value || !activeSpriteId.value || !canvasRef.value) return;
+ if (!isDragging.value || !activeSpriteId.value || !canvasRef.value || activeSpriteCellIndex.value === null) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
@@ -119,50 +168,101 @@
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
- const deltaX = mouseX - dragStartX.value;
- const deltaY = mouseY - dragStartY.value;
-
- // Calculate new position with constraints
const spriteIndex = props.sprites.findIndex(s => s.id === activeSpriteId.value);
if (spriteIndex === -1) return;
+ // Find the cell the mouse is currently over
+ const hoverCell = findCellAtPosition(mouseX, mouseY);
+ currentHoverCell.value = hoverCell;
+
+ if (allowCellSwap.value && hoverCell) {
+ // If we're hovering over a different cell than the sprite's current cell
+ if (hoverCell.index !== activeSpriteCellIndex.value) {
+ // Show a highlight for the target cell
+ highlightCell.value = hoverCell;
+
+ // Create a ghost sprite that follows the mouse
+ ghostSprite.value = {
+ id: activeSpriteId.value,
+ x: mouseX - dragOffsetX.value,
+ y: mouseY - dragOffsetY.value,
+ };
+
+ drawCanvas();
+ } else {
+ // Same cell as the sprite's origin, just do regular movement
+ highlightCell.value = null;
+ ghostSprite.value = null;
+ handleInCellMovement(mouseX, mouseY, spriteIndex);
+ }
+ } else {
+ // Regular in-cell movement
+ handleInCellMovement(mouseX, mouseY, spriteIndex);
+ }
+ };
+
+ const handleInCellMovement = (mouseX: number, mouseY: number, spriteIndex: number) => {
+ if (!activeSpriteId.value) return;
+
const position = spritePositions.value.find(pos => pos.id === activeSpriteId.value);
if (!position) return;
- let newX = spritePosBeforeDrag.value.x + deltaX;
- let newY = spritePosBeforeDrag.value.y + deltaY;
+ // Calculate new position based on mouse position and initial click offset
+ const newX = mouseX - position.cellX - dragOffsetX.value;
+ const newY = mouseY - position.cellY - dragOffsetY.value;
- // Constrain movement strictly within cell
- newX = Math.max(0, Math.min(position.maxWidth - props.sprites[spriteIndex].width, newX));
- newY = Math.max(0, Math.min(position.maxHeight - props.sprites[spriteIndex].height, newY));
+ // Constrain within cell boundaries
+ const constrainedX = Math.max(0, Math.min(position.maxWidth - props.sprites[spriteIndex].width, newX));
+ const constrainedY = Math.max(0, Math.min(position.maxHeight - props.sprites[spriteIndex].height, newY));
- emit('updateSprite', activeSpriteId.value, newX, newY);
+ emit('updateSprite', activeSpriteId.value, constrainedX, constrainedY);
drawCanvas();
};
const stopDrag = () => {
+ if (isDragging.value && allowCellSwap.value && activeSpriteId.value && activeSpriteCellIndex.value !== null && currentHoverCell.value && activeSpriteCellIndex.value !== currentHoverCell.value.index) {
+ // We've dragged from one cell to another
+ // Tell parent component to update the sprite's cell index
+ emit('updateSpriteCell', activeSpriteId.value, currentHoverCell.value.index);
+
+ // Also reset the sprite's position within the cell to 0,0
+ emit('updateSprite', activeSpriteId.value, 0, 0);
+ }
+
+ // Reset all drag state
isDragging.value = false;
activeSpriteId.value = null;
+ activeSpriteCellIndex.value = null;
+ currentHoverCell.value = null;
+ highlightCell.value = null;
+ ghostSprite.value = null;
+
+ // Redraw without highlights
+ drawCanvas();
};
const handleTouchStart = (event: TouchEvent) => {
+ event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
- const mouseEvent = new MouseEvent('mousedown', {
+ const mouseEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
- });
+ preventDefault: () => {},
+ } as unknown as MouseEvent;
startDrag(mouseEvent);
}
};
const handleTouchMove = (event: TouchEvent) => {
+ event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
- const mouseEvent = new MouseEvent('mousemove', {
+ const mouseEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
- });
+ preventDefault: () => {},
+ } as unknown as MouseEvent;
drag(mouseEvent);
}
};
@@ -198,25 +298,47 @@
// Disable image smoothing based on pixel perfect setting
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
- // Draw grid
+ // Draw grid and highlights
ctx.value.strokeStyle = '#e5e7eb';
for (let col = 0; col < props.columns; col++) {
for (let row = 0; row < rows; row++) {
+ // Highlight the target cell if specified
+ if (highlightCell.value && highlightCell.value.col === col && highlightCell.value.row === row) {
+ ctx.value.fillStyle = 'rgba(59, 130, 246, 0.2)'; // Light blue highlight
+ ctx.value.fillRect(Math.floor(col * maxWidth), Math.floor(row * maxHeight), maxWidth, maxHeight);
+ }
+
ctx.value.strokeRect(Math.floor(col * maxWidth), Math.floor(row * maxHeight), maxWidth, maxHeight);
}
}
// Draw sprites
props.sprites.forEach((sprite, index) => {
+ // Skip the active sprite if we're showing a ghost instead
+ if (activeSpriteId.value === sprite.id && ghostSprite.value) {
+ return;
+ }
+
const col = index % props.columns;
const row = Math.floor(index / props.columns);
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
- // Draw sprite using integer positions
+ // Draw sprite using integer positions for pixel-perfect rendering
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
});
+
+ // Draw ghost sprite if we're dragging between cells
+ if (ghostSprite.value && activeSpriteId.value) {
+ const sprite = props.sprites.find(s => s.id === activeSpriteId.value);
+ if (sprite) {
+ // Semi-transparent ghost
+ ctx.value.globalAlpha = 0.6;
+ ctx.value.drawImage(sprite.img, Math.floor(ghostSprite.value.x), Math.floor(ghostSprite.value.y));
+ ctx.value.globalAlpha = 1.0;
+ }
+ }
};
onMounted(() => {
diff --git a/src/components/SpritesheetSplitter.vue b/src/components/SpritesheetSplitter.vue
new file mode 100644
index 0000000..c8e986b
--- /dev/null
+++ b/src/components/SpritesheetSplitter.vue
@@ -0,0 +1,348 @@
+
+
+
+
+
+
![Spritesheet]()
+
+
+
+
+
+
+
+
+
+
+
+
+ Low
+ High
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Preview ({{ previewSprites.length }} sprites)
+
+
+
![Sprite preview]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+