forked from noxious/client
Worked on sprite manager
This commit is contained in:
parent
47474b3d97
commit
1782ad0bc1
102
package-lock.json
generated
102
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vue": "^3.4.33",
|
||||
"vue-draggable-plus": "^0.5.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -44,7 +45,7 @@
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-vue-devtools": "^7.3.6",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^2.0.26"
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@ -1772,6 +1773,12 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sortablejs": {
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
@ -2097,32 +2104,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz",
|
||||
"integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
|
||||
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "2.4.0-alpha.18"
|
||||
"@volar/source-map": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "2.4.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz",
|
||||
"integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "2.4.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz",
|
||||
"integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
|
||||
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.0-alpha.18",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
"muggle-string": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
|
||||
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "1.11.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||
@ -2344,18 +2353,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.28.tgz",
|
||||
"integrity": "sha512-0z4tyCCaqqPbdyz0T4yTFQeLpCo4TOM/ZHAC3geGLHeCiFAjVbROB9PiEtrXR1AoLObqUPFHSmKZeWtEMssSqw==",
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
|
||||
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~2.4.0-alpha.18",
|
||||
"@vue/compiler-dom": "^3.4.0",
|
||||
"@vue/shared": "^3.4.0",
|
||||
"@volar/language-core": "~1.11.1",
|
||||
"@volar/source-map": "~1.11.1",
|
||||
"@vue/compiler-dom": "^3.3.0",
|
||||
"@vue/shared": "^3.3.0",
|
||||
"computeds": "^0.0.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.4.1",
|
||||
"muggle-string": "^0.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
},
|
||||
@ -5067,9 +5077,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
|
||||
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -7170,13 +7180,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.4.34",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.34.tgz",
|
||||
@ -7205,6 +7208,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue-draggable-plus": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.5.2.tgz",
|
||||
"integrity": "sha512-+EWOYOtY6MVbhPpG+H4rjyuWAJiQU2VoLI/ehOCZkz62anrKrGHz5CBL2HDXi01hpNSP9BTDgkBcibhdO9/Jgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/sortablejs": "^1.15.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/sortablejs": "^1.15.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "9.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
||||
@ -7242,21 +7262,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.28.tgz",
|
||||
"integrity": "sha512-PQ/OFDM3NtQVMThaVlQf8plyL0j7UGdak4lb1KkUOSL0uyx/F9Liu6aOclgHiMMBKNGIjJWoiFh3HjIdV6DS/Q==",
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
|
||||
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~2.4.0-alpha.18",
|
||||
"@vue/language-core": "2.0.28",
|
||||
"@volar/typescript": "~1.11.1",
|
||||
"@vue/language-core": "1.8.27",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
|
@ -23,6 +23,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vue": "^3.4.33",
|
||||
"vue-draggable-plus": "^0.5.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -51,6 +52,6 @@
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-vue-devtools": "^7.3.6",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^2.0.26"
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="border border-gray-300 rounded mb-4">
|
||||
<div @click="toggle" class="w-[98%] p-3 bg-gray-100 bg-opacity-50 rounded hover:bg-gray-200 text-left text-white font-default cursor-pointer transition-colors duration-200 ease-in-out">
|
||||
<div @click="toggle" class="p-3 bg-gray-100 bg-opacity-50 rounded hover:bg-gray-200 text-left text-white font-default cursor-pointer transition-colors duration-200 ease-in-out">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0">
|
||||
<div v-if="isOpen" class="p-3 overflow-hidden">
|
||||
<div v-if="isOpen" class="py-3 overflow-hidden">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -1,72 +1,73 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-between">
|
||||
<div class="w-full flex flex-col sm:flex-row items-center gap-9 mb-5">
|
||||
<div class="w-full sm:flex-grow">
|
||||
<input v-model="spriteName" class="input-cyan w-full" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
<div class="flex gap-2 w-full sm:w-auto">
|
||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||
<button class="btn-bordeaux px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="removeSprite">Remove</button>
|
||||
<div class="relative p-4 flex flex-col">
|
||||
<div class="gap-2.5 flex flex-wrap">
|
||||
<div class="w-full flex flex-col mb-1">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="spriteName" class="input-cyan" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-px bg-cyan-200"></div>
|
||||
</div>
|
||||
<div class="m-2.5 px-2.5 block">
|
||||
<button class="btn-cyan px-4 py-1.5 flex-1 sm:flex-none sm:min-w-24 mb-5" type="button" @click.prevent="addNewImage">New IMG</button>
|
||||
<Accordion v-for="image in spriteImages" :key="image.id">
|
||||
<div class="flex gap-2 py-2">
|
||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||
<button class="btn-bordeaux px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="removeSprite">Remove</button>
|
||||
</div>
|
||||
<div class="w-full mb-2 h-px bg-cyan-200"></div>
|
||||
<button class="btn-cyan px-4 py-1.5 flex-1 sm:flex-none sm:min-w-24 mb-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
{{ image.name }}
|
||||
{{ action.action }}
|
||||
<div class="ml-auto flex gap-2">
|
||||
<label for="upload-asset" class="text-sm bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 px-4 py-1.5 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
|
||||
<input class="hidden" id="upload-asset" type="file" accept="image/png" @change="(event) => handleImageUpload(event, image)" />
|
||||
Set image
|
||||
</label>
|
||||
<button class="btn-bordeaux px-4 py-1.5 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="() => spriteImages.splice(spriteImages.indexOf(image), 1)">Remove</button>
|
||||
<button class="btn-bordeaux px-4 py-1.5 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="image.name" class="input-cyan" type="text" name="name" placeholder="Wall #1" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<div class="w-full flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="action">Action</label>
|
||||
<input v-model="image.action" class="input-cyan" type="text" name="action" placeholder="Action" />
|
||||
<input v-model="action.action" class="input-cyan" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="origin-x">Origin X</label>
|
||||
<input v-model.number="image.origin_x" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
<input v-model.number="action.origin_x" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="origin-y">Origin Y</label>
|
||||
<input v-model.number="image.origin_y" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="w-full flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-speed">Frame speed</label>
|
||||
<input v-model.number="image.frameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
<input v-model.number="action.origin_y" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-width">Frame width</label>
|
||||
<input v-model.number="image.frameWidth" class="input-cyan" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-height">Frame height</label>
|
||||
<input v-model.number="image.frameHeight" class="input-cyan" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<div class="w-full flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="is-looping">Is looping</label>
|
||||
<select v-model="image.isLooping" class="input-cyan" name="is-looping">
|
||||
<label class="mb-1.5 font-titles" for="is-animated">Is animated</label>
|
||||
<select v-model="action.isAnimated" class="input-cyan" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="image.base64">
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="is-looping">Is looping</label>
|
||||
<select v-model="action.isLooping" class="input-cyan" name="is-looping">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-full flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-speed">Frame speed</label>
|
||||
<input v-model.number="action.frameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-width">Frame width</label>
|
||||
<input v-model.number="action.frameWidth" class="input-cyan" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="w-[calc(50%_-_5px)] flex flex-col mb-5">
|
||||
<label class="mb-1.5 font-titles" for="frame-height">Frame height</label>
|
||||
<input v-model.number="action.frameHeight" class="input-cyan" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<div class="w-full flex mb-5 gap-2">
|
||||
<SpriteActionsInput v-model="action.spriteActions" />
|
||||
</div>
|
||||
<div v-if="action.base64">
|
||||
<div class="w-full h-px bg-cyan-200"></div>
|
||||
<img class="max-h-56" :src="`data:image/png;base64,${image.base64}`" />
|
||||
<img class="max-h-56" :src="`data:image/png;base64,${action.base64}`" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@ -76,11 +77,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Sprite, SpriteImage } from '@/types'
|
||||
import type { Sprite, spriteAction } from '@/types'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManager'
|
||||
import { useGameStore } from '@/stores/game'
|
||||
import Accordion from '@/components/utilities/Accordion.vue'
|
||||
import SpriteActionsInput from '@/components/utilities/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
@ -89,11 +91,11 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||
|
||||
const spriteName = ref('')
|
||||
|
||||
type uploadSpriteImage = SpriteImage & {
|
||||
type uploadSpriteAction = spriteAction & {
|
||||
base64?: string
|
||||
}
|
||||
|
||||
const spriteImages = ref<uploadSpriteImage[]>([])
|
||||
const spriteActions = ref<uploadSpriteAction[]>([])
|
||||
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
@ -101,19 +103,7 @@ if (!selectedSprite.value) {
|
||||
|
||||
if (selectedSprite.value) {
|
||||
spriteName.value = selectedSprite.value.name
|
||||
spriteImages.value = selectedSprite.value.spriteImages
|
||||
}
|
||||
|
||||
function handleImageUpload(event: Event, image: uploadSpriteImage) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target?.result as string
|
||||
image.base64 = base64String.split(',')[1] // Remove the data:image/png;base64, prefix
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
spriteActions.value = selectedSprite.value.spriteActions
|
||||
}
|
||||
|
||||
function removeSprite() {
|
||||
@ -145,7 +135,7 @@ function saveSprite() {
|
||||
const updatedSprite = {
|
||||
id: selectedSprite.value.id,
|
||||
name: spriteName.value,
|
||||
spriteImages: spriteImages.value
|
||||
spriteActions: spriteActions.value
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
||||
@ -160,9 +150,9 @@ function saveSprite() {
|
||||
function addNewImage() {
|
||||
if (!selectedSprite.value) return
|
||||
|
||||
const newImage: SpriteImage = {
|
||||
const newImage: spriteAction = {
|
||||
id: Date.now().toString(), // Temporary ID, should be replaced by server-generated ID
|
||||
name: 'New image',
|
||||
action: 'new_action',
|
||||
origin_x: 0,
|
||||
origin_y: 0,
|
||||
frameSpeed: 0,
|
||||
@ -171,22 +161,21 @@ function addNewImage() {
|
||||
isAnimated: false,
|
||||
spriteId: selectedSprite.value.id,
|
||||
sprite: selectedSprite.value,
|
||||
action: '',
|
||||
isLooping: false
|
||||
}
|
||||
|
||||
// spriteimages value can be undefined
|
||||
if (!spriteImages.value) {
|
||||
spriteImages.value = []
|
||||
if (!spriteActions.value) {
|
||||
spriteActions.value = []
|
||||
}
|
||||
|
||||
spriteImages.value.push(newImage)
|
||||
spriteActions.value.push(newImage)
|
||||
}
|
||||
|
||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||
if (!sprite) return
|
||||
spriteName.value = sprite.name
|
||||
spriteImages.value = sprite.spriteImages
|
||||
spriteActions.value = sprite.spriteActions
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div v-for="(image, index) in images" :key="image.id" class="min-h-24 min-w-24 p-3 bg-gray-50 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||
<img :src="image.base64" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||
<button @click.stop="deleteImage(image.id)" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="min-h-24 min-w-24 p-3 bg-gray-50 bg-opacity-30 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" ref="fileInput" @change="onFileChange" multiple accept="image/*" class="hidden" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface ImageItem {
|
||||
id: string
|
||||
base64: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: ImageItem[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: ImageItem[]): void
|
||||
}>()
|
||||
|
||||
const images = ref<ImageItem[]>(props.modelValue)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const draggedIndex = ref<number | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
images.value = newValue
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const onFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files) {
|
||||
handleFiles(target.files)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
if (event.dataTransfer?.files) {
|
||||
handleFiles(event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFiles = (files: FileList) => {
|
||||
Array.from(files).forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
const newImage: ImageItem = {
|
||||
id: Date.now().toString() + Math.random().toString(36).substring(2, 15),
|
||||
base64: e.target.result
|
||||
}
|
||||
updateImages([...images.value, newImage])
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateImages = (newImages: ImageItem[]) => {
|
||||
images.value = newImages
|
||||
emit('update:modelValue', newImages)
|
||||
}
|
||||
|
||||
const deleteImage = (id: string) => {
|
||||
const newImages = images.value.filter((img) => img.id !== id)
|
||||
updateImages(newImages)
|
||||
}
|
||||
|
||||
const dragStart = (event: DragEvent, index: number) => {
|
||||
draggedIndex.value = index
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const drop = (event: DragEvent, dropIndex: number) => {
|
||||
event.preventDefault()
|
||||
if (draggedIndex.value !== null && draggedIndex.value !== dropIndex) {
|
||||
const newImages = [...images.value]
|
||||
const [reorderedItem] = newImages.splice(draggedIndex.value, 1)
|
||||
newImages.splice(dropIndex, 0, reorderedItem)
|
||||
updateImages(newImages)
|
||||
}
|
||||
draggedIndex.value = null
|
||||
}
|
||||
</script>
|
@ -152,15 +152,14 @@ export type Sprite = {
|
||||
name: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
spriteImages: SpriteImage[]
|
||||
spriteActions: spriteAction[]
|
||||
characterTypes: CharacterType[]
|
||||
}
|
||||
|
||||
export type SpriteImage = {
|
||||
export type spriteAction = {
|
||||
id: string
|
||||
spriteId: string
|
||||
sprite: Sprite
|
||||
name: string
|
||||
action: string
|
||||
origin_x: number
|
||||
origin_y: number
|
||||
|
Loading…
x
Reference in New Issue
Block a user