|
// ==UserScript== |
|
// @name WPlace Art Exporter |
|
// @namespace kotton.tools |
|
// @version 0.5 |
|
// @description Export clean artwork from wplace with transparent background |
|
// @author Kottonye |
|
// @match https://wplace.live/* |
|
// @match https://*.wplace.live/* |
|
// @run-at document-start |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
const TILE_SIZE = 1000; |
|
const VIEWPORT_TILE_AGE_LIMIT = 15000; |
|
|
|
const DEFAULT_SETTINGS = { |
|
maxCacheSize: 100, |
|
autoCleanup: true, |
|
showNotifications: true, |
|
exportFormat: 'png', |
|
compressionQuality: 0.95, |
|
debugMode: false, |
|
panelPosition: 'top-right' |
|
}; |
|
|
|
let isSelecting = false; |
|
let selectionStart = null; |
|
let selectionBox = null; |
|
let overlay = null; |
|
let cachedTiles = new Map(); |
|
let mapInstance = null; |
|
let originalFetch = null; |
|
let currentTileCoords = [0, 0]; |
|
let currentPixelCoords = [0, 0]; |
|
let currentViewportBounds = null; |
|
let settings = { ...DEFAULT_SETTINGS }; |
|
|
|
function addStyles() { |
|
if (document.getElementById('wplace-styles')) return; |
|
|
|
const style = document.createElement('style'); |
|
style.id = 'wplace-styles'; |
|
style.textContent = ` |
|
.wplace-btn { |
|
transition: all 0.2s ease; |
|
cursor: pointer; |
|
border: none; |
|
border-radius: 5px; |
|
font-family: Arial, sans-serif; |
|
font-weight: 500; |
|
} |
|
|
|
.wplace-btn:hover { |
|
transform: translateY(-1px); |
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2); |
|
} |
|
|
|
.wplace-btn:active { |
|
transform: translateY(0); |
|
} |
|
|
|
.wplace-close-btn { |
|
background: none; |
|
border: none; |
|
color: white; |
|
font-size: 18px; |
|
cursor: pointer; |
|
padding: 4px 8px; |
|
border-radius: 4px; |
|
transition: background-color 0.2s ease; |
|
} |
|
|
|
.wplace-close-btn:hover { |
|
background: rgba(255,255,255,0.2); |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
function loadSettings() { |
|
try { |
|
const saved = localStorage.getItem('wplace-export-settings'); |
|
if (saved) { |
|
const parsed = JSON.parse(saved); |
|
settings = { ...DEFAULT_SETTINGS, ...parsed }; |
|
console.log('Settings loaded'); |
|
} |
|
} catch (e) { |
|
console.warn('Failed to load settings:', e); |
|
} |
|
} |
|
|
|
function saveSettings() { |
|
try { |
|
localStorage.setItem('wplace-export-settings', JSON.stringify(settings)); |
|
console.log('Settings saved'); |
|
} catch (e) { |
|
console.warn('Failed to save settings:', e); |
|
} |
|
} |
|
|
|
function showNotification(message, type = 'info') { |
|
if (!settings.showNotifications) return; |
|
|
|
const notification = document.createElement('div'); |
|
notification.style.cssText = ` |
|
position: fixed; |
|
top: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
padding: 12px 20px; |
|
border-radius: 6px; |
|
font-family: Arial, sans-serif; |
|
font-size: 13px; |
|
z-index: 1000002; |
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
transition: opacity 0.3s ease; |
|
`; |
|
|
|
const colors = { |
|
info: { bg: 'rgba(33, 150, 243, 0.95)', color: '#fff' }, |
|
success: { bg: 'rgba(76, 175, 80, 0.95)', color: '#fff' }, |
|
warning: { bg: 'rgba(255, 152, 0, 0.95)', color: '#fff' }, |
|
error: { bg: 'rgba(244, 67, 54, 0.95)', color: '#fff' } |
|
}; |
|
|
|
const color = colors[type] || colors.info; |
|
notification.style.backgroundColor = color.bg; |
|
notification.style.color = color.color; |
|
notification.textContent = message; |
|
|
|
document.body.appendChild(notification); |
|
|
|
setTimeout(() => { |
|
notification.style.opacity = '0'; |
|
setTimeout(() => notification.remove(), 300); |
|
}, 3000); |
|
} |
|
|
|
function manageCacheSize() { |
|
if (cachedTiles.size <= settings.maxCacheSize) return; |
|
|
|
const tileArray = Array.from(cachedTiles.entries()) |
|
.map(([key, data]) => ({ key, ...data })) |
|
.sort((a, b) => a.timestamp - b.timestamp); |
|
|
|
const tilesToRemove = tileArray.slice(0, cachedTiles.size - settings.maxCacheSize); |
|
|
|
for (const tile of tilesToRemove) { |
|
cachedTiles.delete(tile.key); |
|
} |
|
|
|
if (settings.debugMode) { |
|
console.log(`Cache managed: removed ${tilesToRemove.length} old tiles`); |
|
} |
|
} |
|
|
|
function makeDraggable(element, dragHandle) { |
|
let isDragging = false; |
|
let startX = 0, startY = 0, startLeft = 0, startTop = 0; |
|
|
|
const handle = dragHandle || element; |
|
handle.style.cursor = 'move'; |
|
|
|
const handleMouseDown = (e) => { |
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; |
|
|
|
isDragging = true; |
|
startX = e.clientX; |
|
startY = e.clientY; |
|
|
|
const rect = element.getBoundingClientRect(); |
|
startLeft = rect.left; |
|
startTop = rect.top; |
|
|
|
document.body.style.userSelect = 'none'; |
|
e.preventDefault(); |
|
}; |
|
|
|
const handleMouseMove = (e) => { |
|
if (!isDragging) return; |
|
|
|
const deltaX = e.clientX - startX; |
|
const deltaY = e.clientY - startY; |
|
|
|
let newLeft = startLeft + deltaX; |
|
let newTop = startTop + deltaY; |
|
|
|
const rect = element.getBoundingClientRect(); |
|
const minVisible = 50; |
|
newLeft = Math.max(-rect.width + minVisible, Math.min(newLeft, window.innerWidth - minVisible)); |
|
newTop = Math.max(0, Math.min(newTop, window.innerHeight - minVisible)); |
|
|
|
element.style.left = newLeft + 'px'; |
|
element.style.top = newTop + 'px'; |
|
element.style.right = 'auto'; |
|
element.style.bottom = 'auto'; |
|
}; |
|
|
|
const handleMouseUp = () => { |
|
if (!isDragging) return; |
|
isDragging = false; |
|
document.body.style.userSelect = ''; |
|
}; |
|
|
|
handle.addEventListener('mousedown', handleMouseDown); |
|
document.addEventListener('mousemove', handleMouseMove); |
|
document.addEventListener('mouseup', handleMouseUp); |
|
|
|
handle.addEventListener('touchstart', (e) => { |
|
const touch = e.touches[0]; |
|
handleMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: e.target, preventDefault: () => e.preventDefault() }); |
|
}, { passive: false }); |
|
|
|
document.addEventListener('touchmove', (e) => { |
|
if (!isDragging) return; |
|
const touch = e.touches[0]; |
|
handleMouseMove({ clientX: touch.clientX, clientY: touch.clientY }); |
|
e.preventDefault(); |
|
}, { passive: false }); |
|
|
|
document.addEventListener('touchend', handleMouseUp); |
|
} |
|
|
|
function setupTileInterception() { |
|
if (originalFetch) return; |
|
|
|
originalFetch = window.fetch; |
|
window.fetch = async function(...args) { |
|
const response = await originalFetch.apply(this, args); |
|
|
|
const url = args[0]?.url || args[0]; |
|
if (typeof url === 'string' && |
|
(url.includes('/tiles/') || url.includes('.png')) && |
|
url.includes('wplace')) { |
|
|
|
try { |
|
const clonedResponse = response.clone(); |
|
const blob = await clonedResponse.blob(); |
|
|
|
const tileMatch = url.match(/\/tiles\/(\d+)\/(\d+)\.png/) || |
|
url.match(/\/(\d+)\/(\d+)\.png/) || |
|
url.match(/tile_(\d+)_(\d+)\.png/); |
|
|
|
if (tileMatch && blob.type.startsWith('image/')) { |
|
const tileX = parseInt(tileMatch[1]); |
|
const tileY = parseInt(tileMatch[2]); |
|
const cacheKey = `${tileX},${tileY}`; |
|
|
|
const bitmap = await createImageBitmap(blob); |
|
cachedTiles.set(cacheKey, { |
|
bitmap: bitmap, |
|
tileX: tileX, |
|
tileY: tileY, |
|
worldMinX: tileX * TILE_SIZE, |
|
worldMinY: tileY * TILE_SIZE, |
|
worldMaxX: (tileX + 1) * TILE_SIZE, |
|
worldMaxY: (tileY + 1) * TILE_SIZE, |
|
timestamp: Date.now(), |
|
isCurrentlyVisible: true |
|
}); |
|
|
|
if (settings.autoCleanup) manageCacheSize(); |
|
setTimeout(updateViewportBounds, 100); |
|
|
|
if (settings.debugMode) { |
|
console.log(`Cached tile: ${cacheKey} (${cachedTiles.size}/${settings.maxCacheSize})`); |
|
} |
|
} |
|
} catch (e) { |
|
console.warn('Failed to cache tile:', e); |
|
} |
|
} |
|
|
|
return response; |
|
}; |
|
|
|
setInterval(updateViewportBounds, 1000); |
|
} |
|
|
|
function setupBluemarbleListener() { |
|
window.addEventListener('message', (event) => { |
|
const data = event.data; |
|
if (data.source === 'blue-marble' && data.endpoint && data.endpoint.includes('/pixel')) { |
|
try { |
|
const endpoint = data.endpoint.split('?')[0].split('/').filter(t => t && !isNaN(Number(t))); |
|
const params = new URLSearchParams(data.endpoint.split('?')[1]); |
|
const pixelCoords = [params.get('x'), params.get('y')]; |
|
|
|
if (endpoint.length >= 2 && pixelCoords[0] && pixelCoords[1]) { |
|
currentTileCoords = [parseInt(endpoint[0]), parseInt(endpoint[1])]; |
|
currentPixelCoords = [parseInt(pixelCoords[0]), parseInt(pixelCoords[1])]; |
|
|
|
if (settings.debugMode) { |
|
console.log(`Blue Marble coords: Tile(${currentTileCoords[0]}, ${currentTileCoords[1]}) Pixel(${currentPixelCoords[0]}, ${currentPixelCoords[1]})`); |
|
} |
|
} |
|
} catch (e) { |
|
console.warn('Failed to parse Blue Marble coordinates:', e); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function bluemarbleToWorldCoords(tileX, tileY, pixelX, pixelY) { |
|
return { worldX: tileX * TILE_SIZE + pixelX, worldY: tileY * TILE_SIZE + pixelY }; |
|
} |
|
|
|
function worldToBluemarbleCoords(worldX, worldY) { |
|
return { |
|
tileX: Math.floor(worldX / TILE_SIZE), |
|
tileY: Math.floor(worldY / TILE_SIZE), |
|
pixelX: worldX % TILE_SIZE, |
|
pixelY: worldY % TILE_SIZE |
|
}; |
|
} |
|
|
|
function generateBluemarbleCoords(minX, maxX, minY, maxY) { |
|
const topLeft = worldToBluemarbleCoords(minX, minY); |
|
return `${topLeft.tileX} ${topLeft.tileY} ${topLeft.pixelX} ${topLeft.pixelY}`; |
|
} |
|
|
|
// Display coordinates popup dialog |
|
function showCoordinatesDialog(minX, maxX, minY, maxY) { |
|
const existingDialog = document.getElementById('wplace-coords-dialog'); |
|
if (existingDialog) { |
|
existingDialog.remove(); |
|
} |
|
|
|
const bluemarbleCoords = generateBluemarbleCoords(minX, maxX, minY, maxY); |
|
const width = maxX - minX; |
|
const height = maxY - minY; |
|
const topLeft = worldToBluemarbleCoords(minX, minY); |
|
const bottomRight = worldToBluemarbleCoords(maxX - 1, maxY - 1); |
|
|
|
const dialog = document.createElement('div'); |
|
dialog.id = 'wplace-coords-dialog'; |
|
dialog.style.cssText = ` |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
background: rgba(0,0,0,0.95); |
|
color: white; |
|
padding: 0; |
|
border-radius: 10px; |
|
font-family: monospace; |
|
z-index: 1000001; |
|
box-shadow: 0 4px 20px rgba(0,0,0,0.7); |
|
max-width: 600px; |
|
border: 2px solid rgba(255,255,255,0.2); |
|
`; |
|
|
|
const header = document.createElement('div'); |
|
header.style.cssText = ` |
|
background: linear-gradient(135deg, #2196f3, #4caf50); |
|
padding: 10px 15px; |
|
border-radius: 8px 8px 0 0; |
|
cursor: move; |
|
user-select: none; |
|
border-bottom: 1px solid rgba(255,255,255,0.1); |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
`; |
|
header.innerHTML = ` |
|
<span style="font-weight: bold;">Export Coordinates</span> |
|
<button class="wplace-close-btn" onclick="this.closest('#wplace-coords-dialog').remove()">✕</button> |
|
`; |
|
|
|
const content = document.createElement('div'); |
|
content.style.cssText = `padding: 20px;`; |
|
content.innerHTML = ` |
|
<div style="margin-bottom: 15px; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 5px;"> |
|
<div style="margin-bottom: 5px;"><strong>Selection Size:</strong> ${width} × ${height} pixels</div> |
|
<div><strong>World Bounds:</strong> (${minX}, ${minY}) to (${maxX}, ${maxY})</div> |
|
</div> |
|
|
|
<div style="margin-bottom: 15px;"> |
|
<div style="margin-bottom: 8px;"><strong>Blue Marble Coordinates:</strong></div> |
|
<div style="background: rgba(255,255,255,0.1); padding: 8px; border-radius: 5px; margin-bottom: 8px;"> |
|
<div style="font-size: 12px; opacity: 0.8; margin-bottom: 4px;">Top-Left Corner:</div> |
|
<div style="font-weight: bold;">Tile: (${topLeft.tileX}, ${topLeft.tileY}) | Pixel: (${topLeft.pixelX}, ${topLeft.pixelY})</div> |
|
</div> |
|
<div style="background: rgba(255,255,255,0.1); padding: 8px; border-radius: 5px;"> |
|
<div style="font-size: 12px; opacity: 0.8; margin-bottom: 4px;">Bottom-Right Corner:</div> |
|
<div style="font-weight: bold;">Tile: (${bottomRight.tileX}, ${bottomRight.tileY}) | Pixel: (${bottomRight.pixelX}, ${bottomRight.pixelY})</div> |
|
</div> |
|
</div> |
|
|
|
<div style="margin-bottom: 15px;"> |
|
<div style="margin-bottom: 8px;"><strong>Blue Marble Template Format:</strong></div> |
|
<input type="text" value="${bluemarbleCoords}" readonly style="width: 100%; background: rgba(255,255,255,0.1); border: 1px solid #666; color: white; padding: 8px; border-radius: 3px; font-family: monospace;" onclick="this.select()"> |
|
<div style="font-size: 11px; opacity: 0.7; margin-top: 4px;">Format: TileX TileY PixelX PixelY (paste this into Blue Marble's coordinate fields)</div> |
|
</div> |
|
|
|
<div style="display: flex; gap: 10px;"> |
|
<button id="copy-coords-btn" class="wplace-btn" style="flex: 1; padding: 10px; background: #28a745; color: white; font-weight: bold;">Copy Coordinates</button> |
|
<button id="close-coords-btn" class="wplace-btn" style="flex: 1; padding: 10px; background: #dc3545; color: white; font-weight: bold;">Close</button> |
|
</div> |
|
|
|
<div style="margin-top: 15px; font-size: 11px; opacity: 0.7; text-align: center;"> |
|
Tip: Click the coordinate input to select all text for easy copying |
|
</div> |
|
`; |
|
|
|
dialog.appendChild(header); |
|
dialog.appendChild(content); |
|
document.body.appendChild(dialog); |
|
|
|
makeDraggable(dialog, header); |
|
|
|
const copyBtn = content.querySelector('#copy-coords-btn'); |
|
const closeBtn = content.querySelector('#close-coords-btn'); |
|
|
|
copyBtn.onclick = () => { |
|
navigator.clipboard.writeText(bluemarbleCoords).then(() => { |
|
copyBtn.textContent = 'Copied!'; |
|
setTimeout(() => { |
|
copyBtn.textContent = 'Copy Coordinates'; |
|
}, 2000); |
|
}).catch(() => { |
|
copyBtn.textContent = 'Copy failed'; |
|
setTimeout(() => { |
|
copyBtn.textContent = 'Copy Coordinates'; |
|
}, 2000); |
|
}); |
|
}; |
|
|
|
closeBtn.onclick = () => dialog.remove(); |
|
|
|
// Auto-close after 60 seconds |
|
setTimeout(() => { |
|
if (dialog.parentElement) { |
|
dialog.remove(); |
|
} |
|
}, 60000); |
|
} |
|
|
|
function getActualViewportBounds() { |
|
try { |
|
const canvas = document.querySelector('canvas[class*="map"], canvas'); |
|
if (!canvas) return null; |
|
|
|
const recentTiles = Array.from(cachedTiles.values()) |
|
.filter(tile => Date.now() - tile.timestamp < VIEWPORT_TILE_AGE_LIMIT) |
|
.sort((a, b) => b.timestamp - a.timestamp); |
|
|
|
if (recentTiles.length === 0) return null; |
|
|
|
const rect = canvas.getBoundingClientRect(); |
|
const tilesMinX = Math.min(...recentTiles.map(t => t.worldMinX)); |
|
const tilesMaxX = Math.max(...recentTiles.map(t => t.worldMaxX)); |
|
const tilesMinY = Math.min(...recentTiles.map(t => t.worldMinY)); |
|
const tilesMaxY = Math.max(...recentTiles.map(t => t.worldMaxY)); |
|
|
|
const tilesWidth = tilesMaxX - tilesMinX; |
|
const tilesHeight = tilesMaxY - tilesMinY; |
|
const avgScale = ((tilesWidth / rect.width) + (tilesHeight / rect.height)) / 2; |
|
|
|
const viewportWorldWidth = rect.width * avgScale; |
|
const viewportWorldHeight = rect.height * avgScale; |
|
const tilesCenterX = (tilesMinX + tilesMaxX) / 2; |
|
const tilesCenterY = (tilesMinY + tilesMaxY) / 2; |
|
|
|
return { |
|
minX: Math.round(tilesCenterX - viewportWorldWidth / 2), |
|
maxX: Math.round(tilesCenterX + viewportWorldWidth / 2), |
|
minY: Math.round(tilesCenterY - viewportWorldHeight / 2), |
|
maxY: Math.round(tilesCenterY + viewportWorldHeight / 2), |
|
width: Math.round(viewportWorldWidth), |
|
height: Math.round(viewportWorldHeight) |
|
}; |
|
} catch (e) { |
|
return null; |
|
} |
|
} |
|
|
|
function updateViewportBounds() { |
|
currentViewportBounds = getActualViewportBounds(); |
|
|
|
if (currentViewportBounds) { |
|
for (const [key, tile] of cachedTiles.entries()) { |
|
const overlapsX = tile.worldMinX < currentViewportBounds.maxX && tile.worldMaxX > currentViewportBounds.minX; |
|
const overlapsY = tile.worldMinY < currentViewportBounds.maxY && tile.worldMaxY > currentViewportBounds.minY; |
|
const isRecent = Date.now() - tile.timestamp < VIEWPORT_TILE_AGE_LIMIT; |
|
tile.isCurrentlyVisible = overlapsX && overlapsY && isRecent; |
|
} |
|
} |
|
} |
|
|
|
function getVisibleTiles() { |
|
const visibleTiles = new Map(); |
|
const now = Date.now(); |
|
|
|
for (const [key, tile] of cachedTiles.entries()) { |
|
if (now - tile.timestamp < VIEWPORT_TILE_AGE_LIMIT && tile.isCurrentlyVisible) { |
|
visibleTiles.set(key, tile); |
|
} |
|
} |
|
|
|
return visibleTiles; |
|
} |
|
|
|
function screenToWorldCoords(screenX, screenY) { |
|
try { |
|
const canvas = document.querySelector('canvas[class*="map"], canvas'); |
|
if (!canvas) return null; |
|
|
|
const rect = canvas.getBoundingClientRect(); |
|
const relX = (screenX - rect.left) / rect.width; |
|
const relY = (screenY - rect.top) / rect.height; |
|
|
|
if (currentViewportBounds) { |
|
return { |
|
worldX: Math.round(currentViewportBounds.minX + (relX * currentViewportBounds.width)), |
|
worldY: Math.round(currentViewportBounds.minY + (relY * currentViewportBounds.height)) |
|
}; |
|
} |
|
|
|
return { worldX: Math.round(relX * 50000), worldY: Math.round(relY * 50000) }; |
|
} catch (e) { |
|
return null; |
|
} |
|
} |
|
|
|
function createSettingsMenu() { |
|
const existingMenu = document.getElementById('wplace-settings-menu'); |
|
if (existingMenu) { |
|
existingMenu.remove(); |
|
return; |
|
} |
|
|
|
const settingsMenu = document.createElement('div'); |
|
settingsMenu.id = 'wplace-settings-menu'; |
|
settingsMenu.style.cssText = ` |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
background: rgba(0,0,0,0.95); |
|
color: white; |
|
padding: 0; |
|
border-radius: 10px; |
|
font-family: Arial, sans-serif; |
|
z-index: 1000003; |
|
box-shadow: 0 4px 20px rgba(0,0,0,0.7); |
|
max-width: 450px; |
|
min-width: 400px; |
|
border: 2px solid rgba(255,255,255,0.2); |
|
`; |
|
|
|
const header = document.createElement('div'); |
|
header.style.cssText = ` |
|
background: linear-gradient(135deg, #2196f3, #4caf50); |
|
padding: 15px 20px; |
|
border-radius: 8px 8px 0 0; |
|
cursor: move; |
|
user-select: none; |
|
border-bottom: 1px solid rgba(255,255,255,0.1); |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
`; |
|
header.innerHTML = ` |
|
<div style="font-weight: bold; font-size: 16px;">Export Settings</div> |
|
<button class="wplace-close-btn" onclick="this.closest('#wplace-settings-menu').remove()">✕</button> |
|
`; |
|
|
|
const content = document.createElement('div'); |
|
content.style.cssText = `padding: 20px;`; |
|
|
|
content.innerHTML = ` |
|
<div style="margin-bottom: 20px;"> |
|
<div style="font-weight: bold; margin-bottom: 10px; color: #64b5f6;">Export Options</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
<label style="font-size: 14px;">Export Format:</label> |
|
<select id="export-format" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 5px 8px; border-radius: 4px;"> |
|
<option value="png" ${settings.exportFormat === 'png' ? 'selected' : ''}>PNG</option> |
|
<option value="jpeg" ${settings.exportFormat === 'jpeg' ? 'selected' : ''}>JPEG</option> |
|
<option value="webp" ${settings.exportFormat === 'webp' ? 'selected' : ''}>WebP</option> |
|
</select> |
|
</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> |
|
<label style="font-size: 14px;">Quality:</label> |
|
<div style="display: flex; align-items: center; gap: 10px;"> |
|
<input type="range" id="compression-quality" min="0.1" max="1" step="0.05" value="${settings.compressionQuality}" style="width: 100px;"> |
|
<span id="quality-display" style="font-size: 12px; min-width: 35px;">${Math.round(settings.compressionQuality * 100)}%</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div style="margin-bottom: 20px;"> |
|
<div style="font-weight: bold; margin-bottom: 10px; color: #64b5f6;">Cache Settings</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
<label style="font-size: 14px;">Max Cache Size:</label> |
|
<div style="display: flex; align-items: center; gap: 8px;"> |
|
<input type="number" id="max-cache-size" min="10" max="1000" value="${settings.maxCacheSize}" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 5px 8px; border-radius: 4px; width: 70px;"> |
|
<span style="font-size: 12px; opacity: 0.7;">tiles</span> |
|
</div> |
|
</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> |
|
<label style="font-size: 14px;">Auto Cleanup:</label> |
|
<input type="checkbox" id="auto-cleanup" ${settings.autoCleanup ? 'checked' : ''} style="transform: scale(1.2);"> |
|
</div> |
|
</div> |
|
|
|
<div style="margin-bottom: 20px;"> |
|
<div style="font-weight: bold; margin-bottom: 10px; color: #64b5f6;">Interface</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
<label style="font-size: 14px;">Show Notifications:</label> |
|
<input type="checkbox" id="show-notifications" ${settings.showNotifications ? 'checked' : ''} style="transform: scale(1.2);"> |
|
</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
<label style="font-size: 14px;">Panel Position:</label> |
|
<select id="panel-position" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 5px 8px; border-radius: 4px;"> |
|
<option value="top-right" ${settings.panelPosition === 'top-right' ? 'selected' : ''}>Top Right</option> |
|
<option value="top-left" ${settings.panelPosition === 'top-left' ? 'selected' : ''}>Top Left</option> |
|
<option value="bottom-right" ${settings.panelPosition === 'bottom-right' ? 'selected' : ''}>Bottom Right</option> |
|
<option value="bottom-left" ${settings.panelPosition === 'bottom-left' ? 'selected' : ''}>Bottom Left</option> |
|
</select> |
|
</div> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> |
|
<label style="font-size: 14px;">Debug Mode:</label> |
|
<input type="checkbox" id="debug-mode" ${settings.debugMode ? 'checked' : ''} style="transform: scale(1.2);"> |
|
</div> |
|
</div> |
|
|
|
<div style="display: flex; gap: 10px; margin-top: 20px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1);"> |
|
<button id="reset-settings" class="wplace-btn" style="flex: 1; padding: 10px; background: #ff7043; color: white; font-weight: bold;">Reset</button> |
|
<button id="save-settings" class="wplace-btn" style="flex: 1; padding: 10px; background: #4caf50; color: white; font-weight: bold;">Save</button> |
|
</div> |
|
`; |
|
|
|
settingsMenu.appendChild(header); |
|
settingsMenu.appendChild(content); |
|
document.body.appendChild(settingsMenu); |
|
|
|
makeDraggable(settingsMenu, header); |
|
|
|
const qualitySlider = content.querySelector('#compression-quality'); |
|
const qualityDisplay = content.querySelector('#quality-display'); |
|
|
|
qualitySlider.addEventListener('input', () => { |
|
qualityDisplay.textContent = Math.round(qualitySlider.value * 100) + '%'; |
|
}); |
|
|
|
content.querySelector('#save-settings').onclick = () => { |
|
settings.exportFormat = content.querySelector('#export-format').value; |
|
settings.compressionQuality = parseFloat(content.querySelector('#compression-quality').value); |
|
settings.maxCacheSize = parseInt(content.querySelector('#max-cache-size').value); |
|
settings.autoCleanup = content.querySelector('#auto-cleanup').checked; |
|
settings.showNotifications = content.querySelector('#show-notifications').checked; |
|
settings.panelPosition = content.querySelector('#panel-position').value; |
|
settings.debugMode = content.querySelector('#debug-mode').checked; |
|
|
|
saveSettings(); |
|
showNotification('Settings saved successfully!', 'success'); |
|
settingsMenu.remove(); |
|
|
|
const panel = document.getElementById('wplace-export-panel'); |
|
if (panel) { |
|
panel.remove(); |
|
createMainPanel(); |
|
} |
|
}; |
|
|
|
content.querySelector('#reset-settings').onclick = () => { |
|
if (confirm('Reset all settings to defaults?')) { |
|
settings = { ...DEFAULT_SETTINGS }; |
|
saveSettings(); |
|
showNotification('Settings reset to defaults', 'info'); |
|
settingsMenu.remove(); |
|
|
|
const panel = document.getElementById('wplace-export-panel'); |
|
if (panel) { |
|
panel.remove(); |
|
createMainPanel(); |
|
} |
|
} |
|
}; |
|
|
|
const handleEscape = (e) => { |
|
if (e.key === 'Escape') { |
|
settingsMenu.remove(); |
|
document.removeEventListener('keydown', handleEscape); |
|
} |
|
}; |
|
document.addEventListener('keydown', handleEscape); |
|
} |
|
|
|
function createMainPanel() { |
|
if (document.getElementById('wplace-export-panel')) return; |
|
|
|
addStyles(); |
|
|
|
const panel = document.createElement('div'); |
|
panel.id = 'wplace-export-panel'; |
|
|
|
const positions = { |
|
'top-right': { top: '20px', right: '20px' }, |
|
'top-left': { top: '20px', left: '20px' }, |
|
'bottom-right': { bottom: '20px', right: '20px' }, |
|
'bottom-left': { bottom: '20px', left: '20px' } |
|
}; |
|
|
|
const pos = positions[settings.panelPosition] || positions['top-right']; |
|
const positionStyle = Object.entries(pos).map(([k, v]) => `${k}: ${v}`).join('; '); |
|
|
|
panel.style.cssText = ` |
|
position: fixed; |
|
${positionStyle}; |
|
background: rgba(0,0,0,0.95); |
|
color: white; |
|
padding: 0; |
|
border-radius: 10px; |
|
font-family: Arial, sans-serif; |
|
z-index: 999998; |
|
box-shadow: 0 4px 12px rgba(0,0,0,0.7); |
|
min-width: 340px; |
|
max-width: 400px; |
|
border: 2px solid rgba(255,255,255,0.2); |
|
`; |
|
|
|
const header = document.createElement('div'); |
|
header.style.cssText = ` |
|
background: linear-gradient(135deg, #2196f3, #4caf50); |
|
padding: 15px 20px; |
|
border-radius: 8px 8px 0 0; |
|
cursor: move; |
|
user-select: none; |
|
border-bottom: 1px solid rgba(255,255,255,0.1); |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
`; |
|
header.innerHTML = ` |
|
<div> |
|
<div style="font-weight: bold; font-size: 16px;">WPlace Art Exporter</div> |
|
<div style="font-size: 11px; opacity: 0.8;">v0.5 • Transparent backgrounds & Blue Marble support</div> |
|
</div> |
|
<button class="wplace-close-btn" onclick="this.closest('#wplace-export-panel').remove()">✕</button> |
|
`; |
|
|
|
const content = document.createElement('div'); |
|
content.style.cssText = `padding: 20px;`; |
|
content.innerHTML = ` |
|
<div id="wplace-export-status" style="margin-bottom: 12px; font-size: 12px; opacity: 0.8; background: rgba(255,255,255,0.1); padding: 8px; border-radius: 4px;">Ready - Navigate around to cache tiles</div> |
|
|
|
<div style="margin-bottom: 12px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 4px;"> |
|
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">Cache Status:</div> |
|
<div style="font-size: 11px; opacity: 0.6;">Total cached: <span id="total-tile-count">0</span>/<span id="max-cache-display">${settings.maxCacheSize}</span> tiles</div> |
|
<div style="font-size: 11px; opacity: 0.6; color: #90EE90;">Visible now: <span id="visible-tile-count">0</span> tiles</div> |
|
<div style="font-size: 11px; opacity: 0.6;">Blue Marble: (<span id="current-coords">0, 0, 0, 0</span>)</div> |
|
</div> |
|
|
|
<div id="viewport-info" style="margin-bottom: 12px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 4px;"> |
|
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">Viewport:</div> |
|
<div id="viewport-bounds" style="font-size: 11px; opacity: 0.6;">Calculating...</div> |
|
</div> |
|
|
|
<div id="cached-tiles-list" style="margin-bottom: 12px; font-size: 11px; opacity: 0.6; max-height: 80px; overflow-y: auto; background: rgba(255,255,255,0.05); padding: 6px; border-radius: 4px;"></div> |
|
|
|
<button id="export-selection" class="wplace-btn" style="display: block; width: 100%; margin: 8px 0; padding: 12px; background: #28a745; color: white; font-weight: bold;">Select Area to Export</button> |
|
<button id="export-cached" class="wplace-btn" style="display: block; width: 100%; margin: 8px 0; padding: 12px; background: #007acc; color: white; font-weight: bold;">Export Cached Tiles</button> |
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0;"> |
|
<button id="settings-btn" class="wplace-btn" style="padding: 10px; background: #6c757d; color: white; font-size: 12px;">Settings</button> |
|
<button id="clear-cache" class="wplace-btn" style="padding: 10px; background: #dc3545; color: white; font-size: 12px;">Clear</button> |
|
</div> |
|
|
|
<div style="margin-top: 12px; font-size: 11px; opacity: 0.6; background: rgba(255,255,255,0.05); padding: 6px; border-radius: 4px;"> |
|
<div>Press Alt+E to toggle panel</div> |
|
<div>Drag header to move windows</div> |
|
<div>Selection exports only your selection</div> |
|
<div>Cached exports all visited areas</div> |
|
</div> |
|
`; |
|
|
|
panel.appendChild(header); |
|
panel.appendChild(content); |
|
document.body.appendChild(panel); |
|
|
|
makeDraggable(panel, header); |
|
|
|
setInterval(() => { |
|
const totalTileCountEl = content.querySelector('#total-tile-count'); |
|
const visibleTileCountEl = content.querySelector('#visible-tile-count'); |
|
const currentCoordsEl = content.querySelector('#current-coords'); |
|
const viewportBoundsEl = content.querySelector('#viewport-bounds'); |
|
const maxCacheDisplayEl = content.querySelector('#max-cache-display'); |
|
const tilesListEl = content.querySelector('#cached-tiles-list'); |
|
|
|
const visibleTiles = getVisibleTiles(); |
|
|
|
if (totalTileCountEl) totalTileCountEl.textContent = cachedTiles.size; |
|
if (visibleTileCountEl) visibleTileCountEl.textContent = visibleTiles.size; |
|
if (maxCacheDisplayEl) maxCacheDisplayEl.textContent = settings.maxCacheSize; |
|
|
|
if (currentCoordsEl) { |
|
currentCoordsEl.textContent = `${currentTileCoords[0]}, ${currentTileCoords[1]}, ${currentPixelCoords[0]}, ${currentPixelCoords[1]}`; |
|
} |
|
|
|
if (viewportBoundsEl && currentViewportBounds) { |
|
const { width, height } = currentViewportBounds; |
|
viewportBoundsEl.textContent = `${width}×${height}px`; |
|
} else if (viewportBoundsEl) { |
|
viewportBoundsEl.textContent = 'Navigate to establish bounds'; |
|
} |
|
|
|
if (tilesListEl) { |
|
if (cachedTiles.size > 0) { |
|
const recentTiles = Array.from(cachedTiles.entries()) |
|
.slice(-4) |
|
.map(([key, data]) => { |
|
const age = Math.round((Date.now() - data.timestamp) / 1000); |
|
const isVisible = visibleTiles.has(key) ? 'Visible' : 'Cached'; |
|
return `${isVisible}: ${key} (${age}s ago)`; |
|
}) |
|
.join('<br>'); |
|
tilesListEl.innerHTML = `Recent tiles:<br>${recentTiles}`; |
|
} else { |
|
tilesListEl.innerHTML = 'No cached tiles. Navigate around the map!'; |
|
} |
|
} |
|
}, 1000); |
|
|
|
content.querySelector('#export-selection').onclick = startSelection; |
|
content.querySelector('#export-cached').onclick = exportCachedTiles; |
|
content.querySelector('#settings-btn').onclick = createSettingsMenu; |
|
content.querySelector('#clear-cache').onclick = () => { |
|
if (confirm('Clear all cached tiles?')) { |
|
cachedTiles.clear(); |
|
currentViewportBounds = null; |
|
showNotification('Cache cleared successfully', 'success'); |
|
} |
|
}; |
|
|
|
if (settings.showNotifications) { |
|
showNotification('WPlace Art Exporter v0.5 ready!', 'success'); |
|
} |
|
} |
|
|
|
function processTileForTransparency(imageBitmap) { |
|
return new Promise((resolve) => { |
|
const canvas = document.createElement('canvas'); |
|
canvas.width = imageBitmap.width; |
|
canvas.height = imageBitmap.height; |
|
const ctx = canvas.getContext('2d'); |
|
ctx.drawImage(imageBitmap, 0, 0); |
|
createImageBitmap(canvas).then(resolve); |
|
}); |
|
} |
|
|
|
function findOverlappingTiles(minX, maxX, minY, maxY, useOnlyVisible = true) { |
|
const overlappingTiles = []; |
|
const tilesToSearch = useOnlyVisible ? getVisibleTiles() : cachedTiles; |
|
|
|
for (const [key, tileData] of tilesToSearch.entries()) { |
|
const overlapsX = tileData.worldMinX < maxX && tileData.worldMaxX > minX; |
|
const overlapsY = tileData.worldMinY < maxY && tileData.worldMaxY > minY; |
|
|
|
if (overlapsX && overlapsY) { |
|
overlappingTiles.push({ |
|
key, data: tileData, |
|
overlapMinX: Math.max(minX, tileData.worldMinX), |
|
overlapMaxX: Math.min(maxX, tileData.worldMaxX), |
|
overlapMinY: Math.max(minY, tileData.worldMinY), |
|
overlapMaxY: Math.min(maxY, tileData.worldMaxY) |
|
}); |
|
} |
|
} |
|
|
|
return overlappingTiles; |
|
} |
|
|
|
async function exportFromCache(startCoords, endCoords) { |
|
if (!startCoords || !endCoords) { |
|
showNotification('Invalid coordinates provided', 'error'); |
|
return; |
|
} |
|
|
|
const minX = Math.min(startCoords.worldX, endCoords.worldX); |
|
const maxX = Math.max(startCoords.worldX, endCoords.worldX); |
|
const minY = Math.min(startCoords.worldY, endCoords.worldY); |
|
const maxY = Math.max(startCoords.worldY, endCoords.worldY); |
|
|
|
const width = maxX - minX; |
|
const height = maxY - minY; |
|
|
|
if (width <= 0 || height <= 0) { |
|
showNotification('Invalid selection area', 'error'); |
|
return; |
|
} |
|
|
|
showCoordinatesDialog(minX, maxX, minY, maxY); |
|
|
|
const overlappingTiles = findOverlappingTiles(minX, maxX, minY, maxY, true); |
|
|
|
if (overlappingTiles.length === 0) { |
|
showNotification('No visible tiles in selected area', 'warning'); |
|
return; |
|
} |
|
|
|
showNotification(`Exporting ${width}×${height} pixels...`, 'info'); |
|
|
|
const canvas = document.createElement('canvas'); |
|
canvas.width = width; |
|
canvas.height = height; |
|
const ctx = canvas.getContext('2d'); |
|
|
|
for (const tileInfo of overlappingTiles) { |
|
const tile = tileInfo.data.bitmap; |
|
if (tile) { |
|
const processedTile = await processTileForTransparency(tile); |
|
|
|
const srcX = Math.max(0, minX - tileInfo.data.worldMinX); |
|
const srcY = Math.max(0, minY - tileInfo.data.worldMinY); |
|
const srcW = Math.min(TILE_SIZE - srcX, maxX - Math.max(minX, tileInfo.data.worldMinX)); |
|
const srcH = Math.min(TILE_SIZE - srcY, maxY - Math.max(minY, tileInfo.data.worldMinY)); |
|
|
|
const dstX = Math.max(0, tileInfo.data.worldMinX - minX); |
|
const dstY = Math.max(0, tileInfo.data.worldMinY - minY); |
|
|
|
if (srcW > 0 && srcH > 0) { |
|
ctx.drawImage(processedTile, srcX, srcY, srcW, srcH, dstX, dstY, srcW, srcH); |
|
} |
|
} |
|
} |
|
|
|
canvas.toBlob(blob => { |
|
if (!blob) { |
|
showNotification('Failed to create image', 'error'); |
|
return; |
|
} |
|
|
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
|
const bluemarbleCoords = generateBluemarbleCoords(minX, maxX, minY, maxY).replace(/\s+/g, '_'); |
|
a.href = url; |
|
a.download = `wplace-selection-${width}x${height}-${bluemarbleCoords}-${timestamp}.${settings.exportFormat}`; |
|
document.body.appendChild(a); |
|
a.click(); |
|
a.remove(); |
|
URL.revokeObjectURL(url); |
|
|
|
showNotification(`Exported ${width}×${height} pixels!`, 'success'); |
|
}, `image/${settings.exportFormat}`, settings.compressionQuality); |
|
} |
|
|
|
async function exportCachedTiles() { |
|
if (cachedTiles.size === 0) { |
|
showNotification('No cached tiles available', 'warning'); |
|
return; |
|
} |
|
|
|
const allTiles = Array.from(cachedTiles.values()); |
|
const minX = Math.min(...allTiles.map(t => t.worldMinX)); |
|
const maxX = Math.max(...allTiles.map(t => t.worldMaxX)); |
|
const minY = Math.min(...allTiles.map(t => t.worldMinY)); |
|
const maxY = Math.max(...allTiles.map(t => t.worldMaxY)); |
|
|
|
const canvasWidth = maxX - minX; |
|
const canvasHeight = maxY - minY; |
|
|
|
showCoordinatesDialog(minX, maxX, minY, maxY); |
|
|
|
showNotification(`Exporting ${allTiles.length} cached tiles...`, 'info'); |
|
|
|
const canvas = document.createElement('canvas'); |
|
canvas.width = canvasWidth; |
|
canvas.height = canvasHeight; |
|
const ctx = canvas.getContext('2d'); |
|
|
|
for (const tileData of allTiles) { |
|
if (tileData.bitmap) { |
|
const processedTile = await processTileForTransparency(tileData.bitmap); |
|
const drawX = tileData.worldMinX - minX; |
|
const drawY = tileData.worldMinY - minY; |
|
ctx.drawImage(processedTile, drawX, drawY); |
|
} |
|
} |
|
|
|
canvas.toBlob(blob => { |
|
if (!blob) { |
|
showNotification('Failed to create image', 'error'); |
|
return; |
|
} |
|
|
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
|
const bluemarbleCoords = generateBluemarbleCoords(minX, maxX, minY, maxY).replace(/\s+/g, '_'); |
|
a.href = url; |
|
a.download = `wplace-cached-${canvasWidth}x${canvasHeight}-${bluemarbleCoords}-${timestamp}.${settings.exportFormat}`; |
|
document.body.appendChild(a); |
|
a.click(); |
|
a.remove(); |
|
URL.revokeObjectURL(url); |
|
|
|
showNotification(`Exported ${canvasWidth}×${canvasHeight} pixels!`, 'success'); |
|
}, `image/${settings.exportFormat}`, settings.compressionQuality); |
|
} |
|
|
|
function createOverlay() { |
|
if (overlay) return overlay; |
|
|
|
overlay = document.createElement('div'); |
|
overlay.style.cssText = ` |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100vw; |
|
height: 100vh; |
|
background: rgba(0,0,0,0.3); |
|
cursor: crosshair; |
|
z-index: 999999; |
|
display: none; |
|
`; |
|
|
|
selectionBox = document.createElement('div'); |
|
selectionBox.style.cssText = ` |
|
position: fixed; |
|
border: 2px dashed #00ff00; |
|
background: rgba(0,255,0,0.1); |
|
display: none; |
|
z-index: 1000000; |
|
pointer-events: none; |
|
`; |
|
|
|
document.body.appendChild(overlay); |
|
document.body.appendChild(selectionBox); |
|
|
|
return overlay; |
|
} |
|
|
|
function startSelection() { |
|
updateViewportBounds(); |
|
const overlay = createOverlay(); |
|
overlay.style.display = 'block'; |
|
isSelecting = true; |
|
selectionStart = null; |
|
updateStatus('Click and drag to select export area...'); |
|
} |
|
|
|
function setupSelectionEvents() { |
|
const overlay = createOverlay(); |
|
|
|
overlay.addEventListener('mousedown', (e) => { |
|
if (!isSelecting) return; |
|
selectionStart = { x: e.clientX, y: e.clientY }; |
|
selectionBox.style.display = 'block'; |
|
e.preventDefault(); |
|
}); |
|
|
|
overlay.addEventListener('mousemove', (e) => { |
|
if (!isSelecting || !selectionStart) return; |
|
|
|
const left = Math.min(e.clientX, selectionStart.x); |
|
const top = Math.min(e.clientY, selectionStart.y); |
|
const width = Math.abs(e.clientX - selectionStart.x); |
|
const height = Math.abs(e.clientY - selectionStart.y); |
|
|
|
Object.assign(selectionBox.style, { |
|
left: `${left}px`, |
|
top: `${top}px`, |
|
width: `${width}px`, |
|
height: `${height}px` |
|
}); |
|
}); |
|
|
|
overlay.addEventListener('mouseup', async (e) => { |
|
if (!isSelecting || !selectionStart) return; |
|
|
|
const startCoords = screenToWorldCoords(selectionStart.x, selectionStart.y); |
|
const endCoords = screenToWorldCoords(e.clientX, e.clientY); |
|
|
|
overlay.style.display = 'none'; |
|
selectionBox.style.display = 'none'; |
|
isSelecting = false; |
|
|
|
if (startCoords && endCoords) { |
|
await exportFromCache(startCoords, endCoords); |
|
} else { |
|
showNotification('Could not determine coordinates', 'error'); |
|
} |
|
}); |
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (e.key === 'Escape' && isSelecting) { |
|
overlay.style.display = 'none'; |
|
selectionBox.style.display = 'none'; |
|
isSelecting = false; |
|
updateStatus('Selection cancelled'); |
|
} |
|
}); |
|
} |
|
|
|
function updateStatus(message) { |
|
const statusEl = document.getElementById('wplace-export-status'); |
|
if (statusEl) { |
|
statusEl.textContent = message; |
|
} |
|
if (settings.debugMode) console.log(`[WPlace Export] ${message}`); |
|
} |
|
|
|
function waitForMap() { |
|
return new Promise((resolve) => { |
|
let attempts = 0; |
|
const checkMap = () => { |
|
attempts++; |
|
if (window.map && typeof window.map.getBounds === 'function') { |
|
mapInstance = window.map; |
|
resolve(mapInstance); |
|
return; |
|
} |
|
|
|
const canvas = document.querySelector('canvas[class*="map"], canvas'); |
|
if (canvas) { |
|
mapInstance = { container: canvas.parentElement, canvas }; |
|
resolve(mapInstance); |
|
return; |
|
} |
|
|
|
if (attempts < 60) { |
|
setTimeout(checkMap, 500); |
|
} else { |
|
mapInstance = { fallback: true }; |
|
resolve(mapInstance); |
|
} |
|
}; |
|
checkMap(); |
|
}); |
|
} |
|
|
|
function init() { |
|
loadSettings(); |
|
setupTileInterception(); |
|
setupBluemarbleListener(); |
|
setupSelectionEvents(); |
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (e.altKey && e.key.toLowerCase() === 'e') { |
|
const panel = document.getElementById('wplace-export-panel'); |
|
if (panel) { |
|
panel.remove(); |
|
} else { |
|
createMainPanel(); |
|
} |
|
e.preventDefault(); |
|
} |
|
}); |
|
|
|
const checkReady = () => { |
|
if (document.body && (document.querySelector('canvas') || document.querySelector('#map'))) { |
|
waitForMap().then(() => { |
|
setTimeout(() => createMainPanel(), 1000); |
|
console.log('WPlace Art Exporter v0.5 ready! Press Alt+E to open.'); |
|
}); |
|
} else { |
|
setTimeout(checkReady, 500); |
|
} |
|
}; |
|
|
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', checkReady); |
|
} else { |
|
checkReady(); |
|
} |
|
} |
|
|
|
init(); |
|
|
|
})(); |
Hello. Can you fix the code so that the Select area to export function works correctly? :3