Allow for JSON import/export

This commit is contained in:
Dennis Postma 2025-04-05 23:43:47 +02:00
parent 5e2a231c13
commit f54b87140c
2 changed files with 161 additions and 5 deletions

View File

@ -9,7 +9,15 @@
</div>
<div class="bg-white rounded-2xl shadow-soft p-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-800">Upload sprites</h2>
<button @click="openJSONImportDialog" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
<i class="fas fa-file-import"></i>
<span>Import JSON</span>
</button>
</div>
<file-uploader @upload-sprites="handleSpritesUpload" />
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
<div v-if="sprites.length > 0" class="mt-8">
<div class="flex flex-wrap items-center gap-6 mb-8">
@ -23,6 +31,11 @@
<span>Download spritesheet</span>
</button>
<button @click="exportSpritesheetJSON" class="px-6 py-2.5 bg-purple-500 hover:bg-purple-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
<i class="fas fa-file-code"></i>
<span>Export as JSON</span>
</button>
<button @click="openPreviewModal" class="px-6 py-2.5 bg-green-500 hover:bg-green-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
<i class="fas fa-play"></i>
<span>Preview animation</span>
@ -65,8 +78,19 @@
const columns = ref(4);
const isPreviewModalOpen = ref(false);
const isHelpModalOpen = ref(false);
const jsonFileInput = ref<HTMLInputElement | null>(null);
const handleSpritesUpload = (files: File[]) => {
// Check if any of the files is a JSON file
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
if (jsonFile) {
// If it's a JSON file, try to import it
importSpritesheetJSON(jsonFile);
return;
}
// Otherwise, process as normal image files
Promise.all(
files.map(file => {
return new Promise<Sprite>(resolve => {
@ -164,4 +188,135 @@
const closeHelpModal = () => {
isHelpModalOpen.value = false;
};
// Export spritesheet as JSON with base64 images
const exportSpritesheetJSON = async () => {
if (sprites.value.length === 0) return;
// Create an array to store sprite data with base64 images
const spritesData = await Promise.all(
sprites.value.map(async (sprite, index) => {
// Create a canvas for each sprite to get its base64 data
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return null;
// Set canvas size to match the sprite
canvas.width = sprite.width;
canvas.height = sprite.height;
// Draw the sprite
ctx.drawImage(sprite.img, 0, 0);
// Get base64 data
const base64Data = canvas.toDataURL('image/png');
return {
id: sprite.id,
width: sprite.width,
height: sprite.height,
x: sprite.x,
y: sprite.y,
base64: base64Data,
name: sprite.file.name,
};
})
);
// Create JSON object with all necessary data
const jsonData = {
columns: columns.value,
sprites: spritesData.filter(Boolean), // Remove any null values
};
// Convert to JSON string
const jsonString = JSON.stringify(jsonData, null, 2);
// Create download link
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'spritesheet.json';
link.href = url;
link.click();
// Clean up
URL.revokeObjectURL(url);
};
// Open file dialog for JSON import
const openJSONImportDialog = () => {
jsonFileInput.value?.click();
};
// Handle JSON file selection
const handleJSONFileChange = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const jsonFile = input.files[0];
importSpritesheetJSON(jsonFile);
// Reset input value so uploading the same file again will trigger the event
if (jsonFileInput.value) jsonFileInput.value.value = '';
}
};
// Import spritesheet from JSON
const importSpritesheetJSON = async (jsonFile: File) => {
try {
const jsonText = await jsonFile.text();
const jsonData = JSON.parse(jsonText);
if (!jsonData.sprites || !Array.isArray(jsonData.sprites)) {
throw new Error('Invalid JSON format: missing sprites array');
}
// Set columns if available
if (jsonData.columns && typeof jsonData.columns === 'number') {
columns.value = jsonData.columns;
}
// Process each sprite
const newSprites = await Promise.all(
jsonData.sprites.map(async (spriteData: any) => {
return new Promise<Sprite>(resolve => {
// Create image from base64
const img = new Image();
img.onload = () => {
// Create a file from the base64 data
const byteString = atob(spriteData.base64.split(',')[1]);
const mimeType = spriteData.base64.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeType });
const fileName = spriteData.name || `sprite-${spriteData.id}.png`;
const file = new File([blob], fileName, { type: mimeType });
resolve({
id: spriteData.id || crypto.randomUUID(),
file,
img,
url: spriteData.base64,
width: spriteData.width,
height: spriteData.height,
x: spriteData.x || 0,
y: spriteData.y || 0,
});
};
img.src = spriteData.base64;
});
})
);
// Replace current sprites with imported ones
sprites.value = newSprites;
} catch (error) {
console.error('Error importing JSON:', error);
alert('Failed to import JSON file. Please check the file format.');
}
};
</script>

View File

@ -9,19 +9,20 @@
@dragleave.prevent="isDragging = false"
@dragover.prevent
@drop.prevent="handleDrop"
@click="openFileDialog"
>
<input ref="fileInput" type="file" multiple accept="image/*" class="hidden" @change="handleFileChange" />
<input ref="fileInput" type="file" multiple accept="image/*,.json" class="hidden" @change="handleFileChange" />
<div class="mb-6">
<img src="@/assets/images/file.svg" alt="File upload" class="w-20 h-20 mx-auto mb-4 opacity-75" />
</div>
<p class="text-xl font-medium text-gray-700 mb-2">Drag and drop your sprite images here</p>
<p class="text-xl font-medium text-gray-700 mb-2">Drag and drop your sprite images or JSON file here</p>
<p class="text-sm text-gray-500 mb-6">or</p>
<button @click="openFileDialog" class="px-6 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors inline-flex items-center space-x-2">
<button class="px-6 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors inline-flex items-center space-x-2 cursor-pointer">
<i class="fas fa-folder-open"></i>
<span>Select Files</span>
<span>Select files</span>
</button>
</div>
</template>
@ -55,7 +56,7 @@
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files).filter(file => {
return file.type.startsWith('image/');
return file.type.startsWith('image/') || file.type === 'application/json' || file.name.endsWith('.json');
});
if (files.length > 0) {