Compare commits

..

No commits in common. "335bc0321872aada9c07d114d4a5ebd495ce5110" and "0a0838832ee03ba537b4da5044dce2ecc6560ceb" have entirely different histories.

5 changed files with 39 additions and 135 deletions

View File

@ -1,11 +1,5 @@
All notable changes to this project will be documented in this file.
## [1.0.1] - 2025-04-06
### Added
- 📝 Help modal with instructions and tips
- 🎨 Pixel perfect mode for better sprite alignment
## [1.0.0] - 2025-04-06
### Added

View File

@ -21,33 +21,11 @@
<div v-if="sprites.length > 0" class="mt-8">
<div class="flex flex-wrap items-center gap-6 mb-8">
<div class="flex items-center space-x-1">
<div class="flex items-center space-x-3">
<label for="columns" class="text-gray-700 font-medium">Columns:</label>
<input id="columns" type="number" v-model="columns" min="1" max="10" class="w-20 px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all" />
</div>
<!-- Add mass position buttons -->
<div class="flex items-center space-x-2">
<button @click="alignSprites('left')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Left">
<i class="fas fa-arrow-left"></i>
</button>
<button @click="alignSprites('center')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Center">
<i class="fas fa-arrows-left-right"></i>
</button>
<button @click="alignSprites('right')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Right">
<i class="fas fa-arrow-right"></i>
</button>
<button @click="alignSprites('top')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Top">
<i class="fas fa-arrow-up"></i>
</button>
<button @click="alignSprites('middle')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Middle">
<i class="fas fa-arrows-up-down"></i>
</button>
<button @click="alignSprites('bottom')" class="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" title="Align Bottom">
<i class="fas fa-arrow-down"></i>
</button>
</div>
<button @click="downloadSpritesheet" class="px-6 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
<i class="fas fa-download"></i>
<span>Download spritesheet</span>
@ -167,24 +145,24 @@
canvas.width = maxWidth * columns.value;
canvas.height = maxHeight * rows;
// Disable image smoothing for pixel-perfect rendering
// Apply pixel art optimization for the export canvas
ctx.imageSmoothingEnabled = false;
// Draw sprites with integer positions
// Draw sprites
sprites.value.forEach((sprite, index) => {
const col = index % columns.value;
const row = Math.floor(index / columns.value);
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
const cellX = col * maxWidth;
const cellY = row * maxHeight;
ctx.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
ctx.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
});
// Create download link with PNG format
// Create download link
const link = document.createElement('a');
link.download = 'spritesheet.png';
link.href = canvas.toDataURL('image/png', 1.0); // Use maximum quality
link.href = canvas.toDataURL('image/png');
link.click();
};
@ -298,8 +276,7 @@
}
// Process each sprite
// Replace current sprites with imported ones
sprites.value = await Promise.all(
const newSprites = await Promise.all(
jsonData.sprites.map(async (spriteData: any) => {
return new Promise<Sprite>(resolve => {
// Create image from base64
@ -334,50 +311,12 @@
});
})
);
// Replace current sprites with imported ones
sprites.value = newSprites;
} catch (error) {
console.error('Error importing JSON:', error);
alert('Failed to import JSON file. Please check the file format.');
}
};
// Add new alignment function
const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => {
if (sprites.value.length === 0) return;
// Find max dimensions for the current column layout
let maxWidth = 0;
let maxHeight = 0;
sprites.value.forEach(sprite => {
maxWidth = Math.max(maxWidth, sprite.width);
maxHeight = Math.max(maxHeight, sprite.height);
});
sprites.value = sprites.value.map((sprite, index) => {
let x = sprite.x;
let y = sprite.y;
switch (position) {
case 'left':
x = 0;
break;
case 'center':
x = (maxWidth - sprite.width) / 2;
break;
case 'right':
x = maxWidth - sprite.width;
break;
case 'top':
y = 0;
break;
case 'middle':
y = (maxHeight - sprite.height) / 2;
break;
case 'bottom':
y = maxHeight - sprite.height;
break;
}
return { ...sprite, x, y };
});
};
</script>

View File

@ -2,20 +2,19 @@
<div class="space-y-4">
<div class="flex justify-between items-center">
<div class="flex items-center">
<input id="pixel-perfect" type="checkbox" v-model="settingsStore.pixelPerfect" class="mr-2" @change="drawCanvas" />
<input id="pixel-perfect" type="checkbox" v-model="pixelPerfect" class="mr-2" @change="drawCanvas" />
<label for="pixel-perfect">Pixel perfect rendering (for pixel art)</label>
</div>
</div>
<div class="relative border border-gray-300 rounded-lg">
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"></canvas>
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
interface Sprite {
id: string;
@ -35,8 +34,8 @@
(e: 'updateSprite', id: string, x: number, y: number): void;
}>();
// Get settings from store
const settingsStore = useSettingsStore();
// Pixel art optimization
const pixelPerfect = ref(true);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
@ -195,14 +194,18 @@
// Clear canvas
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
// Disable image smoothing based on pixel perfect setting
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Set pixel art optimization
if (pixelPerfect.value) {
ctx.value.imageSmoothingEnabled = false;
} else {
ctx.value.imageSmoothingEnabled = true;
}
// Draw grid
ctx.value.strokeStyle = '#e5e7eb';
for (let col = 0; col < props.columns; col++) {
for (let row = 0; row < rows; row++) {
ctx.value.strokeRect(Math.floor(col * maxWidth), Math.floor(row * maxHeight), maxWidth, maxHeight);
ctx.value.strokeRect(col * maxWidth, row * maxHeight, maxWidth, maxHeight);
}
}
@ -211,11 +214,11 @@
const col = index % props.columns;
const row = Math.floor(index / props.columns);
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
const cellX = col * maxWidth;
const cellY = row * maxHeight;
// Draw sprite using integer positions
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
// Draw sprite
ctx.value?.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
});
};
@ -228,5 +231,4 @@
watch(() => props.sprites, drawCanvas, { deep: true });
watch(() => props.columns, drawCanvas);
watch(() => settingsStore.pixelPerfect, drawCanvas);
</script>

View File

@ -45,11 +45,6 @@
<input type="checkbox" v-model="showAllSprites" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300" />
<span class="text-sm whitespace-nowrap">Hide sprites</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300" @change="drawPreviewCanvas" />
<span class="text-sm whitespace-nowrap">Pixel perfect</span>
</label>
</div>
</div>
</div>
@ -89,7 +84,7 @@
<div v-for="(sprite, index) in props.sprites" :key="sprite.id" class="flex items-center gap-3 px-3 py-2 hover:bg-gray-50 cursor-pointer" @click="toggleHiddenFrame(index)">
<input type="checkbox" :checked="!hiddenFrames.includes(index)" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300" @click.stop @change="toggleHiddenFrame(index)" />
<div class="w-12 h-12 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
<img :src="sprite.img.src" class="max-w-full max-h-full object-contain" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" />
<img :src="sprite.img.src" class="max-w-full max-h-full object-contain" style="image-rendering: pixelated" />
</div>
<span class="text-sm">Frame {{ index + 1 }}</span>
</div>
@ -111,7 +106,7 @@
@touchend="stopDrag"
class="block"
:class="{ 'cursor-move': isDraggable }"
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left', ...(settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}) }"
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }"
>
</canvas>
</div>
@ -120,7 +115,6 @@
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
interface Sprite {
id: string;
@ -160,12 +154,12 @@
const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Computed properties
const totalFrames = computed(() => props.sprites.length);
// Add this after other refs
const hiddenFrames = ref<number[]>([]);
// Get settings from store
const settingsStore = useSettingsStore();
// Add these computed properties
const visibleFrames = computed(() => props.sprites.filter((_, index) => !hiddenFrames.value.includes(index)));
const visibleFramesCount = computed(() => visibleFrames.value.length);
@ -195,9 +189,6 @@
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Apply pixel art optimization consistently from store
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Set canvas size to just fit one sprite cell
previewCanvasRef.value.width = maxWidth;
previewCanvasRef.value.height = maxHeight;
@ -221,8 +212,8 @@
}
}
// Keep pixel art optimization consistent throughout all drawing operations
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Apply pixel art optimization
ctx.value.imageSmoothingEnabled = false;
// Draw all sprites with transparency if enabled
if (showAllSprites.value && props.sprites.length > 1) {
@ -338,17 +329,17 @@
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
const deltaX = Math.round(mouseX - dragStartX.value);
const deltaY = Math.round(mouseY - dragStartY.value);
const deltaX = mouseX - dragStartX.value;
const deltaY = mouseY - dragStartY.value;
const sprite = props.sprites[currentFrameIndex.value];
if (!sprite || sprite.id !== activeSpriteId.value) return;
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Calculate new position with constraints and round to integers
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
// Calculate new position with constraints
let newX = spritePosBeforeDrag.value.x + deltaX;
let newY = spritePosBeforeDrag.value.y + deltaY;
// Constrain movement within cell
newX = Math.max(0, Math.min(maxWidth - sprite.width, newX));
@ -404,7 +395,6 @@
watch(isDraggable, drawPreviewCanvas);
watch(showAllSprites, drawPreviewCanvas);
watch(hiddenFrames, drawPreviewCanvas);
watch(() => settingsStore.pixelPerfect, drawPreviewCanvas);
// Initial draw
if (props.sprites.length > 0) {

View File

@ -1,21 +0,0 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
const pixelPerfect = ref(true);
export const useSettingsStore = defineStore('settings', () => {
// Actions
function togglePixelPerfect() {
pixelPerfect.value = !pixelPerfect.value;
}
function setPixelPerfect(value: boolean) {
pixelPerfect.value = value;
}
return {
pixelPerfect,
togglePixelPerfect,
setPixelPerfect,
};
});