| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- {% extends "base.html" %}
- {% block title %}Image2Sand - {{ app_name or 'Dune Weaver' }}{% endblock %}
- {% block additional_styles %}
- <style>
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .spinner {
- animation: spin 1s linear infinite;
- }
- .custom-checkbox::after {
- content: "✓";
- color: white;
- font-size: 0.75rem;
- font-weight: bold;
- }
- /* Override base template's dark mode rules for image2sand page in light mode */
- html:not(.dark) .bg-white {
- background-color: #ffffff !important;
- }
- html:not(.dark) .bg-gray-50 {
- background-color: #f9fafb !important;
- }
- html:not(.dark) .bg-gray-100 {
- background-color: #f3f4f6 !important;
- }
- html:not(.dark) .bg-gray-200 {
- background-color: #e5e7eb !important;
- }
- html:not(.dark) .text-gray-900 {
- color: #111827 !important;
- }
- html:not(.dark) .text-gray-800 {
- color: #1f2937 !important;
- }
- html:not(.dark) .text-gray-700 {
- color: #374151 !important;
- }
- html:not(.dark) .text-gray-600 {
- color: #4b5563 !important;
- }
- html:not(.dark) .text-gray-500 {
- color: #6b7280 !important;
- }
- html:not(.dark) .text-gray-400 {
- color: #9ca3af !important;
- }
- html:not(.dark) .text-gray-300 {
- color: #d1d5db !important;
- }
- html:not(.dark) .border-gray-200 {
- border-color: #e5e7eb !important;
- }
- html:not(.dark) .border-gray-300 {
- border-color: #d1d5db !important;
- }
- /* Fix hover states in light mode */
- html:not(.dark) .hover\:bg-gray-50:hover {
- background-color: #f9fafb !important;
- }
- html:not(.dark) .hover\:bg-gray-100:hover {
- background-color: #f3f4f6 !important;
- }
- html:not(.dark) .hover\:bg-gray-200:hover {
- background-color: #e5e7eb !important;
- }
- html:not(.dark) .hover\:text-gray-700:hover {
- color: #374151 !important;
- }
- html:not(.dark) .hover\:text-gray-800:hover {
- color: #1f2937 !important;
- }
- html:not(.dark) .hover\:text-gray-900:hover {
- color: #111827 !important;
- }
- /* Fix focus states */
- html:not(.dark) .focus\:ring-blue-500:focus {
- --tw-ring-color: #3b82f6 !important;
- }
- html:not(.dark) .focus\:border-blue-500:focus {
- border-color: #3b82f6 !important;
- }
- </style>
- {% endblock %}
- {% block scripts %}
- <!-- OpenCV.js for image processing -->
- <script async src="/static/js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
- <!-- FontAwesome for icons -->
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
- <!-- Image2Sand processing scripts -->
- <script src="/static/js/image2sand-init.js"></script>
- <script src="/static/js/image2sand.js"></script>
- {% endblock %}
- {% block content %}
- <div class="min-h-screen p-4 sm:p-8 pt-2 pb-[75px]">
- <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">
- <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
- <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 m-0">Convert Image to Pattern</h1>
- <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>
- </div>
-
- <!-- Image Converter Section - Required structure for JS -->
- <section id="image-converter" class="visible">
- <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">
-
- <!-- Processing Indicator -->
- <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">
- <div class="spinner w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full"></div>
- <span id="processing-message" class="text-gray-700 dark:text-gray-300">Processing image...</span>
- </div>
-
- <!-- Image Steps (Left Column) -->
- <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">
- <!-- Tab Content -->
- <div class="flex-1 relative">
- <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">
- <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">
- <i class="fa-solid fa-image text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
- <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Upload an Image</h3>
- <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Select an image file to begin the conversion process</p>
- <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()">
- <i class="fa-solid fa-upload"></i>
- Choose Image
- </button>
- </div>
- <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
- <canvas id="original-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
- </div>
- </div>
- <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">
- <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">
- <i class="fa-solid fa-bezier-curve text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
- <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Edge Detection</h3>
- <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Processed edges will appear here after uploading an image</p>
- </div>
- <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
- <canvas id="edge-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
- </div>
- </div>
- <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">
- <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">
- <i class="fa-solid fa-circle-dot text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
- <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Identified Points</h3>
- <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>
- </div>
- <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
- <canvas id="dot-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
- </div>
- </div>
- </div>
- <!-- Tab Navigation -->
- <div class="flex border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 tab-container">
- <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>
- <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>
- <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>
- </div>
- </div>
- <!-- Settings Column -->
- <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">
- <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Processing Settings</h3>
-
- <div class="flex flex-col gap-2 setting-item">
- <label for="epsilon-slider" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Detail Level</label>
- <div class="flex flex-col gap-1 slider-container">
- <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">
- <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 slider-labels">
- <small>Fine</small>
- <small id="epsilon-value-display" class="font-medium">0.3</small>
- <small>Coarse</small>
- </div>
- </div>
- </div>
-
- <div class="flex flex-col gap-2 setting-item">
- <label for="dot-number" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Point Limit</label>
- <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">
- <option value="100">100 points</option>
- <option value="200">200 points</option>
- <option value="300">300 points</option>
- <option value="400">400 points</option>
- <option value="500" selected>500 points</option>
- <option value="1000">1000 points</option>
- </select>
- </div>
-
- <div class="flex flex-col gap-2 setting-item">
- <label for="contour-mode" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Contour Mode</label>
- <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">
- <option value="External">External Only</option>
- <option value="Tree" selected>External + Internal</option>
- </select>
- </div>
-
- <div class="flex flex-col gap-3 control-group">
- <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
- <input type="checkbox" id="is-loop" class="hidden">
- <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>
- Loop Drawing
- </label>
- <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
- <input type="checkbox" id="no-shortcuts" checked class="hidden">
- <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>
- No Shortcuts
- </label>
- </div>
-
- <input type="hidden" id="output-type" value="2">
-
- <div class="mt-auto generate-button-container">
- <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()">
- <i class="fa-solid fa-wand-magic-sparkles"></i>
- <span>Regenerate Pattern</span>
- </button>
- </div>
- </div>
- <!-- Preview Section -->
- <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">
- <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Dune Weaver Preview</h3>
- <div class="preview-canvas-wrapper flex items-center justify-center w-full" style="max-width:500px; max-height:500px; margin:auto;">
- <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>
- </div>
- <div id="total-points" class="text-sm text-gray-500 dark:text-gray-400"></div>
- <div style="display: none;">
- <h4>contours</h4>
- <canvas id="plotcontours" width="400" height="400" style="border:1px solid #000000;"></canvas>
- <button id="plotButton">Plot Next Contour</button>
- </div>
- </div>
- <!-- Hidden compatibility elements -->
- <div style="display: none;">
- <label id="generation-status" style="display: none;">Image is generating - please wait...</label>
- <textarea id="polar-coordinates-textarea"></textarea>
- <div id="simple-coords"></div>
- <div id="simple-coords-title"></div>
- <input type="checkbox" id="gaussian-blur-toggle">
- <div>
- <input type="file" id="file-input" accept="image/*">
- <button id="file-button"></button>
- <span id="file-name"></span>
- </div>
- </div>
-
- <!-- Actions -->
- <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">
- <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()">
- <i class="fa-solid fa-arrow-left"></i>
- <span>Back</span>
- </button>
- <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()">
- <i class="fa-solid fa-floppy-disk"></i>
- <span>Save Pattern</span>
- </button>
- </div>
- </div>
- </section>
- </div>
- </div>
- <script>
- // OpenCV ready callback
- function onOpenCvReady() {
- console.log('OpenCV.js is ready');
- }
- // Tab switching function
- function switchTab(tabName, group) {
- // Hide all tab contents
- document.querySelectorAll(`[data-group="${group}"].tab-content`).forEach(tab => {
- tab.classList.add('hidden');
- tab.style.display = 'none';
-
- // Hide all canvases in the tab
- const canvas = tab.querySelector('canvas');
- if (canvas) {
- canvas.classList.add('hidden');
- }
-
- // Show the placeholder
- const placeholder = tab.querySelector('.upload-placeholder');
- if (placeholder) {
- placeholder.style.display = 'flex';
- }
- });
-
- // Remove active styling from all tab buttons
- document.querySelectorAll(`[data-group="${group}"].tab-button`).forEach(btn => {
- 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';
- });
-
- // Show selected tab content
- const selectedTab = document.getElementById(`${tabName}-tab`);
- selectedTab.classList.remove('hidden');
- selectedTab.style.display = 'flex';
-
- // Show the canvas if it exists and has content
- const canvas = selectedTab.querySelector('canvas');
- if (canvas && canvas.width > 0 && canvas.height > 0) {
- canvas.classList.remove('hidden');
- // Hide the placeholder if canvas has content
- const placeholder = selectedTab.querySelector('.upload-placeholder');
- if (placeholder) {
- placeholder.style.display = 'none';
- }
- }
-
- // Add active styling to selected tab button
- const activeButton = document.getElementById(`nav-${tabName}`);
- 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';
-
- // Remove border-r from last button
- if (activeButton.nextElementSibling === null) {
- activeButton.className = activeButton.className.replace('border-r border-gray-200 dark:border-gray-700', '');
- }
- }
- document.addEventListener('DOMContentLoaded', function() {
- const epsilonSlider = document.getElementById('epsilon-slider');
- const epsilonDisplay = document.getElementById('epsilon-value-display');
- const fileInput = document.getElementById('file-input');
- // Epsilon slider update
- epsilonSlider.addEventListener('input', function() {
- epsilonDisplay.textContent = this.value;
- });
- // File input handling
- fileInput.addEventListener('change', function(event) {
- if (event.target.files.length > 0) {
- handleImageUpload(event);
- }
- });
- // Custom checkbox handling
- document.querySelectorAll('.custom-input').forEach(label => {
- const checkbox = label.querySelector('input[type="checkbox"]');
- const customCheckbox = label.querySelector('.custom-checkbox');
-
- label.addEventListener('click', function(e) {
- e.preventDefault();
- checkbox.checked = !checkbox.checked;
- updateCheckboxAppearance(checkbox, customCheckbox);
- });
-
- // Initialize appearance
- updateCheckboxAppearance(checkbox, customCheckbox);
- });
-
- function updateCheckboxAppearance(checkbox, customCheckbox) {
- if (checkbox.checked) {
- customCheckbox.className = 'custom-checkbox w-4 h-4 border-2 border-blue-600 bg-blue-600 rounded flex items-center justify-center transition-all';
- } else {
- 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';
- }
- }
- });
- // Regenerate pattern function
- function regeneratePattern() {
- convertImage();
- }
- // Save pattern function
- async function savePattern() {
- try {
- await saveConvertedPattern();
- } catch (error) {
- console.error('Error saving pattern:', error);
- if (typeof showStatusMessage === 'function') {
- showStatusMessage('Error saving pattern: ' + error.message, 'error');
- }
- }
- }
- </script>
- {% endblock %}
|