From 224d0d62fea312912787ec45c1637eae643d46fb Mon Sep 17 00:00:00 2001 From: root Date: Fri, 2 Jan 2026 22:16:23 +0100 Subject: [PATCH] [FEAT] Clean code --- src/components/Breadcrumbs.vue | 3 - src/components/FeedbackModal.vue | 2 - src/components/FileUploader.vue | 1 - src/components/HelpModal.vue | 2 - src/components/ShareModal.vue | 3 - src/components/SpriteCanvas.vue | 33 +-------- src/components/SpritePreview.vue | 29 -------- src/components/SpritesheetSplitter.vue | 7 -- src/components/auth/AuthModal.vue | 1 - src/components/layout/Navbar.vue | 2 - src/components/layout/navbar/NavbarLinks.vue | 36 ++++----- .../layout/navbar/NavbarMobileMenu.vue | 7 +- .../layout/navbar/NavbarSocials.vue | 4 +- src/components/project/NewProjectModal.vue | 1 - src/components/project/ProjectList.vue | 2 - src/components/utilities/Modal.vue | 16 ---- src/components/utilities/ToastItem.vue | 1 - src/components/utilities/Tooltip.vue | 6 -- src/composables/useAnimationFrames.ts | 13 ---- src/composables/useBackgroundStyles.ts | 2 - src/composables/useCanvas2D.ts | 6 -- src/composables/useDragSprite.ts | 74 ++++++++++++++----- src/composables/useExport.ts | 8 -- src/composables/useExportLayers.ts | 8 -- src/composables/useFileDrop.ts | 3 - src/composables/useGridMetrics.ts | 9 --- src/composables/useLayers.ts | 57 -------------- src/composables/useNegativeSpacing.ts | 2 - src/composables/useProjectManager.ts | 14 ---- src/composables/useSEO.ts | 5 -- src/composables/useShare.ts | 2 - src/composables/useSprites.ts | 1 - src/composables/useSpritesheetSplitter.ts | 5 -- src/composables/useStructuredData.ts | 6 -- src/composables/useZoom.ts | 3 - src/stores/useAuthStore.ts | 2 - src/stores/useSettingsStore.ts | 7 -- src/views/AboutUs.vue | 1 - src/views/BlogDetail.vue | 6 -- src/views/BlogOverview.vue | 2 - src/views/Contact.vue | 1 - src/views/EditorView.vue | 8 -- src/views/FAQ.vue | 1 - src/views/HomeView.seo.ts | 4 - src/views/NotFound.vue | 15 ++-- src/views/PrivacyPolicy.vue | 1 - src/views/ShareView.vue | 5 -- .../irregularSpriteDetection.worker.ts | 40 ---------- 48 files changed, 83 insertions(+), 384 deletions(-) diff --git a/src/components/Breadcrumbs.vue b/src/components/Breadcrumbs.vue index af56450..0e75296 100644 --- a/src/components/Breadcrumbs.vue +++ b/src/components/Breadcrumbs.vue @@ -13,7 +13,6 @@ const breadcrumbs = computed(() => { const items: BreadcrumbItem[] = [{ name: 'Home', path: '/' }]; - // Map route names to breadcrumb labels const routeLabels: Record = { 'blog-overview': 'Blog', 'blog-detail': 'Blog', @@ -27,10 +26,8 @@ const routeName = route.name.toString(); if (routeName === 'blog-detail') { - // For blog detail pages, add Blog first, then the post title 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'; items.push({ name: postTitle, path: route.path }); } else if (routeLabels[routeName]) { diff --git a/src/components/FeedbackModal.vue b/src/components/FeedbackModal.vue index c1fc49b..0cec9eb 100644 --- a/src/components/FeedbackModal.vue +++ b/src/components/FeedbackModal.vue @@ -86,12 +86,10 @@ } success.value = 'Thank you! Your feedback was sent.'; - // Reset fields name.value = ''; contact.value = ''; content.value = ''; - // Optionally close after short delay setTimeout(() => close(), 600); } catch (e: any) { console.error('Failed to send feedback:', e); diff --git a/src/components/FileUploader.vue b/src/components/FileUploader.vue index bd20cd0..89aae7a 100644 --- a/src/components/FileUploader.vue +++ b/src/components/FileUploader.vue @@ -63,7 +63,6 @@ if (input.files && input.files.length > 0) { const files = Array.from(input.files); emit('uploadSprites', files); - // Reset input value so uploading the same file again will trigger the event if (fileInput.value) fileInput.value.value = ''; } }; diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue index 0f20800..34e7576 100644 --- a/src/components/HelpModal.vue +++ b/src/components/HelpModal.vue @@ -170,13 +170,11 @@ const response = await fetch('/CHANGELOG.md'); const text = await response.text(); - // Configure marked options marked.setOptions({ gfm: true, // GitHub Flavored Markdown breaks: true, // Convert line breaks to
}); - // Convert markdown to HTML changelogHtml.value = await marked(text); } catch (error) { console.error('Failed to load changelog:', error); diff --git a/src/components/ShareModal.vue b/src/components/ShareModal.vue index 284881f..0ecc08b 100644 --- a/src/components/ShareModal.vue +++ b/src/components/ShareModal.vue @@ -87,7 +87,6 @@ copied.value = false; }, 2000); } catch { - // Fallback for older browsers const input = document.createElement('input'); input.value = shareUrl.value; document.body.appendChild(input); @@ -101,14 +100,12 @@ } }; - // Start sharing when modal opens watch( () => props.isOpen, isOpen => { if (isOpen) { performShare(); } else { - // Reset state when closing loading.value = false; shareUrl.value = ''; error.value = ''; diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index be9d901..ac2d924 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -225,7 +225,6 @@ (e: 'copySpriteToFrame', spriteId: string, targetLayerId: string, targetFrameIndex: number): void; }>(); - // Get settings from store const settingsStore = useSettingsStore(); const gridContainerRef = ref(null); @@ -242,6 +241,8 @@ }; }; + const selectedSpriteIds = ref>(new Set()); + const { isDragging, activeSpriteId, @@ -266,6 +267,7 @@ manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'), manualCellWidth: toRef(settingsStore, 'manualCellWidth'), manualCellHeight: toRef(settingsStore, 'manualCellHeight'), + selectedSpriteIds, getMousePosition, onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y), onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex), @@ -287,16 +289,13 @@ const contextMenuY = ref(0); const contextMenuIndex = ref(null); const contextMenuSpriteId = ref(null); - const selectedSpriteIds = ref>(new Set()); const replacingSpriteId = ref(null); const fileInput = ref(null); - // Copy to frame modal state const showCopyToFrameModal = ref(false); const copyTargetLayerId = ref(props.activeLayerId); const copySpriteId = ref(null); - // Clear selection when toggling multi-select mode watch( () => props.isMultiSelectMode, () => { @@ -304,7 +303,6 @@ } ); - // Use the new useGridMetrics composable for consistent calculations const { gridMetrics: gridMetricsRef, getCellPosition: getCellPositionHelper } = useGridMetrics({ layers: toRef(props, 'layers'), negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'), @@ -316,13 +314,11 @@ const gridMetrics = gridMetricsRef; 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)); return Math.max(1, Math.ceil(maxLen / props.columns)) * props.columns; }); 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 rows = Math.max(1, Math.ceil(maxLen / props.columns)); return { @@ -335,7 +331,6 @@ return getCellPositionHelper(index, props.columns, gridMetrics.value); }; - // Use the new useBackgroundStyles composable for consistent background styling const { backgroundColor: cellBackgroundColor, backgroundImage: cellBackgroundImage, @@ -353,17 +348,14 @@ const getCellBackgroundPosition = () => cellBackgroundPosition.value; const startDrag = (event: MouseEvent) => { - // If the click originated from an interactive element (button, link, input), ignore drag handling const target = event.target as HTMLElement; if (target && target.closest('button, a, input, select, textarea')) { return; } if (!gridContainerRef.value) return; - // Hide context menu if open showContextMenu.value = false; - // Handle right-click for context menu if ('button' in event && (event as MouseEvent).button === 2) { event.preventDefault(); const pos = getMousePosition(event, props.zoom); @@ -374,14 +366,11 @@ contextMenuSpriteId.value = clickedSprite?.id || null; 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)) { selectedSpriteIds.value.clear(); selectedSpriteIds.value.add(clickedSprite.id); } - // If it IS in the selection, keep the current selection (so we can apply action to all) } else { - // Right click on empty space selectedSpriteIds.value.clear(); } @@ -392,37 +381,27 @@ return; } - // Ignore non-left mouse buttons if ('button' in event && (event as MouseEvent).button !== 0) return; - // Handle selection logic for left click const pos = getMousePosition(event, props.zoom); if (pos) { const clickedSprite = findSpriteAtPosition(pos.x, pos.y); if (clickedSprite) { - // Selection logic with multi-select mode check if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) { - // Toggle selection - if (selectedSpriteIds.value.has(clickedSprite.id)) { - selectedSpriteIds.value.delete(clickedSprite.id); - } else { + if (!selectedSpriteIds.value.has(clickedSprite.id)) { selectedSpriteIds.value.add(clickedSprite.id); } } 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)) { selectedSpriteIds.value.clear(); selectedSpriteIds.value.add(clickedSprite.id); } } } else { - // Clicked on empty space selectedSpriteIds.value.clear(); } } - // Delegate to composable for actual drag handling dragStart(event); }; @@ -430,7 +409,6 @@ const latestEvent = ref(null); const drag = (event: MouseEvent) => { - // Store the latest event and schedule a single animation frame update latestEvent.value = event; if (!pendingDrag.value) { pendingDrag.value = true; @@ -481,7 +459,6 @@ const handleKeyDown = (event: KeyboardEvent) => { 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 (selectedSpriteIds.value.size > 0) { @@ -571,8 +548,6 @@ document.removeEventListener('mouseup', stopDrag); document.removeEventListener('keydown', handleKeyDown); }); - - // Watch for background color changes diff --git a/src/components/SpritePreview.vue b/src/components/SpritePreview.vue index eba2ecf..16f10f1 100644 --- a/src/components/SpritePreview.vue +++ b/src/components/SpritePreview.vue @@ -303,7 +303,6 @@ const previewContainerRef = ref(null); - // Get settings from store const settingsStore = useSettingsStore(); const { @@ -331,14 +330,12 @@ onDraw: () => {}, // No longer needed for canvas drawing }); - // Preview state const isDraggable = ref(false); const repositionAllLayers = ref(false); const arrowKeyMovement = ref(false); const showAllSprites = ref(false); const isDragOver = ref(false); - // Context menu state const showContextMenu = ref(false); const contextMenuX = ref(0); const contextMenuY = ref(0); @@ -347,11 +344,9 @@ const fileInput = ref(null); const replacingSpriteId = ref(null); - // Copy to frame modal state const showCopyToFrameModal = ref(false); const copyTargetLayerId = ref(props.activeLayerId); - // Drag and drop for new sprites const onDragOver = () => { isDragOver.value = true; }; @@ -373,10 +368,8 @@ }; const compositeFrames = computed(() => { - // Show frames from the active layer for the thumbnail list const activeLayer = props.layers.find(l => l.id === props.activeLayerId); if (!activeLayer) { - // Fallback to first visible layer if no active layer const v = getVisibleLayers(); const len = maxFrames(); const arr: Sprite[] = []; @@ -395,7 +388,6 @@ return layer.sprites[currentFrameIndex.value] || null; }); - // Use the new useGridMetrics composable for consistent calculations const { gridMetrics, getCellPosition: getCellPositionHelper } = useGridMetrics({ layers: toRef(props, 'layers'), negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'), @@ -404,19 +396,16 @@ manualCellHeight: toRef(settingsStore, 'manualCellHeight'), }); - // Helper function to get cell position (same as SpriteCanvas) const getCellPosition = (index: number) => { return getCellPositionHelper(index, props.columns, gridMetrics.value); }; - // Computed cell dimensions (for backward compatibility with existing code) const cellDimensions = computed(() => ({ cellWidth: gridMetrics.value.maxWidth, cellHeight: gridMetrics.value.maxHeight, negativeSpacing: gridMetrics.value.negativeSpacing, })); - // Use the new useBackgroundStyles composable for consistent background styling const { backgroundImage: previewBackgroundImage, backgroundSize: previewBackgroundSize, @@ -429,7 +418,6 @@ const getPreviewBackgroundImage = () => previewBackgroundImage.value; - // Dragging state const isDragging = ref(false); const activeSpriteId = ref(null); const activeLayerId = ref(null); @@ -438,7 +426,6 @@ const spritePosBeforeDrag = ref({ x: 0, y: 0 }); const allSpritesPosBeforeDrag = ref>(new Map()); - // Drag functionality const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => { if (!isDraggable.value || !previewContainerRef.value) return; @@ -455,7 +442,6 @@ dragStartX.value = mouseX; dragStartY.value = mouseY; - // Store initial positions for all sprites in this frame from all visible layers allSpritesPosBeforeDrag.value.clear(); const visibleLayers = getVisibleLayers(); visibleLayers.forEach(layer => { @@ -490,7 +476,6 @@ const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value; if (activeSpriteId.value === 'ALL_LAYERS') { - // Move all sprites in current frame from all visible layers const visibleLayers = getVisibleLayers(); visibleLayers.forEach(layer => { const sprite = layer.sprites[currentFrameIndex.value]; @@ -499,26 +484,21 @@ const originalPos = allSpritesPosBeforeDrag.value.get(sprite.id); if (!originalPos) return; - // Calculate new position with constraints let newX = Math.round(originalPos.x + deltaX); let newY = Math.round(originalPos.y + deltaY); - // Constrain movement within expanded cell newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX)); newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY)); emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY); }); } else { - // Move only the active layer sprite const activeSprite = props.layers.find(l => l.id === activeLayerId.value)?.sprites[currentFrameIndex.value]; 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 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)); newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY)); @@ -562,7 +542,6 @@ } }; - // Arrow key movement handler const handleKeyDown = (event: KeyboardEvent) => { if (!isDraggable.value || !arrowKeyMovement.value) return; @@ -593,7 +572,6 @@ const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value; if (repositionAllLayers.value) { - // Move all sprites in current frame from all visible layers const visibleLayers = getVisibleLayers(); visibleLayers.forEach(layer => { const sprite = layer.sprites[currentFrameIndex.value]; @@ -602,14 +580,12 @@ let newX = Math.round(sprite.x + deltaX); let newY = Math.round(sprite.y + deltaY); - // Constrain movement within expanded cell newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX)); newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY)); emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY); }); } else { - // Move only the active layer sprite const activeLayer = props.layers.find(l => l.id === props.activeLayerId); if (!activeLayer) return; @@ -619,7 +595,6 @@ let newX = Math.round(sprite.x + deltaX); let newY = Math.round(sprite.y + deltaY); - // Constrain movement within expanded cell newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX)); newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY)); @@ -627,7 +602,6 @@ } }; - // Lifecycle hooks onMounted(() => { window.addEventListener('keydown', handleKeyDown); }); @@ -637,8 +611,6 @@ window.removeEventListener('keydown', handleKeyDown); }); - // Watchers - most canvas-related watchers removed - // Keep layer watchers to ensure reactivity watch( () => props.layers, () => {}, @@ -650,7 +622,6 @@ ); watch(currentFrameIndex, () => {}); - // Context menu functions const openContextMenu = (event: MouseEvent, sprite: Sprite, layerId: string) => { event.preventDefault(); contextMenuSpriteId.value = sprite.id; diff --git a/src/components/SpritesheetSplitter.vue b/src/components/SpritesheetSplitter.vue index 17b51eb..4dbb235 100644 --- a/src/components/SpritesheetSplitter.vue +++ b/src/components/SpritesheetSplitter.vue @@ -115,7 +115,6 @@ const settingsStore = useSettingsStore(); const splitter = useSpritesheetSplitter(); - // State const detectionMode = ref('grid'); const cellWidth = ref(64); const cellHeight = ref(64); @@ -126,12 +125,10 @@ const isProcessing = ref(false); const imageElement = ref(null); - // Computed 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)); - // Load image and set initial cell size watch( () => props.imageUrl, url => { @@ -141,7 +138,6 @@ img.onload = () => { imageElement.value = img; - // Set suggested cell size const suggested = splitter.getSuggestedCellSize(img.width, img.height); cellWidth.value = suggested.width; cellHeight.value = suggested.height; @@ -153,14 +149,12 @@ { immediate: true } ); - // Regenerate preview when options change watch([detectionMode, cellWidth, cellHeight, sensitivity, removeEmpty, preserveCellSize], () => { if (imageElement.value) { generatePreview(); } }); - // Generate preview async function generatePreview() { if (!imageElement.value) return; @@ -190,7 +184,6 @@ } } - // Actions function cancel() { emit('close'); } diff --git a/src/components/auth/AuthModal.vue b/src/components/auth/AuthModal.vue index 1cb8519..2116ca4 100644 --- a/src/components/auth/AuthModal.vue +++ b/src/components/auth/AuthModal.vue @@ -92,7 +92,6 @@ close(); } catch (e: any) { error.value = e.message || 'An error occurred'; - // Better PB error handling if (e?.data?.message) error.value = e.data.message; } finally { loading.value = false; diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index 2928bbe..a40660d 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -74,7 +74,6 @@ import { useProjectManager } from '@/composables/useProjectManager'; import { useToast } from '@/composables/useToast'; - // Sub-components import NavbarLogo from './navbar/NavbarLogo.vue'; import NavbarLinks from './navbar/NavbarLinks.vue'; import NavbarProjectActions from './navbar/NavbarProjectActions.vue'; @@ -144,7 +143,6 @@ } catch (error) { addToast('Failed to save project', 'error'); console.error(error); - // Error handled in composable but kept here for toast } finally { isSaving.value = false; } diff --git a/src/components/layout/navbar/NavbarLinks.vue b/src/components/layout/navbar/NavbarLinks.vue index a3b1fed..e601d28 100644 --- a/src/components/layout/navbar/NavbarLinks.vue +++ b/src/components/layout/navbar/NavbarLinks.vue @@ -5,11 +5,7 @@ :key="link.path" :to="link.path" class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200" - :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' - ]" + :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']" > {{ link.name }} @@ -17,22 +13,22 @@ diff --git a/src/components/layout/navbar/NavbarMobileMenu.vue b/src/components/layout/navbar/NavbarMobileMenu.vue index 8afb875..6149270 100644 --- a/src/components/layout/navbar/NavbarMobileMenu.vue +++ b/src/components/layout/navbar/NavbarMobileMenu.vue @@ -39,11 +39,7 @@ :key="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="[ - 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' - ]" + :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']" @click="$emit('close')" > {{ link.name }} @@ -163,6 +159,5 @@ const handleLogout = () => { authStore.logout(); - // emit('close'); // Optional: close menu on logout? Maybe better to keep open so they can login again if they want. }; diff --git a/src/components/layout/navbar/NavbarSocials.vue b/src/components/layout/navbar/NavbarSocials.vue index e5b2e14..abd5ce0 100644 --- a/src/components/layout/navbar/NavbarSocials.vue +++ b/src/components/layout/navbar/NavbarSocials.vue @@ -15,9 +15,9 @@ - +
- + diff --git a/src/components/project/NewProjectModal.vue b/src/components/project/NewProjectModal.vue index a6a7c47..dcc2bd4 100644 --- a/src/components/project/NewProjectModal.vue +++ b/src/components/project/NewProjectModal.vue @@ -61,7 +61,6 @@ return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0; }); - // Reset to defaults when opened watch( () => props.isOpen, val => { diff --git a/src/components/project/ProjectList.vue b/src/components/project/ProjectList.vue index 96155f2..471e733 100644 --- a/src/components/project/ProjectList.vue +++ b/src/components/project/ProjectList.vue @@ -187,9 +187,7 @@ const sprites: any[] = []; if (!project.data || !project.data.layers) return sprites; - // Iterate through layers to find sprites 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.sprites && layer.sprites.length > 0) { diff --git a/src/components/utilities/Modal.vue b/src/components/utilities/Modal.vue index a0ae7c6..b7cfe0a 100644 --- a/src/components/utilities/Modal.vue +++ b/src/components/utilities/Modal.vue @@ -76,59 +76,47 @@ const startPos = ref({ x: 0, y: 0 }); const startSize = ref({ width: 0, height: 0 }); - // Add isFullScreen ref const isFullScreen = ref(false); const isMobile = ref(false); - // Add previous state storage for restoring from full screen const previousState = ref({ position: { x: 0, y: 0 }, size: { width: 0, height: 0 }, }); - // Check if device is mobile const checkMobile = () => { isMobile.value = window.innerWidth < 640; // sm breakpoint in Tailwind - // Auto fullscreen on mobile if (isMobile.value && !isFullScreen.value) { toggleFullScreen(); } else if (!isMobile.value && isFullScreen.value && autoFullScreened.value) { - // If we're no longer on mobile and were auto-fullscreened, exit fullscreen toggleFullScreen(); autoFullScreened.value = false; } }; - // Track if fullscreen was automatic (for mobile) const autoFullScreened = ref(false); - // Add toggleFullScreen function const toggleFullScreen = () => { if (!isFullScreen.value) { - // Store current state before going full screen previousState.value = { position: { ...position.value }, size: { ...size.value }, }; - // If toggling to fullscreen on mobile automatically, track it if (isMobile.value) { autoFullScreened.value = true; } } else { - // Restore previous state position.value = { ...previousState.value.position }; size.value = { ...previousState.value.size }; } isFullScreen.value = !isFullScreen.value; }; - // Unified start function for both drag and resize const startAction = (event: MouseEvent | TouchEvent, action: 'drag' | 'resize') => { if (isFullScreen.value) return; - // Extract the correct coordinates based on event type const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX; const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY; @@ -211,7 +199,6 @@ position.value = { x: 0, y: 0 }; }; - // Event handlers const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && props.isOpen) close(); }; @@ -223,7 +210,6 @@ } }; - // Add these new touch handling functions const handleTouchStart = (event: TouchEvent) => { if (isFullScreen.value) return; if (event.touches.length === 1) { @@ -237,7 +223,6 @@ handleMove(event); }; - // Lifecycle watch( () => props.isOpen, newValue => { @@ -250,7 +235,6 @@ window.addEventListener('resize', handleResize); window.addEventListener('resize', checkMobile); - // Initial check for mobile checkMobile(); if (props.isOpen) centerModal(); diff --git a/src/components/utilities/ToastItem.vue b/src/components/utilities/ToastItem.vue index 3cbe5ad..b1454fd 100644 --- a/src/components/utilities/ToastItem.vue +++ b/src/components/utilities/ToastItem.vue @@ -27,7 +27,6 @@ import type { Toast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast'; - // Simple functional components for icons using h (avoid runtime compiler) const SuccessIcon = { render: () => h( diff --git a/src/components/utilities/Tooltip.vue b/src/components/utilities/Tooltip.vue index ef5d829..0f1d742 100644 --- a/src/components/utilities/Tooltip.vue +++ b/src/components/utilities/Tooltip.vue @@ -42,30 +42,24 @@ let x = mouseX.value + offsetX; let y = mouseY.value + offsetY; - // Get tooltip dimensions (estimate if not mounted yet) const tooltipWidth = tooltipRef.value?.offsetWidth || 200; const tooltipHeight = tooltipRef.value?.offsetHeight || 30; - // Screen boundaries const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; - // Adjust horizontal position if too close to right edge if (x + tooltipWidth + padding > screenWidth) { x = mouseX.value - tooltipWidth - offsetX; } - // Adjust horizontal position if too close to left edge if (x < padding) { x = padding; } - // Adjust vertical position if too close to bottom edge if (y + tooltipHeight + padding > screenHeight) { y = mouseY.value - tooltipHeight - offsetY; } - // Adjust vertical position if too close to top edge if (y < padding) { y = padding; } diff --git a/src/composables/useAnimationFrames.ts b/src/composables/useAnimationFrames.ts index 526dda4..56022bc 100644 --- a/src/composables/useAnimationFrames.ts +++ b/src/composables/useAnimationFrames.ts @@ -9,7 +9,6 @@ export interface AnimationFramesOptions { export function useAnimationFrames(options: AnimationFramesOptions) { const { onDraw } = options; - // Convert sprites to a computed ref for reactivity const spritesRef = computed(() => { if (typeof options.sprites === 'function') { return options.sprites(); @@ -20,20 +19,16 @@ export function useAnimationFrames(options: AnimationFramesOptions) { return options.sprites; }); - // Helper to get sprites array const getSprites = () => spritesRef.value; - // State const currentFrameIndex = ref(0); const isPlaying = ref(false); const fps = ref(12); const hiddenFrames = ref([]); - // Animation internals const animationFrameId = ref(null); const lastFrameTime = ref(0); - // Computed properties for visible frames const visibleFrames = computed(() => getSprites().filter((_, index) => !hiddenFrames.value.includes(index))); const visibleFramesCount = computed(() => visibleFrames.value.length); @@ -46,7 +41,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) { const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1); - // Animation control const animateFrame = () => { const now = performance.now(); const elapsed = now - lastFrameTime.value; @@ -109,7 +103,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) { currentFrameIndex.value = sprites.indexOf(visibleFrames.value[index]); }; - // Frame visibility management const toggleHiddenFrame = (index: number) => { const sprites = getSprites(); const currentIndex = hiddenFrames.value.indexOf(index); @@ -117,7 +110,6 @@ export function useAnimationFrames(options: AnimationFramesOptions) { if (currentIndex === -1) { hiddenFrames.value.push(index); - // If hiding current frame, switch to next visible if (index === currentFrameIndex.value) { const nextVisible = sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i)); if (nextVisible !== -1) { @@ -140,32 +132,27 @@ export function useAnimationFrames(options: AnimationFramesOptions) { const sprites = getSprites(); hiddenFrames.value = sprites.map((_, index) => index); - // Keep at least one frame visible if (hiddenFrames.value.length > 0) { hiddenFrames.value.splice(currentFrameIndex.value, 1); } onDraw(); }; - // Cleanup on unmount onUnmounted(() => { stopAnimation(); }); return { - // State currentFrameIndex, isPlaying, fps, hiddenFrames, - // Computed visibleFrames, visibleFramesCount, visibleFrameIndex, visibleFrameNumber, - // Methods togglePlayback, nextFrame, previousFrame, diff --git a/src/composables/useBackgroundStyles.ts b/src/composables/useBackgroundStyles.ts index 3886763..1198cd7 100644 --- a/src/composables/useBackgroundStyles.ts +++ b/src/composables/useBackgroundStyles.ts @@ -18,7 +18,6 @@ export interface BackgroundStyles { * Handles transparent backgrounds with checkerboard patterns and dark mode. */ export function useBackgroundStyles(options: BackgroundStylesOptions) { - // Helper to get reactive values const getBackgroundColor = () => (typeof options.backgroundColor === 'string' ? options.backgroundColor : options.backgroundColor.value); const getCheckerboardEnabled = () => (typeof options.checkerboardEnabled === 'boolean' ? options.checkerboardEnabled : (options.checkerboardEnabled?.value ?? true)); const getDarkMode = () => (typeof options.darkMode === 'boolean' ? options.darkMode : (options.darkMode?.value ?? false)); @@ -40,7 +39,6 @@ export function useBackgroundStyles(options: BackgroundStylesOptions) { const darkMode = getDarkMode(); if (bg === 'transparent' && checkerboardEnabled) { - // Checkerboard pattern for transparent backgrounds (dark mode friendly) 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%)`; } diff --git a/src/composables/useCanvas2D.ts b/src/composables/useCanvas2D.ts index 3243f3e..d120e34 100644 --- a/src/composables/useCanvas2D.ts +++ b/src/composables/useCanvas2D.ts @@ -71,7 +71,6 @@ export function useCanvas2D(canvasRef: Ref, options?: } }; - // Helper to ensure integer positions for pixel-perfect rendering const ensureIntegerPositions = (items: T[]) => { items.forEach(item => { item.x = Math.floor(item.x); @@ -79,7 +78,6 @@ export function useCanvas2D(canvasRef: Ref, options?: }); }; - // Centralized force redraw handler const createForceRedrawHandler = (items: T[], drawCallback: () => void) => { return () => { ensureIntegerPositions(items); @@ -88,7 +86,6 @@ export function useCanvas2D(canvasRef: Ref, options?: }; }; - // Get mouse position relative to canvas, accounting for zoom const getMousePosition = (event: MouseEvent, zoom = 1): { x: number; y: number } | null => { if (!canvasRef.value) return null; @@ -102,7 +99,6 @@ export function useCanvas2D(canvasRef: Ref, options?: }; }; - // Helper to attach load/error listeners to images that aren't yet loaded const attachImageListeners = (sprites: Sprite[], onLoad: () => void, tracked: WeakSet) => { sprites.forEach(sprite => { const img = sprite.img as HTMLImageElement | undefined; @@ -116,14 +112,12 @@ export function useCanvas2D(canvasRef: Ref, options?: }); }; - // Fill cell background with selected color or transparent const fillCellBackground = (x: number, y: number, width: number, height: number) => { if (settingsStore.backgroundColor === 'transparent') return; const color = settingsStore.backgroundColor === 'custom' ? settingsStore.backgroundColor : settingsStore.backgroundColor; fillRect(x, y, width, height, color); }; - // Stroke grid with theme-aware color const strokeGridCell = (x: number, y: number, width: number, height: number) => { const color = settingsStore.darkMode ? '#4B5563' : '#e5e7eb'; strokeRect(x, y, width, height, color, 1); diff --git a/src/composables/useDragSprite.ts b/src/composables/useDragSprite.ts index f82d859..17c01a0 100644 --- a/src/composables/useDragSprite.ts +++ b/src/composables/useDragSprite.ts @@ -35,6 +35,7 @@ export interface DragSpriteOptions { manualCellSizeEnabled?: Ref; manualCellWidth?: Ref; manualCellHeight?: Ref; + selectedSpriteIds?: Ref> | ComputedRef>; getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null; onUpdateSprite: (id: string, x: number, y: number) => void; onUpdateSpriteCell?: (id: string, newIndex: number) => void; @@ -44,7 +45,6 @@ export interface DragSpriteOptions { export function useDragSprite(options: DragSpriteOptions) { const { getMousePosition, onUpdateSprite, onUpdateSpriteCell, onDraw } = options; - // Helper to get reactive values const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value); const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null); 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 getManualCellWidth = () => options.manualCellWidth?.value ?? 64; const getManualCellHeight = () => options.manualCellHeight?.value ?? 64; + const getSelectedSpriteIds = () => options.selectedSpriteIds?.value ?? new Set(); - // Drag state const isDragging = ref(false); const activeSpriteId = ref(null); const activeSpriteCellIndex = ref(null); @@ -65,11 +65,11 @@ export function useDragSprite(options: DragSpriteOptions) { const dragOffsetY = ref(0); const currentHoverCell = ref(null); - // Visual feedback + const initialSpritePositions = ref>(new Map()); + const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null); const highlightCell = ref(null); - // Use the new useGridMetrics composable for consistent calculations const gridMetricsComposable = useGridMetrics({ layers: options.layers, sprites: options.sprites, @@ -83,7 +83,6 @@ export function useDragSprite(options: DragSpriteOptions) { return gridMetricsComposable.calculateCellDimensions(); }; - // Computed sprite positions const spritePositions = computed(() => { const sprites = getSprites(); const columns = getColumns(); @@ -118,7 +117,6 @@ export function useDragSprite(options: DragSpriteOptions) { const col = Math.floor(x / maxWidth); const row = Math.floor(y / maxHeight); - // Allow dropping anywhere in the columns, assuming infinite rows effectively if (col >= 0 && col < columns && row >= 0) { const index = row * columns + col; return { col, row, index }; @@ -162,6 +160,19 @@ export function useDragSprite(options: DragSpriteOptions) { 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 { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions(); - // Use the sprite's current index in the array to calculate cell position const cellCol = spriteIndex % columns; const cellRow = Math.floor(spriteIndex / columns); const cellX = Math.round(cellCol * maxWidth); 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 newY = mouseY - cellY - negativeSpacing - dragOffsetY.value; - // The sprite can move within the full expanded cell area - // Allow negative values up to -negativeSpacing so sprite can fill the expanded area - 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 selectedIds = getSelectedSpriteIds(); + const isMultiDrag = selectedIds.has(activeSpriteId.value) && selectedIds.size > 1; - onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY); + 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 constrainedY = Math.floor(Math.max(-negativeSpacing, Math.min(maxHeight - negativeSpacing - sprites[spriteIndex].height, newY))); + + onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY); + } onDraw(); }; @@ -204,7 +236,10 @@ export function useDragSprite(options: DragSpriteOptions) { const hoverCell = findCellAtPosition(pos.x, pos.y); 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) { highlightCell.value = hoverCell; ghostSprite.value = { @@ -224,7 +259,10 @@ export function useDragSprite(options: DragSpriteOptions) { }; 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) { onUpdateSpriteCell(activeSpriteId.value, currentHoverCell.value.index); } @@ -237,11 +275,11 @@ export function useDragSprite(options: DragSpriteOptions) { currentHoverCell.value = null; highlightCell.value = null; ghostSprite.value = null; + initialSpritePositions.value.clear(); onDraw(); }; - // Touch event handlers const handleTouchStart = (event: TouchEvent) => { if (event.touches.length === 1) { const touch = event.touches[0]; @@ -271,14 +309,12 @@ export function useDragSprite(options: DragSpriteOptions) { }; return { - // State isDragging, activeSpriteId, ghostSprite, highlightCell, spritePositions, - // Methods startDrag, drag, stopDrag, diff --git a/src/composables/useExport.ts b/src/composables/useExport.ts index 5fbe1b3..94cf30d 100644 --- a/src/composables/useExport.ts +++ b/src/composables/useExport.ts @@ -9,7 +9,6 @@ import { getMaxDimensionsAcrossLayers } from './useLayers'; export const useExport = (sprites: Ref, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref, layers?: Ref) => { const getCellDimensions = () => { - // If manual cell size is enabled, use manual values if (manualCellSizeEnabled?.value) { return { cellWidth: manualCellWidth?.value ?? 64, @@ -18,11 +17,8 @@ export const useExport = (sprites: Ref, columns: Ref, 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); - // Calculate negative spacing from all layers' sprites for consistency const allSprites = layers?.value ? layers.value.flatMap(l => l.sprites) : sprites.value; const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value); @@ -50,7 +46,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative canvas.height = cellHeight * rows; ctx.imageSmoothingEnabled = false; - // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); @@ -151,7 +146,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative if (typeof jsonData.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = jsonData.manualCellWidth; if (typeof jsonData.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = jsonData.manualCellHeight; - // revoke existing blob urls if (sprites.value.length) { sprites.value.forEach(s => { if (s.url && s.url.startsWith('blob:')) { @@ -215,7 +209,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative sprites.value.forEach(sprite => { ctx.clearRect(0, 0, canvas.width, canvas.height); - // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); @@ -263,7 +256,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative sprites.value.forEach((sprite, index) => { ctx.clearRect(0, 0, canvas.width, canvas.height); - // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); diff --git a/src/composables/useExportLayers.ts b/src/composables/useExportLayers.ts index 4a46b35..71b9706 100644 --- a/src/composables/useExportLayers.ts +++ b/src/composables/useExportLayers.ts @@ -11,7 +11,6 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites); const getCellDimensions = () => { - // If manual cell size is enabled, use manual values if (manualCellSizeEnabled?.value) { return { cellWidth: manualCellWidth?.value ?? 64, @@ -20,10 +19,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, 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); - // Calculate negative spacing from ALL layers (not just visible) to keep canvas size stable const allSprites = layersRef.value.flatMap(l => l.sprites); const negativeSpacing = calculateNegativeSpacing(allSprites, negativeSpacingEnabled.value); return { @@ -35,7 +31,6 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => { ctx.clearRect(0, 0, cellWidth, cellHeight); - // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, cellWidth, cellHeight); @@ -78,7 +73,6 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n canvas.height = cellHeight * rows; ctx.imageSmoothingEnabled = false; - // Apply background color to entire canvas if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); @@ -186,7 +180,6 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n 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 }); } - // Ensure at least one layer with sprites is visible if (newLayers.length > 0 && !newLayers.some(l => l.visible && l.sprites.length > 0)) { const firstLayerWithSprites = newLayers.find(l => l.sprites.length > 0); if (firstLayerWithSprites) { @@ -194,7 +187,6 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n } } layersRef.value = newLayers; - // Set active layer to the first layer with sprites if (activeLayerId && newLayers.length > 0) { const firstWithSprites = newLayers.find(l => l.sprites.length > 0); activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id; diff --git a/src/composables/useFileDrop.ts b/src/composables/useFileDrop.ts index 5221006..46970eb 100644 --- a/src/composables/useFileDrop.ts +++ b/src/composables/useFileDrop.ts @@ -11,7 +11,6 @@ export interface FileDropOptions { export function useFileDrop(options: FileDropOptions) { const { onAddSprite, onAddSpriteWithResize } = options; - // Helper to get sprites array const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value); const isDragOver = ref(false); @@ -60,7 +59,6 @@ export function useFileDrop(options: FileDropOptions) { const sprites = getSprites(); const { maxWidth, maxHeight } = getMaxDimensions(sprites); - // Check if the dropped image is larger than current cells if (img.naturalWidth > maxWidth || img.naturalHeight > maxHeight) { onAddSpriteWithResize(file); } else { @@ -94,7 +92,6 @@ export function useFileDrop(options: FileDropOptions) { return; } - // Process each dropped file for (const file of files) { await processDroppedImage(file); } diff --git a/src/composables/useGridMetrics.ts b/src/composables/useGridMetrics.ts index e1dfeb1..80089cb 100644 --- a/src/composables/useGridMetrics.ts +++ b/src/composables/useGridMetrics.ts @@ -37,7 +37,6 @@ export interface GridMetricsOptions { * Provides a single source of truth for cell dimensions and positioning calculations. */ 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 getSprites = () => (options.sprites ? (Array.isArray(options.sprites) ? options.sprites : options.sprites.value) : []); 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 manualCellSizeEnabled = getManualCellSizeEnabled(); - // If manual cell size is enabled, use manual dimensions if (manualCellSizeEnabled) { return { 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 spritesToMeasure = layers ? layers.flatMap(l => l.sprites) : getSprites(); - // Calculate base dimensions from sprites const base = getMaxDimensions(spritesToMeasure); const baseMaxWidth = Math.max(1, base.maxWidth); const baseMaxHeight = Math.max(1, base.maxHeight); - // Calculate negative spacing const negativeSpacingEnabled = getNegativeSpacingEnabled(); const negativeSpacing = Math.round(calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled)); - // Add negative spacing to expand each cell return { maxWidth: Math.round(baseMaxWidth + negativeSpacing), maxHeight: Math.round(baseMaxHeight + negativeSpacing), @@ -116,10 +110,8 @@ export function useGridMetrics(options: GridMetricsOptions = {}) { const gridMetrics = computed(() => calculateCellDimensions()); return { - // Computed values gridMetrics, - // Methods calculateCellDimensions, getCellPosition, 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 sprites = isLayers ? (spritesOrLayers as Layer[]).flatMap(l => l.sprites) : (spritesOrLayers as Sprite[]); diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts index 79750fc..d8b78eb 100644 --- a/src/composables/useLayers.ts +++ b/src/composables/useLayers.ts @@ -70,16 +70,13 @@ export const useLayers = () => { const l = activeLayer.value; if (!l || !l.sprites.length) return; - // Determine the cell dimensions to align within let cellWidth: number; let cellHeight: number; if (settingsStore.manualCellSizeEnabled) { - // Use manual cell size (without negative spacing) cellWidth = settingsStore.manualCellWidth; cellHeight = settingsStore.manualCellHeight; } else { - // Use auto-calculated dimensions based on ALL visible layers (not just active layer) const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value); cellWidth = maxWidth; cellHeight = maxHeight; @@ -120,60 +117,26 @@ export const useLayers = () => { const next = [...l.sprites]; - // Remove the moving sprite first 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) { 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) { - // Perform Swap const target = l.sprites[newIndex]; const moving = l.sprites[currentIndex]; - // Clone array const newSprites = [...l.sprites]; newSprites[currentIndex] = target; newSprites[newIndex] = moving; l.sprites = newSprites; } else { - // Move to previously empty/non-existent cell const newSprites = [...l.sprites]; - // Remove from old pos const [moved] = newSprites.splice(currentIndex, 1); - // Pad while (newSprites.length < newIndex) { newSprites.push(createEmptySprite()); } - // Insert (or push if equal length) newSprites.splice(newIndex, 0, moved); l.sprites = newSprites; } @@ -197,7 +160,6 @@ export const useLayers = () => { const l = activeLayer.value; if (!l) return; - // Sort indices in descending order to avoid shift issues when splicing const indicesToRemove: number[] = []; ids.forEach(id => { const i = l.sprites.findIndex(s => s.id === id); @@ -291,21 +253,11 @@ export const useLayers = () => { const currentSprites = [...l.sprites]; if (typeof index === 'number') { - // If index is provided, insert there (padding if needed) while (currentSprites.length < index) { 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); } else { - // No index, append to end currentSprites.push(next); } l.sprites = currentSprites; @@ -353,7 +305,6 @@ export const useLayers = () => { }; const copySpriteToFrame = (spriteId: string, targetLayerId: string, targetFrameIndex: number) => { - // Find the source sprite in any layer let sourceSprite: Sprite | undefined; for (const layer of layers.value) { sourceSprite = layer.sprites.find(s => s.id === spriteId); @@ -362,11 +313,9 @@ export const useLayers = () => { if (!sourceSprite) return; - // Find target layer const targetLayer = layers.value.find(l => l.id === targetLayerId); if (!targetLayer) return; - // Create a deep copy of the sprite with a new ID const copiedSprite: Sprite = { id: crypto.randomUUID(), file: sourceSprite.file, @@ -381,14 +330,11 @@ export const useLayers = () => { flipY: sourceSprite.flipY, }; - // Expand the sprites array if necessary with empty placeholder sprites while (targetLayer.sprites.length < targetFrameIndex) { targetLayer.sprites.push(createEmptySprite()); } - // Replace or insert the sprite at the target index if (targetFrameIndex < targetLayer.sprites.length) { - // Replace existing sprite at this frame const old = targetLayer.sprites[targetFrameIndex]; if (old.url && old.url.startsWith('blob:')) { try { @@ -397,7 +343,6 @@ export const useLayers = () => { } targetLayer.sprites[targetFrameIndex] = copiedSprite; } else { - // Add at the end targetLayer.sprites.push(copiedSprite); } }; @@ -429,8 +374,6 @@ export const useLayers = () => { }; 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)); return getMaxDimensionsSingle(sprites); }; diff --git a/src/composables/useNegativeSpacing.ts b/src/composables/useNegativeSpacing.ts index 58865e4..a33474b 100644 --- a/src/composables/useNegativeSpacing.ts +++ b/src/composables/useNegativeSpacing.ts @@ -12,10 +12,8 @@ export function calculateNegativeSpacing(sprites: Sprite[], enabled: boolean): n const minWidth = Math.min(...sprites.map(s => s.width)); 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 availableHeight = maxHeight - minHeight; - // Use half to balance spacing equally on all sides return Math.floor(Math.min(availableWidth, availableHeight) / 2); } diff --git a/src/composables/useProjectManager.ts b/src/composables/useProjectManager.ts index a686485..9be8b0a 100644 --- a/src/composables/useProjectManager.ts +++ b/src/composables/useProjectManager.ts @@ -23,22 +23,17 @@ export const useProjectManager = () => { ); const createProject = (config: { width: number; height: number; columns: number; rows: number }) => { - // 1. Reset Settings settingsStore.setManualCellSize(config.width, config.height); settingsStore.manualCellSizeEnabled = true; - // 2. Reset Layers const newLayer = createEmptyLayer('Base'); layers.value = [newLayer]; activeLayerId.value = newLayer.id; - // 3. Set Columns columns.value = config.columns; - // 4. Reset Project Store projectStore.currentProject = null; - // 5. Navigate to Editor router.push('/editor'); }; @@ -60,12 +55,9 @@ export const useProjectManager = () => { const data = await generateProjectJSON(); if (projectStore.currentProject) { - // Update existing project (even if name changed) await projectStore.updateProject(projectStore.currentProject.id, name, data); } else { - // Create new project if none exists 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; if (newProject) { router.replace({ name: 'editor', params: { id: newProject.id } }); @@ -81,9 +73,7 @@ export const useProjectManager = () => { const saveAsProject = async (name: string) => { try { const data = await generateProjectJSON(); - // Always create new await projectStore.createProject(name, data); - // Navigate to new project if (projectStore.currentProject) { router.push({ name: 'editor', params: { id: projectStore.currentProject.id } }); } @@ -95,18 +85,14 @@ export const useProjectManager = () => { }; const closeProject = () => { - // Reset Layers const newLayer = createEmptyLayer('Base'); layers.value = [newLayer]; activeLayerId.value = newLayer.id; - // Reset columns columns.value = 4; - // Reset Project Store projectStore.currentProject = null; - // Navigate Home router.push('/'); }; diff --git a/src/composables/useSEO.ts b/src/composables/useSEO.ts index a9e9862..26f7195 100644 --- a/src/composables/useSEO.ts +++ b/src/composables/useSEO.ts @@ -24,12 +24,10 @@ export function useSEO(metadata: SEOMetaData) { const imageUrl = metadata.image ? `${SITE_URL}${metadata.image}` : `${SITE_URL}${DEFAULT_IMAGE}`; const metaTags: any[] = [ - // Primary Meta Tags { name: 'title', content: fullTitle }, { name: 'description', content: metadata.description }, { name: 'robots', content: 'index, follow' }, - // Open Graph / Facebook { property: 'og:type', content: metadata.type || 'website' }, { property: 'og:url', content: fullUrl }, { property: 'og:title', content: fullTitle }, @@ -37,7 +35,6 @@ export function useSEO(metadata: SEOMetaData) { { property: 'og:image', content: imageUrl }, { property: 'og:site_name', content: SITE_NAME }, - // Twitter { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:url', content: fullUrl }, { name: 'twitter:title', content: fullTitle }, @@ -45,7 +42,6 @@ export function useSEO(metadata: SEOMetaData) { { name: 'twitter:image', content: imageUrl }, ]; - // Add article-specific meta tags if (metadata.type === 'article') { if (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) { metaTags.push({ name: 'keywords', content: metadata.keywords }); } diff --git a/src/composables/useShare.ts b/src/composables/useShare.ts index 26aab45..e5bb7cb 100644 --- a/src/composables/useShare.ts +++ b/src/composables/useShare.ts @@ -59,7 +59,6 @@ export const buildShareUrl = (id: string): string => { * Share a spritesheet by uploading to PocketBase */ export const shareSpritesheet = async (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref): Promise => { - // Build layers data with base64 sprites (same format as exportSpritesheetJSON) const layersData = await Promise.all( layersRef.value.map(async layer => { const sprites = await Promise.all( @@ -80,7 +79,6 @@ export const shareSpritesheet = async (layersRef: Ref, columns: Ref { const sprites = ref([]); const columns = ref(4); - // Clamp and coerce columns to a safe range [1..10] watch(columns, val => { const num = typeof val === 'number' ? val : parseInt(String(val)); const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1; diff --git a/src/composables/useSpritesheetSplitter.ts b/src/composables/useSpritesheetSplitter.ts index da7ed92..5df8db2 100644 --- a/src/composables/useSpritesheetSplitter.ts +++ b/src/composables/useSpritesheetSplitter.ts @@ -49,10 +49,8 @@ export function useSpritesheetSplitter() { let height = cellHeight; if (preserveCellSize) { - // Keep full cell with transparent padding url = canvas.toDataURL('image/png'); } else { - // Crop to sprite bounds const bounds = getSpriteBounds(ctx, cellWidth, cellHeight); if (bounds) { x = bounds.x; @@ -94,7 +92,6 @@ export function useSpritesheetSplitter() { const imageData = ctx.getImageData(0, 0, img.width, img.height); - // Initialize worker lazily if (!worker.value) { try { 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.drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height); - // Remove background color removeBackground(spriteCtx, width, height, backgroundColor); const isEmpty = removeEmpty ? isCanvasEmpty(spriteCtx, width, height) : false; @@ -209,7 +205,6 @@ export function useSpritesheetSplitter() { if (!hasContent) return null; - // Add small padding const pad = 1; return { x: Math.max(0, minX - pad), diff --git a/src/composables/useStructuredData.ts b/src/composables/useStructuredData.ts index 068f5e2..8a3e345 100644 --- a/src/composables/useStructuredData.ts +++ b/src/composables/useStructuredData.ts @@ -24,7 +24,6 @@ export interface FAQItem { } export function useStructuredData() { - // Organization Schema const addOrganizationSchema = () => { const schema = { '@context': 'https://schema.org', @@ -46,7 +45,6 @@ export function useStructuredData() { }); }; - // WebSite Schema const addWebSiteSchema = () => { const schema = { '@context': 'https://schema.org', @@ -71,7 +69,6 @@ export function useStructuredData() { }); }; - // BlogPosting Schema const addBlogPostSchema = (post: BlogPostSchema) => { const schema = { '@context': 'https://schema.org', @@ -109,7 +106,6 @@ export function useStructuredData() { }); }; - // Breadcrumb Schema const addBreadcrumbSchema = (items: BreadcrumbItem[]) => { const schema = { '@context': 'https://schema.org', @@ -132,7 +128,6 @@ export function useStructuredData() { }); }; - // Blog List Schema const addBlogListSchema = (posts: BlogPostSchema[]) => { const schema = { '@context': 'https://schema.org', @@ -164,7 +159,6 @@ export function useStructuredData() { }); }; - // FAQ Schema const addFAQSchema = (faqs: FAQItem[]) => { const schema = { '@context': 'https://schema.org', diff --git a/src/composables/useZoom.ts b/src/composables/useZoom.ts index 40d29b8..44c653b 100644 --- a/src/composables/useZoom.ts +++ b/src/composables/useZoom.ts @@ -32,7 +32,6 @@ export function useZoom(options: ZoomOptions) { if (currentIndex < options.allowedValues.length - 1) { zoom.value = options.allowedValues[currentIndex + 1]; } else if (currentIndex === -1) { - // Find the nearest higher value const higher = options.allowedValues.find(v => v > zoom.value); if (higher !== undefined) { zoom.value = higher; @@ -49,7 +48,6 @@ export function useZoom(options: ZoomOptions) { if (currentIndex > 0) { zoom.value = options.allowedValues[currentIndex - 1]; } else if (currentIndex === -1) { - // Find the nearest lower value const lower = [...options.allowedValues].reverse().find(v => v < zoom.value); if (lower !== undefined) { zoom.value = lower; @@ -66,7 +64,6 @@ export function useZoom(options: ZoomOptions) { if (isStepOptions(options)) { zoom.value = Math.max(options.min, Math.min(options.max, value)); } else { - // Snap to nearest allowed value const nearest = options.allowedValues.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); zoom.value = nearest; } diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts index d259895..4ac129b 100644 --- a/src/stores/useAuthStore.ts +++ b/src/stores/useAuthStore.ts @@ -6,7 +6,6 @@ export const useAuthStore = defineStore('auth', () => { const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL); const user = ref(pb.authStore.model); - // Sync user state on change pb.authStore.onChange(() => { user.value = pb.authStore.model; }); @@ -21,7 +20,6 @@ export const useAuthStore = defineStore('auth', () => { password, passwordConfirm, }); - // Auto login after register await login(email, password); } diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts index 4788297..739b089 100644 --- a/src/stores/useSettingsStore.ts +++ b/src/stores/useSettingsStore.ts @@ -10,27 +10,21 @@ const manualCellWidth = ref(64); const manualCellHeight = ref(64); const checkerboardEnabled = ref(false); -// Initialize dark mode from localStorage or system preference if (typeof window !== 'undefined') { - // Check localStorage first const storedDarkMode = localStorage.getItem('darkMode'); if (storedDarkMode !== null) { darkMode.value = storedDarkMode === 'true'; } else { - // If not in localStorage, check system preference darkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches; } } export const useSettingsStore = defineStore('settings', () => { - // Watch for changes to update localStorage and apply class watch( darkMode, newValue => { - // Save to localStorage localStorage.setItem('darkMode', newValue.toString()); - // Apply or remove dark class on document if (newValue) { document.documentElement.classList.add('dark'); } else { @@ -40,7 +34,6 @@ export const useSettingsStore = defineStore('settings', () => { { immediate: true } ); - // Actions function togglePixelPerfect() { pixelPerfect.value = !pixelPerfect.value; } diff --git a/src/views/AboutUs.vue b/src/views/AboutUs.vue index de50c96..12863a2 100644 --- a/src/views/AboutUs.vue +++ b/src/views/AboutUs.vue @@ -4,7 +4,6 @@ const { addBreadcrumbSchema } = useStructuredData(); - // Set SEO synchronously useSEO({ 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.', diff --git a/src/views/BlogDetail.vue b/src/views/BlogDetail.vue index 06e9204..4ad1487 100644 --- a/src/views/BlogDetail.vue +++ b/src/views/BlogDetail.vue @@ -13,7 +13,6 @@ 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 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'); - // Dynamic meta tags using reactive computed values useHead({ title: pageTitle, meta: [ @@ -33,7 +31,6 @@ { name: 'keywords', content: keywords }, { name: 'robots', content: 'index, follow' }, - // Open Graph { property: 'og:type', content: 'article' }, { property: 'og:url', content: pageUrl }, { property: 'og:title', content: pageTitle }, @@ -43,7 +40,6 @@ { property: 'article:author', content: computed(() => post.value?.author || 'streetshadow') }, { property: 'article:published_time', content: computed(() => post.value?.date || '') }, - // Twitter { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:url', content: pageUrl }, { name: 'twitter:title', content: pageTitle }, @@ -54,7 +50,6 @@ script: computed(() => { const scripts = []; - // Breadcrumb schema scripts.push({ type: 'application/ld+json', children: JSON.stringify({ @@ -83,7 +78,6 @@ }), }); - // Blog post schema if (post.value) { scripts.push({ type: 'application/ld+json', diff --git a/src/views/BlogOverview.vue b/src/views/BlogOverview.vue index 9ab734b..7c2285c 100644 --- a/src/views/BlogOverview.vue +++ b/src/views/BlogOverview.vue @@ -9,7 +9,6 @@ const { addBreadcrumbSchema } = useStructuredData(); const posts = ref([]); - // Set SEO meta tags synchronously useSEO({ title: 'Blog - Latest Articles on Spritesheet Generation', 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', }); - // Add breadcrumb synchronously addBreadcrumbSchema([ { name: 'Home', url: '/' }, { name: 'Blog', url: '/blog' }, diff --git a/src/views/Contact.vue b/src/views/Contact.vue index 91185aa..7fd3a06 100644 --- a/src/views/Contact.vue +++ b/src/views/Contact.vue @@ -4,7 +4,6 @@ const { addBreadcrumbSchema } = useStructuredData(); - // Set SEO synchronously useSEO({ 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!", diff --git a/src/views/EditorView.vue b/src/views/EditorView.vue index 9c2df28..891a292 100644 --- a/src/views/EditorView.vue +++ b/src/views/EditorView.vue @@ -374,7 +374,6 @@ toRef(settingsStore, 'manualCellHeight') ); - // Zoom Control const { zoom, increase: zoomIn, @@ -387,7 +386,6 @@ initial: 1, }); - // View Options & Tools const isMultiSelectMode = ref(false); const showActiveBorder = ref(true); const allowCellSwap = ref(false); @@ -397,7 +395,6 @@ const customColor = ref('#ffffff'); const isCustomMode = ref(false); - // Background Color Logic 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); @@ -460,7 +457,6 @@ const editingLayerName = ref(''); const layerNameInput = ref(null); - // Upload Handlers const handleSpritesUpload = async (files: File[]) => { 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) => { editingLayerId.value = layerId; editingLayerName.value = currentName; @@ -640,7 +635,6 @@ const id = route.params.id as string; if (id) { if (projectStore.currentProject?.id !== id) { - // Only load if active project is different await projectStore.loadProject(id); if (projectStore.currentProject?.data) { await loadProjectData(projectStore.currentProject.data); @@ -660,8 +654,6 @@ } } } else { - // If navigated to /editor without ID, maybe clear logic? - // We likely want to keep state if user created new project. } } ); diff --git a/src/views/FAQ.vue b/src/views/FAQ.vue index ecc425c..77e7962 100644 --- a/src/views/FAQ.vue +++ b/src/views/FAQ.vue @@ -4,7 +4,6 @@ const { addFAQSchema, addBreadcrumbSchema } = useStructuredData(); - // Set SEO synchronously useSEO({ 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.', diff --git a/src/views/HomeView.seo.ts b/src/views/HomeView.seo.ts index 4bcbfd2..0c19791 100644 --- a/src/views/HomeView.seo.ts +++ b/src/views/HomeView.seo.ts @@ -5,7 +5,6 @@ import { useHead } from '@vueuse/head'; export function useHomeViewSEO() { const { addOrganizationSchema, addWebSiteSchema } = useStructuredData(); - // Set page SEO synchronously useSEO({ 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.', @@ -14,13 +13,10 @@ export function useHomeViewSEO() { keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools', }); - // Add organization schema addOrganizationSchema(); - // Add website schema addWebSiteSchema(); - // Add SoftwareApplication schema useHead({ script: [ { diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue index e5499df..db29f90 100644 --- a/src/views/NotFound.vue +++ b/src/views/NotFound.vue @@ -6,18 +6,13 @@
👻
- +

Page not found

-

- Oops! The page you're looking for doesn't exist or has been moved. -

- - +

Oops! The page you're looking for doesn't exist or has been moved.

+ + Go home - \ No newline at end of file + diff --git a/src/views/PrivacyPolicy.vue b/src/views/PrivacyPolicy.vue index 7c76a29..a77df7f 100644 --- a/src/views/PrivacyPolicy.vue +++ b/src/views/PrivacyPolicy.vue @@ -4,7 +4,6 @@ const { addBreadcrumbSchema } = useStructuredData(); - // Set SEO synchronously useSEO({ 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.', diff --git a/src/views/ShareView.vue b/src/views/ShareView.vue index 6ee0e9f..9b63407 100644 --- a/src/views/ShareView.vue +++ b/src/views/ShareView.vue @@ -169,7 +169,6 @@ const data = spritesheetData.value; - // Apply config settings columns.value = data.config.columns; settingsStore.negativeSpacingEnabled = data.config.negativeSpacingEnabled; settingsStore.backgroundColor = data.config.backgroundColor; @@ -177,7 +176,6 @@ settingsStore.manualCellWidth = data.config.manualCellWidth; settingsStore.manualCellHeight = data.config.manualCellHeight; - // Load sprites into layers const loadSprite = (spriteData: any): Promise => new Promise(resolve => { const img = new Image(); @@ -199,7 +197,6 @@ height: spriteData.height, x: spriteData.x || 0, y: spriteData.y || 0, - // Transformations are already baked into the base64 image rotation: 0, flipX: false, flipY: false, @@ -226,10 +223,8 @@ activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id; } - // Clear current project so it's treated as new/unsaved projectStore.currentProject = null; - // Navigate to editor router.push({ name: 'editor' }); }; diff --git a/src/workers/irregularSpriteDetection.worker.ts b/src/workers/irregularSpriteDetection.worker.ts index b9129ae..25bff37 100644 --- a/src/workers/irregularSpriteDetection.worker.ts +++ b/src/workers/irregularSpriteDetection.worker.ts @@ -19,7 +19,6 @@ interface WorkerResponse { backgroundColor: [number, number, number, number]; } -// Pre-allocate arrays for better performance let maskBuffer: Uint8Array; let visitedBuffer: Uint8Array; let stackBuffer: Int32Array; @@ -43,7 +42,6 @@ self.onmessage = function (e: MessageEvent) { function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSize: number): { sprites: SpriteRegion[]; backgroundColor: [number, number, number, number] } { const { data, width, height } = imageData; - // Downsample for very large images const shouldDownsample = width > maxSize || height > maxSize; let processedData: Uint8ClampedArray; let processedWidth: number; @@ -61,23 +59,17 @@ function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSi processedHeight = height; } - // Fast background detection using histogram const backgroundColor = fastBackgroundDetection(processedData, processedWidth, processedHeight); - // Create optimized mask const mask = createOptimizedMask(processedData, processedWidth, processedHeight, backgroundColor, sensitivity); - // Clean up mask with morphological operations const cleanedMask = cleanUpMask(mask, processedWidth, processedHeight); - // Find connected components with optimized flood fill const sprites = findOptimizedConnectedComponents(cleanedMask, processedWidth, processedHeight); - // Filter noise const minSpriteSize = Math.max(4, Math.floor(Math.min(processedWidth, processedHeight) / 100)); const filteredSprites = sprites.filter(sprite => sprite.pixelCount >= minSpriteSize); - // Scale results back up if downsampled const finalSprites = shouldDownsample ? filteredSprites.map(sprite => ({ x: Math.floor(sprite.x / scale), @@ -88,7 +80,6 @@ function detectIrregularSprites(imageData: ImageData, sensitivity: number, maxSi })) : filteredSprites; - // Convert background color back to original format const finalBackgroundColor: [number, number, number, number] = [backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]]; return { @@ -120,10 +111,8 @@ function downsampleImageData(data: Uint8ClampedArray, width: number, height: num } function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height: number): Uint32Array { - // Enhanced background detection focusing on edges and corners const colorCounts = new Map(); - // Sample from corners (most likely to be background) const cornerSamples = [ [0, 0], [width - 1, 0], @@ -131,26 +120,21 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height: [width - 1, height - 1], ]; - // Sample from edges (also likely background) const edgeSamples: [number, number][] = []; const edgeStep = Math.max(1, Math.floor(Math.min(width, height) / 20)); - // Top and bottom edges for (let x = 0; x < width; x += edgeStep) { edgeSamples.push([x, 0]); edgeSamples.push([x, height - 1]); } - // Left and right edges for (let y = 0; y < height; y += edgeStep) { edgeSamples.push([0, y]); edgeSamples.push([width - 1, y]); } - // Collect all samples const allSamples = [...cornerSamples, ...edgeSamples]; - // Count colors with tolerance grouping const tolerance = 15; for (const [x, y] of allSamples) { @@ -160,7 +144,6 @@ function fastBackgroundDetection(data: Uint8ClampedArray, width: number, height: const b = data[idx + 2]; const a = data[idx + 3]; - // Find existing similar color or create new entry let matched = false; for (const [colorKey, count] of colorCounts.entries()) { 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 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 { const size = width * height; - // Reuse buffer if possible if (!maskBuffer || maskBuffer.length < 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 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 a = data[idx + 3]; - // Handle fully transparent pixels (common background case) if (a < 10) { maskBuffer[i] = 0; // Treat as background idx += 4; continue; } - // Calculate color difference using Euclidean distance for better accuracy const rDiff = r - bgR; const gDiff = g - bgG; 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 alphaDistance = Math.abs(aDiff); - // Pixel is foreground if it's significantly different from background const isBackground = colorDistance <= colorTolerance && alphaDistance <= alphaTolerance; 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 { - // Simple morphological closing to fill small gaps in sprites - // and opening to remove small noise - 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 x = 1; x < width - 1; x++) { const idx = y * width + x; - // Count non-zero neighbors in 3x3 area let neighbors = 0; for (let dy = -1; dy <= 1; dy++) { 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; } } - // Copy borders as-is for (let x = 0; x < width; x++) { cleaned[x] = mask[x]; // Top 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[] { const size = width * height; - // Reuse buffers if (!visitedBuffer || visitedBuffer.length < size) { visitedBuffer = new Uint8Array(size); } @@ -293,7 +257,6 @@ function findOptimizedConnectedComponents(mask: Uint8Array, width: number, heigh stackBuffer = new Int32Array(size * 2); } - // Clear visited array visitedBuffer.fill(0); const sprites: SpriteRegion[] = []; @@ -337,13 +300,11 @@ function optimizedFloodFill(mask: Uint8Array, visited: Uint8Array, startX: numbe visited[idx] = 1; pixelCount++; - // Update bounding box if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; - // Add neighbors (check bounds to avoid stack overflow) if (x + 1 < width && !visited[idx + 1] && mask[idx + 1]) { stackBuffer[stackTop++] = x + 1; stackBuffer[stackTop++] = y; @@ -364,7 +325,6 @@ function optimizedFloodFill(mask: Uint8Array, visited: Uint8Array, startX: numbe if (pixelCount === 0) return null; - // Add padding const padding = 1; return { x: Math.max(0, minX - padding),