🌐
index_copy2.html
Back
📝 Html ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tile Sheet Editor</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); overflow: hidden; display: flex; flex-direction: column; height: calc(100vh - 40px); } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px 30px; text-align: center; } .header h1 { font-size: 2rem; margin-bottom: 5px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .header p { font-size: 1rem; opacity: 0.9; } /* Top buttons section */ .top-section { background: #f8f9fa; border-bottom: 1px solid #e9ecef; padding: 15px 20px; display: flex; gap: 15px; align-items: center; justify-content: space-between; } .left-controls { display: flex; gap: 15px; align-items: center; } .right-controls { display: flex; gap: 10px; align-items: center; position: relative; } .btn { padding: 10px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; font-size: 14px; white-space: nowrap; display: flex; align-items: center; gap: 8px; } .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); } .btn.toggle { background: linear-gradient(135deg, #6c757d 0%, #495057 100%); } .btn.toggle.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .btn.success { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); } .btn.warning { background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%); color: #333; } .btn.menu { background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 10px 15px; position: relative; } .dropdown-menu { position: absolute; top: 100%; right: 0; background: white; border: 1px solid #e9ecef; border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); z-index: 1000; min-width: 200px; display: none; overflow: hidden; } .dropdown-menu.show { display: block; } .dropdown-item { display: block; width: 100%; padding: 12px 16px; background: none; border: none; text-align: left; color: #333; cursor: pointer; transition: background-color 0.2s ease; font-size: 14px; border-radius: 0; } .dropdown-item:hover { background: #f8f9fa; transform: none; box-shadow: none; } .dropdown-item.success { color: #28a745; } .dropdown-item.warning { color: #dc3545; } .dropdown-item.primary { color: #667eea; } /* Collapsible sections */ .collapsible-section { background: #f8f9fa; border-bottom: 1px solid #e9ecef; overflow: hidden; transition: max-height 0.3s ease; } .collapsible-section.collapsed { max-height: 0; border-bottom: none; } .collapsible-section:not(.collapsed) { max-height: 500px; } .section-content { padding: 15px 20px; } .section-title { margin-bottom: 10px; color: #333; font-size: 1.1rem; font-weight: 600; } .image-gallery { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 10px; } .image-gallery::-webkit-scrollbar { height: 8px; } .image-gallery::-webkit-scrollbar-track { background: #e9ecef; border-radius: 4px; } .image-gallery::-webkit-scrollbar-thumb { background: #667eea; border-radius: 4px; } .image-item { display: flex; flex-direction: column; align-items: center; padding: 10px; background: white; border-radius: 10px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; min-width: 100px; flex-shrink: 0; } .image-item:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); border-color: #667eea; } .image-item.active { border-color: #667eea; background: #f0f4ff; } .image-item img { width: 80px; height: 80px; object-fit: contain; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa; border: 1px solid #e9ecef; } .image-name { font-weight: 500; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; max-width: 80px; text-align: center; } .controls-scroll { display: flex; gap: 15px; overflow-x: auto; padding-bottom: 10px; } .controls-scroll::-webkit-scrollbar { height: 8px; } .controls-scroll::-webkit-scrollbar-track { background: #e9ecef; border-radius: 4px; } .controls-scroll::-webkit-scrollbar-thumb { background: #667eea; border-radius: 4px; } .control-group { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 120px; flex-shrink: 0; } .control-group label { font-weight: 500; color: #555; font-size: 13px; text-align: center; } /* Stepper Input Styles */ .stepper-container { display: flex; align-items: center; border: 2px solid #ddd; border-radius: 5px; background: white; overflow: hidden; transition: border-color 0.3s ease; } .stepper-container:focus-within { border-color: #667eea; } .stepper-btn { background: #f8f9fa; border: none; width: 28px; height: 32px; cursor: pointer; font-size: 14px; font-weight: bold; color: #666; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .stepper-btn:hover { background: #e9ecef; color: #333; } .stepper-btn:active { background: #dee2e6; } .stepper-input { border: none; text-align: center; font-size: 14px; width: 50px; height: 32px; outline: none; background: white; color: #333; } /* Main content area */ .main-content { flex: 1; display: flex; flex-direction: column; padding: 20px; overflow: hidden; } .editor-header { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #e9ecef; } .editor-title { font-size: 1.3rem; color: #333; margin-bottom: 10px; } .selected-tiles-section { margin-bottom: 15px; } .selected-tiles-list { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; min-height: 30px; padding: 10px; background: #f8f9fa; border-radius: 8px; border: 2px dashed #ddd; } .tile-tag { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; display: flex; align-items: center; gap: 5px; } .tile-tag .remove { cursor: pointer; font-weight: bold; opacity: 0.7; } .tile-tag .remove:hover { opacity: 1; } .canvas-container { flex: 1; display: flex; justify-content: center; align-items: center; background: #f8f9fa; border-radius: 10px; padding: 20px; position: relative; overflow: auto; } .tile-grid-container { position: relative; display: inline-block; border: 2px solid #ddd; border-radius: 8px; background: white; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); image-rendering: pixelated; image-rendering: crisp-edges; } .tile-image { display: block; image-rendering: pixelated; image-rendering: crisp-edges; } .tile-overlay { position: absolute; top: 0; left: 0; display: grid; cursor: crosshair; } .tile-cell { position: relative; border-right: 1px solid rgba(0, 255, 0, 0.5); border-bottom: 1px solid rgba(0, 255, 0, 0.5); box-sizing: border-box; } .tile-cell:nth-child(-n + var(--grid-cols)) { border-top: 1px solid rgba(0, 255, 0, 0.5); } .tile-cell:nth-child(var(--grid-cols) + 1) ~ .tile-cell { border-top: 1px solid rgba(0, 255, 0, 0.5); } .tile-cell:nth-child(n + var(--grid-cols) * (var(--grid-rows) - 1) + 1) { border-bottom: none; } .tile-cell:nth-child(var(--grid-cols)) { border-right: none; } .tile-cell.selected { background: rgba(255, 0, 0, 0.3); outline: 2px solid #ff0000; box-sizing: border-box; } .tile-info { position: absolute; top: 10px; right: 10px; background: rgba(0, 0, 0, 0.8); color: white; padding: 10px; border-radius: 5px; font-size: 12px; display: none; z-index: 10; } .image-preview { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: none; justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(5px); } .preview-content { position: relative; max-width: 90vw; max-height: 90vh; background: white; border-radius: 15px; padding: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); } .preview-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #e9ecef; } .preview-title { font-size: 1.2rem; font-weight: 600; color: #333; } .preview-close { background: #dc3545; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; } .preview-close:hover { background: #c82333; } .preview-canvas-container { position: relative; display: flex; justify-content: center; align-items: center; max-height: 70vh; overflow: auto; border-radius: 8px; background: #f8f9fa; } .preview-canvas { border: 2px solid #ddd; border-radius: 5px; background: white; image-rendering: pixelated; image-rendering: crisp-edges; } .preview-info { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px; font-size: 12px; color: #666; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; } .loading { text-align: center; padding: 40px; color: #666; } .loading::before { content: ''; display: inline-block; width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .no-images { text-align: center; padding: 40px; color: #666; } .json-output { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 80vw; max-height: 80vh; background: white; border-radius: 15px; padding: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); z-index: 1001; display: none; } .json-output h4 { margin-bottom: 10px; color: #333; } .json-output pre { background: #f8f9fa; padding: 15px; border-radius: 5px; border: 1px solid #ddd; font-size: 12px; overflow: auto; max-height: 60vh; margin-bottom: 15px; } .json-close { position: absolute; top: 10px; right: 10px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px; } @media (max-width: 768px) { .container { height: calc(100vh - 20px); margin: 10px; border-radius: 15px; } .header { padding: 15px 20px; } .header h1 { font-size: 1.5rem; } .top-section, .image-section, .controls-section { padding: 10px 15px; } .main-content { padding: 15px; } .image-item { min-width: 80px; } .image-item img { width: 60px; height: 60px; } .control-group { min-width: 100px; } .btn { padding: 8px 12px; font-size: 12px; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎮 Tile Sheet Editor</h1> <p>Select tiles and generate JSON configuration</p> </div> <!-- Top buttons section --> <div class="top-section"> <div class="left-controls"> <input type="file" id="fileInput" accept="image/*" style="display: none;" multiple> <button class="btn" onclick="document.getElementById('fileInput').click()"> 📁 Add Tile Sheet(s) </button> <button class="btn toggle" id="imagesToggle" onclick="toggleSection('images')"> 🖼️ Images </button> <button class="btn toggle" id="configToggle" onclick="toggleSection('config')"> ⚙️ Configuration </button> <div id="uploadStatus" style="font-size: 12px; color: #666; margin-left: 15px;"></div> </div> <div class="right-controls"> <button class="btn" onclick="updateGrid()">🔄 Update Grid</button> <button class="btn menu" id="menuButton" onclick="toggleMenu()"> ⋯ </button> <div class="dropdown-menu" id="dropdownMenu"> <button class="dropdown-item success" onclick="generateJSON(); closeMenu();"> 📄 Generate JSON </button> <button class="dropdown-item warning" onclick="clearSelection(); closeMenu();"> 🗑️ Clear Selection </button> <button class="dropdown-item primary" onclick="downloadTile(); closeMenu();"> 💾 Download Selected Tile </button> </div> </div> </div> <!-- Image gallery section (collapsible) --> <div class="collapsible-section collapsed" id="imagesSection"> <div class="section-content"> <h3 class="section-title">Available Images</h3> <div class="image-gallery" id="imageGallery"> <div class="loading">Loading images...</div> </div> </div> </div> <!-- Controls section (collapsible) --> <div class="collapsible-section collapsed" id="configSection"> <div class="section-content"> <h3 class="section-title">Tile Configuration</h3> <div class="controls-scroll"> <div class="control-group"> <label>Tile Width</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('tileWidth', -8)">-</button> <input type="number" id="tileWidth" value="32" min="8" step="8" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('tileWidth', 8)">+</button> </div> </div> <div class="control-group"> <label>Tile Height</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('tileHeight', -8)">-</button> <input type="number" id="tileHeight" value="32" min="8" step="8" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('tileHeight', 8)">+</button> </div> </div> <div class="control-group"> <label>H Spacing</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('horizontalSpacing', -1)">-</button> <input type="number" id="horizontalSpacing" value="0" min="0" max="50" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('horizontalSpacing', 1)">+</button> </div> </div> <div class="control-group"> <label>V Spacing</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('verticalSpacing', -1)">-</button> <input type="number" id="verticalSpacing" value="0" min="0" max="50" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('verticalSpacing', 1)">+</button> </div> </div> <div class="control-group"> <label>X Offset</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('xOffset', -1)">-</button> <input type="number" id="xOffset" value="0" min="-50" max="50" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('xOffset', 1)">+</button> </div> </div> <div class="control-group"> <label>Y Offset</label> <div class="stepper-container"> <button class="stepper-btn" onclick="changeValue('yOffset', -1)">-</button> <input type="number" id="yOffset" value="0" min="-50" max="50" class="stepper-input"> <button class="stepper-btn" onclick="changeValue('yOffset', 1)">+</button> </div> </div> </div> </div> </div> <!-- Main content area --> <div class="main-content"> <div class="editor-header"> <div class="editor-title" id="editorTitle">Select an image to start editing</div> <div class="selected-tiles-section"> <h4>Selected Tiles:</h4> <div class="selected-tiles-list" id="selectedTilesList"> <span style="color: #999; font-style: italic;">Click tiles to select them...</span> </div> </div> </div> <div class="canvas-container"> <div class="tile-grid-container" id="tileGridContainer"> <img id="tileImage" class="tile-image" style="display: none;"> <div id="tileOverlay" class="tile-overlay" style="display: none;"></div> </div> <div class="tile-info" id="tileInfo"></div> </div> </div> <!-- Image preview modal --> <div class="image-preview" id="imagePreview"> <div class="preview-content"> <div class="preview-header"> <div class="preview-title" id="previewTitle">Image Preview</div> <button class="preview-close" onclick="closeImagePreview()">×</button> </div> <div class="preview-canvas-container"> <canvas id="previewCanvas" class="preview-canvas"></canvas> </div> <div class="preview-info" id="previewInfo"> <span>Click and drag to pan • Scroll to zoom</span> </div> </div> </div> <!-- JSON output modal --> <div class="json-output" id="jsonOutput"> <button class="json-close" onclick="closeJSONOutput()">×</button> <h4>Generated JSON:</h4> <pre id="jsonContent"></pre> <button class="btn" onclick="copyJSON()">Copy to Clipboard</button> <button class="btn" onclick="downloadJSON()">Download JSON</button> </div> </div> <script> let currentImage = null; let currentImagePath = ''; let selectedTiles = new Set(); let lastSelectedTile = { x: -1, y: -1 }; let gridCols = 0; let gridRows = 0; // Toggle section functionality function toggleSection(sectionName) { const section = document.getElementById(sectionName + 'Section'); const button = document.getElementById(sectionName + 'Toggle'); if (section.classList.contains('collapsed')) { section.classList.remove('collapsed'); button.classList.add('active'); } else { section.classList.add('collapsed'); button.classList.remove('active'); } } // Menu functionality function toggleMenu() { const menu = document.getElementById('dropdownMenu'); menu.classList.toggle('show'); } function closeMenu() { const menu = document.getElementById('dropdownMenu'); menu.classList.remove('show'); } // Close menu when clicking outside document.addEventListener('click', function(e) { const menu = document.getElementById('dropdownMenu'); const button = document.getElementById('menuButton'); if (!menu.contains(e.target) && !button.contains(e.target)) { closeMenu(); } }); // Stepper functionality function changeValue(inputId, change) { const input = document.getElementById(inputId); const currentValue = parseInt(input.value) || 0; const min = parseInt(input.min) || -Infinity; const max = parseInt(input.max) || Infinity; const step = parseInt(input.step) || 1; let newValue = currentValue + change; // Apply step constraints for tile dimensions if (inputId === 'tileWidth' || inputId === 'tileHeight') { newValue = Math.round(newValue / step) * step; } // Apply min/max constraints newValue = Math.max(min, Math.min(max, newValue)); input.value = newValue; updateGrid(); } // Load images from PHP script async function loadImages() { try { const response = await fetch('get_images.php'); const result = await response.json(); const imageGallery = document.getElementById('imageGallery'); // Handle error response if (result.error) { imageGallery.innerHTML = ` <div class="no-images"> <strong>Error:</strong> ${result.error}<br> <small>Tried paths: ${result.tried_paths ? result.tried_paths.join(', ') : 'N/A'}</small><br> <small>Current dir: ${result.current_dir || 'N/A'}</small> </div> `; return; } // Handle array of images const images = Array.isArray(result) ? result : (result.images || []); if (images.length === 0) { imageGallery.innerHTML = '<div class="no-images">No images found in the images folder</div>'; return; } imageGallery.innerHTML = ''; images.forEach((imagePath, index) => { const item = document.createElement('div'); item.className = 'image-item'; // Create thumbnail image with error handling const img = document.createElement('img'); img.loading = 'lazy'; img.alt = imagePath; // Try different path variations const possiblePaths = [ imagePath, '../' + imagePath, '/' + imagePath, imagePath.replace('../', ''), 'images/' + imagePath.split('/').pop() ]; let pathIndex = 0; function tryNextPath() { if (pathIndex < possiblePaths.length) { img.src = possiblePaths[pathIndex]; pathIndex++; } else { // All paths failed, show placeholder img.style.display = 'none'; const placeholder = document.createElement('div'); placeholder.style.cssText = ` width: 80px; height: 80px; background: #f0f0f0; border: 1px solid #ddd; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #666; margin-bottom: 8px; border-radius: 5px; `; placeholder.textContent = 'No img'; item.insertBefore(placeholder, img); } } img.onerror = tryNextPath; img.onload = function() { console.log('Image loaded successfully:', img.src); }; // Add preview functionality img.addEventListener('click', (e) => { e.stopPropagation(); showImagePreview(img.src); }); const nameDiv = document.createElement('div'); nameDiv.className = 'image-name'; nameDiv.textContent = imagePath.split('/').pop(); item.appendChild(img); item.appendChild(nameDiv); item.addEventListener('click', () => { document.querySelectorAll('.image-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); loadImage(img.src); }); imageGallery.appendChild(item); // Start trying paths tryNextPath(); }); // Auto-load first image if (images.length > 0) { setTimeout(() => { const firstImg = document.querySelector('.image-item img'); if (firstImg && firstImg.complete && firstImg.naturalWidth > 0) { document.querySelector('.image-item').click(); } }, 1000); } } catch (error) { console.error('Error loading images:', error); document.getElementById('imageGallery').innerHTML = ` <div class="no-images"> Error loading images: ${error.message}<br> <small>Check browser console for details</small> </div> `; } } function loadImage(imagePath) { const img = new Image(); img.onload = function() { currentImage = img; currentImagePath = imagePath; document.getElementById('editorTitle').textContent = `Editing: ${imagePath.split('/').pop()}`; const tileImg = document.getElementById('tileImage'); tileImg.src = imagePath; tileImg.style.display = 'block'; tileImg.style.width = currentImage.width + 'px'; tileImg.style.height = currentImage.height + 'px'; // Log image dimensions for debugging console.log(`Image loaded: ${currentImage.width}x${currentImage.height}px`); // Clear selection when loading new image selectedTiles.clear(); updateSelectedTilesList(); updateGrid(); }; img.onerror = function() { console.error('Failed to load image:', imagePath); alert('Failed to load image: ' + imagePath); }; img.src = imagePath; } function updateGrid() { if (!currentImage) return; const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const hSpacing = parseInt(document.getElementById('horizontalSpacing').value); const vSpacing = parseInt(document.getElementById('verticalSpacing').value); const xOffset = parseInt(document.getElementById('xOffset').value); const yOffset = parseInt(document.getElementById('yOffset').value); // Validate tile sizes to be multiples of 8 if (tileWidth % 8 !== 0 || tileHeight % 8 !== 0) { console.warn('Tile width and height should be multiples of 8'); document.getElementById('tileWidth').value = Math.round(tileWidth / 8) * 8 || 8; document.getElementById('tileHeight').value = Math.round(tileHeight / 8) * 8 || 8; return updateGrid(); // Recalculate with corrected values } // Calculate grid dimensions const effectiveWidth = tileWidth + hSpacing; const effectiveHeight = tileHeight + vSpacing; gridCols = Math.floor((currentImage.width - xOffset) / effectiveWidth); gridRows = Math.floor((currentImage.height - yOffset) / effectiveHeight); // If no spacing, use direct division if (hSpacing === 0) { gridCols = Math.floor((currentImage.width - xOffset) / tileWidth); } if (vSpacing === 0) { gridRows = Math.floor((currentImage.height - yOffset) / tileHeight); } // Log grid calculations for debugging console.log(`Grid calculated: ${gridCols} cols x ${gridRows} rows`); console.log(`Tile size: ${tileWidth}x${tileHeight}px, Spacing: ${hSpacing}x${vSpacing}px, Offset: ${xOffset}x${yOffset}px`); // Calculate total tiled area including gaps but excluding trailing spacing const totalWidth = gridCols * tileWidth + (gridCols > 0 ? (gridCols - 1) * hSpacing : 0); const totalHeight = gridRows * tileHeight + (gridRows > 0 ? (gridRows - 1) * vSpacing : 0); // Set overlay dimensions and position const overlay = document.getElementById('tileOverlay'); overlay.style.width = totalWidth + 'px'; overlay.style.height = totalHeight + 'px'; overlay.style.marginLeft = xOffset + 'px'; overlay.style.marginTop = yOffset + 'px'; overlay.style.display = 'grid'; // Set grid template overlay.style.gridTemplateColumns = `repeat(${gridCols}, ${tileWidth}px)`; overlay.style.gridTemplateRows = `repeat(${gridRows}, ${tileHeight}px)`; overlay.style.gap = `${vSpacing}px ${hSpacing}px`; // row-gap column-gap overlay.style.setProperty('--grid-cols', gridCols); overlay.style.setProperty('--grid-rows', gridRows); // Log overlay dimensions for debugging console.log(`Overlay size: ${totalWidth}x${totalHeight}px`); // Clear existing cells overlay.innerHTML = ''; // Create tile cells for (let row = 0; row < gridRows; row++) { for (let col = 0; col < gridCols; col++) { const cell = document.createElement('div'); cell.className = 'tile-cell'; cell.dataset.col = col; cell.dataset.row = row; cell.dataset.key = `${col},${row}`; // Check if selected if (selectedTiles.has(`${col},${row}`)) { cell.classList.add('selected'); } // Add click event cell.addEventListener('click', function(e) { e.stopPropagation(); toggleTile(col, row, cell.dataset.key); }); overlay.appendChild(cell); } } } function toggleTile(col, row, tileKey) { if (selectedTiles.has(tileKey)) { selectedTiles.delete(tileKey); console.log(`Deselected tile: ${tileKey}`); } else { selectedTiles.add(tileKey); console.log(`Selected tile: ${tileKey}`); } lastSelectedTile.x = col; lastSelectedTile.y = row; updateSelectedTilesList(); updateGrid(); // Rebuild to update selected class // Show tile info const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const hSpacing = parseInt(document.getElementById('horizontalSpacing').value); const vSpacing = parseInt(document.getElementById('verticalSpacing').value); const xOffset = parseInt(document.getElementById('xOffset').value); const yOffset = parseInt(document.getElementById('yOffset').value); const actualTileX = xOffset + col * (tileWidth + hSpacing); const actualTileY = yOffset + row * (tileHeight + vSpacing); const tileInfo = document.getElementById('tileInfo'); tileInfo.innerHTML = ` Tile: (${col}, ${row})<br> Position: (${actualTileX}px, ${actualTileY}px)<br> Size: ${tileWidth}x${tileHeight}px<br> Spacing: H${hSpacing}px V${vSpacing}px<br> Status: ${selectedTiles.has(tileKey) ? 'Selected' : 'Unselected'} `; tileInfo.style.display = 'block'; } function updateSelectedTilesList() { const container = document.getElementById('selectedTilesList'); if (selectedTiles.size === 0) { container.innerHTML = '<span style="color: #999; font-style: italic;">Click tiles to select them...</span>'; return; } container.innerHTML = ''; Array.from(selectedTiles).sort().forEach(tileKey => { const [x, y] = tileKey.split(',').map(Number); const tag = document.createElement('span'); tag.className = 'tile-tag'; tag.innerHTML = `(${x},${y}) <span class="remove" onclick="removeTile('${tileKey}')">×</span>`; container.appendChild(tag); }); } function removeTile(tileKey) { selectedTiles.delete(tileKey); updateSelectedTilesList(); updateGrid(); } function clearSelection() { selectedTiles.clear(); updateSelectedTilesList(); updateGrid(); } function closeJSONOutput() { document.getElementById('jsonOutput').style.display = 'none'; } // Event listeners for controls with validation - now handled by stepper functions document.getElementById('tileWidth').addEventListener('change', function() { let value = parseInt(this.value); if (value < 8) value = 8; this.value = Math.round(value / 8) * 8; updateGrid(); }); document.getElementById('tileHeight').addEventListener('change', function() { let value = parseInt(this.value); if (value < 8) value = 8; this.value = Math.round(value / 8) * 8; updateGrid(); }); document.getElementById('horizontalSpacing').addEventListener('change', updateGrid); document.getElementById('verticalSpacing').addEventListener('change', updateGrid); document.getElementById('xOffset').addEventListener('change', updateGrid); document.getElementById('yOffset').addEventListener('change', updateGrid); // Hide info on mouse leave document.getElementById('tileGridContainer').addEventListener('mouseleave', function() { document.getElementById('tileInfo').style.display = 'none'; }); function generateJSON() { if (selectedTiles.size === 0) { alert('Please select at least one tile'); return; } const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const hSpacing = parseInt(document.getElementById('horizontalSpacing').value); const vSpacing = parseInt(document.getElementById('verticalSpacing').value); const xOffset = parseInt(document.getElementById('xOffset').value); const yOffset = parseInt(document.getElementById('yOffset').value); const tilesData = Array.from(selectedTiles).map(tileKey => { const [x, y] = tileKey.split(',').map(Number); const pixelX = xOffset + x * (tileWidth + hSpacing); const pixelY = yOffset + y * (tileHeight + vSpacing); return { id: `tile_${x}_${y}`, grid_position: { x, y }, pixel_position: { x: pixelX, y: pixelY }, size: { width: tileWidth, height: tileHeight } }; }); const jsonData = { source_image: `root/images/${currentImagePath.split('/').pop()}`, image_dimensions: { width: currentImage.width, height: currentImage.height }, tile_configuration: { tile_size: { width: tileWidth, height: tileHeight }, spacing: { horizontal: hSpacing, vertical: vSpacing }, offset: { x: xOffset, y: yOffset } }, grid_dimensions: { columns: gridCols, rows: gridRows }, selected_tiles: tilesData, total_selected: tilesData.length, generated_at: new Date().toISOString() }; document.getElementById('jsonContent').textContent = JSON.stringify(jsonData, null, 2); document.getElementById('jsonOutput').style.display = 'block'; } function copyJSON() { const jsonText = document.getElementById('jsonContent').textContent; navigator.clipboard.writeText(jsonText).then(() => { alert('JSON copied to clipboard!'); }); } function downloadJSON() { const jsonText = document.getElementById('jsonContent').textContent; const blob = new Blob([jsonText], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `tilemap_${currentImagePath.split('/').pop().split('.')[0]}_${new Date().getTime()}.json`; link.click(); URL.revokeObjectURL(url); } function downloadTile() { if (lastSelectedTile.x < 0 || lastSelectedTile.y < 0) { alert('Please select a tile first'); return; } const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const hSpacing = parseInt(document.getElementById('horizontalSpacing').value); const vSpacing = parseInt(document.getElementById('verticalSpacing').value); const xOffset = parseInt(document.getElementById('xOffset').value); const yOffset = parseInt(document.getElementById('yOffset').value); // Calculate the actual position of the tile in the image const actualTileX = xOffset + lastSelectedTile.x * (tileWidth + hSpacing); const actualTileY = yOffset + lastSelectedTile.y * (tileHeight + vSpacing); // Create a temporary canvas for the tile const tileCanvas = document.createElement('canvas'); const tileCtx = tileCanvas.getContext('2d'); tileCanvas.width = tileWidth; tileCanvas.height = tileHeight; // Draw the selected tile using the correct position calculation tileCtx.drawImage( currentImage, actualTileX, actualTileY, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight ); // Download the tile const link = document.createElement('a'); link.download = `tile_${lastSelectedTile.x}_${lastSelectedTile.y}.png`; link.href = tileCanvas.toDataURL(); link.click(); } // Load images when page loads loadImages(); // Image preview functionality let previewCanvas = document.getElementById('previewCanvas'); let previewCtx = previewCanvas.getContext('2d'); let previewImage = null; let previewZoom = 1; let previewOffsetX = 0; let previewOffsetY = 0; let isDragging = false; let lastMouseX = 0; let lastMouseY = 0; function showImagePreview(imagePath) { const img = new Image(); img.onload = function() { previewImage = img; // Reset zoom and position previewZoom = 1; previewOffsetX = 0; previewOffsetY = 0; // Set canvas size const maxWidth = window.innerWidth * 0.7; const maxHeight = window.innerHeight * 0.6; let displayWidth = img.width; let displayHeight = img.height; // Scale down if too large if (displayWidth > maxWidth || displayHeight > maxHeight) { const scale = Math.min(maxWidth / displayWidth, maxHeight / displayHeight); displayWidth *= scale; displayHeight *= scale; previewZoom = scale; } previewCanvas.width = displayWidth; previewCanvas.height = displayHeight; // Update title and info document.getElementById('previewTitle').textContent = imagePath.split('/').pop(); document.getElementById('previewInfo').innerHTML = ` <span>Dimensions: ${img.width} × ${img.height}px</span> <span>Zoom: ${Math.round(previewZoom * 100)}%</span> <span>Click thumbnail to edit • Right-click to close</span> `; drawPreview(); document.getElementById('imagePreview').style.display = 'flex'; }; img.onerror = function() { console.error('Failed to load preview image:', imagePath); alert('Failed to load preview for: ' + imagePath.split('/').pop()); }; img.src = imagePath; } function drawPreview() { if (!previewImage) return; const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const hSpacing = parseInt(document.getElementById('horizontalSpacing').value); const vSpacing = parseInt(document.getElementById('verticalSpacing').value); const xOffset = parseInt(document.getElementById('xOffset').value); const yOffset = parseInt(document.getElementById('yOffset').value); previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); // Draw the image previewCtx.save(); previewCtx.translate(previewOffsetX, previewOffsetY); previewCtx.scale(previewZoom, previewZoom); previewCtx.imageSmoothingEnabled = false; previewCtx.drawImage(previewImage, 0, 0); // Draw grid overlay with spacing and offset const effectiveTileWidth = tileWidth + hSpacing; const effectiveTileHeight = tileHeight + vSpacing; const gridCols = Math.floor((previewImage.width - xOffset) / effectiveTileWidth); const gridRows = Math.floor((previewImage.height - yOffset) / effectiveTileHeight); previewCtx.strokeStyle = 'rgba(0, 255, 0, 0.8)'; previewCtx.lineWidth = 1 / previewZoom; // Draw tile boundaries for (let row = 0; row < gridRows; row++) { for (let col = 0; col < gridCols; col++) { const x = xOffset + col * effectiveTileWidth; const y = yOffset + row * effectiveTileHeight; previewCtx.strokeRect(x, y, tileWidth, tileHeight); } } previewCtx.restore(); } function closeImagePreview() { document.getElementById('imagePreview').style.display = 'none'; previewImage = null; } // Preview canvas event listeners previewCanvas.addEventListener('wheel', function(e) { e.preventDefault(); const rect = previewCanvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.max(0.1, Math.min(5, previewZoom * zoomFactor)); // Zoom towards mouse position previewOffsetX = mouseX - (mouseX - previewOffsetX) * (newZoom / previewZoom); previewOffsetY = mouseY - (mouseY - previewOffsetY) * (newZoom / previewZoom); previewZoom = newZoom; // Update info document.getElementById('previewInfo').innerHTML = ` <span>Dimensions: ${previewImage.width} × ${previewImage.height}px</span> <span>Zoom: ${Math.round(previewZoom * 100)}%</span> <span>Click thumbnail to edit • Right-click to close</span> `; drawPreview(); }); previewCanvas.addEventListener('mousedown', function(e) { if (e.button === 2) { // Right click to close closeImagePreview(); return; } isDragging = true; lastMouseX = e.clientX; lastMouseY = e.clientY; previewCanvas.style.cursor = 'grabbing'; }); previewCanvas.addEventListener('mousemove', function(e) { if (isDragging) { const deltaX = e.clientX - lastMouseX; const deltaY = e.clientY - lastMouseY; previewOffsetX += deltaX; previewOffsetY += deltaY; lastMouseX = e.clientX; lastMouseY = e.clientY; drawPreview(); } }); previewCanvas.addEventListener('mouseup', function() { isDragging = false; previewCanvas.style.cursor = 'grab'; }); previewCanvas.addEventListener('mouseleave', function() { isDragging = false; previewCanvas.style.cursor = 'grab'; }); previewCanvas.style.cursor = 'grab'; // Close preview on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeImagePreview(); if (document.getElementById('jsonOutput').style.display === 'block') { closeJSONOutput(); } } }); // Close preview when clicking outside document.getElementById('imagePreview').addEventListener('click', function(e) { if (e.target === this) { closeImagePreview(); } }); // Handle file uploads document.getElementById('fileInput').addEventListener('change', function(e) { const files = e.target.files; if (files.length === 0) return; uploadFiles(files); }); async function uploadFiles(files) { const uploadStatus = document.getElementById('uploadStatus'); uploadStatus.textContent = `Uploading ${files.length} file(s)...`; uploadStatus.style.color = '#667eea'; const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('images[]', files[i]); } try { const response = await fetch('upload_images.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { uploadStatus.textContent = `✅ Successfully uploaded ${result.uploaded.length} file(s)`; uploadStatus.style.color = '#28a745'; // Refresh the image list setTimeout(() => { loadImages(); uploadStatus.textContent = ''; }, 2000); } else { uploadStatus.textContent = `❌ Upload failed: ${result.error}`; uploadStatus.style.color = '#dc3545'; } } catch (error) { console.error('Upload error:', error); uploadStatus.textContent = '❌ Upload failed: Network error'; uploadStatus.style.color = '#dc3545'; } // Clear the file input document.getElementById('fileInput').value = ''; } </script> </body> </html>