image2sand.js 49 KB

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