🌐
index.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; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .header p { font-size: 1.1rem; opacity: 0.9; } @media (max-width: 768px) { .header { padding: 20px; } .header h1 { font-size: 1.8rem; } .header p { font-size: 1rem; } } .main-content { display: flex; min-height: 600px; } .sidebar { width: 300px; background: #f8f9fa; border-right: 1px solid #e9ecef; padding: 20px; overflow-y: auto; } @media (max-width: 768px) { .main-content { flex-direction: column; } .sidebar { width: 100%; border-right: none; border-bottom: 1px solid #e9ecef; padding: 15px; max-height: 300px; } } .image-list { display: flex; flex-direction: column; gap: 10px; } .image-item { display: flex; align-items: center; padding: 10px; background: white; border-radius: 10px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .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: 60px; height: 60px; object-fit: contain; border-radius: 5px; margin-right: 10px; background: #f8f9fa; border: 1px solid #e9ecef; } .image-name { font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; } .editor-area { flex: 1; padding: 20px; display: flex; flex-direction: column; } @media (max-width: 768px) { .editor-area { padding: 15px; } } .editor-header { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #e9ecef; } @media (max-width: 768px) { .editor-header { margin-bottom: 15px; padding-bottom: 15px; } } .editor-title { font-size: 1.5rem; color: #333; margin-bottom: 10px; } @media (max-width: 768px) { .editor-title { font-size: 1.2rem; margin-bottom: 8px; } } .controls { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; margin-bottom: 15px; } .control-group { display: flex; align-items: center; gap: 8px; min-width: 120px; } .control-group label { font-weight: 500; color: #555; min-width: 80px; font-size: 14px; } @media (max-width: 768px) { .controls { gap: 10px; } .control-group { min-width: 100%; justify-content: space-between; margin-bottom: 8px; } .control-group label { min-width: auto; flex: 1; font-size: 13px; } } input[type="number"] { width: 80px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; font-size: 14px; transition: border-color 0.3s ease; } input[type="number"]:focus { outline: none; border-color: #667eea; } @media (max-width: 768px) { input[type="number"] { width: 70px; padding: 6px; font-size: 13px; } } .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; margin-right: 10px; margin-bottom: 8px; font-size: 14px; } .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); } @media (max-width: 768px) { .btn { padding: 8px 16px; font-size: 13px; margin-right: 8px; margin-bottom: 8px; flex: 1; min-width: calc(50% - 4px); } .btn:last-child { margin-right: 0; } } @media (max-width: 480px) { .btn { min-width: 100%; margin-right: 0; } } .btn.success { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); } .btn.warning { background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%); color: #333; } .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; } @media (max-width: 768px) { .selected-tiles-list { gap: 6px; padding: 8px; min-height: 40px; } } .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; } @media (max-width: 768px) { .canvas-container { padding: 10px; min-height: 400px; } } canvas { border: 2px solid #ddd; border-radius: 8px; background: white; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); cursor: crosshair; max-width: 100%; height: auto; } @media (max-width: 768px) { canvas { max-width: calc(100vw - 60px); max-height: calc(100vh - 400px); } } .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); } } .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; } .no-images { text-align: center; padding: 40px; color: #666; } .json-output { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; } .json-output h4 { margin-bottom: 10px; color: #333; } .json-output pre { background: #fff; padding: 15px; border-radius: 5px; border: 1px solid #ddd; font-size: 12px; overflow-x: auto; max-height: 300px; overflow-y: auto; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎮 Tile Sheet Editor</h1> <p>Select tiles and generate JSON configuration</p> </div> <div class="main-content"> <div class="sidebar"> <h3 style="margin-bottom: 15px; color: #333;">Available Images</h3> <div style="margin-bottom: 20px;"> <input type="file" id="fileInput" accept="image/*" style="display: none;" multiple> <button class="btn" onclick="document.getElementById('fileInput').click()" style="width: 100%; margin-bottom: 10px;"> 📁 Add Sample Images </button> <div id="uploadStatus" style="font-size: 12px; color: #666; text-align: center;"></div> </div> <div class="image-list" id="imageList"> <div class="no-images">Add some tile sheet images to get started</div> </div> </div> <div class="editor-area"> <div class="editor-header"> <div class="editor-title" id="editorTitle">Select an image to start editing</div> <div class="controls"> <div class="control-group"> <label>Tile Width:</label> <input type="number" id="tileWidth" value="32" min="1"> </div> <div class="control-group"> <label>Tile Height:</label> <input type="number" id="tileHeight" value="32" min="1"> </div> <div class="control-group"> <label>H Spacing:</label> <input type="number" id="horizontalSpacing" value="0" min="0" max="50"> </div> <div class="control-group"> <label>V Spacing:</label> <input type="number" id="verticalSpacing" value="0" min="0" max="50"> </div> <div class="control-group"> <label>X Offset:</label> <input type="number" id="xOffset" value="0" min="-50" max="50"> </div> <div class="control-group"> <label>Y Offset:</label> <input type="number" id="yOffset" value="0" min="-50" max="50"> </div> <div class="control-group"> <label>Zoom:</label> <input type="number" id="zoomLevel" value="2" min="1" max="8"> </div> <button class="btn" onclick="updateGrid()">Update Grid</button> </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 class="controls"> <button class="btn success" onclick="generateJSON()">Generate JSON</button> <button class="btn warning" onclick="clearSelection()">Clear Selection</button> <button class="btn" onclick="downloadTile()">Download Selected Tile</button> </div> </div> </div> <div class="canvas-container"> <canvas id="canvas"></canvas> <div class="tile-info" id="tileInfo"></div> </div> <div class="json-output" id="jsonOutput" style="display: none;"> <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> </div> </div> <script> let currentImage = null; let currentImagePath = ''; let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); let selectedTiles = new Set(); let lastSelectedTile = { x: -1, y: -1 }; let gridCols = 0; let gridRows = 0; // Load images from PHP script with enhanced debugging async function loadImages() { const imageList = document.getElementById('imageList'); try { console.log('Attempting to fetch from get_images.php...'); const response = await fetch('get_images.php'); console.log('Response status:', response.status); console.log('Response headers:', response.headers); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const responseText = await response.text(); console.log('Raw response:', responseText); let result; try { result = JSON.parse(responseText); } catch (parseError) { console.error('JSON parse error:', parseError); throw new Error(`Invalid JSON response: ${responseText.substring(0, 100)}...`); } console.log('Parsed result:', result); // Handle error response if (result.error) { imageList.innerHTML = ` <div class="no-images"> <strong>PHP 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><br> <button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug PHP</button> </div> `; return false; } // Handle array of images const images = Array.isArray(result) ? result : (result.images || []); console.log('Found images:', images); if (images.length === 0) { imageList.innerHTML = ` <div class="no-images"> No images found in the images folder<br> <small>PHP script working but no images detected</small><br> <button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug PHP</button> </div> `; return false; } imageList.innerHTML = ''; let successfulImages = 0; images.forEach((imagePath, index) => { console.log(`Processing image ${index + 1}/${images.length}:`, imagePath); 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(), imagePath.replace('images/', '../images/') ]; console.log('Trying paths for', imagePath, ':', possiblePaths); let pathIndex = 0; function tryNextPath() { if (pathIndex < possiblePaths.length) { const currentPath = possiblePaths[pathIndex]; console.log(`Trying path ${pathIndex + 1}/${possiblePaths.length}:`, currentPath); img.src = currentPath; pathIndex++; } else { console.error('All paths failed for:', imagePath); // All paths failed, show placeholder img.style.display = 'none'; const placeholder = document.createElement('div'); placeholder.style.cssText = ` width: 60px; height: 60px; background: #ffebee; border: 1px solid #f44336; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #c62828; margin-right: 10px; border-radius: 5px; `; placeholder.textContent = 'Failed'; placeholder.title = `Failed to load: ${imagePath}`; item.insertBefore(placeholder, img); } } img.onerror = function() { console.error('Failed to load:', img.src); tryNextPath(); }; img.onload = function() { console.log('Successfully loaded:', img.src); successfulImages++; }; const nameDiv = document.createElement('div'); nameDiv.className = 'image-name'; nameDiv.textContent = imagePath.split('/').pop(); nameDiv.title = imagePath; // Show full path on hover item.appendChild(img); item.appendChild(nameDiv); item.addEventListener('click', () => { document.querySelectorAll('.image-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); loadImageFromURL(img.src, imagePath); }); imageList.appendChild(item); // Start trying paths tryNextPath(); }); // Auto-load first image after a delay setTimeout(() => { console.log(`Loaded ${successfulImages}/${images.length} images successfully`); const firstWorkingImg = document.querySelector('.image-item img[src]:not([style*="display: none"])'); if (firstWorkingImg && firstWorkingImg.complete && firstWorkingImg.naturalWidth > 0) { firstWorkingImg.closest('.image-item').click(); console.log('Auto-loaded first working image'); } }, 2000); return true; } catch (error) { console.error('Error loading images:', error); imageList.innerHTML = ` <div class="no-images"> <strong>Connection Error:</strong> ${error.message}<br> <small>Cannot reach get_images.php</small><br> <button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug Connection</button> </div> `; return false; } } // Debug function to help troubleshoot PHP connection async function debugPHP() { console.log('Starting PHP debug...'); try { const response = await fetch('get_images.php?debug=1'); const result = await response.text(); console.log('Debug response:', result); // Show debug info in a popup const debugWindow = window.open('', 'debug', 'width=800,height=600'); debugWindow.document.write(` <html> <head><title>PHP Debug Info</title></head> <body style="font-family: monospace; padding: 20px;"> <h2>PHP Debug Response</h2> <pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow: auto;"> ${result} </pre> <br> <button onclick="window.close()">Close</button> </body> </html> `); } catch (error) { alert('Debug failed: ' + error.message); } } function loadImageFromURL(url, filename) { const img = new Image(); img.onload = function() { currentImage = img; currentImagePath = filename; document.getElementById('editorTitle').textContent = `Editing: ${filename}`; const zoom = parseInt(document.getElementById('zoomLevel').value); canvas.width = img.width * zoom; canvas.height = img.height * zoom; // Clear selection when loading new image selectedTiles.clear(); updateSelectedTilesList(); drawImage(); updateGrid(); }; img.src = url; } function drawImage() { if (!currentImage) return; const zoom = parseInt(document.getElementById('zoomLevel').value); ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the image scaled ctx.imageSmoothingEnabled = false; ctx.drawImage(currentImage, 0, 0, currentImage.width * zoom, currentImage.height * zoom); } function drawGrid() { 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); const zoom = parseInt(document.getElementById('zoomLevel').value); // Calculate grid dimensions properly accounting for spacing and offset const availableWidth = currentImage.width - xOffset; const availableHeight = currentImage.height - yOffset; gridCols = Math.floor(availableWidth / (tileWidth + hSpacing)); gridRows = Math.floor(availableHeight / (tileHeight + vSpacing)); // If there's no horizontal spacing, we can fit one more column potentially if (hSpacing === 0 && availableWidth % tileWidth !== 0) { gridCols = Math.floor(availableWidth / tileWidth); } if (vSpacing === 0 && availableHeight % tileHeight !== 0) { gridRows = Math.floor(availableHeight / tileHeight); } ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 1; // Draw grid with proper spacing and offset for (let row = 0; row < gridRows; row++) { for (let col = 0; col < gridCols; col++) { const x = (xOffset + col * (tileWidth + hSpacing)) * zoom; const y = (yOffset + row * (tileHeight + vSpacing)) * zoom; const w = tileWidth * zoom; const h = tileHeight * zoom; ctx.strokeRect(x, y, w, h); } } // Highlight selected tiles ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 2; selectedTiles.forEach(tileKey => { const [col, row] = tileKey.split(',').map(Number); const x = (xOffset + col * (tileWidth + hSpacing)) * zoom; const y = (yOffset + row * (tileHeight + vSpacing)) * zoom; const w = tileWidth * zoom; const h = tileHeight * zoom; ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); }); } function updateGrid() { drawImage(); drawGrid(); } 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(); } canvas.addEventListener('click', function(e) { if (!currentImage) return; const rect = canvas.getBoundingClientRect(); const canvasX = e.clientX - rect.left; const canvasY = e.clientY - rect.top; 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 zoom = parseInt(document.getElementById('zoomLevel').value); // Convert canvas coordinates to image coordinates const imageX = canvasX / zoom; const imageY = canvasY / zoom; // Adjust for offset const adjustedX = imageX - xOffset; const adjustedY = imageY - yOffset; // Check if click is within the grid area if (adjustedX < 0 || adjustedY < 0) return; // Calculate which tile was clicked const tileCol = Math.floor(adjustedX / (tileWidth + hSpacing)); const tileRow = Math.floor(adjustedY / (tileHeight + vSpacing)); // Check if the click is within valid grid bounds if (tileCol >= gridCols || tileRow >= gridRows || tileCol < 0 || tileRow < 0) { return; } // Check if click is within the actual tile area (not in spacing) const tileStartX = tileCol * (tileWidth + hSpacing); const tileStartY = tileRow * (tileHeight + vSpacing); const relativeX = adjustedX - tileStartX; const relativeY = adjustedY - tileStartY; if (relativeX >= tileWidth || relativeY >= tileHeight || relativeX < 0 || relativeY < 0) { return; // Click was in spacing area } const tileKey = `${tileCol},${tileRow}`; if (selectedTiles.has(tileKey)) { selectedTiles.delete(tileKey); } else { selectedTiles.add(tileKey); } lastSelectedTile.x = tileCol; lastSelectedTile.y = tileRow; updateSelectedTilesList(); updateGrid(); // Calculate actual pixel position in the original image const actualTileX = xOffset + tileCol * (tileWidth + hSpacing); const actualTileY = yOffset + tileRow * (tileHeight + vSpacing); const tileInfo = document.getElementById('tileInfo'); tileInfo.innerHTML = ` Tile: (${tileCol}, ${tileRow})<br> Position: (${actualTileX}px, ${actualTileY}px)<br> Size: ${tileWidth}×${tileHeight}px<br> Spacing: H${hSpacing}px V${vSpacing}px<br> Status: ${selectedTiles.has(tileKey) ? 'Selected' : 'Unselected'} `; tileInfo.style.display = 'block'; }); canvas.addEventListener('mousemove', function(e) { if (!currentImage) return; const rect = canvas.getBoundingClientRect(); const canvasX = e.clientX - rect.left; const canvasY = e.clientY - rect.top; const zoom = parseInt(document.getElementById('zoomLevel').value); 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); // Convert to image coordinates const imageX = canvasX / zoom; const imageY = canvasY / zoom; const adjustedX = imageX - xOffset; const adjustedY = imageY - yOffset; if (adjustedX >= 0 && adjustedY >= 0) { const tileCol = Math.floor(adjustedX / (tileWidth + hSpacing)); const tileRow = Math.floor(adjustedY / (tileHeight + vSpacing)); if (tileCol < gridCols && tileRow < gridRows && tileCol >= 0 && tileRow >= 0) { // Check if we're in the tile area, not spacing const tileStartX = tileCol * (tileWidth + hSpacing); const tileStartY = tileRow * (tileHeight + vSpacing); const relativeX = adjustedX - tileStartX; const relativeY = adjustedY - tileStartY; if (relativeX < tileWidth && relativeY < tileHeight && relativeX >= 0 && relativeY >= 0) { canvas.style.cursor = 'pointer'; } else { canvas.style.cursor = 'crosshair'; } } else { canvas.style.cursor = 'crosshair'; } } else { canvas.style.cursor = 'crosshair'; } }); canvas.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: currentImagePath, image_dimensions: { width: currentImage.width, height: currentImage.height