1
0

image2sand.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. {% extends "base.html" %}
  2. {% block title %}Image2Sand - Kinetic Sand Table{% endblock %}
  3. {% block additional_styles %}
  4. <style>
  5. @keyframes spin {
  6. 0% { transform: rotate(0deg); }
  7. 100% { transform: rotate(360deg); }
  8. }
  9. .spinner {
  10. animation: spin 1s linear infinite;
  11. }
  12. .custom-checkbox::after {
  13. content: "✓";
  14. color: white;
  15. font-size: 0.75rem;
  16. font-weight: bold;
  17. }
  18. /* Override base template's dark mode rules for image2sand page in light mode */
  19. html:not(.dark) .bg-white {
  20. background-color: #ffffff !important;
  21. }
  22. html:not(.dark) .bg-gray-50 {
  23. background-color: #f9fafb !important;
  24. }
  25. html:not(.dark) .bg-gray-100 {
  26. background-color: #f3f4f6 !important;
  27. }
  28. html:not(.dark) .bg-gray-200 {
  29. background-color: #e5e7eb !important;
  30. }
  31. html:not(.dark) .text-gray-900 {
  32. color: #111827 !important;
  33. }
  34. html:not(.dark) .text-gray-800 {
  35. color: #1f2937 !important;
  36. }
  37. html:not(.dark) .text-gray-700 {
  38. color: #374151 !important;
  39. }
  40. html:not(.dark) .text-gray-600 {
  41. color: #4b5563 !important;
  42. }
  43. html:not(.dark) .text-gray-500 {
  44. color: #6b7280 !important;
  45. }
  46. html:not(.dark) .text-gray-400 {
  47. color: #9ca3af !important;
  48. }
  49. html:not(.dark) .text-gray-300 {
  50. color: #d1d5db !important;
  51. }
  52. html:not(.dark) .border-gray-200 {
  53. border-color: #e5e7eb !important;
  54. }
  55. html:not(.dark) .border-gray-300 {
  56. border-color: #d1d5db !important;
  57. }
  58. /* Fix hover states in light mode */
  59. html:not(.dark) .hover\:bg-gray-50:hover {
  60. background-color: #f9fafb !important;
  61. }
  62. html:not(.dark) .hover\:bg-gray-100:hover {
  63. background-color: #f3f4f6 !important;
  64. }
  65. html:not(.dark) .hover\:bg-gray-200:hover {
  66. background-color: #e5e7eb !important;
  67. }
  68. html:not(.dark) .hover\:text-gray-700:hover {
  69. color: #374151 !important;
  70. }
  71. html:not(.dark) .hover\:text-gray-800:hover {
  72. color: #1f2937 !important;
  73. }
  74. html:not(.dark) .hover\:text-gray-900:hover {
  75. color: #111827 !important;
  76. }
  77. /* Fix focus states */
  78. html:not(.dark) .focus\:ring-blue-500:focus {
  79. --tw-ring-color: #3b82f6 !important;
  80. }
  81. html:not(.dark) .focus\:border-blue-500:focus {
  82. border-color: #3b82f6 !important;
  83. }
  84. </style>
  85. {% endblock %}
  86. {% block scripts %}
  87. <!-- OpenCV.js for image processing -->
  88. <script async src="/static/js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
  89. <!-- FontAwesome for icons -->
  90. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
  91. <!-- Image2Sand processing scripts -->
  92. <script src="/static/js/image2sand-init.js"></script>
  93. <script src="/static/js/image2sand.js"></script>
  94. {% endblock %}
  95. {% block content %}
  96. <div class="min-h-screen p-4 sm:p-8 pt-2 pb-[75px]">
  97. <div class="max-w-4xl mx-auto bg-white dark:bg-gray-900 rounded-xl sm:rounded-2xl overflow-hidden shadow-lg border border-gray-200 dark:border-gray-700">
  98. <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
  99. <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 m-0">Convert Image to Pattern</h1>
  100. <p class="mt-2 text-gray-600 dark:text-gray-400 text-base sm:text-md">Transform your images into kinetic sand table patterns with advanced edge detection and path optimization. Works well with images with less colors, high contrast, and have clearly defined & enclosed areas.</p>
  101. </div>
  102. <!-- Image Converter Section - Required structure for JS -->
  103. <section id="image-converter" class="visible">
  104. <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-8 p-4 sm:p-8 min-h-[400px] sm:min-h-[600px] bg-white dark:bg-gray-900 image-converter-content">
  105. <!-- Processing Indicator -->
  106. <div id="processing-status" class="hidden fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-4 z-10 bg-white dark:bg-gray-800 p-6 sm:p-8 rounded-lg shadow-2xl border border-gray-200 dark:border-gray-600 processing-indicator">
  107. <div class="spinner w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full"></div>
  108. <span id="processing-message" class="text-gray-700 dark:text-gray-300">Processing image...</span>
  109. </div>
  110. <!-- Image Steps (Left Column) -->
  111. <div class="lg:col-span-2 flex flex-col bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden image-converter-steps">
  112. <!-- Tab Content -->
  113. <div class="flex-1 relative">
  114. <div class="tab-content flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="original-tab" data-group="image-converter">
  115. <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
  116. <i class="fa-solid fa-image text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
  117. <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Upload an Image</h3>
  118. <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Select an image file to begin the conversion process</p>
  119. <button class="bg-blue-600 hover:bg-blue-700 text-white px-4 sm:px-6 py-2 sm:py-3 rounded-lg font-semibold transition-colors flex items-center gap-2 shadow-sm text-sm sm:text-base" onclick="document.getElementById('file-input').click()">
  120. <i class="fa-solid fa-upload"></i>
  121. Choose Image
  122. </button>
  123. </div>
  124. <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
  125. <canvas id="original-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
  126. </div>
  127. </div>
  128. <div class="tab-content hidden flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="edge-tab" data-group="image-converter">
  129. <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
  130. <i class="fa-solid fa-bezier-curve text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
  131. <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Edge Detection</h3>
  132. <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Processed edges will appear here after uploading an image</p>
  133. </div>
  134. <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
  135. <canvas id="edge-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
  136. </div>
  137. </div>
  138. <div class="tab-content hidden flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="dot-tab" data-group="image-converter">
  139. <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
  140. <i class="fa-solid fa-circle-dot text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
  141. <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Identified Points</h3>
  142. <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Key points for the sand table path will be shown here</p>
  143. </div>
  144. <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
  145. <canvas id="dot-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
  146. </div>
  147. </div>
  148. </div>
  149. <!-- Tab Navigation -->
  150. <div class="flex border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 tab-container">
  151. <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-white bg-blue-600 border-r border-gray-200 dark:border-gray-700 hover:bg-blue-700 transition-colors" onclick="switchTab('original', 'image-converter')" id="nav-original" data-group="image-converter">Original Image</button>
  152. <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 border-r border-gray-200 dark:border-gray-700 transition-colors" onclick="switchTab('edge', 'image-converter')" id="nav-edge" data-group="image-converter">Detected Edges</button>
  153. <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 transition-colors" onclick="switchTab('dot', 'image-converter')" id="nav-dot" data-group="image-converter">Identified Points</button>
  154. </div>
  155. </div>
  156. <!-- Settings Column -->
  157. <div class="bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 p-4 sm:p-6 flex flex-col gap-4 sm:gap-6 image-converter-settings">
  158. <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Processing Settings</h3>
  159. <div class="flex flex-col gap-2 setting-item">
  160. <label for="epsilon-slider" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Detail Level</label>
  161. <div class="flex flex-col gap-1 slider-container">
  162. <input type="range" id="epsilon-slider" min="0.1" max="1" step="0.05" value="0.3" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer">
  163. <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 slider-labels">
  164. <small>Fine</small>
  165. <small id="epsilon-value-display" class="font-medium">0.3</small>
  166. <small>Coarse</small>
  167. </div>
  168. </div>
  169. </div>
  170. <div class="flex flex-col gap-2 setting-item">
  171. <label for="dot-number" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Point Limit</label>
  172. <select id="dot-number" class="p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
  173. <option value="100">100 points</option>
  174. <option value="200">200 points</option>
  175. <option value="300">300 points</option>
  176. <option value="400">400 points</option>
  177. <option value="500" selected>500 points</option>
  178. <option value="1000">1000 points</option>
  179. </select>
  180. </div>
  181. <div class="flex flex-col gap-2 setting-item">
  182. <label for="contour-mode" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Contour Mode</label>
  183. <select id="contour-mode" class="p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
  184. <option value="External">External Only</option>
  185. <option value="Tree" selected>External + Internal</option>
  186. </select>
  187. </div>
  188. <div class="flex flex-col gap-3 control-group">
  189. <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
  190. <input type="checkbox" id="is-loop" class="hidden">
  191. <span class="custom-checkbox w-4 h-4 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center transition-all"></span>
  192. Loop Drawing
  193. </label>
  194. <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
  195. <input type="checkbox" id="no-shortcuts" checked class="hidden">
  196. <span class="custom-checkbox w-4 h-4 border-2 border-blue-600 bg-blue-600 rounded flex items-center justify-center transition-all"></span>
  197. No Shortcuts
  198. </label>
  199. </div>
  200. <input type="hidden" id="output-type" value="2">
  201. <div class="mt-auto generate-button-container">
  202. <button id="generate-button" class="w-full bg-blue-600 hover:bg-blue-700 text-white p-2 sm:p-3 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2 text-sm sm:text-base cta" onclick="regeneratePattern()">
  203. <i class="fa-solid fa-wand-magic-sparkles"></i>
  204. <span>Regenerate Pattern</span>
  205. </button>
  206. </div>
  207. </div>
  208. <!-- Preview Section -->
  209. <div class="lg:col-span-3 bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 p-4 sm:p-6 flex flex-col gap-3 sm:gap-4 dune-weaver-preview">
  210. <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Dune Weaver Preview</h3>
  211. <div class="preview-canvas-wrapper flex items-center justify-center w-full" style="max-width:500px; max-height:500px; margin:auto;">
  212. <canvas id="connect-image" class="flex-1 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 min-h-[200px] sm:min-h-[300px] max-w-full max-h-[500px]" style="max-width:500px; max-height:500px;"></canvas>
  213. </div>
  214. <div id="total-points" class="text-sm text-gray-500 dark:text-gray-400"></div>
  215. <div style="display: none;">
  216. <h4>contours</h4>
  217. <canvas id="plotcontours" width="400" height="400" style="border:1px solid #000000;"></canvas>
  218. <button id="plotButton">Plot Next Contour</button>
  219. </div>
  220. </div>
  221. <!-- Hidden compatibility elements -->
  222. <div style="display: none;">
  223. <label id="generation-status" style="display: none;">Image is generating - please wait...</label>
  224. <textarea id="polar-coordinates-textarea"></textarea>
  225. <div id="simple-coords"></div>
  226. <div id="simple-coords-title"></div>
  227. <input type="checkbox" id="gaussian-blur-toggle">
  228. <div>
  229. <input type="file" id="file-input" accept="image/*">
  230. <button id="file-button"></button>
  231. <span id="file-name"></span>
  232. </div>
  233. </div>
  234. <!-- Actions -->
  235. <div class="lg:col-span-3 flex justify-end gap-3 sm:gap-4 pt-4 sm:pt-6 border-t border-gray-200 dark:border-gray-700 image-converter-actions">
  236. <button class="secondary bg-white hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 sm:px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 border border-gray-300 dark:border-gray-600 text-sm sm:text-base cancel" onclick="window.history.back()">
  237. <i class="fa-solid fa-arrow-left"></i>
  238. <span>Back</span>
  239. </button>
  240. <button class="cta bg-blue-600 hover:bg-blue-700 text-white px-4 sm:px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 text-sm sm:text-base" id="save-pattern-button" onclick="savePattern()">
  241. <i class="fa-solid fa-floppy-disk"></i>
  242. <span>Save Pattern</span>
  243. </button>
  244. </div>
  245. </div>
  246. </section>
  247. </div>
  248. </div>
  249. <script>
  250. // OpenCV ready callback
  251. function onOpenCvReady() {
  252. console.log('OpenCV.js is ready');
  253. }
  254. // Tab switching function
  255. function switchTab(tabName, group) {
  256. // Hide all tab contents
  257. document.querySelectorAll(`[data-group="${group}"].tab-content`).forEach(tab => {
  258. tab.classList.add('hidden');
  259. tab.style.display = 'none';
  260. // Hide all canvases in the tab
  261. const canvas = tab.querySelector('canvas');
  262. if (canvas) {
  263. canvas.classList.add('hidden');
  264. }
  265. // Show the placeholder
  266. const placeholder = tab.querySelector('.upload-placeholder');
  267. if (placeholder) {
  268. placeholder.style.display = 'flex';
  269. }
  270. });
  271. // Remove active styling from all tab buttons
  272. document.querySelectorAll(`[data-group="${group}"].tab-button`).forEach(btn => {
  273. btn.className = 'tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 border-r border-gray-200 dark:border-gray-700 transition-colors';
  274. });
  275. // Show selected tab content
  276. const selectedTab = document.getElementById(`${tabName}-tab`);
  277. selectedTab.classList.remove('hidden');
  278. selectedTab.style.display = 'flex';
  279. // Show the canvas if it exists and has content
  280. const canvas = selectedTab.querySelector('canvas');
  281. if (canvas && canvas.width > 0 && canvas.height > 0) {
  282. canvas.classList.remove('hidden');
  283. // Hide the placeholder if canvas has content
  284. const placeholder = selectedTab.querySelector('.upload-placeholder');
  285. if (placeholder) {
  286. placeholder.style.display = 'none';
  287. }
  288. }
  289. // Add active styling to selected tab button
  290. const activeButton = document.getElementById(`nav-${tabName}`);
  291. activeButton.className = 'tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-white bg-blue-600 border-r border-gray-200 dark:border-gray-700 hover:bg-blue-700 transition-colors';
  292. // Remove border-r from last button
  293. if (activeButton.nextElementSibling === null) {
  294. activeButton.className = activeButton.className.replace('border-r border-gray-200 dark:border-gray-700', '');
  295. }
  296. }
  297. document.addEventListener('DOMContentLoaded', function() {
  298. const epsilonSlider = document.getElementById('epsilon-slider');
  299. const epsilonDisplay = document.getElementById('epsilon-value-display');
  300. const fileInput = document.getElementById('file-input');
  301. // Epsilon slider update
  302. epsilonSlider.addEventListener('input', function() {
  303. epsilonDisplay.textContent = this.value;
  304. });
  305. // File input handling
  306. fileInput.addEventListener('change', function(event) {
  307. if (event.target.files.length > 0) {
  308. handleImageUpload(event);
  309. }
  310. });
  311. // Custom checkbox handling
  312. document.querySelectorAll('.custom-input').forEach(label => {
  313. const checkbox = label.querySelector('input[type="checkbox"]');
  314. const customCheckbox = label.querySelector('.custom-checkbox');
  315. label.addEventListener('click', function(e) {
  316. e.preventDefault();
  317. checkbox.checked = !checkbox.checked;
  318. updateCheckboxAppearance(checkbox, customCheckbox);
  319. });
  320. // Initialize appearance
  321. updateCheckboxAppearance(checkbox, customCheckbox);
  322. });
  323. function updateCheckboxAppearance(checkbox, customCheckbox) {
  324. if (checkbox.checked) {
  325. customCheckbox.className = 'custom-checkbox w-4 h-4 border-2 border-blue-600 bg-blue-600 rounded flex items-center justify-center transition-all';
  326. } else {
  327. customCheckbox.className = 'custom-checkbox w-4 h-4 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center transition-all';
  328. }
  329. }
  330. });
  331. // Regenerate pattern function
  332. function regeneratePattern() {
  333. convertImage();
  334. }
  335. // Save pattern function
  336. async function savePattern() {
  337. try {
  338. await saveConvertedPattern();
  339. } catch (error) {
  340. console.error('Error saving pattern:', error);
  341. if (typeof showStatusMessage === 'function') {
  342. showStatusMessage('Error saving pattern: ' + error.message, 'error');
  343. }
  344. }
  345. }
  346. </script>
  347. {% endblock %}