forked from noxious/client
Added web worker to improve tile analysis performance
This commit is contained in:
parent
09ee9bf01d
commit
ac1396304f
@ -29,7 +29,7 @@
|
|||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
:alt="group.parent.name"
|
:alt="group.parent.name"
|
||||||
@click="openGroup(group)"
|
@click="openGroup(group)"
|
||||||
@load="() => processTile(group.parent)"
|
@load="() => tileProcessor.processTile(group.parent)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
@ -88,13 +88,14 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { liveQuery } from 'dexie'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const tileProcessor = useTileProcessingComposable()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const tileCategories = ref<Map<string, string>>(new Map())
|
const tileCategories = ref<Map<string, string>>(new Map())
|
||||||
@ -121,7 +122,7 @@ const groupedTiles = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
filteredTiles.forEach((tile) => {
|
filteredTiles.forEach((tile) => {
|
||||||
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
|
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
||||||
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
||||||
parentGroup.children.push(tile)
|
parentGroup.children.push(tile)
|
||||||
} else {
|
} else {
|
||||||
@ -132,32 +133,6 @@ const groupedTiles = computed(() => {
|
|||||||
return groups
|
return groups
|
||||||
})
|
})
|
||||||
|
|
||||||
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
|
|
||||||
const tileEdgeData = ref<Map<string, number>>(new Map())
|
|
||||||
|
|
||||||
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
|
||||||
const colorSimilarityThreshold = 30 // Adjust this value as needed
|
|
||||||
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
|
|
||||||
|
|
||||||
const color1 = tileColorData.value.get(tile1.id)
|
|
||||||
const color2 = tileColorData.value.get(tile2.id)
|
|
||||||
const edge1 = tileEdgeData.value.get(tile1.id)
|
|
||||||
const edge2 = tileEdgeData.value.get(tile2.id)
|
|
||||||
|
|
||||||
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
|
|
||||||
|
|
||||||
const edgeComplexityDifference = Math.abs(edge1 - edge2)
|
|
||||||
|
|
||||||
const namePrefix1 = tile1.name.split('_')[0]
|
|
||||||
const namePrefix2 = tile2.name.split('_')[0]
|
|
||||||
|
|
||||||
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -166,59 +141,6 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processTile(tile: Tile) {
|
|
||||||
const img = new Image()
|
|
||||||
img.crossOrigin = 'Anonymous'
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
canvas.width = img.width
|
|
||||||
canvas.height = img.height
|
|
||||||
ctx!.drawImage(img, 0, 0, img.width, img.height)
|
|
||||||
|
|
||||||
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
|
|
||||||
tileColorData.value.set(tile.id, getDominantColor(imageData))
|
|
||||||
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
|
|
||||||
}
|
|
||||||
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDominantColor(imageData: ImageData) {
|
|
||||||
let r = 0,
|
|
||||||
g = 0,
|
|
||||||
b = 0,
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
||||||
if (imageData.data[i + 3] > 0) {
|
|
||||||
// Only consider non-transparent pixels
|
|
||||||
r += imageData.data[i]
|
|
||||||
g += imageData.data[i + 1]
|
|
||||||
b += imageData.data[i + 2]
|
|
||||||
total++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: Math.round(r / total),
|
|
||||||
g: Math.round(g / total),
|
|
||||||
b: Math.round(b / total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEdgeComplexity(imageData: ImageData) {
|
|
||||||
let edgePixels = 0
|
|
||||||
for (let y = 0; y < imageData.height; y++) {
|
|
||||||
for (let x = 0; x < imageData.width; x++) {
|
|
||||||
const i = (y * imageData.width + x) * 4
|
|
||||||
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
|
|
||||||
edgePixels++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return edgePixels
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTileCategory(tile: Tile): string {
|
function getTileCategory(tile: Tile): string {
|
||||||
return tileCategories.value.get(tile.id) || ''
|
return tileCategories.value.get(tile.id) || ''
|
||||||
}
|
}
|
||||||
@ -239,22 +161,19 @@ function isActiveTile(tile: Tile): boolean {
|
|||||||
return mapEditor.selectedTile.value === tile.id
|
return mapEditor.selectedTile.value === tile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscription: any = null
|
onMounted(async () => {
|
||||||
|
tiles.value = await tileStorage.getAll()
|
||||||
|
const initialBatchSize = 20
|
||||||
|
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
||||||
|
initialTiles.forEach(tile => tileProcessor.processTile(tile))
|
||||||
|
|
||||||
onMounted(() => {
|
// Process remaining tiles in background
|
||||||
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
setTimeout(() => {
|
||||||
next: (result) => {
|
tiles.value.slice(initialBatchSize).forEach(tile => tileProcessor.processTile(tile))
|
||||||
tiles.value = result
|
}, 1000)
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (subscription) {
|
tileProcessor.cleanup()
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
107
src/composables/useTileProcessingComposable.ts
Normal file
107
src/composables/useTileProcessingComposable.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { Tile } from '@/application/types'
|
||||||
|
import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes'
|
||||||
|
|
||||||
|
// Constants for image processing
|
||||||
|
const DOWNSCALE_WIDTH = 32
|
||||||
|
const DOWNSCALE_HEIGHT = 16
|
||||||
|
const COLOR_SIMILARITY_THRESHOLD = 30
|
||||||
|
const EDGE_SIMILARITY_THRESHOLD = 20
|
||||||
|
const BATCH_SIZE = 4
|
||||||
|
|
||||||
|
export function useTileProcessingComposable() {
|
||||||
|
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
|
||||||
|
const processingQueue = ref<Tile[]>([])
|
||||||
|
let isProcessing = false
|
||||||
|
const worker = new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' })
|
||||||
|
|
||||||
|
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): Promise<void> {
|
||||||
|
if (tileAnalysisCache.value.has(tile.id)) return
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'Anonymous'
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = DOWNSCALE_WIDTH
|
||||||
|
canvas.height = DOWNSCALE_HEIGHT
|
||||||
|
ctx.drawImage(img, 0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
|
||||||
|
const message: TileWorkerMessage = {
|
||||||
|
imageData,
|
||||||
|
tileId: tile.id,
|
||||||
|
tileName: tile.name
|
||||||
|
}
|
||||||
|
worker.postMessage(message)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
img.onerror = () => resolve()
|
||||||
|
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function processBatch() {
|
||||||
|
if (isProcessing || processingQueue.value.length === 0) return
|
||||||
|
isProcessing = true
|
||||||
|
|
||||||
|
const batch = processingQueue.value.splice(0, BATCH_SIZE)
|
||||||
|
Promise.all(batch.map(tile => processTileAsync(tile)))
|
||||||
|
.then(() => {
|
||||||
|
isProcessing = false
|
||||||
|
if (processingQueue.value.length > 0) {
|
||||||
|
setTimeout(processBatch, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTile(tile: Tile) {
|
||||||
|
if (!processingQueue.value.includes(tile)) {
|
||||||
|
processingQueue.value.push(tile)
|
||||||
|
processBatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
||||||
|
const data1 = tileAnalysisCache.value.get(tile1.id)
|
||||||
|
const data2 = tileAnalysisCache.value.get(tile2.id)
|
||||||
|
|
||||||
|
if (!data1 || !data2) return false
|
||||||
|
|
||||||
|
const colorDifference = Math.sqrt(
|
||||||
|
Math.pow(data1.color.r - data2.color.r, 2) +
|
||||||
|
Math.pow(data1.color.g - data2.color.g, 2) +
|
||||||
|
Math.pow(data1.color.b - data2.color.b, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
colorDifference <= COLOR_SIMILARITY_THRESHOLD &&
|
||||||
|
Math.abs(data1.edge - data2.edge) <= EDGE_SIMILARITY_THRESHOLD &&
|
||||||
|
data1.namePrefix === data2.namePrefix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processTile,
|
||||||
|
areTilesRelated,
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
}
|
20
src/types/tileTypes.ts
Normal file
20
src/types/tileTypes.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export interface TileAnalysisResult {
|
||||||
|
tileId: string
|
||||||
|
color: {
|
||||||
|
r: number
|
||||||
|
g: number
|
||||||
|
b: number
|
||||||
|
}
|
||||||
|
edge: number
|
||||||
|
namePrefix: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileWorkerMessage {
|
||||||
|
imageData: ImageData
|
||||||
|
tileId: string
|
||||||
|
tileName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileCache {
|
||||||
|
[key: string]: TileAnalysisResult
|
||||||
|
}
|
64
src/workers/tileAnalyzerWorker.ts
Normal file
64
src/workers/tileAnalyzerWorker.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { TileAnalysisResult } from '@/types/tileTypes'
|
||||||
|
|
||||||
|
const PIXEL_SAMPLE_RATE = 4
|
||||||
|
|
||||||
|
self.onmessage = async (e: MessageEvent) => {
|
||||||
|
const { imageData, tileId, tileName } = e.data
|
||||||
|
const result = analyzeTile(imageData, tileId, tileName)
|
||||||
|
self.postMessage(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeTile(imageData: ImageData, tileId: string, tileName: string): TileAnalysisResult {
|
||||||
|
const { r, g, b } = getDominantColorFast(imageData)
|
||||||
|
const edge = getEdgeComplexityFast(imageData)
|
||||||
|
const namePrefix = tileName.split('_')[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
tileId,
|
||||||
|
color: { r, g, b },
|
||||||
|
edge,
|
||||||
|
namePrefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDominantColorFast(imageData: ImageData) {
|
||||||
|
const data = new Uint8ClampedArray(imageData.data.buffer)
|
||||||
|
let r = 0, g = 0, b = 0, total = 0
|
||||||
|
const length = data.length
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i += 4 * PIXEL_SAMPLE_RATE) {
|
||||||
|
if (data[i + 3] > 0) {
|
||||||
|
r += data[i]
|
||||||
|
g += data[i + 1]
|
||||||
|
b += data[i + 2]
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total > 0 ? {
|
||||||
|
r: Math.round(r / total),
|
||||||
|
g: Math.round(g / total),
|
||||||
|
b: Math.round(b / total)
|
||||||
|
} : { r: 0, g: 0, b: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeComplexityFast(imageData: ImageData) {
|
||||||
|
const data = new Uint8ClampedArray(imageData.data.buffer)
|
||||||
|
const width = imageData.width
|
||||||
|
const height = imageData.height
|
||||||
|
let edgePixels = 0
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE) {
|
||||||
|
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE) {
|
||||||
|
const i = (y * width + x) * 4
|
||||||
|
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 + 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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgePixels * PIXEL_SAMPLE_RATE
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user