image2sand-init.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. // Invalidate pattern files cache to include new file
  144. invalidatePatternFilesCache();
  145. // Close the converter dialog
  146. closeImageConverter();
  147. // clear the file input
  148. if (fileInput) {
  149. fileInput.value = '';
  150. }
  151. // Refresh the file list
  152. await loadThetaRhoFiles();
  153. // Select the newly created file
  154. const fileList = document.getElementById('theta_rho_files');
  155. if (fileList) {
  156. const listItems = fileList.querySelectorAll('li');
  157. for (const item of listItems) {
  158. if (item.textContent === finalFileName) {
  159. selectFile(finalFileName, item);
  160. break;
  161. }
  162. }
  163. }
  164. } else {
  165. logMessage(`Failed to save pattern: ${result.error || 'Unknown error'}`, LOG_TYPE.ERROR);
  166. }
  167. } catch (error) {
  168. logMessage(`Error saving pattern: ${error.message}`, LOG_TYPE.ERROR);
  169. } finally {
  170. // Hide processing indicator
  171. document.getElementById('processing-status').classList.remove('visible');
  172. }
  173. }
  174. /**
  175. * Clear a canvas
  176. * @param {string} canvasId - The ID of the canvas element to clear
  177. */
  178. function clearCanvas(canvasId) {
  179. const canvas = document.getElementById(canvasId);
  180. const ctx = canvas.getContext('2d');
  181. ctx.clearRect(0, 0, canvas.width, canvas.height);
  182. }
  183. /**
  184. * Close the image converter dialog
  185. */
  186. function closeImageConverter() {
  187. const overlay = document.getElementById('image-converter');
  188. overlay.classList.remove('visible');
  189. overlay.classList.add('hidden');
  190. // Clear the canvases
  191. clearCanvas('original-image');
  192. clearCanvas('edge-image');
  193. clearCanvas('dot-image');
  194. clearCanvas('connect-image');
  195. // Reset variables
  196. originalImage = null;
  197. fileName = '';
  198. convertedCoordinates = null;
  199. currentImageData = null;
  200. // Disable the save button
  201. //document.getElementById('save-pattern-button').disabled = true;
  202. }
  203. async function generateOpenAIImage(apiKey, prompt) {
  204. if (isGeneratingImage) {
  205. logMessage("Image is still generating - please don't press the button.", LOG_TYPE.INFO);
  206. } else {
  207. isGeneratingImage = true;
  208. clearCanvas('original-image');
  209. clearCanvas('edge-image');
  210. clearCanvas('dot-image');
  211. clearCanvas('connect-image');
  212. document.getElementById('gen-image-button').disabled = true;
  213. // Show processing indicator
  214. const processingIndicator = document.getElementById('processing-status');
  215. const processingMessage = document.getElementById('processing-message');
  216. if (processingMessage) {
  217. processingMessage.textContent = `Generating image...`;
  218. }
  219. processingIndicator.classList.add('visible');
  220. try {
  221. 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.`;
  222. const response = await fetch('https://api.openai.com/v1/images/generations', {
  223. method: 'POST',
  224. headers: {
  225. 'Authorization': `Bearer ${apiKey}`,
  226. 'Content-Type': 'application/json'
  227. },
  228. body: JSON.stringify({
  229. model: 'dall-e-3',
  230. prompt: fullPrompt,
  231. size: '1024x1024',
  232. quality: 'standard',
  233. response_format: 'b64_json', // Specify base64 encoding
  234. n: 1
  235. })
  236. });
  237. const data = await response.json();
  238. //const imageUrl = data.data[0].url;
  239. if ('error' in data) {
  240. throw new Error(data.error.message);
  241. }
  242. const imageData = data.data[0].b64_json;
  243. //console.log("Image Data: ", imageData);
  244. const imgElement = new Image();
  245. imgElement.onload = function() {
  246. // Draw the image on the canvas
  247. originalImage = imgElement;
  248. drawAndPrepImage(imgElement);
  249. // Convert the image with default settings
  250. convertImage();
  251. };
  252. imgElement.src = `data:image/png;base64,${imageData}`;
  253. //console.log(`Image generated successfully`);
  254. logMessage('Image generated successfully', LOG_TYPE.SUCCESS);
  255. } catch (error) {
  256. //console.error('Image generation error:', error);
  257. logMessage('Image generation error: ' + error, LOG_TYPE.ERROR);
  258. }
  259. isGeneratingImage = false;
  260. document.getElementById('gen-image-button').disabled = false;
  261. document.getElementById('processing-status').classList.remove('visible');
  262. }
  263. }
  264. function regeneratePattern() {
  265. const generateButton = document.getElementById('generate-button');
  266. // Disable button & show existing loader
  267. generateButton.disabled = true;
  268. generateButton.classList.add('loading');
  269. // Wrap convertImage() in a Promise
  270. new Promise((resolve, reject) => {
  271. try {
  272. convertImage();
  273. setTimeout(resolve, 1000);
  274. } catch (error) {
  275. reject(error);
  276. }
  277. })
  278. .then(() => {
  279. logMessage("Pattern regenerated successfully.", LOG_TYPE.SUCCESS);
  280. })
  281. .catch(error => {
  282. logMessage("Error regenerating pattern: " + error.message, LOG_TYPE.ERROR);
  283. })
  284. .finally(() => {
  285. // Re-enable button & hide loader
  286. generateButton.disabled = false;
  287. generateButton.classList.remove('loading');
  288. });
  289. }
  290. // Override the uploadThetaRho function to handle image files
  291. const originalUploadThetaRho = window.uploadThetaRho;
  292. window.uploadThetaRho = async function() {
  293. const fileInput = document.getElementById('upload_file');
  294. const file = fileInput.files[0];
  295. if (!file) {
  296. logMessage('No file selected for upload.', LOG_TYPE.ERROR);
  297. return;
  298. }
  299. // Check if the file is an image
  300. if (file.type.startsWith('image/')) {
  301. // Handle image files with the converter
  302. openImageConverter(file);
  303. return;
  304. }
  305. // For non-image files, use the original function
  306. await originalUploadThetaRho();
  307. };
  308. // Remove existing event listener and add a new one
  309. document.getElementById('gen-image-button')?.addEventListener('click', function() {
  310. let apiKey = document.getElementById('api-key')?.value || '';
  311. const googlyEyes = document.getElementById('googly-eyes');
  312. const promptElement = document.getElementById('prompt');
  313. // Add null checks
  314. const promptValue = promptElement?.value || '';
  315. const googlyEyesChecked = googlyEyes?.checked || false;
  316. const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
  317. // Show the converter dialog
  318. const overlay = document.getElementById('image-converter');
  319. if (overlay) {
  320. overlay.classList.remove('hidden');
  321. // Initialize the UI elements
  322. initializeUI();
  323. generateOpenAIImage(apiKey, prompt);
  324. }
  325. });
  326. async function loadThetaRhoFiles() {
  327. const fileList = document.getElementById('theta_rho_files');
  328. if (!fileList) return;
  329. // Clear the list
  330. fileList.innerHTML = '';
  331. try {
  332. // Fetch the file list from your backend
  333. const files = await getCachedPatternFiles();
  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. }