🌐
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; } } /* Number stepper styles */ .number-stepper { display: flex; align-items: center; border: 2px solid #ddd; border-radius: 5px; overflow: hidden; background: white; transition: border-color 0.3s ease; } .number-stepper:focus-within { border-color: #667eea; } .number-stepper input { width: 60px; padding: 8px 4px; border: none; font-size: 14px; text-align: center; background: transparent; } .number-stepper input:focus { outline: none; } .stepper-btn { width: 24px; height: 32px; border: none; background: #f8f9fa; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; transition: background-color 0.2s ease; user-select: none; } .stepper-btn:hover { background: #e9ecef; } .stepper-btn:active { background: #dee2e6; } @media (max-width: 768px) { .number-stepper input { width: 50px; padding: 6px 2px; font-size: 13px; } .stepper-btn { width: 20px; height: 28px; font-size: 11px; } } .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: hidden; touch-action: none; } @media (max-width: 768px) { .canvas-container { padding: 10px; min-height: 400px; } } .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; transform-origin: center center; transition: transform 0.1s ease-out; } .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; } .zoom-controls { position: absolute; top: 10px; left: 10px; display: flex; flex-direction: column; gap: 5px; z-index: 10; } .zoom-btn { width: 40px; height: 40px; border: none; border-radius: 8px; background: rgba(255, 255, 255, 0.9); cursor: pointer; font-size: 18px; font-weight: bold; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .zoom-btn:hover { background: white; transform: scale(1.1); } .zoom-indicator { position: absolute; bottom: 10px; left: 10px; background: rgba(0, 0, 0, 0.8); color: white; padding: 8px 12px; border-radius: 5px; font-size: 12px; z-index: 10; } .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 { 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; } @media (max-width: 768px) { .tile-grid-container { max-width: calc(100vw - 60px); max-height: calc(100vh - 400px); } } </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> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('tileWidth', -8)">-</button> <input type="number" id="tileWidth" value="32" min="8" step="8" onchange="validateAndUpdate(this, 8)"> <button class="stepper-btn" onclick="adjustValue('tileWidth', 8)">+</button> </div> </div> <div class="control-group"> <label>Tile Height:</label> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('tileHeight', -8)">-</button> <input type="number" id="tileHeight" value="32" min="8" step="8" onchange="validateAndUpdate(this, 8)"> <button class="stepper-btn" onclick="adjustValue('tileHeight', 8)">+</button> </div> </div> <div class="control-group"> <label>H Spacing:</label> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('horizontalSpacing', -1)">-</button> <input type="number" id="horizontalSpacing" value="0" min="0" max="50" onchange="updateGrid()"> <button class="stepper-btn" onclick="adjustValue('horizontalSpacing', 1)">+</button> </div> </div> <div class="control-group"> <label>V Spacing:</label> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('verticalSpacing', -1)">-</button> <input type="number" id="verticalSpacing" value="0" min="0" max="50" onchange="updateGrid()"> <button class="stepper-btn" onclick="adjustValue('verticalSpacing', 1)">+</button> </div> </div> <div class="control-group"> <label>X Offset:</label> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('xOffset', -1)">-</button> <input type="number" id="xOffset" value="0" min="-50" max="50" onchange="updateGrid()"> <button class="stepper-btn" onclick="adjustValue('xOffset', 1)">+</button> </div> </div> <div class="control-group"> <label>Y Offset:</label> <div class="number-stepper"> <button class="stepper-btn" onclick="adjustValue('yOffset', -1)">-</button> <input type="number" id="yOffset" value="0" min="-50" max="50" onchange="updateGrid()"> <button class="stepper-btn" onclick="adjustValue('yOffset', 1)">+</button> </div> </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" id="canvasContainer"> <div class="zoom-controls"> <button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button> <button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">-</button> <button class="zoom-btn" onclick="resetZoom()" title="Reset Zoom">⌂</button> </div> <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="zoom-indicator" id="zoomIndicator">100%</div> <div class="tile-info" id="tileInfo"></div> </div> <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 selectedTiles = new Set(); let lastSelectedTile = { x: -1, y: -1 }; let gridCols = 0; let gridRows = 0; // Zoom and pan variables let currentZoom = 1; let panX = 0; let panY = 0; let isPanning = false; let lastPanX = 0; let lastPanY = 0; // Touch handling let lastTouchDistance = 0; let lastTouchCenter = { x: 0, y: 0 }; // Number stepper functions function adjustValue(elementId, increment) { const input = document.getElementById(elementId); const currentValue = parseInt(input.value) || 0; const min = parseInt(input.min) || -Infinity; const max = parseInt(input.max) || Infinity; let newValue = currentValue + increment; // Ensure within bounds newValue = Math.max(min, Math.min(max, newValue)); // Special handling for tile dimensions (must be multiples of 8) if (elementId === 'tileWidth' || elementId === 'tileHeight') { newValue = Math.max(8, Math.round(newValue / 8) * 8); } input.value = newValue; updateGrid(); } function validateAndUpdate(input, multipleOf = 1) { let value = parseInt(input.value) || 0; const min = parseInt(input.min) || -Infinity; const max = parseInt(input.max) || Infinity; // Ensure within bounds value = Math.max(min, Math.min(max, value)); // Ensure multiple of specified value if (multipleOf > 1) { value = Math.max(multipleOf, Math.round(value / multipleOf) * multipleOf); } input.value = value; updateGrid(); } // Zoom functions function zoomIn() { setZoom(currentZoom * 1.2); } function zoomOut() { setZoom(currentZoom / 1.2); } function resetZoom() { setZoom(1); panX = 0; panY = 0; updateTransform(); } function setZoom(newZoom) { const minZoom = 0.1; const maxZoom = 5; currentZoom = Math.max(minZoom, Math.min(maxZoom, newZoom)); updateZoomIndicator(); updateTransform(); } function updateZoomIndicator() { document.getElementById('zoomIndicator').textContent = Math.round(currentZoom * 100) + '%'; } function updateTransform() { const container = document.getElementById('tileGridContainer'); container.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`; } // Touch event handlers for pinch zoom and pan function setupTouchEvents() { const canvasContainer = document.getElementById('canvasContainer'); // Mouse events for desktop canvasContainer.addEventListener('wheel', handleWheel, { passive: false }); canvasContainer.addEventListener('mousedown', handleMouseDown); canvasContainer.addEventListener('mousemove', handleMouseMove); canvasContainer.addEventListener('mouseup', handleMouseUp); canvasContainer.addEventListener('mouseleave', handleMouseUp); // Touch events for mobile canvasContainer.addEventListener('touchstart', handleTouchStart, { passive: false }); canvasContainer.addEventListener('touchmove', handleTouchMove, { passive: false }); canvasContainer.addEventListener('touchend', handleTouchEnd, { passive: false }); } function handleWheel(e) { e.preventDefault(); const rect = e.target.getBoundingClientRect(); const centerX = rect.width / 2; const centerY = rect.height / 2; const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = currentZoom * zoomFactor; // Zoom towards cursor position const beforeZoomX = (centerX - panX) / currentZoom; const beforeZoomY = (centerY - panY) / currentZoom; setZoom(newZoom); const afterZoomX = (centerX - panX) / currentZoom; const afterZoomY = (centerY - panY) / currentZoom; panX += (afterZoomX - beforeZoomX) * currentZoom; panY += (afterZoomY - beforeZoomY) * currentZoom; updateTransform(); } function handleMouseDown(e) { if (e.button === 0) { // Left mouse button isPanning = true; lastPanX = e.clientX; lastPanY = e.clientY; e.target.style.cursor = 'grabbing'; } } function handleMouseMove(e) { if (isPanning) { const deltaX = e.clientX - lastPanX; const deltaY = e.clientY - lastPanY; panX += deltaX; panY += deltaY; lastPanX = e.clientX; lastPanY = e.clientY; updateTransform(); } } function handleMouseUp(e) { isPanning = false; e.target.style.cursor = 'grab'; } function handleTouchStart(e) { e.preventDefault(); if (e.touches.length === 1) { // Single touch - start panning isPanning = true; lastPanX = e.touches[0].clientX; lastPanY = e.touches[0].clientY; } else if (e.touches.length === 2) { // Two touches - start pinch zoom isPanning = false; const touch1 = e.touches[0]; const touch2 = e.touches[1]; lastTouchDistance = Math.sqrt( Math.pow(touch2.clientX - touch1.clientX, 2) + Math.pow(touch2.clientY - touch1.clientY, 2) ); lastTouchCenter = { x: (touch1.clientX + touch2.clientX) / 2, y: (touch1.clientY + touch2.clientY) / 2 }; } } function handleTouchMove(e) { e.preventDefault(); if (e.touches.length === 1 && isPanning) { // Single touch - pan const deltaX = e.touches[0].clientX - lastPanX; const deltaY = e.touches[0].clientY - lastPanY; panX += deltaX; panY += deltaY; lastPanX = e.touches[0].clientX; lastPanY = e.touches[0].clientY; updateTransform(); } else if (e.touches.length === 2) { // Two touches - pinch zoom const touch1 = e.touches[0]; const touch2 = e.touches[1]; const currentDistance = Math.sqrt( Math.pow(touch2.clientX - touch1.clientX, 2) + Math.pow(touch2.clientY - touch1.clientY, 2) ); const currentCenter = { x: (touch1.clientX + touch2.clientX) / 2, y: (touch1.clientY + touch2.clientY) / 2 }; if (lastTouchDistance > 0) { const zoomFactor = currentDistance / lastTouchDistance; const newZoom = currentZoom * zoomFactor; // Zoom towards touch center const rect = e.target.getBoundingClientRect(); const centerX = currentCenter.x - rect.left; const centerY = currentCenter.y - rect.top; const beforeZoomX = (centerX - panX) / currentZoom; const beforeZoomY = (centerY - panY) / currentZoom; setZoom(newZoom); const afterZoomX = (centerX - panX) / currentZoom; const afterZoomY = (centerY - panY) / currentZoom; panX += (afterZoomX - beforeZoomX) * currentZoom; panY += (afterZoomY - beforeZoomY) * currentZoom; updateTransform(); } lastTouchDistance = currentDistance; lastTouchCenter = currentCenter; } } function handleTouchEnd(e) { e.preventDefault(); if (e.touches.length === 0) { isPanning = false; lastTouchDistance = 0; } else if (e.touches.length === 1) { // One finger remaining, switch to pan mode isPanning = true; lastPanX = e.touches[0].clientX; lastPanY = e.touches[0].clientY; lastTouchDistance = 0; } } // Load images from local storage or default set async function loadImages() { const imageList = document.getElementById('imageList'); // For demo purposes, create some sample images const sampleImages = [ 'sample_tileset_1.png', 'sample_tileset_2.png', 'character_sprites.png', 'ui_elements.png' ]; imageList.innerHTML = ''; sampleImages.forEach((imageName, index) => { const item = document.createElement('div'); item.className = 'image-item'; const img = document.createElement('img'); img.src = `data:image/svg+xml;base64,${btoa(` <svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"> <rect width="64" height="64" fill="#e0e0e0"/> <rect x="0" y="0" width="16" height="16" fill="#ff6b6b"/> <rect x="16" y="0" width="16" height="16" fill="#4ecdc4"/> <rect x="32" y="0" width="16" height="16" fill="#45b7d1"/> <rect x="48" y="0" width="16" height="16" fill="#96ceb4"/> <rect x="0" y="16" width="16" height="16" fill="#feca57"/> <rect x="16" y="16" width="16" height="16" fill="#ff9ff3"/> <rect x="32" y="16" width="16" height="16" fill="#54a0ff"/> <rect x="48" y="16" width="16" height="16" fill="#5f27cd"/> <text x="32" y="40" text-anchor="middle" font-family="Arial" font-size="8" fill="#333">${index + 1}</text> </svg> `)}`; img.alt = imageName; // Add preview functionality img.addEventListener('click', (e) => { e.stopPropagation(); showImagePreview(img.src); }); const nameDiv = document.createElement('div'); nameDiv.className = 'image-name'; nameDiv.textContent = imageName; 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); }); // Auto-load first image setTimeout(() => { document.querySelector('.image-item').click(); }, 500); } function loadImage(imagePath) { const img = new Image(); img.onload = function() { currentImage = img; currentImagePath = imagePath; document.getElementById('editorTitle').textContent = `Editing: Sample Tileset`; const tileImg = document.getElementById('tileImage'); tileImg.src = imagePath; tileImg.style.display = 'block'; tileImg.style.width = currentImage.width + 'px'; tileImg.style.height = currentImage.height + 'px'; // Reset zoom and pan when loading new image resetZoom(); // Clear selection when loading new image selectedTiles.clear(); updateSelectedTilesList(); updateGrid(); }; 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 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); } // 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`; 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}`; // 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); } 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> 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(); } // 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: currentImagePath, 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_${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(); } // 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 isPreviewDragging = false; let lastPreviewMouseX = 0; let lastPreviewMouseY = 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 = 'Sample Tileset Preview'; 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.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; } // Initialize everything when page loads document.addEventListener('DOMContentLoaded', function() { loadImages(); setupTouchEvents(); updateZoomIndicator(); // Set up stepper events for better UX document.querySelectorAll('.stepper-btn').forEach(btn => { btn.addEventListener('mousedown', function(e) { e.preventDefault(); // Prevent focus stealing }); }); }); // 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(); } }); </script> </body> </html>