1
0
forked from noxious/client

Added web worker to improve tile analysis performance

This commit is contained in:
Dennis Postma 2025-02-01 03:11:13 +01:00
parent 09ee9bf01d
commit ac1396304f
4 changed files with 205 additions and 95 deletions

View File

@ -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>

View 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
View 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
}

View 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
}