PageSourceSearch

https://stringart.ro/workers/string-knitting-worker.js

js stringart.ro collected 2026-10-02 14:29:04 UTC 15,484 bytes, 500 lines download raw bytes

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.