[FEAT] Add pixel editor

This commit is contained in:
2026-01-03 17:17:14 +01:00
parent 224d0d62fe
commit 2f0404d698
6 changed files with 1007 additions and 5 deletions

View File

@@ -89,7 +89,7 @@
</section>
<!-- Canvas Grid Settings (Editor only) -->
<section v-if="activeTab === 'canvas'">
<section v-if="activeTab === 'canvas' || activeTab === 'preview'">
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Grid Layout</h3>
<div class="card p-3 bg-gray-50/50 dark:bg-gray-800/40 space-y-3">
<div class="flex items-center justify-between">
@@ -112,7 +112,7 @@
</section>
<!-- View Options (Editor only) -->
<section v-if="activeTab === 'canvas'">
<section v-if="activeTab === 'canvas' || activeTab === 'preview'">
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">View Options</h3>
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 grid grid-cols-2 gap-2">
<Tooltip text="Disable anti-aliasing for crisp pixel art rendering">
@@ -173,7 +173,7 @@
</section>
<!-- Tools (Editor only) -->
<section v-if="activeTab === 'canvas'">
<section v-if="activeTab === 'canvas' || activeTab === 'preview'">
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Tools</h3>
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 space-y-2">
<div class="flex gap-2">
@@ -243,6 +243,13 @@
>
<i class="fas fa-play mr-2"></i>Preview
</button>
<button
@click="activeTab = 'draw'"
:class="activeTab === 'draw' ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
>
<i class="fas fa-paint-brush mr-2"></i>Draw
</button>
</div>
<!-- Background Color (Compact) -->
@@ -303,9 +310,10 @@
@rotate-sprite="rotateSprite"
@flip-sprite="flipSprite"
@copy-sprite-to-frame="copySpriteToFrame"
@open-pixel-editor="openPixelEditor"
/>
</div>
<div v-if="activeTab === 'preview'" class="h-full flex items-center justify-center">
<div v-else-if="activeTab === 'preview'" class="h-full flex items-center justify-center">
<sprite-preview
:layers="layers"
:active-layer-id="activeLayerId"
@@ -317,6 +325,15 @@
@flip-sprite="flipSprite"
@copy-sprite-to-frame="copySpriteToFrame"
@replace-sprite="replaceSprite"
@open-pixel-editor="openPixelEditor"
/>
</div>
<div v-else-if="activeTab === 'draw'" class="h-full">
<draw-tab
:layers="layers"
:initial-frame="pixelEditorFrame"
@save-frame="handleSaveFrame"
@close="pixelEditorFrame = null"
/>
</div>
</div>
@@ -340,6 +357,7 @@
import FileUploader from '@/components/FileUploader.vue';
import SpriteCanvas from '@/components/SpriteCanvas.vue';
import SpritePreview from '@/components/SpritePreview.vue';
import DrawTab from '@/components/DrawTab.vue';
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
import GifFpsModal from '@/components/GifFpsModal.vue';
import ShareModal from '@/components/ShareModal.vue';
@@ -442,7 +460,8 @@
};
const cellSize = computed(getCellSize);
const activeTab = ref<'canvas' | 'preview'>('canvas');
const activeTab = ref<'canvas' | 'preview' | 'draw'>('canvas');
const pixelEditorFrame = ref<{ layerId: string; frameIndex: number } | null>(null);
const isSpritesheetSplitterOpen = ref(false);
const isGifFpsModalOpen = ref(false);
@@ -631,6 +650,46 @@
});
};
const handleSaveFrame = (layerId: string, frameIndex: number, file: File) => {
const layer = layers.value.find(l => l.id === layerId);
if (!layer) return;
const reader = new FileReader();
reader.onload = e => {
const url = e.target?.result as string;
const img = new Image();
img.onload = () => {
const oldSprite = layer.sprites[frameIndex];
const sprite = {
id: oldSprite?.id || crypto.randomUUID(),
file,
img,
url,
width: img.width,
height: img.height,
x: oldSprite?.x || 0,
y: oldSprite?.y || 0,
rotation: oldSprite?.rotation || 0,
flipX: oldSprite?.flipX || false,
flipY: oldSprite?.flipY || false,
};
if (frameIndex < layer.sprites.length) {
layer.sprites[frameIndex] = sprite;
}
};
img.src = url;
};
reader.readAsDataURL(file);
pixelEditorFrame.value = null;
};
const openPixelEditor = (layerId: string, frameIndex: number) => {
pixelEditorFrame.value = { layerId, frameIndex };
activeTab.value = 'draw';
};
onMounted(async () => {
const id = route.params.id as string;
if (id) {