Compare commits
No commits in common. "335bc0321872aada9c07d114d4a5ebd495ce5110" and "0a0838832ee03ba537b4da5044dce2ecc6560ceb" have entirely different histories.
335bc03218
...
0a0838832e
@ -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
|
||||
|
85
src/App.vue
85
src/App.vue
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user