您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Overlay for Wplace
// ==UserScript== // @name Wplace Overlay // @namespace https://github.com/cfpwastaken/wplace-overlay // @version v2.0 // @description Overlay for Wplace // @author cfp // @match https://wplace.live/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wplace.live // @license GPL-3.0-or-later // @grant none // ==/UserScript== let overlayMode = "over"; const OVERLAY_MODES = ["aus", "over", "difference", "out", "fill"]; let darken = false; // ============================================================= // Want to add your own image to the overlay? // We can help you with that on our discord server. // See our web page on https://cfp.is-a.dev/wplace/ for details. // ============================================================= const HOST = "cfp.is-a.dev"; const SUBPATH = "/wplace"; // Worker for canvas operations const WORKER_CODE = ` self.addEventListener("message", async (event) => { const { id, originalBlob, overlayBlob, width, height, darken, overlayMode } = event.data; const OVERLAY_MODES = {"over": "source-over", "difference": "difference", "out": "source-out", "fill": "source-over"} const originalBitmap = await createImageBitmap(originalBlob); const overlayBitmap = await createImageBitmap(overlayBlob); const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext("2d"); ctx.imageSmoothingEnabled = false; ctx.drawImage(originalBitmap, 0, 0, width, height); ctx.globalCompositeOperation = OVERLAY_MODES[overlayMode] || "source-over"; ctx.drawImage(overlayBitmap, 0, 0, width, height); if(darken) { ctx.globalCompositeOperation = "destination-over"; ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.fillRect(0, 0, width, height); } ctx.globalCompositeOperation = "source-over"; const resultBlob = await canvas.convertToBlob(); self.postMessage({ id, resultBlob }); }) ` const workerBlob = new Blob([WORKER_CODE], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(workerBlob); const worker = new Worker(workerUrl); const pending = new Map(); worker.onmessage = (e) => { const { id, resultBlob, error } = e.data; if (!pending.has(id)) return; if (error) { pending.get(id).reject(new Error(error)); } else { pending.get(id).resolve(resultBlob); } pending.delete(id); }; function postToWorker(data) { return new Promise((resolve, reject) => { pending.set(data.id, { resolve, reject }); worker.postMessage(data); }); } const fallbackSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"> <rect width="64" height="64" fill="#ccc"/> <text x="32" y="37" font-size="24" text-anchor="middle" fill="#333">!</text> </svg>`; const fallbackBlob = new Blob([fallbackSVG], { type: "image/svg+xml" }); const originalFetch = fetch; fetch = new Proxy(fetch, { apply: async (target, thisArg, argList) => { console.log(target, thisArg, argList); if(!argList[0]) { throw new Error("No URL provided to fetch"); } const urlString = typeof argList[0] === "object" ? argList[0].url : argList[0]; let url; try { url = new URL(urlString); } catch (e) { throw new Error("Invalid URL provided to fetch"); } if (url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) { console.log("Intercepted fetch request to wplace.live"); if(overlayMode !== "aus") { // url.host = HOST; // url.pathname = `${SUBPATH}${url.pathname}`; // url.searchParams.set("blending", overlayMode); // url.searchParams.set("darken", darken + ""); // console.log("Modified URL:", url); // if(typeof argList[0] === "object") { // argList[0] = new Request(url, argList[0]); // } else { // argList[0] = url.toString(); // } // backend.wplace.live/files/s0/tiles/X/Y.png const tileX = url.pathname.split("/")[4]; let tileY = url.pathname.split("/")[5]; if(overlayMode !== "over") { tileY = tileY.replace(".png", "_orig.png"); } const overlayUrl = `https://${HOST}${SUBPATH}/tiles/${tileX}/${tileY}`; const [originalRes, overlayRes] = await Promise.all([ originalFetch(url), originalFetch(overlayUrl) ]) if(overlayRes.status !== 200) { if(overlayRes.status === 404) { return originalRes; } console.error(`Overlay fetch failed with status ${overlayRes.status}, returning fallback`); return new Response(fallbackBlob, { status: 200, statusText: "OK", headers: { "Content-Type": fallbackBlob.type, "Cache-Control": "no-cache", } }); } if(originalRes.status !== 200) { if(originalRes.status === 404) { return overlayRes; } // throw new Error(`Original fetch failed with status ${originalRes.status}`); console.error(`Original fetch failed with status ${originalRes.status}, returning fallback`); return new Response(fallbackBlob, { status: 200, statusText: "OK", headers: { "Content-Type": fallbackBlob.type, "Cache-Control": "no-cache", } }); } const [originalBlob, overlayBlob] = await Promise.all([ originalRes.blob(), overlayRes.blob() ]); const width = 3000; const height = 3000; const id = crypto.randomUUID(); const resultBlob = await postToWorker({ id, originalBlob, overlayBlob, width, height, darken, overlayMode }); reloadText.style.display = "none"; return new Response(resultBlob, { status: 200, statusText: "OK", headers: { "Content-Type": resultBlob.type, "Cache-Control": originalRes.headers.get("Cache-Control") || "no-cache", } }); } reloadText.style.display = "none"; } return target.apply(thisArg, argList); } }); let reloadText = document.createElement("span"); function patchUI() { if(document.getElementById("overlay-blend-button")) { return; // Button already exists, no need to patch again } let blendButton = document.createElement("button"); blendButton.id = "overlay-blend-button"; blendButton.textContent = "Overlay: " + overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1); blendButton.style.backgroundColor = "#0e0e0e7f"; blendButton.style.color = "white"; blendButton.style.border = "solid"; blendButton.style.borderColor = "#1d1d1d7f"; blendButton.style.borderRadius = "4px"; blendButton.style.padding = "5px 10px"; blendButton.style.cursor = "pointer"; blendButton.style.backdropFilter = "blur(2px)"; blendButton.addEventListener("click", () => { overlayMode = OVERLAY_MODES[(OVERLAY_MODES.indexOf(overlayMode) + 1) % OVERLAY_MODES.length]; blendButton.textContent = `Overlay: ${overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1)}`; console.log("Overlay mode set to:", overlayMode); reloadText.style.display = ""; }); let darkenMode = document.createElement("button"); darkenMode.textContent = "Darken: " + (darken ? "An" : "Aus"); darkenMode.style.backgroundColor = "#0e0e0e7f"; darkenMode.style.color = "white"; darkenMode.style.border = "solid"; darkenMode.style.borderColor = "#1d1d1d7f"; darkenMode.style.borderRadius = "4px"; darkenMode.style.padding = "5px 10px"; darkenMode.style.cursor = "pointer"; darkenMode.style.backdropFilter = "blur(2px)"; darkenMode.addEventListener("click", () => { darken = !darken; darkenMode.textContent = `Darken: ${darken ? "An" : "Aus"}`; console.log("Darken mode set to:", darken); reloadText.style.display = ""; }); reloadText.textContent = "Rein und wieder raus zoomen, um das Overlay zu sehen!"; reloadText.style.color = "red"; reloadText.style.fontWeight = "bold"; reloadText.style.maxWidth = "200px"; reloadText.style.textAlign = "right"; reloadText.style.backgroundColor = "#ffffff7f"; reloadText.style.borderRadius = "4px"; reloadText.style.backdropFilter = "blur(2px)"; const buttonContainer = document.querySelector("div.gap-4:nth-child(1) > div:nth-child(2)"); const leftSidebar = document.querySelector("html body div div.disable-pinch-zoom.relative.h-full.overflow-hidden.svelte-6wmtgk div.absolute.right-2.top-2.z-30 div.flex.flex-col.gap-4.items-center"); if(buttonContainer) { buttonContainer.appendChild(blendButton); buttonContainer.appendChild(darkenMode); buttonContainer.appendChild(reloadText); buttonContainer.classList.remove("items-center"); buttonContainer.classList.add("items-end"); } if(leftSidebar) { leftSidebar.classList.add("items-end"); leftSidebar.classList.remove("items-center"); } } const observer = new MutationObserver(() => { patchUI(); }); observer.observe(document.querySelector("div.gap-4:nth-child(1)"), { childList: true, subtree: true }); patchUI();