| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400 |
- /*
- * Image2Sand - Convert images to sand table coordinates
- *
- * This script processes images and converts them to polar coordinates
- * according to the specified settings, supporting multiple output formats including:
- * Default: HackPack Sand Garden .ino in this repository
- * theta-rho format: for use with sand tables like Sisyphus and Dune Weaver Mini.
- *
- * Note:
- * For Dune Weaver Mini compatibility, this script uses continuous theta values
- * that can exceed 2π (360 degrees). This allows the arm to make multiple revolutions
- * without creating unintended circles in the patterns.
- */
- class PriorityQueue {
- constructor() {
- this.heap = [];
- }
- enqueue(priority, key) {
- this.heap.push({ key, priority });
- this._bubbleUp(this.heap.length - 1);
- }
- dequeue() {
- const min = this.heap[0];
- const end = this.heap.pop();
-
- if (this.heap.length > 0) {
- this.heap[0] = end;
- this._sinkDown(0);
- }
-
- return min;
- }
- isEmpty() {
- return this.heap.length === 0;
- }
- _bubbleUp(index) {
- const element = this.heap[index];
-
- while (index > 0) {
- const parentIndex = Math.floor((index - 1) / 2);
- const parent = this.heap[parentIndex];
-
- if (element.priority >= parent.priority) break;
-
- this.heap[parentIndex] = element;
- this.heap[index] = parent;
- index = parentIndex;
- }
- }
- _sinkDown(index) {
- const length = this.heap.length;
- const element = this.heap[index];
-
- while (true) {
- const leftChildIndex = 2 * index + 1;
- const rightChildIndex = 2 * index + 2;
- let smallestChildIndex = null;
-
- if (leftChildIndex < length) {
- if (this.heap[leftChildIndex].priority < element.priority) {
- smallestChildIndex = leftChildIndex;
- }
- }
-
- if (rightChildIndex < length) {
- if (
- (smallestChildIndex === null && this.heap[rightChildIndex].priority < element.priority) ||
- (smallestChildIndex !== null && this.heap[rightChildIndex].priority < this.heap[leftChildIndex].priority)
- ) {
- smallestChildIndex = rightChildIndex;
- }
- }
-
- if (smallestChildIndex === null) break;
-
- this.heap[index] = this.heap[smallestChildIndex];
- this.heap[smallestChildIndex] = element;
- index = smallestChildIndex;
- }
- }
- }
- let currentContourIndex = 0;
- let isFirstClick = true;
- let originalImageElement = null;
- let isGeneratingImage = false;
- let isGeneratingCoords = false;
- function drawAndPrepImage(imgElement) {
- const canvas = document.getElementById('original-image');
- const ctx = canvas.getContext('2d');
- canvas.width = imgElement.naturalWidth;
- canvas.height = imgElement.naturalHeight;
- ctx.drawImage(imgElement, 0, 0);
- // Set originalImageElement to the current image
- originalImageElement = imgElement;
- // Enable Generate button
- document.getElementById('generate-button').disabled = false;
- }
- async function generateImage(apiKey, prompt, autoprocess) {
- if (isGeneratingImage) {
- document.getElementById('generation-status').textContent = "Image is still generating - please don't press the button.";
- } else {
- isGeneratingImage = true;
- document.getElementById('gen-image-button').disabled = true;
- document.getElementById('generation-status').style.display = 'block';
- try {
- 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.`;
- const response = await fetch('https://api.openai.com/v1/images/generations', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- model: 'dall-e-3',
- prompt: fullPrompt,
- size: '1024x1024',
- quality: 'standard',
- response_format: 'b64_json', // Specify base64 encoding
- n: 1
- })
- });
- const data = await response.json();
- //const imageUrl = data.data[0].url;
- if ('error' in data) {
- throw new Error(data.error.message);
- }
- const imageData = data.data[0].b64_json;
- console.log("Image Data: ", imageData);
- const imgElement = new Image();
- imgElement.onload = function() {
- drawAndPrepImage(imgElement);
- if (autoprocess) {
- convertImage();
- }
- };
- imgElement.src = `data:image/png;base64,${imageData}`;
- console.log(`Image generated successfully`);
- } catch (error) {
- console.error('Image generation error:', error);
- }
- isGeneratingImage = false;
- document.getElementById('generation-status').style.display = 'none';
- document.getElementById('generation-status').textContent = "Image is generating - please wait...";
- document.getElementById('gen-image-button').disabled = false;
- }
- }
- function handleImageUpload(event) {
- const file = event.target.files[0];
- const reader = new FileReader();
- reader.onload = e => {
- if (!originalImageElement) {
- originalImageElement = new Image();
- originalImageElement.id = 'uploaded-image';
- originalImageElement.onload = () => {
- drawAndPrepImage(originalImageElement);
- };
- document.getElementById('original-image').appendChild(originalImageElement);
- }
- originalImageElement.src = e.target.result;
- };
- reader.readAsDataURL(file);
- }
- function processImage(imgElement) {
- document.getElementById('processing-status').style.display = 'block';
- document.getElementById('generate-button').disabled = true;
- // Use setTimeout to allow the UI to update
- setTimeout(() => {
- const src = cv.imread(imgElement), dst = new cv.Mat();
- cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
- cv.Canny(src, dst, 50, 150, 3, false);
- // Add morphological operations
- const kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));
- cv.dilate(dst, dst, kernel);
- cv.erode(dst, dst, kernel);
- // Invert the colors of the detected edges image
- cv.bitwise_not(dst, dst);
- // Ensure edge-image canvas has the same dimensions as the original image
- const edgeCanvas = document.getElementById('edge-image');
- edgeCanvas.width = imgElement.naturalWidth;
- edgeCanvas.height = imgElement.naturalHeight;
- // Show the edge detection results
- cv.imshow('edge-image', dst);
-
- // Make the canvas visible
- edgeCanvas.classList.remove('hidden');
-
- // Hide the placeholder
- const placeholder = edgeCanvas.parentElement.querySelector('.upload-placeholder');
- if (placeholder) {
- placeholder.style.display = 'none';
- }
-
- // Switch to the edge detection tab
- switchTab('edge', 'image-converter');
- cv.bitwise_not(dst, dst);
- generateDots(dst);
- src.delete(); dst.delete();
- // Hide the processing status label
- document.getElementById('processing-status').style.display = 'none';
- document.getElementById('generate-button').disabled = false;
- }, 0);
- }
- function plotNextContour() {
- const canvas = document.getElementById('plotcontours');
- const ctx = canvas.getContext('2d');
- if (isFirstClick) {
- // Clear the canvas on first click
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- isFirstClick = false;
- }
- console.log('Cur Contour: ', currentContourIndex + '/' + orderedContoursSave.length + ":", JSON.stringify(orderedContoursSave[currentContourIndex]));
- if (currentContourIndex < orderedContoursSave.length) {
- const contour = orderedContoursSave[currentContourIndex];
- const baseColor = getRandomColor();
- const [r, g, b] = hexToRgb(baseColor);
- const length = contour.length;
- contour.forEach((point, i) => {
- if (i === 0) {
- ctx.beginPath();
- ctx.moveTo(point.x, point.y);
- } else {
- ctx.lineTo(point.x, point.y);
- // Calculate color fade
- const ratio = i / length;
- const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
- ctx.strokeStyle = fadedColor;
- ctx.lineWidth = 2;
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(point.x, point.y);
- }
- });
- // Mark the start and end points
- ctx.fillStyle = baseColor;
- ctx.font = '12px Arial';
- // Start point
- ctx.fillText(`S${currentContourIndex + 1}`, contour[0].x, contour[0].y);
- ctx.beginPath();
- ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
- ctx.fill();
- // End point
- ctx.fillText(`E${currentContourIndex + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
- ctx.beginPath();
- ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
- ctx.fill();
- // Label the contour with its number
- const midPoint = contour[Math.floor(contour.length / 2)];
- ctx.fillText(`${currentContourIndex + 1}`, midPoint.x, midPoint.y);
- // Increment the contour index
- currentContourIndex++;
- } else {
- alert("All contours have been plotted. Starting over.");
- currentContourIndex = 0; // Reset the index
- isFirstClick = true; // Reset the first click flag
- }
- }
- function findNearestPoint(lastPoint, contours, visitedPoints) {
- let nearestPoint = null;
- let nearestDistance = Infinity;
- contours.forEach(contour => {
- contour.forEach(point => {
- if (!point || visitedPoints.has(JSON.stringify(point))) return;
- const distance = Math.sqrt(
- Math.pow(lastPoint.x - point.x, 2) +
- Math.pow(lastPoint.y - point.y, 2)
- );
- if (distance < nearestDistance) {
- nearestDistance = distance;
- nearestPoint = point;
- }
- });
- });
- return nearestPoint;
- }
- function findMaximalCenter(points) {
- const minX = Math.min(...points.map(p => p.x));
- const maxX = Math.max(...points.map(p => p.x));
- const minY = Math.min(...points.map(p => p.y));
- const maxY = Math.max(...points.map(p => p.y));
- const centerX = (minX + maxX) / 2;
- const centerY = (minY + maxY) / 2;
- const width = maxX - minX;
- const height = maxY - minY;
- return { centerX, centerY, width, height };
- }
- function calculateCentroid(points) {
- let sumX = 0, sumY = 0;
- points.forEach(p => {
- sumX += p.x;
- sumY += p.y;
- });
- return { x: sumX / points.length, y: sumY / points.length };
- }
- function calculateDistances(contours) {
- const distances = [];
- for (let i = 0; i < contours.length; i++) {
- distances[i] = [];
- for (let j = 0; j < contours.length; j++) {
- if (i !== j) {
- const startToStart = Math.hypot(contours[i][0].x - contours[j][0].x, contours[i][0].y - contours[j][0].y);
- 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);
- 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);
- 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);
- distances[i][j] = Math.min(startToStart, startToEnd, endToStart, endToEnd);
- }
- }
- }
- return distances;
- }
- function tspNearestNeighbor(distances, contours) {
- const path = [0];
- const visited = new Set([0]);
- while (path.length < contours.length) {
- let last = path[path.length - 1];
- let nearest = -1;
- let nearestDistance = Infinity;
- for (let i = 0; i < contours.length; i++) {
- if (!visited.has(i) && distances[last][i] < nearestDistance) {
- nearestDistance = distances[last][i];
- nearest = i;
- }
- }
- if (nearest !== -1) {
- path.push(nearest);
- visited.add(nearest);
- }
- }
- return path;
- }
- function reorderContours(contours, path) {
- const orderedContours = [];
- console.log("Path:", path); // Debugging
- for (let i = 0; i < path.length; i++) {
- const contourIndex = path[i];
- let contour = contours[contourIndex];
- // Determine the direction to use the contour
- if (i > 0) {
- const prevContour = orderedContours[orderedContours.length - 1];
- const prevPoint = prevContour[prevContour.length - 1];
-
- if (isFullyClosed(contour)) {
- // Contour is fully closed, so can move the startPoint
- contour = reorderPointsForLoop(contour, startNear = prevPoint)
- } else if (prevPoint && contour[0]) {
- // Contour not fully closed, decide whether to reverse contour
- const startToStart = Math.hypot(prevPoint.x - contour[0].x, prevPoint.y - contour[0].y);
- const startToEnd = Math.hypot(prevPoint.x - contour[contour.length - 1].x, prevPoint.y - contour[contour.length - 1].y);
- if (startToEnd < startToStart) {
- contour.reverse();
- }
- } else {
- console.error('Previous point or current contour start point is undefined.', { prevPoint, currentStart: contour[0] });
- continue; // Skip if any point is undefined
- }
- }
- orderedContours.push(contour);
- }
- return orderedContours;
- }
- function findClosestPoint(contours, point) {
- let minDistance = Infinity;
- let closestPoint = null;
- contours.forEach(contour => {
- contour.forEach(pt => {
- const distance = Math.hypot(point.x - pt.x, point.y - pt.y);
- if (distance < minDistance) {
- minDistance = distance;
- closestPoint = pt;
- }
- });
- });
- return closestPoint;
- }
- function createGraphWithConnectionTypes(contours) {
- const graph = [];
- const nodeMap = new Map(); // Map to quickly find nodes by coordinates
- const MAX_JUMP_CONNECTIONS = 10; // Limit the number of jump connections per node
- // Create nodes for each point in the contours
- contours.forEach(contour => {
- contour.forEach(pt => {
- const key = `${pt.x},${pt.y}`;
- if (!nodeMap.has(key)) {
- const node = { x: pt.x, y: pt.y, neighbors: [] };
- graph.push(node);
- nodeMap.set(key, node);
- }
- });
- });
- // Connect points within the same contour (regular path connections)
- contours.forEach(contour => {
- for (let i = 0; i < contour.length; i++) {
- const key = `${contour[i].x},${contour[i].y}`;
- const node = nodeMap.get(key);
-
- if (i > 0) {
- const prevKey = `${contour[i - 1].x},${contour[i - 1].y}`;
- const prevNode = nodeMap.get(prevKey);
-
- if (!node.neighbors.some(neighbor => neighbor.node === prevNode)) {
- node.neighbors.push({ node: prevNode, isJump: false });
- prevNode.neighbors.push({ node: node, isJump: false });
- }
- }
- }
- });
- // Create a spatial index for efficient nearest neighbor search
- const spatialIndex = [];
- graph.forEach(node => {
- spatialIndex.push({
- node: node,
- x: node.x,
- y: node.y
- });
- });
- // Connect nodes from different contours with jump connections, but limit the number
- graph.forEach(nodeA => {
- // Sort other nodes by distance to current node
- const distances = spatialIndex
- .filter(item => item.node !== nodeA)
- .map(item => ({
- node: item.node,
- distance: Math.hypot(nodeA.x - item.node.x, nodeA.y - item.node.y)
- }))
- .sort((a, b) => a.distance - b.distance);
-
- // Only connect to the closest MAX_JUMP_CONNECTIONS nodes
- distances.slice(0, MAX_JUMP_CONNECTIONS).forEach(({ node: nodeB, distance }) => {
- if (!nodeA.neighbors.some(neighbor => neighbor.node === nodeB)) {
- nodeA.neighbors.push({ node: nodeB, isJump: true, jumpDistance: distance });
- }
- if (!nodeB.neighbors.some(neighbor => neighbor.node === nodeA)) {
- nodeB.neighbors.push({ node: nodeA, isJump: true, jumpDistance: distance });
- }
- });
- });
- return graph;
- }
- function addStartEndToGraph(graph, start, end) {
- const nodeMap = new Map();
- const MAX_CONNECTIONS = 10; // Limit the number of connections from start/end points
-
- // Create a map for faster node lookups
- graph.forEach((node, index) => {
- nodeMap.set(`${node.x},${node.y}`, { node, index });
- });
-
- // Check if start and end points already exist in the graph
- const startKey = `${start.x},${start.y}`;
- const endKey = `${end.x},${end.y}`;
-
- let startIdx = nodeMap.has(startKey) ? nodeMap.get(startKey).index : graph.length;
- let endIdx = nodeMap.has(endKey) ? nodeMap.get(endKey).index : (startIdx === graph.length ? graph.length + 1 : graph.length);
-
- // Add start point if it doesn't exist
- if (!nodeMap.has(startKey)) {
- const startNode = { x: start.x, y: start.y, neighbors: [] };
- graph.push(startNode);
- nodeMap.set(startKey, { node: startNode, index: startIdx });
- }
-
- // Add end point if it doesn't exist
- if (!nodeMap.has(endKey)) {
- const endNode = { x: end.x, y: end.y, neighbors: [] };
- graph.push(endNode);
- nodeMap.set(endKey, { node: endNode, index: endIdx });
- }
-
- // Find the closest nodes to connect to start and end
- const startNode = graph[startIdx];
- const endNode = graph[endIdx];
-
- // Calculate distances from start to all other nodes
- const startDistances = [];
- graph.forEach((node, idx) => {
- if (idx !== startIdx) {
- const distance = Math.hypot(start.x - node.x, start.y - node.y);
- startDistances.push({ node, idx, distance });
- }
- });
-
- // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
- startDistances.sort((a, b) => a.distance - b.distance);
- startDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
- startNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
- node.neighbors.push({ node: startNode, isJump: true, jumpDistance: distance });
- });
-
- // Calculate distances from end to all other nodes
- const endDistances = [];
- graph.forEach((node, idx) => {
- if (idx !== endIdx) {
- const distance = Math.hypot(end.x - node.x, end.y - node.y);
- endDistances.push({ node, idx, distance });
- }
- });
-
- // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
- endDistances.sort((a, b) => a.distance - b.distance);
- endDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
- endNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
- node.neighbors.push({ node: endNode, isJump: true, jumpDistance: distance });
- });
-
- return { startIdx, endIdx };
- }
- function dijkstraWithMinimalJumps(graph, startIdx, endIdx) {
- const distances = Array(graph.length).fill(Infinity);
- const previous = Array(graph.length).fill(null);
- const totalJumpDistances = Array(graph.length).fill(Infinity);
- const priorityQueue = new PriorityQueue();
- const nodeIndices = new Map(); // Map to quickly find node indices
-
- // Create a map of node coordinates to indices for faster lookups
- graph.forEach((node, index) => {
- nodeIndices.set(`${node.x},${node.y}`, index);
- });
- distances[startIdx] = 0;
- totalJumpDistances[startIdx] = 0;
- priorityQueue.enqueue(0, startIdx);
- while (!priorityQueue.isEmpty()) {
- const { key: minDistanceNode } = priorityQueue.dequeue();
- if (minDistanceNode === endIdx) break;
- const currentNode = graph[minDistanceNode];
- currentNode.neighbors.forEach(neighbor => {
- // Use the map for faster node index lookup
- const neighborKey = `${neighbor.node.x},${neighbor.node.y}`;
- const neighborIdx = nodeIndices.get(neighborKey);
-
- const jumpDistance = neighbor.isJump ? neighbor.jumpDistance : 0;
- const alt = distances[minDistanceNode] + Math.hypot(currentNode.x - neighbor.node.x, currentNode.y - neighbor.node.y);
- const totalJumpDist = totalJumpDistances[minDistanceNode] + jumpDistance;
- if (totalJumpDist < totalJumpDistances[neighborIdx] || (totalJumpDist === totalJumpDistances[neighborIdx] && alt < distances[neighborIdx])) {
- distances[neighborIdx] = alt;
- previous[neighborIdx] = minDistanceNode;
- totalJumpDistances[neighborIdx] = totalJumpDist;
- priorityQueue.enqueue(totalJumpDist, neighborIdx);
- }
- });
- }
- const path = [];
- let u = endIdx;
- while (u !== null) {
- path.unshift({ x: graph[u].x, y: graph[u].y });
- u = previous[u];
- }
- return path;
- }
- function adjustEpsilon(epsilon, pointsOver) {
- if (pointsOver > 100) {
- return epsilon + 0.5;
- } else if (pointsOver <= 20) {
- return epsilon + 0.1;
- } else {
- // Scale adjustment for points over the target between 20 and 100
- let scale = (pointsOver - 20) / (100 - 20); // Normalized to range 0-1
- return epsilon + 0.1 + 0.5 * scale; // Adjust between 0.1 and 0.5
- }
- }
- // Checks if a contour is nearly closed
- function isNearlyClosed(contour, percentThreshold = 0.1) {
- // Get the bounding box of the contour
- const rect = cv.boundingRect(contour);
- const size = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
- // Calculate the distance between the first and last points
- const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
- const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
- const distance = Math.sqrt((startPoint.x - endPoint.x) ** 2 + (startPoint.y - endPoint.y) ** 2);
- // Use a threshold based on the size of the object
- const threshold = size * percentThreshold;
- return (distance < threshold);
- }
- // Check if contour is fully closed
- function isContourClosed(contour) {
- // Calculate the distance between the first and last points
- const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
- const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
-
- return (startPoint === endPoint);
- }
- // Checks if a PointList has the same first and last point
- function isFullyClosed(points) {
- return ((points[0].x === points[points.length - 1].x) && (points[0].y === points[points.length - 1].y));
- }
- // Closes a contour by adding the first point at the end
- function closeContour(points) {
- if (points.length > 1 && (points[0].x !== points[points.length - 1].x || points[0].y !== points[points.length - 1].y)) {
- points.push({ x: points[0].x, y: points[0].y });
- }
- return points;
- }
- function areContoursSimilar(contour1, contour2, similarityThreshold) {
- // Calculate the bounding boxes of the contours
- const rect1 = cv.boundingRect(contour1);
- const rect2 = cv.boundingRect(contour2);
- // Calculate the intersection of the bounding boxes
- const x1 = Math.max(rect1.x, rect2.x);
- const y1 = Math.max(rect1.y, rect2.y);
- const x2 = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
- const y2 = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
- // Check if there is an intersection
- const intersectionWidth = Math.max(0, x2 - x1);
- const intersectionHeight = Math.max(0, y2 - y1);
- const intersectionArea = intersectionWidth * intersectionHeight;
- // Calculate the union of the bounding boxes
- const area1 = rect1.width * rect1.height;
- const area2 = rect2.width * rect2.height;
- const unionArea = area1 + area2 - intersectionArea;
- // Calculate the similarity based on the intersection over union (IoU)
- const similarity = intersectionArea / unionArea;
- return similarity > similarityThreshold;
- }
- function deduplicateContours(contours, similarityThreshold = 0.5) {
- const uniqueContours = [];
- for (let i = 0; i < contours.size(); i++) {
- const contour = contours.get(i);
- let isDuplicate = false;
- for (let j = 0; j < uniqueContours.length; j++) {
- if (areContoursSimilar(contour, uniqueContours[j], similarityThreshold)) {
- isDuplicate = true;
- break;
- }
- }
- if (!isDuplicate) {
- uniqueContours.push(contour);
- }
- }
- return uniqueContours;
- }
- // Function to interpolate points along a straight line
- function interpolatePoints(startPoint, endPoint, numPoints) {
- if (numPoints <= 2) return [startPoint, endPoint];
-
- const points = [];
- for (let i = 0; i < numPoints; i++) {
- const t = i / (numPoints - 1);
- const x = startPoint.x + t * (endPoint.x - startPoint.x);
- const y = startPoint.y + t * (endPoint.y - startPoint.y);
- points.push({ x, y });
- }
- return points;
- }
- // Function to calculate the distance between two points
- function distanceBetweenPoints(p1, p2) {
- return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
- }
- // Function to add interpolated points to a contour based on segment length
- function addInterpolatedPoints(points, epsilon) {
- if (points.length <= 1) return points;
-
- const result = [];
- for (let i = 0; i < points.length - 1; i++) {
- const startPoint = points[i];
- const endPoint = points[i + 1];
-
- // Calculate distance between points
- const distance = distanceBetweenPoints(startPoint, endPoint);
-
- // Determine how many points to add based on distance and epsilon
- // For longer segments and smaller epsilon values, we want more points
- // The smaller the epsilon, the more detailed the contour, so we add more points
- const pointsToAdd = Math.max(2, Math.ceil(distance / (epsilon * 5)));
-
- // Add interpolated points for this segment
- const interpolated = interpolatePoints(startPoint, endPoint, pointsToAdd);
-
- // Add all points except the last one (to avoid duplicates)
- if (i < points.length - 2) {
- result.push(...interpolated.slice(0, -1));
- } else {
- // For the last segment, include the end point
- result.push(...interpolated);
- }
- }
-
- return result;
- }
- function getOrderedContours(edgeImage, initialEpsilon, retrievalMode, maxPoints) {
- const contours = new cv.MatVector(), hierarchy = new cv.Mat();
- cv.findContours(edgeImage, contours, hierarchy, retrievalMode, cv.CHAIN_APPROX_SIMPLE);
- console.log("# Contours: ", contours.size());
- // Deduplicate contours
- const uniqueContours = deduplicateContours(contours);
- const maxIterations = 100; // Maximum iterations to avoid infinite loop
- let contourPoints = [];
- let totalPoints = 0;
- let epsilon = initialEpsilon;
- let iterations = 0;
- do {
- totalPoints = 0;
- contourPoints = [];
- for (let i = 0; i < uniqueContours.length; i++) {
- const contour = uniqueContours[i]; // Use [] to access array elements
- const simplified = new cv.Mat();
- cv.approxPolyDP(contour, simplified, epsilon, true);
-
- let points = [];
- for (let j = 0; j < simplified.rows; j++) {
- const point = simplified.intPtr(j);
- points.push({ x: point[0], y: point[1] });
- }
- simplified.delete();
-
- if (points.length > 0) { // Check for empty contours
- if (isNearlyClosed(contour)) { // Only close the contour if it's nearly closed
- points = closeContour(points);
- }
-
- // We no longer interpolate points here - we'll do it later only if needed
-
- if (isFullyClosed(points)) {
- // Move starting point to nearest the center
- points = reorderPointsForLoop(points);
- }
- contourPoints.push(points);
- totalPoints += points.length;
- }
- }
- if (totalPoints > maxPoints) {
- let pointsOver = totalPoints - maxPoints;
- epsilon = adjustEpsilon(epsilon, pointsOver);
- iterations++;
- }
- } while (totalPoints > maxPoints && iterations < maxIterations);
- if (totalPoints > maxPoints && iterations >= maxIterations) {
- let flattenedPoints = contourPoints.flat();
- contourPoints = [flattenedPoints.slice(0, maxPoints)]; // Take the first N points
- }
- if (contourPoints.length === 0) {
- console.error("No valid contours found.");
- return [];
- }
- // Calculate distances and find the best path
- const distances = calculateDistances(contourPoints);
- const path = tspNearestNeighbor(distances, contourPoints);
- const orderedContours = reorderContours(contourPoints, path);
- return orderedContours;
- }
- function getRandomColor() {
- const letters = '0123456789ABCDEF';
- let color = '#';
- for (let i = 0; i < 6; i++) {
- color += letters[Math.floor(Math.random() * 16)];
- }
- return color;
- }
- function resetCanvas(canvasId) {
- const canvas = document.getElementById(canvasId);
- const ctx = canvas.getContext('2d');
-
- // Store current dimensions
- const width = canvas.width;
- const height = canvas.height;
-
- // Clear the canvas
- ctx.clearRect(0, 0, width, height);
-
- // Reset transformation matrix
- ctx.setTransform(1, 0, 0, 1, 0, 0);
-
- // Ensure dimensions are maintained
- canvas.width = width;
- canvas.height = height;
- }
- function traceContours(orderedContours, isLoop = false, minimizeJumps = true) {
- let result = [];
- let pathsUsed = [...orderedContours];
-
- for (let i = 0; i < orderedContours.length - (isLoop ? 0 : 1); i++) {
- const currentContour = orderedContours[i];
-
- // If looping, add 1st contour again
- const nextContour = orderedContours[(i + 1) % orderedContours.length];
- const start = currentContour[currentContour.length - 1]; // End of the current contour
- const end = nextContour[0]; // Start of the next contour
- let path = [];
- if (minimizeJumps){
- // Find path between contours
- path = findPathWithMinimalJumpDistances(pathsUsed, start, end);
- }
-
- result.push(currentContour);
- if (path.length > 0) { // Add the path only if it has points
- result.push(path);
- pathsUsed.push(path); // Add the used path to the list of paths
- //console.log('Added Path: ', i, JSON.stringify(path));
- } else {
- //console.log('No Path Needed', i)
- }
- }
- // If not looping, add the last contour as it doesn't need a connecting path and wasn't added above
- if (!isLoop) { result.push(orderedContours[orderedContours.length - 1]); }
- return result;
- }
- function removeConsecutiveDuplicates(points) {
- return points.filter((point, index) => {
- if (index === 0) return true; // Keep the first point
- const prevPoint = points[index - 1];
- return !(point.x === prevPoint.x && point.y === prevPoint.y);
- });
- }
- function findPathWithMinimalJumpDistances(contours, start, end) {
- const graph = createGraphWithConnectionTypes(contours);
- const { startIdx, endIdx } = addStartEndToGraph(graph, start, end);
- const path = dijkstraWithMinimalJumps(graph, startIdx, endIdx);
- return path;
- }
- function generateDots(edgeImage) {
- // Reset the canvas before drawing the new image
- resetCanvas('dot-image');
- resetCanvas('connect-image');
-
- // Ensure canvases have the same dimensions
- const dotCanvas = document.getElementById('dot-image');
- const connectCanvas = document.getElementById('connect-image');
- const originalCanvas = document.getElementById('original-image');
- const edgeCanvas = document.getElementById('edge-image');
-
- if (originalImageElement) {
- // Set all canvases to the same dimensions
- dotCanvas.width = originalImageElement.naturalWidth;
- dotCanvas.height = originalImageElement.naturalHeight;
- connectCanvas.width = originalImageElement.naturalWidth;
- connectCanvas.height = originalImageElement.naturalHeight;
- }
- const epsilon = parseFloat(document.getElementById('epsilon-slider').value),
- contourMode = document.getElementById('contour-mode').value,
- isLoop = document.getElementById('is-loop').checked,
- minimizeJumps = document.getElementById('no-shortcuts').checked,
- outputFormat = parseInt(document.getElementById('output-type').value),
- maxPoints = parseInt(document.getElementById('dot-number').value);
- // useGaussianBlur = document.getElementById('gaussian-blur-toggle').checked,
- const retrievalMode = (contourMode == 'External') ? cv.RETR_EXTERNAL : cv.RETR_TREE;
-
- orderedContours = getOrderedContours(edgeImage, epsilon, retrievalMode, maxPoints);
- console.log('Ordered Contours: ', JSON.stringify(orderedContours));
- const tracedContours = traceContours(orderedContours, isLoop, minimizeJumps);
- console.log('Traced: ', JSON.stringify(tracedContours));
- // Only apply additional interpolation if .thr format (format 2) is selected
- let processedContours;
- if (outputFormat === 2) {
- // Apply interpolation for .thr format which needs more points for straight lines
- processedContours = tracedContours.map(contour =>
- addInterpolatedPoints(contour, epsilon)
- );
- } else {
- processedContours = tracedContours;
- }
- plotContours(processedContours);
- // Save for future plotting
- orderedContoursSave = processedContours;
- let orderedPoints = processedContours.flat();
- // Should always be the case for isLoop
- if (isFullyClosed(orderedPoints) || isLoop) {
- orderedPoints = reorderPointsForLoop(orderedPoints);
- }
- orderedPoints = removeConsecutiveDuplicates(orderedPoints);
- // For final output - if last point is same as first point, drop it.
- if (isFullyClosed(orderedPoints)) {
- orderedPoints = [...orderedPoints.slice(0,orderedPoints.length-1)];
- }
- const polarPoints = drawDots(orderedPoints);
- WriteCoords(polarPoints, outputFormat);
- drawConnections(polarPoints);
- document.getElementById('total-points').innerText = `(${orderedPoints.length} Points)`;
- }
- function WriteCoords(polarPoints, outputFormat = 0){
- let formattedPolarPoints = '';
- switch (outputFormat) {
- case 0: //Default
- // For Image2Sand.ino code, we normalize the theta values
- // We'll use modulo for this format
- formattedPolarPoints = polarPoints.map(p => {
- const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
- return `{${p.r.toFixed(0)},${normalizedTheta.toFixed(0)}}`;
- }).join(',');
- break;
- case 1: //Single Byte
- // For single byte format, we need to normalize the theta values
- // We'll use modulo for this format since it's just for Arduino code
- formattedPolarPoints = polarPoints.map(p => {
- const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
- return `{${Math.round(255 * p.r / 1000)},${Math.round(255 * normalizedTheta / 3600)}}`;
- }).join(',');
- break;
- case 2: //.thr
- // For .thr format, we keep the continuous theta values
- // Convert from tenths of degrees back to radians
- // Apply a 90° clockwise rotation by subtracting π/2 (900 in tenths of degrees) from theta
- formattedPolarPoints = polarPoints.map(p => {
- // Subtract 900 (90 degrees) to rotate clockwise
- const rotatedTheta = p.theta - 900;
- return `${(-rotatedTheta * Math.PI / 1800).toFixed(5)} ${(p.r / 1000).toFixed(5)}`;
- }).join("\n");
- break;
- case 3: // whitespace (might cause problems as it outputs a space)
- // For whitespace format, we need to normalize the theta values
- // We'll use modulo for this format
- formattedPolarPoints = polarPoints.map(p => {
- const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
- 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")}`;
- }).join("\n");
- break;
- default:
- break;
- }
-
- document.getElementById('polar-coordinates-textarea').value = formattedPolarPoints;
- document.getElementById('simple-coords').textContent = formattedPolarPoints;
- document.getElementById('simple-coords-title').style = 'visibility: hidden';
- }
- function reorderPointsForLoop(points, startNear = calculateCentroid(points)) {
- let minDist = Infinity;
- let startIndex = 0;
- // Find the point nearest to the centroid
- points.forEach((point, index) => {
- const dist = Math.hypot(point.x - startNear.x, point.y - startNear.y);
- if (dist < minDist) {
- minDist = dist;
- startIndex = index;
- }
- });
- // Reorder points to start from the point nearest to the centroid
- return removeConsecutiveDuplicates([...points.slice(startIndex), ...points.slice(0, startIndex+1)]);
-
- }
- function drawDots(points) {
- const canvas = document.getElementById('dot-image'), ctx = canvas.getContext('2d');
-
- // Set canvas dimensions to match the original image
- if (originalImageElement) {
- canvas.width = originalImageElement.naturalWidth;
- canvas.height = originalImageElement.naturalHeight;
- }
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- const width = canvas.width, height = canvas.height;
- const scaleX = width / originalImageElement.width;
- const scaleY = height / originalImageElement.height;
- const scale = Math.min(scaleX, scaleY);
- points = points.map(p => ({ x: (p.x) * scale, y: (p.y) * scale }));
- points.forEach(point => {
- ctx.beginPath();
- ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
- ctx.fill();
- });
- const formattedPoints = points.map(p => `{${p.x.toFixed(2)}, ${p.y.toFixed(2)}}`).join(',\n');
- // Calculate polar coordinates
- const center = findMaximalCenter(points);
- points = points.map(p => ({ x: p.x - center.centerX, y: p.y - center.centerY }));
-
- // Calculate initial angles for all points
- let polarPoints = points.map(p => {
- const r = Math.sqrt(p.x * p.x + p.y * p.y);
- // Get the basic angle in radians
- let theta = Math.atan2(p.y, p.x);
-
- // Adjust theta to align 0 degrees to the right and 90 degrees up by flipping the y-axis
- theta = -theta;
-
- return {
- r: r * (1000 / Math.max(...points.map(p => Math.sqrt(p.x * p.x + p.y * p.y)))),
- theta: theta, // Store in radians initially
- x: p.x,
- y: p.y
- };
- });
-
- // Process points to create continuous theta values
- for (let i = 1; i < polarPoints.length; i++) {
- const prev = polarPoints[i-1];
- const curr = polarPoints[i];
-
- // Calculate the difference between current and previous theta
- let diff = curr.theta - prev.theta;
-
- // If the difference is greater than π, it means we've wrapped around counterclockwise
- // Adjust by subtracting 2π
- if (diff > Math.PI) {
- curr.theta -= 2 * Math.PI;
- }
- // If the difference is less than -π, it means we've wrapped around clockwise
- // Adjust by adding 2π
- else if (diff < -Math.PI) {
- curr.theta += 2 * Math.PI;
- }
- }
-
- // Convert to degrees * 10 for the final format
- polarPoints = polarPoints.map(p => ({
- r: p.r,
- theta: p.theta * (1800 / Math.PI) // Convert radians to tenths of degrees
- }));
- return polarPoints;
- }
- function plotContours(orderedContours) {
- const canvas = document.getElementById('plotcontours');
- const ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- orderedContours.forEach((contour, index) => {
- const baseColor = getRandomColor();
- const [r, g, b] = hexToRgb(baseColor);
- const length = contour.length;
- contour.forEach((point, i) => {
- if (i === 0) {
- ctx.beginPath();
- ctx.moveTo(point.x, point.y);
- } else {
- ctx.lineTo(point.x, point.y);
- // Calculate color fade
- const ratio = i / length;
- const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
- ctx.strokeStyle = fadedColor;
- ctx.lineWidth = 2;
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(point.x, point.y);
- }
- });
- // Mark the start and end points
- ctx.fillStyle = baseColor;
- ctx.font = '12px Arial';
- // Start point
- ctx.fillText(`S${index + 1}`, contour[0].x, contour[0].y);
- ctx.beginPath();
- ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
- ctx.fill();
- // End point
- ctx.fillText(`E${index + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
- ctx.beginPath();
- ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
- ctx.fill();
- // Label the contour with its number
- const midPoint = contour[Math.floor(contour.length / 2)];
- ctx.fillText(`${index + 1}`, midPoint.x, midPoint.y);
- });
- }
- function hexToRgb(hex) {
- const bigint = parseInt(hex.slice(1), 16);
- return [
- (bigint >> 16) & 255,
- (bigint >> 8) & 255,
- bigint & 255
- ];
- }
- function drawConnections(polarPoints) {
- const canvas = document.getElementById('connect-image'), ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- const width = canvas.width, height = canvas.height;
- // Reset transformation matrix
- ctx.setTransform(1, 0, 0, 1, 0, 0);
- // Translate the context to the center of the canvas
- ctx.translate(width / 2, height / 2);
- // Scale the points based on the size of the original image
- const scaleX = width / 2000; // Since the circle radius is 1000
- const scaleY = height / 2000;
- const scale = Math.min(scaleX, scaleY);
- // Draw the outline circle
- ctx.beginPath();
- ctx.arc(0, 0, 1000 * scale, 0, 2 * Math.PI);
- ctx.strokeStyle = 'black';
- ctx.stroke();
- // Draw the connections based on polar coordinates
- for (let i = 0; i < polarPoints.length - 1; i++) {
- // Calculate the color for each segment
- const t = i / (polarPoints.length - 1);
- const color = `hsl(${t * 270}, 100%, 50%)`; // 270 degrees covers red to violet
- ctx.strokeStyle = color;
- const p1 = polarPoints[i];
- const p2 = polarPoints[i + 1];
- // Convert from tenths of degrees to radians for visualization
- const theta1 = p1.theta * Math.PI / 1800;
- const theta2 = p2.theta * Math.PI / 1800;
- // Adjust y-coordinate calculation to invert the y-axis
- const x1 = p1.r * Math.cos(theta1) * scale;
- const y1 = -p1.r * Math.sin(theta1) * scale;
- const x2 = p2.r * Math.cos(theta2) * scale;
- const y2 = -p2.r * Math.sin(theta2) * scale;
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- }
- }
- function convertImage() {
- originalImageElement && processImage(originalImageElement);
- }
- // Function to get URL parameters
- function getUrlParams() {
- const params = new URLSearchParams(window.location.search);
- return {
- apikey: params.get('apikey'),
- prompt: params.get('prompt'),
- run: params.get('run')
- };
- }
- // Function to fill inputs from URL parameters
- function fillInputsFromParams(params) {
- if (params.apikey) {
- document.getElementById('api-key').value = params.apikey;
- }
- if (params.prompt) {
- document.getElementById('prompt').value = params.prompt;
- }
- }
- function setDefaultsForAutoGenerate() {
- document.getElementById('epsilon-slider').value = 0.5;
- document.getElementById('dot-number').value = 300;
- document.getElementById('no-shortcuts').checked = true;
- document.getElementById('is-loop').checked = true;
- document.getElementById('contour-mode').value = 'Tree';
- hiddenResponse();
- }
- function hiddenResponse() {
- document.getElementById('master-container').style = 'display: none;';
- document.getElementById('simple-container').style = 'visibility: visible';
- }
- document.addEventListener('DOMContentLoaded', function() {
- const fileInput = document.getElementById('file-input');
- const fileButton = document.getElementById('file-button');
- const fileNameDisplay = document.getElementById('file-name');
- const generateButton = document.getElementById('generate-button');
- const epsilonSlider = document.getElementById('epsilon-slider');
- const epsilonValueDisplay = document.getElementById('epsilon-value-display');
- const dotNumberInput = document.getElementById('dot-number');
- const contourModeSelect = document.getElementById('contour-mode');
- //const gaussianBlurToggle = document.getElementById('gaussian-blur-toggle');
- document.getElementById('plotButton').addEventListener('click', plotNextContour);
- generateButton.addEventListener('click', convertImage);
- fileButton.addEventListener('click', function() {
- fileInput.click();
- });
- fileInput.addEventListener('change', function(event) {
- const file = event.target.files[0];
- if (file) {
- fileNameDisplay.textContent = file.name;
- if (typeof openImageConverter === 'function') {
- openImageConverter(file);
- } else {
- handleImageUpload(event);
- }
- }
- });
- epsilonSlider.addEventListener('input', function() {
- epsilonValueDisplay.textContent = epsilonSlider.value;
- });
- window.showTab = function(tabName) {
- const tabContents = document.querySelectorAll('.tab-content');
- tabContents.forEach(content => { content.style.display = 'none'; });
- document.getElementById(tabName).style.display = 'block';
- };
- });
- // Initialize the page with URL parameters if present
- document.addEventListener('DOMContentLoaded', (event) => {
- const { apikey, prompt, run } = getUrlParams();
- // Fill inputs with URL parameters if they exist
- fillInputsFromParams({ apikey, prompt });
- if (apikey) {
- document.getElementById('api-key-group').style.display = 'none';
- }
- // Generate image if all parameters are present
- if (apikey && prompt && run) {
- setDefaultsForAutoGenerate();
- generateImage(apikey, prompt, run);
- convertImage();
- }
- // Add event listener to the button inside the DOMContentLoaded event
- const genImageButton = document.getElementById('gen-image-button');
- if (genImageButton) {
- genImageButton.addEventListener('click', () => {
- const apiKeyElement = document.getElementById('api-key');
- const promptElement = document.getElementById('prompt');
- const googlyEyes = document.getElementById('googly-eyes');
-
- // Add null checks
- const apiKey = apiKeyElement?.value || '';
- const promptValue = promptElement?.value || '';
- const googlyEyesChecked = googlyEyes?.checked || false;
-
- const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
- generateImage(apiKey, prompt, false);
- });
- }
-
- });
|