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