1// Web Worker version of StringKnitting algorithm (index.js) 2// This allows multiple instances to run in parallel 3 4// Algorithm state variables (isolated per worker) 5let plate; 6let image; 7let imageData; 8let pinList; 9let lineList = []; 10let lineSequence = []; 11let lastState = null; 12 13let lineLimit = 1500; 14let lineCount = 0; 15let lineWeight = 20; 16let isStopped = false; 17const MIN_DISTANCE = 20; 18const MIN_LOOP = 20; 19 20let recentPins = []; // queue 21const recentPinsLimit = 5; // Match original algorithm - reduced for better coverage 22 23// Error map for residual tracking 24let errorMap; 25 26// Precomputed line cache (matching original algorithm) 27let precomputedLineCache = null; 28 29// Progress callback with batching 30let progressCallback = null; 31let progressBatch = []; 32let progressBatchSize = 5; // Send progress every 5 lines for more fluent real-time display 33 34// Render configuration (matching original algorithm) 35let renderConfig = { lineWidth: 0.5, lineOpacity: 0.6 }; 36 37// Edge enhancement configuration 38let edgeEnhancementConfig = { 39 enabled: false, 40 intensity: 0, 41 useOptimizedMap: true, 42 sobelWeight: 0.4, 43 cannyWeight: 0.3, 44 gradientWeight: 0.3 45}; 46 47// Load image and start generation - OPTIMIZED 48async function loadImageAndStart(imageSrc, startPinIndex) { 49 try { 50 // OPTIMIZATION 1: Use direct image data if it's already a data URL 51 if (imageSrc.startsWith('data:image/')) { 52 // In Web Workers, we can't use Image constructor, so use createImageBitmap 53 const response = await fetch(imageSrc); 54 const blob = await response.blob(); 55 const imageBitmap = await createImageBitmap(blob, { 56 resizeWidth: 600, 57 resizeHeight: 600, 58 resizeQuality: 'high' 59 }); 60 processImageData(imageBitmap, startPinIndex); 61 return; 62 } 63 64 // OPTIMIZATION 2: Fetch with better caching 65 const response = await fetch(imageSrc, { 66 cache: 'force-cache', // Use browser cache 67 headers: { 68 'Accept': 'image/*' 69 } 70 }); 71 72 if (!response.ok) { 73 throw new Error(`Failed to fetch image: ${response.status}`); 74 } 75 76 const blob = await response.blob(); 77 78 // OPTIMIZATION 3: Use createImageBitmap with better options 79 const imageBitmap = await createImageBitmap(blob, { 80 resizeWidth: 600, 81 resizeHeight: 600, 82 resizeQuality: 'high' 83 }); 84 85 processImageData(imageBitmap, startPinIndex); 86 87 } catch (error) { 88 self.postMessage({ type: 'error', data: { error: error.message } }); 89 } 90} 91 92// OPTIMIZATION 4: Separate image processing function 93function processImageData(imageSource, startPinIndex) { 94 try { 95 // Create a canvas to process the image 96 const canvas = new OffscreenCanvas(600, 600); 97 const ctx = canvas.getContext('2d', { 98 alpha: false, // Disable alpha for better performance 99 desynchronized: true // Allow async rendering 100 }); 101 102 // Draw image to canvas 103 ctx.drawImage(imageSource, 0, 0, 600, 600); 104 105 // Get image data 106 imageData = ctx.getImageData(0, 0, 600, 600); 107 108 // Apply grayscale conversion (matching original algorithm) 109 grayscaleInPlace(imageData); 110 111 // Initialize error map 112 errorMap = initResiduals(imageData, imageData.width, imageData.height); 113 114 // Initialize precomputed line cache (matching original algorithm) 115 precomputedLineCache = precomputeAllLines(pinList, imageData.width); 116 117 // Start generation 118 draw(startPinIndex); 119 120 } catch (error) { 121 self.postMessage({ type: 'error', data: { error: error.message } }); 122 } 123} 124 125// Utility functions 126function makeKey(a, b) { return a < b ? `${a}|${b}` : `${b}|${a}`; } 127 128function precomputeAllLines(pinList, width) { 129 const totalPins = pinList.length; 130 const map = new Map(); 131 for (let a = 0; a < totalPins; a++) { 132 for (let b = a + MIN_DISTANCE; b < totalPins; b++) { 133 const key = `${a}|${b}`; 134 const [x0, y0] = pinList[a]; 135 const [x1, y1] = pinList[b]; 136 const dx = x1 - x0, dy = y1 - y0; 137 const dist = Math.max(1, Math.floor(Math.sqrt(dx * dx + dy * dy))); 138 const out = new Uint32Array(dist); 139 const n = dist - 1; 140 for (let i = 0; i < dist; i++) { 141 const t = i / n; 142 const x = Math.round(x0 + t * dx); 143 const y = Math.round(y0 + t * dy); 144 out[i] = y * width + x; 145 } 146 map.set(key, out); 147 } 148 } 149 return map; 150} 151 152function getLineIndices(a, b, width) { 153 const arr = precomputedLineCache && precomputedLineCache.get(makeKey(a, b)); 154 if (arr) return arr; 155 // Fallback: compute on the fly 156 const [x0, y0] = pinList[a]; 157 const [x1, y1] = pinList[b]; 158 const dx = x1 - x0, dy = y1 - y0; 159 const dist = Math.max(1, Math.floor(Math.sqrt(dx * dx + dy * dy))); 160 const out = new Uint32Array(dist); 161 const n = dist - 1; 162 for (let i = 0; i < dist; i++) { 163 const t = i / n; 164 const x = Math.round(x0 + t * dx); 165 const y = Math.round(y0 + t * dy); 166 out[i] = y * width + x; 167 } 168 return out; 169} 170 171function grayscaleInPlace(imgData) { 172 const d = imgData.data; // RGBA 173 for (let i = 0; i < d.length; i += 4) { 174 const r = d[i], g = d[i + 1], b = d[i + 2]; 175 const avg = (r + g + b) / 3; 176 d[i] = d[i + 1] = d[i + 2] = avg; 177 } 178} 179 180function generatePinList(length, width, height) { 181 const center = [width / 2, height / 2]; 182 const radius = width / 2; // Match original algorithm - use full radius 183 const angleUnit = (Math.PI * 2) / length; 184 185 return Array(length).fill().map((_, i) => { 186 const angle = angleUnit * i - Math.PI / 2; // Match original algorithm - start from top 187 const x = Math.round(center[0] + radius * Math.cos(angle)); 188 const y = Math.round(center[1] + radius * Math.sin(angle)); 189 return [Math.min(x, width - 1), Math.min(y, height - 1)]; // Match original bounds checking 190 }); 191} 192 193function initResiduals(imageData, width, height) { 194 const data = imageData.data; 195 const residuals = new Uint16Array(width * height); 196 197 // Circle masking - only process pixels within the circle (matching original algorithm) 198 const cx = width / 2; 199 const cy = height / 2; 200 const r2 = (Math.min(width, height) / 2) ** 2; 201 202 for (let y = 0; y < height; y++) { 203 for (let x = 0; x < width; x++) { 204 const inCircle = ((x - cx) ** 2 + (y - cy) ** 2) <= r2; 205 const idx = y * width + x; 206 const idxRGBA = idx * 4; 207 208 if (inCircle) { 209 // CORRECT Piellardj threading algorithm: pure inversion (matching original) 210 const r = data[idxRGBA]; 211 const g = data[idxRGBA + 1]; 212 const b = data[idxRGBA + 2]; 213 const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); 214 residuals[idx] = 255 - gray; 215 } else { 216 // Outside circle - no residual 217 residuals[idx] = 0; 218 } 219 } 220 } 221 222 return residuals; 223} 224 225function getLinePixels(startPin, endPin, width, height) { 226 const pixels = []; 227 const [x1, y1] = startPin; 228 const [x2, y2] = endPin; 229 230 const dx = Math.abs(x2 - x1); 231 const dy = Math.abs(y2 - y1); 232 const sx = x1 < x2 ? 1 : -1; 233 const sy = y1 < y2 ? 1 : -1; 234 let err = dx - dy; 235 236 let x = Math.round(x1); 237 let y = Math.round(y1); 238 let iterations = 0; 239 const maxIterations = Math.max(dx, dy) + 10; // More reasonable limit 240 241 while (iterations < maxIterations) { 242 iterations++; 243 244 if (x >= 0 && x < width && y >= 0 && y < height) { 245 pixels.push(Math.floor(y) * width + Math.floor(x)); 246 } 247 248 // Check if we've reached the end point 249 if (Math.abs(x - x2) <= 1 && Math.abs(y - y2) <= 1) { 250 break; 251 } 252 253 const e2 = 2 * err; 254 if (e2 > -dy) { 255 err -= dy; 256 x += sx; 257 } 258 if (e2 < dx) { 259 err += dx; 260 y += sy; 261 } 262 } 263 264 return pixels; 265} 266 267function applyLine(startPinIndex, endPinIndex) { 268 const indices = getLineIndices(startPinIndex, endPinIndex, imageData.width); 269 270 // PURE Piellardj approach: simple fixed subtraction (matching original algorithm) 271 for (let i = 0; i < indices.length; i++) { 272 const idx = indices[i]; 273 const currentError = errorMap[idx] || 0; 274 275 // Simple subtraction with fixed line weight (matching original) 276 const newError = Math.max(0, currentError - lineWeight); 277 errorMap[idx] = newError; 278 } 279} 280 281function selectNextPin(currentPinIndex) { 282 const totalPins = pinList.length; 283 let bestPin = -1; 284 let bestScore = -1; 285 286 // Improved algorithm: more thorough evaluation (matching original) 287 for (let endPin = 0; endPin < totalPins; endPin++) { 288 // Skip invalid pins 289 if (endPin === currentPinIndex) continue; 290 291 // Check minimum distance constraint using pin index distance (matching original) 292 const distance = Math.min( 293 Math.abs(endPin - currentPinIndex), 294 totalPins - Math.abs(endPin - currentPinIndex) 295 ); 296 if (distance < MIN_DISTANCE) continue; 297 298 // Skip recently used pins to avoid loops 299 if (recentPins.includes(endPin)) continue; 300 301 // Calculate line score based on error map (matching original algorithm) 302 const indices = getLineIndices(currentPinIndex, endPin, imageData.width); 303 let score = 0; 304 305 for (let j = 0; j < indices.length; j++) { 306 const pixelIndex = indices[j]; 307 const error = errorMap[pixelIndex] || 0; 308 score += error; // Simple sum - no bonuses or modifications! 309 } 310 311 if (score > bestScore) { 312 bestScore = score; 313 bestPin = endPin; 314 } 315 } 316 317 // Fallback if no valid pin found (matching original) 318 if (bestPin === -1) { 319 bestPin = (currentPinIndex + Math.floor(totalPins / 2)) % totalPins; 320 } 321 322 return bestPin; 323} 324 325function persistState() { 326 try { 327 lastState = { 328 pinList: pinList, 329 lineList: lineList, 330 numPins: pinList ? pinList.length : 0, 331 lineLimit: lineLimit, 332 MIN_DISTANCE: MIN_DISTANCE, 333 LINE_WEIGHT: lineWeight, // Use LINE_WEIGHT to match original algorithm structure 334 renderWidth: renderConfig.lineWidth, 335 renderOpacity: renderConfig.lineOpacity, 336 edgeEnhancement: edgeEnhancementConfig.intensity 337 }; 338 } catch (e) { 339 console.error('Error persisting state:', e); 340 } 341} 342 343function draw(currentPinIndex) { 344 if (isStopped) { 345 return; 346 } 347 348 // Select next pin and actually draw the line first 349 const endPinIndex = selectNextPin(currentPinIndex); 350 351 // Push/apply the line before incrementing to avoid off-by-one 352 lineList.push([currentPinIndex, endPinIndex]); 353 applyLine(currentPinIndex, endPinIndex); 354 lineSequence.push(endPinIndex); 355 356 recentPins.push(endPinIndex); 357 if (recentPins.length > recentPinsLimit) recentPins.shift(); 358 359 // Increment AFTER applying 360 lineCount++; 361 362 // Persist state 363 persistState(); 364 365 // Send real-time line update for SVG construction with batching 366 if (progressCallback) { 367 const startPin = pinList[currentPinIndex]; 368 const endPin = pinList[endPinIndex]; 369 const lineSvg = `<line x1="${startPin[0]}" y1="${startPin[1]}" x2="${endPin[0]}" y2="${endPin[1]}" stroke="#000000" stroke-width="${renderConfig.lineWidth}" stroke-opacity="${renderConfig.lineOpacity}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"></line>`; 370 371 const progressData = { 372 lineCount, 373 lineLimit, 374 startPinIndex: currentPinIndex, 375 endPinIndex, 376 progress: Math.min(100, Math.round((lineCount / lineLimit) * 100)), 377 lineSvg: lineSvg, // Real-time line for SVG construction 378 isComplete: lineCount >= lineLimit
379 }; 380 381 // Batch progress updates to reduce message passing overhead 382 progressBatch.push(progressData); 383 384 // Send batch when it reaches the batch size or when complete 385 if (progressBatch.length >= progressBatchSize || lineCount >= lineLimit) { 386 // Send the latest progress from the batch 387 const latestProgress = progressBatch[progressBatch.length - 1]; 388 progressCallback(latestProgress); 389 progressBatch = []; // Clear the batch 390 } 391 } 392 393 // OPTIMIZATION 5: Use requestAnimationFrame for better performance 394 if (!isStopped && lineCount < lineLimit) { 395 // Use requestAnimationFrame for smoother execution 396 requestAnimationFrame(() => draw(endPinIndex)); 397 } 398} 399 400// Worker message handler 401self.onmessage = function(e) { 402 const { type, data } = e.data; 403 404 switch (type) { 405 case 'init': 406 try { 407 const { numPins, width, height } = data; 408 pinList = generatePinList(numPins, width, height); 409 lineList = []; 410 lineSequence = []; 411 recentPins = []; 412 lineCount = 0; 413 isStopped = false; 414 415 self.postMessage({ type: 'initComplete', data: { numPins: pinList.length } }); 416 } catch (error) { 417 self.postMessage({ type: 'error', data: { error: error.message } }); 418 } 419 break; 420 421 case 'start': 422 (async () => { 423 try { 424 const { 425 imageSrc, 426 maxLines, 427 algoLineWeight, 428 lineWidth = 0.5, 429 lineOpacity = 0.6, 430 edgeEnhancement = 0, 431 quality = 'HIGH', 432 startPinIndex = 0 433 } = data; 434 435 lineLimit = maxLines; 436 lineWeight = algoLineWeight; 437 // Reset state for a clean run 438 lineCount = 0; 439 lineList = []; 440 lineSequence = []; 441 recentPins = []; 442 isStopped = false; 443 444 // Update render configuration 445 renderConfig.lineWidth = Number(lineWidth) || 0.5; 446 renderConfig.lineOpacity = Number(lineOpacity) || 0.6; 447 448 // Configure edge enhancement 449 const enhancementValue = Math.max(0, Math.min(100, Number(edgeEnhancement) || 0)); 450 edgeEnhancementConfig.enabled = enhancementValue > 0; 451 edgeEnhancementConfig.intensity = enhancementValue; 452 453 // Set up progress callback 454 progressCallback = (progress) => { 455 self.postMessage({ type: 'progress', data: progress }); 456 }; 457 458 // Load image and start generation 459 await loadImageAndStart(imageSrc, startPinIndex); 460 461 } catch (error) { 462 self.postMessage({ type: 'error', data: { error: error.message } }); 463 } 464 })(); 465 break; 466 467 case 'stop': 468 isStopped = true; 469 self.postMessage({ type: 'stopped' }); 470 break; 471 472 case 'getState': 473 self.postMessage({ type: 'state', data: lastState }); 474 break; 475 476 case 'getSvg': 477 try { 478 // Generate SVG from lineList using dynamic render configuration 479 let svg = '<svg width="100%" height="100%" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">'; 480 481 if (lineList && lineList.length > 0) { 482 for (let i = 0; i < lineList.length; i++) { 483 const [startIdx, endIdx] = lineList[i]; 484 const startPin = pinList[startIdx]; 485 const endPin = pinList[endIdx]; 486 487 // Use dynamic render configuration - match original algorithm exactly 488 svg += `<line x1="${startPin[0]}" y1="${startPin[1]}" x2="${endPin[0]}" y2="${endPin[1]}" stroke="#000000" stroke-width="${renderConfig.lineWidth}" stroke-opacity="${renderConfig.lineOpacity}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"></line>`; 489 } 490 } 491 492 svg += '</svg>'; 493 self.postMessage({ type: 'svg', data: svg }); 494 } catch (error) { 495 self.postMessage({ type: 'error', data: { error: error.message } }); 496 } 497 break; 498 } 499}; 500
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.