Compare commits

...

4 Commits

Author SHA1 Message Date
376bf00ad4 Tooltips, donation, new help window 2025-04-03 04:38:23 +02:00
8fdb0dbae3 Pixel art fixes 2025-04-03 04:23:02 +02:00
1c7a7db299 MainContent preview border 2025-04-03 04:19:12 +02:00
5218669744 Border 2025-04-03 04:16:03 +02:00
10 changed files with 469 additions and 55 deletions

View File

@ -38,6 +38,7 @@
<preview-modal ref="previewModalRef" />
<settings-modal />
<sprites-modal />
<help-modal />
<notification />
<help-button @show-help="showHelpModal" />
</div>
@ -51,6 +52,7 @@
import PreviewModal from './components/PreviewModal.vue';
import SettingsModal from './components/SettingsModal.vue';
import SpritesModal from './components/SpritesModal.vue';
import HelpModal from './components/HelpModal.vue';
import Navigation from './components/Navigation.vue';
import Notification from './components/Notification.vue';
import HelpButton from './components/HelpButton.vue';
@ -71,6 +73,7 @@
);
const showHelpModal = () => {
alert('Keyboard shortcuts:\n\n' + 'Shift + Drag: Fine-tune sprite position\n' + 'Space: Play/Pause animation\n' + 'Esc: Close preview modal\n' + 'Arrow Keys: Navigate frames when paused');
// Open the help modal instead of showing an alert
store.isHelpModalOpen.value = true;
};
</script>

View File

@ -23,9 +23,11 @@
</button>
<!-- Help button -->
<button @click="emit('toggleHelp')" class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors" title="Keyboard shortcuts">
<i class="fas fa-keyboard"></i>
</button>
<tooltip text="Keyboard Shortcuts" position="bottom">
<button @click="openHelpModal" class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors">
<i class="fas fa-keyboard"></i>
</button>
</tooltip>
</div>
</header>
</template>
@ -33,6 +35,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
import Tooltip from './Tooltip.vue';
const store = useSpritesheetStore();
const zoomLevel = computed(() => store.zoomLevel.value);
@ -41,6 +44,10 @@
(e: 'toggleHelp'): void;
}>();
const openHelpModal = () => {
store.isHelpModalOpen.value = true;
};
// Expose store methods directly
const { zoomIn, zoomOut } = store;
</script>

View File

@ -1,11 +1,22 @@
<template>
<button @click="emit('showHelp')" class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40">
<i class="fas fa-question"></i>
</button>
<tooltip text="Help & Support" position="left">
<button @click="openHelpModal" class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40">
<i class="fas fa-question"></i>
</button>
</tooltip>
</template>
<script setup lang="ts">
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
import Tooltip from './Tooltip.vue';
const store = useSpritesheetStore();
const emit = defineEmits<{
showHelp: [];
}>();
const openHelpModal = () => {
store.isHelpModalOpen.value = true;
emit('showHelp');
};
</script>

View File

@ -0,0 +1,150 @@
<template>
<!-- Help Modal -->
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
<!-- Modal backdrop with semi-transparent background -->
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
<!-- Modal content -->
<div class="bg-gray-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-auto shadow-lg pointer-events-auto relative">
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
<div class="flex items-center gap-2 text-lg font-semibold">
<i class="fas fa-question-circle text-blue-500"></i>
<span>Help</span>
</div>
<button @click="closeModal" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
<!-- Help content tabs -->
<div class="mb-6">
<div class="flex border-b border-gray-600 mb-4">
<button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" class="px-4 py-2 font-medium text-sm transition-colors" :class="activeTab === tab.id ? 'text-blue-500 border-b-2 border-blue-500' : 'text-gray-400 hover:text-gray-200'">
<i :class="tab.icon" class="mr-2"></i>{{ tab.label }}
</button>
</div>
<!-- Keyboard Shortcuts Tab -->
<div v-if="activeTab === 'shortcuts'" class="space-y-4">
<h3 class="text-lg font-semibold mb-2">Keyboard Shortcuts</h3>
<div class="bg-gray-700 rounded-lg p-4">
<div class="grid grid-cols-1 gap-4">
<div v-for="(shortcut, index) in shortcuts" :key="index" class="flex justify-between">
<span class="font-medium">{{ shortcut.key }}</span>
<span class="text-gray-300">{{ shortcut.description }}</span>
</div>
</div>
</div>
</div>
<!-- Usage Guide Tab -->
<div v-if="activeTab === 'guide'" class="space-y-4">
<h3 class="text-lg font-semibold mb-2">Usage Guide</h3>
<div class="bg-gray-700 rounded-lg p-4 space-y-3">
<p>This tool helps you create spritesheets from individual sprite images.</p>
<ol class="list-decimal pl-5 space-y-2">
<li>Upload your sprite images using the upload area</li>
<li>Arrange sprites by dragging them to desired positions</li>
<li>Adjust settings like column count in the Settings panel</li>
<li>Preview animation by clicking the Play button</li>
<li>Download your spritesheet when ready</li>
</ol>
<p class="bg-blue-600 p-2 rounded">Questions? Add me on discord: <b>nu11ed</b></p>
</div>
</div>
<!-- Donation Tab -->
<div v-if="activeTab === 'donate'" class="space-y-4">
<h3 class="text-lg font-semibold mb-2">Buy me a coffee</h3>
<p class="text-gray-300 mb-4">If you find this tool useful, please consider supporting its development with a donation.</p>
<div class="space-y-4">
<div v-for="wallet in wallets" :key="wallet.type" class="bg-gray-700 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<i :class="wallet.icon" class="text-xl mr-2 bg-gray-800 p-1 px-2 rounded-lg" :style="{ color: wallet.color }"></i>
<span class="font-medium">{{ wallet.name }}</span>
</div>
<button @click="copyToClipboard(wallet.address)" class="text-xs bg-gray-600 hover:bg-gray-500 px-2 py-1 rounded transition-colors">Copy</button>
</div>
<div class="bg-gray-800 p-2 rounded text-xs font-mono break-all">
{{ wallet.address }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
const store = useSpritesheetStore();
const isModalOpen = computed(() => store.isHelpModalOpen.value);
const activeTab = ref('shortcuts');
const tabs = [
{ id: 'shortcuts', label: 'Shortcuts', icon: 'fas fa-keyboard' },
{ id: 'guide', label: 'Guide', icon: 'fas fa-book' },
{ id: 'donate', label: 'Donate', icon: 'fas fa-heart' },
];
const shortcuts = [
{ key: 'Shift + Drag', description: 'Fine-tune sprite position' },
{ key: 'Space', description: 'Play/Pause animation' },
{ key: 'Esc', description: 'Close preview modal' },
{ key: 'Arrow Keys', description: 'Navigate frames when paused' },
];
const wallets = [
{
type: 'paypal',
name: 'PayPal',
address: 'https://www.paypal.com/paypalme/DennisPostma298',
icon: 'fab fa-paypal',
color: '#00457c',
},
{
type: 'btc',
name: 'Bitcoin native segwit (BTC)',
address: 'bc1ql2a3nxnhfwft7qex0cclj5ar2lfsslvs0aygeq',
icon: 'fab fa-bitcoin',
color: '#f7931a',
},
{
type: 'eth',
name: 'Ethereum (ETH)',
address: '0x30843c72DF6E9A9226d967bf2403602f1C2aB67b',
icon: 'fab fa-ethereum',
color: '#627eea',
},
{
type: 'ltc',
name: 'Litecoin native segwit (LTC)',
address: 'ltc1qdkn46hpt39ppmhk25ed2eycu7m2pj5cdzuxw84',
icon: 'fas fa-litecoin-sign',
color: '#345d9d',
},
];
const closeModal = () => {
store.isHelpModalOpen.value = false;
};
const copyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
store.showNotification('Address copied to clipboard');
})
.catch(err => {
console.error('Failed to copy: ', err);
store.showNotification('Failed to copy address', 'error');
});
};
</script>

View File

@ -18,6 +18,7 @@
:style="{
transform: `scale(${store.zoomLevel.value})`,
transformOrigin: 'top left',
imageRendering: 'pixelated', // Keep pixel art sharp when zooming
}"
></canvas>
</div>
@ -34,6 +35,9 @@
const canvasEl = ref<HTMLCanvasElement | null>(null);
const containerEl = ref<HTMLDivElement | null>(null);
// Access the preview border settings
const previewBorder = computed(() => store.previewBorder);
// Panning state
const isPanning = ref(false);
const isAltPressed = ref(false);
@ -85,6 +89,17 @@
}
);
// Watch for changes in border settings to update the canvas
watch(
() => previewBorder.value,
() => {
if (store.sprites.value.length > 0) {
store.renderSpritesheetPreview();
}
},
{ deep: true }
);
const setupCheckerboardPattern = () => {
if (!canvasEl.value) return;

View File

@ -8,40 +8,44 @@
<!-- Navigation Items -->
<div class="flex flex-col gap-6 items-center">
<!-- Dashboard/Home -->
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'dashboard' }" @click="setActiveSection('dashboard')" title="Dashboard">
<i class="fas fa-home"></i>
</button>
<tooltip text="Dashboard" position="right">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'dashboard' }" @click="setActiveSection('dashboard')">
<i class="fas fa-home"></i>
</button>
</tooltip>
<!-- Sprites -->
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors relative" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'sprites' }" @click="openSpritesModal" title="Sprites">
<i class="fas fa-images"></i>
<span v-if="sprites.length > 0" class="absolute -top-1 -right-1 bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{{ sprites.length }}
</span>
</button>
<tooltip text="Manage sprites" position="right">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors relative" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'sprites' }" @click="openSpritesModal">
<i class="fas fa-images"></i>
<span v-if="sprites.length > 0" class="absolute -top-1 -right-1 bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{{ sprites.length }}
</span>
</button>
</tooltip>
<!-- Preview -->
<button
class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors"
:class="{ 'bg-gray-700 text-blue-500': activeSection === 'preview' }"
@click="openPreviewModal"
title="Preview Animation"
:disabled="sprites.length === 0"
>
<i class="fas fa-play"></i>
</button>
<tooltip text="Preview animation" position="right">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'preview' }" @click="openPreviewModal" :disabled="sprites.length === 0">
<i class="fas fa-play"></i>
</button>
</tooltip>
<!-- Settings -->
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'settings' }" @click="openSettingsModal" title="Settings">
<i class="fas fa-cog"></i>
</button>
<tooltip text="Settings" position="right">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" :class="{ 'bg-gray-700 text-blue-500': activeSection === 'settings' }" @click="openSettingsModal">
<i class="fas fa-cog"></i>
</button>
</tooltip>
</div>
<!-- Help Button at Bottom -->
<div class="mt-auto">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" @click="showHelp" title="Help">
<i class="fas fa-question-circle"></i>
</button>
<tooltip text="Help & Support" position="right">
<button class="w-10 h-10 rounded-lg flex items-center justify-center text-gray-200 hover:bg-gray-700 hover:text-blue-500 transition-colors" @click="showHelp">
<i class="fas fa-question-circle"></i>
</button>
</tooltip>
</div>
</nav>
</template>
@ -49,6 +53,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
import Tooltip from './Tooltip.vue';
const store = useSpritesheetStore();
const sprites = computed(() => store.sprites.value);
@ -83,6 +88,7 @@
}>();
const showHelp = () => {
emit('showHelp');
// Instead of just emitting, we'll now open the help modal directly
store.isHelpModalOpen.value = true;
};
</script>

View File

@ -56,7 +56,7 @@
</div>
<div class="flex justify-center bg-gray-700 p-6 rounded mb-6">
<canvas ref="animCanvas" class="block"></canvas>
<canvas ref="animCanvas" class="block" style="image-rendering: pixelated"></canvas>
</div>
</div>
</div>
@ -73,6 +73,7 @@
const isModalOpen = computed(() => store.isModalOpen.value);
const sprites = computed(() => store.sprites.value);
const animation = computed(() => store.animation);
const previewBorder = computed(() => store.previewBorder);
const currentFrame = ref(0);
const position = ref({ x: 0, y: 0 });
@ -268,6 +269,17 @@
{ deep: true }
);
// Watch for changes in border settings to update the preview
watch(
() => previewBorder.value,
() => {
if (isModalOpen.value && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value);
}
},
{ deep: true }
);
// Expose openModal for external use
defineExpose({ openModal });
</script>

View File

@ -59,6 +59,34 @@
</div>
</div>
<!-- Preview Border Settings Section -->
<div class="mb-8">
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-border-style text-blue-500"></i> Preview border</h3>
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label class="text-sm text-gray-400">Enable border (preview only)</label>
<div class="relative inline-block w-10 align-middle select-none">
<input type="checkbox" v-model="previewBorder.enabled" id="toggle-border" class="sr-only" />
<label for="toggle-border" class="block h-6 rounded-full bg-gray-600 cursor-pointer"></label>
<div :class="{ 'translate-x-4': previewBorder.enabled, 'translate-x-0': !previewBorder.enabled }" class="absolute left-0 top-0 w-6 h-6 rounded-full bg-white border border-gray-300 transform transition-transform duration-200 ease-in-out"></div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-4" v-if="previewBorder.enabled">
<div>
<label class="block text-sm text-gray-400 mb-2">Border color</label>
<input type="color" v-model="previewBorder.color" class="w-full h-8 bg-gray-700 rounded cursor-pointer" />
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">Border width: {{ previewBorder.width }}px</label>
<input type="range" v-model.number="previewBorder.width" min="1" max="10" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
</div>
</div>
<div class="text-xs text-gray-400 mt-2"><i class="fas fa-info-circle mr-1"></i> Border will only be visible in the preview and won't be included in the downloaded spritesheet.</div>
</div>
</div>
<!-- Keyboard Shortcuts Section -->
<div>
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-keyboard text-blue-500"></i> Keyboard shortcuts</h3>
@ -93,6 +121,7 @@
const store = useSpritesheetStore();
const sprites = computed(() => store.sprites.value);
const isModalOpen = computed(() => store.isSettingsModalOpen.value);
const previewBorder = computed(() => store.previewBorder);
// Column count control
const columnCount = ref(store.columns.value);
@ -130,3 +159,47 @@
store.showNotification(`Column count updated to ${columnCount.value}`);
};
</script>
<style scoped>
/* Toggle switch styles */
input[type='checkbox'] + label {
width: 2.5rem;
}
input[type='checkbox']:checked + label {
background-color: #0096ff;
}
/* Range input styles */
input[type='range']::-webkit-slider-thumb {
appearance: none;
width: 15px;
height: 15px;
background: #0096ff;
border-radius: 50%;
cursor: pointer;
}
input[type='range']::-moz-range-thumb {
width: 15px;
height: 15px;
background: #0096ff;
border-radius: 50%;
cursor: pointer;
}
/* Color input styles */
input[type='color'] {
-webkit-appearance: none;
border: none;
}
input[type='color']::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type='color']::-webkit-color-swatch {
border: none;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="relative inline-block">
<div @mouseenter="showTooltip" @mouseleave="hideTooltip">
<slot></slot>
</div>
<div v-show="isVisible" class="tooltip absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-800 rounded shadow-lg whitespace-nowrap" :class="[positionClass]" :style="customStyle">
{{ text }}
<div class="absolute w-2 h-2 bg-gray-800 transform rotate-45" :class="[arrowPositionClass]"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const props = defineProps<{
text: string;
position?: 'top' | 'right' | 'bottom' | 'left';
offset?: number;
}>();
const isVisible = ref(false);
const customStyle = ref({});
// Default position is top if not specified
const position = computed(() => props.position || 'top');
const offset = computed(() => props.offset || 8);
// Compute position classes based on the position prop
const positionClass = computed(() => {
switch (position.value) {
case 'top':
return 'bottom-full mb-2';
case 'right':
return 'left-full ml-2';
case 'bottom':
return 'top-full mt-2';
case 'left':
return 'right-full mr-2';
default:
return 'bottom-full mb-2';
}
});
// Compute arrow position classes based on the position prop
const arrowPositionClass = computed(() => {
switch (position.value) {
case 'top':
return 'bottom-[-4px] left-1/2 -translate-x-1/2';
case 'right':
return 'left-[-4px] top-1/2 -translate-y-1/2';
case 'bottom':
return 'top-[-4px] left-1/2 -translate-x-1/2';
case 'left':
return 'right-[-4px] top-1/2 -translate-y-1/2';
default:
return 'bottom-[-4px] left-1/2 -translate-x-1/2';
}
});
const showTooltip = () => {
isVisible.value = true;
};
const hideTooltip = () => {
isVisible.value = false;
};
</script>

View File

@ -39,8 +39,16 @@ const isShiftPressed = ref(false);
const isModalOpen = ref(false);
const isSettingsModalOpen = ref(false);
const isSpritesModalOpen = ref(false);
const isHelpModalOpen = ref(false);
const zoomLevel = ref(1); // Default zoom level (1 = 100%)
// Preview border settings
const previewBorder = reactive({
enabled: false,
color: '#ff0000', // Default red color
width: 2, // Default width in pixels
});
export function useSpritesheetStore() {
const animation = reactive<AnimationState>({
canvas: null,
@ -137,8 +145,6 @@ export function useSpritesheetStore() {
const cols = columns.value;
const rows = Math.ceil(totalSprites / cols);
console.log(`Store: Updating canvas size for ${totalSprites} sprites, ${cols} columns, ${rows} rows`);
if (cellSize.width <= 0 || cellSize.height <= 0) {
console.error('Store: Invalid cell size for canvas update', cellSize);
return;
@ -149,7 +155,6 @@ export function useSpritesheetStore() {
// Ensure the canvas is large enough to display all sprites
if (canvas.value.width !== newWidth || canvas.value.height !== newHeight) {
console.log(`Store: Resizing canvas from ${canvas.value.width}x${canvas.value.height} to ${newWidth}x${newHeight}`);
canvas.value.width = newWidth;
canvas.value.height = newHeight;
@ -176,8 +181,6 @@ export function useSpritesheetStore() {
return;
}
console.log(`Store: Auto-arranging ${sprites.value.length} sprites with ${columns.value} columns`);
// First update the canvas size to ensure it's large enough
updateCanvasSize();
@ -188,9 +191,6 @@ export function useSpritesheetStore() {
sprite.x = column * cellSize.width;
sprite.y = row * cellSize.height;
// Log the position of each sprite for debugging
console.log(`Store: Sprite ${index} (${sprite.name}) positioned at (${sprite.x}, ${sprite.y})`);
});
// Check if the canvas is ready before attempting to render
@ -226,8 +226,6 @@ export function useSpritesheetStore() {
// Make sure the canvas size is correct before rendering
updateCanvasSize();
console.log(`Store: Rendering ${sprites.value.length} sprites on canvas ${canvas.value.width}x${canvas.value.height}`);
// Clear the canvas
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
@ -235,6 +233,14 @@ export function useSpritesheetStore() {
drawGrid();
}
// First, collect all occupied cells
const occupiedCells = new Set<string>();
sprites.value.forEach(sprite => {
const cellX = Math.floor(sprite.x / cellSize.width);
const cellY = Math.floor(sprite.y / cellSize.height);
occupiedCells.add(`${cellX},${cellY}`);
});
// Draw each sprite - remove the zoom scaling from context
sprites.value.forEach((sprite, index) => {
try {
@ -246,13 +252,23 @@ export function useSpritesheetStore() {
// Check if sprite is within canvas bounds
if (sprite.x >= 0 && sprite.y >= 0 && sprite.x + sprite.width <= canvas.value!.width && sprite.y + sprite.height <= canvas.value!.height) {
if (sprite.img.complete && sprite.img.naturalWidth !== 0) {
// Draw the image at its original size
ctx.value!.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height);
// For pixel art, ensure we're drawing at exact pixel boundaries
const x = Math.round(sprite.x);
const y = Math.round(sprite.y);
// Draw the image at its original size with pixel-perfect rendering
ctx.value!.imageSmoothingEnabled = false; // Keep pixel art sharp
ctx.value!.drawImage(sprite.img, x, y, sprite.width, sprite.height);
} else {
console.warn(`Store: Sprite image ${index} not fully loaded, setting onload handler`);
sprite.img.onload = () => {
if (ctx.value && canvas.value) {
ctx.value.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height);
// For pixel art, ensure we're drawing at exact pixel boundaries
const x = Math.round(sprite.x);
const y = Math.round(sprite.y);
ctx.value.imageSmoothingEnabled = false; // Keep pixel art sharp
ctx.value.drawImage(sprite.img, x, y, sprite.width, sprite.height);
}
};
}
@ -263,6 +279,28 @@ export function useSpritesheetStore() {
console.error(`Store: Error rendering sprite at index ${index}:`, spriteError);
}
});
// Draw borders around occupied cells if enabled (preview only)
if (previewBorder.enabled && occupiedCells.size > 0) {
ctx.value!.strokeStyle = previewBorder.color;
ctx.value!.lineWidth = previewBorder.width / zoomLevel.value; // Adjust for zoom
// Draw borders around each occupied cell
occupiedCells.forEach(cellKey => {
const [cellX, cellY] = cellKey.split(',').map(Number);
// Calculate pixel-perfect coordinates for the cell
// Add 0.5 to align with pixel boundaries for crisp lines
const x = Math.floor(cellX * cellSize.width) + 0.5;
const y = Math.floor(cellY * cellSize.height) + 0.5;
// Adjust width and height to ensure the border is inside the cell
const width = cellSize.width - 1;
const height = cellSize.height - 1;
ctx.value!.strokeRect(x, y, width, height);
});
}
} catch (error) {
console.error('Store: Error in renderSpritesheetPreview:', error);
}
@ -278,19 +316,21 @@ export function useSpritesheetStore() {
const visibleWidth = canvas.value.width / zoomLevel.value;
const visibleHeight = canvas.value.height / zoomLevel.value;
// Draw vertical lines
// Draw vertical lines - ensure pixel-perfect grid lines
for (let x = 0; x <= visibleWidth; x += cellSize.width) {
const pixelX = Math.floor(x) + 0.5; // Align to pixel boundary for crisp lines
ctx.value.beginPath();
ctx.value.moveTo(x, 0);
ctx.value.lineTo(x, visibleHeight);
ctx.value.moveTo(pixelX, 0);
ctx.value.lineTo(pixelX, visibleHeight);
ctx.value.stroke();
}
// Draw horizontal lines
// Draw horizontal lines - ensure pixel-perfect grid lines
for (let y = 0; y <= visibleHeight; y += cellSize.height) {
const pixelY = Math.floor(y) + 0.5; // Align to pixel boundary for crisp lines
ctx.value.beginPath();
ctx.value.moveTo(0, y);
ctx.value.lineTo(visibleWidth, y);
ctx.value.moveTo(0, pixelY);
ctx.value.lineTo(visibleWidth, pixelY);
ctx.value.stroke();
}
}
@ -353,8 +393,14 @@ export function useSpritesheetStore() {
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
// Ensure pixel art remains sharp in the downloaded file
tempCtx.imageSmoothingEnabled = false;
sprites.value.forEach(sprite => {
tempCtx.drawImage(sprite.img, sprite.x, sprite.y);
// Use rounded coordinates for pixel-perfect rendering
const x = Math.round(sprite.x);
const y = Math.round(sprite.y);
tempCtx.drawImage(sprite.img, x, y);
});
const link = document.createElement('a');
@ -399,15 +445,36 @@ export function useSpritesheetStore() {
animation.ctx.clearRect(0, 0, animation.canvas.width, animation.canvas.height);
// Draw background (transparent by default)
animation.ctx.fillStyle = 'transparent';
animation.ctx.fillRect(0, 0, animation.canvas.width, animation.canvas.height);
const currentSprite = sprites.value[frameIndex % sprites.value.length];
const cellX = Math.floor(currentSprite.x / cellSize.width);
const cellY = Math.floor(currentSprite.y / cellSize.height);
const offsetX = currentSprite.x - cellX * cellSize.width;
const offsetY = currentSprite.y - cellY * cellSize.height;
// Calculate precise offset for pixel-perfect rendering
const offsetX = Math.round(currentSprite.x - cellX * cellSize.width);
const offsetY = Math.round(currentSprite.y - cellY * cellSize.height);
// Keep pixel art sharp
animation.ctx.imageSmoothingEnabled = false;
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
// Draw border around the cell if enabled (only for preview, not included in download)
if (previewBorder.enabled) {
animation.ctx.strokeStyle = previewBorder.color;
animation.ctx.lineWidth = previewBorder.width;
// Use pixel-perfect coordinates for the border (0.5 offset for crisp lines)
const x = 0.5;
const y = 0.5;
const width = animation.canvas.width - 1;
const height = animation.canvas.height - 1;
animation.ctx.strokeRect(x, y, width, height);
}
}
function animationLoop(timestamp?: number) {
@ -477,9 +544,11 @@ export function useSpritesheetStore() {
isModalOpen,
isSettingsModalOpen,
isSpritesModalOpen,
isHelpModalOpen,
animation,
notification,
zoomLevel,
previewBorder,
addSprites,
updateCellSize,
updateCanvasSize,