[FEAT] Add sharing function, UI enhancement
This commit is contained in:
@@ -45,9 +45,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Two-column layout: Left controls, Right preview -->
|
||||
<div v-if="layers.some(l => l.sprites.length)" class="flex flex-col flex-1 lg:grid lg:grid-cols-[380px_1fr] xl:grid-cols-[420px_1fr] lg:overflow-hidden">
|
||||
<div v-if="layers.some(l => l.sprites.length)" class="flex flex-col flex-1 lg:grid lg:grid-cols-[380px_1fr] xl:grid-cols-[420px_1fr] lg:overflow-hidden min-h-0">
|
||||
<!-- Left sidebar - Controls -->
|
||||
<div class="p-6 bg-gray-50/50 dark:bg-gray-900/30 border-b lg:border-b-0 lg:border-r border-gray-200 dark:border-gray-700 lg:overflow-y-auto lg:overflow-x-auto">
|
||||
<div class="p-6 bg-gray-50/50 dark:bg-gray-900/30 border-b lg:border-b-0 lg:border-r border-gray-200 dark:border-gray-700 lg:overflow-y-auto lg:overflow-x-auto lg:min-h-0">
|
||||
<div class="space-y-8">
|
||||
<!-- Upload Section -->
|
||||
<section>
|
||||
@@ -203,42 +203,58 @@
|
||||
<i class="fas fa-file-archive"></i>
|
||||
<span>ZIP</span>
|
||||
</button>
|
||||
<button @click="openShareModal" class="btn btn-dark btn-sm col-span-2" data-rybbit-event="share-spritesheet">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel - Tabs -->
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<div class="flex flex-col overflow-hidden min-h-0">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="bg-gray-50/50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex gap-1 p-2">
|
||||
<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']"
|
||||
>
|
||||
<i class="fas fa-th"></i>
|
||||
<span>Canvas</span>
|
||||
</button>
|
||||
<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']"
|
||||
data-rybbit-event="preview-animation"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
<span>Preview</span>
|
||||
<div class="flex items-center justify-between gap-1 p-2">
|
||||
<div class="flex gap-1">
|
||||
<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',
|
||||
]"
|
||||
>
|
||||
<i class="fas fa-th"></i>
|
||||
<span>Canvas</span>
|
||||
</button>
|
||||
<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',
|
||||
]"
|
||||
data-rybbit-event="preview-animation"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="openShareModal" class="flex items-center gap-2 px-4 py-2 mr-2.5 text-sm font-medium rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all cursor-pointer" data-rybbit-event="share-spritesheet-header">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
<span>Share spritesheet</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-6 lg:flex-1 lg:overflow-auto">
|
||||
<div v-if="activeTab === 'canvas'">
|
||||
<div class="p-6 lg:flex-1 lg:overflow-auto lg:min-h-0">
|
||||
<div v-if="activeTab === 'canvas'" class="h-full">
|
||||
<sprite-canvas :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-cell="updateSpriteCell" @remove-sprite="removeSprite" @replace-sprite="replaceSprite" @add-sprite="addSprite" />
|
||||
</div>
|
||||
<div v-if="activeTab === 'preview'">
|
||||
<div v-if="activeTab === 'preview'" class="h-full">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,6 +263,7 @@
|
||||
|
||||
<SpritesheetSplitter :is-open="isSpritesheetSplitterOpen" :image-url="spritesheetImageUrl" :image-file="spritesheetImageFile" @close="closeSpritesheetSplitter" @split="handleSplitSpritesheet" />
|
||||
<GifFpsModal :is-open="isGifFpsModalOpen" @close="closeGifFpsModal" @confirm="downloadAsGif" :default-fps="10" />
|
||||
<ShareModal :is-open="isShareModalOpen" :share-function="shareFunction" @close="closeShareModal" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -257,7 +274,9 @@
|
||||
import SpritePreview from '@/components/SpritePreview.vue';
|
||||
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from '@/components/GifFpsModal.vue';
|
||||
import ShareModal from '@/components/ShareModal.vue';
|
||||
import { useExportLayers } from '@/composables/useExportLayers';
|
||||
import { useShare } from '@/composables/useShare';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
@@ -299,6 +318,7 @@
|
||||
const activeTab = ref<'canvas' | 'preview'>('canvas');
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const isShareModalOpen = ref(false);
|
||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const spritesheetImageUrl = ref('');
|
||||
@@ -378,6 +398,23 @@
|
||||
isGifFpsModalOpen.value = false;
|
||||
};
|
||||
|
||||
// Share functionality
|
||||
const { share } = useShare(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'), toRef(settingsStore, 'backgroundColor'), toRef(settingsStore, 'manualCellSizeEnabled'), toRef(settingsStore, 'manualCellWidth'), toRef(settingsStore, 'manualCellHeight'));
|
||||
|
||||
const shareFunction = () => share();
|
||||
|
||||
const openShareModal = () => {
|
||||
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
|
||||
alert('Please upload or import sprites before sharing.');
|
||||
return;
|
||||
}
|
||||
isShareModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeShareModal = () => {
|
||||
isShareModalOpen.value = false;
|
||||
};
|
||||
|
||||
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||
processImageFiles(spriteFiles.map(s => s.file));
|
||||
};
|
||||
|
||||
286
src/views/ShareView.vue
Normal file
286
src/views/ShareView.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<main class="flex-1 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 transition-all duration-300">
|
||||
<div class="p-6 sm:p-10 max-w-4xl mx-auto">
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-24">
|
||||
<div class="relative">
|
||||
<div class="w-16 h-16 border-4 border-gray-200 dark:border-gray-700 rounded-full"></div>
|
||||
<div class="absolute top-0 left-0 w-16 h-16 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p class="mt-6 text-lg font-medium text-gray-600 dark:text-gray-300">Loading shared spritesheet...</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Please wait while we fetch your spritesheet</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" class="flex flex-col items-center justify-center py-24">
|
||||
<div class="w-20 h-20 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mb-6">
|
||||
<i class="fas fa-exclamation-triangle text-3xl text-red-500 dark:text-red-400"></i>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-2">Oops! Something went wrong</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">{{ error }}</p>
|
||||
<router-link to="/" class="btn btn-dark px-6 py-3">
|
||||
<i class="fas fa-home mr-2"></i>
|
||||
Back to Home
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Success state - show spritesheet info -->
|
||||
<div v-else-if="spritesheetData">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-10">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl shadow-lg mb-4">
|
||||
<i class="fas fa-share-alt text-2xl text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 mb-2">Shared spritesheet</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">Shared {{ formatDate(spritesheetData.created) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
|
||||
<div class="group p-5 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl border border-blue-200/50 dark:border-blue-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-blue-500/20 dark:bg-blue-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-columns text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-blue-600/80 dark:text-blue-400/80 uppercase tracking-wider">Columns</p>
|
||||
<p class="text-2xl font-bold text-blue-700 dark:text-blue-300">{{ spritesheetData.config.columns }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-2xl border border-purple-200/50 dark:border-purple-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-purple-500/20 dark:bg-purple-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-layer-group text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-purple-600/80 dark:text-purple-400/80 uppercase tracking-wider">Layers</p>
|
||||
<p class="text-2xl font-bold text-purple-700 dark:text-purple-300">{{ spritesheetData.sprites.length }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20 rounded-2xl border border-emerald-200/50 dark:border-emerald-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-images text-emerald-600 dark:text-emerald-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-emerald-600/80 dark:text-emerald-400/80 uppercase tracking-wider">Sprites</p>
|
||||
<p class="text-2xl font-bold text-emerald-700 dark:text-emerald-300">{{ totalSprites }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-900/20 dark:to-amber-800/20 rounded-2xl border border-amber-200/50 dark:border-amber-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-amber-500/20 dark:bg-amber-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-fill-drip text-amber-600 dark:text-amber-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-amber-600/80 dark:text-amber-400/80 uppercase tracking-wider">Background</p>
|
||||
<p class="text-lg font-bold text-amber-700 dark:text-amber-300 truncate">{{ spritesheetData.config.backgroundColor }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="flex items-center gap-2 text-lg font-bold text-gray-800 dark:text-gray-100 mb-4">
|
||||
<i class="fas fa-eye text-gray-500 dark:text-gray-400"></i>
|
||||
Sprite preview
|
||||
</h3>
|
||||
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-2xl border border-gray-200 dark:border-gray-700">
|
||||
<div v-for="(layer, layerIndex) in spritesheetData.sprites" :key="layerIndex" class="mb-6 last:mb-0">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 bg-gray-200 dark:bg-gray-700 rounded-full text-xs font-bold text-gray-600 dark:text-gray-300">
|
||||
{{ layerIndex + 1 }}
|
||||
</span>
|
||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ layer.name || `Layer ${layerIndex + 1}` }}</p>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">({{ layer.sprites.length }} sprites)</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="(sprite, spriteIndex) in layer.sprites.slice(0, 12)" :key="spriteIndex" class="relative group">
|
||||
<div class="w-16 h-16 bg-checkerboard rounded-lg border-2 border-gray-200 dark:border-gray-600 overflow-hidden transition-transform group-hover:scale-110 group-hover:border-blue-400 group-hover:shadow-lg">
|
||||
<img :src="sprite.base64" :alt="`Sprite ${spriteIndex + 1}`" class="w-full h-full object-contain" style="image-rendering: pixelated" />
|
||||
</div>
|
||||
<div class="absolute -bottom-1 -right-1 w-5 h-5 bg-gray-800 dark:bg-gray-600 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="text-[10px] font-bold text-white">{{ spriteIndex + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="layer.sprites.length > 12" class="w-16 h-16 flex flex-col items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-700 dark:to-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
|
||||
<span class="text-lg font-bold text-gray-500 dark:text-gray-400">+{{ layer.sprites.length - 12 }}</span>
|
||||
<span class="text-[10px] text-gray-400 dark:text-gray-500">more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button @click="loadIntoEditor" class="btn btn-dark px-8 py-3 text-base font-semibold shadow-lg hover:shadow-xl transition-shadow">
|
||||
<i class="fas fa-edit mr-2"></i>
|
||||
Edit in a copy
|
||||
</button>
|
||||
<button @click="downloadJSON" class="btn btn-secondary px-8 py-3 text-base font-semibold">
|
||||
<i class="fas fa-download mr-2"></i>
|
||||
Download JSON
|
||||
</button>
|
||||
<router-link to="/" class="btn btn-ghost px-8 py-3 text-base font-semibold text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Create new
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { fetchSpritesheet, type SpritesheetRecord } from '@/composables/useShare';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import type { Layer, Sprite } from '@/types/sprites';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { layers, activeLayerId, columns } = useLayers();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
const spritesheetData = ref<SpritesheetRecord | null>(null);
|
||||
|
||||
const totalSprites = computed(() => {
|
||||
if (!spritesheetData.value) return 0;
|
||||
return spritesheetData.value.sprites.reduce((sum, layer) => sum + (layer.sprites?.length || 0), 0);
|
||||
});
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
|
||||
if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
||||
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const loadIntoEditor = async () => {
|
||||
if (!spritesheetData.value) return;
|
||||
|
||||
const data = spritesheetData.value;
|
||||
|
||||
// Apply config settings
|
||||
columns.value = data.config.columns;
|
||||
settingsStore.negativeSpacingEnabled = data.config.negativeSpacingEnabled;
|
||||
settingsStore.backgroundColor = data.config.backgroundColor;
|
||||
settingsStore.manualCellSizeEnabled = data.config.manualCellSizeEnabled;
|
||||
settingsStore.manualCellWidth = data.config.manualCellWidth;
|
||||
settingsStore.manualCellHeight = data.config.manualCellHeight;
|
||||
|
||||
// Load sprites into layers
|
||||
const loadSprite = (spriteData: any): Promise<Sprite> =>
|
||||
new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
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;
|
||||
});
|
||||
|
||||
const newLayers: Layer[] = [];
|
||||
for (const layerData of data.sprites) {
|
||||
const sprites: Sprite[] = await Promise.all(layerData.sprites.map((s: any) => loadSprite(s)));
|
||||
newLayers.push({
|
||||
id: layerData.id || crypto.randomUUID(),
|
||||
name: layerData.name || 'Layer',
|
||||
visible: layerData.visible !== false,
|
||||
locked: !!layerData.locked,
|
||||
sprites,
|
||||
});
|
||||
}
|
||||
|
||||
layers.value = newLayers;
|
||||
if (newLayers.length > 0) {
|
||||
const firstWithSprites = newLayers.find(l => l.sprites.length > 0);
|
||||
activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id;
|
||||
}
|
||||
|
||||
// Navigate to home
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const downloadJSON = () => {
|
||||
if (!spritesheetData.value) return;
|
||||
|
||||
const json = {
|
||||
version: spritesheetData.value.config.version,
|
||||
columns: spritesheetData.value.config.columns,
|
||||
negativeSpacingEnabled: spritesheetData.value.config.negativeSpacingEnabled,
|
||||
backgroundColor: spritesheetData.value.config.backgroundColor,
|
||||
manualCellSizeEnabled: spritesheetData.value.config.manualCellSizeEnabled,
|
||||
manualCellWidth: spritesheetData.value.config.manualCellWidth,
|
||||
manualCellHeight: spritesheetData.value.config.manualCellHeight,
|
||||
layers: spritesheetData.value.sprites,
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(json, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'spritesheet.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id as string;
|
||||
if (!id) {
|
||||
error.value = 'No spritesheet ID provided';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
spritesheetData.value = await fetchSpritesheet(id);
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load spritesheet:', e);
|
||||
error.value = e.message || 'Failed to load spritesheet';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-checkerboard {
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%), linear-gradient(-45deg, #e5e5e5 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e5e5 75%), linear-gradient(-45deg, transparent 75%, #e5e5e5 75%);
|
||||
background-size: 12px 12px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 6px,
|
||||
6px -6px,
|
||||
-6px 0px;
|
||||
}
|
||||
|
||||
.dark .bg-checkerboard {
|
||||
background-color: #374151;
|
||||
background-image: linear-gradient(45deg, #4b5563 25%, transparent 25%), linear-gradient(-45deg, #4b5563 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #4b5563 75%), linear-gradient(-45deg, transparent 75%, #4b5563 75%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user