[FEAT] Add blog
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user