您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Upload and overlay images onto tiles on wplace.live.
当前为
// ==UserScript== // @name Wplace Overlay Pro // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Upload and overlay images onto tiles on wplace.live. // @author shinkonet // @match https://wplace.live/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function () { 'use strict'; // Default template: 12x12 white square with an X const DEFAULT_TEMPLATE_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAFFJREFUGJWNkEEOACEMAinZ/3+5Hki6ihzk0minFQQeVCrdndtVAChCh5vQMGeTcUP8kHE7AeDbp4czi8SDDkiv3P5oRMzB6NRzzFV0E/8vawFhxDAS2NwXhAAAAABJRU5ErkJggg=="; // Persisted config const config = { overlayImageBase64: DEFAULT_TEMPLATE_BASE64, pixelUrl: "https://backend.wplace.live/s0/pixel/1851/1258?x=22&y=217", offsetX: 0, offsetY: 0, opacity: 0.7, overlayMode: "overlay", isPanelCollapsed: false, autoCapturePixelUrl: true }; // Page realm const page = unsafeWindow; // Utils async function loadConfig() { try { config.overlayImageBase64 = await GM_getValue("overlayImageBase64", config.overlayImageBase64); config.pixelUrl = await GM_getValue("pixelUrl", config.pixelUrl); config.offsetX = await GM_getValue("offsetX", config.offsetX); config.offsetY = await GM_getValue("offsetY", config.offsetY); config.opacity = await GM_getValue("opacity", config.opacity); config.overlayMode = await GM_getValue("overlayMode", config.overlayMode); config.isPanelCollapsed = await GM_getValue("isPanelCollapsed", config.isPanelCollapsed); config.autoCapturePixelUrl = await GM_getValue("autoCapturePixelUrl", config.autoCapturePixelUrl); } catch (e) { console.error("Overlay Pro: Failed to load config", e); } } async function saveConfig() { try { await GM_setValue("overlayImageBase64", config.overlayImageBase64); await GM_setValue("pixelUrl", config.pixelUrl); await GM_setValue("offsetX", config.offsetX); await GM_setValue("offsetY", config.offsetY); await GM_setValue("opacity", config.opacity); await GM_setValue("overlayMode", config.overlayMode); await GM_setValue("isPanelCollapsed", config.isPanelCollapsed); await GM_setValue("autoCapturePixelUrl", config.autoCapturePixelUrl); } catch (e) { console.error("Overlay Pro: Failed to save config", e); } } function createCanvas(w, h) { // OffscreenCanvas is slightly more performant and avoids polluting the DOM. if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h); const c = document.createElement('canvas'); c.width = w; c.height = h; return c; } function canvasToBlob(canvas) { if (canvas.convertToBlob) return canvas.convertToBlob(); // For OffscreenCanvas return new Promise((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png")); } function blobToImage(blob) { return createImageBitmap(blob); } function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } function extractPixelCoords(pixelUrl) { try { const u = new URL(pixelUrl); const parts = u.pathname.split('/'); const sp = new URLSearchParams(u.search); return { chunk1: parseInt(parts[3], 10), chunk2: parseInt(parts[4], 10), posX: parseInt(sp.get('x') || '0', 10), posY: parseInt(sp.get('y') || '0', 10), }; } catch { return { chunk1: 0, chunk2: 0, posX: 0, posY: 0 }; } } function matchTileUrl(urlStr) { try { const u = new URL(urlStr, location.href); if (u.hostname !== 'backend.wplace.live' || !u.pathname.startsWith('/files/')) return null; const m = u.pathname.match(/\/(\d+)\/(\d+)\.png$/i); if (!m) return null; return { chunk1: parseInt(m[1], 10), chunk2: parseInt(m[2], 10) }; } catch { return null; } } function matchPixelUrl(urlStr) { try { const u = new URL(urlStr, location.href); if (u.hostname !== 'backend.wplace.live') return null; const m = u.pathname.match(/\/s0\/pixel\/(\d+)\/(\d+)$/); if (!m) return null; const sp = u.searchParams; return { normalized: `https://backend.wplace.live/s0/pixel/${m[1]}/${m[2]}?x=${sp.get('x')||0}&y=${sp.get('y')||0}` }; } catch { return null; } } // Build overlay data for a specific chunk async function buildOverlayDataForChunk(targetChunk1, targetChunk2) { if (!config.overlayImageBase64) return null; const img = await loadImage(config.overlayImageBase64); if (!img) return null; const base = extractPixelCoords(config.pixelUrl); if (!Number.isFinite(base.chunk1) || !Number.isFinite(base.chunk2)) return null; const TILE_SIZE = 1000; const drawX = (base.chunk1 * TILE_SIZE + base.posX + config.offsetX) - (targetChunk1 * TILE_SIZE); const drawY = (base.chunk2 * TILE_SIZE + base.posY + config.offsetY) - (targetChunk2 * TILE_SIZE); const overlayCanvas = createCanvas(TILE_SIZE, TILE_SIZE); const ctx = overlayCanvas.getContext('2d'); ctx.drawImage(img, drawX, drawY); // Apply the opacity blending. This algorithm blends the template color with white. const imageData = ctx.getImageData(0, 0, TILE_SIZE, TILE_SIZE); const data = imageData.data; const colorStrength = config.opacity; const whiteStrength = 1 - colorStrength; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] > 0) { // Only modify non-transparent pixels data[i] = Math.round(data[i] * colorStrength + 255 * whiteStrength); data[i + 1] = Math.round(data[i + 1] * colorStrength + 255 * whiteStrength); data[i + 2] = Math.round(data[i + 2] * colorStrength + 255 * whiteStrength); data[i + 3] = 255; // Make it fully opaque to sit behind the original } } return imageData; } // Merge overlay behind original pixels (returns a new Blob) async function mergeChunk(originalBlob, overlayData) { if (!overlayData) return originalBlob; const originalImage = await blobToImage(originalBlob); const w = originalImage.width; const h = originalImage.height; const canvas = createCanvas(w, h); const ctx = canvas.getContext('2d'); // Draw the generated overlay first ctx.putImageData(overlayData, 0, 0); // Draw the original tile image on top of the overlay ctx.drawImage(originalImage, 0, 0); return await canvasToBlob(canvas); } // Network hook to intercept tile requests function hookFetch() { const originalFetch = page.fetch; if (originalFetch.__overlayHooked) return; const hookedFetch = async (input, init) => { const urlStr = typeof input === 'string' ? input : (input && input.url) || ''; // Auto-capture pixel placement URLs if (config.autoCapturePixelUrl) { const pixelMatch = matchPixelUrl(urlStr); if (pixelMatch && pixelMatch.normalized !== config.pixelUrl) { config.pixelUrl = pixelMatch.normalized; saveConfig(); updateUI(); } } const tileMatch = matchTileUrl(urlStr); // If it's not a tile URL, or the overlay is off, proceed with the original request if (!tileMatch || config.overlayMode !== 'overlay' || !config.overlayImageBase64) { return originalFetch(input, init); } try { const response = await originalFetch(input, init); if (!response.ok) return response; const originalBlob = await response.blob(); const overlayData = await buildOverlayDataForChunk(tileMatch.chunk1, tileMatch.chunk2); const mergedBlob = await mergeChunk(originalBlob, overlayData); // Return a new response with the merged image return new Response(mergedBlob, { status: response.status, statusText: response.statusText, headers: { 'Content-Type': 'image/png' } }); } catch (e) { console.error("Overlay Pro: Error processing tile", e); // Fallback to original fetch on error return originalFetch(input, init); } }; hookedFetch.__overlayHooked = true; page.fetch = hookedFetch; // Also hook window.fetch for broader compatibility window.fetch = hookedFetch; console.log('Overlay Pro (Slim): Fetch hook installed.'); } // UI Injection and Management function injectStyles() { const style = document.createElement('style'); style.textContent = ` #overlay-pro-panel { position: fixed; top: 230px; right: 15px; z-index: 10001; background: rgba(20,20,20,0.9); backdrop-filter: blur(8px); border: 1px solid #444; border-radius: 8px; color: white; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-size: 14px; transition: transform 0.3s ease-in-out; } .op-header { padding: 8px 12px; background: #333; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-top-left-radius: 8px; border-top-right-radius: 8px; } .op-header h3 { margin: 0; font-size: 16px; user-select: none; } .op-toggle-btn { font-size: 20px; background: none; border: none; color: white; cursor: pointer; padding: 0 5px; } .op-content { padding: 12px; display: flex; flex-direction: column; gap: 12px; width: 280px; } .op-control-group { display: flex; flex-direction: column; gap: 6px; } .op-control-row { display: flex; align-items: center; gap: 8px; } .op-button { background: #555; color: white; border: 1px solid #777; border-radius: 4px; padding: 6px 10px; cursor: pointer; flex-grow: 1; } .op-button:hover { background: #666; } .op-button.active { background: #007bff; border-color: #0056b3; font-weight: bold; } .op-input { background: #222; border: 1px solid #555; color: white; border-radius: 4px; padding: 5px; flex-grow: 1; } .op-input[type="number"] { text-align: center; min-width: 60px; } .op-slider { width: 100%; } #op-coord-display { font-size: 12px; color: #ccc; word-break: break-all; } #op-dropzone { border: 2px dashed #555; border-radius: 4px; padding: 10px; text-align: center; background-size: contain; background-position: center; background-repeat: no-repeat; min-height: 56px; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s; } #op-dropzone.dragover { border-color: #007bff; } .op-full-width-btn { width: 100%; } `; document.head.appendChild(style); } function createUI() { if (document.getElementById('overlay-pro-panel')) return; const panel = document.createElement('div'); panel.id = 'overlay-pro-panel'; panel.innerHTML = ` <div class="op-header" id="op-header"> <h3>Overlay Pro</h3> <button class="op-toggle-btn" id="op-panel-toggle">▶</button> </div> <div class="op-content" id="op-content"> <div class="op-control-group"> <label>Template Image</label> <div id="op-dropzone">Drop image here or click</div> <input type="file" id="op-file-input" accept="image/*" style="display: none;" /> </div> <div class="op-control-group"> <label>Mode</label> <button class="op-button op-full-width-btn" id="op-mode-toggle"></button> </div> <div class="op-control-group"> <label>Auto-capture Pixel URL</label> <button class="op-button op-full-width-btn" id="op-autocap-toggle"></button> <div id="op-coord-display"></div> </div> <div class="op-control-group"> <label>Offset</label> <div class="op-control-row"> <span>X:</span> <input type="number" class="op-input" id="op-offset-x" /> <button class="op-button" data-offset="x" data-amount="-1">-</button> <button class="op-button" data-offset="x" data-amount="1">+</button> </div> <div class="op-control-row"> <span>Y:</span> <input type="number" class="op-input" id="op-offset-y" /> <button class="op-button" data-offset="y" data-amount="-1">-</button> <button class="op-button" data-offset="y" data-amount="1">+</button> </div> </div> <div class="op-control-group"> <label>Opacity: <span id="op-opacity-value">70%</span></label> <input type="range" min="0" max="1" step="0.05" class="op-slider" id="op-opacity-slider" /> </div> <div class="op-control-group"> <button class="op-button op-full-width-btn" id="op-reload-btn">Reload Tiles (Refresh Page)</button> </div> </div> `; document.body.appendChild(panel); addEventListeners(); updateUI(); } function handleImageFile(file) { if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = async (event) => { config.overlayImageBase64 = event.target.result; await saveConfig(); updateUI(); alert("Template updated. You may need to refresh the page to see changes on already loaded tiles."); }; reader.readAsDataURL(file); } } function addEventListeners() { document.getElementById('op-header').addEventListener('click', () => { config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(); updateUI(); }); document.getElementById('op-mode-toggle').addEventListener('click', () => { config.overlayMode = config.overlayMode === 'overlay' ? 'original' : 'overlay'; saveConfig(); updateUI(); }); document.getElementById('op-autocap-toggle').addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(); updateUI(); }); document.querySelectorAll('[data-offset]').forEach(btn => btn.addEventListener('click', () => { const offset = btn.dataset.offset; const amount = parseInt(btn.dataset.amount, 10); if (offset === 'x') config.offsetX += amount; if (offset === 'y') config.offsetY += amount; saveConfig(); updateUI(); })); document.getElementById('op-offset-x').addEventListener('change', e => { config.offsetX = parseInt(e.target.value, 10) || 0; saveConfig(); }); document.getElementById('op-offset-y').addEventListener('change', e => { config.offsetY = parseInt(e.target.value, 10) || 0; saveConfig(); }); document.getElementById('op-opacity-slider').addEventListener('input', e => { config.opacity = parseFloat(e.target.value); document.getElementById('op-opacity-value').textContent = Math.round(config.opacity * 100) + '%'; }); document.getElementById('op-opacity-slider').addEventListener('change', () => { saveConfig(); }); const dropzone = document.getElementById('op-dropzone'); const fileInput = document.getElementById('op-file-input'); dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', (e) => handleImageFile(e.target.files[0])); dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); }); dropzone.addEventListener('dragleave', e => { e.preventDefault(); dropzone.classList.remove('dragover'); }); dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('dragover'); handleImageFile(e.dataTransfer.files[0]); }); document.getElementById('op-reload-btn').addEventListener('click', () => { location.reload(); }); } function updateUI() { const panel = document.getElementById('overlay-pro-panel'); if (!panel) return; panel.classList.toggle('collapsed', config.isPanelCollapsed); document.getElementById('op-content').style.display = config.isPanelCollapsed ? 'none' : 'flex'; document.getElementById('op-panel-toggle').textContent = config.isPanelCollapsed ? '◀' : '▶'; const dropzone = document.getElementById('op-dropzone'); if (config.overlayImageBase64 && config.overlayImageBase64 !== DEFAULT_TEMPLATE_BASE64) { dropzone.textContent = ''; dropzone.style.backgroundImage = `url(${config.overlayImageBase64})`; } else { dropzone.textContent = 'Drop image or click'; dropzone.style.backgroundImage = `url(${DEFAULT_TEMPLATE_BASE64})`; } const modeBtn = document.getElementById('op-mode-toggle'); modeBtn.textContent = `Mode: ${config.overlayMode.charAt(0).toUpperCase() + config.overlayMode.slice(1)}`; modeBtn.classList.toggle('active', config.overlayMode === 'overlay'); const autoBtn = document.getElementById('op-autocap-toggle'); autoBtn.textContent = `Auto-capture: ${config.autoCapturePixelUrl ? 'ON' : 'OFF'}`; autoBtn.classList.toggle('active', !!config.autoCapturePixelUrl); const coords = extractPixelCoords(config.pixelUrl); document.getElementById('op-coord-display').textContent = `Ref: chunk ${coords.chunk1}/${coords.chunk2} at (${coords.posX}, ${coords.posY})`; document.getElementById('op-offset-x').value = config.offsetX; document.getElementById('op-offset-y').value = config.offsetY; document.getElementById('op-opacity-slider').value = String(config.opacity); document.getElementById('op-opacity-value').textContent = Math.round(config.opacity * 100) + '%'; } // Main execution async function main() { await loadConfig(); injectStyles(); // Create the UI once the DOM is ready if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } // Install the core network hook hookFetch(); console.log("Overlay Pro (Slim) v3.0.0: Initialized successfully."); } main(); })();