[FEAT] Clean code
This commit is contained in:
@@ -13,7 +13,6 @@
|
|||||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
||||||
const items: BreadcrumbItem[] = [{ name: 'Home', path: '/' }];
|
const items: BreadcrumbItem[] = [{ name: 'Home', path: '/' }];
|
||||||
|
|
||||||
// Map route names to breadcrumb labels
|
|
||||||
const routeLabels: Record<string, string> = {
|
const routeLabels: Record<string, string> = {
|
||||||
'blog-overview': 'Blog',
|
'blog-overview': 'Blog',
|
||||||
'blog-detail': 'Blog',
|
'blog-detail': 'Blog',
|
||||||
@@ -27,10 +26,8 @@
|
|||||||
const routeName = route.name.toString();
|
const routeName = route.name.toString();
|
||||||
|
|
||||||
if (routeName === 'blog-detail') {
|
if (routeName === 'blog-detail') {
|
||||||
// For blog detail pages, add Blog first, then the post title
|
|
||||||
items.push({ name: 'Blog', path: '/blog' });
|
items.push({ name: 'Blog', path: '/blog' });
|
||||||
|
|
||||||
// Get the post title from route meta or params if available
|
|
||||||
const postTitle = (route.meta.title as string) || 'Article';
|
const postTitle = (route.meta.title as string) || 'Article';
|
||||||
items.push({ name: postTitle, path: route.path });
|
items.push({ name: postTitle, path: route.path });
|
||||||
} else if (routeLabels[routeName]) {
|
} else if (routeLabels[routeName]) {
|
||||||
|
|||||||
@@ -86,12 +86,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
success.value = 'Thank you! Your feedback was sent.';
|
success.value = 'Thank you! Your feedback was sent.';
|
||||||
// Reset fields
|
|
||||||
name.value = '';
|
name.value = '';
|
||||||
contact.value = '';
|
contact.value = '';
|
||||||
content.value = '';
|
content.value = '';
|
||||||
|
|
||||||
// Optionally close after short delay
|
|
||||||
setTimeout(() => close(), 600);
|
setTimeout(() => close(), 600);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to send feedback:', e);
|
console.error('Failed to send feedback:', e);
|
||||||
|
|||||||
@@ -63,7 +63,6 @@
|
|||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
const files = Array.from(input.files);
|
const files = Array.from(input.files);
|
||||||
emit('uploadSprites', files);
|
emit('uploadSprites', files);
|
||||||
// Reset input value so uploading the same file again will trigger the event
|
|
||||||
if (fileInput.value) fileInput.value.value = '';
|
if (fileInput.value) fileInput.value.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,13 +170,11 @@
|
|||||||
const response = await fetch('/CHANGELOG.md');
|
const response = await fetch('/CHANGELOG.md');
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
// Configure marked options
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true, // GitHub Flavored Markdown
|
gfm: true, // GitHub Flavored Markdown
|
||||||
breaks: true, // Convert line breaks to <br>
|
breaks: true, // Convert line breaks to <br>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert markdown to HTML
|
|
||||||
changelogHtml.value = await marked(text);
|
changelogHtml.value = await marked(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load changelog:', error);
|
console.error('Failed to load changelog:', error);
|
||||||
|
|||||||
@@ -87,7 +87,6 @@
|
|||||||
copied.value = false;
|
copied.value = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for older browsers
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.value = shareUrl.value;
|
input.value = shareUrl.value;
|
||||||
document.body.appendChild(input);
|
document.body.appendChild(input);
|
||||||
@@ -101,14 +100,12 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start sharing when modal opens
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOpen,
|
() => props.isOpen,
|
||||||
isOpen => {
|
isOpen => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
performShare();
|
performShare();
|
||||||
} else {
|
} else {
|
||||||
// Reset state when closing
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
shareUrl.value = '';
|
shareUrl.value = '';
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|||||||
@@ -225,7 +225,6 @@
|
|||||||
(e: 'copySpriteToFrame', spriteId: string, targetLayerId: string, targetFrameIndex: number): void;
|
(e: 'copySpriteToFrame', spriteId: string, targetLayerId: string, targetFrameIndex: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Get settings from store
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const gridContainerRef = ref<HTMLDivElement | null>(null);
|
const gridContainerRef = ref<HTMLDivElement | null>(null);
|
||||||
@@ -242,6 +241,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedSpriteIds = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isDragging,
|
isDragging,
|
||||||
activeSpriteId,
|
activeSpriteId,
|
||||||
@@ -266,6 +267,7 @@
|
|||||||
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||||
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||||
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
||||||
|
selectedSpriteIds,
|
||||||
getMousePosition,
|
getMousePosition,
|
||||||
onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y),
|
onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y),
|
||||||
onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex),
|
onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex),
|
||||||
@@ -287,16 +289,13 @@
|
|||||||
const contextMenuY = ref(0);
|
const contextMenuY = ref(0);
|
||||||
const contextMenuIndex = ref<number | null>(null);
|
const contextMenuIndex = ref<number | null>(null);
|
||||||
const contextMenuSpriteId = ref<string | null>(null);
|
const contextMenuSpriteId = ref<string | null>(null);
|
||||||
const selectedSpriteIds = ref<Set<string>>(new Set());
|
|
||||||
const replacingSpriteId = ref<string | null>(null);
|
const replacingSpriteId = ref<string | null>(null);
|
||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// Copy to frame modal state
|
|
||||||
const showCopyToFrameModal = ref(false);
|
const showCopyToFrameModal = ref(false);
|
||||||
const copyTargetLayerId = ref(props.activeLayerId);
|
const copyTargetLayerId = ref(props.activeLayerId);
|
||||||
const copySpriteId = ref<string | null>(null);
|
const copySpriteId = ref<string | null>(null);
|
||||||
|
|
||||||
// Clear selection when toggling multi-select mode
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isMultiSelectMode,
|
() => props.isMultiSelectMode,
|
||||||
() => {
|
() => {
|
||||||
@@ -304,7 +303,6 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use the new useGridMetrics composable for consistent calculations
|
|
||||||
const { gridMetrics: gridMetricsRef, getCellPosition: getCellPositionHelper } = useGridMetrics({
|
const { gridMetrics: gridMetricsRef, getCellPosition: getCellPositionHelper } = useGridMetrics({
|
||||||
layers: toRef(props, 'layers'),
|
layers: toRef(props, 'layers'),
|
||||||
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||||
@@ -316,13 +314,11 @@
|
|||||||
const gridMetrics = gridMetricsRef;
|
const gridMetrics = gridMetricsRef;
|
||||||
|
|
||||||
const totalCells = computed(() => {
|
const totalCells = computed(() => {
|
||||||
// Use all layers regardless of visibility to keep canvas size stable
|
|
||||||
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
||||||
return Math.max(1, Math.ceil(maxLen / props.columns)) * props.columns;
|
return Math.max(1, Math.ceil(maxLen / props.columns)) * props.columns;
|
||||||
});
|
});
|
||||||
|
|
||||||
const gridDimensions = computed(() => {
|
const gridDimensions = computed(() => {
|
||||||
// Use all layers regardless of visibility to keep canvas size stable
|
|
||||||
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
||||||
const rows = Math.max(1, Math.ceil(maxLen / props.columns));
|
const rows = Math.max(1, Math.ceil(maxLen / props.columns));
|
||||||
return {
|
return {
|
||||||
@@ -335,7 +331,6 @@
|
|||||||
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the new useBackgroundStyles composable for consistent background styling
|
|
||||||
const {
|
const {
|
||||||
backgroundColor: cellBackgroundColor,
|
backgroundColor: cellBackgroundColor,
|
||||||
backgroundImage: cellBackgroundImage,
|
backgroundImage: cellBackgroundImage,
|
||||||
@@ -353,17 +348,14 @@
|
|||||||
const getCellBackgroundPosition = () => cellBackgroundPosition.value;
|
const getCellBackgroundPosition = () => cellBackgroundPosition.value;
|
||||||
|
|
||||||
const startDrag = (event: MouseEvent) => {
|
const startDrag = (event: MouseEvent) => {
|
||||||
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (target && target.closest('button, a, input, select, textarea')) {
|
if (target && target.closest('button, a, input, select, textarea')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!gridContainerRef.value) return;
|
if (!gridContainerRef.value) return;
|
||||||
|
|
||||||
// Hide context menu if open
|
|
||||||
showContextMenu.value = false;
|
showContextMenu.value = false;
|
||||||
|
|
||||||
// Handle right-click for context menu
|
|
||||||
if ('button' in event && (event as MouseEvent).button === 2) {
|
if ('button' in event && (event as MouseEvent).button === 2) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const pos = getMousePosition(event, props.zoom);
|
const pos = getMousePosition(event, props.zoom);
|
||||||
@@ -374,14 +366,11 @@
|
|||||||
contextMenuSpriteId.value = clickedSprite?.id || null;
|
contextMenuSpriteId.value = clickedSprite?.id || null;
|
||||||
|
|
||||||
if (clickedSprite) {
|
if (clickedSprite) {
|
||||||
// If the right-clicked sprite is not in the selection, clear selection and select just this one
|
|
||||||
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
|
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
|
||||||
selectedSpriteIds.value.clear();
|
selectedSpriteIds.value.clear();
|
||||||
selectedSpriteIds.value.add(clickedSprite.id);
|
selectedSpriteIds.value.add(clickedSprite.id);
|
||||||
}
|
}
|
||||||
// If it IS in the selection, keep the current selection (so we can apply action to all)
|
|
||||||
} else {
|
} else {
|
||||||
// Right click on empty space
|
|
||||||
selectedSpriteIds.value.clear();
|
selectedSpriteIds.value.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,37 +381,27 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore non-left mouse buttons
|
|
||||||
if ('button' in event && (event as MouseEvent).button !== 0) return;
|
if ('button' in event && (event as MouseEvent).button !== 0) return;
|
||||||
|
|
||||||
// Handle selection logic for left click
|
|
||||||
const pos = getMousePosition(event, props.zoom);
|
const pos = getMousePosition(event, props.zoom);
|
||||||
if (pos) {
|
if (pos) {
|
||||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||||
if (clickedSprite) {
|
if (clickedSprite) {
|
||||||
// Selection logic with multi-select mode check
|
|
||||||
if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) {
|
if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) {
|
||||||
// Toggle selection
|
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
|
||||||
if (selectedSpriteIds.value.has(clickedSprite.id)) {
|
|
||||||
selectedSpriteIds.value.delete(clickedSprite.id);
|
|
||||||
} else {
|
|
||||||
selectedSpriteIds.value.add(clickedSprite.id);
|
selectedSpriteIds.value.add(clickedSprite.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single select (but don't clear if dragging starts immediately?
|
|
||||||
// Usually standard behavior is to clear others unless shift/ctrl held)
|
|
||||||
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
|
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
|
||||||
selectedSpriteIds.value.clear();
|
selectedSpriteIds.value.clear();
|
||||||
selectedSpriteIds.value.add(clickedSprite.id);
|
selectedSpriteIds.value.add(clickedSprite.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clicked on empty space
|
|
||||||
selectedSpriteIds.value.clear();
|
selectedSpriteIds.value.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to composable for actual drag handling
|
|
||||||
dragStart(event);
|
dragStart(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -430,7 +409,6 @@
|
|||||||
const latestEvent = ref<MouseEvent | null>(null);
|
const latestEvent = ref<MouseEvent | null>(null);
|
||||||
|
|
||||||
const drag = (event: MouseEvent) => {
|
const drag = (event: MouseEvent) => {
|
||||||
// Store the latest event and schedule a single animation frame update
|
|
||||||
latestEvent.value = event;
|
latestEvent.value = event;
|
||||||
if (!pendingDrag.value) {
|
if (!pendingDrag.value) {
|
||||||
pendingDrag.value = true;
|
pendingDrag.value = true;
|
||||||
@@ -481,7 +459,6 @@
|
|||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
// Don't delete if editing text/input
|
|
||||||
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return;
|
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
if (selectedSpriteIds.value.size > 0) {
|
if (selectedSpriteIds.value.size > 0) {
|
||||||
@@ -571,8 +548,6 @@
|
|||||||
document.removeEventListener('mouseup', stopDrag);
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for background color changes
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -303,7 +303,6 @@
|
|||||||
|
|
||||||
const previewContainerRef = ref<HTMLDivElement | null>(null);
|
const previewContainerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// Get settings from store
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -331,14 +330,12 @@
|
|||||||
onDraw: () => {}, // No longer needed for canvas drawing
|
onDraw: () => {}, // No longer needed for canvas drawing
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preview state
|
|
||||||
const isDraggable = ref(false);
|
const isDraggable = ref(false);
|
||||||
const repositionAllLayers = ref(false);
|
const repositionAllLayers = ref(false);
|
||||||
const arrowKeyMovement = ref(false);
|
const arrowKeyMovement = ref(false);
|
||||||
const showAllSprites = ref(false);
|
const showAllSprites = ref(false);
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false);
|
||||||
|
|
||||||
// Context menu state
|
|
||||||
const showContextMenu = ref(false);
|
const showContextMenu = ref(false);
|
||||||
const contextMenuX = ref(0);
|
const contextMenuX = ref(0);
|
||||||
const contextMenuY = ref(0);
|
const contextMenuY = ref(0);
|
||||||
@@ -347,11 +344,9 @@
|
|||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
const replacingSpriteId = ref<string | null>(null);
|
const replacingSpriteId = ref<string | null>(null);
|
||||||
|
|
||||||
// Copy to frame modal state
|
|
||||||
const showCopyToFrameModal = ref(false);
|
const showCopyToFrameModal = ref(false);
|
||||||
const copyTargetLayerId = ref(props.activeLayerId);
|
const copyTargetLayerId = ref(props.activeLayerId);
|
||||||
|
|
||||||
// Drag and drop for new sprites
|
|
||||||
const onDragOver = () => {
|
const onDragOver = () => {
|
||||||
isDragOver.value = true;
|
isDragOver.value = true;
|
||||||
};
|
};
|
||||||
@@ -373,10 +368,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const compositeFrames = computed<Sprite[]>(() => {
|
const compositeFrames = computed<Sprite[]>(() => {
|
||||||
// Show frames from the active layer for the thumbnail list
|
|
||||||
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
|
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
|
||||||
if (!activeLayer) {
|
if (!activeLayer) {
|
||||||
// Fallback to first visible layer if no active layer
|
|
||||||
const v = getVisibleLayers();
|
const v = getVisibleLayers();
|
||||||
const len = maxFrames();
|
const len = maxFrames();
|
||||||
const arr: Sprite[] = [];
|
const arr: Sprite[] = [];
|
||||||
@@ -395,7 +388,6 @@
|
|||||||
return layer.sprites[currentFrameIndex.value] || null;
|
return layer.sprites[currentFrameIndex.value] || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the new useGridMetrics composable for consistent calculations
|
|
||||||
const { gridMetrics, getCellPosition: getCellPositionHelper } = useGridMetrics({
|
const { gridMetrics, getCellPosition: getCellPositionHelper } = useGridMetrics({
|
||||||
layers: toRef(props, 'layers'),
|
layers: toRef(props, 'layers'),
|
||||||
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||||
@@ -404,19 +396,16 @@
|
|||||||
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to get cell position (same as SpriteCanvas)
|
|
||||||
const getCellPosition = (index: number) => {
|
const getCellPosition = (index: number) => {
|
||||||
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed cell dimensions (for backward compatibility with existing code)
|
|
||||||
const cellDimensions = computed(() => ({
|
const cellDimensions = computed(() => ({
|
||||||
cellWidth: gridMetrics.value.maxWidth,
|
cellWidth: gridMetrics.value.maxWidth,
|
||||||
cellHeight: gridMetrics.value.maxHeight,
|
cellHeight: gridMetrics.value.maxHeight,
|
||||||
negativeSpacing: gridMetrics.value.negativeSpacing,
|
negativeSpacing: gridMetrics.value.negativeSpacing,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use the new useBackgroundStyles composable for consistent background styling
|
|
||||||
const {
|
const {
|
||||||
backgroundImage: previewBackgroundImage,
|
backgroundImage: previewBackgroundImage,
|
||||||
backgroundSize: previewBackgroundSize,
|
backgroundSize: previewBackgroundSize,
|
||||||
@@ -429,7 +418,6 @@
|
|||||||
|
|
||||||
const getPreviewBackgroundImage = () => previewBackgroundImage.value;
|
const getPreviewBackgroundImage = () => previewBackgroundImage.value;
|
||||||
|
|
||||||
// Dragging state
|
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const activeSpriteId = ref<string | null>(null);
|
const activeSpriteId = ref<string | null>(null);
|
||||||
const activeLayerId = ref<string | null>(null);
|
const activeLayerId = ref<string | null>(null);
|
||||||
@@ -438,7 +426,6 @@
|
|||||||
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
|
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
|
||||||
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
|
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
|
||||||
|
|
||||||
// Drag functionality
|
|
||||||
const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => {
|
const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => {
|
||||||
if (!isDraggable.value || !previewContainerRef.value) return;
|
if (!isDraggable.value || !previewContainerRef.value) return;
|
||||||
|
|
||||||
@@ -455,7 +442,6 @@
|
|||||||
dragStartX.value = mouseX;
|
dragStartX.value = mouseX;
|
||||||
dragStartY.value = mouseY;
|
dragStartY.value = mouseY;
|
||||||
|
|
||||||
// Store initial positions for all sprites in this frame from all visible layers
|
|
||||||
allSpritesPosBeforeDrag.value.clear();
|
allSpritesPosBeforeDrag.value.clear();
|
||||||
const visibleLayers = getVisibleLayers();
|
const visibleLayers = getVisibleLayers();
|
||||||
visibleLayers.forEach(layer => {
|
visibleLayers.forEach(layer => {
|
||||||
@@ -490,7 +476,6 @@
|
|||||||
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
|
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
|
||||||
|
|
||||||
if (activeSpriteId.value === 'ALL_LAYERS') {
|
if (activeSpriteId.value === 'ALL_LAYERS') {
|
||||||
// Move all sprites in current frame from all visible layers
|
|
||||||
const visibleLayers = getVisibleLayers();
|
const visibleLayers = getVisibleLayers();
|
||||||
visibleLayers.forEach(layer => {
|
visibleLayers.forEach(layer => {
|
||||||
const sprite = layer.sprites[currentFrameIndex.value];
|
const sprite = layer.sprites[currentFrameIndex.value];
|
||||||
@@ -499,26 +484,21 @@
|
|||||||
const originalPos = allSpritesPosBeforeDrag.value.get(sprite.id);
|
const originalPos = allSpritesPosBeforeDrag.value.get(sprite.id);
|
||||||
if (!originalPos) return;
|
if (!originalPos) return;
|
||||||
|
|
||||||
// Calculate new position with constraints
|
|
||||||
let newX = Math.round(originalPos.x + deltaX);
|
let newX = Math.round(originalPos.x + deltaX);
|
||||||
let newY = Math.round(originalPos.y + deltaY);
|
let newY = Math.round(originalPos.y + deltaY);
|
||||||
|
|
||||||
// Constrain movement within expanded cell
|
|
||||||
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
||||||
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
||||||
|
|
||||||
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Move only the active layer sprite
|
|
||||||
const activeSprite = props.layers.find(l => l.id === activeLayerId.value)?.sprites[currentFrameIndex.value];
|
const activeSprite = props.layers.find(l => l.id === activeLayerId.value)?.sprites[currentFrameIndex.value];
|
||||||
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
|
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
|
||||||
|
|
||||||
// Calculate new position with constraints and round to integers
|
|
||||||
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
|
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
|
||||||
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
|
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
|
||||||
|
|
||||||
// Constrain movement within expanded cell (allow negative values up to -negativeSpacing)
|
|
||||||
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
|
||||||
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
|
||||||
|
|
||||||
@@ -562,7 +542,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Arrow key movement handler
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!isDraggable.value || !arrowKeyMovement.value) return;
|
if (!isDraggable.value || !arrowKeyMovement.value) return;
|
||||||
|
|
||||||
@@ -593,7 +572,6 @@
|
|||||||
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
|
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
|
||||||
|
|
||||||
if (repositionAllLayers.value) {
|
if (repositionAllLayers.value) {
|
||||||
// Move all sprites in current frame from all visible layers
|
|
||||||
const visibleLayers = getVisibleLayers();
|
const visibleLayers = getVisibleLayers();
|
||||||
visibleLayers.forEach(layer => {
|
visibleLayers.forEach(layer => {
|
||||||
const sprite = layer.sprites[currentFrameIndex.value];
|
const sprite = layer.sprites[currentFrameIndex.value];
|
||||||
@@ -602,14 +580,12 @@
|
|||||||
let newX = Math.round(sprite.x + deltaX);
|
let newX = Math.round(sprite.x + deltaX);
|
||||||
let newY = Math.round(sprite.y + deltaY);
|
let newY = Math.round(sprite.y + deltaY);
|
||||||
|
|
||||||
// Constrain movement within expanded cell
|
|
||||||
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
||||||
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
||||||
|
|
||||||
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Move only the active layer sprite
|
|
||||||
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
|
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
|
||||||
if (!activeLayer) return;
|
if (!activeLayer) return;
|
||||||
|
|
||||||
@@ -619,7 +595,6 @@
|
|||||||
let newX = Math.round(sprite.x + deltaX);
|
let newX = Math.round(sprite.x + deltaX);
|
||||||
let newY = Math.round(sprite.y + deltaY);
|
let newY = Math.round(sprite.y + deltaY);
|
||||||
|
|
||||||
// Constrain movement within expanded cell
|
|
||||||
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
||||||
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
||||||
|
|
||||||
@@ -627,7 +602,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
@@ -637,8 +611,6 @@
|
|||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watchers - most canvas-related watchers removed
|
|
||||||
// Keep layer watchers to ensure reactivity
|
|
||||||
watch(
|
watch(
|
||||||
() => props.layers,
|
() => props.layers,
|
||||||
() => {},
|
() => {},
|
||||||
@@ -650,7 +622,6 @@
|
|||||||
);
|
);
|
||||||
watch(currentFrameIndex, () => {});
|
watch(currentFrameIndex, () => {});
|
||||||
|
|
||||||
// Context menu functions
|
|
||||||
const openContextMenu = (event: MouseEvent, sprite: Sprite, layerId: string) => {
|
const openContextMenu = (event: MouseEvent, sprite: Sprite, layerId: string) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
contextMenuSpriteId.value = sprite.id;
|
contextMenuSpriteId.value = sprite.id;
|
||||||
|
|||||||
@@ -115,7 +115,6 @@
|
|||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const splitter = useSpritesheetSplitter();
|
const splitter = useSpritesheetSplitter();
|
||||||
|
|
||||||
// State
|
|
||||||
const detectionMode = ref<DetectionMode>('grid');
|
const detectionMode = ref<DetectionMode>('grid');
|
||||||
const cellWidth = ref(64);
|
const cellWidth = ref(64);
|
||||||
const cellHeight = ref(64);
|
const cellHeight = ref(64);
|
||||||
@@ -126,12 +125,10 @@
|
|||||||
const isProcessing = ref(false);
|
const isProcessing = ref(false);
|
||||||
const imageElement = ref<HTMLImageElement | null>(null);
|
const imageElement = ref<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
// Computed
|
|
||||||
const gridCols = computed(() => (imageElement.value && cellWidth.value > 0 ? Math.floor(imageElement.value.width / cellWidth.value) : 0));
|
const gridCols = computed(() => (imageElement.value && cellWidth.value > 0 ? Math.floor(imageElement.value.width / cellWidth.value) : 0));
|
||||||
|
|
||||||
const gridRows = computed(() => (imageElement.value && cellHeight.value > 0 ? Math.floor(imageElement.value.height / cellHeight.value) : 0));
|
const gridRows = computed(() => (imageElement.value && cellHeight.value > 0 ? Math.floor(imageElement.value.height / cellHeight.value) : 0));
|
||||||
|
|
||||||
// Load image and set initial cell size
|
|
||||||
watch(
|
watch(
|
||||||
() => props.imageUrl,
|
() => props.imageUrl,
|
||||||
url => {
|
url => {
|
||||||
@@ -141,7 +138,6 @@
|
|||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
imageElement.value = img;
|
imageElement.value = img;
|
||||||
|
|
||||||
// Set suggested cell size
|
|
||||||
const suggested = splitter.getSuggestedCellSize(img.width, img.height);
|
const suggested = splitter.getSuggestedCellSize(img.width, img.height);
|
||||||
cellWidth.value = suggested.width;
|
cellWidth.value = suggested.width;
|
||||||
cellHeight.value = suggested.height;
|
cellHeight.value = suggested.height;
|
||||||
@@ -153,14 +149,12 @@
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Regenerate preview when options change
|
|
||||||
watch([detectionMode, cellWidth, cellHeight, sensitivity, removeEmpty, preserveCellSize], () => {
|
watch([detectionMode, cellWidth, cellHeight, sensitivity, removeEmpty, preserveCellSize], () => {
|
||||||
if (imageElement.value) {
|
if (imageElement.value) {
|
||||||
generatePreview();
|
generatePreview();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate preview
|
|
||||||
async function generatePreview() {
|
async function generatePreview() {
|
||||||
if (!imageElement.value) return;
|
if (!imageElement.value) return;
|
||||||
|
|
||||||
@@ -190,7 +184,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,6 @@
|
|||||||
close();
|
close();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message || 'An error occurred';
|
error.value = e.message || 'An error occurred';
|
||||||
// Better PB error handling
|
|
||||||
if (e?.data?.message) error.value = e.data.message;
|
if (e?.data?.message) error.value = e.data.message;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|||||||
@@ -74,7 +74,6 @@
|
|||||||
import { useProjectManager } from '@/composables/useProjectManager';
|
import { useProjectManager } from '@/composables/useProjectManager';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
// Sub-components
|
|
||||||
import NavbarLogo from './navbar/NavbarLogo.vue';
|
import NavbarLogo from './navbar/NavbarLogo.vue';
|
||||||
import NavbarLinks from './navbar/NavbarLinks.vue';
|
import NavbarLinks from './navbar/NavbarLinks.vue';
|
||||||
import NavbarProjectActions from './navbar/NavbarProjectActions.vue';
|
import NavbarProjectActions from './navbar/NavbarProjectActions.vue';
|
||||||
@@ -144,7 +143,6 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
addToast('Failed to save project', 'error');
|
addToast('Failed to save project', 'error');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// Error handled in composable but kept here for toast
|
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
:key="link.path"
|
:key="link.path"
|
||||||
:to="link.path"
|
:to="link.path"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200"
|
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200"
|
||||||
:class="[
|
:class="[isActive(link.path) ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400']"
|
||||||
isActive(link.path)
|
|
||||||
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -39,11 +39,7 @@
|
|||||||
:key="link.path"
|
:key="link.path"
|
||||||
:to="link.path"
|
:to="link.path"
|
||||||
class="flex items-center gap-3 px-3 py-3 rounded-md text-base font-medium transition-all duration-200"
|
class="flex items-center gap-3 px-3 py-3 rounded-md text-base font-medium transition-all duration-200"
|
||||||
:class="[
|
:class="[isActive(link.path) ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400' : 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400']"
|
||||||
isActive(link.path)
|
|
||||||
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400'
|
|
||||||
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400'
|
|
||||||
]"
|
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i :class="link.icon" class="w-5 text-center"></i> {{ link.name }}
|
<i :class="link.icon" class="w-5 text-center"></i> {{ link.name }}
|
||||||
@@ -163,6 +159,5 @@
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
// emit('close'); // Optional: close menu on logout? Maybe better to keep open so they can login again if they want.
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
|
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset to defaults when opened
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOpen,
|
() => props.isOpen,
|
||||||
val => {
|
val => {
|
||||||
|
|||||||
@@ -187,9 +187,7 @@
|
|||||||
const sprites: any[] = [];
|
const sprites: any[] = [];
|
||||||
if (!project.data || !project.data.layers) return sprites;
|
if (!project.data || !project.data.layers) return sprites;
|
||||||
|
|
||||||
// Iterate through layers to find sprites
|
|
||||||
for (const layer of project.data.layers as any[]) {
|
for (const layer of project.data.layers as any[]) {
|
||||||
// Check if layer is visible (default to true if undefined)
|
|
||||||
if (layer.visible === false) continue;
|
if (layer.visible === false) continue;
|
||||||
|
|
||||||
if (layer.sprites && layer.sprites.length > 0) {
|
if (layer.sprites && layer.sprites.length > 0) {
|
||||||
|
|||||||
@@ -76,59 +76,47 @@
|
|||||||
const startPos = ref({ x: 0, y: 0 });
|
const startPos = ref({ x: 0, y: 0 });
|
||||||
const startSize = ref({ width: 0, height: 0 });
|
const startSize = ref({ width: 0, height: 0 });
|
||||||
|
|
||||||
// Add isFullScreen ref
|
|
||||||
const isFullScreen = ref(false);
|
const isFullScreen = ref(false);
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
|
|
||||||
// Add previous state storage for restoring from full screen
|
|
||||||
const previousState = ref({
|
const previousState = ref({
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
size: { width: 0, height: 0 },
|
size: { width: 0, height: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if device is mobile
|
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
isMobile.value = window.innerWidth < 640; // sm breakpoint in Tailwind
|
isMobile.value = window.innerWidth < 640; // sm breakpoint in Tailwind
|
||||||
|
|
||||||
// Auto fullscreen on mobile
|
|
||||||
if (isMobile.value && !isFullScreen.value) {
|
if (isMobile.value && !isFullScreen.value) {
|
||||||
toggleFullScreen();
|
toggleFullScreen();
|
||||||
} else if (!isMobile.value && isFullScreen.value && autoFullScreened.value) {
|
} else if (!isMobile.value && isFullScreen.value && autoFullScreened.value) {
|
||||||
// If we're no longer on mobile and were auto-fullscreened, exit fullscreen
|
|
||||||
toggleFullScreen();
|
toggleFullScreen();
|
||||||
autoFullScreened.value = false;
|
autoFullScreened.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track if fullscreen was automatic (for mobile)
|
|
||||||
const autoFullScreened = ref(false);
|
const autoFullScreened = ref(false);
|
||||||
|
|
||||||
// Add toggleFullScreen function
|
|
||||||
const toggleFullScreen = () => {
|
const toggleFullScreen = () => {
|
||||||
if (!isFullScreen.value) {
|
if (!isFullScreen.value) {
|
||||||
// Store current state before going full screen
|
|
||||||
previousState.value = {
|
previousState.value = {
|
||||||
position: { ...position.value },
|
position: { ...position.value },
|
||||||
size: { ...size.value },
|
size: { ...size.value },
|
||||||
};
|
};
|
||||||
|
|
||||||
// If toggling to fullscreen on mobile automatically, track it
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
autoFullScreened.value = true;
|
autoFullScreened.value = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Restore previous state
|
|
||||||
position.value = { ...previousState.value.position };
|
position.value = { ...previousState.value.position };
|
||||||
size.value = { ...previousState.value.size };
|
size.value = { ...previousState.value.size };
|
||||||
}
|
}
|
||||||
isFullScreen.value = !isFullScreen.value;
|
isFullScreen.value = !isFullScreen.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Unified start function for both drag and resize
|
|
||||||
const startAction = (event: MouseEvent | TouchEvent, action: 'drag' | 'resize') => {
|
const startAction = (event: MouseEvent | TouchEvent, action: 'drag' | 'resize') => {
|
||||||
if (isFullScreen.value) return;
|
if (isFullScreen.value) return;
|
||||||
|
|
||||||
// Extract the correct coordinates based on event type
|
|
||||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||||
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||||
|
|
||||||
@@ -211,7 +199,6 @@
|
|||||||
position.value = { x: 0, y: 0 };
|
position.value = { x: 0, y: 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape' && props.isOpen) close();
|
if (event.key === 'Escape' && props.isOpen) close();
|
||||||
};
|
};
|
||||||
@@ -223,7 +210,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add these new touch handling functions
|
|
||||||
const handleTouchStart = (event: TouchEvent) => {
|
const handleTouchStart = (event: TouchEvent) => {
|
||||||
if (isFullScreen.value) return;
|
if (isFullScreen.value) return;
|
||||||
if (event.touches.length === 1) {
|
if (event.touches.length === 1) {
|
||||||
@@ -237,7 +223,6 @@
|
|||||||
handleMove(event);
|
handleMove(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOpen,
|
() => props.isOpen,
|
||||||
newValue => {
|
newValue => {
|
||||||
@@ -250,7 +235,6 @@
|
|||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
window.addEventListener('resize', checkMobile);
|
window.addEventListener('resize', checkMobile);
|
||||||
|
|
||||||
// Initial check for mobile
|
|
||||||
checkMobile();
|
checkMobile();
|
||||||
|
|
||||||
if (props.isOpen) centerModal();
|
if (props.isOpen) centerModal();
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
import type { Toast } from '@/composables/useToast';
|
import type { Toast } from '@/composables/useToast';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
// Simple functional components for icons using h (avoid runtime compiler)
|
|
||||||
const SuccessIcon = {
|
const SuccessIcon = {
|
||||||
render: () =>
|
render: () =>
|
||||||
h(
|
h(
|
||||||
|
|||||||
@@ -42,30 +42,24 @@
|
|||||||
let x = mouseX.value + offsetX;
|
let x = mouseX.value + offsetX;
|
||||||
let y = mouseY.value + offsetY;
|
let y = mouseY.value + offsetY;
|
||||||
|
|
||||||
// Get tooltip dimensions (estimate if not mounted yet)
|
|
||||||
const tooltipWidth = tooltipRef.value?.offsetWidth || 200;
|
const tooltipWidth = tooltipRef.value?.offsetWidth || 200;
|
||||||
const tooltipHeight = tooltipRef.value?.offsetHeight || 30;
|
const tooltipHeight = tooltipRef.value?.offsetHeight || 30;
|
||||||
|
|
||||||
// Screen boundaries
|
|
||||||
const screenWidth = window.innerWidth;
|
const screenWidth = window.innerWidth;
|
||||||
const screenHeight = window.innerHeight;
|
const screenHeight = window.innerHeight;
|
||||||
|
|
||||||
// Adjust horizontal position if too close to right edge
|
|
||||||
if (x + tooltipWidth + padding > screenWidth) {
|
if (x + tooltipWidth + padding > screenWidth) {
|
||||||
x = mouseX.value - tooltipWidth - offsetX;
|
x = mouseX.value - tooltipWidth - offsetX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust horizontal position if too close to left edge
|
|
||||||
if (x < padding) {
|
if (x < padding) {
|
||||||
x = padding;
|
x = padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust vertical position if too close to bottom edge
|
|
||||||
if (y + tooltipHeight + padding > screenHeight) {
|
if (y + tooltipHeight + padding > screenHeight) {
|
||||||
y = mouseY.value - tooltipHeight - offsetY;
|
y = mouseY.value - tooltipHeight - offsetY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust vertical position if too close to top edge
|
|
||||||
if (y < padding) {
|
if (y < padding) {
|
||||||
y = padding;
|
y = padding;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface AnimationFramesOptions {
|
|||||||
export function useAnimationFrames(options: AnimationFramesOptions) {
|
export function useAnimationFrames(options: AnimationFramesOptions) {
|
||||||
const { onDraw } = options;
|
const { onDraw } = options;
|
||||||
|
|
||||||
// Convert sprites to a computed ref for reactivity
|
|
||||||
const spritesRef = computed(() => {
|
const spritesRef = computed(() => {
|
||||||
if (typeof options.sprites === 'function') {
|
if (typeof options.sprites === 'function') {
|
||||||
return options.sprites();
|
return options.sprites();
|
||||||
@@ -20,20 +19,16 @@ export function useAnimationFrames(options: AnimationFramesOptions) {
|
|||||||
return options.sprites;
|
return options.sprites;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to get sprites array
|
|
||||||
const getSprites = () => spritesRef.value;
|
const getSprites = () => spritesRef.value;
|
||||||
|
|
||||||
// State
|
|
||||||
const currentFrameIndex = ref(0);
|
const currentFrameIndex = ref(0);
|
||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const fps = ref(12);
|
const fps = ref(12);
|
||||||
const hiddenFrames = ref<number[]>([]);
|
const hiddenFrames = ref<number[]>([]);
|
||||||
|
|
||||||
// Animation internals
|
|
||||||
const animationFrameId = ref<number | null>(null);
|
const animationFrameId = ref<number | null>(null);
|
||||||
const lastFrameTime = ref(0);
|
const lastFrameTime = ref(0);
|
||||||
|
|
||||||
// Computed properties for visible frames
|
|
||||||
const visibleFrames = computed(() => getSprites().filter((_, index) => !hiddenFrames.value.includes(index)));
|
const visibleFrames = computed(() => getSprites().filter((_, index) => !hiddenFrames.value.includes(index)));
|
||||||
|
|
||||||
const visibleFramesCount = computed(() => visibleFrames.value.length);
|
const visibleFramesCount = computed(() => visibleFrames.value.length);
|
||||||
@@ -46,7 +41,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) {
|
|||||||
|
|
||||||
const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1);
|
const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1);
|
||||||
|
|
||||||
// Animation control
|
|
||||||
const animateFrame = () => {
|
const animateFrame = () => {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const elapsed = now - lastFrameTime.value;
|
const elapsed = now - lastFrameTime.value;
|
||||||
@@ -109,7 +103,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) {
|
|||||||
currentFrameIndex.value = sprites.indexOf(visibleFrames.value[index]);
|
currentFrameIndex.value = sprites.indexOf(visibleFrames.value[index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Frame visibility management
|
|
||||||
const toggleHiddenFrame = (index: number) => {
|
const toggleHiddenFrame = (index: number) => {
|
||||||
const sprites = getSprites();
|
const sprites = getSprites();
|
||||||
const currentIndex = hiddenFrames.value.indexOf(index);
|
const currentIndex = hiddenFrames.value.indexOf(index);
|
||||||
@@ -117,7 +110,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) {
|
|||||||
if (currentIndex === -1) {
|
if (currentIndex === -1) {
|
||||||
hiddenFrames.value.push(index);
|
hiddenFrames.value.push(index);
|
||||||
|
|
||||||
// If hiding current frame, switch to next visible
|
|
||||||
if (index === currentFrameIndex.value) {
|
if (index === currentFrameIndex.value) {
|
||||||
const nextVisible = sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i));
|
const nextVisible = sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i));
|
||||||
if (nextVisible !== -1) {
|
if (nextVisible !== -1) {
|
||||||
@@ -140,32 +132,27 @@ export function useAnimationFrames(options: AnimationFramesOptions) {
|
|||||||
const sprites = getSprites();
|
const sprites = getSprites();
|
||||||
hiddenFrames.value = sprites.map((_, index) => index);
|
hiddenFrames.value = sprites.map((_, index) => index);
|
||||||
|
|
||||||
// Keep at least one frame visible
|
|
||||||
if (hiddenFrames.value.length > 0) {
|
if (hiddenFrames.value.length > 0) {
|
||||||
hiddenFrames.value.splice(currentFrameIndex.value, 1);
|
hiddenFrames.value.splice(currentFrameIndex.value, 1);
|
||||||
}
|
}
|
||||||
onDraw();
|
onDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopAnimation();
|
stopAnimation();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
|
||||||
currentFrameIndex,
|
currentFrameIndex,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
fps,
|
fps,
|
||||||
hiddenFrames,
|
hiddenFrames,
|
||||||
|
|
||||||
// Computed
|
|
||||||
visibleFrames,
|
visibleFrames,
|
||||||
visibleFramesCount,
|
visibleFramesCount,
|
||||||
visibleFrameIndex,
|
visibleFrameIndex,
|
||||||
visibleFrameNumber,
|
visibleFrameNumber,
|
||||||
|
|
||||||
// Methods
|
|
||||||
togglePlayback,
|
togglePlayback,
|
||||||
nextFrame,
|
nextFrame,
|
||||||
previousFrame,
|
previousFrame,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface BackgroundStyles {
|
|||||||
* Handles transparent backgrounds with checkerboard patterns and dark mode.
|
* Handles transparent backgrounds with checkerboard patterns and dark mode.
|
||||||
*/
|
*/
|
||||||
export function useBackgroundStyles(options: BackgroundStylesOptions) {
|
export function useBackgroundStyles(options: BackgroundStylesOptions) {
|
||||||
// Helper to get reactive values
|
|
||||||
const getBackgroundColor = () => (typeof options.backgroundColor === 'string' ? options.backgroundColor : options.backgroundColor.value);
|
const getBackgroundColor = () => (typeof options.backgroundColor === 'string' ? options.backgroundColor : options.backgroundColor.value);
|
||||||
const getCheckerboardEnabled = () => (typeof options.checkerboardEnabled === 'boolean' ? options.checkerboardEnabled : (options.checkerboardEnabled?.value ?? true));
|
const getCheckerboardEnabled = () => (typeof options.checkerboardEnabled === 'boolean' ? options.checkerboardEnabled : (options.checkerboardEnabled?.value ?? true));
|
||||||
const getDarkMode = () => (typeof options.darkMode === 'boolean' ? options.darkMode : (options.darkMode?.value ?? false));
|
const getDarkMode = () => (typeof options.darkMode === 'boolean' ? options.darkMode : (options.darkMode?.value ?? false));
|
||||||
@@ -40,7 +39,6 @@ export function useBackgroundStyles(options: BackgroundStylesOptions) {
|
|||||||
const darkMode = getDarkMode();
|
const darkMode = getDarkMode();
|
||||||
|
|
||||||
if (bg === 'transparent' && checkerboardEnabled) {
|
if (bg === 'transparent' && checkerboardEnabled) {
|
||||||
// Checkerboard pattern for transparent backgrounds (dark mode friendly)
|
|
||||||
const color = darkMode ? '#4b5563' : '#d1d5db';
|
const color = darkMode ? '#4b5563' : '#d1d5db';
|
||||||
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to ensure integer positions for pixel-perfect rendering
|
|
||||||
const ensureIntegerPositions = <T extends { x: number; y: number }>(items: T[]) => {
|
const ensureIntegerPositions = <T extends { x: number; y: number }>(items: T[]) => {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
item.x = Math.floor(item.x);
|
item.x = Math.floor(item.x);
|
||||||
@@ -79,7 +78,6 @@ export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?:
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Centralized force redraw handler
|
|
||||||
const createForceRedrawHandler = <T extends { x: number; y: number }>(items: T[], drawCallback: () => void) => {
|
const createForceRedrawHandler = <T extends { x: number; y: number }>(items: T[], drawCallback: () => void) => {
|
||||||
return () => {
|
return () => {
|
||||||
ensureIntegerPositions(items);
|
ensureIntegerPositions(items);
|
||||||
@@ -88,7 +86,6 @@ export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?:
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get mouse position relative to canvas, accounting for zoom
|
|
||||||
const getMousePosition = (event: MouseEvent, zoom = 1): { x: number; y: number } | null => {
|
const getMousePosition = (event: MouseEvent, zoom = 1): { x: number; y: number } | null => {
|
||||||
if (!canvasRef.value) return null;
|
if (!canvasRef.value) return null;
|
||||||
|
|
||||||
@@ -102,7 +99,6 @@ export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?:
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to attach load/error listeners to images that aren't yet loaded
|
|
||||||
const attachImageListeners = (sprites: Sprite[], onLoad: () => void, tracked: WeakSet<HTMLImageElement>) => {
|
const attachImageListeners = (sprites: Sprite[], onLoad: () => void, tracked: WeakSet<HTMLImageElement>) => {
|
||||||
sprites.forEach(sprite => {
|
sprites.forEach(sprite => {
|
||||||
const img = sprite.img as HTMLImageElement | undefined;
|
const img = sprite.img as HTMLImageElement | undefined;
|
||||||
@@ -116,14 +112,12 @@ export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?:
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fill cell background with selected color or transparent
|
|
||||||
const fillCellBackground = (x: number, y: number, width: number, height: number) => {
|
const fillCellBackground = (x: number, y: number, width: number, height: number) => {
|
||||||
if (settingsStore.backgroundColor === 'transparent') return;
|
if (settingsStore.backgroundColor === 'transparent') return;
|
||||||
const color = settingsStore.backgroundColor === 'custom' ? settingsStore.backgroundColor : settingsStore.backgroundColor;
|
const color = settingsStore.backgroundColor === 'custom' ? settingsStore.backgroundColor : settingsStore.backgroundColor;
|
||||||
fillRect(x, y, width, height, color);
|
fillRect(x, y, width, height, color);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stroke grid with theme-aware color
|
|
||||||
const strokeGridCell = (x: number, y: number, width: number, height: number) => {
|
const strokeGridCell = (x: number, y: number, width: number, height: number) => {
|
||||||
const color = settingsStore.darkMode ? '#4B5563' : '#e5e7eb';
|
const color = settingsStore.darkMode ? '#4B5563' : '#e5e7eb';
|
||||||
strokeRect(x, y, width, height, color, 1);
|
strokeRect(x, y, width, height, color, 1);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface DragSpriteOptions {
|
|||||||
manualCellSizeEnabled?: Ref<boolean>;
|
manualCellSizeEnabled?: Ref<boolean>;
|
||||||
manualCellWidth?: Ref<number>;
|
manualCellWidth?: Ref<number>;
|
||||||
manualCellHeight?: Ref<number>;
|
manualCellHeight?: Ref<number>;
|
||||||
|
selectedSpriteIds?: Ref<Set<string>> | ComputedRef<Set<string>>;
|
||||||
getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null;
|
getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null;
|
||||||
onUpdateSprite: (id: string, x: number, y: number) => void;
|
onUpdateSprite: (id: string, x: number, y: number) => void;
|
||||||
onUpdateSpriteCell?: (id: string, newIndex: number) => void;
|
onUpdateSpriteCell?: (id: string, newIndex: number) => void;
|
||||||
@@ -44,7 +45,6 @@ export interface DragSpriteOptions {
|
|||||||
export function useDragSprite(options: DragSpriteOptions) {
|
export function useDragSprite(options: DragSpriteOptions) {
|
||||||
const { getMousePosition, onUpdateSprite, onUpdateSpriteCell, onDraw } = options;
|
const { getMousePosition, onUpdateSprite, onUpdateSpriteCell, onDraw } = options;
|
||||||
|
|
||||||
// Helper to get reactive values
|
|
||||||
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
||||||
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
||||||
const getColumns = () => (typeof options.columns === 'number' ? options.columns : options.columns.value);
|
const getColumns = () => (typeof options.columns === 'number' ? options.columns : options.columns.value);
|
||||||
@@ -54,8 +54,8 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const getManualCellSizeEnabled = () => options.manualCellSizeEnabled?.value ?? false;
|
const getManualCellSizeEnabled = () => options.manualCellSizeEnabled?.value ?? false;
|
||||||
const getManualCellWidth = () => options.manualCellWidth?.value ?? 64;
|
const getManualCellWidth = () => options.manualCellWidth?.value ?? 64;
|
||||||
const getManualCellHeight = () => options.manualCellHeight?.value ?? 64;
|
const getManualCellHeight = () => options.manualCellHeight?.value ?? 64;
|
||||||
|
const getSelectedSpriteIds = () => options.selectedSpriteIds?.value ?? new Set<string>();
|
||||||
|
|
||||||
// Drag state
|
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const activeSpriteId = ref<string | null>(null);
|
const activeSpriteId = ref<string | null>(null);
|
||||||
const activeSpriteCellIndex = ref<number | null>(null);
|
const activeSpriteCellIndex = ref<number | null>(null);
|
||||||
@@ -65,11 +65,11 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const dragOffsetY = ref(0);
|
const dragOffsetY = ref(0);
|
||||||
const currentHoverCell = ref<CellPosition | null>(null);
|
const currentHoverCell = ref<CellPosition | null>(null);
|
||||||
|
|
||||||
// Visual feedback
|
const initialSpritePositions = ref<Map<string, { x: number; y: number; index: number }>>(new Map());
|
||||||
|
|
||||||
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
||||||
const highlightCell = ref<CellPosition | null>(null);
|
const highlightCell = ref<CellPosition | null>(null);
|
||||||
|
|
||||||
// Use the new useGridMetrics composable for consistent calculations
|
|
||||||
const gridMetricsComposable = useGridMetrics({
|
const gridMetricsComposable = useGridMetrics({
|
||||||
layers: options.layers,
|
layers: options.layers,
|
||||||
sprites: options.sprites,
|
sprites: options.sprites,
|
||||||
@@ -83,7 +83,6 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
return gridMetricsComposable.calculateCellDimensions();
|
return gridMetricsComposable.calculateCellDimensions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed sprite positions
|
|
||||||
const spritePositions = computed<SpritePosition[]>(() => {
|
const spritePositions = computed<SpritePosition[]>(() => {
|
||||||
const sprites = getSprites();
|
const sprites = getSprites();
|
||||||
const columns = getColumns();
|
const columns = getColumns();
|
||||||
@@ -118,7 +117,6 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const col = Math.floor(x / maxWidth);
|
const col = Math.floor(x / maxWidth);
|
||||||
const row = Math.floor(y / maxHeight);
|
const row = Math.floor(y / maxHeight);
|
||||||
|
|
||||||
// Allow dropping anywhere in the columns, assuming infinite rows effectively
|
|
||||||
if (col >= 0 && col < columns && row >= 0) {
|
if (col >= 0 && col < columns && row >= 0) {
|
||||||
const index = row * columns + col;
|
const index = row * columns + col;
|
||||||
return { col, row, index };
|
return { col, row, index };
|
||||||
@@ -162,6 +160,19 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
highlightCell.value = null;
|
highlightCell.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedIds = getSelectedSpriteIds();
|
||||||
|
initialSpritePositions.value.clear();
|
||||||
|
if (selectedIds.has(clickedSprite.id) && selectedIds.size > 1) {
|
||||||
|
const sprites = getSprites();
|
||||||
|
selectedIds.forEach(id => {
|
||||||
|
const sprite = sprites.find(s => s.id === id);
|
||||||
|
const spriteIdx = sprites.findIndex(s => s.id === id);
|
||||||
|
if (sprite) {
|
||||||
|
initialSpritePositions.value.set(id, { x: sprite.x, y: sprite.y, index: spriteIdx });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,23 +182,44 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const columns = getColumns();
|
const columns = getColumns();
|
||||||
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
|
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
|
||||||
|
|
||||||
// Use the sprite's current index in the array to calculate cell position
|
|
||||||
const cellCol = spriteIndex % columns;
|
const cellCol = spriteIndex % columns;
|
||||||
const cellRow = Math.floor(spriteIndex / columns);
|
const cellRow = Math.floor(spriteIndex / columns);
|
||||||
const cellX = Math.round(cellCol * maxWidth);
|
const cellX = Math.round(cellCol * maxWidth);
|
||||||
const cellY = Math.round(cellRow * maxHeight);
|
const cellY = Math.round(cellRow * maxHeight);
|
||||||
|
|
||||||
// Calculate new position relative to cell origin (without the negative spacing offset)
|
|
||||||
// The sprite's x,y is stored relative to where it would be drawn after the negativeSpacing offset
|
|
||||||
const newX = mouseX - cellX - negativeSpacing - dragOffsetX.value;
|
const newX = mouseX - cellX - negativeSpacing - dragOffsetX.value;
|
||||||
const newY = mouseY - cellY - negativeSpacing - dragOffsetY.value;
|
const newY = mouseY - cellY - negativeSpacing - dragOffsetY.value;
|
||||||
|
|
||||||
// The sprite can move within the full expanded cell area
|
const selectedIds = getSelectedSpriteIds();
|
||||||
// Allow negative values up to -negativeSpacing so sprite can fill the expanded area
|
const isMultiDrag = selectedIds.has(activeSpriteId.value) && selectedIds.size > 1;
|
||||||
|
|
||||||
|
if (isMultiDrag && initialSpritePositions.value.size > 0) {
|
||||||
|
const activeInitial = initialSpritePositions.value.get(activeSpriteId.value);
|
||||||
|
if (activeInitial) {
|
||||||
|
const deltaX = newX - activeInitial.x;
|
||||||
|
const deltaY = newY - activeInitial.y;
|
||||||
|
|
||||||
|
initialSpritePositions.value.forEach((initPos, id) => {
|
||||||
|
const sprite = sprites.find(s => s.id === id);
|
||||||
|
if (sprite) {
|
||||||
|
const newSpriteX = initPos.x + deltaX;
|
||||||
|
const newSpriteY = initPos.y + deltaY;
|
||||||
|
|
||||||
|
const spriteCellCol = initPos.index % columns;
|
||||||
|
const spriteCellRow = Math.floor(initPos.index / columns);
|
||||||
|
const constrainedX = Math.floor(Math.max(-negativeSpacing, Math.min(maxWidth - negativeSpacing - sprite.width, newSpriteX)));
|
||||||
|
const constrainedY = Math.floor(Math.max(-negativeSpacing, Math.min(maxHeight - negativeSpacing - sprite.height, newSpriteY)));
|
||||||
|
|
||||||
|
onUpdateSprite(id, constrainedX, constrainedY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const constrainedX = Math.floor(Math.max(-negativeSpacing, Math.min(maxWidth - negativeSpacing - sprites[spriteIndex].width, newX)));
|
const constrainedX = Math.floor(Math.max(-negativeSpacing, Math.min(maxWidth - negativeSpacing - sprites[spriteIndex].width, newX)));
|
||||||
const constrainedY = Math.floor(Math.max(-negativeSpacing, Math.min(maxHeight - negativeSpacing - sprites[spriteIndex].height, newY)));
|
const constrainedY = Math.floor(Math.max(-negativeSpacing, Math.min(maxHeight - negativeSpacing - sprites[spriteIndex].height, newY)));
|
||||||
|
|
||||||
onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY);
|
onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY);
|
||||||
|
}
|
||||||
onDraw();
|
onDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,7 +236,10 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const hoverCell = findCellAtPosition(pos.x, pos.y);
|
const hoverCell = findCellAtPosition(pos.x, pos.y);
|
||||||
currentHoverCell.value = hoverCell;
|
currentHoverCell.value = hoverCell;
|
||||||
|
|
||||||
if (getAllowCellSwap() && hoverCell) {
|
const selectedIds = getSelectedSpriteIds();
|
||||||
|
const isMultiDrag = selectedIds.has(activeSpriteId.value) && selectedIds.size > 1;
|
||||||
|
|
||||||
|
if (getAllowCellSwap() && hoverCell && !isMultiDrag) {
|
||||||
if (hoverCell.index !== activeSpriteCellIndex.value) {
|
if (hoverCell.index !== activeSpriteCellIndex.value) {
|
||||||
highlightCell.value = hoverCell;
|
highlightCell.value = hoverCell;
|
||||||
ghostSprite.value = {
|
ghostSprite.value = {
|
||||||
@@ -224,7 +259,10 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stopDrag = () => {
|
const stopDrag = () => {
|
||||||
if (isDragging.value && getAllowCellSwap() && activeSpriteId.value && activeSpriteCellIndex.value !== null && currentHoverCell.value && activeSpriteCellIndex.value !== currentHoverCell.value.index) {
|
const selectedIds = getSelectedSpriteIds();
|
||||||
|
const isMultiDrag = activeSpriteId.value && selectedIds.has(activeSpriteId.value) && selectedIds.size > 1;
|
||||||
|
|
||||||
|
if (isDragging.value && getAllowCellSwap() && !isMultiDrag && activeSpriteId.value && activeSpriteCellIndex.value !== null && currentHoverCell.value && activeSpriteCellIndex.value !== currentHoverCell.value.index) {
|
||||||
if (onUpdateSpriteCell) {
|
if (onUpdateSpriteCell) {
|
||||||
onUpdateSpriteCell(activeSpriteId.value, currentHoverCell.value.index);
|
onUpdateSpriteCell(activeSpriteId.value, currentHoverCell.value.index);
|
||||||
}
|
}
|
||||||
@@ -237,11 +275,11 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
currentHoverCell.value = null;
|
currentHoverCell.value = null;
|
||||||
highlightCell.value = null;
|
highlightCell.value = null;
|
||||||
ghostSprite.value = null;
|
ghostSprite.value = null;
|
||||||
|
initialSpritePositions.value.clear();
|
||||||
|
|
||||||
onDraw();
|
onDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Touch event handlers
|
|
||||||
const handleTouchStart = (event: TouchEvent) => {
|
const handleTouchStart = (event: TouchEvent) => {
|
||||||
if (event.touches.length === 1) {
|
if (event.touches.length === 1) {
|
||||||
const touch = event.touches[0];
|
const touch = event.touches[0];
|
||||||
@@ -271,14 +309,12 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
|
||||||
isDragging,
|
isDragging,
|
||||||
activeSpriteId,
|
activeSpriteId,
|
||||||
ghostSprite,
|
ghostSprite,
|
||||||
highlightCell,
|
highlightCell,
|
||||||
spritePositions,
|
spritePositions,
|
||||||
|
|
||||||
// Methods
|
|
||||||
startDrag,
|
startDrag,
|
||||||
drag,
|
drag,
|
||||||
stopDrag,
|
stopDrag,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { getMaxDimensionsAcrossLayers } from './useLayers';
|
|||||||
|
|
||||||
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>, layers?: Ref<Layer[]>) => {
|
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>, layers?: Ref<Layer[]>) => {
|
||||||
const getCellDimensions = () => {
|
const getCellDimensions = () => {
|
||||||
// If manual cell size is enabled, use manual values
|
|
||||||
if (manualCellSizeEnabled?.value) {
|
if (manualCellSizeEnabled?.value) {
|
||||||
return {
|
return {
|
||||||
cellWidth: manualCellWidth?.value ?? 64,
|
cellWidth: manualCellWidth?.value ?? 64,
|
||||||
@@ -18,11 +17,8 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use dimensions from ALL layers to keep canvas size stable when hiding layers
|
|
||||||
// Fall back to current sprites if layers ref is not provided
|
|
||||||
const { maxWidth, maxHeight } = layers?.value ? getMaxDimensionsAcrossLayers(layers.value, false) : getMaxDimensions(sprites.value);
|
const { maxWidth, maxHeight } = layers?.value ? getMaxDimensionsAcrossLayers(layers.value, false) : getMaxDimensions(sprites.value);
|
||||||
|
|
||||||
// Calculate negative spacing from all layers' sprites for consistency
|
|
||||||
const allSprites = layers?.value ? layers.value.flatMap(l => l.sprites) : sprites.value;
|
const allSprites = layers?.value ? layers.value.flatMap(l => l.sprites) : sprites.value;
|
||||||
const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value);
|
const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value);
|
||||||
|
|
||||||
@@ -50,7 +46,6 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
canvas.height = cellHeight * rows;
|
canvas.height = cellHeight * rows;
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
// Apply background color if not transparent
|
|
||||||
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
ctx.fillStyle = backgroundColor.value;
|
ctx.fillStyle = backgroundColor.value;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -151,7 +146,6 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
if (typeof jsonData.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = jsonData.manualCellWidth;
|
if (typeof jsonData.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = jsonData.manualCellWidth;
|
||||||
if (typeof jsonData.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = jsonData.manualCellHeight;
|
if (typeof jsonData.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = jsonData.manualCellHeight;
|
||||||
|
|
||||||
// revoke existing blob urls
|
|
||||||
if (sprites.value.length) {
|
if (sprites.value.length) {
|
||||||
sprites.value.forEach(s => {
|
sprites.value.forEach(s => {
|
||||||
if (s.url && s.url.startsWith('blob:')) {
|
if (s.url && s.url.startsWith('blob:')) {
|
||||||
@@ -215,7 +209,6 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
|
|
||||||
sprites.value.forEach(sprite => {
|
sprites.value.forEach(sprite => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
// Apply background color if not transparent
|
|
||||||
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
ctx.fillStyle = backgroundColor.value;
|
ctx.fillStyle = backgroundColor.value;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -263,7 +256,6 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
|
|
||||||
sprites.value.forEach((sprite, index) => {
|
sprites.value.forEach((sprite, index) => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
// Apply background color if not transparent
|
|
||||||
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
ctx.fillStyle = backgroundColor.value;
|
ctx.fillStyle = backgroundColor.value;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites);
|
const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites);
|
||||||
|
|
||||||
const getCellDimensions = () => {
|
const getCellDimensions = () => {
|
||||||
// If manual cell size is enabled, use manual values
|
|
||||||
if (manualCellSizeEnabled?.value) {
|
if (manualCellSizeEnabled?.value) {
|
||||||
return {
|
return {
|
||||||
cellWidth: manualCellWidth?.value ?? 64,
|
cellWidth: manualCellWidth?.value ?? 64,
|
||||||
@@ -20,10 +19,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, calculate from sprite dimensions across ALL layers (same as canvas)
|
|
||||||
// This ensures export dimensions match what's shown in the canvas
|
|
||||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(layersRef.value);
|
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(layersRef.value);
|
||||||
// Calculate negative spacing from ALL layers (not just visible) to keep canvas size stable
|
|
||||||
const allSprites = layersRef.value.flatMap(l => l.sprites);
|
const allSprites = layersRef.value.flatMap(l => l.sprites);
|
||||||
const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value);
|
const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value);
|
||||||
return {
|
return {
|
||||||
@@ -35,7 +31,6 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
|
|
||||||
const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => {
|
const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => {
|
||||||
ctx.clearRect(0, 0, cellWidth, cellHeight);
|
ctx.clearRect(0, 0, cellWidth, cellHeight);
|
||||||
// Apply background color if not transparent
|
|
||||||
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
ctx.fillStyle = backgroundColor.value;
|
ctx.fillStyle = backgroundColor.value;
|
||||||
ctx.fillRect(0, 0, cellWidth, cellHeight);
|
ctx.fillRect(0, 0, cellWidth, cellHeight);
|
||||||
@@ -78,7 +73,6 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
canvas.height = cellHeight * rows;
|
canvas.height = cellHeight * rows;
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
// Apply background color to entire canvas if not transparent
|
|
||||||
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
ctx.fillStyle = backgroundColor.value;
|
ctx.fillStyle = backgroundColor.value;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -186,7 +180,6 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
const sprites: Sprite[] = await Promise.all(layerData.sprites.map((s: any) => loadSprite(s)));
|
const sprites: Sprite[] = await Promise.all(layerData.sprites.map((s: any) => loadSprite(s)));
|
||||||
newLayers.push({ id: layerData.id || crypto.randomUUID(), name: layerData.name || 'Layer', visible: layerData.visible !== false, locked: !!layerData.locked, sprites });
|
newLayers.push({ id: layerData.id || crypto.randomUUID(), name: layerData.name || 'Layer', visible: layerData.visible !== false, locked: !!layerData.locked, sprites });
|
||||||
}
|
}
|
||||||
// Ensure at least one layer with sprites is visible
|
|
||||||
if (newLayers.length > 0 && !newLayers.some(l => l.visible && l.sprites.length > 0)) {
|
if (newLayers.length > 0 && !newLayers.some(l => l.visible && l.sprites.length > 0)) {
|
||||||
const firstLayerWithSprites = newLayers.find(l => l.sprites.length > 0);
|
const firstLayerWithSprites = newLayers.find(l => l.sprites.length > 0);
|
||||||
if (firstLayerWithSprites) {
|
if (firstLayerWithSprites) {
|
||||||
@@ -194,7 +187,6 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
layersRef.value = newLayers;
|
layersRef.value = newLayers;
|
||||||
// Set active layer to the first layer with sprites
|
|
||||||
if (activeLayerId && newLayers.length > 0) {
|
if (activeLayerId && newLayers.length > 0) {
|
||||||
const firstWithSprites = newLayers.find(l => l.sprites.length > 0);
|
const firstWithSprites = newLayers.find(l => l.sprites.length > 0);
|
||||||
activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id;
|
activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export interface FileDropOptions {
|
|||||||
export function useFileDrop(options: FileDropOptions) {
|
export function useFileDrop(options: FileDropOptions) {
|
||||||
const { onAddSprite, onAddSpriteWithResize } = options;
|
const { onAddSprite, onAddSpriteWithResize } = options;
|
||||||
|
|
||||||
// Helper to get sprites array
|
|
||||||
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
||||||
|
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false);
|
||||||
@@ -60,7 +59,6 @@ export function useFileDrop(options: FileDropOptions) {
|
|||||||
const sprites = getSprites();
|
const sprites = getSprites();
|
||||||
const { maxWidth, maxHeight } = getMaxDimensions(sprites);
|
const { maxWidth, maxHeight } = getMaxDimensions(sprites);
|
||||||
|
|
||||||
// Check if the dropped image is larger than current cells
|
|
||||||
if (img.naturalWidth > maxWidth || img.naturalHeight > maxHeight) {
|
if (img.naturalWidth > maxWidth || img.naturalHeight > maxHeight) {
|
||||||
onAddSpriteWithResize(file);
|
onAddSpriteWithResize(file);
|
||||||
} else {
|
} else {
|
||||||
@@ -94,7 +92,6 @@ export function useFileDrop(options: FileDropOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each dropped file
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await processDroppedImage(file);
|
await processDroppedImage(file);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export interface GridMetricsOptions {
|
|||||||
* Provides a single source of truth for cell dimensions and positioning calculations.
|
* Provides a single source of truth for cell dimensions and positioning calculations.
|
||||||
*/
|
*/
|
||||||
export function useGridMetrics(options: GridMetricsOptions = {}) {
|
export function useGridMetrics(options: GridMetricsOptions = {}) {
|
||||||
// Helper to get reactive values
|
|
||||||
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
||||||
const getSprites = () => (options.sprites ? (Array.isArray(options.sprites) ? options.sprites : options.sprites.value) : []);
|
const getSprites = () => (options.sprites ? (Array.isArray(options.sprites) ? options.sprites : options.sprites.value) : []);
|
||||||
const getNegativeSpacingEnabled = () => (typeof options.negativeSpacingEnabled === 'boolean' ? options.negativeSpacingEnabled : (options.negativeSpacingEnabled?.value ?? false));
|
const getNegativeSpacingEnabled = () => (typeof options.negativeSpacingEnabled === 'boolean' ? options.negativeSpacingEnabled : (options.negativeSpacingEnabled?.value ?? false));
|
||||||
@@ -52,7 +51,6 @@ export function useGridMetrics(options: GridMetricsOptions = {}) {
|
|||||||
const calculateCellDimensions = (): GridMetrics => {
|
const calculateCellDimensions = (): GridMetrics => {
|
||||||
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
||||||
|
|
||||||
// If manual cell size is enabled, use manual dimensions
|
|
||||||
if (manualCellSizeEnabled) {
|
if (manualCellSizeEnabled) {
|
||||||
return {
|
return {
|
||||||
maxWidth: Math.round(getManualCellWidth()),
|
maxWidth: Math.round(getManualCellWidth()),
|
||||||
@@ -61,20 +59,16 @@ export function useGridMetrics(options: GridMetricsOptions = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get sprites to measure from layers or direct sprites array
|
|
||||||
const layers = getLayers();
|
const layers = getLayers();
|
||||||
const spritesToMeasure = layers ? layers.flatMap(l => l.sprites) : getSprites();
|
const spritesToMeasure = layers ? layers.flatMap(l => l.sprites) : getSprites();
|
||||||
|
|
||||||
// Calculate base dimensions from sprites
|
|
||||||
const base = getMaxDimensions(spritesToMeasure);
|
const base = getMaxDimensions(spritesToMeasure);
|
||||||
const baseMaxWidth = Math.max(1, base.maxWidth);
|
const baseMaxWidth = Math.max(1, base.maxWidth);
|
||||||
const baseMaxHeight = Math.max(1, base.maxHeight);
|
const baseMaxHeight = Math.max(1, base.maxHeight);
|
||||||
|
|
||||||
// Calculate negative spacing
|
|
||||||
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
||||||
const negativeSpacing = Math.round(calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled));
|
const negativeSpacing = Math.round(calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled));
|
||||||
|
|
||||||
// Add negative spacing to expand each cell
|
|
||||||
return {
|
return {
|
||||||
maxWidth: Math.round(baseMaxWidth + negativeSpacing),
|
maxWidth: Math.round(baseMaxWidth + negativeSpacing),
|
||||||
maxHeight: Math.round(baseMaxHeight + negativeSpacing),
|
maxHeight: Math.round(baseMaxHeight + negativeSpacing),
|
||||||
@@ -116,10 +110,8 @@ export function useGridMetrics(options: GridMetricsOptions = {}) {
|
|||||||
const gridMetrics = computed(() => calculateCellDimensions());
|
const gridMetrics = computed(() => calculateCellDimensions());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Computed values
|
|
||||||
gridMetrics,
|
gridMetrics,
|
||||||
|
|
||||||
// Methods
|
|
||||||
calculateCellDimensions,
|
calculateCellDimensions,
|
||||||
getCellPosition,
|
getCellPosition,
|
||||||
getSpriteCanvasPosition,
|
getSpriteCanvasPosition,
|
||||||
@@ -147,7 +139,6 @@ export function getGridMetrics(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have layers or sprites
|
|
||||||
const isLayers = spritesOrLayers.length > 0 && 'sprites' in spritesOrLayers[0];
|
const isLayers = spritesOrLayers.length > 0 && 'sprites' in spritesOrLayers[0];
|
||||||
const sprites = isLayers ? (spritesOrLayers as Layer[]).flatMap(l => l.sprites) : (spritesOrLayers as Sprite[]);
|
const sprites = isLayers ? (spritesOrLayers as Layer[]).flatMap(l => l.sprites) : (spritesOrLayers as Sprite[]);
|
||||||
|
|
||||||
|
|||||||
@@ -70,16 +70,13 @@ export const useLayers = () => {
|
|||||||
const l = activeLayer.value;
|
const l = activeLayer.value;
|
||||||
if (!l || !l.sprites.length) return;
|
if (!l || !l.sprites.length) return;
|
||||||
|
|
||||||
// Determine the cell dimensions to align within
|
|
||||||
let cellWidth: number;
|
let cellWidth: number;
|
||||||
let cellHeight: number;
|
let cellHeight: number;
|
||||||
|
|
||||||
if (settingsStore.manualCellSizeEnabled) {
|
if (settingsStore.manualCellSizeEnabled) {
|
||||||
// Use manual cell size (without negative spacing)
|
|
||||||
cellWidth = settingsStore.manualCellWidth;
|
cellWidth = settingsStore.manualCellWidth;
|
||||||
cellHeight = settingsStore.manualCellHeight;
|
cellHeight = settingsStore.manualCellHeight;
|
||||||
} else {
|
} else {
|
||||||
// Use auto-calculated dimensions based on ALL visible layers (not just active layer)
|
|
||||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||||
cellWidth = maxWidth;
|
cellWidth = maxWidth;
|
||||||
cellHeight = maxHeight;
|
cellHeight = maxHeight;
|
||||||
@@ -120,60 +117,26 @@ export const useLayers = () => {
|
|||||||
|
|
||||||
const next = [...l.sprites];
|
const next = [...l.sprites];
|
||||||
|
|
||||||
// Remove the moving sprite first
|
|
||||||
const [moving] = next.splice(currentIndex, 1);
|
const [moving] = next.splice(currentIndex, 1);
|
||||||
|
|
||||||
// Determine the actual index to insert at, considering we removed one item
|
|
||||||
// If the target index was greater than current index, it shifts down by 1 in the original array perspective?
|
|
||||||
// Actually simpler: we just want to put 'moving' at 'newIndex' in the final array.
|
|
||||||
|
|
||||||
// If newIndex is beyond the current bounds (after removal), fill with placeholders
|
|
||||||
while (next.length < newIndex) {
|
while (next.length < newIndex) {
|
||||||
next.push(createEmptySprite());
|
next.push(createEmptySprite());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now insert
|
|
||||||
// If newIndex is within bounds, we might be swapping if there was something there
|
|
||||||
// But the DragSprite logic implies we are "moving to this cell".
|
|
||||||
// If there is existing content at newIndex, we should swap or splice?
|
|
||||||
// The previous implementation did a swap if newIndex < length (before removal).
|
|
||||||
|
|
||||||
// Let's stick to the "swap" logic if there's a sprite there, or "move" if we are reordering.
|
|
||||||
// Wait, Drag and Drop usually implies "insert here" or "swap with this".
|
|
||||||
// useDragSprite says: "if allowCellSwap... updateSpriteCell".
|
|
||||||
|
|
||||||
// The original logic:
|
|
||||||
// if (newIndex < next.length) -> swap
|
|
||||||
// else -> splice (move)
|
|
||||||
|
|
||||||
// Re-evaluating original logic:
|
|
||||||
// next has NOT had the item removed yet in the original logic 'if' block.
|
|
||||||
|
|
||||||
// Let's implement robust swap/move logic.
|
|
||||||
// 1. If target is empty placeholder -> just move there (replace placeholder).
|
|
||||||
// 2. If target has sprite -> swap.
|
|
||||||
// 3. If target is out of bounds -> pad and move.
|
|
||||||
|
|
||||||
if (newIndex < l.sprites.length) {
|
if (newIndex < l.sprites.length) {
|
||||||
// Perform Swap
|
|
||||||
const target = l.sprites[newIndex];
|
const target = l.sprites[newIndex];
|
||||||
const moving = l.sprites[currentIndex];
|
const moving = l.sprites[currentIndex];
|
||||||
|
|
||||||
// Clone array
|
|
||||||
const newSprites = [...l.sprites];
|
const newSprites = [...l.sprites];
|
||||||
newSprites[currentIndex] = target;
|
newSprites[currentIndex] = target;
|
||||||
newSprites[newIndex] = moving;
|
newSprites[newIndex] = moving;
|
||||||
l.sprites = newSprites;
|
l.sprites = newSprites;
|
||||||
} else {
|
} else {
|
||||||
// Move to previously empty/non-existent cell
|
|
||||||
const newSprites = [...l.sprites];
|
const newSprites = [...l.sprites];
|
||||||
// Remove from old pos
|
|
||||||
const [moved] = newSprites.splice(currentIndex, 1);
|
const [moved] = newSprites.splice(currentIndex, 1);
|
||||||
// Pad
|
|
||||||
while (newSprites.length < newIndex) {
|
while (newSprites.length < newIndex) {
|
||||||
newSprites.push(createEmptySprite());
|
newSprites.push(createEmptySprite());
|
||||||
}
|
}
|
||||||
// Insert (or push if equal length)
|
|
||||||
newSprites.splice(newIndex, 0, moved);
|
newSprites.splice(newIndex, 0, moved);
|
||||||
l.sprites = newSprites;
|
l.sprites = newSprites;
|
||||||
}
|
}
|
||||||
@@ -197,7 +160,6 @@ export const useLayers = () => {
|
|||||||
const l = activeLayer.value;
|
const l = activeLayer.value;
|
||||||
if (!l) return;
|
if (!l) return;
|
||||||
|
|
||||||
// Sort indices in descending order to avoid shift issues when splicing
|
|
||||||
const indicesToRemove: number[] = [];
|
const indicesToRemove: number[] = [];
|
||||||
ids.forEach(id => {
|
ids.forEach(id => {
|
||||||
const i = l.sprites.findIndex(s => s.id === id);
|
const i = l.sprites.findIndex(s => s.id === id);
|
||||||
@@ -291,21 +253,11 @@ export const useLayers = () => {
|
|||||||
const currentSprites = [...l.sprites];
|
const currentSprites = [...l.sprites];
|
||||||
|
|
||||||
if (typeof index === 'number') {
|
if (typeof index === 'number') {
|
||||||
// If index is provided, insert there (padding if needed)
|
|
||||||
while (currentSprites.length < index) {
|
while (currentSprites.length < index) {
|
||||||
currentSprites.push(createEmptySprite());
|
currentSprites.push(createEmptySprite());
|
||||||
}
|
}
|
||||||
// If valid index, replace if empty or splice?
|
|
||||||
// "Adds it not in the one I selected".
|
|
||||||
// If I select a cell, I expect it to go there.
|
|
||||||
// If the cell is empty (placeholder), replace it.
|
|
||||||
// If the cell has a sprite, maybe insert/shift?
|
|
||||||
// Usually "Add" implies append, but context menu "Add sprite" on a cell implies "Put it here".
|
|
||||||
// Let's Insert (Shift others) for safety, or check if empty.
|
|
||||||
// But simpler: just splice it in.
|
|
||||||
currentSprites.splice(index, 0, next);
|
currentSprites.splice(index, 0, next);
|
||||||
} else {
|
} else {
|
||||||
// No index, append to end
|
|
||||||
currentSprites.push(next);
|
currentSprites.push(next);
|
||||||
}
|
}
|
||||||
l.sprites = currentSprites;
|
l.sprites = currentSprites;
|
||||||
@@ -353,7 +305,6 @@ export const useLayers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copySpriteToFrame = (spriteId: string, targetLayerId: string, targetFrameIndex: number) => {
|
const copySpriteToFrame = (spriteId: string, targetLayerId: string, targetFrameIndex: number) => {
|
||||||
// Find the source sprite in any layer
|
|
||||||
let sourceSprite: Sprite | undefined;
|
let sourceSprite: Sprite | undefined;
|
||||||
for (const layer of layers.value) {
|
for (const layer of layers.value) {
|
||||||
sourceSprite = layer.sprites.find(s => s.id === spriteId);
|
sourceSprite = layer.sprites.find(s => s.id === spriteId);
|
||||||
@@ -362,11 +313,9 @@ export const useLayers = () => {
|
|||||||
|
|
||||||
if (!sourceSprite) return;
|
if (!sourceSprite) return;
|
||||||
|
|
||||||
// Find target layer
|
|
||||||
const targetLayer = layers.value.find(l => l.id === targetLayerId);
|
const targetLayer = layers.value.find(l => l.id === targetLayerId);
|
||||||
if (!targetLayer) return;
|
if (!targetLayer) return;
|
||||||
|
|
||||||
// Create a deep copy of the sprite with a new ID
|
|
||||||
const copiedSprite: Sprite = {
|
const copiedSprite: Sprite = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
file: sourceSprite.file,
|
file: sourceSprite.file,
|
||||||
@@ -381,14 +330,11 @@ export const useLayers = () => {
|
|||||||
flipY: sourceSprite.flipY,
|
flipY: sourceSprite.flipY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expand the sprites array if necessary with empty placeholder sprites
|
|
||||||
while (targetLayer.sprites.length < targetFrameIndex) {
|
while (targetLayer.sprites.length < targetFrameIndex) {
|
||||||
targetLayer.sprites.push(createEmptySprite());
|
targetLayer.sprites.push(createEmptySprite());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace or insert the sprite at the target index
|
|
||||||
if (targetFrameIndex < targetLayer.sprites.length) {
|
if (targetFrameIndex < targetLayer.sprites.length) {
|
||||||
// Replace existing sprite at this frame
|
|
||||||
const old = targetLayer.sprites[targetFrameIndex];
|
const old = targetLayer.sprites[targetFrameIndex];
|
||||||
if (old.url && old.url.startsWith('blob:')) {
|
if (old.url && old.url.startsWith('blob:')) {
|
||||||
try {
|
try {
|
||||||
@@ -397,7 +343,6 @@ export const useLayers = () => {
|
|||||||
}
|
}
|
||||||
targetLayer.sprites[targetFrameIndex] = copiedSprite;
|
targetLayer.sprites[targetFrameIndex] = copiedSprite;
|
||||||
} else {
|
} else {
|
||||||
// Add at the end
|
|
||||||
targetLayer.sprites.push(copiedSprite);
|
targetLayer.sprites.push(copiedSprite);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -429,8 +374,6 @@ export const useLayers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxDimensionsAcrossLayers = (layers: Layer[], visibleOnly: boolean = false) => {
|
export const getMaxDimensionsAcrossLayers = (layers: Layer[], visibleOnly: boolean = false) => {
|
||||||
// When visibleOnly is false (default), consider ALL layers to keep canvas size stable
|
|
||||||
// When visibleOnly is true (export), only consider visible layers
|
|
||||||
const sprites = layers.flatMap(l => (visibleOnly ? (l.visible ? l.sprites : []) : l.sprites));
|
const sprites = layers.flatMap(l => (visibleOnly ? (l.visible ? l.sprites : []) : l.sprites));
|
||||||
return getMaxDimensionsSingle(sprites);
|
return getMaxDimensionsSingle(sprites);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ export function calculateNegativeSpacing(sprites: Sprite[], enabled: boolean): n
|
|||||||
const minWidth = Math.min(...sprites.map(s => s.width));
|
const minWidth = Math.min(...sprites.map(s => s.width));
|
||||||
const minHeight = Math.min(...sprites.map(s => s.height));
|
const minHeight = Math.min(...sprites.map(s => s.height));
|
||||||
|
|
||||||
// Available space is the gap between cell size and smallest sprite
|
|
||||||
const availableWidth = maxWidth - minWidth;
|
const availableWidth = maxWidth - minWidth;
|
||||||
const availableHeight = maxHeight - minHeight;
|
const availableHeight = maxHeight - minHeight;
|
||||||
|
|
||||||
// Use half to balance spacing equally on all sides
|
|
||||||
return Math.floor(Math.min(availableWidth, availableHeight) / 2);
|
return Math.floor(Math.min(availableWidth, availableHeight) / 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,22 +23,17 @@ export const useProjectManager = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const createProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
const createProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
||||||
// 1. Reset Settings
|
|
||||||
settingsStore.setManualCellSize(config.width, config.height);
|
settingsStore.setManualCellSize(config.width, config.height);
|
||||||
settingsStore.manualCellSizeEnabled = true;
|
settingsStore.manualCellSizeEnabled = true;
|
||||||
|
|
||||||
// 2. Reset Layers
|
|
||||||
const newLayer = createEmptyLayer('Base');
|
const newLayer = createEmptyLayer('Base');
|
||||||
layers.value = [newLayer];
|
layers.value = [newLayer];
|
||||||
activeLayerId.value = newLayer.id;
|
activeLayerId.value = newLayer.id;
|
||||||
|
|
||||||
// 3. Set Columns
|
|
||||||
columns.value = config.columns;
|
columns.value = config.columns;
|
||||||
|
|
||||||
// 4. Reset Project Store
|
|
||||||
projectStore.currentProject = null;
|
projectStore.currentProject = null;
|
||||||
|
|
||||||
// 5. Navigate to Editor
|
|
||||||
router.push('/editor');
|
router.push('/editor');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,12 +55,9 @@ export const useProjectManager = () => {
|
|||||||
const data = await generateProjectJSON();
|
const data = await generateProjectJSON();
|
||||||
|
|
||||||
if (projectStore.currentProject) {
|
if (projectStore.currentProject) {
|
||||||
// Update existing project (even if name changed)
|
|
||||||
await projectStore.updateProject(projectStore.currentProject.id, name, data);
|
await projectStore.updateProject(projectStore.currentProject.id, name, data);
|
||||||
} else {
|
} else {
|
||||||
// Create new project if none exists
|
|
||||||
await projectStore.createProject(name, data);
|
await projectStore.createProject(name, data);
|
||||||
// After creating, we should update route to include ID so subsequent saves update it
|
|
||||||
const newProject = projectStore.currentProject as Project | null;
|
const newProject = projectStore.currentProject as Project | null;
|
||||||
if (newProject) {
|
if (newProject) {
|
||||||
router.replace({ name: 'editor', params: { id: newProject.id } });
|
router.replace({ name: 'editor', params: { id: newProject.id } });
|
||||||
@@ -81,9 +73,7 @@ export const useProjectManager = () => {
|
|||||||
const saveAsProject = async (name: string) => {
|
const saveAsProject = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await generateProjectJSON();
|
const data = await generateProjectJSON();
|
||||||
// Always create new
|
|
||||||
await projectStore.createProject(name, data);
|
await projectStore.createProject(name, data);
|
||||||
// Navigate to new project
|
|
||||||
if (projectStore.currentProject) {
|
if (projectStore.currentProject) {
|
||||||
router.push({ name: 'editor', params: { id: projectStore.currentProject.id } });
|
router.push({ name: 'editor', params: { id: projectStore.currentProject.id } });
|
||||||
}
|
}
|
||||||
@@ -95,18 +85,14 @@ export const useProjectManager = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeProject = () => {
|
const closeProject = () => {
|
||||||
// Reset Layers
|
|
||||||
const newLayer = createEmptyLayer('Base');
|
const newLayer = createEmptyLayer('Base');
|
||||||
layers.value = [newLayer];
|
layers.value = [newLayer];
|
||||||
activeLayerId.value = newLayer.id;
|
activeLayerId.value = newLayer.id;
|
||||||
|
|
||||||
// Reset columns
|
|
||||||
columns.value = 4;
|
columns.value = 4;
|
||||||
|
|
||||||
// Reset Project Store
|
|
||||||
projectStore.currentProject = null;
|
projectStore.currentProject = null;
|
||||||
|
|
||||||
// Navigate Home
|
|
||||||
router.push('/');
|
router.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ export function useSEO(metadata: SEOMetaData) {
|
|||||||
const imageUrl = metadata.image ? `${SITE_URL}${metadata.image}` : `${SITE_URL}${DEFAULT_IMAGE}`;
|
const imageUrl = metadata.image ? `${SITE_URL}${metadata.image}` : `${SITE_URL}${DEFAULT_IMAGE}`;
|
||||||
|
|
||||||
const metaTags: any[] = [
|
const metaTags: any[] = [
|
||||||
// Primary Meta Tags
|
|
||||||
{ name: 'title', content: fullTitle },
|
{ name: 'title', content: fullTitle },
|
||||||
{ name: 'description', content: metadata.description },
|
{ name: 'description', content: metadata.description },
|
||||||
{ name: 'robots', content: 'index, follow' },
|
{ name: 'robots', content: 'index, follow' },
|
||||||
|
|
||||||
// Open Graph / Facebook
|
|
||||||
{ property: 'og:type', content: metadata.type || 'website' },
|
{ property: 'og:type', content: metadata.type || 'website' },
|
||||||
{ property: 'og:url', content: fullUrl },
|
{ property: 'og:url', content: fullUrl },
|
||||||
{ property: 'og:title', content: fullTitle },
|
{ property: 'og:title', content: fullTitle },
|
||||||
@@ -37,7 +35,6 @@ export function useSEO(metadata: SEOMetaData) {
|
|||||||
{ property: 'og:image', content: imageUrl },
|
{ property: 'og:image', content: imageUrl },
|
||||||
{ property: 'og:site_name', content: SITE_NAME },
|
{ property: 'og:site_name', content: SITE_NAME },
|
||||||
|
|
||||||
// Twitter
|
|
||||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||||
{ name: 'twitter:url', content: fullUrl },
|
{ name: 'twitter:url', content: fullUrl },
|
||||||
{ name: 'twitter:title', content: fullTitle },
|
{ name: 'twitter:title', content: fullTitle },
|
||||||
@@ -45,7 +42,6 @@ export function useSEO(metadata: SEOMetaData) {
|
|||||||
{ name: 'twitter:image', content: imageUrl },
|
{ name: 'twitter:image', content: imageUrl },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add article-specific meta tags
|
|
||||||
if (metadata.type === 'article') {
|
if (metadata.type === 'article') {
|
||||||
if (metadata.author) {
|
if (metadata.author) {
|
||||||
metaTags.push({ property: 'article:author', content: metadata.author });
|
metaTags.push({ property: 'article:author', content: metadata.author });
|
||||||
@@ -58,7 +54,6 @@ export function useSEO(metadata: SEOMetaData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add keywords if provided
|
|
||||||
if (metadata.keywords) {
|
if (metadata.keywords) {
|
||||||
metaTags.push({ name: 'keywords', content: metadata.keywords });
|
metaTags.push({ name: 'keywords', content: metadata.keywords });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export const buildShareUrl = (id: string): string => {
|
|||||||
* Share a spritesheet by uploading to PocketBase
|
* Share a spritesheet by uploading to PocketBase
|
||||||
*/
|
*/
|
||||||
export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>): Promise<ShareResult> => {
|
export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>): Promise<ShareResult> => {
|
||||||
// Build layers data with base64 sprites (same format as exportSpritesheetJSON)
|
|
||||||
const layersData = await Promise.all(
|
const layersData = await Promise.all(
|
||||||
layersRef.value.map(async layer => {
|
layersRef.value.map(async layer => {
|
||||||
const sprites = await Promise.all(
|
const sprites = await Promise.all(
|
||||||
@@ -80,7 +79,6 @@ export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<num
|
|||||||
ctx.drawImage(sprite.img, 0, 0);
|
ctx.drawImage(sprite.img, 0, 0);
|
||||||
}
|
}
|
||||||
const base64 = canvas.toDataURL('image/png');
|
const base64 = canvas.toDataURL('image/png');
|
||||||
// Since we bake transformations into the image, set them to 0/false in metadata
|
|
||||||
return {
|
return {
|
||||||
id: sprite.id,
|
id: sprite.id,
|
||||||
width: sprite.width,
|
width: sprite.width,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export const useSprites = () => {
|
|||||||
const sprites = ref<Sprite[]>([]);
|
const sprites = ref<Sprite[]>([]);
|
||||||
const columns = ref(4);
|
const columns = ref(4);
|
||||||
|
|
||||||
// Clamp and coerce columns to a safe range [1..10]
|
|
||||||
watch(columns, val => {
|
watch(columns, val => {
|
||||||
const num = typeof val === 'number' ? val : parseInt(String(val));
|
const num = typeof val === 'number' ? val : parseInt(String(val));
|
||||||
const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1;
|
const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1;
|
||||||
|
|||||||
@@ -49,10 +49,8 @@ export function useSpritesheetSplitter() {
|
|||||||
let height = cellHeight;
|
let height = cellHeight;
|
||||||
|
|
||||||
if (preserveCellSize) {
|
if (preserveCellSize) {
|
||||||
// Keep full cell with transparent padding
|
|
||||||
url = canvas.toDataURL('image/png');
|
url = canvas.toDataURL('image/png');
|
||||||
} else {
|
} else {
|
||||||
// Crop to sprite bounds
|
|
||||||
const bounds = getSpriteBounds(ctx, cellWidth, cellHeight);
|
const bounds = getSpriteBounds(ctx, cellWidth, cellHeight);
|
||||||
if (bounds) {
|
if (bounds) {
|
||||||
x = bounds.x;
|
x = bounds.x;
|
||||||
@@ -94,7 +92,6 @@ export function useSpritesheetSplitter() {
|
|||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
|
||||||
// Initialize worker lazily
|
|
||||||
if (!worker.value) {
|
if (!worker.value) {
|
||||||
try {
|
try {
|
||||||
worker.value = new Worker(new URL('../workers/irregularSpriteDetection.worker.ts', import.meta.url), { type: 'module' });
|
worker.value = new Worker(new URL('../workers/irregularSpriteDetection.worker.ts', import.meta.url), { type: 'module' });
|
||||||
@@ -161,7 +158,6 @@ export function useSpritesheetSplitter() {
|
|||||||
spriteCtx.clearRect(0, 0, width, height);
|
spriteCtx.clearRect(0, 0, width, height);
|
||||||
spriteCtx.drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height);
|
spriteCtx.drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height);
|
||||||
|
|
||||||
// Remove background color
|
|
||||||
removeBackground(spriteCtx, width, height, backgroundColor);
|
removeBackground(spriteCtx, width, height, backgroundColor);
|
||||||
|
|
||||||
const isEmpty = removeEmpty ? isCanvasEmpty(spriteCtx, width, height) : false;
|
const isEmpty = removeEmpty ? isCanvasEmpty(spriteCtx, width, height) : false;
|
||||||
@@ -209,7 +205,6 @@ export function useSpritesheetSplitter() {
|
|||||||
|
|
||||||
if (!hasContent) return null;
|
if (!hasContent) return null;
|
||||||
|
|
||||||
// Add small padding
|
|
||||||
const pad = 1;
|
const pad = 1;
|
||||||
return {
|
return {
|
||||||
x: Math.max(0, minX - pad),
|
x: Math.max(0, minX - pad),
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface FAQItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useStructuredData() {
|
export function useStructuredData() {
|
||||||
// Organization Schema
|
|
||||||
const addOrganizationSchema = () => {
|
const addOrganizationSchema = () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -46,7 +45,6 @@ export function useStructuredData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebSite Schema
|
|
||||||
const addWebSiteSchema = () => {
|
const addWebSiteSchema = () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -71,7 +69,6 @@ export function useStructuredData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// BlogPosting Schema
|
|
||||||
const addBlogPostSchema = (post: BlogPostSchema) => {
|
const addBlogPostSchema = (post: BlogPostSchema) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -109,7 +106,6 @@ export function useStructuredData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Breadcrumb Schema
|
|
||||||
const addBreadcrumbSchema = (items: BreadcrumbItem[]) => {
|
const addBreadcrumbSchema = (items: BreadcrumbItem[]) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -132,7 +128,6 @@ export function useStructuredData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Blog List Schema
|
|
||||||
const addBlogListSchema = (posts: BlogPostSchema[]) => {
|
const addBlogListSchema = (posts: BlogPostSchema[]) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -164,7 +159,6 @@ export function useStructuredData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// FAQ Schema
|
|
||||||
const addFAQSchema = (faqs: FAQItem[]) => {
|
const addFAQSchema = (faqs: FAQItem[]) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export function useZoom(options: ZoomOptions) {
|
|||||||
if (currentIndex < options.allowedValues.length - 1) {
|
if (currentIndex < options.allowedValues.length - 1) {
|
||||||
zoom.value = options.allowedValues[currentIndex + 1];
|
zoom.value = options.allowedValues[currentIndex + 1];
|
||||||
} else if (currentIndex === -1) {
|
} else if (currentIndex === -1) {
|
||||||
// Find the nearest higher value
|
|
||||||
const higher = options.allowedValues.find(v => v > zoom.value);
|
const higher = options.allowedValues.find(v => v > zoom.value);
|
||||||
if (higher !== undefined) {
|
if (higher !== undefined) {
|
||||||
zoom.value = higher;
|
zoom.value = higher;
|
||||||
@@ -49,7 +48,6 @@ export function useZoom(options: ZoomOptions) {
|
|||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
zoom.value = options.allowedValues[currentIndex - 1];
|
zoom.value = options.allowedValues[currentIndex - 1];
|
||||||
} else if (currentIndex === -1) {
|
} else if (currentIndex === -1) {
|
||||||
// Find the nearest lower value
|
|
||||||
const lower = [...options.allowedValues].reverse().find(v => v < zoom.value);
|
const lower = [...options.allowedValues].reverse().find(v => v < zoom.value);
|
||||||
if (lower !== undefined) {
|
if (lower !== undefined) {
|
||||||
zoom.value = lower;
|
zoom.value = lower;
|
||||||
@@ -66,7 +64,6 @@ export function useZoom(options: ZoomOptions) {
|
|||||||
if (isStepOptions(options)) {
|
if (isStepOptions(options)) {
|
||||||
zoom.value = Math.max(options.min, Math.min(options.max, value));
|
zoom.value = Math.max(options.min, Math.min(options.max, value));
|
||||||
} else {
|
} else {
|
||||||
// Snap to nearest allowed value
|
|
||||||
const nearest = options.allowedValues.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
const nearest = options.allowedValues.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||||
zoom.value = nearest;
|
zoom.value = nearest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
|
const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
|
||||||
const user = ref(pb.authStore.model);
|
const user = ref(pb.authStore.model);
|
||||||
|
|
||||||
// Sync user state on change
|
|
||||||
pb.authStore.onChange(() => {
|
pb.authStore.onChange(() => {
|
||||||
user.value = pb.authStore.model;
|
user.value = pb.authStore.model;
|
||||||
});
|
});
|
||||||
@@ -21,7 +20,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
password,
|
password,
|
||||||
passwordConfirm,
|
passwordConfirm,
|
||||||
});
|
});
|
||||||
// Auto login after register
|
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,27 +10,21 @@ const manualCellWidth = ref(64);
|
|||||||
const manualCellHeight = ref(64);
|
const manualCellHeight = ref(64);
|
||||||
const checkerboardEnabled = ref(false);
|
const checkerboardEnabled = ref(false);
|
||||||
|
|
||||||
// Initialize dark mode from localStorage or system preference
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// Check localStorage first
|
|
||||||
const storedDarkMode = localStorage.getItem('darkMode');
|
const storedDarkMode = localStorage.getItem('darkMode');
|
||||||
if (storedDarkMode !== null) {
|
if (storedDarkMode !== null) {
|
||||||
darkMode.value = storedDarkMode === 'true';
|
darkMode.value = storedDarkMode === 'true';
|
||||||
} else {
|
} else {
|
||||||
// If not in localStorage, check system preference
|
|
||||||
darkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
darkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
// Watch for changes to update localStorage and apply class
|
|
||||||
watch(
|
watch(
|
||||||
darkMode,
|
darkMode,
|
||||||
newValue => {
|
newValue => {
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('darkMode', newValue.toString());
|
localStorage.setItem('darkMode', newValue.toString());
|
||||||
|
|
||||||
// Apply or remove dark class on document
|
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
} else {
|
} else {
|
||||||
@@ -40,7 +34,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Actions
|
|
||||||
function togglePixelPerfect() {
|
function togglePixelPerfect() {
|
||||||
pixelPerfect.value = !pixelPerfect.value;
|
pixelPerfect.value = !pixelPerfect.value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
const { addBreadcrumbSchema } = useStructuredData();
|
const { addBreadcrumbSchema } = useStructuredData();
|
||||||
|
|
||||||
// Set SEO synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'About Us - Our Mission & Story',
|
title: 'About Us - Our Mission & Story',
|
||||||
description: 'Learn about Spritesheet Generator, a free tool designed to help game developers and artists streamline their workflow with optimized spritesheet creation.',
|
description: 'Learn about Spritesheet Generator, a free tool designed to help game developers and artists streamline their workflow with optimized spritesheet creation.',
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
const slug = computed(() => route.params.slug as string);
|
const slug = computed(() => route.params.slug as string);
|
||||||
|
|
||||||
// Reactive SEO data that updates when post loads
|
|
||||||
const pageTitle = computed(() => (post.value ? `${post.value.title} - Spritesheet Generator` : 'Blog Post - Spritesheet Generator'));
|
const pageTitle = computed(() => (post.value ? `${post.value.title} - Spritesheet Generator` : 'Blog Post - Spritesheet Generator'));
|
||||||
|
|
||||||
const pageDescription = computed(() => post.value?.description || 'Read our latest article about spritesheet generation and game development.');
|
const pageDescription = computed(() => post.value?.description || 'Read our latest article about spritesheet generation and game development.');
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
|
|
||||||
const keywords = computed(() => post.value?.keywords || 'sprite sheet, game development, blog');
|
const keywords = computed(() => post.value?.keywords || 'sprite sheet, game development, blog');
|
||||||
|
|
||||||
// Dynamic meta tags using reactive computed values
|
|
||||||
useHead({
|
useHead({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
meta: [
|
meta: [
|
||||||
@@ -33,7 +31,6 @@
|
|||||||
{ name: 'keywords', content: keywords },
|
{ name: 'keywords', content: keywords },
|
||||||
{ name: 'robots', content: 'index, follow' },
|
{ name: 'robots', content: 'index, follow' },
|
||||||
|
|
||||||
// Open Graph
|
|
||||||
{ property: 'og:type', content: 'article' },
|
{ property: 'og:type', content: 'article' },
|
||||||
{ property: 'og:url', content: pageUrl },
|
{ property: 'og:url', content: pageUrl },
|
||||||
{ property: 'og:title', content: pageTitle },
|
{ property: 'og:title', content: pageTitle },
|
||||||
@@ -43,7 +40,6 @@
|
|||||||
{ property: 'article:author', content: computed(() => post.value?.author || 'streetshadow') },
|
{ property: 'article:author', content: computed(() => post.value?.author || 'streetshadow') },
|
||||||
{ property: 'article:published_time', content: computed(() => post.value?.date || '') },
|
{ property: 'article:published_time', content: computed(() => post.value?.date || '') },
|
||||||
|
|
||||||
// Twitter
|
|
||||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||||
{ name: 'twitter:url', content: pageUrl },
|
{ name: 'twitter:url', content: pageUrl },
|
||||||
{ name: 'twitter:title', content: pageTitle },
|
{ name: 'twitter:title', content: pageTitle },
|
||||||
@@ -54,7 +50,6 @@
|
|||||||
script: computed(() => {
|
script: computed(() => {
|
||||||
const scripts = [];
|
const scripts = [];
|
||||||
|
|
||||||
// Breadcrumb schema
|
|
||||||
scripts.push({
|
scripts.push({
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify({
|
children: JSON.stringify({
|
||||||
@@ -83,7 +78,6 @@
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Blog post schema
|
|
||||||
if (post.value) {
|
if (post.value) {
|
||||||
scripts.push({
|
scripts.push({
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
const { addBreadcrumbSchema } = useStructuredData();
|
const { addBreadcrumbSchema } = useStructuredData();
|
||||||
const posts = ref<BlogPost[]>([]);
|
const posts = ref<BlogPost[]>([]);
|
||||||
|
|
||||||
// Set SEO meta tags synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'Blog - Latest Articles on Spritesheet Generation',
|
title: 'Blog - Latest Articles on Spritesheet Generation',
|
||||||
description: 'Explore our latest articles about sprite sheet generation, game development, pixel art, and sprite animation techniques.',
|
description: 'Explore our latest articles about sprite sheet generation, game development, pixel art, and sprite animation techniques.',
|
||||||
@@ -18,7 +17,6 @@
|
|||||||
keywords: 'sprite sheet blog, game development articles, pixel art tutorials, sprite animation',
|
keywords: 'sprite sheet blog, game development articles, pixel art tutorials, sprite animation',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add breadcrumb synchronously
|
|
||||||
addBreadcrumbSchema([
|
addBreadcrumbSchema([
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'Blog', url: '/blog' },
|
{ name: 'Blog', url: '/blog' },
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
const { addBreadcrumbSchema } = useStructuredData();
|
const { addBreadcrumbSchema } = useStructuredData();
|
||||||
|
|
||||||
// Set SEO synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'Contact Us - Get in Touch',
|
title: 'Contact Us - Get in Touch',
|
||||||
description: "Contact the Spritesheet Generator team. Join our Discord community or report bugs and contribute on Gitea. We'd love to hear from you!",
|
description: "Contact the Spritesheet Generator team. Join our Discord community or report bugs and contribute on Gitea. We'd love to hear from you!",
|
||||||
|
|||||||
@@ -374,7 +374,6 @@
|
|||||||
toRef(settingsStore, 'manualCellHeight')
|
toRef(settingsStore, 'manualCellHeight')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Zoom Control
|
|
||||||
const {
|
const {
|
||||||
zoom,
|
zoom,
|
||||||
increase: zoomIn,
|
increase: zoomIn,
|
||||||
@@ -387,7 +386,6 @@
|
|||||||
initial: 1,
|
initial: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// View Options & Tools
|
|
||||||
const isMultiSelectMode = ref(false);
|
const isMultiSelectMode = ref(false);
|
||||||
const showActiveBorder = ref(true);
|
const showActiveBorder = ref(true);
|
||||||
const allowCellSwap = ref(false);
|
const allowCellSwap = ref(false);
|
||||||
@@ -397,7 +395,6 @@
|
|||||||
const customColor = ref('#ffffff');
|
const customColor = ref('#ffffff');
|
||||||
const isCustomMode = ref(false);
|
const isCustomMode = ref(false);
|
||||||
|
|
||||||
// Background Color Logic
|
|
||||||
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||||
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||||
|
|
||||||
@@ -460,7 +457,6 @@
|
|||||||
const editingLayerName = ref('');
|
const editingLayerName = ref('');
|
||||||
const layerNameInput = ref<HTMLInputElement | null>(null);
|
const layerNameInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// Upload Handlers
|
|
||||||
const handleSpritesUpload = async (files: File[]) => {
|
const handleSpritesUpload = async (files: File[]) => {
|
||||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||||
|
|
||||||
@@ -561,7 +557,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layer Editing
|
|
||||||
const startEditingLayer = (layerId: string, currentName: string) => {
|
const startEditingLayer = (layerId: string, currentName: string) => {
|
||||||
editingLayerId.value = layerId;
|
editingLayerId.value = layerId;
|
||||||
editingLayerName.value = currentName;
|
editingLayerName.value = currentName;
|
||||||
@@ -640,7 +635,6 @@
|
|||||||
const id = route.params.id as string;
|
const id = route.params.id as string;
|
||||||
if (id) {
|
if (id) {
|
||||||
if (projectStore.currentProject?.id !== id) {
|
if (projectStore.currentProject?.id !== id) {
|
||||||
// Only load if active project is different
|
|
||||||
await projectStore.loadProject(id);
|
await projectStore.loadProject(id);
|
||||||
if (projectStore.currentProject?.data) {
|
if (projectStore.currentProject?.data) {
|
||||||
await loadProjectData(projectStore.currentProject.data);
|
await loadProjectData(projectStore.currentProject.data);
|
||||||
@@ -660,8 +654,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If navigated to /editor without ID, maybe clear logic?
|
|
||||||
// We likely want to keep state if user created new project.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
const { addFAQSchema, addBreadcrumbSchema } = useStructuredData();
|
const { addFAQSchema, addBreadcrumbSchema } = useStructuredData();
|
||||||
|
|
||||||
// Set SEO synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'FAQ - Frequently Asked Questions',
|
title: 'FAQ - Frequently Asked Questions',
|
||||||
description: 'Find answers to common questions about the Spritesheet Generator. Learn about supported formats, export options, and how to use the tool effectively.',
|
description: 'Find answers to common questions about the Spritesheet Generator. Learn about supported formats, export options, and how to use the tool effectively.',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useHead } from '@vueuse/head';
|
|||||||
export function useHomeViewSEO() {
|
export function useHomeViewSEO() {
|
||||||
const { addOrganizationSchema, addWebSiteSchema } = useStructuredData();
|
const { addOrganizationSchema, addWebSiteSchema } = useStructuredData();
|
||||||
|
|
||||||
// Set page SEO synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'Spritesheet Generator - Create Game Spritesheets Online',
|
title: 'Spritesheet Generator - Create Game Spritesheets Online',
|
||||||
description: 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
|
description: 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
|
||||||
@@ -14,13 +13,10 @@ export function useHomeViewSEO() {
|
|||||||
keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools',
|
keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add organization schema
|
|
||||||
addOrganizationSchema();
|
addOrganizationSchema();
|
||||||
|
|
||||||
// Add website schema
|
|
||||||
addWebSiteSchema();
|
addWebSiteSchema();
|
||||||
|
|
||||||
// Add SoftwareApplication schema
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,14 +8,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">Page not found</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">Page not found</h1>
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mb-8">
|
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mb-8">Oops! The page you're looking for doesn't exist or has been moved.</p>
|
||||||
Oops! The page you're looking for doesn't exist or has been moved.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<router-link
|
<router-link to="/" class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-all shadow-lg hover:shadow-indigo-500/30 flex items-center gap-2">
|
||||||
to="/"
|
|
||||||
class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-all shadow-lg hover:shadow-indigo-500/30 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<i class="fas fa-home"></i>
|
<i class="fas fa-home"></i>
|
||||||
<span>Go home</span>
|
<span>Go home</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
const { addBreadcrumbSchema } = useStructuredData();
|
const { addBreadcrumbSchema } = useStructuredData();
|
||||||
|
|
||||||
// Set SEO synchronously
|
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'Privacy Policy - Your Data Protection',
|
title: 'Privacy Policy - Your Data Protection',
|
||||||
description: 'Read our privacy policy. Spritesheet Generator is a client-side tool that does not collect personal data or upload your images to our servers.',
|
description: 'Read our privacy policy. Spritesheet Generator is a client-side tool that does not collect personal data or upload your images to our servers.',
|
||||||
|
|||||||
@@ -169,7 +169,6 @@
|
|||||||
|
|
||||||
const data = spritesheetData.value;
|
const data = spritesheetData.value;
|
||||||
|
|
||||||
// Apply config settings
|
|
||||||
columns.value = data.config.columns;
|
columns.value = data.config.columns;
|
||||||
settingsStore.negativeSpacingEnabled = data.config.negativeSpacingEnabled;
|
settingsStore.negativeSpacingEnabled = data.config.negativeSpacingEnabled;
|
||||||
settingsStore.backgroundColor = data.config.backgroundColor;
|
settingsStore.backgroundColor = data.config.backgroundColor;
|
||||||
@@ -177,7 +176,6 @@
|
|||||||
settingsStore.manualCellWidth = data.config.manualCellWidth;
|
settingsStore.manualCellWidth = data.config.manualCellWidth;
|
||||||
settingsStore.manualCellHeight = data.config.manualCellHeight;
|
settingsStore.manualCellHeight = data.config.manualCellHeight;
|
||||||
|
|
||||||
// Load sprites into layers
|
|
||||||
const loadSprite = (spriteData: any): Promise<Sprite> =>
|
const loadSprite = (spriteData: any): Promise<Sprite> =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -199,7 +197,6 @@
|
|||||||
height: spriteData.height,
|
height: spriteData.height,
|
||||||
x: spriteData.x || 0,
|
x: spriteData.x || 0,
|
||||||
y: spriteData.y || 0,
|
y: spriteData.y || 0,
|
||||||
// Transformations are already baked into the base64 image
|
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
flipX: false,
|
flipX: false,
|
||||||
flipY: false,
|
flipY: false,
|
||||||
@@ -226,10 +223,8 @@
|
|||||||
activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id;
|
activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear current project so it's treated as new/unsaved
|
|
||||||
projectStore.currentProject = null;
|
projectStore.currentProject = null;
|
||||||
|
|
||||||
// Navigate to editor
|
|
||||||
router.push({ name: 'editor' });
|
router.push({ name: 'editor' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ interface WorkerResponse {
|
|||||||
backgroundColor: [number, number, number, number];
|
backgroundColor: [number, number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-allocate arrays for better performance
|
|
||||||
let maskBuffer: Uint8Array;
|
let maskBuffer: Uint8Array;
|
||||||
let visitedBuffer: Uint8Array;
|
let visitedBuffer: Uint8Array;
|
||||||
let stackBuffer: Int32Array;
|
let stackBuffer: Int32Array;
|
||||||
@@ -43,7 +42,6 @@ self.onmessage = function (e: MessageEvent<WorkerMessage>) {
|
|||||||
function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSize: number): { sprites: SpriteRegion[]; backgroundColor: [number, number, number, number] } {
|
function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSize: number): { sprites: SpriteRegion[]; backgroundColor: [number, number, number, number] } {
|
||||||
const { data, width, height } = imageData;
|
const { data, width, height } = imageData;
|
||||||
|
|
||||||
// Downsample for very large images
|
|
||||||
const shouldDownsample = width > maxSize || height > maxSize;
|
const shouldDownsample = width > maxSize || height > maxSize;
|
||||||
let processedData: Uint8ClampedArray;
|
let processedData: Uint8ClampedArray;
|
||||||
let processedWidth: number;
|
let processedWidth: number;
|
||||||
@@ -61,23 +59,17 @@ function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSi
|
|||||||
processedHeight = height;
|
processedHeight = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast background detection using histogram
|
|
||||||
const backgroundColor = fastBackgroundDetection(processedData, processedWidth, processedHeight);
|
const backgroundColor = fastBackgroundDetection(processedData, processedWidth, processedHeight);
|
||||||
|
|
||||||
// Create optimized mask
|
|
||||||
const mask = createOptimizedMask(processedData, processedWidth, processedHeight, backgroundColor, sensitivity);
|
const mask = createOptimizedMask(processedData, processedWidth, processedHeight, backgroundColor, sensitivity);
|
||||||
|
|
||||||
// Clean up mask with morphological operations
|
|
||||||
const cleanedMask = cleanUpMask(mask, processedWidth, processedHeight);
|
const cleanedMask = cleanUpMask(mask, processedWidth, processedHeight);
|
||||||
|
|
||||||
// Find connected components with optimized flood fill
|
|
||||||
const sprites = findOptimizedConnectedComponents(cleanedMask, processedWidth, processedHeight);
|
const sprites = findOptimizedConnectedComponents(cleanedMask, processedWidth, processedHeight);
|
||||||
|
|
||||||
// Filter noise
|
|
||||||
const minSpriteSize = Math.max(4, Math.floor(Math.min(processedWidth, processedHeight) / 100));
|
const minSpriteSize = Math.max(4, Math.floor(Math.min(processedWidth, processedHeight) / 100));
|
||||||
const filteredSprites = sprites.filter(sprite => sprite.pixelCount >= minSpriteSize);
|
const filteredSprites = sprites.filter(sprite => sprite.pixelCount >= minSpriteSize);
|
||||||
|
|
||||||
// Scale results back up if downsampled
|
|
||||||
const finalSprites = shouldDownsample
|
const finalSprites = shouldDownsample
|
||||||
? filteredSprites.map(sprite => ({
|
? filteredSprites.map(sprite => ({
|
||||||
x: Math.floor(sprite.x / scale),
|
x: Math.floor(sprite.x / scale),
|
||||||
@@ -88,7 +80,6 @@ function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSi
|
|||||||
}))
|
}))
|
||||||
: filteredSprites;
|
: filteredSprites;
|
||||||
|
|
||||||
// Convert background color back to original format
|
|
||||||
const finalBackgroundColor: [number, number, number, number] = [backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]];
|
const finalBackgroundColor: [number, number, number, number] = [backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -120,10 +111,8 @@ function downsampleImageData(data: Uint8ClampedArray, width: number, height: num
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height: number): Uint32Array {
|
function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height: number): Uint32Array {
|
||||||
// Enhanced background detection focusing on edges and corners
|
|
||||||
const colorCounts = new Map<string, number>();
|
const colorCounts = new Map<string, number>();
|
||||||
|
|
||||||
// Sample from corners (most likely to be background)
|
|
||||||
const cornerSamples = [
|
const cornerSamples = [
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[width - 1, 0],
|
[width - 1, 0],
|
||||||
@@ -131,26 +120,21 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height:
|
|||||||
[width - 1, height - 1],
|
[width - 1, height - 1],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sample from edges (also likely background)
|
|
||||||
const edgeSamples: [number, number][] = [];
|
const edgeSamples: [number, number][] = [];
|
||||||
const edgeStep = Math.max(1, Math.floor(Math.min(width, height) / 20));
|
const edgeStep = Math.max(1, Math.floor(Math.min(width, height) / 20));
|
||||||
|
|
||||||
// Top and bottom edges
|
|
||||||
for (let x = 0; x < width; x += edgeStep) {
|
for (let x = 0; x < width; x += edgeStep) {
|
||||||
edgeSamples.push([x, 0]);
|
edgeSamples.push([x, 0]);
|
||||||
edgeSamples.push([x, height - 1]);
|
edgeSamples.push([x, height - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left and right edges
|
|
||||||
for (let y = 0; y < height; y += edgeStep) {
|
for (let y = 0; y < height; y += edgeStep) {
|
||||||
edgeSamples.push([0, y]);
|
edgeSamples.push([0, y]);
|
||||||
edgeSamples.push([width - 1, y]);
|
edgeSamples.push([width - 1, y]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all samples
|
|
||||||
const allSamples = [...cornerSamples, ...edgeSamples];
|
const allSamples = [...cornerSamples, ...edgeSamples];
|
||||||
|
|
||||||
// Count colors with tolerance grouping
|
|
||||||
const tolerance = 15;
|
const tolerance = 15;
|
||||||
|
|
||||||
for (const [x, y] of allSamples) {
|
for (const [x, y] of allSamples) {
|
||||||
@@ -160,7 +144,6 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height:
|
|||||||
const b = data[idx + 2];
|
const b = data[idx + 2];
|
||||||
const a = data[idx + 3];
|
const a = data[idx + 3];
|
||||||
|
|
||||||
// Find existing similar color or create new entry
|
|
||||||
let matched = false;
|
let matched = false;
|
||||||
for (const [colorKey, count] of colorCounts.entries()) {
|
for (const [colorKey, count] of colorCounts.entries()) {
|
||||||
const [existingR, existingG, existingB, existingA] = colorKey.split(',').map(Number);
|
const [existingR, existingG, existingB, existingA] = colorKey.split(',').map(Number);
|
||||||
@@ -178,7 +161,6 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find most common color
|
|
||||||
let maxCount = 0;
|
let maxCount = 0;
|
||||||
let backgroundColor = [0, 0, 0, 0];
|
let backgroundColor = [0, 0, 0, 0];
|
||||||
|
|
||||||
@@ -195,13 +177,10 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height:
|
|||||||
function createOptimizedMask(data: Uint8ClampedArray, width: number, height: number, backgroundColor: Uint32Array, sensitivity: number): Uint8Array {
|
function createOptimizedMask(data: Uint8ClampedArray, width: number, height: number, backgroundColor: Uint32Array, sensitivity: number): Uint8Array {
|
||||||
const size = width * height;
|
const size = width * height;
|
||||||
|
|
||||||
// Reuse buffer if possible
|
|
||||||
if (!maskBuffer || maskBuffer.length < size) {
|
if (!maskBuffer || maskBuffer.length < size) {
|
||||||
maskBuffer = new Uint8Array(size);
|
maskBuffer = new Uint8Array(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map sensitivity (1-100) to more aggressive thresholds
|
|
||||||
// Higher sensitivity = stricter background matching (lower tolerance)
|
|
||||||
const colorTolerance = Math.round(50 - sensitivity * 0.45); // 50 down to 5
|
const colorTolerance = Math.round(50 - sensitivity * 0.45); // 50 down to 5
|
||||||
const alphaTolerance = Math.round(40 - sensitivity * 0.35); // 40 down to 5
|
const alphaTolerance = Math.round(40 - sensitivity * 0.35); // 40 down to 5
|
||||||
|
|
||||||
@@ -214,14 +193,12 @@ function createOptimizedMask(data: Uint8ClampedArray, width: number, height: num
|
|||||||
const b = data[idx + 2];
|
const b = data[idx + 2];
|
||||||
const a = data[idx + 3];
|
const a = data[idx + 3];
|
||||||
|
|
||||||
// Handle fully transparent pixels (common background case)
|
|
||||||
if (a < 10) {
|
if (a < 10) {
|
||||||
maskBuffer[i] = 0; // Treat as background
|
maskBuffer[i] = 0; // Treat as background
|
||||||
idx += 4;
|
idx += 4;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate color difference using Euclidean distance for better accuracy
|
|
||||||
const rDiff = r - bgR;
|
const rDiff = r - bgR;
|
||||||
const gDiff = g - bgG;
|
const gDiff = g - bgG;
|
||||||
const bDiff = b - bgB;
|
const bDiff = b - bgB;
|
||||||
@@ -230,7 +207,6 @@ function createOptimizedMask(data: Uint8ClampedArray, width: number, height: num
|
|||||||
const colorDistance = Math.sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
|
const colorDistance = Math.sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
|
||||||
const alphaDistance = Math.abs(aDiff);
|
const alphaDistance = Math.abs(aDiff);
|
||||||
|
|
||||||
// Pixel is foreground if it's significantly different from background
|
|
||||||
const isBackground = colorDistance <= colorTolerance && alphaDistance <= alphaTolerance;
|
const isBackground = colorDistance <= colorTolerance && alphaDistance <= alphaTolerance;
|
||||||
maskBuffer[i] = isBackground ? 0 : 1;
|
maskBuffer[i] = isBackground ? 0 : 1;
|
||||||
|
|
||||||
@@ -241,20 +217,12 @@ function createOptimizedMask(data: Uint8ClampedArray, width: number, height: num
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanUpMask(mask: Uint8Array, width: number, height: number): Uint8Array {
|
function cleanUpMask(mask: Uint8Array, width: number, height: number): Uint8Array {
|
||||||
// Simple morphological closing to fill small gaps in sprites
|
|
||||||
// and opening to remove small noise
|
|
||||||
|
|
||||||
const cleaned = new Uint8Array(mask.length);
|
const cleaned = new Uint8Array(mask.length);
|
||||||
|
|
||||||
// Erosion followed by dilation (opening) to remove small noise
|
|
||||||
// Then dilation followed by erosion (closing) to fill gaps
|
|
||||||
|
|
||||||
// Simple 3x3 kernel operations
|
|
||||||
for (let y = 1; y < height - 1; y++) {
|
for (let y = 1; y < height - 1; y++) {
|
||||||
for (let x = 1; x < width - 1; x++) {
|
for (let x = 1; x < width - 1; x++) {
|
||||||
const idx = y * width + x;
|
const idx = y * width + x;
|
||||||
|
|
||||||
// Count non-zero neighbors in 3x3 area
|
|
||||||
let neighbors = 0;
|
let neighbors = 0;
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
@@ -263,13 +231,10 @@ function cleanUpMask(mask: Uint8Array, width: number, height: number): Uint8Arra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use majority rule for cleaning
|
|
||||||
// If more than half the neighbors are foreground, make this foreground
|
|
||||||
cleaned[idx] = neighbors >= 5 ? 1 : 0;
|
cleaned[idx] = neighbors >= 5 ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy borders as-is
|
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
cleaned[x] = mask[x]; // Top row
|
cleaned[x] = mask[x]; // Top row
|
||||||
cleaned[(height - 1) * width + x] = mask[(height - 1) * width + x]; // Bottom row
|
cleaned[(height - 1) * width + x] = mask[(height - 1) * width + x]; // Bottom row
|
||||||
@@ -285,7 +250,6 @@ function cleanUpMask(mask: Uint8Array, width: number, height: number): Uint8Arra
|
|||||||
function findOptimizedConnectedComponents(mask: Uint8Array, width: number, height: number): SpriteRegion[] {
|
function findOptimizedConnectedComponents(mask: Uint8Array, width: number, height: number): SpriteRegion[] {
|
||||||
const size = width * height;
|
const size = width * height;
|
||||||
|
|
||||||
// Reuse buffers
|
|
||||||
if (!visitedBuffer || visitedBuffer.length < size) {
|
if (!visitedBuffer || visitedBuffer.length < size) {
|
||||||
visitedBuffer = new Uint8Array(size);
|
visitedBuffer = new Uint8Array(size);
|
||||||
}
|
}
|
||||||
@@ -293,7 +257,6 @@ function findOptimizedConnectedComponents(mask: Uint8Array, width: number, heigh
|
|||||||
stackBuffer = new Int32Array(size * 2);
|
stackBuffer = new Int32Array(size * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear visited array
|
|
||||||
visitedBuffer.fill(0);
|
visitedBuffer.fill(0);
|
||||||
|
|
||||||
const sprites: SpriteRegion[] = [];
|
const sprites: SpriteRegion[] = [];
|
||||||
@@ -337,13 +300,11 @@ function optimizedFloodFill(mask: Uint8Array, visited: Uint8Array, startX: numbe
|
|||||||
visited[idx] = 1;
|
visited[idx] = 1;
|
||||||
pixelCount++;
|
pixelCount++;
|
||||||
|
|
||||||
// Update bounding box
|
|
||||||
if (x < minX) minX = x;
|
if (x < minX) minX = x;
|
||||||
if (y < minY) minY = y;
|
if (y < minY) minY = y;
|
||||||
if (x > maxX) maxX = x;
|
if (x > maxX) maxX = x;
|
||||||
if (y > maxY) maxY = y;
|
if (y > maxY) maxY = y;
|
||||||
|
|
||||||
// Add neighbors (check bounds to avoid stack overflow)
|
|
||||||
if (x + 1 < width && !visited[idx + 1] && mask[idx + 1]) {
|
if (x + 1 < width && !visited[idx + 1] && mask[idx + 1]) {
|
||||||
stackBuffer[stackTop++] = x + 1;
|
stackBuffer[stackTop++] = x + 1;
|
||||||
stackBuffer[stackTop++] = y;
|
stackBuffer[stackTop++] = y;
|
||||||
@@ -364,7 +325,6 @@ function optimizedFloodFill(mask: Uint8Array, visited: Uint8Array, startX: numbe
|
|||||||
|
|
||||||
if (pixelCount === 0) return null;
|
if (pixelCount === 0) return null;
|
||||||
|
|
||||||
// Add padding
|
|
||||||
const padding = 1;
|
const padding = 1;
|
||||||
return {
|
return {
|
||||||
x: Math.max(0, minX - padding),
|
x: Math.max(0, minX - padding),
|
||||||
|
|||||||
Reference in New Issue
Block a user