Compare commits
4 Commits
9c0f10b977
...
376bf00ad4
Author | SHA1 | Date | |
---|---|---|---|
376bf00ad4 | |||
8fdb0dbae3 | |||
1c7a7db299 | |||
5218669744 |
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
150
src/components/HelpModal.vue
Normal file
150
src/components/HelpModal.vue
Normal 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>
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
68
src/components/Tooltip.vue
Normal file
68
src/components/Tooltip.vue
Normal 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>
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user