image2sand-init.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /**
  2. * Image2Sand (https://github.com/orionwc/Image2Sand) Initialization
  3. *
  4. * This script handles the initialization of the Image2Sand converter.
  5. *
  6. */
  7. // Log types for message styling
  8. const LOG_TYPE = {
  9. INFO: 'info',
  10. SUCCESS: 'success',
  11. WARNING: 'warning',
  12. ERROR: 'error'
  13. };
  14. /**
  15. * Display a message to the user
  16. * @param {string} message - The message to display
  17. * @param {string} type - The type of message (info, success, warning, error)
  18. */
  19. function logMessage(message, type = LOG_TYPE.INFO) {
  20. console.log(`[${type.toUpperCase()}] ${message}`);
  21. // Create message element
  22. const messageElement = document.createElement('div');
  23. messageElement.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 transition-opacity duration-300 ${
  24. type === LOG_TYPE.ERROR ? 'bg-red-100 text-red-700' :
  25. type === LOG_TYPE.SUCCESS ? 'bg-green-100 text-green-700' :
  26. type === LOG_TYPE.WARNING ? 'bg-yellow-100 text-yellow-700' :
  27. 'bg-blue-100 text-blue-700'
  28. }`;
  29. messageElement.textContent = message;
  30. // Add to document
  31. document.body.appendChild(messageElement);
  32. // Remove after 5 seconds
  33. setTimeout(() => {
  34. messageElement.style.opacity = '0';
  35. setTimeout(() => messageElement.remove(), 300);
  36. }, 5000);
  37. }
  38. // Global variables for the image converter
  39. let originalImage = null;
  40. let fileName = '';
  41. let convertedCoordinates = null;
  42. let currentImageData = null;
  43. /**
  44. * Open the image converter dialog with the selected image
  45. * @param {File} file - The image file to convert
  46. */
  47. function openImageConverter(file) {
  48. if (!file) {
  49. logMessage('No file selected for conversion.', LOG_TYPE.ERROR);
  50. return;
  51. }
  52. // Check if the file is an image
  53. if (!file.type.startsWith('image/')) {
  54. // If not an image, let the original uploadThetaRho handle it
  55. return;
  56. }
  57. // Store the original file name globally for later use
  58. window._originalUploadedFileName = file.name;
  59. // Remove extension for display/use
  60. fileName = file.name.replace(/\.[^/.]+$/, '');
  61. // Create an image element to load the file
  62. const img = new Image();
  63. img.onload = function() {
  64. // Draw the image on the canvas
  65. originalImage = img;
  66. drawAndPrepImage(img);
  67. // Show the converter dialog
  68. const overlay = document.getElementById('image-converter');
  69. overlay.classList.remove('hidden');
  70. overlay.classList.add('visible');
  71. // Initialize the UI elements
  72. initializeUI();
  73. // Convert the image with default settings
  74. convertImage();
  75. };
  76. img.onerror = function() {
  77. logMessage(`Failed to load image: ${file.name}`, LOG_TYPE.ERROR);
  78. };
  79. // Load the image from the file
  80. img.src = URL.createObjectURL(file);
  81. }
  82. /**
  83. * Initialize UI elements for the image converter
  84. */
  85. function initializeUI() {
  86. // Set up event listeners for UI controls
  87. const epsilonSlider = document.getElementById('epsilon-slider');
  88. const epsilonValueDisplay = document.getElementById('epsilon-value-display');
  89. epsilonSlider.addEventListener('input', function() {
  90. epsilonValueDisplay.textContent = this.value;
  91. });
  92. // Set up event listeners for other controls
  93. //document.getElementById('epsilon-slider').addEventListener('change', convertImage);
  94. //document.getElementById('dot-number').addEventListener('change', convertImage);
  95. //document.getElementById('contour-mode').addEventListener('change', convertImage);
  96. //document.getElementById('is-loop').addEventListener('change', convertImage);
  97. //document.getElementById('no-shortcuts').addEventListener('change', convertImage);
  98. }
  99. /**
  100. * Save the converted pattern as a .thr file
  101. */
  102. async function saveConvertedPattern() {
  103. convertedCoordinates = document.getElementById('polar-coordinates-textarea').value;
  104. if (!convertedCoordinates) {
  105. logMessage('No converted coordinates to save.', LOG_TYPE.ERROR);
  106. return;
  107. }
  108. try {
  109. // Always use the original uploaded file name if available
  110. let originalName = window._originalUploadedFileName;
  111. if (!originalName || typeof originalName !== 'string') {
  112. originalName = fileName || 'pattern';
  113. }
  114. // Remove extension
  115. let baseName = originalName.replace(/\.[^/.]+$/, '');
  116. // Replace spaces with underscores
  117. baseName = baseName.replace(/\s+/g, '_').trim();
  118. // Fallback if baseName is empty
  119. if (!baseName) baseName = 'pattern';
  120. let thrFileName = baseName + '.thr';
  121. // Create a Blob with the coordinates
  122. const blob = new Blob([convertedCoordinates], { type: 'text/plain' });
  123. // Create a FormData object
  124. const formData = new FormData();
  125. formData.append('file', new File([blob], thrFileName, { type: 'text/plain' }));
  126. // Show processing indicator
  127. const processingIndicator = document.getElementById('processing-status');
  128. const processingMessage = document.getElementById('processing-message');
  129. if (processingMessage) {
  130. processingMessage.textContent = `Saving pattern as ${thrFileName}...`;
  131. }
  132. processingIndicator.classList.add('visible');
  133. // Upload the file
  134. const response = await fetch('/upload_theta_rho', {
  135. method: 'POST',
  136. body: formData
  137. });
  138. const result = await response.json();
  139. if (result.success) {
  140. const fileInput = document.getElementById('upload_file');
  141. const finalFileName = 'custom_patterns/' + thrFileName;
  142. logMessage(`Image converted and saved as ${finalFileName}`, LOG_TYPE.SUCCESS);
  143. // Close the converter dialog
  144. closeImageConverter();
  145. // clear the file input
  146. if (fileInput) {
  147. fileInput.value = '';
  148. }
  149. // Refresh the file list
  150. await loadThetaRhoFiles();
  151. // Select the newly created file
  152. const fileList = document.getElementById('theta_rho_files');
  153. if (fileList) {
  154. const listItems = fileList.querySelectorAll('li');
  155. for (const item of listItems) {
  156. if (item.textContent === finalFileName) {
  157. selectFile(finalFileName, item);
  158. break;
  159. }
  160. }
  161. }
  162. } else {
  163. logMessage(`Failed to save pattern: ${result.error || 'Unknown error'}`, LOG_TYPE.ERROR);
  164. }
  165. } catch (error) {
  166. logMessage(`Error saving pattern: ${error.message}`, LOG_TYPE.ERROR);
  167. } finally {
  168. // Hide processing indicator
  169. document.getElementById('processing-status').classList.remove('visible');
  170. }
  171. }
  172. /**
  173. * Clear a canvas
  174. * @param {string} canvasId - The ID of the canvas element to clear
  175. */
  176. function clearCanvas(canvasId) {
  177. const canvas = document.getElementById(canvasId);
  178. const ctx = canvas.getContext('2d');
  179. ctx.clearRect(0, 0, canvas.width, canvas.height);
  180. }
  181. /**
  182. * Close the image converter dialog
  183. */
  184. function closeImageConverter() {
  185. const overlay = document.getElementById('image-converter');
  186. overlay.classList.remove('visible');
  187. overlay.classList.add('hidden');
  188. // Clear the canvases
  189. clearCanvas('original-image');
  190. clearCanvas('edge-image');
  191. clearCanvas('dot-image');
  192. clearCanvas('connect-image');
  193. // Reset variables
  194. originalImage = null;
  195. fileName = '';
  196. convertedCoordinates = null;
  197. currentImageData = null;
  198. // Disable the save button
  199. //document.getElementById('save-pattern-button').disabled = true;
  200. }
  201. async function generateOpenAIImage(apiKey, prompt) {
  202. if (isGeneratingImage) {
  203. logMessage("Image is still generating - please don't press the button.", LOG_TYPE.INFO);
  204. } else {
  205. isGeneratingImage = true;
  206. clearCanvas('original-image');
  207. clearCanvas('edge-image');
  208. clearCanvas('dot-image');
  209. clearCanvas('connect-image');
  210. document.getElementById('gen-image-button').disabled = true;
  211. // Show processing indicator
  212. const processingIndicator = document.getElementById('processing-status');
  213. const processingMessage = document.getElementById('processing-message');
  214. if (processingMessage) {
  215. processingMessage.textContent = `Generating image...`;
  216. }
  217. processingIndicator.classList.add('visible');
  218. try {
  219. 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.`;
  220. const response = await fetch('https://api.openai.com/v1/images/generations', {
  221. method: 'POST',
  222. headers: {
  223. 'Authorization': `Bearer ${apiKey}`,
  224. 'Content-Type': 'application/json'
  225. },
  226. body: JSON.stringify({
  227. model: 'dall-e-3',
  228. prompt: fullPrompt,
  229. size: '1024x1024',
  230. quality: 'standard',
  231. response_format: 'b64_json', // Specify base64 encoding
  232. n: 1
  233. })
  234. });
  235. const data = await response.json();
  236. //const imageUrl = data.data[0].url;
  237. if ('error' in data) {
  238. throw new Error(data.error.message);
  239. }
  240. const imageData = data.data[0].b64_json;
  241. //console.log("Image Data: ", imageData);
  242. const imgElement = new Image();
  243. imgElement.onload = function() {
  244. // Draw the image on the canvas
  245. originalImage = imgElement;
  246. drawAndPrepImage(imgElement);
  247. // Convert the image with default settings
  248. convertImage();
  249. };
  250. imgElement.src = `data:image/png;base64,${imageData}`;
  251. //console.log(`Image generated successfully`);
  252. logMessage('Image generated successfully', LOG_TYPE.SUCCESS);
  253. } catch (error) {
  254. //console.error('Image generation error:', error);
  255. logMessage('Image generation error: ' + error, LOG_TYPE.ERROR);
  256. }
  257. isGeneratingImage = false;
  258. document.getElementById('gen-image-button').disabled = false;
  259. document.getElementById('processing-status').classList.remove('visible');
  260. }
  261. }
  262. function regeneratePattern() {
  263. const generateButton = document.getElementById('generate-button');
  264. // Disable button & show existing loader
  265. generateButton.disabled = true;
  266. generateButton.classList.add('loading');
  267. // Wrap convertImage() in a Promise
  268. new Promise((resolve, reject) => {
  269. try {
  270. convertImage();
  271. setTimeout(resolve, 1000);
  272. } catch (error) {
  273. reject(error);
  274. }
  275. })
  276. .then(() => {
  277. logMessage("Pattern regenerated successfully.", LOG_TYPE.SUCCESS);
  278. })
  279. .catch(error => {
  280. logMessage("Error regenerating pattern: " + error.message, LOG_TYPE.ERROR);
  281. })
  282. .finally(() => {
  283. // Re-enable button & hide loader
  284. generateButton.disabled = false;
  285. generateButton.classList.remove('loading');
  286. });
  287. }
  288. // Override the uploadThetaRho function to handle image files
  289. const originalUploadThetaRho = window.uploadThetaRho;
  290. window.uploadThetaRho = async function() {
  291. const fileInput = document.getElementById('upload_file');
  292. const file = fileInput.files[0];
  293. if (!file) {
  294. logMessage('No file selected for upload.', LOG_TYPE.ERROR);
  295. return;
  296. }
  297. // Check if the file is an image
  298. if (file.type.startsWith('image/')) {
  299. // Handle image files with the converter
  300. openImageConverter(file);
  301. return;
  302. }
  303. // For non-image files, use the original function
  304. await originalUploadThetaRho();
  305. };
  306. // Remove existing event listener and add a new one
  307. document.getElementById('gen-image-button')?.addEventListener('click', function() {
  308. let apiKey = document.getElementById('api-key')?.value || '';
  309. const googlyEyes = document.getElementById('googly-eyes');
  310. const promptElement = document.getElementById('prompt');
  311. // Add null checks
  312. const promptValue = promptElement?.value || '';
  313. const googlyEyesChecked = googlyEyes?.checked || false;
  314. const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
  315. // Show the converter dialog
  316. const overlay = document.getElementById('image-converter');
  317. if (overlay) {
  318. overlay.classList.remove('hidden');
  319. // Initialize the UI elements
  320. initializeUI();
  321. generateOpenAIImage(apiKey, prompt);
  322. }
  323. });
  324. async function loadThetaRhoFiles() {
  325. const fileList = document.getElementById('theta_rho_files');
  326. if (!fileList) return;
  327. // Clear the list
  328. fileList.innerHTML = '';
  329. try {
  330. // Fetch the file list from your backend
  331. const response = await fetch('/list_theta_rho_files');
  332. if (!response.ok) throw new Error('Failed to fetch file list');
  333. const files = await response.json();
  334. // Populate the list
  335. files.forEach(filename => {
  336. const li = document.createElement('li');
  337. li.textContent = filename;
  338. fileList.appendChild(li);
  339. });
  340. } catch (error) {
  341. const li = document.createElement('li');
  342. li.textContent = 'Error loading file list';
  343. fileList.appendChild(li);
  344. }
  345. }