Continuation of separting logic into domain specific composables
This commit is contained in:
287
src/composables/useDragSprite.ts
Normal file
287
src/composables/useDragSprite.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Sprite } from '@/types/sprites';
|
||||
import { getMaxDimensions } from './useSprites';
|
||||
|
||||
export interface CellPosition {
|
||||
col: number;
|
||||
row: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface SpritePosition {
|
||||
id: string;
|
||||
canvasX: number;
|
||||
canvasY: number;
|
||||
cellX: number;
|
||||
cellY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
col: number;
|
||||
row: number;
|
||||
index: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface DragSpriteOptions {
|
||||
sprites: Ref<Sprite[]> | ComputedRef<Sprite[]> | Sprite[];
|
||||
columns: Ref<number> | number;
|
||||
zoom?: Ref<number>;
|
||||
allowCellSwap?: Ref<boolean>;
|
||||
getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null;
|
||||
onUpdateSprite: (id: string, x: number, y: number) => void;
|
||||
onUpdateSpriteCell?: (id: string, newIndex: number) => void;
|
||||
onDraw: () => void;
|
||||
}
|
||||
|
||||
export function useDragSprite(options: DragSpriteOptions) {
|
||||
const { getMousePosition, onUpdateSprite, onUpdateSpriteCell, onDraw } = options;
|
||||
|
||||
// Helper to get reactive values
|
||||
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
||||
const getColumns = () => (typeof options.columns === 'number' ? options.columns : options.columns.value);
|
||||
const getZoom = () => options.zoom?.value ?? 1;
|
||||
const getAllowCellSwap = () => options.allowCellSwap?.value ?? false;
|
||||
|
||||
// Drag state
|
||||
const isDragging = ref(false);
|
||||
const activeSpriteId = ref<string | null>(null);
|
||||
const activeSpriteCellIndex = ref<number | null>(null);
|
||||
const dragStartX = ref(0);
|
||||
const dragStartY = ref(0);
|
||||
const dragOffsetX = ref(0);
|
||||
const dragOffsetY = ref(0);
|
||||
const currentHoverCell = ref<CellPosition | null>(null);
|
||||
|
||||
// Visual feedback
|
||||
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
||||
const highlightCell = ref<CellPosition | null>(null);
|
||||
|
||||
// Cache for max dimensions
|
||||
const lastMaxWidth = ref(1);
|
||||
const lastMaxHeight = ref(1);
|
||||
|
||||
const calculateMaxDimensions = () => {
|
||||
const sprites = getSprites();
|
||||
const base = getMaxDimensions(sprites);
|
||||
const maxWidth = Math.max(1, base.maxWidth, lastMaxWidth.value);
|
||||
const maxHeight = Math.max(1, base.maxHeight, lastMaxHeight.value);
|
||||
lastMaxWidth.value = maxWidth;
|
||||
lastMaxHeight.value = maxHeight;
|
||||
return { maxWidth, maxHeight };
|
||||
};
|
||||
|
||||
// Computed sprite positions
|
||||
const spritePositions = computed<SpritePosition[]>(() => {
|
||||
const sprites = getSprites();
|
||||
const columns = getColumns();
|
||||
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
||||
|
||||
return sprites.map((sprite, index) => {
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
|
||||
return {
|
||||
id: sprite.id,
|
||||
canvasX: col * maxWidth + sprite.x,
|
||||
canvasY: row * maxHeight + sprite.y,
|
||||
cellX: col * maxWidth,
|
||||
cellY: row * maxHeight,
|
||||
width: sprite.width,
|
||||
height: sprite.height,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
col,
|
||||
row,
|
||||
index,
|
||||
x: sprite.x,
|
||||
y: sprite.y,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const findCellAtPosition = (x: number, y: number): CellPosition | null => {
|
||||
const sprites = getSprites();
|
||||
const columns = getColumns();
|
||||
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
||||
const col = Math.floor(x / maxWidth);
|
||||
const row = Math.floor(y / maxHeight);
|
||||
|
||||
const totalRows = Math.ceil(sprites.length / columns);
|
||||
if (col >= 0 && col < columns && row >= 0 && row < totalRows) {
|
||||
const index = row * columns + col;
|
||||
if (index < sprites.length) {
|
||||
return { col, row, index };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findSpriteAtPosition = (x: number, y: number): Sprite | null => {
|
||||
const sprites = getSprites();
|
||||
const positions = spritePositions.value;
|
||||
|
||||
for (let i = positions.length - 1; i >= 0; i--) {
|
||||
const pos = positions[i];
|
||||
if (x >= pos.canvasX && x <= pos.canvasX + pos.width && y >= pos.canvasY && y <= pos.canvasY + pos.height) {
|
||||
return sprites.find(s => s.id === pos.id) || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
const pos = getMousePosition(event, getZoom());
|
||||
if (!pos) return;
|
||||
|
||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||
if (clickedSprite) {
|
||||
isDragging.value = true;
|
||||
activeSpriteId.value = clickedSprite.id;
|
||||
dragStartX.value = pos.x;
|
||||
dragStartY.value = pos.y;
|
||||
|
||||
const spritePosition = spritePositions.value.find(p => p.id === clickedSprite.id);
|
||||
if (spritePosition) {
|
||||
dragOffsetX.value = pos.x - spritePosition.canvasX;
|
||||
dragOffsetY.value = pos.y - spritePosition.canvasY;
|
||||
activeSpriteCellIndex.value = spritePosition.index;
|
||||
|
||||
const startCell = findCellAtPosition(pos.x, pos.y);
|
||||
if (startCell) {
|
||||
currentHoverCell.value = startCell;
|
||||
highlightCell.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInCellMovement = (mouseX: number, mouseY: number, spriteIndex: number) => {
|
||||
if (!activeSpriteId.value) return;
|
||||
const sprites = getSprites();
|
||||
const columns = getColumns();
|
||||
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
||||
|
||||
// Use the sprite's current index in the array to calculate cell position
|
||||
const cellCol = spriteIndex % columns;
|
||||
const cellRow = Math.floor(spriteIndex / columns);
|
||||
const cellX = cellCol * maxWidth;
|
||||
const cellY = cellRow * maxHeight;
|
||||
|
||||
const newX = mouseX - cellX - dragOffsetX.value;
|
||||
const newY = mouseY - cellY - dragOffsetY.value;
|
||||
|
||||
const constrainedX = Math.floor(Math.max(0, Math.min(maxWidth - sprites[spriteIndex].width, newX)));
|
||||
const constrainedY = Math.floor(Math.max(0, Math.min(maxHeight - sprites[spriteIndex].height, newY)));
|
||||
|
||||
onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY);
|
||||
onDraw();
|
||||
};
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
if (!isDragging.value || !activeSpriteId.value || activeSpriteCellIndex.value === null) return;
|
||||
|
||||
const pos = getMousePosition(event, getZoom());
|
||||
if (!pos) return;
|
||||
|
||||
const sprites = getSprites();
|
||||
const spriteIndex = sprites.findIndex(s => s.id === activeSpriteId.value);
|
||||
if (spriteIndex === -1) return;
|
||||
|
||||
const hoverCell = findCellAtPosition(pos.x, pos.y);
|
||||
currentHoverCell.value = hoverCell;
|
||||
|
||||
if (getAllowCellSwap() && hoverCell) {
|
||||
if (hoverCell.index !== activeSpriteCellIndex.value) {
|
||||
highlightCell.value = hoverCell;
|
||||
ghostSprite.value = {
|
||||
id: activeSpriteId.value,
|
||||
x: pos.x - dragOffsetX.value,
|
||||
y: pos.y - dragOffsetY.value,
|
||||
};
|
||||
onDraw();
|
||||
} else {
|
||||
highlightCell.value = null;
|
||||
ghostSprite.value = null;
|
||||
handleInCellMovement(pos.x, pos.y, spriteIndex);
|
||||
}
|
||||
} else {
|
||||
handleInCellMovement(pos.x, pos.y, spriteIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
if (
|
||||
isDragging.value &&
|
||||
getAllowCellSwap() &&
|
||||
activeSpriteId.value &&
|
||||
activeSpriteCellIndex.value !== null &&
|
||||
currentHoverCell.value &&
|
||||
activeSpriteCellIndex.value !== currentHoverCell.value.index
|
||||
) {
|
||||
if (onUpdateSpriteCell) {
|
||||
onUpdateSpriteCell(activeSpriteId.value, currentHoverCell.value.index);
|
||||
}
|
||||
onUpdateSprite(activeSpriteId.value, 0, 0);
|
||||
}
|
||||
|
||||
isDragging.value = false;
|
||||
activeSpriteId.value = null;
|
||||
activeSpriteCellIndex.value = null;
|
||||
currentHoverCell.value = null;
|
||||
highlightCell.value = null;
|
||||
ghostSprite.value = null;
|
||||
|
||||
onDraw();
|
||||
};
|
||||
|
||||
// Touch event handlers
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
if (event.touches.length === 1) {
|
||||
const touch = event.touches[0];
|
||||
const mouseEvent = {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
preventDefault: () => {},
|
||||
} as unknown as MouseEvent;
|
||||
startDrag(mouseEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (isDragging.value) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.touches.length === 1) {
|
||||
const touch = event.touches[0];
|
||||
const mouseEvent = {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
preventDefault: () => {},
|
||||
} as unknown as MouseEvent;
|
||||
drag(mouseEvent);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
isDragging,
|
||||
activeSpriteId,
|
||||
ghostSprite,
|
||||
highlightCell,
|
||||
spritePositions,
|
||||
|
||||
// Methods
|
||||
startDrag,
|
||||
drag,
|
||||
stopDrag,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
findSpriteAtPosition,
|
||||
findCellAtPosition,
|
||||
calculateMaxDimensions,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user