[FEAT] Add blog
This commit is contained in:
@@ -3,19 +3,13 @@
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">About Us</h1>
|
||||
<div class="space-y-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">Welcome to Spritesheet Generator, a tool designed to help game developers and artists streamline their workflow.</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
Welcome to Spritesheet Generator, a tool designed to help game developers and artists streamline their workflow.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
Our mission is to provide a simple, powerful, and free tool for creating optimized spritesheets directly in your browser.
|
||||
Whether you are an indie developer, a hobbyist, or part of a large studio, we hope this tool makes your life easier.
|
||||
Our mission is to provide a simple, powerful, and free tool for creating optimized spritesheets directly in your browser. Whether you are an indie developer, a hobbyist, or part of a large studio, we hope this tool makes your life easier.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Our Story</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
This project started as a small utility for personal game jams and has grown into a full-featured spritesheet packer.
|
||||
We believe in open source and community-driven development.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">This project started as a small utility for personal game jams and has grown into a full-featured spritesheet packer. We believe in open source and community-driven development.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
44
src/views/BlogDetail.vue
Normal file
44
src/views/BlogDetail.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useBlog, type BlogPost } from '../composables/useBlog';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { getPost } = useBlog();
|
||||
const post = ref<BlogPost | undefined>(undefined);
|
||||
const renderedContent = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
const slug = route.params.slug as string;
|
||||
post.value = await getPost(slug);
|
||||
|
||||
if (post.value) {
|
||||
renderedContent.value = await marked(post.value.content);
|
||||
} else {
|
||||
router.push({ name: 'blog-overview' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="post">
|
||||
<RouterLink :to="{ name: 'blog-overview' }" class="inline-flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to overview
|
||||
</RouterLink>
|
||||
|
||||
<h1 class="text-4xl font-bold mb-4 text-gray-900 dark:text-white">{{ post.title }}</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-8">{{ post.date }}</p>
|
||||
<!-- Image is intentionally omitted here as per requirements -->
|
||||
<div class="prose max-w-none" v-html="renderedContent"></div>
|
||||
</div>
|
||||
<div v-else class="text-center py-12">
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
30
src/views/BlogOverview.vue
Normal file
30
src/views/BlogOverview.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useBlog, type BlogPost } from '../composables/useBlog';
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
const { getPosts } = useBlog();
|
||||
const posts = ref<BlogPost[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
posts.value = await getPosts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<h1 class="text-4xl font-bold mb-8 text-gray-900 dark:text-white">Blog</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<article v-for="post in posts" :key="post.slug" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 flex flex-col h-full">
|
||||
<RouterLink :to="{ name: 'blog-detail', params: { slug: post.slug } }" class="flex flex-col h-full">
|
||||
<img :src="post.image" :alt="post.title" class="w-full h-48 object-cover" />
|
||||
<div class="p-6 flex-1 flex flex-col">
|
||||
<h2 class="text-xl font-bold mb-2 text-gray-900 dark:text-white line-clamp-2">{{ post.title }}</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-xs mb-3">{{ post.date }}</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 text-sm line-clamp-3 flex-1">{{ post.description }}</p>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,9 +3,7 @@
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">Contact Us</h1>
|
||||
<div class="space-y-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
We'd love to hear from you! Whether you have a question, feedback, or just want to say hi, feel free to reach out.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We'd love to hear from you! Whether you have a question, feedback, or just want to say hi, feel free to reach out.</p>
|
||||
<div class="flex flex-col gap-4 mt-8">
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline">
|
||||
<i class="fab fa-discord text-2xl text-[#5865F2]"></i>
|
||||
@@ -14,7 +12,7 @@
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Chat with the community and developers</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline">
|
||||
<i class="fab fa-github text-2xl text-gray-900 dark:text-white"></i>
|
||||
<div>
|
||||
|
||||
@@ -56,7 +56,10 @@
|
||||
<span>JSON</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="w-full p-6 text-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl hover:border-gray-500 dark:hover:border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-500 group" @click="openFileDialog">
|
||||
<button
|
||||
class="w-full p-6 text-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl hover:border-gray-500 dark:hover:border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-500 group"
|
||||
@click="openFileDialog"
|
||||
>
|
||||
<i class="fas fa-plus-circle text-3xl text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300 mb-3 transition-colors"></i>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-200 transition-colors">Add sprites</p>
|
||||
</button>
|
||||
@@ -135,23 +138,9 @@
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="w-4 h-4 rounded" />
|
||||
</label>
|
||||
<div v-if="settingsStore.manualCellSizeEnabled" class="flex items-center gap-1.5 mt-2">
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="settingsStore.manualCellWidth"
|
||||
min="1"
|
||||
max="2048"
|
||||
class="input-field w-full min-w-0"
|
||||
placeholder="W"
|
||||
/>
|
||||
<input type="number" v-model.number="settingsStore.manualCellWidth" min="1" max="2048" class="input-field w-full min-w-0" placeholder="W" />
|
||||
<span class="flex-shrink-0 text-gray-500 dark:text-gray-400">×</span>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="settingsStore.manualCellHeight"
|
||||
min="1"
|
||||
max="2048"
|
||||
class="input-field w-full min-w-0"
|
||||
placeholder="H"
|
||||
/>
|
||||
<input type="number" v-model.number="settingsStore.manualCellHeight" min="1" max="2048" class="input-field w-full min-w-0" placeholder="H" />
|
||||
</div>
|
||||
<div v-else class="mt-1 text-xs font-mono text-gray-500 dark:text-gray-400 break-words">{{ cellSize.width }} × {{ cellSize.height }}px</div>
|
||||
</div>
|
||||
@@ -222,10 +211,7 @@
|
||||
<button
|
||||
@click="activeTab = 'canvas'"
|
||||
class="border-gray-600 border"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer',
|
||||
activeTab === 'canvas' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50',
|
||||
]"
|
||||
:class="['flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer', activeTab === 'canvas' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50']"
|
||||
>
|
||||
<i class="fas fa-th"></i>
|
||||
<span>Canvas</span>
|
||||
@@ -233,10 +219,7 @@
|
||||
<button
|
||||
@click="activeTab = 'preview'"
|
||||
class="border-gray-600 border"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer',
|
||||
activeTab === 'preview' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50',
|
||||
]"
|
||||
:class="['flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer', activeTab === 'preview' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50']"
|
||||
data-rybbit-event="preview-animation"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
@@ -263,180 +246,180 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRef, computed } from 'vue';
|
||||
import FileUploader from '@/components/FileUploader.vue';
|
||||
import SpriteCanvas from '@/components/SpriteCanvas.vue';
|
||||
import SpritePreview from '@/components/SpritePreview.vue';
|
||||
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from '@/components/GifFpsModal.vue';
|
||||
import { useExportLayers } from '@/composables/useExportLayers';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||
import type { SpriteFile } from '@/types/sprites';
|
||||
import { ref, toRef, computed } from 'vue';
|
||||
import FileUploader from '@/components/FileUploader.vue';
|
||||
import SpriteCanvas from '@/components/SpriteCanvas.vue';
|
||||
import SpritePreview from '@/components/SpritePreview.vue';
|
||||
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from '@/components/GifFpsModal.vue';
|
||||
import { useExportLayers } from '@/composables/useExportLayers';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||
import type { SpriteFile } from '@/types/sprites';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();
|
||||
|
||||
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(
|
||||
layers,
|
||||
columns,
|
||||
toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||
activeLayerId,
|
||||
toRef(settingsStore, 'backgroundColor'),
|
||||
toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||
toRef(settingsStore, 'manualCellWidth'),
|
||||
toRef(settingsStore, 'manualCellHeight')
|
||||
);
|
||||
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(
|
||||
layers,
|
||||
columns,
|
||||
toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||
activeLayerId,
|
||||
toRef(settingsStore, 'backgroundColor'),
|
||||
toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||
toRef(settingsStore, 'manualCellWidth'),
|
||||
toRef(settingsStore, 'manualCellHeight')
|
||||
);
|
||||
|
||||
const getCellSize = () => {
|
||||
if (!visibleLayers.value.length) return { width: 0, height: 0 };
|
||||
const getCellSize = () => {
|
||||
if (!visibleLayers.value.length) return { width: 0, height: 0 };
|
||||
|
||||
if (settingsStore.manualCellSizeEnabled) {
|
||||
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
|
||||
}
|
||||
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
|
||||
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
|
||||
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
|
||||
};
|
||||
|
||||
const cellSize = computed(getCellSize);
|
||||
const activeTab = ref<'canvas' | 'preview'>('canvas');
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const spritesheetImageUrl = ref('');
|
||||
const spritesheetImageFile = ref<File | null>(null);
|
||||
const editingLayerId = ref<string | null>(null);
|
||||
const editingLayerName = ref('');
|
||||
const layerNameInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const handleSpritesUpload = async (files: File[]) => {
|
||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||
|
||||
if (jsonFile) {
|
||||
await handleJSONImport(jsonFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 1 && files[0].type.startsWith('image/')) {
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const url = e.target?.result as string;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (confirm('This looks like it might be a spritesheet. Would you like to split it into individual sprites?')) {
|
||||
spritesheetImageUrl.value = url;
|
||||
spritesheetImageFile.value = file;
|
||||
isSpritesheetSplitterOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles([file]);
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image:', file.name);
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error('Failed to read image file:', file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles(files);
|
||||
};
|
||||
|
||||
const handleJSONImport = async (jsonFile: File) => {
|
||||
try {
|
||||
await importSpritesheetJSON(jsonFile);
|
||||
} catch (error) {
|
||||
console.error('Error importing JSON:', error);
|
||||
alert('Failed to import JSON file. Please check the file format.');
|
||||
}
|
||||
};
|
||||
|
||||
const closeSpritesheetSplitter = () => {
|
||||
isSpritesheetSplitterOpen.value = false;
|
||||
if (spritesheetImageUrl.value && spritesheetImageUrl.value.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(spritesheetImageUrl.value);
|
||||
} catch {}
|
||||
}
|
||||
spritesheetImageUrl.value = '';
|
||||
spritesheetImageFile.value = null;
|
||||
};
|
||||
|
||||
const openGifFpsModal = () => {
|
||||
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
|
||||
alert('Please upload or import sprites before generating a GIF.');
|
||||
return;
|
||||
}
|
||||
isGifFpsModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeGifFpsModal = () => {
|
||||
isGifFpsModalOpen.value = false;
|
||||
};
|
||||
|
||||
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||
processImageFiles(spriteFiles.map(s => s.file));
|
||||
};
|
||||
|
||||
const openJSONImportDialog = () => {
|
||||
jsonFileInput.value?.click();
|
||||
};
|
||||
|
||||
const handleJSONFileChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
await handleJSONImport(input.files[0]);
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const handleUploadChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
await handleSpritesUpload(Array.from(input.files));
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const startEditingLayer = (layerId: string, currentName: string) => {
|
||||
editingLayerId.value = layerId;
|
||||
editingLayerName.value = currentName;
|
||||
// Focus the input on next tick
|
||||
setTimeout(() => {
|
||||
layerNameInput.value?.focus();
|
||||
layerNameInput.value?.select();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const finishEditingLayer = () => {
|
||||
if (editingLayerId.value && editingLayerName.value.trim()) {
|
||||
const layer = layers.value.find(l => l.id === editingLayerId.value);
|
||||
if (layer) {
|
||||
layer.name = editingLayerName.value.trim();
|
||||
if (settingsStore.manualCellSizeEnabled) {
|
||||
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
|
||||
}
|
||||
}
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
|
||||
const cancelEditingLayer = () => {
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
|
||||
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
|
||||
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
|
||||
};
|
||||
|
||||
const cellSize = computed(getCellSize);
|
||||
const activeTab = ref<'canvas' | 'preview'>('canvas');
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const spritesheetImageUrl = ref('');
|
||||
const spritesheetImageFile = ref<File | null>(null);
|
||||
const editingLayerId = ref<string | null>(null);
|
||||
const editingLayerName = ref('');
|
||||
const layerNameInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const handleSpritesUpload = async (files: File[]) => {
|
||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||
|
||||
if (jsonFile) {
|
||||
await handleJSONImport(jsonFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 1 && files[0].type.startsWith('image/')) {
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const url = e.target?.result as string;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (confirm('This looks like it might be a spritesheet. Would you like to split it into individual sprites?')) {
|
||||
spritesheetImageUrl.value = url;
|
||||
spritesheetImageFile.value = file;
|
||||
isSpritesheetSplitterOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles([file]);
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image:', file.name);
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error('Failed to read image file:', file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles(files);
|
||||
};
|
||||
|
||||
const handleJSONImport = async (jsonFile: File) => {
|
||||
try {
|
||||
await importSpritesheetJSON(jsonFile);
|
||||
} catch (error) {
|
||||
console.error('Error importing JSON:', error);
|
||||
alert('Failed to import JSON file. Please check the file format.');
|
||||
}
|
||||
};
|
||||
|
||||
const closeSpritesheetSplitter = () => {
|
||||
isSpritesheetSplitterOpen.value = false;
|
||||
if (spritesheetImageUrl.value && spritesheetImageUrl.value.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(spritesheetImageUrl.value);
|
||||
} catch {}
|
||||
}
|
||||
spritesheetImageUrl.value = '';
|
||||
spritesheetImageFile.value = null;
|
||||
};
|
||||
|
||||
const openGifFpsModal = () => {
|
||||
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
|
||||
alert('Please upload or import sprites before generating a GIF.');
|
||||
return;
|
||||
}
|
||||
isGifFpsModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeGifFpsModal = () => {
|
||||
isGifFpsModalOpen.value = false;
|
||||
};
|
||||
|
||||
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||
processImageFiles(spriteFiles.map(s => s.file));
|
||||
};
|
||||
|
||||
const openJSONImportDialog = () => {
|
||||
jsonFileInput.value?.click();
|
||||
};
|
||||
|
||||
const handleJSONFileChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
await handleJSONImport(input.files[0]);
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const handleUploadChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
await handleSpritesUpload(Array.from(input.files));
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const startEditingLayer = (layerId: string, currentName: string) => {
|
||||
editingLayerId.value = layerId;
|
||||
editingLayerName.value = currentName;
|
||||
// Focus the input on next tick
|
||||
setTimeout(() => {
|
||||
layerNameInput.value?.focus();
|
||||
layerNameInput.value?.select();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const finishEditingLayer = () => {
|
||||
if (editingLayerId.value && editingLayerName.value.trim()) {
|
||||
const layer = layers.value.find(l => l.id === editingLayerId.value);
|
||||
if (layer) {
|
||||
layer.name = editingLayerName.value.trim();
|
||||
}
|
||||
}
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
|
||||
const cancelEditingLayer = () => {
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -7,37 +7,27 @@
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">1. Introduction</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
We respect your privacy. This Spritesheet Generator is a client-side tool, meaning your images are processed directly in your browser and are not uploaded to our servers.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We respect your privacy. This Spritesheet Generator is a client-side tool, meaning your images are processed directly in your browser and are not uploaded to our servers.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">2. Data Collection</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
We do not collect any personal data or uploaded images. All processing happens locally on your device.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We do not collect any personal data or uploaded images. All processing happens locally on your device.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">3. Local Storage</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
We use your browser's Local Storage to save your preferences (such as dark mode and grid settings) to improve your experience.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We use your browser's Local Storage to save your preferences (such as dark mode and grid settings) to improve your experience.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">4. Third-Party Services</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
We may use third-party services for analytics or hosting (e.g., Cloudflare, Vercel) which may collect standard server logs.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We may use third-party services for analytics or hosting (e.g., Cloudflare, Vercel) which may collect standard server logs.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">5. Contact</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
If you have any questions about this Privacy Policy, please contact us via our Discord server.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">If you have any questions about this Privacy Policy, please contact us via our Discord server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user