[FEAT] Clean code

This commit is contained in:
2026-01-02 22:16:23 +01:00
parent 647083d5b9
commit 224d0d62fe
48 changed files with 83 additions and 384 deletions

View File

@@ -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]) {

View File

@@ -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);

View File

@@ -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 = '';
} }
}; };

View File

@@ -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);

View File

@@ -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 = '';

View File

@@ -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>

View File

@@ -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;

View File

@@ -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');
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 => {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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(

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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%)`;
} }

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
} }

View 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[]);

View File

@@ -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);
}; };

View File

@@ -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);
} }

View File

@@ -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('/');
}; };

View File

@@ -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 });
} }

View File

@@ -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,

View File

@@ -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;

View File

@@ -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),

View File

@@ -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',

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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.',

View File

@@ -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',

View File

@@ -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' },

View File

@@ -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!",

View File

@@ -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.
} }
} }
); );

View File

@@ -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.',

View File

@@ -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: [
{ {

View File

@@ -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>

View File

@@ -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.',

View File

@@ -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' });
}; };

View File

@@ -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),