image2sand.js 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378
  1. /*
  2. * Image2Sand - Convert images to sand table coordinates
  3. *
  4. * This script processes images and converts them to polar coordinates
  5. * according to the specified settings, supporting multiple output formats including:
  6. * Default: HackPack Sand Garden .ino in this repository
  7. * theta-rho format: for use with sand tables like Sisyphus and Dune Weaver Mini.
  8. *
  9. * Note:
  10. * For Dune Weaver Mini compatibility, this script uses continuous theta values
  11. * that can exceed 2π (360 degrees). This allows the arm to make multiple revolutions
  12. * without creating unintended circles in the patterns.
  13. */
  14. class PriorityQueue {
  15. constructor() {
  16. this.heap = [];
  17. }
  18. enqueue(priority, key) {
  19. this.heap.push({ key, priority });
  20. this._bubbleUp(this.heap.length - 1);
  21. }
  22. dequeue() {
  23. const min = this.heap[0];
  24. const end = this.heap.pop();
  25. if (this.heap.length > 0) {
  26. this.heap[0] = end;
  27. this._sinkDown(0);
  28. }
  29. return min;
  30. }
  31. isEmpty() {
  32. return this.heap.length === 0;
  33. }
  34. _bubbleUp(index) {
  35. const element = this.heap[index];
  36. while (index > 0) {
  37. const parentIndex = Math.floor((index - 1) / 2);
  38. const parent = this.heap[parentIndex];
  39. if (element.priority >= parent.priority) break;
  40. this.heap[parentIndex] = element;
  41. this.heap[index] = parent;
  42. index = parentIndex;
  43. }
  44. }
  45. _sinkDown(index) {
  46. const length = this.heap.length;
  47. const element = this.heap[index];
  48. while (true) {
  49. const leftChildIndex = 2 * index + 1;
  50. const rightChildIndex = 2 * index + 2;
  51. let smallestChildIndex = null;
  52. if (leftChildIndex < length) {
  53. if (this.heap[leftChildIndex].priority < element.priority) {
  54. smallestChildIndex = leftChildIndex;
  55. }
  56. }
  57. if (rightChildIndex < length) {
  58. if (
  59. (smallestChildIndex === null && this.heap[rightChildIndex].priority < element.priority) ||
  60. (smallestChildIndex !== null && this.heap[rightChildIndex].priority < this.heap[leftChildIndex].priority)
  61. ) {
  62. smallestChildIndex = rightChildIndex;
  63. }
  64. }
  65. if (smallestChildIndex === null) break;
  66. this.heap[index] = this.heap[smallestChildIndex];
  67. this.heap[smallestChildIndex] = element;
  68. index = smallestChildIndex;
  69. }
  70. }
  71. }
  72. let currentContourIndex = 0;
  73. let isFirstClick = true;
  74. let originalImageElement = null;
  75. let isGeneratingImage = false;
  76. let isGeneratingCoords = false;
  77. function drawAndPrepImage(imgElement) {
  78. const canvas = document.getElementById('original-image');
  79. const ctx = canvas.getContext('2d');
  80. canvas.width = imgElement.naturalWidth;
  81. canvas.height = imgElement.naturalHeight;
  82. ctx.drawImage(imgElement, 0, 0);
  83. // Set originalImageElement to the current image
  84. originalImageElement = imgElement;
  85. // Enable Generate button
  86. document.getElementById('generate-button').disabled = false;
  87. }
  88. async function generateImage(apiKey, prompt, autoprocess) {
  89. if (isGeneratingImage) {
  90. document.getElementById('generation-status').textContent = "Image is still generating - please don't press the button.";
  91. } else {
  92. isGeneratingImage = true;
  93. document.getElementById('gen-image-button').disabled = true;
  94. document.getElementById('generation-status').style.display = 'block';
  95. try {
  96. const fullPrompt = `Draw an image of the following: ${prompt}. But make it a simple black silhouette on a white background, with very minimal detail and no additional content in the image, so I can use it for a computer icon.`;
  97. const response = await fetch('https://api.openai.com/v1/images/generations', {
  98. method: 'POST',
  99. headers: {
  100. 'Authorization': `Bearer ${apiKey}`,
  101. 'Content-Type': 'application/json'
  102. },
  103. body: JSON.stringify({
  104. model: 'dall-e-3',
  105. prompt: fullPrompt,
  106. size: '1024x1024',
  107. quality: 'standard',
  108. response_format: 'b64_json', // Specify base64 encoding
  109. n: 1
  110. })
  111. });
  112. const data = await response.json();
  113. //const imageUrl = data.data[0].url;
  114. if ('error' in data) {
  115. throw new Error(data.error.message);
  116. }
  117. const imageData = data.data[0].b64_json;
  118. console.log("Image Data: ", imageData);
  119. const imgElement = new Image();
  120. imgElement.onload = function() {
  121. drawAndPrepImage(imgElement);
  122. if (autoprocess) {
  123. convertImage();
  124. }
  125. };
  126. imgElement.src = `data:image/png;base64,${imageData}`;
  127. console.log(`Image generated successfully`);
  128. } catch (error) {
  129. console.error('Image generation error:', error);
  130. }
  131. isGeneratingImage = false;
  132. document.getElementById('generation-status').style.display = 'none';
  133. document.getElementById('generation-status').textContent = "Image is generating - please wait...";
  134. document.getElementById('gen-image-button').disabled = false;
  135. }
  136. }
  137. function handleImageUpload(event) {
  138. const file = event.target.files[0];
  139. const reader = new FileReader();
  140. reader.onload = e => {
  141. if (!originalImageElement) {
  142. originalImageElement = new Image();
  143. originalImageElement.id = 'uploaded-image';
  144. originalImageElement.onload = () => {
  145. drawAndPrepImage(originalImageElement);
  146. };
  147. document.getElementById('original-image').appendChild(originalImageElement);
  148. }
  149. originalImageElement.src = e.target.result;
  150. };
  151. reader.readAsDataURL(file);
  152. }
  153. function processImage(imgElement) {
  154. document.getElementById('processing-status').style.display = 'block';
  155. document.getElementById('generate-button').disabled = true;
  156. // Use setTimeout to allow the UI to update
  157. setTimeout(() => {
  158. const src = cv.imread(imgElement), dst = new cv.Mat();
  159. cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
  160. cv.Canny(src, dst, 50, 150, 3, false);
  161. // Add morphological operations
  162. const kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));
  163. cv.dilate(dst, dst, kernel);
  164. cv.erode(dst, dst, kernel);
  165. // Invert the colors of the detected edges image
  166. cv.bitwise_not(dst, dst);
  167. // Ensure edge-image canvas has the same dimensions as the original image
  168. const edgeCanvas = document.getElementById('edge-image');
  169. edgeCanvas.width = imgElement.naturalWidth;
  170. edgeCanvas.height = imgElement.naturalHeight;
  171. cv.imshow('edge-image', dst);
  172. cv.bitwise_not(dst, dst);
  173. generateDots(dst);
  174. src.delete(); dst.delete();
  175. // Hide the processing status label
  176. document.getElementById('processing-status').style.display = 'none';
  177. document.getElementById('generate-button').disabled = false;
  178. }, 0); // Set delay to 0 to allow the UI to update
  179. // Ensure grid height does not exceed 70% of the viewport height
  180. const gridHeight = document.querySelector('.grid').clientHeight;
  181. const viewportHeight = window.innerHeight * 0.7;
  182. if (gridHeight > viewportHeight) {
  183. document.querySelector('.grid').style.height = `${viewportHeight}px`;
  184. }
  185. }
  186. function plotNextContour() {
  187. const canvas = document.getElementById('plotcontours');
  188. const ctx = canvas.getContext('2d');
  189. if (isFirstClick) {
  190. // Clear the canvas on first click
  191. ctx.clearRect(0, 0, canvas.width, canvas.height);
  192. isFirstClick = false;
  193. }
  194. console.log('Cur Contour: ', currentContourIndex + '/' + orderedContoursSave.length + ":", JSON.stringify(orderedContoursSave[currentContourIndex]));
  195. if (currentContourIndex < orderedContoursSave.length) {
  196. const contour = orderedContoursSave[currentContourIndex];
  197. const baseColor = getRandomColor();
  198. const [r, g, b] = hexToRgb(baseColor);
  199. const length = contour.length;
  200. contour.forEach((point, i) => {
  201. if (i === 0) {
  202. ctx.beginPath();
  203. ctx.moveTo(point.x, point.y);
  204. } else {
  205. ctx.lineTo(point.x, point.y);
  206. // Calculate color fade
  207. const ratio = i / length;
  208. const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
  209. ctx.strokeStyle = fadedColor;
  210. ctx.lineWidth = 2;
  211. ctx.stroke();
  212. ctx.beginPath();
  213. ctx.moveTo(point.x, point.y);
  214. }
  215. });
  216. // Mark the start and end points
  217. ctx.fillStyle = baseColor;
  218. ctx.font = '12px Arial';
  219. // Start point
  220. ctx.fillText(`S${currentContourIndex + 1}`, contour[0].x, contour[0].y);
  221. ctx.beginPath();
  222. ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
  223. ctx.fill();
  224. // End point
  225. ctx.fillText(`E${currentContourIndex + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
  226. ctx.beginPath();
  227. ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
  228. ctx.fill();
  229. // Label the contour with its number
  230. const midPoint = contour[Math.floor(contour.length / 2)];
  231. ctx.fillText(`${currentContourIndex + 1}`, midPoint.x, midPoint.y);
  232. // Increment the contour index
  233. currentContourIndex++;
  234. } else {
  235. alert("All contours have been plotted. Starting over.");
  236. currentContourIndex = 0; // Reset the index
  237. isFirstClick = true; // Reset the first click flag
  238. }
  239. }
  240. function findNearestPoint(lastPoint, contours, visitedPoints) {
  241. let nearestPoint = null;
  242. let nearestDistance = Infinity;
  243. contours.forEach(contour => {
  244. contour.forEach(point => {
  245. if (!point || visitedPoints.has(JSON.stringify(point))) return;
  246. const distance = Math.sqrt(
  247. Math.pow(lastPoint.x - point.x, 2) +
  248. Math.pow(lastPoint.y - point.y, 2)
  249. );
  250. if (distance < nearestDistance) {
  251. nearestDistance = distance;
  252. nearestPoint = point;
  253. }
  254. });
  255. });
  256. return nearestPoint;
  257. }
  258. function findMaximalCenter(points) {
  259. const minX = Math.min(...points.map(p => p.x));
  260. const maxX = Math.max(...points.map(p => p.x));
  261. const minY = Math.min(...points.map(p => p.y));
  262. const maxY = Math.max(...points.map(p => p.y));
  263. const centerX = (minX + maxX) / 2;
  264. const centerY = (minY + maxY) / 2;
  265. const width = maxX - minX;
  266. const height = maxY - minY;
  267. return { centerX, centerY, width, height };
  268. }
  269. function calculateCentroid(points) {
  270. let sumX = 0, sumY = 0;
  271. points.forEach(p => {
  272. sumX += p.x;
  273. sumY += p.y;
  274. });
  275. return { x: sumX / points.length, y: sumY / points.length };
  276. }
  277. function calculateDistances(contours) {
  278. const distances = [];
  279. for (let i = 0; i < contours.length; i++) {
  280. distances[i] = [];
  281. for (let j = 0; j < contours.length; j++) {
  282. if (i !== j) {
  283. const startToStart = Math.hypot(contours[i][0].x - contours[j][0].x, contours[i][0].y - contours[j][0].y);
  284. const startToEnd = Math.hypot(contours[i][0].x - contours[j][contours[j].length - 1].x, contours[i][0].y - contours[j][contours[j].length - 1].y);
  285. const endToStart = Math.hypot(contours[i][contours[i].length - 1].x - contours[j][0].x, contours[i][contours[i].length - 1].y - contours[j][0].y);
  286. const endToEnd = Math.hypot(contours[i][contours[i].length - 1].x - contours[j][contours[j].length - 1].x, contours[i][contours[i].length - 1].y - contours[j][contours[j].length - 1].y);
  287. distances[i][j] = Math.min(startToStart, startToEnd, endToStart, endToEnd);
  288. }
  289. }
  290. }
  291. return distances;
  292. }
  293. function tspNearestNeighbor(distances, contours) {
  294. const path = [0];
  295. const visited = new Set([0]);
  296. while (path.length < contours.length) {
  297. let last = path[path.length - 1];
  298. let nearest = -1;
  299. let nearestDistance = Infinity;
  300. for (let i = 0; i < contours.length; i++) {
  301. if (!visited.has(i) && distances[last][i] < nearestDistance) {
  302. nearestDistance = distances[last][i];
  303. nearest = i;
  304. }
  305. }
  306. if (nearest !== -1) {
  307. path.push(nearest);
  308. visited.add(nearest);
  309. }
  310. }
  311. return path;
  312. }
  313. function reorderContours(contours, path) {
  314. const orderedContours = [];
  315. console.log("Path:", path); // Debugging
  316. for (let i = 0; i < path.length; i++) {
  317. const contourIndex = path[i];
  318. let contour = contours[contourIndex];
  319. // Determine the direction to use the contour
  320. if (i > 0) {
  321. const prevContour = orderedContours[orderedContours.length - 1];
  322. const prevPoint = prevContour[prevContour.length - 1];
  323. if (isFullyClosed(contour)) {
  324. // Contour is fully closed, so can move the startPoint
  325. contour = reorderPointsForLoop(contour, startNear = prevPoint)
  326. } else if (prevPoint && contour[0]) {
  327. // Contour not fully closed, decide whether to reverse contour
  328. const startToStart = Math.hypot(prevPoint.x - contour[0].x, prevPoint.y - contour[0].y);
  329. const startToEnd = Math.hypot(prevPoint.x - contour[contour.length - 1].x, prevPoint.y - contour[contour.length - 1].y);
  330. if (startToEnd < startToStart) {
  331. contour.reverse();
  332. }
  333. } else {
  334. console.error('Previous point or current contour start point is undefined.', { prevPoint, currentStart: contour[0] });
  335. continue; // Skip if any point is undefined
  336. }
  337. }
  338. orderedContours.push(contour);
  339. }
  340. return orderedContours;
  341. }
  342. function findClosestPoint(contours, point) {
  343. let minDistance = Infinity;
  344. let closestPoint = null;
  345. contours.forEach(contour => {
  346. contour.forEach(pt => {
  347. const distance = Math.hypot(point.x - pt.x, point.y - pt.y);
  348. if (distance < minDistance) {
  349. minDistance = distance;
  350. closestPoint = pt;
  351. }
  352. });
  353. });
  354. return closestPoint;
  355. }
  356. function createGraphWithConnectionTypes(contours) {
  357. const graph = [];
  358. const nodeMap = new Map(); // Map to quickly find nodes by coordinates
  359. const MAX_JUMP_CONNECTIONS = 10; // Limit the number of jump connections per node
  360. // Create nodes for each point in the contours
  361. contours.forEach(contour => {
  362. contour.forEach(pt => {
  363. const key = `${pt.x},${pt.y}`;
  364. if (!nodeMap.has(key)) {
  365. const node = { x: pt.x, y: pt.y, neighbors: [] };
  366. graph.push(node);
  367. nodeMap.set(key, node);
  368. }
  369. });
  370. });
  371. // Connect points within the same contour (regular path connections)
  372. contours.forEach(contour => {
  373. for (let i = 0; i < contour.length; i++) {
  374. const key = `${contour[i].x},${contour[i].y}`;
  375. const node = nodeMap.get(key);
  376. if (i > 0) {
  377. const prevKey = `${contour[i - 1].x},${contour[i - 1].y}`;
  378. const prevNode = nodeMap.get(prevKey);
  379. if (!node.neighbors.some(neighbor => neighbor.node === prevNode)) {
  380. node.neighbors.push({ node: prevNode, isJump: false });
  381. prevNode.neighbors.push({ node: node, isJump: false });
  382. }
  383. }
  384. }
  385. });
  386. // Create a spatial index for efficient nearest neighbor search
  387. const spatialIndex = [];
  388. graph.forEach(node => {
  389. spatialIndex.push({
  390. node: node,
  391. x: node.x,
  392. y: node.y
  393. });
  394. });
  395. // Connect nodes from different contours with jump connections, but limit the number
  396. graph.forEach(nodeA => {
  397. // Sort other nodes by distance to current node
  398. const distances = spatialIndex
  399. .filter(item => item.node !== nodeA)
  400. .map(item => ({
  401. node: item.node,
  402. distance: Math.hypot(nodeA.x - item.node.x, nodeA.y - item.node.y)
  403. }))
  404. .sort((a, b) => a.distance - b.distance);
  405. // Only connect to the closest MAX_JUMP_CONNECTIONS nodes
  406. distances.slice(0, MAX_JUMP_CONNECTIONS).forEach(({ node: nodeB, distance }) => {
  407. if (!nodeA.neighbors.some(neighbor => neighbor.node === nodeB)) {
  408. nodeA.neighbors.push({ node: nodeB, isJump: true, jumpDistance: distance });
  409. }
  410. if (!nodeB.neighbors.some(neighbor => neighbor.node === nodeA)) {
  411. nodeB.neighbors.push({ node: nodeA, isJump: true, jumpDistance: distance });
  412. }
  413. });
  414. });
  415. return graph;
  416. }
  417. function addStartEndToGraph(graph, start, end) {
  418. const nodeMap = new Map();
  419. const MAX_CONNECTIONS = 10; // Limit the number of connections from start/end points
  420. // Create a map for faster node lookups
  421. graph.forEach((node, index) => {
  422. nodeMap.set(`${node.x},${node.y}`, { node, index });
  423. });
  424. // Check if start and end points already exist in the graph
  425. const startKey = `${start.x},${start.y}`;
  426. const endKey = `${end.x},${end.y}`;
  427. let startIdx = nodeMap.has(startKey) ? nodeMap.get(startKey).index : graph.length;
  428. let endIdx = nodeMap.has(endKey) ? nodeMap.get(endKey).index : (startIdx === graph.length ? graph.length + 1 : graph.length);
  429. // Add start point if it doesn't exist
  430. if (!nodeMap.has(startKey)) {
  431. const startNode = { x: start.x, y: start.y, neighbors: [] };
  432. graph.push(startNode);
  433. nodeMap.set(startKey, { node: startNode, index: startIdx });
  434. }
  435. // Add end point if it doesn't exist
  436. if (!nodeMap.has(endKey)) {
  437. const endNode = { x: end.x, y: end.y, neighbors: [] };
  438. graph.push(endNode);
  439. nodeMap.set(endKey, { node: endNode, index: endIdx });
  440. }
  441. // Find the closest nodes to connect to start and end
  442. const startNode = graph[startIdx];
  443. const endNode = graph[endIdx];
  444. // Calculate distances from start to all other nodes
  445. const startDistances = [];
  446. graph.forEach((node, idx) => {
  447. if (idx !== startIdx) {
  448. const distance = Math.hypot(start.x - node.x, start.y - node.y);
  449. startDistances.push({ node, idx, distance });
  450. }
  451. });
  452. // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
  453. startDistances.sort((a, b) => a.distance - b.distance);
  454. startDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
  455. startNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
  456. node.neighbors.push({ node: startNode, isJump: true, jumpDistance: distance });
  457. });
  458. // Calculate distances from end to all other nodes
  459. const endDistances = [];
  460. graph.forEach((node, idx) => {
  461. if (idx !== endIdx) {
  462. const distance = Math.hypot(end.x - node.x, end.y - node.y);
  463. endDistances.push({ node, idx, distance });
  464. }
  465. });
  466. // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
  467. endDistances.sort((a, b) => a.distance - b.distance);
  468. endDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
  469. endNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
  470. node.neighbors.push({ node: endNode, isJump: true, jumpDistance: distance });
  471. });
  472. return { startIdx, endIdx };
  473. }
  474. function dijkstraWithMinimalJumps(graph, startIdx, endIdx) {
  475. const distances = Array(graph.length).fill(Infinity);
  476. const previous = Array(graph.length).fill(null);
  477. const totalJumpDistances = Array(graph.length).fill(Infinity);
  478. const priorityQueue = new PriorityQueue();
  479. const nodeIndices = new Map(); // Map to quickly find node indices
  480. // Create a map of node coordinates to indices for faster lookups
  481. graph.forEach((node, index) => {
  482. nodeIndices.set(`${node.x},${node.y}`, index);
  483. });
  484. distances[startIdx] = 0;
  485. totalJumpDistances[startIdx] = 0;
  486. priorityQueue.enqueue(0, startIdx);
  487. while (!priorityQueue.isEmpty()) {
  488. const { key: minDistanceNode } = priorityQueue.dequeue();
  489. if (minDistanceNode === endIdx) break;
  490. const currentNode = graph[minDistanceNode];
  491. currentNode.neighbors.forEach(neighbor => {
  492. // Use the map for faster node index lookup
  493. const neighborKey = `${neighbor.node.x},${neighbor.node.y}`;
  494. const neighborIdx = nodeIndices.get(neighborKey);
  495. const jumpDistance = neighbor.isJump ? neighbor.jumpDistance : 0;
  496. const alt = distances[minDistanceNode] + Math.hypot(currentNode.x - neighbor.node.x, currentNode.y - neighbor.node.y);
  497. const totalJumpDist = totalJumpDistances[minDistanceNode] + jumpDistance;
  498. if (totalJumpDist < totalJumpDistances[neighborIdx] || (totalJumpDist === totalJumpDistances[neighborIdx] && alt < distances[neighborIdx])) {
  499. distances[neighborIdx] = alt;
  500. previous[neighborIdx] = minDistanceNode;
  501. totalJumpDistances[neighborIdx] = totalJumpDist;
  502. priorityQueue.enqueue(totalJumpDist, neighborIdx);
  503. }
  504. });
  505. }
  506. const path = [];
  507. let u = endIdx;
  508. while (u !== null) {
  509. path.unshift({ x: graph[u].x, y: graph[u].y });
  510. u = previous[u];
  511. }
  512. return path;
  513. }
  514. function adjustEpsilon(epsilon, pointsOver) {
  515. if (pointsOver > 100) {
  516. return epsilon + 0.5;
  517. } else if (pointsOver <= 20) {
  518. return epsilon + 0.1;
  519. } else {
  520. // Scale adjustment for points over the target between 20 and 100
  521. let scale = (pointsOver - 20) / (100 - 20); // Normalized to range 0-1
  522. return epsilon + 0.1 + 0.5 * scale; // Adjust between 0.1 and 0.5
  523. }
  524. }
  525. // Checks if a contour is nearly closed
  526. function isNearlyClosed(contour, percentThreshold = 0.1) {
  527. // Get the bounding box of the contour
  528. const rect = cv.boundingRect(contour);
  529. const size = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
  530. // Calculate the distance between the first and last points
  531. const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
  532. const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
  533. const distance = Math.sqrt((startPoint.x - endPoint.x) ** 2 + (startPoint.y - endPoint.y) ** 2);
  534. // Use a threshold based on the size of the object
  535. const threshold = size * percentThreshold;
  536. return (distance < threshold);
  537. }
  538. // Check if contour is fully closed
  539. function isContourClosed(contour) {
  540. // Calculate the distance between the first and last points
  541. const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
  542. const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
  543. return (startPoint === endPoint);
  544. }
  545. // Checks if a PointList has the same first and last point
  546. function isFullyClosed(points) {
  547. return ((points[0].x === points[points.length - 1].x) && (points[0].y === points[points.length - 1].y));
  548. }
  549. // Closes a contour by adding the first point at the end
  550. function closeContour(points) {
  551. if (points.length > 1 && (points[0].x !== points[points.length - 1].x || points[0].y !== points[points.length - 1].y)) {
  552. points.push({ x: points[0].x, y: points[0].y });
  553. }
  554. return points;
  555. }
  556. function areContoursSimilar(contour1, contour2, similarityThreshold) {
  557. // Calculate the bounding boxes of the contours
  558. const rect1 = cv.boundingRect(contour1);
  559. const rect2 = cv.boundingRect(contour2);
  560. // Calculate the intersection of the bounding boxes
  561. const x1 = Math.max(rect1.x, rect2.x);
  562. const y1 = Math.max(rect1.y, rect2.y);
  563. const x2 = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
  564. const y2 = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
  565. // Check if there is an intersection
  566. const intersectionWidth = Math.max(0, x2 - x1);
  567. const intersectionHeight = Math.max(0, y2 - y1);
  568. const intersectionArea = intersectionWidth * intersectionHeight;
  569. // Calculate the union of the bounding boxes
  570. const area1 = rect1.width * rect1.height;
  571. const area2 = rect2.width * rect2.height;
  572. const unionArea = area1 + area2 - intersectionArea;
  573. // Calculate the similarity based on the intersection over union (IoU)
  574. const similarity = intersectionArea / unionArea;
  575. return similarity > similarityThreshold;
  576. }
  577. function deduplicateContours(contours, similarityThreshold = 0.5) {
  578. const uniqueContours = [];
  579. for (let i = 0; i < contours.size(); i++) {
  580. const contour = contours.get(i);
  581. let isDuplicate = false;
  582. for (let j = 0; j < uniqueContours.length; j++) {
  583. if (areContoursSimilar(contour, uniqueContours[j], similarityThreshold)) {
  584. isDuplicate = true;
  585. break;
  586. }
  587. }
  588. if (!isDuplicate) {
  589. uniqueContours.push(contour);
  590. }
  591. }
  592. return uniqueContours;
  593. }
  594. // Function to interpolate points along a straight line
  595. function interpolatePoints(startPoint, endPoint, numPoints) {
  596. if (numPoints <= 2) return [startPoint, endPoint];
  597. const points = [];
  598. for (let i = 0; i < numPoints; i++) {
  599. const t = i / (numPoints - 1);
  600. const x = startPoint.x + t * (endPoint.x - startPoint.x);
  601. const y = startPoint.y + t * (endPoint.y - startPoint.y);
  602. points.push({ x, y });
  603. }
  604. return points;
  605. }
  606. // Function to calculate the distance between two points
  607. function distanceBetweenPoints(p1, p2) {
  608. return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  609. }
  610. // Function to add interpolated points to a contour based on segment length
  611. function addInterpolatedPoints(points, epsilon) {
  612. if (points.length <= 1) return points;
  613. const result = [];
  614. for (let i = 0; i < points.length - 1; i++) {
  615. const startPoint = points[i];
  616. const endPoint = points[i + 1];
  617. // Calculate distance between points
  618. const distance = distanceBetweenPoints(startPoint, endPoint);
  619. // Determine how many points to add based on distance and epsilon
  620. // For longer segments and smaller epsilon values, we want more points
  621. // The smaller the epsilon, the more detailed the contour, so we add more points
  622. const pointsToAdd = Math.max(2, Math.ceil(distance / (epsilon * 5)));
  623. // Add interpolated points for this segment
  624. const interpolated = interpolatePoints(startPoint, endPoint, pointsToAdd);
  625. // Add all points except the last one (to avoid duplicates)
  626. if (i < points.length - 2) {
  627. result.push(...interpolated.slice(0, -1));
  628. } else {
  629. // For the last segment, include the end point
  630. result.push(...interpolated);
  631. }
  632. }
  633. return result;
  634. }
  635. function getOrderedContours(edgeImage, initialEpsilon, retrievalMode, maxPoints) {
  636. const contours = new cv.MatVector(), hierarchy = new cv.Mat();
  637. cv.findContours(edgeImage, contours, hierarchy, retrievalMode, cv.CHAIN_APPROX_SIMPLE);
  638. console.log("# Contours: ", contours.size());
  639. // Deduplicate contours
  640. const uniqueContours = deduplicateContours(contours);
  641. const maxIterations = 100; // Maximum iterations to avoid infinite loop
  642. let contourPoints = [];
  643. let totalPoints = 0;
  644. let epsilon = initialEpsilon;
  645. let iterations = 0;
  646. do {
  647. totalPoints = 0;
  648. contourPoints = [];
  649. for (let i = 0; i < uniqueContours.length; i++) {
  650. const contour = uniqueContours[i]; // Use [] to access array elements
  651. const simplified = new cv.Mat();
  652. cv.approxPolyDP(contour, simplified, epsilon, true);
  653. let points = [];
  654. for (let j = 0; j < simplified.rows; j++) {
  655. const point = simplified.intPtr(j);
  656. points.push({ x: point[0], y: point[1] });
  657. }
  658. simplified.delete();
  659. if (points.length > 0) { // Check for empty contours
  660. if (isNearlyClosed(contour)) { // Only close the contour if it's nearly closed
  661. points = closeContour(points);
  662. }
  663. // We no longer interpolate points here - we'll do it later only if needed
  664. if (isFullyClosed(points)) {
  665. // Move starting point to nearest the center
  666. points = reorderPointsForLoop(points);
  667. }
  668. contourPoints.push(points);
  669. totalPoints += points.length;
  670. }
  671. }
  672. if (totalPoints > maxPoints) {
  673. let pointsOver = totalPoints - maxPoints;
  674. epsilon = adjustEpsilon(epsilon, pointsOver);
  675. iterations++;
  676. }
  677. } while (totalPoints > maxPoints && iterations < maxIterations);
  678. if (totalPoints > maxPoints && iterations >= maxIterations) {
  679. let flattenedPoints = contourPoints.flat();
  680. contourPoints = [flattenedPoints.slice(0, maxPoints)]; // Take the first N points
  681. }
  682. if (contourPoints.length === 0) {
  683. console.error("No valid contours found.");
  684. return [];
  685. }
  686. // Calculate distances and find the best path
  687. const distances = calculateDistances(contourPoints);
  688. const path = tspNearestNeighbor(distances, contourPoints);
  689. const orderedContours = reorderContours(contourPoints, path);
  690. return orderedContours;
  691. }
  692. function getRandomColor() {
  693. const letters = '0123456789ABCDEF';
  694. let color = '#';
  695. for (let i = 0; i < 6; i++) {
  696. color += letters[Math.floor(Math.random() * 16)];
  697. }
  698. return color;
  699. }
  700. function resetCanvas(canvasId) {
  701. const canvas = document.getElementById(canvasId);
  702. const ctx = canvas.getContext('2d');
  703. // Store current dimensions
  704. const width = canvas.width;
  705. const height = canvas.height;
  706. // Clear the canvas
  707. ctx.clearRect(0, 0, width, height);
  708. // Reset transformation matrix
  709. ctx.setTransform(1, 0, 0, 1, 0, 0);
  710. // Ensure dimensions are maintained
  711. canvas.width = width;
  712. canvas.height = height;
  713. }
  714. function traceContours(orderedContours, isLoop = false, minimizeJumps = true) {
  715. let result = [];
  716. let pathsUsed = [...orderedContours];
  717. for (let i = 0; i < orderedContours.length - (isLoop ? 0 : 1); i++) {
  718. const currentContour = orderedContours[i];
  719. // If looping, add 1st contour again
  720. const nextContour = orderedContours[(i + 1) % orderedContours.length];
  721. const start = currentContour[currentContour.length - 1]; // End of the current contour
  722. const end = nextContour[0]; // Start of the next contour
  723. let path = [];
  724. if (minimizeJumps){
  725. // Find path between contours
  726. path = findPathWithMinimalJumpDistances(pathsUsed, start, end);
  727. }
  728. result.push(currentContour);
  729. if (path.length > 0) { // Add the path only if it has points
  730. result.push(path);
  731. pathsUsed.push(path); // Add the used path to the list of paths
  732. //console.log('Added Path: ', i, JSON.stringify(path));
  733. } else {
  734. //console.log('No Path Needed', i)
  735. }
  736. }
  737. // If not looping, add the last contour as it doesn't need a connecting path and wasn't added above
  738. if (!isLoop) { result.push(orderedContours[orderedContours.length - 1]); }
  739. return result;
  740. }
  741. function removeConsecutiveDuplicates(points) {
  742. return points.filter((point, index) => {
  743. if (index === 0) return true; // Keep the first point
  744. const prevPoint = points[index - 1];
  745. return !(point.x === prevPoint.x && point.y === prevPoint.y);
  746. });
  747. }
  748. function findPathWithMinimalJumpDistances(contours, start, end) {
  749. const graph = createGraphWithConnectionTypes(contours);
  750. const { startIdx, endIdx } = addStartEndToGraph(graph, start, end);
  751. const path = dijkstraWithMinimalJumps(graph, startIdx, endIdx);
  752. return path;
  753. }
  754. function generateDots(edgeImage) {
  755. // Reset the canvas before drawing the new image
  756. resetCanvas('dot-image');
  757. resetCanvas('connect-image');
  758. // Ensure canvases have the same dimensions
  759. const dotCanvas = document.getElementById('dot-image');
  760. const connectCanvas = document.getElementById('connect-image');
  761. const originalCanvas = document.getElementById('original-image');
  762. const edgeCanvas = document.getElementById('edge-image');
  763. if (originalImageElement) {
  764. // Set all canvases to the same dimensions
  765. dotCanvas.width = originalImageElement.naturalWidth;
  766. dotCanvas.height = originalImageElement.naturalHeight;
  767. connectCanvas.width = originalImageElement.naturalWidth;
  768. connectCanvas.height = originalImageElement.naturalHeight;
  769. }
  770. const epsilon = parseFloat(document.getElementById('epsilon-slider').value),
  771. contourMode = document.getElementById('contour-mode').value,
  772. isLoop = document.getElementById('is-loop').checked,
  773. minimizeJumps = document.getElementById('no-shortcuts').checked,
  774. outputFormat = parseInt(document.getElementById('output-type').value),
  775. maxPoints = parseInt(document.getElementById('dot-number').value);
  776. // useGaussianBlur = document.getElementById('gaussian-blur-toggle').checked,
  777. const retrievalMode = (contourMode == 'External') ? cv.RETR_EXTERNAL : cv.RETR_TREE;
  778. orderedContours = getOrderedContours(edgeImage, epsilon, retrievalMode, maxPoints);
  779. console.log('Ordered Contours: ', JSON.stringify(orderedContours));
  780. const tracedContours = traceContours(orderedContours, isLoop, minimizeJumps);
  781. console.log('Traced: ', JSON.stringify(tracedContours));
  782. // Only apply additional interpolation if .thr format (format 2) is selected
  783. let processedContours;
  784. if (outputFormat === 2) {
  785. // Apply interpolation for .thr format which needs more points for straight lines
  786. processedContours = tracedContours.map(contour =>
  787. addInterpolatedPoints(contour, epsilon)
  788. );
  789. } else {
  790. processedContours = tracedContours;
  791. }
  792. plotContours(processedContours);
  793. // Save for future plotting
  794. orderedContoursSave = processedContours;
  795. let orderedPoints = processedContours.flat();
  796. // Should always be the case for isLoop
  797. if (isFullyClosed(orderedPoints) || isLoop) {
  798. orderedPoints = reorderPointsForLoop(orderedPoints);
  799. }
  800. orderedPoints = removeConsecutiveDuplicates(orderedPoints);
  801. // For final output - if last point is same as first point, drop it.
  802. if (isFullyClosed(orderedPoints)) {
  803. orderedPoints = [...orderedPoints.slice(0,orderedPoints.length-1)];
  804. }
  805. const polarPoints = drawDots(orderedPoints);
  806. WriteCoords(polarPoints, outputFormat);
  807. drawConnections(polarPoints);
  808. document.getElementById('total-points').innerText = `(${orderedPoints.length} Points)`;
  809. }
  810. function WriteCoords(polarPoints, outputFormat = 0){
  811. let formattedPolarPoints = '';
  812. switch (outputFormat) {
  813. case 0: //Default
  814. // For Image2Sand.ino code, we normalize the theta values
  815. // We'll use modulo for this format
  816. formattedPolarPoints = polarPoints.map(p => {
  817. const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
  818. return `{${p.r.toFixed(0)},${normalizedTheta.toFixed(0)}}`;
  819. }).join(',');
  820. break;
  821. case 1: //Single Byte
  822. // For single byte format, we need to normalize the theta values
  823. // We'll use modulo for this format since it's just for Arduino code
  824. formattedPolarPoints = polarPoints.map(p => {
  825. const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
  826. return `{${Math.round(255 * p.r / 1000)},${Math.round(255 * normalizedTheta / 3600)}}`;
  827. }).join(',');
  828. break;
  829. case 2: //.thr
  830. // For .thr format, we keep the continuous theta values
  831. // Convert from tenths of degrees back to radians
  832. // Apply a 90° clockwise rotation by subtracting π/2 (900 in tenths of degrees) from theta
  833. formattedPolarPoints = polarPoints.map(p => {
  834. // Subtract 900 (90 degrees) to rotate clockwise
  835. const rotatedTheta = p.theta - 900;
  836. return `${(-rotatedTheta * Math.PI / 1800).toFixed(5)} ${(p.r / 1000).toFixed(5)}`;
  837. }).join("\n");
  838. break;
  839. case 3: // whitespace (might cause problems as it outputs a space)
  840. // For whitespace format, we need to normalize the theta values
  841. // We'll use modulo for this format
  842. formattedPolarPoints = polarPoints.map(p => {
  843. const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
  844. return `${Math.round(255 * p.r / 1000).toString(2).padStart(8,'0').replaceAll('0',' ').replaceAll('1',"\t")}${Math.round(255 * normalizedTheta / 3600).toString(2).padStart(8,'0').replaceAll('0',' ').replaceAll('1',"\t")}`;
  845. }).join("\n");
  846. break;
  847. default:
  848. break;
  849. }
  850. document.getElementById('polar-coordinates-textarea').value = formattedPolarPoints;
  851. document.getElementById('simple-coords').textContent = formattedPolarPoints;
  852. document.getElementById('simple-coords-title').style = 'visibility: hidden';
  853. }
  854. function reorderPointsForLoop(points, startNear = calculateCentroid(points)) {
  855. let minDist = Infinity;
  856. let startIndex = 0;
  857. // Find the point nearest to the centroid
  858. points.forEach((point, index) => {
  859. const dist = Math.hypot(point.x - startNear.x, point.y - startNear.y);
  860. if (dist < minDist) {
  861. minDist = dist;
  862. startIndex = index;
  863. }
  864. });
  865. // Reorder points to start from the point nearest to the centroid
  866. return removeConsecutiveDuplicates([...points.slice(startIndex), ...points.slice(0, startIndex+1)]);
  867. }
  868. function drawDots(points) {
  869. const canvas = document.getElementById('dot-image'), ctx = canvas.getContext('2d');
  870. // Set canvas dimensions to match the original image
  871. if (originalImageElement) {
  872. canvas.width = originalImageElement.naturalWidth;
  873. canvas.height = originalImageElement.naturalHeight;
  874. }
  875. ctx.clearRect(0, 0, canvas.width, canvas.height);
  876. const width = canvas.width, height = canvas.height;
  877. const scaleX = width / originalImageElement.width;
  878. const scaleY = height / originalImageElement.height;
  879. const scale = Math.min(scaleX, scaleY);
  880. points = points.map(p => ({ x: (p.x) * scale, y: (p.y) * scale }));
  881. points.forEach(point => {
  882. ctx.beginPath();
  883. ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
  884. ctx.fill();
  885. });
  886. const formattedPoints = points.map(p => `{${p.x.toFixed(2)}, ${p.y.toFixed(2)}}`).join(',\n');
  887. // Calculate polar coordinates
  888. const center = findMaximalCenter(points);
  889. points = points.map(p => ({ x: p.x - center.centerX, y: p.y - center.centerY }));
  890. // Calculate initial angles for all points
  891. let polarPoints = points.map(p => {
  892. const r = Math.sqrt(p.x * p.x + p.y * p.y);
  893. // Get the basic angle in radians
  894. let theta = Math.atan2(p.y, p.x);
  895. // Adjust theta to align 0 degrees to the right and 90 degrees up by flipping the y-axis
  896. theta = -theta;
  897. return {
  898. r: r * (1000 / Math.max(...points.map(p => Math.sqrt(p.x * p.x + p.y * p.y)))),
  899. theta: theta, // Store in radians initially
  900. x: p.x,
  901. y: p.y
  902. };
  903. });
  904. // Process points to create continuous theta values
  905. for (let i = 1; i < polarPoints.length; i++) {
  906. const prev = polarPoints[i-1];
  907. const curr = polarPoints[i];
  908. // Calculate the difference between current and previous theta
  909. let diff = curr.theta - prev.theta;
  910. // If the difference is greater than π, it means we've wrapped around counterclockwise
  911. // Adjust by subtracting 2π
  912. if (diff > Math.PI) {
  913. curr.theta -= 2 * Math.PI;
  914. }
  915. // If the difference is less than -π, it means we've wrapped around clockwise
  916. // Adjust by adding 2π
  917. else if (diff < -Math.PI) {
  918. curr.theta += 2 * Math.PI;
  919. }
  920. }
  921. // Convert to degrees * 10 for the final format
  922. polarPoints = polarPoints.map(p => ({
  923. r: p.r,
  924. theta: p.theta * (1800 / Math.PI) // Convert radians to tenths of degrees
  925. }));
  926. return polarPoints;
  927. }
  928. function plotContours(orderedContours) {
  929. const canvas = document.getElementById('plotcontours');
  930. const ctx = canvas.getContext('2d');
  931. ctx.clearRect(0, 0, canvas.width, canvas.height);
  932. orderedContours.forEach((contour, index) => {
  933. const baseColor = getRandomColor();
  934. const [r, g, b] = hexToRgb(baseColor);
  935. const length = contour.length;
  936. contour.forEach((point, i) => {
  937. if (i === 0) {
  938. ctx.beginPath();
  939. ctx.moveTo(point.x, point.y);
  940. } else {
  941. ctx.lineTo(point.x, point.y);
  942. // Calculate color fade
  943. const ratio = i / length;
  944. const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
  945. ctx.strokeStyle = fadedColor;
  946. ctx.lineWidth = 2;
  947. ctx.stroke();
  948. ctx.beginPath();
  949. ctx.moveTo(point.x, point.y);
  950. }
  951. });
  952. // Mark the start and end points
  953. ctx.fillStyle = baseColor;
  954. ctx.font = '12px Arial';
  955. // Start point
  956. ctx.fillText(`S${index + 1}`, contour[0].x, contour[0].y);
  957. ctx.beginPath();
  958. ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
  959. ctx.fill();
  960. // End point
  961. ctx.fillText(`E${index + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
  962. ctx.beginPath();
  963. ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
  964. ctx.fill();
  965. // Label the contour with its number
  966. const midPoint = contour[Math.floor(contour.length / 2)];
  967. ctx.fillText(`${index + 1}`, midPoint.x, midPoint.y);
  968. });
  969. }
  970. function hexToRgb(hex) {
  971. const bigint = parseInt(hex.slice(1), 16);
  972. return [
  973. (bigint >> 16) & 255,
  974. (bigint >> 8) & 255,
  975. bigint & 255
  976. ];
  977. }
  978. function drawConnections(polarPoints) {
  979. const canvas = document.getElementById('connect-image'), ctx = canvas.getContext('2d');
  980. ctx.clearRect(0, 0, canvas.width, canvas.height);
  981. const width = canvas.width, height = canvas.height;
  982. // Reset transformation matrix
  983. ctx.setTransform(1, 0, 0, 1, 0, 0);
  984. // Translate the context to the center of the canvas
  985. ctx.translate(width / 2, height / 2);
  986. // Scale the points based on the size of the original image
  987. const scaleX = width / 2000; // Since the circle radius is 1000
  988. const scaleY = height / 2000;
  989. const scale = Math.min(scaleX, scaleY);
  990. // Draw the outline circle
  991. ctx.beginPath();
  992. ctx.arc(0, 0, 1000 * scale, 0, 2 * Math.PI);
  993. ctx.strokeStyle = 'black';
  994. ctx.stroke();
  995. // Draw the connections based on polar coordinates
  996. for (let i = 0; i < polarPoints.length - 1; i++) {
  997. // Calculate the color for each segment
  998. const t = i / (polarPoints.length - 1);
  999. const color = `hsl(${t * 270}, 100%, 50%)`; // 270 degrees covers red to violet
  1000. ctx.strokeStyle = color;
  1001. const p1 = polarPoints[i];
  1002. const p2 = polarPoints[i + 1];
  1003. // Convert from tenths of degrees to radians for visualization
  1004. const theta1 = p1.theta * Math.PI / 1800;
  1005. const theta2 = p2.theta * Math.PI / 1800;
  1006. // Adjust y-coordinate calculation to invert the y-axis
  1007. const x1 = p1.r * Math.cos(theta1) * scale;
  1008. const y1 = -p1.r * Math.sin(theta1) * scale;
  1009. const x2 = p2.r * Math.cos(theta2) * scale;
  1010. const y2 = -p2.r * Math.sin(theta2) * scale;
  1011. ctx.beginPath();
  1012. ctx.moveTo(x1, y1);
  1013. ctx.lineTo(x2, y2);
  1014. ctx.stroke();
  1015. }
  1016. }
  1017. function convertImage() {
  1018. originalImageElement && processImage(originalImageElement);
  1019. }
  1020. // Function to get URL parameters
  1021. function getUrlParams() {
  1022. const params = new URLSearchParams(window.location.search);
  1023. return {
  1024. apikey: params.get('apikey'),
  1025. prompt: params.get('prompt'),
  1026. run: params.get('run')
  1027. };
  1028. }
  1029. // Function to fill inputs from URL parameters
  1030. function fillInputsFromParams(params) {
  1031. if (params.apikey) {
  1032. document.getElementById('api-key').value = params.apikey;
  1033. }
  1034. if (params.prompt) {
  1035. document.getElementById('prompt').value = params.prompt;
  1036. }
  1037. }
  1038. function setDefaultsForAutoGenerate() {
  1039. document.getElementById('epsilon-slider').value = 0.5;
  1040. document.getElementById('dot-number').value = 300;
  1041. document.getElementById('no-shortcuts').checked = true;
  1042. document.getElementById('is-loop').checked = true;
  1043. document.getElementById('contour-mode').value = 'Tree';
  1044. hiddenResponse();
  1045. }
  1046. function hiddenResponse() {
  1047. document.getElementById('master-container').style = 'display: none;';
  1048. document.getElementById('simple-container').style = 'visibility: visible';
  1049. }
  1050. document.addEventListener('DOMContentLoaded', function() {
  1051. const fileInput = document.getElementById('file-input');
  1052. const fileButton = document.getElementById('file-button');
  1053. const fileNameDisplay = document.getElementById('file-name');
  1054. const generateButton = document.getElementById('generate-button');
  1055. const epsilonSlider = document.getElementById('epsilon-slider');
  1056. const epsilonValueDisplay = document.getElementById('epsilon-value-display');
  1057. const dotNumberInput = document.getElementById('dot-number');
  1058. const contourModeSelect = document.getElementById('contour-mode');
  1059. //const gaussianBlurToggle = document.getElementById('gaussian-blur-toggle');
  1060. document.getElementById('plotButton').addEventListener('click', plotNextContour);
  1061. generateButton.addEventListener('click', convertImage);
  1062. fileButton.addEventListener('click', function() {
  1063. fileInput.click();
  1064. });
  1065. fileInput.addEventListener('change', function(event) {
  1066. const file = event.target.files[0];
  1067. if (file) {
  1068. fileNameDisplay.textContent = file.name;
  1069. handleImageUpload(event);
  1070. }
  1071. });
  1072. epsilonSlider.addEventListener('input', function() {
  1073. epsilonValueDisplay.textContent = epsilonSlider.value;
  1074. });
  1075. window.showTab = function(tabName) {
  1076. const tabContents = document.querySelectorAll('.tab-content');
  1077. tabContents.forEach(content => { content.style.display = 'none'; });
  1078. document.getElementById(tabName).style.display = 'block';
  1079. };
  1080. });
  1081. // Initialize the page with URL parameters if present
  1082. document.addEventListener('DOMContentLoaded', (event) => {
  1083. const { apikey, prompt, run } = getUrlParams();
  1084. // Fill inputs with URL parameters if they exist
  1085. fillInputsFromParams({ apikey, prompt });
  1086. if (apikey) {
  1087. document.getElementById('api-key-group').style.display = 'none';
  1088. }
  1089. // Generate image if all parameters are present
  1090. if (apikey && prompt && run) {
  1091. setDefaultsForAutoGenerate();
  1092. generateImage(apikey, prompt, run);
  1093. convertImage();
  1094. }
  1095. // Add event listener to the button inside the DOMContentLoaded event
  1096. document.getElementById('gen-image-button').addEventListener('click', () => {
  1097. let apiKey = document.getElementById('api-key').value;
  1098. const prompt = document.getElementById('prompt').value + (document.getElementById('googly-eyes').checked ? ' with disproportionately large googly eyes' : '');
  1099. generateImage(apiKey, prompt, false);
  1100. });
  1101. });