[FEAT] Add add new frame btn

This commit is contained in:
2026-01-03 17:21:15 +01:00
parent 2f0404d698
commit e290eb21a4
2 changed files with 260 additions and 274 deletions

View File

@@ -15,12 +15,7 @@
class="aspect-square bg-white dark:bg-gray-800 rounded-lg border-2 border-transparent hover:border-indigo-500 transition-all overflow-hidden group relative" class="aspect-square bg-white dark:bg-gray-800 rounded-lg border-2 border-transparent hover:border-indigo-500 transition-all overflow-hidden group relative"
:class="{ 'opacity-50': !sprite.url }" :class="{ 'opacity-50': !sprite.url }"
> >
<img <img v-if="sprite.url" :src="sprite.url" class="w-full h-full object-contain relative z-10" :style="{ imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto' }" />
v-if="sprite.url"
:src="sprite.url"
class="w-full h-full object-contain relative z-10"
:style="{ imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto' }"
/>
<div v-else class="w-full h-full flex items-center justify-center text-gray-400 relative z-10"> <div v-else class="w-full h-full flex items-center justify-center text-gray-400 relative z-10">
<i class="fas fa-image"></i> <i class="fas fa-image"></i>
</div> </div>
@@ -29,6 +24,17 @@
</span> </span>
<i class="fas fa-edit text-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity text-[10px] absolute top-1 left-1 z-20"></i> <i class="fas fa-edit text-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity text-[10px] absolute top-1 left-1 z-20"></i>
</button> </button>
<!-- Add New Frame Button -->
<button
@click="addNewFrame(layer)"
class="aspect-square bg-gray-50 dark:bg-gray-800/50 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-700 hover:border-indigo-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-all flex items-center justify-center text-gray-400 hover:text-indigo-500 group"
>
<div class="flex flex-col items-center gap-1">
<i class="fas fa-plus text-xl group-hover:scale-110 transition-transform"></i>
<span class="text-[10px] font-medium uppercase tracking-wide">Add</span>
</div>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -41,20 +47,12 @@
<!-- Tools --> <!-- Tools -->
<div class="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1"> <div class="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
<Tooltip text="Pencil (P)"> <Tooltip text="Pencil (P)">
<button <button @click="editor.currentTool.value = 'pencil'" :class="editor.currentTool.value === 'pencil' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'" class="p-2 rounded-md transition-all">
@click="editor.currentTool.value = 'pencil'"
:class="editor.currentTool.value === 'pencil' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'"
class="p-2 rounded-md transition-all"
>
<i class="fas fa-pencil-alt text-gray-600 dark:text-gray-300"></i> <i class="fas fa-pencil-alt text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip text="Eraser (E)"> <Tooltip text="Eraser (E)">
<button <button @click="editor.currentTool.value = 'eraser'" :class="editor.currentTool.value === 'eraser' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'" class="p-2 rounded-md transition-all">
@click="editor.currentTool.value = 'eraser'"
:class="editor.currentTool.value === 'eraser' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'"
class="p-2 rounded-md transition-all"
>
<i class="fas fa-eraser text-gray-600 dark:text-gray-300"></i> <i class="fas fa-eraser text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
@@ -64,29 +62,15 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="relative group"> <div class="relative group">
<Tooltip text="Current Color"> <Tooltip text="Current Color">
<div <div class="w-8 h-8 rounded-lg border-2 border-gray-300 dark:border-gray-600 cursor-pointer shadow-sm overflow-hidden" :style="{ backgroundColor: editor.currentColor.value }">
class="w-8 h-8 rounded-lg border-2 border-gray-300 dark:border-gray-600 cursor-pointer shadow-sm overflow-hidden" <input type="color" v-model="editor.currentColor.value" class="absolute inset-0 opacity-0 cursor-pointer w-full h-full" />
:style="{ backgroundColor: editor.currentColor.value }"
>
<input
type="color"
v-model="editor.currentColor.value"
class="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
/>
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
<!-- Recent Colors --> <!-- Recent Colors -->
<div class="flex flex-wrap gap-1 w-20"> <div class="flex flex-wrap gap-1 w-20">
<div <div v-for="color in recentColors.slice(0, 8)" :key="color" class="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-pointer hover:scale-110 transition-transform" :style="{ backgroundColor: color }" @click="editor.currentColor.value = color" :title="color"></div>
v-for="color in recentColors.slice(0, 8)"
:key="color"
class="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-pointer hover:scale-110 transition-transform"
:style="{ backgroundColor: color }"
@click="editor.currentColor.value = color"
:title="color"
></div>
</div> </div>
</div> </div>
@@ -113,18 +97,12 @@
<!-- Canvas Size --> <!-- Canvas Size -->
<Tooltip text="Resize Canvas"> <Tooltip text="Resize Canvas">
<button <button @click="showResizeModal = true" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
@click="showResizeModal = true"
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<i class="fas fa-expand-arrows-alt text-gray-600 dark:text-gray-300"></i> <i class="fas fa-expand-arrows-alt text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip text="Trim Empty Space"> <Tooltip text="Trim Empty Space">
<button <button @click="editor.trimCanvas()" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
@click="editor.trimCanvas()"
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<i class="fas fa-crop-alt text-gray-600 dark:text-gray-300"></i> <i class="fas fa-crop-alt text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
@@ -134,22 +112,12 @@
<!-- History --> <!-- History -->
<Tooltip text="Undo (Ctrl+Z)"> <Tooltip text="Undo (Ctrl+Z)">
<button <button @click="editor.undo()" :disabled="!editor.canUndo.value" :class="editor.canUndo.value ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-40 cursor-not-allowed'" class="p-2 rounded-lg transition-colors">
@click="editor.undo()"
:disabled="!editor.canUndo.value"
:class="editor.canUndo.value ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-40 cursor-not-allowed'"
class="p-2 rounded-lg transition-colors"
>
<i class="fas fa-undo text-gray-600 dark:text-gray-300"></i> <i class="fas fa-undo text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip text="Redo (Ctrl+Shift+Z)"> <Tooltip text="Redo (Ctrl+Shift+Z)">
<button <button @click="editor.redo()" :disabled="!editor.canRedo.value" :class="editor.canRedo.value ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-40 cursor-not-allowed'" class="p-2 rounded-lg transition-colors">
@click="editor.redo()"
:disabled="!editor.canRedo.value"
:class="editor.canRedo.value ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-40 cursor-not-allowed'"
class="p-2 rounded-lg transition-colors"
>
<i class="fas fa-redo text-gray-600 dark:text-gray-300"></i> <i class="fas fa-redo text-gray-600 dark:text-gray-300"></i>
</button> </button>
</Tooltip> </Tooltip>
@@ -167,14 +135,10 @@
</label> </label>
<!-- Pointer Location Display --> <!-- Pointer Location Display -->
<div v-if="editor.showPointerLocation.value" class="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded min-w-[60px] text-center"> <div v-if="editor.showPointerLocation.value" class="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded min-w-[60px] text-center">{{ editor.pointerX.value }}, {{ editor.pointerY.value }}</div>
{{ editor.pointerX.value }}, {{ editor.pointerY.value }}
</div>
<!-- Canvas Size --> <!-- Canvas Size -->
<div class="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"> <div class="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">{{ editor.canvasWidth.value }} × {{ editor.canvasHeight.value }}</div>
{{ editor.canvasWidth.value }} × {{ editor.canvasHeight.value }}
</div>
</div> </div>
</div> </div>
@@ -189,10 +153,7 @@
}" }"
> >
<!-- Background (checkerboard or solid) --> <!-- Background (checkerboard or solid) -->
<div <div class="absolute inset-0 pointer-events-none" :style="getBackgroundStyle()"></div>
class="absolute inset-0 pointer-events-none"
:style="getBackgroundStyle()"
></div>
<!-- Canvas --> <!-- Canvas -->
<canvas <canvas
@@ -228,20 +189,10 @@
<!-- Footer Actions --> <!-- Footer Actions -->
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-b-xl"> <div class="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-b-xl">
<Tooltip text="Discard changes"> <Tooltip text="Discard changes">
<button <button @click="cancelEdit" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors font-medium text-sm">Cancel</button>
@click="cancelEdit"
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors font-medium text-sm"
>
Cancel
</button>
</Tooltip> </Tooltip>
<Tooltip text="Save changes to frame"> <Tooltip text="Save changes to frame">
<button <button @click="saveEdit" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium text-sm"><i class="fas fa-save mr-2"></i>Save</button>
@click="saveEdit"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium text-sm"
>
<i class="fas fa-save mr-2"></i>Save
</button>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@@ -252,23 +203,11 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Width</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Width</label>
<input <input type="number" v-model.number="resizeWidth" min="1" max="1024" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" />
type="number"
v-model.number="resizeWidth"
min="1"
max="1024"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Height</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Height</label>
<input <input type="number" v-model.number="resizeHeight" min="1" max="1024" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" />
type="number"
v-model.number="resizeHeight"
min="1"
max="1024"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div> </div>
</div> </div>
@@ -288,12 +227,8 @@
</div> </div>
<div class="flex justify-end gap-3 pt-4 border-t border-gray-100 dark:border-gray-700"> <div class="flex justify-end gap-3 pt-4 border-t border-gray-100 dark:border-gray-700">
<button @click="showResizeModal = false" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors font-medium text-sm"> <button @click="showResizeModal = false" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors font-medium text-sm">Cancel</button>
Cancel <button @click="applyResize" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium text-sm">Apply</button>
</button>
<button @click="applyResize" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium text-sm">
Apply
</button>
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -301,204 +236,258 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'; import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
import { usePixelEditor } from '@/composables/usePixelEditor'; import { usePixelEditor } from '@/composables/usePixelEditor';
import { useSettingsStore } from '@/stores/useSettingsStore'; import { useSettingsStore } from '@/stores/useSettingsStore';
import Modal from './utilities/Modal.vue'; import Modal from './utilities/Modal.vue';
import Tooltip from './utilities/Tooltip.vue'; import Tooltip from './utilities/Tooltip.vue';
import type { Layer, Sprite } from '@/types/sprites'; import type { Layer, Sprite } from '@/types/sprites';
interface SelectedFrame { interface SelectedFrame {
layerId: string; layerId: string;
frameIndex: number; frameIndex: number;
sprite: Sprite; sprite?: Sprite;
}
const props = defineProps<{
layers: Layer[];
initialFrame?: { layerId: string; frameIndex: number } | null;
}>();
const emit = defineEmits<{
(e: 'saveFrame', layerId: string, frameIndex: number, file: File): void;
(e: 'close'): void;
}>();
const settingsStore = useSettingsStore();
const editor = usePixelEditor();
const selectedFrame = ref<SelectedFrame | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const showResizeModal = ref(false);
const resizeWidth = ref(32);
const resizeHeight = ref(32);
const resizeAnchor = ref('top-left');
const recentColors = ref<string[]>(['#ffffff', '#000000', '#ff0000', '#00ff00', '#0000ff']);
watch(
() => editor.currentColor.value,
(newColor) => {
if (!newColor) return;
// Add to recent colors if not already at the start
if (recentColors.value[0] !== newColor) {
recentColors.value = [newColor, ...recentColors.value.filter((c) => c !== newColor)].slice(0, 10);
}
} }
);
const anchors = [ const props = defineProps<{
{ value: 'top-left', icon: 'fas fa-arrow-up -rotate-45' }, layers: Layer[];
{ value: 'top-center', icon: 'fas fa-arrow-up' }, initialFrame?: { layerId: string; frameIndex: number } | null;
{ value: 'top-right', icon: 'fas fa-arrow-up rotate-45' }, }>();
{ value: 'middle-left', icon: 'fas fa-arrow-left' },
{ value: 'center-middle', icon: 'fas fa-circle text-[6px]' },
{ value: 'middle-right', icon: 'fas fa-arrow-right' },
{ value: 'bottom-left', icon: 'fas fa-arrow-down rotate-45' },
{ value: 'bottom-center', icon: 'fas fa-arrow-down' },
{ value: 'bottom-right', icon: 'fas fa-arrow-down -rotate-45' },
];
const getCheckerboardPattern = () => { const emit = defineEmits<{
return `conic-gradient( (e: 'saveFrame', layerId: string, frameIndex: number, file: File): void;
(e: 'close'): void;
}>();
const settingsStore = useSettingsStore();
const editor = usePixelEditor();
const selectedFrame = ref<SelectedFrame | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const showResizeModal = ref(false);
const resizeWidth = ref(32);
const resizeHeight = ref(32);
const resizeAnchor = ref('top-left');
const recentColors = ref<string[]>(['#ffffff', '#000000', '#ff0000', '#00ff00', '#0000ff']);
watch(
() => editor.currentColor.value,
newColor => {
if (!newColor) return;
// Add to recent colors if not already at the start
if (recentColors.value[0] !== newColor) {
recentColors.value = [newColor, ...recentColors.value.filter(c => c !== newColor)].slice(0, 10);
}
}
);
const anchors = [
{ value: 'top-left', icon: 'fas fa-arrow-up -rotate-45' },
{ value: 'top-center', icon: 'fas fa-arrow-up' },
{ value: 'top-right', icon: 'fas fa-arrow-up rotate-45' },
{ value: 'middle-left', icon: 'fas fa-arrow-left' },
{ value: 'center-middle', icon: 'fas fa-circle text-[6px]' },
{ value: 'middle-right', icon: 'fas fa-arrow-right' },
{ value: 'bottom-left', icon: 'fas fa-arrow-down rotate-45' },
{ value: 'bottom-center', icon: 'fas fa-arrow-down' },
{ value: 'bottom-right', icon: 'fas fa-arrow-down -rotate-45' },
];
const getCheckerboardPattern = () => {
return `conic-gradient(
#e0e0e0 0.25turn, #e0e0e0 0.25turn,
transparent 0.25turn 0.5turn, transparent 0.25turn 0.5turn,
#e0e0e0 0.5turn 0.75turn, #e0e0e0 0.5turn 0.75turn,
transparent 0.75turn transparent 0.75turn
)`; )`;
};
const getBackgroundStyle = () => {
const zoom = editor.zoom.value;
const isTransparent = settingsStore.backgroundColor === 'transparent';
// If checkerboard is enabled, show standard white/grey checkerboard
if (editor.showCheckerboard.value) {
return {
backgroundColor: '#ffffff',
backgroundImage: getCheckerboardPattern(),
backgroundSize: `${zoom * 2}px ${zoom * 2}px`,
backgroundPosition: '0 0',
};
}
// Otherwise show the configured background color (default to white if transparent)
return {
backgroundColor: isTransparent ? '#ffffff' : settingsStore.backgroundColor,
backgroundImage: 'none',
}; };
};
const selectFrame = async (layerId: string, frameIndex: number, sprite: Sprite) => { const getBackgroundStyle = () => {
selectedFrame.value = { layerId, frameIndex, sprite }; const zoom = editor.zoom.value;
const isTransparent = settingsStore.backgroundColor === 'transparent';
await nextTick(); // If checkerboard is enabled, show standard white/grey checkerboard
if (editor.showCheckerboard.value) {
if (canvasRef.value) { return {
editor.initCanvas(canvasRef.value); backgroundColor: '#ffffff',
if (sprite.url) { backgroundImage: getCheckerboardPattern(),
await editor.loadFromImage(sprite.url); backgroundSize: `${zoom * 2}px ${zoom * 2}px`,
backgroundPosition: '0 0',
};
} }
}
};
const cancelEdit = () => { // Otherwise show the configured background color (default to white if transparent)
selectedFrame.value = null; return {
}; backgroundColor: isTransparent ? '#ffffff' : settingsStore.backgroundColor,
backgroundImage: 'none',
};
};
const saveEdit = async () => { const selectFrame = async (layerId: string, frameIndex: number, sprite: Sprite) => {
if (!selectedFrame.value) return; selectedFrame.value = { layerId, frameIndex, sprite };
const file = await editor.toFile(`frame-${selectedFrame.value.frameIndex}.png`); await nextTick();
if (file) {
emit('saveFrame', selectedFrame.value.layerId, selectedFrame.value.frameIndex, file);
}
selectedFrame.value = null;
};
const handleMouseMove = (event: MouseEvent) => { if (canvasRef.value) {
editor.continueDrawing(event); // If sprite exists, use its dimensions
editor.updatePointer(event); if (sprite.width && sprite.height) {
}; editor.canvasWidth.value = sprite.width;
editor.canvasHeight.value = sprite.height;
}
const applyResize = () => { editor.initCanvas(canvasRef.value);
editor.resizeCanvas(resizeWidth.value, resizeHeight.value, resizeAnchor.value); if (sprite.url) {
showResizeModal.value = false; await editor.loadFromImage(sprite.url);
};
// Keyboard shortcuts
const handleKeyDown = (event: KeyboardEvent) => {
if (!selectedFrame.value) return;
// Tool shortcuts
if (event.key === 'p' || event.key === 'P') {
editor.currentTool.value = 'pencil';
} else if (event.key === 'e' || event.key === 'E') {
editor.currentTool.value = 'eraser';
}
// Undo/Redo
if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
if (event.shiftKey) {
editor.redo();
} else {
editor.undo();
}
event.preventDefault();
}
// Escape to cancel
if (event.key === 'Escape') {
cancelEdit();
}
};
// Watch for resize modal opening to set current dimensions
watch(showResizeModal, (isOpen) => {
if (isOpen) {
resizeWidth.value = editor.canvasWidth.value;
resizeHeight.value = editor.canvasHeight.value;
}
});
// Handle initial frame passed from context menu
watch(
() => props.initialFrame,
async (frame) => {
if (frame) {
const layer = props.layers.find(l => l.id === frame.layerId);
if (layer && layer.sprites[frame.frameIndex]) {
await selectFrame(frame.layerId, frame.frameIndex, layer.sprites[frame.frameIndex]);
} }
} }
}, };
{ immediate: true }
);
onMounted(() => { const addNewFrame = async (layer: Layer) => {
window.addEventListener('keydown', handleKeyDown); // Determine canvas size based on "biggest layer" logic
}); // Priority 1: Max dimensions of sprites in current layer
// Priority 2: Max dimensions of sprites in any layer
// Priority 3: Default 32x32
onUnmounted(() => { let targetWidth = 0;
window.removeEventListener('keydown', handleKeyDown); let targetHeight = 0;
});
// Check current layer
if (layer.sprites.length > 0) {
targetWidth = Math.max(...layer.sprites.map(s => s.width));
targetHeight = Math.max(...layer.sprites.map(s => s.height));
}
// If still 0, check all layers
if (targetWidth === 0 || targetHeight === 0) {
props.layers.forEach(l => {
if (l.sprites.length > 0) {
targetWidth = Math.max(targetWidth, ...l.sprites.map(s => s.width));
targetHeight = Math.max(targetHeight, ...l.sprites.map(s => s.height));
}
});
}
// Default if fully empty project
if (targetWidth === 0) targetWidth = 32;
if (targetHeight === 0) targetHeight = 32;
// Set editor size
editor.canvasWidth.value = targetWidth;
editor.canvasHeight.value = targetHeight;
// Set selected frame (new)
selectedFrame.value = {
layerId: layer.id,
frameIndex: layer.sprites.length,
sprite: undefined,
};
await nextTick();
if (canvasRef.value) {
editor.initCanvas(canvasRef.value);
editor.clearCanvas(); // Ensure clean start
}
};
const cancelEdit = () => {
selectedFrame.value = null;
};
const saveEdit = async () => {
if (!selectedFrame.value) return;
const file = await editor.toFile(`frame-${selectedFrame.value.frameIndex}.png`);
if (file) {
emit('saveFrame', selectedFrame.value.layerId, selectedFrame.value.frameIndex, file);
}
selectedFrame.value = null;
};
const handleMouseMove = (event: MouseEvent) => {
editor.continueDrawing(event);
editor.updatePointer(event);
};
const applyResize = () => {
editor.resizeCanvas(resizeWidth.value, resizeHeight.value, resizeAnchor.value);
showResizeModal.value = false;
};
// Keyboard shortcuts
const handleKeyDown = (event: KeyboardEvent) => {
if (!selectedFrame.value) return;
// Tool shortcuts
if (event.key === 'p' || event.key === 'P') {
editor.currentTool.value = 'pencil';
} else if (event.key === 'e' || event.key === 'E') {
editor.currentTool.value = 'eraser';
}
// Undo/Redo
if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
if (event.shiftKey) {
editor.redo();
} else {
editor.undo();
}
event.preventDefault();
}
// Escape to cancel
if (event.key === 'Escape') {
cancelEdit();
}
};
// Watch for resize modal opening to set current dimensions
watch(showResizeModal, isOpen => {
if (isOpen) {
resizeWidth.value = editor.canvasWidth.value;
resizeHeight.value = editor.canvasHeight.value;
}
});
// Handle initial frame passed from context menu
watch(
() => props.initialFrame,
async frame => {
if (frame) {
const layer = props.layers.find(l => l.id === frame.layerId);
if (layer && layer.sprites[frame.frameIndex]) {
await selectFrame(frame.layerId, frame.frameIndex, layer.sprites[frame.frameIndex]);
}
}
},
{ immediate: true }
);
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
});
</script> </script>
<style scoped> <style scoped>
.draw-tab { .draw-tab {
/* Custom scrollbar */ /* Custom scrollbar */
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 6px; width: 6px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5); background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px; border-radius: 3px;
}
} }
}
</style> </style>

View File

@@ -329,12 +329,7 @@
/> />
</div> </div>
<div v-else-if="activeTab === 'draw'" class="h-full"> <div v-else-if="activeTab === 'draw'" class="h-full">
<draw-tab <draw-tab :layers="layers" :initial-frame="pixelEditorFrame" @save-frame="handleSaveFrame" @close="pixelEditorFrame = null" />
:layers="layers"
:initial-frame="pixelEditorFrame"
@save-frame="handleSaveFrame"
@close="pixelEditorFrame = null"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -676,6 +671,8 @@
if (frameIndex < layer.sprites.length) { if (frameIndex < layer.sprites.length) {
layer.sprites[frameIndex] = sprite; layer.sprites[frameIndex] = sprite;
} else {
layer.sprites.push(sprite);
} }
}; };
img.src = url; img.src = url;