diff --git a/Auto-Image.js b/Auto-Image.js index 4200f49..41db943 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -43,11 +43,12 @@ "209,128,81": "Dark Beige", "255,197,165": "Light Beige", "109,100,63": "Dark Stone", "148,140,107": "Stone", "205,197,158": "Light Stone", "51,57,65": "Dark Slate", "109,117,141": "Slate", "179,185,209": "Light Slate", }, + // --- CORRECTED PAID COLORS --- + // Corresponds to IDs 32-61 from the main palette PAID_COLORS: new Set([ - "170,170,170", "165,14,30", "250,128,114", "228,92,26", "156,132,49", "197,173,49", "232,212,95", "74,107,58", - "90,148,74", "132,197,115", "15,121,159", "187,250,242", "125,199,255", "77,49,184", "74,66,132", "122,113,196", - "181,174,241", "155,82,73", "209,128,120", "250,182,164", "219,164,99", "123,99,82", "156,132,107", "214,181,148", - "209,128,81", "255,197,165", "109,100,63", "148,140,107", "205,197,158", "51,57,65", "109,117,141", "179,185,209", + "125,199,255", "77,49,184", "107,80,246", "153,177,251", "74,66,132", "122,113,196", "181,174,241", "170,56,185", "224,159,249", "203,0,122", + "236,31,128", "243,141,169", "155,82,73", "209,128,120", "250,182,164", "104,70,52", "149,104,42", "219,164,99", "123,99,82", "156,132,107", + "214,181,148", "209,128,81", "248,178,119", "255,197,165", "109,100,63", "148,140,107", "205,197,158", "51,57,65", "109,117,141", "179,185,209" ]), // --- END: Color data --- // Optimized CSS Classes for reuse @@ -176,7 +177,6 @@ const TEXT = { en: { title: "WPlace Auto-Image", - toggleOverlay: "Toggle Overlay", scanColors: "Scan Colors", uploadImage: "Upload Image", resizeImage: "Resize Image", @@ -552,256 +552,57 @@ language: "en", paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels per second cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + // --- NEW: Overlay state --- + overlayCanvas: null, + overlayCtx: null, + gameCanvas: null, } // Placeholder for the resize preview update function let _updateResizePreview = () => {}; - // --- OVERLAY UPDATE: New OverlayManager class to handle all overlay logic --- - class OverlayManager { - constructor() { - this.isEnabled = false; - this.startCoords = null; // { region: {x, y}, pixel: {x, y} } - this.imageBitmap = null; - this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> - this.tileSize = 1000; + // Turnstile token handling (promise-based) inspired by external logic + let turnstileToken = null + let _resolveToken = null + let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) + + function setTurnstileToken(t) { + if (_resolveToken) { + _resolveToken(t) + _resolveToken = null } - - toggle() { - this.isEnabled = !this.isEnabled; - console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`); - return this.isEnabled; - } - - enable() { this.isEnabled = true; } - disable() { this.isEnabled = false; } - clear() { - this.disable(); - this.imageBitmap = null; - this.chunkedTiles.clear(); - } - - async setImage(imageBitmap) { - this.imageBitmap = imageBitmap; - if (this.imageBitmap && this.startCoords) { - await this.processImageIntoChunks(); - } - } - - async setPosition(startPosition, region) { - if (!startPosition || !region) { - this.startCoords = null; - this.chunkedTiles.clear(); - return; - } - this.startCoords = { region, pixel: startPosition }; - if (this.imageBitmap) { - await this.processImageIntoChunks(); - } - } - - // --- OVERLAY UPDATE: Simplified chunking logic for solid, semi-transparent overlay --- - async processImageIntoChunks() { - if (!this.imageBitmap || !this.startCoords) return; - - this.chunkedTiles.clear(); - const { width: imageWidth, height: imageHeight } = this.imageBitmap; - const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; - const { x: startRegionX, y: startRegionY } = this.startCoords.region; - - const endPixelX = startPixelX + imageWidth; - const endPixelY = startPixelY + imageHeight; - - const startTileX = startRegionX + Math.floor(startPixelX / this.tileSize); - const startTileY = startRegionY + Math.floor(startPixelY / this.tileSize); - const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize); - const endTileY = startRegionY + Math.floor(endPixelY / this.tileSize); - - for (let ty = startTileY; ty <= endTileY; ty++) { - for (let tx = startTileX; tx <= endTileX; tx++) { - const tileKey = `${tx},${ty}`; - - // Calculate the portion of the image that overlaps with this tile - const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; - const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; - - // Crop coordinates within the source image - const sX = Math.max(0, imgStartX); - const sY = Math.max(0, imgStartY); - const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); - const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); - - if (sW <= 0 || sH <= 0) continue; - - // Destination coordinates on the new chunk canvas - const dX = Math.max(0, -imgStartX); - const dY = Math.max(0, -imgStartY); - - const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); - const chunkCtx = chunkCanvas.getContext('2d'); - chunkCtx.imageSmoothingEnabled = false; - - chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); - - const chunkBitmap = await chunkCanvas.transferToImageBitmap(); - this.chunkedTiles.set(tileKey, chunkBitmap); - } - } - - console.log(`Overlay processed into ${this.chunkedTiles.size} chunks.`); - } - - // --- OVERLAY UPDATE: Simplified compositing logic for solid, semi-transparent overlay --- - async processAndRespondToTileRequest(eventData) { - const { endpoint, blobID, blobData } = eventData; - - let finalBlob = blobData; - - if (this.isEnabled && this.chunkedTiles.size > 0) { - const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); - if (tileMatch) { - const tileX = parseInt(tileMatch[1], 10); - const tileY = parseInt(tileMatch[2], 10); - const tileKey = `${tileX},${tileY}`; - - const chunkBitmap = this.chunkedTiles.get(tileKey); - if (chunkBitmap) { - try { - const originalTileBitmap = await createImageBitmap(blobData); - const canvas = new OffscreenCanvas(originalTileBitmap.width, originalTileBitmap.height); - const ctx = canvas.getContext('2d'); - ctx.imageSmoothingEnabled = false; - - // Draw original tile first - ctx.drawImage(originalTileBitmap, 0, 0); - - // Set opacity and draw our solid overlay chunk on top - ctx.globalAlpha = 0.6; - ctx.drawImage(chunkBitmap, 0, 0); - - finalBlob = await canvas.convertToBlob({ type: 'image/png' }); - } catch (e) { - console.error("Error compositing overlay:", e); - } - } - } - } - - // Send the (possibly modified) blob back to the injected script - window.postMessage({ - source: 'auto-image-overlay', - blobID: blobID, - blobData: finalBlob - }, '*'); - } -} - -const overlayManager = new OverlayManager(); - -// Turnstile token handling (promise-based) inspired by external logic -let turnstileToken = null -let _resolveToken = null -let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - -function setTurnstileToken(t) { - if (_resolveToken) { - _resolveToken(t) - _resolveToken = null + turnstileToken = t } - turnstileToken = t -} -async function ensureToken() { - if (!turnstileToken) { - updateUI("captchaNeeded", "error") - Utils.showAlert(Utils.t("captchaNeeded"), "error") - try { await tokenPromise } catch (_) {} + async function ensureToken() { + if (!turnstileToken) { + updateUI("captchaNeeded", "error") + Utils.showAlert(Utils.t("captchaNeeded"), "error") + try { await tokenPromise } catch (_) {} + } + return turnstileToken } - return turnstileToken -} -function inject(callback) { - const script = document.createElement('script'); - script.textContent = `(${callback})();`; - document.documentElement?.appendChild(script); - script.remove(); -} - -inject(() => { - const fetchedBlobQueue = new Map(); - - window.addEventListener('message', (event) => { - const { source, blobID, blobData } = event.data; - if (source === 'auto-image-overlay' && blobID && blobData) { - const callback = fetchedBlobQueue.get(blobID); - if (typeof callback === 'function') { - callback(blobData); - } - fetchedBlobQueue.delete(blobID); + // Intercept fetch to capture Turnstile token from pixel placement requests + const originalFetch = window.fetch + window.fetch = async (url, options) => { + if (typeof url === "string" && url.includes("https://backend.wplace.live/s0/pixel/")) { + try { + const payload = JSON.parse(options.body) + if (payload.t) { + console.log("✅ Turnstile Token Captured:", payload.t) + setTurnstileToken(payload.t) + if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { + Utils.showAlert("Token captured successfully! You can start the bot now.", "success") + updateUI("colorsFound", "success", { count: state.availableColors.length }) + } } - }); - - const originalFetch = window.fetch; - window.fetch = async function(...args) { - const response = await originalFetch.apply(this, args); - const url = (args[0] instanceof Request) ? args[0].url : args[0]; - - if (typeof url === "string") { - if (url.includes("https://backend.wplace.live/s0/pixel/")) { - try { - const payload = JSON.parse(args[1].body); - if (payload.t) { - console.log("✅ Turnstile Token Captured:", payload.t); - window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); - } - } catch (_) { /* ignore */ } - } - - const contentType = response.headers.get('content-type') || ''; - if (contentType.includes('image/png') && url.includes('.png')) { - const cloned = response.clone(); - return new Promise(async (resolve) => { - const blobUUID = crypto.randomUUID(); - const originalBlob = await cloned.blob(); - - fetchedBlobQueue.set(blobUUID, (processedBlob) => { - resolve(new Response(processedBlob, { - headers: cloned.headers, - status: cloned.status, - statusText: cloned.statusText - })); - }); - - window.postMessage({ - source: 'auto-image-tile', - endpoint: url, - blobID: blobUUID, - blobData: originalBlob, - }, '*'); - }); - } - } - - return response; - }; -}); - -window.addEventListener('message', (event) => { - const { source, endpoint, blobID, blobData, token } = event.data; - - if (source === 'auto-image-tile' && endpoint && blobID && blobData) { - overlayManager.processAndRespondToTileRequest(event.data); + } catch (_) { /* ignore */ } } + return originalFetch(url, options) + } - if (source === 'turnstile-capture' && token) { - setTurnstileToken(token); - if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { - Utils.showAlert("Token captured successfully! You can start the bot now.", "success"); - updateUI("colorsFound", "success", { count: state.availableColors.length }); - } - } -}); - + // LANGUAGE DETECTION async function detectLanguage() { try { const response = await fetch("https://backend.wplace.live/me", { @@ -828,9 +629,11 @@ window.addEventListener('message', (event) => { return null; }, + // Optimized DOM creation helpers createElement: (tag, props = {}, children = []) => { const element = document.createElement(tag) + // Set properties efficiently Object.entries(props).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value) @@ -843,6 +646,7 @@ window.addEventListener('message', (event) => { } }) + // Append children efficiently if (typeof children === 'string') { element.textContent = children } else if (Array.isArray(children)) { @@ -858,6 +662,7 @@ window.addEventListener('message', (event) => { return element }, + // Create button with common styling createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => { const button = Utils.createElement('button', { id: id, @@ -927,6 +732,7 @@ window.addEventListener('message', (event) => { colorDistance: (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), + // The color metric from colour-converter.js for higher accuracy findClosestPaletteColor: (r, g, b, palette) => { let menorDist = Infinity; let cor = [0, 0, 0]; @@ -1035,15 +841,19 @@ window.addEventListener('message', (event) => { calculateEstimatedTime: (remainingPixels, charges, cooldown) => { if (remainingPixels <= 0) return 0 + // Calculate time based on painting speed (pixels per second) const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000 - const timeFromSpeed = remainingPixels * paintingSpeedDelay + const timeFromSpeed = remainingPixels * paintingSpeedDelay // ms + // Calculate time based on charges and cooldown const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)) - const timeFromCharges = cyclesNeeded * cooldown + const timeFromCharges = cyclesNeeded * cooldown // ms + // Return the maximum of both calculations (the limiting factor) return Math.max(timeFromSpeed, timeFromCharges) }, + // Save/Load Progress Functions saveProgress: () => { try { const progressData = { @@ -1099,8 +909,10 @@ window.addEventListener('message', (event) => { restoreProgress: (savedData) => { try { + // Restore state Object.assign(state, savedData.state) + // Restore image data if (savedData.imageData) { state.imageData = { ...savedData.imageData, @@ -1108,6 +920,7 @@ window.addEventListener('message', (event) => { } } + // Restore painted map if (savedData.paintedMap) { state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)) } @@ -1293,8 +1106,10 @@ window.addEventListener('message', (event) => { const colorCache = new Map() function findClosestColor(targetRgb, availableColors) { + // Create cache key from RGB values const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}` + // Check cache first if (colorCache.has(cacheKey)) { return colorCache.get(cacheKey) } @@ -1311,18 +1126,23 @@ window.addEventListener('message', (event) => { let minDistance = Number.POSITIVE_INFINITY let closestColorId = availableColors[0]?.id || 1 + // Use optimized loop for better performance for (let i = 0; i < availableColors.length; i++) { const color = availableColors[i] const distance = Utils.colorDistance(targetRgb, color.rgb) if (distance < minDistance) { minDistance = distance closestColorId = color.id + + // If perfect match, break early if (distance === 0) break } } + // Cache the result for future use colorCache.set(cacheKey, closestColorId) + // Limit cache size to prevent memory leaks if (colorCache.size > 10000) { const firstKey = colorCache.keys().next().value colorCache.delete(firstKey) @@ -1336,6 +1156,7 @@ window.addEventListener('message', (event) => { let updateStats = () => {} let updateDataButtons = () => {} + // --- START: Color Palette Functions --- function updateActiveColorPalette() { state.activeColorPalette = []; const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); @@ -1345,6 +1166,7 @@ window.addEventListener('message', (event) => { state.activeColorPalette.push(rgb); }); } + // If the resize dialog is open, update its preview if (document.querySelector('.resize-container')?.style.display === 'block') { _updateResizePreview(); } @@ -1369,6 +1191,7 @@ window.addEventListener('message', (event) => { freeContainer.innerHTML = ''; paidContainer.innerHTML = ''; + // Create a unique set of colors to avoid duplicates const uniqueColors = [...new Set(CONFIG.COLOR_PALETTE.map(JSON.stringify))].map(JSON.parse); uniqueColors.forEach(rgb => { @@ -1386,6 +1209,7 @@ window.addEventListener('message', (event) => { const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name); + // Default state: free are active, paid are not if (!isPaid) { swatch.classList.add('active'); } @@ -1405,13 +1229,18 @@ window.addEventListener('message', (event) => { } }); + // Add event listeners for master buttons container.querySelector('#selectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(true, false)); container.querySelector('#unselectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(false, false)); container.querySelector('#selectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(true, true)); container.querySelector('#unselectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(false, true)); + // Set the initial state updateActiveColorPalette(); } + // --- END: Color Palette Functions --- + + // --- START: Auto-CAPTCHA Solver --- async function handleCaptcha() { return new Promise(async (resolve, reject) => { if (!CONFIG.AUTO_CAPTCHA_ENABLED) { @@ -1419,19 +1248,23 @@ window.addEventListener('message', (event) => { } try { + // Set a timeout for the entire operation const timeoutPromise = Utils.sleep(20000).then(() => reject(new Error("Auto-CAPTCHA timed out."))); const solvePromise = (async () => { + // 1. Find and click the main "Paint" button on the screen const mainPaintBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', 200, 10000); if (!mainPaintBtn) throw new Error("Could not find the main paint button."); mainPaintBtn.click(); await Utils.sleep(500); + // 2. Select the transparent color to avoid wasting a real color const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); if (!transBtn) throw new Error("Could not find the transparent color button."); transBtn.click(); await Utils.sleep(500); + // 3. Find the canvas and simulate a click const canvas = await Utils.waitForSelector('canvas', 200, 5000); if (!canvas) throw new Error("Could not find the canvas element."); @@ -1447,6 +1280,7 @@ window.addEventListener('message', (event) => { canvas.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true })); await Utils.sleep(500); + // 4. Find and click the confirmation button let confirmBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl'); if (!confirmBtn) { const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); @@ -1455,6 +1289,7 @@ window.addEventListener('message', (event) => { if (!confirmBtn) throw new Error("Could not find the confirmation button."); confirmBtn.click(); + // The fetch interceptor will capture the token. We wait for it. await tokenPromise; resolve(); })(); @@ -1467,11 +1302,56 @@ window.addEventListener('message', (event) => { } }); } + // --- END: Auto-CAPTCHA Solver --- + + // --- START: Overlay Management --- + async function initializeOverlay() { + state.gameCanvas = await Utils.waitForSelector('canvas'); + if (!state.gameCanvas || document.getElementById('wplace-overlay-canvas')) { + console.log("Overlay not initialized (game canvas not found or overlay exists)."); + return; // Already initialized or game canvas not found + } + + state.overlayCanvas = document.createElement('canvas'); + state.overlayCanvas.id = 'wplace-overlay-canvas'; + state.overlayCtx = state.overlayCanvas.getContext('2d'); + + // Style the overlay + state.overlayCanvas.style.position = 'absolute'; + state.overlayCanvas.style.pointerEvents = 'none'; + state.overlayCanvas.style.zIndex = '1'; // Just above the game canvas + state.overlayCanvas.style.display = 'none'; + state.overlayCanvas.style.imageRendering = 'pixelated'; // Keep pixels sharp + + // Insert it into the DOM, right after the game canvas + state.gameCanvas.parentElement.insertBefore(state.overlayCanvas, state.gameCanvas.nextSibling); + + const syncOverlay = () => { + if (!state.gameCanvas || !state.overlayCanvas) return; + // The canvas element's width/height attributes are the resolution + // The style width/height are the display size, but transform handles that. + state.overlayCanvas.width = state.gameCanvas.width; + state.overlayCanvas.height = state.gameCanvas.height; + // Copy position and transform to align perfectly + state.overlayCanvas.style.top = state.gameCanvas.offsetTop + 'px'; + state.overlayCanvas.style.left = state.gameCanvas.offsetLeft + 'px'; + state.overlayCanvas.style.transform = state.gameCanvas.style.transform; + }; + + // Use a MutationObserver to watch for style changes (pan/zoom) + const observer = new MutationObserver(syncOverlay); + observer.observe(state.gameCanvas, { attributes: true, attributeFilter: ['style', 'width', 'height'] }); + + syncOverlay(); // Initial sync + console.log("✅ Overlay Initialized."); + } + // --- END: Overlay Management --- async function createUI() { await detectLanguage() + // Clean up existing UI elements to prevent duplicates const existingContainer = document.getElementById("wplace-image-bot-container") const existingStats = document.getElementById("wplace-stats-container") const existingSettings = document.getElementById("wplace-settings-container") @@ -1800,7 +1680,7 @@ window.addEventListener('message', (event) => { ? `linear-gradient(135deg, ${theme.accent} 0%, #4a4a4a 100%)` : theme.accent }; - ${CONFIG.currentTheme === "Classic Autobot" ? "border: 1px solid rgba(255,255,255,0.1);" : ""} + ${CONFIG.currentTheme === "Classic Autobot" ? `border: 1px solid rgba(255,255,255,0.1);` : ""} } ${ @@ -1914,11 +1794,6 @@ window.addEventListener('message', (event) => { .wplace-btn:disabled::before { display: none; } - - .wplace-btn-overlay.active { - background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); - box-shadow: 0 0 15px #8e2de2; - } .wplace-stats { background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.03)" : theme.secondary}; @@ -1952,6 +1827,7 @@ window.addEventListener('message', (event) => { color: ${theme.highlight}; } + /* Styles for the new color display in stats */ .wplace-colors-section { margin-top: 10px; padding-top: 8px; @@ -2101,7 +1977,7 @@ window.addEventListener('message', (event) => { CONFIG.currentTheme === "Classic Autobot" ? "0 0 20px rgba(0,0,0,0.5)" : "0 0 30px rgba(0, 255, 65, 0.5)" }; width: 90%; - max-width: 700px; + max-width: 700px; /* Increased width */ max-height: 90%; overflow: auto; font-family: ${theme.fontFamily}; @@ -2119,7 +1995,7 @@ window.addEventListener('message', (event) => { } .resize-preview { - max-width: none; + max-width: none; /* Allow image to exceed wrapper for zoom */ transition: transform 0.1s ease; image-rendering: pixelated; image-rendering: -moz-crisp-edges; @@ -2176,7 +2052,7 @@ window.addEventListener('message', (event) => { } .resize-zoom-controls { - grid-column: 1 / -1; + grid-column: 1 / -1; /* Span full width */ display: flex; align-items: center; gap: 10px; @@ -2200,9 +2076,10 @@ window.addEventListener('message', (event) => { z-index: 9999; display: none; } + /* --- START: Color Palette Styles --- */ .wplace-color-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); /* Wider columns for name */ gap: 10px; padding-top: 8px; } @@ -2259,7 +2136,9 @@ window.addEventListener('message', (event) => { background: rgba(255,255,255,0.1); margin: 8px 0; } + /* --- END: Color Palette Styles --- */ + /* Cooldown slider styles */ .wplace-cooldown-control { margin-top: 8px; } @@ -2295,6 +2174,7 @@ window.addEventListener('message', (event) => { ${ CONFIG.currentTheme === "Neon Retro" ? ` + /* Retro checkbox styling */ input[type="checkbox"] { -webkit-appearance: none; -moz-appearance: none; @@ -2322,10 +2202,12 @@ window.addEventListener('message', (event) => { font-weight: bold; } + /* Icon styling for retro feel */ .fas, .fa { filter: drop-shadow(0 0 3px currentColor); } + /* Speed Control Styles */ .wplace-speed-control { margin-top: 12px; padding: 12px; @@ -2408,6 +2290,7 @@ window.addEventListener('message', (event) => { opacity: 0.8; } + /* Settings Window Styles */ #wplace-settings-container { position: fixed; top: 50%; @@ -2719,12 +2602,6 @@ window.addEventListener('message', (event) => { ${Utils.t("stopPainting")} -
- -
@@ -3056,6 +2933,7 @@ window.addEventListener('message', (event) => { background: #4a5568; } + /* Dragging state styles */ .wplace-dragging { opacity: 0.9; box-shadow: 0 30px 60px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.2); @@ -3078,11 +2956,11 @@ window.addEventListener('message', (event) => {

${Utils.t("resizeImage")}

+
 Color Palette @@ -3122,6 +3001,7 @@ window.addEventListener('message', (event) => {
+