📜
tilepicker.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// Debug alert for mobile debugging if (typeof debugAlert === 'function') debugAlert('tilepicker.js loaded'); // ===== Shared state from files.js ===== // window.selectedImage, window.selectedImageName, window.selectedTileSize // Groups have no category now let groups = [{ id: 'Group_1', url: null, tiles: [], name: 'Group 1' }]; let currentGroup = 0; let nextUniqueId = 1; // start at 1 (0 = "no object") // ===== Grid settings (toggleable panel) ===== const gridSettings = { tileWidth: Math.max(1, parseInt(window.selectedTileSize || 32, 10)), tileHeight: Math.max(1, parseInt(window.selectedTileSize || 32, 10)), spacing: 0, showPanel: false, __initializedFromFolder: false, __lastFolderSize: null }; // ===== Bridge to tilemap.js ===== // Shape tilemap probably wants: [{ groupId, groupName, url, tiles:[{ id, x, y, w, h, url }] }] function buildTileRects() { return groups.map(g => ({ groupId: g.id, groupName: g.name || '', url: g.url || '', tiles: g.tiles.map(t => ({ id: t.uniqueId, x: t.sourceX, y: t.sourceY, w: t.width ?? t.size, // support old size-only tiles h: t.height ?? t.size, url: t.sourceUrl })) })); } // Notify any listeners (event + optional callback) and expose a helper global function notifyUpdate() { const rects = buildTileRects(); const detail = { rects, groups }; // CustomEvent try { window.dispatchEvent(new CustomEvent('tiles:updated', { detail })); } catch (e) { // no-op in older browsers } // Optional callback if (typeof window.onTilesUpdated === 'function') { try { window.onTilesUpdated(detail); } catch (e) { console.error(e); } } // Helper global for polling window.Tilepicker = window.Tilepicker || {}; window.Tilepicker.getTileRects = () => buildTileRects(); window.Tilepicker.getGroups = () => groups; window.Tilepicker.getCurrentGroupIndex = () => currentGroup; } /** Main entry point (called by files.js via openOverlay('tiles')) */ function openTilePickerOverlay() { const overlayContent = document.getElementById('overlayContent'); const img = window.selectedImage; const name = window.selectedImageName; const tileSize = window.selectedTileSize; // optional hint; we already copied on load if (img && (tileSize || gridSettings.tileWidth)) { // If user picked a new size in Files view, sync it once when opening if (tileSize && (!gridSettings.__initializedFromFolder || gridSettings.__lastFolderSize !== tileSize)) { gridSettings.tileWidth = parseInt(tileSize, 10) || gridSettings.tileWidth; gridSettings.tileHeight = parseInt(tileSize, 10) || gridSettings.tileHeight; gridSettings.__initializedFromFolder = true; gridSettings.__lastFolderSize = tileSize; } overlayContent.innerHTML = ` <h2>Tile Picker 🧩</h2> <div id="groupTabs"></div> <div id="groupControls"></div> <div id="pickedImages"></div> <div id="tileViewport"> <div id="tileContainer" style="position:relative; display:inline-block;"> <img id="tileImage" src="${img}" alt="${name}"> </div> </div> `; initializeTilePicker(); } else { overlayContent.innerHTML = ` <h2>Tile Picker 🧩</h2> <p>Select an image and a numeric folder (tile size) first.</p> `; } } /** Initialize the tile picker functionality */ function initializeTilePicker() { renderTabs(); renderGroupControls(); renderPicked(); setupTileGrid(); } /** Build the grid overlay with width/height/spacing */ function setupTileGrid() { const imgEl = document.getElementById('tileImage'); if (!imgEl) return; const rebuild = () => { const container = document.getElementById('tileContainer'); if (!container) return; const w = imgEl.naturalWidth; const h = imgEl.naturalHeight; const tw = Math.max(1, parseInt(gridSettings.tileWidth, 10) || 1); const th = Math.max(1, parseInt(gridSettings.tileHeight, 10) || 1); const sp = Math.max(0, parseInt(gridSettings.spacing, 10) || 0); // Dimensions imgEl.style.width = w + "px"; imgEl.style.height = h + "px"; container.style.width = w + "px"; container.style.height = h + "px"; // Clear prior cells container.querySelectorAll('.grid-cell').forEach(c => c.remove()); // Create cells; respect spacing (gaps between cells that don't consume image pixels) let tileIndex = 0; for (let y = 0; y + th <= h; y += th + sp) { for (let x = 0; x + tw <= w; x += tw + sp) { tileIndex++; const cell = document.createElement('div'); cell.className = 'grid-cell'; cell.style.cssText = ` position: absolute; left: ${x}px; top: ${y}px; width: ${tw}px; height: ${th}px; border: 2px solid rgba(102, 204, 255, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.3); color: white; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 2px black; box-sizing: border-box; `; const label = document.createElement('span'); label.textContent = tileIndex; cell.appendChild(label); cell.addEventListener('mouseenter', () => { cell.style.background = 'rgba(102, 204, 255, 0.4)'; cell.style.borderColor = '#6cf'; }); cell.addEventListener('mouseleave', () => { cell.style.background = 'rgba(0, 0, 0, 0.3)'; cell.style.borderColor = 'rgba(102, 204, 255, 0.7)'; }); cell.onclick = () => pickTile(imgEl, x, y, tw, th, window.selectedImage); container.appendChild(cell); } } }; imgEl.onload = rebuild; // If the image is cached, onload may not fire; force a tick if (imgEl.complete && imgEl.naturalWidth) { const src = imgEl.src; imgEl.src = ''; imgEl.src = src; } } /** Group controls (toggleable grid settings, rename, clear) */ function renderGroupControls() { const controls = document.getElementById('groupControls'); if (!controls) return; controls.innerHTML = ''; controls.style.cssText = ` margin-bottom:10px;padding:10px;background:#333;border-radius:6px; display:flex;align-items:center;gap:10px;flex-wrap:wrap; `; // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.textContent = gridSettings.showPanel ? 'Hide grid settings ▲' : '⚙️ Grid settings ▼'; toggleBtn.style.cssText = ` background:#555;color:#fff;border:1px solid #777;border-radius:4px; padding:6px 10px;cursor:pointer;font-size:12px; `; toggleBtn.onclick = () => { gridSettings.showPanel = !gridSettings.showPanel; renderGroupControls(); }; controls.appendChild(toggleBtn); // Rename const renameBtn = document.createElement('button'); renameBtn.textContent = 'Rename group'; renameBtn.style.cssText = ` background:#555;color:#fff;border:1px solid #777;border-radius:4px; padding:6px 10px;cursor:pointer;font-size:12px; `; renameBtn.onclick = () => renameGroup(currentGroup); controls.appendChild(renameBtn); // Clear tiles const clearBtn = document.createElement('button'); clearBtn.textContent = 'Clear tiles'; clearBtn.style.cssText = ` background:#d44;color:#fff;border:none;border-radius:4px; padding:6px 10px;cursor:pointer;font-size:12px; `; clearBtn.onclick = () => { if (!groups[currentGroup].tiles.length) return; if (confirm('Clear all tiles from this group?')) clearCurrentGroup(); }; controls.appendChild(clearBtn); // Grid settings panel (hidden unless toggled) if (gridSettings.showPanel) { const panel = document.createElement('div'); panel.style.cssText = ` width:100%; display:flex;align-items:center;gap:10px;flex-wrap:wrap; background:#2b2b2b;border:1px solid #444;border-radius:6px; padding:8px 10px;margin-top:6px; `; const makeField = (label, key, min = 0) => { const wrap = document.createElement('label'); wrap.style.cssText = 'color:#ccc;font-size:12px;display:flex;align-items:center;gap:6px;'; wrap.textContent = label; const input = document.createElement('input'); input.type = 'number'; input.min = String(min); input.value = String(gridSettings[key]); input.style.cssText = ` width:80px;background:#555;color:#fff;border:1px solid #777;border-radius:4px; padding:4px 6px;font-size:12px; `; input.onchange = () => { const v = parseInt(input.value, 10); if (!isNaN(v)) { gridSettings[key] = Math.max(min, v); setupTileGrid(); // live rebuild } }; wrap.appendChild(input); return wrap; }; panel.appendChild(makeField('Width', 'tileWidth', 1)); panel.appendChild(makeField('Height', 'tileHeight', 1)); panel.appendChild(makeField('Spacing', 'spacing', 0)); controls.appendChild(panel); } } /** Group tabs (simple, no categories) */ function renderTabs() { const tabBar = document.getElementById('groupTabs'); if (!tabBar) return; tabBar.innerHTML = ''; tabBar.style.cssText = 'margin-bottom:10px;display:flex;gap:5px;align-items:center;flex-wrap:wrap;'; groups.forEach((g, idx) => { const btn = document.createElement('button'); const name = g.name || `Group ${idx + 1}`; btn.textContent = name; btn.style.cssText = ` background:${idx === currentGroup ? '#6cf' : '#555'}; color:${idx === currentGroup ? '#000' : '#fff'}; border:2px solid #777; padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px; position:relative;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; `; btn.onclick = () => { currentGroup = idx; renderTabs(); renderGroupControls(); renderPicked(); }; btn.ondblclick = (e) => { e.stopPropagation(); renameGroup(idx); }; if (g.tiles.length) { const badge = document.createElement('span'); badge.textContent = g.tiles.length; badge.style.cssText = ` position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border-radius:50%; width:16px;height:16px;font-size:9px;display:flex;align-items:center;justify-content:center; `; btn.appendChild(badge); } tabBar.appendChild(btn); }); // "+" add group const addBtn = document.createElement('button'); addBtn.textContent = '+'; addBtn.style.cssText = ` background:#4a4;color:#fff;border:none;padding:6px 12px;border-radius:4px; cursor:pointer;font-size:14px;font-weight:bold; `; addBtn.onclick = () => { const name = prompt('Enter name for new group:', `Group ${groups.length + 1}`); if (name && name.trim()) createGroup(name.trim()); }; addBtn.title = 'Create new group'; tabBar.appendChild(addBtn); // "Remove group" (if >1 groups) if (groups.length > 1) { const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.style.cssText = ` background:#777;color:#fff;border:none;padding:6px 12px;border-radius:4px; cursor:pointer;font-size:12px; `; removeBtn.onclick = () => { if (confirm('Remove current group?')) removeGroup(currentGroup); }; tabBar.appendChild(removeBtn); } } /** Picked tiles panel */ function renderPicked() { const container = document.getElementById('pickedImages'); if (!container) return; container.innerHTML = ''; container.style.cssText = ` margin-bottom:15px;padding:10px;background:#2a2a2a;border-radius:6px; min-height:80px;max-height:200px;overflow-y:auto; `; const group = groups[currentGroup]; if (!group.tiles.length) { container.innerHTML = '<div style="color:#888;text-align:center;padding:20px;">No tiles picked yet. Click on the grid below to select tiles.</div>'; // broadcast even for empty (some UIs clear palette) notifyUpdate(); return; } const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;'; group.tiles.forEach((tile, idx) => { const card = document.createElement('div'); card.style.cssText = ` position:relative;display:flex;flex-direction:column;align-items:center; padding:5px;background:#333;border-radius:4px;border:2px solid #6cf; `; // Scale preview to fit within 64x64 while preserving aspect const maxPreview = 64; const scale = Math.min(maxPreview / tile.width, maxPreview / tile.height, 1); const cw = Math.max(1, Math.round(tile.width * scale)); const ch = Math.max(1, Math.round(tile.height * scale)); const canvas = document.createElement('canvas'); canvas.width = cw; canvas.height = ch; canvas.style.cssText = 'border:1px solid #666;background:#000;'; const ctx = canvas.getContext('2d'); const temp = document.createElement('canvas'); temp.width = tile.width; temp.height = tile.height; const tctx = temp.getContext('2d'); tctx.putImageData(tile.data, 0, 0); ctx.drawImage(temp, 0, 0, tile.width, tile.height, 0, 0, cw, ch); const rm = document.createElement('button'); rm.textContent = '×'; rm.style.cssText = ` position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border:none; border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:12px; display:flex;align-items:center;justify-content:center; `; rm.onclick = () => { group.tiles.splice(idx, 1); renderPicked(); notifyUpdate(); }; const id = document.createElement('span'); id.textContent = `ID ${tile.uniqueId} (${tile.width}×${tile.height})`; id.style.cssText = 'font-size:10px;color:#ccc;margin-top:4px;text-align:center;'; card.appendChild(canvas); card.appendChild(rm); card.appendChild(id); wrap.appendChild(card); }); container.appendChild(wrap); // Broadcast current state so tilemap can see fresh picks notifyUpdate(); } /** Extract a tile at (x,y) with width/height */ function pickTile(imgEl, x, y, width, height, url) { const group = groups[currentGroup]; // Lock a group to a single spritesheet if (group.url && group.url !== url) { alert('This group already uses a different image. Create a new group for another sheet.'); return; } // Prevent duplicate picks of same cell const duplicate = group.tiles.find(t => t.sourceX === x && t.sourceY === y && t.sourceUrl === url); if (duplicate) { alert(`This tile is already picked (ID ${duplicate.uniqueId})`); return; } group.url = url; if (!group.name) { const base = (url.split('/').pop() || '').split('.')[0] || 'Sheet'; const w = Math.max(1, parseInt(gridSettings.tileWidth, 10) || width); const h = Math.max(1, parseInt(gridSettings.tileHeight, 10) || height); group.name = `${base}_${w}x${h}`; renderTabs(); } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(imgEl, x, y, width, height, 0, 0, width, height); const data = ctx.getImageData(0, 0, width, height); group.tiles.push({ width, height, data, uniqueId: nextUniqueId++, sourceX: x, sourceY: y, sourceUrl: url }); renderPicked(); // also calls notifyUpdate() } /** Group helpers */ function createGroup(groupName) { const id = groupName.replace(/[^a-zA-Z0-9]/g, '_'); groups.push({ id, url: null, tiles: [], name: groupName }); currentGroup = groups.length - 1; renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate(); } function renameGroup(groupIndex) { const group = groups[groupIndex]; const currentName = group.name || `Group ${groupIndex + 1}`; const newName = prompt('Enter new group name:', currentName); if (newName && newName.trim()) { group.name = newName.trim(); group.id = newName.trim().replace(/[^a-zA-Z0-9]/g, '_'); renderTabs(); notifyUpdate(); } } function clearCurrentGroup() { const group = groups[currentGroup]; group.tiles = []; group.url = null; // keep name renderTabs(); renderPicked(); notifyUpdate(); } function removeGroup(idx) { if (groups.length > 1 && idx >= 0 && idx < groups.length) { groups.splice(idx, 1); if (currentGroup >= groups.length) currentGroup = groups.length - 1; renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate(); } } // Optional external API function getCurrentGroup() { return groups[currentGroup]; } function getAllGroups() { return groups; } function setCurrentGroup(idx) { if (idx >= 0 && idx < groups.length) { currentGroup = idx; renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate(); } } // Expose helpers immediately so tilemap.js can call them on load window.Tilepicker = window.Tilepicker || {}; window.Tilepicker.getTileRects = () => buildTileRects(); window.Tilepicker.getGroups = () => groups; window.Tilepicker.getCurrentGroupIndex = () => currentGroup; // Debug alert for mobile debugging - success if (typeof debugAlert === 'function') debugAlert('tilepicker.js loaded successfully');