Improvement
This commit is contained in:
parent
b8b985470f
commit
f79ebedc62
@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="isOpen">
|
|
||||||
<div class="flex flex-col gap-2.5 p-2.5">
|
|
||||||
<div class="relative flex">
|
|
||||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
|
||||||
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<select class="input-field w-full" name="lists" v-model="lists">
|
|
||||||
<option value="tile">Tiles</option>
|
|
||||||
<option value="map_object">Objects</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
|
||||||
<TileList v-if="mapEditor.drawMode.value === 'tile'" />
|
|
||||||
<ObjectList v-if="mapEditor.drawMode.value === 'map_object'" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
|
||||||
<span>Tags:</span>
|
|
||||||
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
|
||||||
<span class="m-auto">No tags selected</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Tile } from '@/application/types'
|
|
||||||
import ObjectList from '@/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue'
|
|
||||||
import TileList from '@/components/gameMaster/mapEditor/partials/lists/TileList.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { TileStorage } from '@/storage/storages'
|
|
||||||
import { liveQuery } from 'dexie'
|
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const tiles = ref<Tile[]>([])
|
|
||||||
const lists = ref<string>(mapEditor.drawMode.value)
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(lists, (list) => {
|
|
||||||
mapEditor.setDrawMode(list)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(mapEditor.drawMode, (list) => {
|
|
||||||
lists.value = list
|
|
||||||
})
|
|
||||||
|
|
||||||
let subscription: any = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
tiles.value = result
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (subscription) {
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
107
src/components/gameMaster/mapEditor/partials/MapObjectList.vue
Normal file
107
src/components/gameMaster/mapEditor/partials/MapObjectList.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="isOpen">
|
||||||
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
|
<div class="relative flex">
|
||||||
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="input-field w-full" name="lists">
|
||||||
|
<option value="tile">Tiles</option>
|
||||||
|
<option value="map_object">Objects</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
||||||
|
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
||||||
|
<img
|
||||||
|
class="border-2 border-solid rounded max-w-full"
|
||||||
|
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
||||||
|
alt="Object"
|
||||||
|
@click="mapEditor.setSelectedMapObject(mapObject)"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer transition-all duration-300': true,
|
||||||
|
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
||||||
|
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
||||||
|
<span>Tags:</span>
|
||||||
|
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
||||||
|
<span class="m-auto">No tags selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { MapObject } from '@/application/types'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
|
import { liveQuery } from 'dexie'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedTags = ref<string[]>([])
|
||||||
|
const mapObjectList = ref<MapObject[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => (isOpen.value = true),
|
||||||
|
close: () => (isOpen.value = false),
|
||||||
|
toggle: () => (isOpen.value = !isOpen.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniqueTags = computed(() => {
|
||||||
|
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
||||||
|
return Array.from(new Set(allTags))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredMapObjects = computed(() => {
|
||||||
|
return mapObjectList.value.filter((object) => {
|
||||||
|
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleTag = (tag: string) => {
|
||||||
|
if (selectedTags.value.includes(tag)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscription: any = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isModalOpen.value = true
|
||||||
|
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
mapObjectList.value = result
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Failed to fetch tiles:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
173
src/components/gameMaster/mapEditor/partials/TileList.vue
Normal file
173
src/components/gameMaster/mapEditor/partials/TileList.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800">
|
||||||
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
|
<div class="relative flex">
|
||||||
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="input-field w-full" name="lists">
|
||||||
|
<option value="tile">Tiles</option>
|
||||||
|
<option value="map_object">Objects</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
|
<div class="h-full" v-if="!selectedGroup">
|
||||||
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
|
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
|
:alt="group.parent.name"
|
||||||
|
@click="openGroup(group)"
|
||||||
|
@load="() => tileProcessor.processTile(group.parent)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
||||||
|
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
||||||
|
{{ group.children.length + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-full overflow-auto">
|
||||||
|
<div class="p-4">
|
||||||
|
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
||||||
|
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
||||||
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
||||||
|
:alt="selectedGroup.parent.name"
|
||||||
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
|
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
||||||
|
:alt="childTile.name"
|
||||||
|
@click="selectTile(childTile.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(childTile),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
||||||
|
<span>Tags:</span>
|
||||||
|
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
||||||
|
<span class="m-auto">No tags selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { Tile } from '@/application/types'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
||||||
|
import { TileStorage } from '@/storage/storages'
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const tileProcessor = useTileProcessingComposable()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedTags = ref<string[]>([])
|
||||||
|
const tileCategories = ref<Map<string, string>>(new Map())
|
||||||
|
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
||||||
|
const tiles = ref<Tile[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => (isOpen.value = true),
|
||||||
|
close: () => (isOpen.value = false),
|
||||||
|
toggle: () => (isOpen.value = !isOpen.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniqueTags = computed(() => {
|
||||||
|
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
||||||
|
return Array.from(new Set(allTags))
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupedTiles = computed(() => {
|
||||||
|
const groups: { parent: Tile; children: Tile[] }[] = []
|
||||||
|
const filteredTiles = tiles.value.filter((tile) => {
|
||||||
|
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
|
||||||
|
filteredTiles.forEach((tile) => {
|
||||||
|
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
||||||
|
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
||||||
|
parentGroup.children.push(tile)
|
||||||
|
} else {
|
||||||
|
groups.push({ parent: tile, children: [] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleTag = (tag: string) => {
|
||||||
|
if (selectedTags.value.includes(tag)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTileCategory(tile: Tile): string {
|
||||||
|
return tileCategories.value.get(tile.id) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGroup(group: { parent: Tile; children: Tile[] }) {
|
||||||
|
selectedGroup.value = group
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGroup() {
|
||||||
|
selectedGroup.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTile(tile: string) {
|
||||||
|
mapEditor.setSelectedTile(tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveTile(tile: Tile): boolean {
|
||||||
|
return mapEditor.selectedTile.value === tile.id
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
tiles.value = await tileStorage.getAll()
|
||||||
|
const initialBatchSize = 20
|
||||||
|
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
||||||
|
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
|
||||||
|
|
||||||
|
// Process remaining tiles in background
|
||||||
|
setTimeout(() => {
|
||||||
|
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
tileProcessor.cleanup()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open' : listOpen }">
|
<div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open': listOpen }">
|
||||||
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||||
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
|
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
|
||||||
@ -104,7 +104,7 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-lists', 'close-lists'])
|
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor'])
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const toolbar = ref(null)
|
const toolbar = ref(null)
|
||||||
@ -116,15 +116,6 @@ const listOpen = ref(false)
|
|||||||
|
|
||||||
// drawMode
|
// drawMode
|
||||||
function setDrawMode(value: string) {
|
function setDrawMode(value: string) {
|
||||||
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
|
|
||||||
listOpen.value = false
|
|
||||||
emit('close-lists')
|
|
||||||
if (value === 'tile' || value === 'map_object') {
|
|
||||||
listOpen.value = true
|
|
||||||
emit('open-lists')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mapEditor.setDrawMode(value)
|
mapEditor.setDrawMode(value)
|
||||||
selectPencilOpen.value = false
|
selectPencilOpen.value = false
|
||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
@ -154,15 +145,10 @@ function handleClick(tool: string) {
|
|||||||
if (tool === 'mapEditorSettings') {
|
if (tool === 'mapEditorSettings') {
|
||||||
isMapEditorSettingsModalOpen.value = true
|
isMapEditorSettingsModalOpen.value = true
|
||||||
listOpen.value = false
|
listOpen.value = false
|
||||||
emit('close-lists')
|
} else if (tool === 'settings') {
|
||||||
}
|
|
||||||
if (tool === 'settings') {
|
|
||||||
listOpen.value = false
|
listOpen.value = false
|
||||||
emit('open-settings')
|
|
||||||
emit('close-lists')
|
|
||||||
} else if (tool === 'move') {
|
} else if (tool === 'move') {
|
||||||
listOpen.value = false
|
listOpen.value = false
|
||||||
emit('close-lists')
|
|
||||||
mapEditor.setTool(tool)
|
mapEditor.setTool(tool)
|
||||||
} else {
|
} else {
|
||||||
mapEditor.setTool(tool)
|
mapEditor.setTool(tool)
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
|
||||||
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
|
||||||
<img
|
|
||||||
class="border-2 border-solid rounded max-w-full"
|
|
||||||
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
|
||||||
alt="Object"
|
|
||||||
@click="mapEditor.setSelectedMapObject(mapObject)"
|
|
||||||
:class="{
|
|
||||||
'cursor-pointer transition-all duration-300': true,
|
|
||||||
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
|
||||||
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import type { MapObject } from '@/application/types'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
|
||||||
import { liveQuery } from 'dexie'
|
|
||||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedTags = ref<string[]>([])
|
|
||||||
const mapObjectList = ref<MapObject[]>([])
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
|
||||||
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
|
||||||
return Array.from(new Set(allTags))
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredMapObjects = computed(() => {
|
|
||||||
return mapObjectList.value.filter((object) => {
|
|
||||||
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
|
||||||
if (selectedTags.value.includes(tag)) {
|
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
|
||||||
} else {
|
|
||||||
selectedTags.value.push(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subscription: any = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
isModalOpen.value = true
|
|
||||||
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
mapObjectList.value = result
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (subscription) {
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full" v-if="!selectedGroup">
|
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
|
||||||
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
|
||||||
:alt="group.parent.name"
|
|
||||||
@click="openGroup(group)"
|
|
||||||
@load="() => tileProcessor.processTile(group.parent)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
|
||||||
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
|
||||||
{{ group.children.length + 1 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="h-full overflow-auto">
|
|
||||||
<div class="p-4">
|
|
||||||
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
|
||||||
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
|
||||||
<div class="flex flex-col items-center justify-center">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
|
||||||
:alt="selectedGroup.parent.name"
|
|
||||||
@click="selectTile(selectedGroup.parent.id)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
|
||||||
:alt="childTile.name"
|
|
||||||
@click="selectTile(childTile.id)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(childTile),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import type { Tile } from '@/application/types'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
|
||||||
import { TileStorage } from '@/storage/storages'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const tileProcessor = useTileProcessingComposable()
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedTags = ref<string[]>([])
|
|
||||||
const tileCategories = ref<Map<string, string>>(new Map())
|
|
||||||
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
|
||||||
const tiles = ref<Tile[]>([])
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
|
||||||
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
|
||||||
return Array.from(new Set(allTags))
|
|
||||||
})
|
|
||||||
|
|
||||||
const groupedTiles = computed(() => {
|
|
||||||
const groups: { parent: Tile; children: Tile[] }[] = []
|
|
||||||
const filteredTiles = tiles.value.filter((tile) => {
|
|
||||||
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
|
|
||||||
filteredTiles.forEach((tile) => {
|
|
||||||
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
|
||||||
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
|
||||||
parentGroup.children.push(tile)
|
|
||||||
} else {
|
|
||||||
groups.push({ parent: tile, children: [] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return groups
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
|
||||||
if (selectedTags.value.includes(tag)) {
|
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
|
||||||
} else {
|
|
||||||
selectedTags.value.push(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTileCategory(tile: Tile): string {
|
|
||||||
return tileCategories.value.get(tile.id) || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function openGroup(group: { parent: Tile; children: Tile[] }) {
|
|
||||||
selectedGroup.value = group
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGroup() {
|
|
||||||
selectedGroup.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTile(tile: string) {
|
|
||||||
mapEditor.setSelectedTile(tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActiveTile(tile: Tile): boolean {
|
|
||||||
return mapEditor.selectedTile.value === tile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
tiles.value = await tileStorage.getAll()
|
|
||||||
const initialBatchSize = 20
|
|
||||||
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
|
||||||
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
|
|
||||||
|
|
||||||
// Process remaining tiles in background
|
|
||||||
setTimeout(() => {
|
|
||||||
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
tileProcessor.cleanup()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -5,9 +5,10 @@
|
|||||||
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
|
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
|
||||||
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" @close-lists="list?.close" @open-lists="list?.open" />
|
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" />
|
||||||
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
||||||
<ListPanel ref="list" />
|
<TileList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile'" />
|
||||||
|
<MapObjectList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'" />
|
||||||
<MapSettings ref="mapSettingsModal" />
|
<MapSettings ref="mapSettingsModal" />
|
||||||
<TeleportModal ref="teleportModal" />
|
<TeleportModal ref="teleportModal" />
|
||||||
</div>
|
</div>
|
||||||
@ -21,10 +22,11 @@ import config from '@/application/config'
|
|||||||
import 'phaser'
|
import 'phaser'
|
||||||
import type { Map as MapT } from '@/application/types'
|
import type { Map as MapT } from '@/application/types'
|
||||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
import ListPanel from '@/components/gameMaster/mapEditor/partials/ListPanel.vue'
|
|
||||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||||
|
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
||||||
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
||||||
|
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
||||||
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { loadAllTileTextures } from '@/services/mapService'
|
import { loadAllTileTextures } from '@/services/mapService'
|
||||||
@ -38,7 +40,6 @@ const mapEditor = useMapEditorComposable()
|
|||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const mapModal = useTemplateRef('mapModal')
|
const mapModal = useTemplateRef('mapModal')
|
||||||
const list = useTemplateRef('list')
|
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||||
|
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
@ -93,7 +94,7 @@ function save() {
|
|||||||
pvp: currentMap.pvp,
|
pvp: currentMap.pvp,
|
||||||
mapEffects: currentMap.mapEffects,
|
mapEffects: currentMap.mapEffects,
|
||||||
mapEventTiles: currentMap.mapEventTiles,
|
mapEventTiles: currentMap.mapEventTiles,
|
||||||
placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
|
placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, isRotated, positionX, positionY }) => ({ id, mapObject, isRotated, positionX, positionY })) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
|
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
|
||||||
|
@ -4,26 +4,32 @@ import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// Constants for image processing
|
// Constants for image processing
|
||||||
const DOWNSCALE_WIDTH = 32
|
const DOWNSCALE_WIDTH = 16
|
||||||
const DOWNSCALE_HEIGHT = 16
|
const DOWNSCALE_HEIGHT = 8
|
||||||
const COLOR_SIMILARITY_THRESHOLD = 30
|
const COLOR_SIMILARITY_THRESHOLD = 30
|
||||||
const EDGE_SIMILARITY_THRESHOLD = 20
|
const EDGE_SIMILARITY_THRESHOLD = 20
|
||||||
const BATCH_SIZE = 4
|
const BATCH_SIZE = 8
|
||||||
|
|
||||||
export function useTileProcessingComposable() {
|
export function useTileProcessingComposable() {
|
||||||
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
|
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
|
||||||
const processingQueue = ref<Tile[]>([])
|
const processingQueue = ref<Tile[]>([])
|
||||||
let isProcessing = false
|
let isProcessing = false
|
||||||
const worker = new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' })
|
|
||||||
|
|
||||||
worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => {
|
const NUM_WORKERS = 4
|
||||||
const { tileId, color, edge, namePrefix } = e.data
|
const workers = Array.from({ length: NUM_WORKERS }, () => new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' }))
|
||||||
tileAnalysisCache.value.set(tileId, { color, edge, namePrefix })
|
let currentWorker = 0
|
||||||
isProcessing = false
|
|
||||||
processBatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processTileAsync(tile: Tile): Promise<void> {
|
// Modify worker message handling
|
||||||
|
workers.forEach((worker) => {
|
||||||
|
worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => {
|
||||||
|
const { tileId, color, edge, namePrefix } = e.data
|
||||||
|
tileAnalysisCache.value.set(tileId, { color, edge, namePrefix })
|
||||||
|
isProcessing = false
|
||||||
|
processBatch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function processTileAsync(tile: Tile, worker: Worker): Promise<void> {
|
||||||
if (tileAnalysisCache.value.has(tile.id)) return
|
if (tileAnalysisCache.value.has(tile.id)) return
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -60,7 +66,12 @@ export function useTileProcessingComposable() {
|
|||||||
isProcessing = true
|
isProcessing = true
|
||||||
|
|
||||||
const batch = processingQueue.value.splice(0, BATCH_SIZE)
|
const batch = processingQueue.value.splice(0, BATCH_SIZE)
|
||||||
Promise.all(batch.map((tile) => processTileAsync(tile))).then(() => {
|
Promise.all(
|
||||||
|
batch.map((tile) => {
|
||||||
|
currentWorker = (currentWorker + 1) % NUM_WORKERS
|
||||||
|
return processTileAsync(tile, workers[currentWorker])
|
||||||
|
})
|
||||||
|
).then(() => {
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
if (processingQueue.value.length > 0) {
|
if (processingQueue.value.length > 0) {
|
||||||
setTimeout(processBatch, 0)
|
setTimeout(processBatch, 0)
|
||||||
@ -87,7 +98,7 @@ export function useTileProcessingComposable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
worker.terminate()
|
workers.forEach((worker) => worker.terminate())
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -12,7 +12,6 @@ function analyzeTile(imageData: ImageData, tileId: string, tileName: string): Ti
|
|||||||
const { r, g, b } = getDominantColorFast(imageData)
|
const { r, g, b } = getDominantColorFast(imageData)
|
||||||
const edge = getEdgeComplexityFast(imageData)
|
const edge = getEdgeComplexityFast(imageData)
|
||||||
const namePrefix = tileName.split('_')[0]
|
const namePrefix = tileName.split('_')[0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tileId,
|
tileId,
|
||||||
color: { r, g, b },
|
color: { r, g, b },
|
||||||
@ -53,16 +52,14 @@ function getEdgeComplexityFast(imageData: ImageData) {
|
|||||||
const height = imageData.height
|
const height = imageData.height
|
||||||
let edgePixels = 0
|
let edgePixels = 0
|
||||||
|
|
||||||
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE) {
|
// Only check every other row/column
|
||||||
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE) {
|
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE * 2) {
|
||||||
|
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE * 2) {
|
||||||
const i = (y * width + x) * 4
|
const i = (y * width + x) * 4
|
||||||
if (
|
if (data[i + 3] > 0 && (x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0)) {
|
||||||
data[i + 3] > 0 &&
|
|
||||||
(x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i - width * 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + width * 4 * PIXEL_SAMPLE_RATE + 3] === 0)
|
|
||||||
) {
|
|
||||||
edgePixels++
|
edgePixels++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return edgePixels * PIXEL_SAMPLE_RATE
|
return edgePixels * PIXEL_SAMPLE_RATE * 2
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user