🌐
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; } .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; } .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 Tile Sheet(s) </button> <div id="uploadStatus" style="font-size: 12px; color: #666; text-align: center;"></div> </div> <div class="image-list" id="imageList"> <div class="loading">Loading images...</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> <!-- 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> <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 async function loadImages() { try { const response = await fetch('get_images.php'); const result = await response.json(); const imageList = document.getElementById('imageList'); // Handle error response if (result.error) { imageList.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) { imageList.innerHTML = '<div class="no-images">No images found in the images folder</div>'; return; } imageList.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: 60px; height: 60px; background: #f0f0f0; border: 1px solid #ddd; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #666; margin-right: 10px; 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); }); imageList.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('imageList').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 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.onerror = function() { console.error('Failed to load image:', imagePath); alert('Failed to load image: ' + imagePath); }; img.src = imagePath; } 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 zoom = parseInt(document.getElementById('zoomLevel').value); const scaledTileWidth = tileWidth * zoom; const scaledTileHeight = tileHeight * zoom; gridCols = Math.floor(currentImage.width / tileWidth); gridRows = Math.floor(currentImage.height / tileHeight); ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 1; // Draw vertical lines for (let x = 0; x <= gridCols; x++) { ctx.beginPath(); ctx.moveTo(x * scaledTileWidth, 0); ctx.lineTo(x * scaledTileWidth, gridRows * scaledTileHeight); ctx.stroke(); } // Draw horizontal lines for (let y = 0; y <= gridRows; y++) { ctx.beginPath(); ctx.moveTo(0, y * scaledTileHeight); ctx.lineTo(gridCols * scaledTileWidth, y * scaledTileHeight); ctx.stroke(); } // Highlight selected tiles ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 2; selectedTiles.forEach(tileKey => { const [x, y] = tileKey.split(',').map(Number); ctx.fillRect( x * scaledTileWidth, y * scaledTileHeight, scaledTileWidth, scaledTileHeight ); ctx.strokeRect( x * scaledTileWidth, y * scaledTileHeight, scaledTileWidth, scaledTileHeight ); }); } 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 x = e.clientX - rect.left; const y = 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); const scaledTileWidth = tileWidth * zoom; const scaledTileHeight = tileHeight * zoom; const scaledHSpacing = hSpacing * zoom; const scaledVSpacing = vSpacing * zoom; const scaledXOffset = xOffset * zoom; const scaledYOffset = yOffset * zoom; // Adjust click position by offset const adjustedX = x - scaledXOffset; const adjustedY = y - scaledYOffset; // Calculate which tile was clicked with spacing const effectiveWidth = scaledTileWidth + scaledHSpacing; const effectiveHeight = scaledTileHeight + scaledVSpacing; const tileX = Math.floor(adjustedX / effectiveWidth); const tileY = Math.floor(adjustedY / effectiveHeight); // Check if click is within a tile (not in spacing area) const withinTileX = (adjustedX % effectiveWidth) < scaledTileWidth; const withinTileY = (adjustedY % effectiveHeight) < scaledTileHeight; if (tileX >= gridCols || tileY >= gridRows || tileX < 0 || tileY < 0 || !withinTileX || !withinTileY) { return; } const tileKey = `${tileX},${tileY}`; if (selectedTiles.has(tileKey)) { selectedTiles.delete(tileKey); } else { selectedTiles.add(tileKey); } lastSelectedTile.x = tileX; lastSelectedTile.y = tileY; updateSelectedTilesList(); updateGrid(); const actualTileX = xOffset + tileX * (tileWidth + hSpacing); const actualTileY = yOffset + tileY * (tileHeight + vSpacing); const tileInfo = document.getElementById('tileInfo'); tileInfo.innerHTML = ` Tile: (${tileX}, ${tileY})<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 x = e.clientX - rect.left; const y = e.clientY - rect.top; const tileWidth = parseInt(document.getElementById('tileWidth').value); const tileHeight = parseInt(document.getElementById('tileHeight').value); const zoom = parseInt(document.getElementById('zoomLevel').value); const scaledTileWidth = tileWidth * zoom; const scaledTileHeight = tileHeight * zoom; const tileX = Math.floor(x / scaledTileWidth); const tileY = Math.floor(y / scaledTileHeight); if (tileX < gridCols && tileY < gridRows && tileX >= 0 && tileY >= 0) { canvas.style.cursor = 'pointer'; } 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: `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'; // Scroll to JSON output document.getElementById('jsonOutput').scrollIntoView({ behavior: 'smooth' }); } 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); // 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 tileCtx.drawImage( currentImage, lastSelectedTile.x * tileWidth, lastSelectedTile.y * tileHeight, 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(); } // Event listeners for controls document.getElementById('tileWidth').addEventListener('change', updateGrid); document.getElementById('tileHeight').addEventListener('change', 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); document.getElementById('zoomLevel').addEventListener('change', function() { if (currentImage) { const zoom = parseInt(this.value); canvas.width = currentImage.width * zoom; canvas.height = currentImage.height * zoom; updateGrid(); } }); // 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(); } }); // 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>