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`"
|
||||
:alt="group.parent.name"
|
||||
@click="openGroup(group)"
|
||||
@load="() => processTile(group.parent)"
|
||||
@load="() => tileProcessor.processTile(group.parent)"
|
||||
:class="{
|
||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||
@ -88,13 +88,14 @@
|
||||
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 { liveQuery } from 'dexie'
|
||||
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())
|
||||
@ -121,7 +122,7 @@ const groupedTiles = computed(() => {
|
||||
})
|
||||
|
||||
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) {
|
||||
parentGroup.children.push(tile)
|
||||
} else {
|
||||
@ -132,32 +133,6 @@ const groupedTiles = computed(() => {
|
||||
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) => {
|
||||
if (selectedTags.value.includes(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 {
|
||||
return tileCategories.value.get(tile.id) || ''
|
||||
}
|
||||
@ -239,22 +161,19 @@ function isActiveTile(tile: Tile): boolean {
|
||||
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(() => {
|
||||
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
||||
next: (result) => {
|
||||
tiles.value = result
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to fetch tiles:', error)
|
||||
}
|
||||
})
|
||||
// Process remaining tiles in background
|
||||
setTimeout(() => {
|
||||
tiles.value.slice(initialBatchSize).forEach(tile => tileProcessor.processTile(tile))
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
tileProcessor.cleanup()
|
||||
})
|
||||
</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