๐ŸŒ
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 - Dark UI</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { margin: 0; font-family: system-ui, sans-serif; background: #121212; color: #eee; height: 100vh; display: flex; flex-direction: column; } /* Top bar */ .topbar { display: flex; justify-content: space-around; padding: 0.5rem 0; background: #1e1e1e; box-shadow: 0 2px 8px rgba(0,0,0,0.5); font-size: 1.5rem; z-index: 10; } .topbar button { flex: 1; background: none; border: none; font-size: 1.6rem; cursor: pointer; padding: 0.8rem 0; color: #eee; transition: background 0.2s, color 0.2s; } .topbar button:hover { background: #2c2c2c; color: #4fc3f7; } /* Slide-down panels */ .panel { max-height: 0; overflow: hidden; background: #1b1b1b; transition: max-height 0.4s ease; } .panel.open { max-height: 260px; } /* Gallery */ .gallery-track { display: flex; gap: 0.75rem; overflow-x: auto; padding: 0.75rem 1rem; } .gallery-track::-webkit-scrollbar { height: 8px; } .gallery-track::-webkit-scrollbar-track { background: #2c2c2c; border-radius: 4px; } .gallery-track::-webkit-scrollbar-thumb { background: #4fc3f7; border-radius: 4px; } .image-item { display: flex; flex-direction: column; align-items: center; padding: 8px; background: #2c2c2c; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; min-width: 120px; flex-shrink: 0; } .image-item:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(79, 195, 247, 0.3); border-color: #4fc3f7; } .image-item.active { border-color: #4fc3f7; background: #333; } .image-item img { width: 100px; height: 70px; object-fit: contain; border-radius: 8px; margin-bottom: 8px; background: #1e1e1e; border: 1px solid #333; } .image-name { font-weight: 500; color: #eee; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; max-width: 100px; text-align: center; } /* Settings row */ .settings-track { display: flex; gap: 1rem; overflow-x: auto; padding: 0.5rem 1rem; } .settings-track::-webkit-scrollbar { height: 8px; } .settings-track::-webkit-scrollbar-track { background: #2c2c2c; border-radius: 4px; } .settings-track::-webkit-scrollbar-thumb { background: #4fc3f7; border-radius: 4px; } .stepper { flex: 0 0 auto; background: #2c2c2c; border-radius: 0.6rem; padding: 0.5rem; text-align: center; color: #eee; min-width: 110px; } .stepper label { display: block; font-size: 0.8rem; margin-bottom: 0.3rem; color: #bbb; } .stepper-controls { display: flex; align-items: center; justify-content: space-between; background: #1e1e1e; border-radius: 0.5rem; overflow: hidden; } .stepper button { flex: 0 0 30%; background: #333; border: none; color: #eee; font-size: 1.2rem; cursor: pointer; padding: 0.3rem 0; transition: background 0.2s; } .stepper button:hover { background: #4fc3f7; color: #000; } .stepper input { flex: 1; background: #1e1e1e; border: none; color: #eee; text-align: center; font-size: 1rem; width: 40px; outline: none; } /* Action panels */ .actions-track { display: flex; flex-wrap: wrap; gap: 0.8rem; padding: 0.8rem 1rem; justify-content: center; } .actions-track button { background: #2c2c2c; border: none; border-radius: 0.5rem; padding: 0.6rem 1rem; font-size: 0.9rem; color: #eee; cursor: pointer; transition: background 0.2s, color 0.2s; } .actions-track button:hover { background: #4fc3f7; color: #000; } /* Content area */ .content { flex: 1; background: linear-gradient(135deg, #212121, #2c2c2c); padding: 1.5rem; border-top-left-radius: 1rem; border-top-right-radius: 1rem; overflow: auto; display: flex; flex-direction: column; } .editor-header { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #333; } .editor-title { font-size: 1.3rem; color: #4fc3f7; 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: #1e1e1e; border-radius: 8px; border: 2px dashed #333; } .tile-tag { background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%); color: #000; padding: 4px 8px; border-radius: 4px; font-size: 12px; display: flex; align-items: center; gap: 5px; font-weight: 500; } .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: #1e1e1e; border-radius: 10px; padding: 20px; position: relative; overflow: auto; min-height: 400px; touch-action: none; /* Prevent default touch behaviors */ } .tile-grid-container { position: relative; display: inline-block; border: 2px solid #333; border-radius: 8px; background: #000; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); image-rendering: pixelated; image-rendering: crisp-edges; transform-origin: top left; /* Ensure zoom originates from top-left */ } .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 #FF0000; /* Bright red */ border-bottom: 1px solid #FF0000; /* Bright red */ box-sizing: border-box; } .tile-cell:nth-child(-n + var(--grid-cols)) { border-top: 1px solid #FF0000; /* Bright red */ } .tile-cell:nth-child(var(--grid-cols) + 1) ~ .tile-cell { border-top: 1px solid #FF0000; /* Bright red */ } .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); /* Bright red with transparency */ outline: 2px solid #FF0000; /* Bright red outline */ box-sizing: border-box; } .tile-info { position: absolute; top: 10px; right: 10px; background: rgba(0, 0, 0, 0.9); color: #4fc3f7; padding: 10px; border-radius: 5px; font-size: 12px; display: none; z-index: 10; border: 1px solid #333; } /* Modal styles */ .modal { 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); } .modal-content { position: relative; max-width: 90vw; max-height: 90vh; background: #1e1e1e; border-radius: 15px; padding: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); border: 1px solid #333; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #333; } .modal-title { font-size: 1.2rem; font-weight: 600; color: #4fc3f7; } .modal-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; } .modal-close:hover { background: #c82333; } .json-output pre { background: #121212; padding: 15px; border-radius: 5px; border: 1px solid #333; font-size: 12px; overflow: auto; max-height: 60vh; margin-bottom: 15px; color: #eee; } .loading { text-align: center; padding: 40px; color: #666; } .loading::before { content: ''; display: inline-block; width: 30px; height: 30px; border: 3px solid #333; border-top: 3px solid #4fc3f7; 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; } /* Preview canvas styles */ .preview-canvas-container { position: relative; display: flex; justify-content: center; align-items: center; max-height: 70vh; overflow: auto; border-radius: 8px; background: #121212; } .preview-canvas { border: 2px solid #333; border-radius: 5px; background: #000; image-rendering: pixelated; image-rendering: crisp-edges; } .preview-info { margin-top: 10px; padding: 10px; background: #121212; border-radius: 8px; font-size: 12px; color: #666; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; } /* File input styling */ input[type="file"] { display: none; } /* Mobile responsive */ @media (max-width: 768px) { .topbar { font-size: 1.2rem; } .topbar button { padding: 0.6rem 0; } .content { padding: 1rem; } .canvas-container { padding: 10px; min-height: 300px; } .tile-grid-container { max-width: calc(100vw - 60px); max-height: calc(100vh - 400px); } } </style> </head> <body> <div class="topbar"> <button id="settingsBtn" title="Settings">โš™๏ธ</button> <button id="imagesBtn" title="Images">๐Ÿ–ผ๏ธ</button> <button id="saveBtn" title="Save">๐Ÿ’พ</button> <button id="refreshBtn" title="Refresh" onclick="updateGrid()">๐Ÿ”„</button> <button id="moreBtn" title="More">โ‹ฎ</button> </div> <!-- Settings panel --> <div class="panel" id="settingsPanel"> <div class="settings-track"> <div class="stepper"> <label>Tile Width</label> <div class="stepper-controls"> <button onclick="changeValue('tileWidth', -8)">-</button> <input type="number" id="tileWidth" value="32" min="8" step="8"> <button onclick="changeValue('tileWidth', 8)">+</button> </div> </div> <div class="stepper"> <label>Tile Height</label> <div class="stepper-controls"> <button onclick="changeValue('tileHeight', -8)">-</button> <input type="number" id="tileHeight" value="32" min="8" step="8"> <button onclick="changeValue('tileHeight', 8)">+</button> </div> </div> <div class="stepper"> <label>X Offset</label> <div class="stepper-controls"> <button onclick="changeValue('xOffset', -1)">-</button> <input type="number" id="xOffset" value="0" min="-50" max="50"> <button onclick="changeValue('xOffset', 1)">+</button> </div> </div> <div class="stepper"> <label>Y Offset</label> <div class="stepper-controls"> <button onclick="changeValue('yOffset', -1)">-</button> <input type="number" id="yOffset" value="0" min="-50" max="50"> <button onclick="changeValue('yOffset', 1)">+</button> </div> </div> <div class="stepper"> <label>V Spacing</label> <div class="stepper-controls"> <button onclick="changeValue('verticalSpacing', -1)">-</button> <input type="number" id="verticalSpacing" value="0" min="0" max="50"> <button onclick="changeValue('verticalSpacing', 1)">+</button> </div> </div> <div class="stepper"> <label>H Spacing</label> <div class="stepper-controls"> <button onclick="changeValue('horizontalSpacing', -1)">-</button> <input type="number" id="horizontalSpacing" value="0" min="0" max="50"> <button onclick="changeValue('horizontalSpacing', 1)">+</button> </div> </div> </div> </div> <!-- Gallery panel --> <div class="panel" id="gallery"> <div class="gallery-track" id="imageGallery"> <div class="loading">Loading images...</div> </div> </div> <!-- Save actions panel --> <div class="panel" id="savePanel"> <div class="actions-track"> <input type="file" id="fileInput" accept="image/*" multiple> <button onclick="document.getElementById('fileInput').click()">๐Ÿ“ Add Images</button> <button onclick="generateJSON()">๐Ÿ“„ Generate JSON</button> <button onclick="downloadTile()">๐Ÿ’พ Download Tile</button> <button onclick="downloadJSON()">๐Ÿ’พ Download JSON</button> </div> </div> <!-- More actions panel --> <div class="panel" id="actionsPanel"> <div class="actions-track"> <button onclick="clearSelection()">๐Ÿงน Clear Selection</button> <button onclick="copyJSON()">๐Ÿ“‹ Copy JSON</button> <button onclick="showImagePreview(currentImagePath)">๐Ÿ” Preview Image</button> </div> </div> <div class="content"> <div class="editor-header"> <div class="editor-title" id="editorTitle">Tile Sheet Editor - Select an image to start</div> <div class="selected-tiles-section"> <h4 style="color: #4fc3f7; margin-bottom: 8px;">Selected Tiles:</h4> <div class="selected-tiles-list" id="selectedTilesList"> <span style="color: #666; font-style: italic;">Click tiles to select them...</span> </div> <div id="uploadStatus" style="font-size: 12px; color: #666; margin-top: 8px;"></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="modal" id="imagePreview"> <div class="modal-content"> <div class="modal-header"> <div class="modal-title" id="previewTitle">Image Preview</div> <button class="modal-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="modal" id="jsonOutput"> <div class="modal-content"> <div class="modal-header"> <div class="modal-title">Generated JSON</div> <button class="modal-close" onclick="closeJSONOutput()">ร—</button> </div> <pre id="jsonContent"></pre> <div class="actions-track"> <button onclick="copyJSON()">๐Ÿ“‹ Copy to Clipboard</button> <button onclick="downloadJSON()">๐Ÿ’พ Download JSON</button> </div> </div> </div> <script> // UI State Management const settingsBtn = document.getElementById('settingsBtn'); const imagesBtn = document.getElementById('imagesBtn'); const saveBtn = document.getElementById('saveBtn'); const moreBtn = document.getElementById('moreBtn'); const settingsPanel = document.getElementById('settingsPanel'); const gallery = document.getElementById('gallery'); const savePanel = document.getElementById('savePanel'); const actionsPanel = document.getElementById('actionsPanel'); function closeAll() { settingsPanel.classList.remove('open'); gallery.classList.remove('open'); savePanel.classList.remove('open'); actionsPanel.classList.remove('open'); } settingsBtn.addEventListener('click', () => { const isOpen = settingsPanel.classList.contains('open'); closeAll(); if (!isOpen) settingsPanel.classList.add('open'); }); imagesBtn.addEventListener('click', () => { const isOpen = gallery.classList.contains('open'); closeAll(); if (!isOpen) gallery.classList.add('open'); }); saveBtn.addEventListener('click', () => { const isOpen = savePanel.classList.contains('open'); closeAll(); if (!isOpen) savePanel.classList.add('open'); }); moreBtn.addEventListener('click', () => { const isOpen = actionsPanel.classList.contains('open'); closeAll(); if (!isOpen) actionsPanel.classList.add('open'); }); // Tile Editor Core Variables let currentImage = null; let currentImagePath = ''; let selectedTiles = new Set(); let lastSelectedTile = { x: -1, y: -1 }; let gridCols = 0; let gridRows = 0; // Zoom and Pan Variables for tile-grid-container let gridZoom = 1; let gridOffsetX = 0; let gridOffsetY = 0; let isGridDragging = false; let gridLastMouseX = 0; let gridLastMouseY = 0; const tileGridContainer = document.getElementById('tileGridContainer'); // Initialize zoom and pan function initGridZoomPan() { tileGridContainer.style.transform = `scale(${gridZoom}) translate(${gridOffsetX}px, ${gridOffsetY}px)`; // Mouse wheel for zooming tileGridContainer.addEventListener('wheel', function(e) { e.preventDefault(); const rect = tileGridContainer.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.5, Math.min(5, gridZoom * zoomFactor)); // Limit zoom: 0.5x to 5x // Adjust offsets to zoom towards mouse position gridOffsetX = mouseX / gridZoom - (mouseX / newZoom - gridOffsetX); gridOffsetY = mouseY / gridZoom - (mouseY / newZoom - gridOffsetY); gridZoom = newZoom; updateGridTransform(); }); // Mouse drag for panning tileGridContainer.addEventListener('mousedown', function(e) { if (e.button === 2) return; // Ignore right-click isGridDragging = true; gridLastMouseX = e.clientX; gridLastMouseY = e.clientY; tileGridContainer.style.cursor = 'grabbing'; }); tileGridContainer.addEventListener('mousemove', function(e) { if (isGridDragging) { const deltaX = (e.clientX - gridLastMouseX) / gridZoom; const deltaY = (e.clientY - gridLastMouseY) / gridZoom; gridOffsetX += deltaX; gridOffsetY += deltaY; gridLastMouseX = e.clientX; gridLastMouseY = e.clientY; updateGridTransform(); } }); tileGridContainer.addEventListener('mouseup', function() { isGridDragging = false; tileGridContainer.style.cursor = 'crosshair'; }); tileGridContainer.addEventListener('mouseleave', function() { isGridDragging = false; tileGridContainer.style.cursor = 'crosshair'; }); // Touch events for pinch-to-zoom and panning let initialDistance = 0; let initialZoom = 1; let initialOffsetX = 0; let initialOffsetY = 0; tileGridContainer.addEventListener('touchstart', function(e) { e.preventDefault(); if (e.touches.length === 1) { isGridDragging = true; gridLastMouseX = e.touches[0].clientX; gridLastMouseY = e.touches[0].clientY; } else if (e.touches.length === 2) { isGridDragging = false; const touch1 = e.touches[0]; const touch2 = e.touches[1]; initialDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY); initialZoom = gridZoom; initialOffsetX = gridOffsetX; initialOffsetY = gridOffsetY; } }); tileGridContainer.addEventListener('touchmove', function(e) { e.preventDefault(); if (e.touches.length === 1 && isGridDragging) { const deltaX = (e.touches[0].clientX - gridLastMouseX) / gridZoom; const deltaY = (e.touches[0].clientY - gridLastMouseY) / gridZoom; gridOffsetX += deltaX; gridOffsetY += deltaY; gridLastMouseX = e.touches[0].clientX; gridLastMouseY = e.touches[0].clientY; updateGridTransform(); } else if (e.touches.length === 2) { const touch1 = e.touches[0]; const touch2 = e.touches[1]; const currentDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY); const zoomFactor = currentDistance / initialDistance; const newZoom = Math.max(0.5, Math.min(5, initialZoom * zoomFactor)); // Adjust offsets to zoom towards the midpoint of the pinch const midX = (touch1.clientX + touch2.clientX) / 2 - tileGridContainer.getBoundingClientRect().left; const midY = (touch1.clientY + touch2.clientY) / 2 - tileGridContainer.getBoundingClientRect().top; gridOffsetX = midX / initialZoom - (midX / newZoom - initialOffsetX); gridOffsetY = midY / initialZoom - (midY / newZoom - initialOffsetY); gridZoom = newZoom; updateGridTransform(); } }); tileGridContainer.addEventListener('touchend', function(e) { if (e.touches.length < 2) { isGridDragging = false; tileGridContainer.style.cursor = 'crosshair'; } }); } function updateGridTransform() { tileGridContainer.style.transform = `scale(${gridZoom}) translate(${gridOffsetX}px, ${gridOffsetY}px)`; } // 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: 100px; height: 70px; background: #333; border: 1px solid #555; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #666; margin-bottom: 8px; border-radius: 8px; `; 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'; // Clear selection when loading new image selectedTiles.clear(); updateSelectedTilesList(); // Reset zoom and pan gridZoom = 1; gridOffsetX = 0; gridOffsetY = 0; updateGridTransform(); 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); // 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 (hSpacing === 0) { gridCols = Math.floor((currentImage.width - xOffset) / tileWidth); } if (vSpacing === 0) { gridRows = Math.floor((currentImage.height - yOffset) / tileHeight); } // Calculate total tiled area 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`; overlay.style.setProperty('--grid-cols', gridCols); overlay.style.setProperty('--grid-rows', gridRows); // 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}`; if (selectedTiles.has(`${col},${row}`)) { cell.classList.add('selected'); } cell.addEventListener('click', function(e) { e.stopPropagation(); // Adjust click coordinates for zoom and pan const rect = tileGridContainer.getBoundingClientRect(); const clickX = (e.clientX - rect.left - gridOffsetX * gridZoom) / gridZoom; const clickY = (e.clientY - rect.top - gridOffsetY * gridZoom) / gridZoom; const adjustedCol = Math.floor((clickX - xOffset) / (tileWidth + hSpacing)); const adjustedRow = Math.floor((clickY - yOffset) / (tileHeight + vSpacing)); if (adjustedCol >= 0 && adjustedCol < gridCols && adjustedRow >= 0 && adjustedRow < gridRows) { toggleTile(adjustedCol, adjustedRow, `${adjustedCol},${adjustedRow}`); } }); overlay.appendChild(cell); } } // Update transform to maintain zoom and pan updateGridTransform(); } function toggleTile(col, row, tileKey) { if (selectedTiles.has(tileKey)) { selectedTiles.delete(tileKey); } else { selectedTiles.add(tileKey); } lastSelectedTile.x = col; lastSelectedTile.y = row; updateSelectedTilesList(); updateGrid(); // 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> 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: #666; 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(); closeAll(); } 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 = 'flex'; closeAll(); } 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 closeJSONOutput() { document.getElementById('jsonOutput').style.display = 'none'; } 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); const actualTileX = xOffset + lastSelectedTile.x * (tileWidth + hSpacing); const actualTileY = yOffset + lastSelectedTile.y * (tileHeight + vSpacing); const tileCanvas = document.createElement('canvas'); const tileCtx = tileCanvas.getContext('2d'); tileCanvas.width = tileWidth; tileCanvas.height = tileHeight; tileCtx.drawImage( currentImage, actualTileX, actualTileY, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight ); const link = document.createElement('a'); link.download = `tile_${lastSelectedTile.x}_${lastSelectedTile.y}.png`; link.href = tileCanvas.toDataURL(); link.click(); closeAll(); } // 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) { if (!imagePath) return; const img = new Image(); img.onload = function() { previewImage = img; previewZoom = 1; previewOffsetX = 0; previewOffsetY = 0; const maxWidth = window.innerWidth * 0.7; const maxHeight = window.innerHeight * 0.6; let displayWidth = img.width; let displayHeight = img.height; 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; 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 and drag to pan โ€ข Scroll to zoom</span> `; drawPreview(); document.getElementById('imagePreview').style.display = 'flex'; closeAll(); }; img.onerror = function() { alert('Failed to load preview for: ' + imagePath.split('/').pop()); }; img.src = imagePath; } function drawPreview() { if (!previewImage) return; previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); previewCtx.save(); previewCtx.translate(previewOffsetX, previewOffsetY); previewCtx.scale(previewZoom, previewZoom); previewCtx.imageSmoothingEnabled = false; previewCtx.drawImage(previewImage, 0, 0); previewCtx.restore(); } function closeImagePreview() { document.getElementById('imagePreview').style.display = 'none'; previewImage = null; } // Preview canvas events 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)); previewOffsetX = mouseX - (mouseX - previewOffsetX) * (newZoom / previewZoom); previewOffsetY = mouseY - (mouseY - previewOffsetY) * (newZoom / previewZoom); previewZoom = newZoom; document.getElementById('previewInfo').innerHTML = ` <span>Dimensions: ${previewImage.width} ร— ${previewImage.height}px</span> <span>Zoom: ${Math.round(previewZoom * 100)}%</span> <span>Click and drag to pan โ€ข Scroll to zoom</span> `; drawPreview(); }); previewCanvas.addEventListener('mousedown', function(e) { if (e.button === 2) { 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'; // File upload handling 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 = '#4fc3f7'; 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 = '#4fc3f7'; setTimeout(() => { loadImages(); uploadStatus.textContent = ''; }, 2000); } else { uploadStatus.textContent = `โŒ Upload failed: ${result.error}`; uploadStatus.style.color = '#f44336'; } } catch (error) { uploadStatus.textContent = 'โŒ Upload failed: Network error'; uploadStatus.style.color = '#f44336'; } document.getElementById('fileInput').value = ''; } // Event listeners document.getElementById('tileGridContainer').addEventListener('mouseleave', function() { document.getElementById('tileInfo').style.display = 'none'; }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeImagePreview(); closeJSONOutput(); closeAll(); } }); document.getElementById('imagePreview').addEventListener('click', function(e) { if (e.target === this) closeImagePreview(); }); document.getElementById('jsonOutput').addEventListener('click', function(e) { if (e.target === this) closeJSONOutput(); }); // Input event listeners 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); // Initialize loadImages(); initGridZoomPan(); </script> </body> </html>