wplace overlay script made by Furry Crew

Minimal overlay. No export/import. Camera lock infra kept without UI. GUI can be minimized.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         wplace overlay script made by Furry Crew
// @namespace    https://c11v.dev/userscripts/wplace-overlay
// @version      2.3.1
// @description  Minimal overlay. No export/import. Camera lock infra kept without UI. GUI can be minimized.
// @match        https://wplace.live/*
// @run-at       document-idle
// @inject-into  page
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==

(function () {
  // single-instance guard
  const KEY = "__WPO_SIMPLE_SINGLETON__";
  try { if (window[KEY]?.destroy) window[KEY].destroy(true); } catch {}
  const singleton = { destroy: ()=>{} };
  window[KEY] = singleton;

  const LS_KEY = "wplace.overlay.gui.simple.v1";
  const WM_TEXT_MAIN = "Made by Furry Crew";

  const log = (...a)=>console.log("[wplace-overlay]", ...a);
  const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));

  function toast(msg){
    const el=document.createElement("div");
    el.textContent=msg;
    el.style.cssText="position:fixed;left:50%;bottom:16px;transform:translateX(-50%);padding:8px 12px;background:rgba(0,0,0,.85);color:#fff;font:12px system-ui,sans-serif;border-radius:8px;z-index:2147483647";
    document.body.appendChild(el);
    setTimeout(()=>el.remove(),1200);
  }

  async function waitForBody(){
    if(document.body) return;
    await new Promise(r=>{
      const obs=new MutationObserver(()=>{
        if(document.body){ obs.disconnect(); r(); }
      });
      obs.observe(document.documentElement,{childList:true,subtree:true});
    });
  }

  let state = {
    on:true, x:0, y:0, scale:1, rot:0, opacity:0.25,
    imageInfo:{ w:0, h:0, name:"" },
    step:5,
    lock:false,
    minimized:false
  };
  try {
    const prev = JSON.parse(localStorage.getItem(LS_KEY)||"{}");
    Object.assign(state, prev);
  } catch {}

  function save(){ localStorage.setItem(LS_KEY, JSON.stringify(state)); }

  // overlay canvas
  let cvs=null, ctx=null, imgBitmap=null, iw=0, ih=0;
  function makeCanvas(){
    cvs=document.createElement("canvas");
    ctx=cvs.getContext("2d");
    Object.assign(cvs.style,{
      position:"fixed", top:"0", left:"0", width:"100vw", height:"100vh",
      pointerEvents:"none", zIndex:"2147483647"
    });
    cvs.width=window.innerWidth; cvs.height=window.innerHeight;
    const onResize=()=>{ cvs.width=innerWidth; cvs.height=innerHeight; draw(); };
    window.addEventListener("resize", onResize);
    cvs.__onResize = onResize;
    document.body.appendChild(cvs);
  }
  function clear(){ if(!ctx) return; ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,cvs.width,cvs.height); }

  function drawWatermark(){
    const pad = 10, baseSize = 14;
    ctx.save();
    ctx.imageSmoothingEnabled=true;
    ctx.globalAlpha = 0.9;
    ctx.font = `600 ${baseSize}px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
    ctx.textAlign = "left";
    ctx.textBaseline = "bottom";
    ctx.shadowColor = "rgba(0,0,0,0.7)";
    ctx.shadowBlur = 4;
    ctx.shadowOffsetY = 1;
    ctx.fillStyle = "white";
    ctx.fillText(WM_TEXT_MAIN, 10, cvs.height - pad);
    ctx.restore();
  }

  function draw(){
    if(!ctx) return;
    clear();
    if(imgBitmap && state.on){
      const cx=cvs.width/2, cy=cvs.height/2;
      ctx.save();
      ctx.globalAlpha=clamp(state.opacity,0,1);
      ctx.imageSmoothingEnabled=false;
      ctx.translate(cx+state.x, cy+state.y);
      ctx.rotate(state.rot*Math.PI/180);
      ctx.scale(state.scale, state.scale);
      ctx.drawImage(imgBitmap, -iw/2, -ih/2);
      ctx.restore();
    }
    drawWatermark();
  }

  async function loadFromFile(file){
    try{
      const bmp=await createImageBitmap(file);
      imgBitmap=bmp; iw=bmp.width; ih=bmp.height;
      state.imageInfo={ w:iw, h:ih, name:file.name||"" };
      draw(); refreshFooter();
      toast(`Loaded ${file.name} ${iw}×${ih}`);
      save();
    }catch{ toast("Failed to load file"); }
  }
  function chooseImageFile(){
    const inp=document.createElement("input");
    inp.type="file";
    inp.accept="image/png,image/jpeg,image/webp,image/gif";
    inp.onchange=()=>{ const f=inp.files && inp.files[0]; if(f) loadFromFile(f); };
    inp.click();
  }

  // camera lock infra kept without UI
  let isLeftDown = false;
  const lockProxy = document.createElement("div");
  Object.assign(lockProxy.style, {
    position:"fixed", inset:"0", zIndex:"2147483646", pointerEvents:"none"
  });
  lockProxy.onclick = (e)=>{
    const target = document.elementFromPoint(e.clientX, e.clientY);
    if(target && target !== lockProxy){
      const evt = new MouseEvent("click", {bubbles:true, cancelable:true, clientX:e.clientX, clientY:e.clientY, button:0});
      target.dispatchEvent(evt);
    }
  };
  ["mousedown","mousemove","mouseup","wheel","contextmenu","touchstart","touchmove","touchend"].forEach(type=>{
    lockProxy.addEventListener(type, ev=>{
      ev.stopImmediatePropagation();
      ev.preventDefault();
    }, {passive:false});
  });

  function applyLockSideEffects(){
    lockProxy.style.pointerEvents = state.lock ? "auto" : "none";
  }

  function installLockHandlers(){
    window.addEventListener("wheel", (e)=>{
      if(!state.lock) return;
      e.stopImmediatePropagation();
      e.preventDefault();
    }, {capture:true, passive:false});

    window.addEventListener("mousedown", (e)=>{
      if(!state.lock) return;
      isLeftDown = e.button === 0;
      e.stopImmediatePropagation();
      e.preventDefault();
    }, {capture:true});

    window.addEventListener("mousemove", (e)=>{
      if(!state.lock) return;
      if(isLeftDown){
        e.stopImmediatePropagation();
        e.preventDefault();
      }
    }, {capture:true});

    window.addEventListener("mouseup", (e)=>{
      if(!state.lock) return;
      isLeftDown = false;
      e.stopImmediatePropagation();
      e.preventDefault();
    }, {capture:true});

    window.addEventListener("touchstart", (e)=>{
      if(!state.lock) return;
      e.stopImmediatePropagation();
      e.preventDefault();
    }, {capture:true, passive:false});

    window.addEventListener("touchmove", (e)=>{
      if(!state.lock) return;
      e.stopImmediatePropagation();
      e.preventDefault();
    }, {capture:true, passive:false});

    window.addEventListener("keydown", (e)=>{
      if(!state.lock) return;
      const k = e.key.toLowerCase();
      const block = new Set(["w","a","s","d","arrowup","arrowdown","arrowleft","arrowright","+","-","=","_"," ","pageup","pagedown","home","end"]);
      if(block.has(k) || (e.ctrlKey && (k==="+" || k==="=" || k==="-" ))){
        e.stopImmediatePropagation();
        e.preventDefault();
      }
    }, {capture:true});
  }
  installLockHandlers();

  // GUI
  let guiHost=null, shadow=null, keepAliveId=null;
  function buildGUI(){
    guiHost=document.createElement("div");
    shadow=guiHost.attachShadow({mode:"open"});

    const style=document.createElement("style");
    style.textContent=`
      :host { all: initial; }
      *, *::before, *::after { box-sizing: border-box; }
      .panel{
        position:fixed; top:16px; right:16px;
        background:#0f0f10; color:#eaeaea; font:12px system-ui,sans-serif;
        border:1px solid #222; border-radius:12px; padding:12px; z-index:2147483647;
        box-shadow:0 12px 24px rgba(0,0,0,.4); user-select:none;
        width:auto; max-width:min(420px, calc(100vw - 32px));
        max-height:calc(100vh - 32px); overflow:auto;
      }
      .title{ display:flex; align-items:center; justify-content:space-between; font-weight:700; margin-bottom:8px; cursor:move; gap:8px; }
      .title-left{ display:flex; align-items:center; gap:8px; }
      .sublabel{ color:#a9a9a9; font-size:10px; white-space:nowrap; }
      .title-btn{
        margin-left:auto; background:#171718; color:#eaeaea; border:1px solid #2d2d2f; border-radius:8px;
        padding:4px 8px; cursor:pointer; font:12px system-ui,sans-serif;
      }
      .btns{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:6px; }
      .row{ display:grid; grid-template-columns:1fr auto; gap:8px; align-items:center; margin:10px 0; }
      .controls{ display:grid; grid-template-columns: 1fr minmax(68px,92px); gap:8px; grid-column:1 / span 2; align-items:center; }
      input[type="number"]{
        width:100%; min-width:68px; background:#171718; color:#eaeaea; border:1px solid #2d2d2f; border-radius:8px;
        padding:6px 8px; font:12px system-ui,sans-serif;
      }
      input[type="range"]{ width:100%; }
      button,label{
        background:#171718; color:#eaeaea; border:1px solid #2d2d2f; border-radius:8px;
        padding:6px 8px; cursor:pointer; font:12px system-ui,sans-serif;
      }
      button:active{ transform:translateY(1px); }
      .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
      .footer{
        margin-top:8px; border-top:1px solid #1d1d1f; padding-top:6px;
        display:flex; justify-content:space-between; align-items:center; color:#a6a6a6; font-size:10px;
        gap:8px; flex-wrap:wrap;
      }
      .footer .mono { white-space:nowrap; }
      .dpad{
        display:grid; grid-template-columns:repeat(3,32px); grid-template-rows:repeat(3,32px);
        gap:6px;
      }
      .dpad button{ padding:0; height:32px; width:32px; }
      .dpad .spacer{ visibility:hidden; }
      .min-note{ color:#a6a6a6; font-size:10px; }
      @media (max-width: 420px){
        .controls{ grid-template-columns: 1fr minmax(60px,80px); }
        .dpad{ grid-template-columns:repeat(3,28px); grid-template-rows:repeat(3,28px); }
        .dpad button{ width:28px; height:28px; }
      }
    `;

    const wrap=document.createElement("div");
    wrap.innerHTML=`
      <div class="panel" id="panel" aria-live="polite">
        <div class="title" id="drag">
          <div class="title-left">
            <span>Overlay</span>
            <span class="sublabel">${WM_TEXT_MAIN}</span>
          </div>
          <button id="min" class="title-btn" title="Minimize">▾</button>
        </div>

        <div id="content">
          <div class="btns">
            <button id="load">Load image</button>
            <button id="toggle">${state.on?"Hide":"Show"}</button>
            <button id="save">Save</button>
            <button id="reset">Reset</button>
          </div>

          <div class="row">
            <label>Opacity</label>
            <div class="controls">
              <input class="range" id="opacity" type="range" min="0" max="1" step="0.05" value="${state.opacity}">
              <input class="mono" id="opacity_n" type="number" min="0" max="1" step="0.01" value="${state.opacity.toFixed(2)}">
            </div>
          </div>

          <div class="row">
            <label>Scale</label>
            <div class="controls">
              <input class="range" id="scale" type="range" min="0.01" max="10" step="0.01" value="${state.scale}">
              <input class="mono" id="scale_n" type="number" min="0.01" max="10" step="0.001" value="${state.scale.toFixed(3)}">
            </div>
          </div>

          <div class="row">
            <label>Rotation</label>
            <div class="controls">
              <input class="range" id="rot" type="range" min="-180" max="180" step="1" value="${state.rot}">
              <input class="mono" id="rot_n" type="number" min="-180" max="180" step="1" value="${state.rot.toFixed(0)}">
            </div>
          </div>

          <div class="row">
            <label>Position</label>
            <div class="row" style="grid-column:1 / span 2; grid-template-columns:auto 1fr; align-items:center;">
              <div class="dpad">
                <span class="spacer"></span>
                <button id="up" title="Up">↑</button>
                <span class="spacer"></span>

                <button id="left" title="Left">←</button>
                <button id="center" title="Center">•</button>
                <button id="right" title="Right">→</button>

                <span class="spacer"></span>
                <button id="down" title="Down">↓</button>
                <span class="spacer"></span>
              </div>
              <div style="justify-self:end; display:flex; align-items:center; gap:8px;">
                <span>Step</span>
                <input class="mono" id="step_n" type="number" min="1" max="200" step="1" value="${state.step}">
              </div>
            </div>
          </div>

          <div class="footer">
            <span id="imgmeta">${state.imageInfo?.name ? `${state.imageInfo.name} ${state.imageInfo.w}×${state.imageInfo.h}` : "No image loaded"}</span>
            <span class="mono" id="scale_meta">${state.scale.toFixed(3)}×</span>
          </div>
        </div>
        <div id="minnote" class="min-note" style="display:none;">Minimized. Click ▸ to expand</div>
      </div>
    `;

    shadow.append(style, wrap);
    document.body.appendChild(guiHost);

    const $$=(id)=>shadow.getElementById(id);

    // drag panel
    const panel=$$("panel");
    const drag=$$("drag");
    let dx=0, dy=0, dragging=false;
    drag.addEventListener("mousedown", e=>{
      dragging=true;
      const r=panel.getBoundingClientRect();
      dx=e.clientX-r.left; dy=e.clientY-r.top; e.preventDefault();
    });
    window.addEventListener("mousemove", e=>{
      if(!dragging) return;
      panel.style.left=(e.clientX-dx)+"px";
      panel.style.top=(e.clientY-dy)+"px";
      panel.style.right="auto";
    });
    window.addEventListener("mouseup", ()=>dragging=false);

    // minimize toggle
    const content = $$("content");
    const minBtn = $$("min");
    const minNote = $$("minnote");
    function applyMinimized(){
      if(state.minimized){
        content.style.display="none";
        minNote.style.display="block";
        minBtn.textContent="▸";
        minBtn.title="Expand";
      }else{
        content.style.display="";
        minNote.style.display="none";
        minBtn.textContent="▾";
        minBtn.title="Minimize";
      }
    }
    minBtn.addEventListener("click", ()=>{
      state.minimized = !state.minimized;
      applyMinimized();
      save();
    });
    // also allow double click on title bar
    drag.addEventListener("dblclick", ()=>{
      state.minimized = !state.minimized;
      applyMinimized();
      save();
    });
    applyMinimized();

    // buttons
    $$("load").addEventListener("click", chooseImageFile);
    $$("toggle").addEventListener("click", ()=>{ state.on=!state.on; $$("toggle").textContent=state.on?"Hide":"Show"; draw(); save(); });
    $$("save").addEventListener("click", ()=>{ save(); toast("Saved"); });
    $$("reset").addEventListener("click", ()=>{
      Object.assign(state,{x:0,y:0,scale:1,rot:0});
      $$("scale").value=state.scale; $$("scale_n").value=state.scale.toFixed(3);
      $$("rot").value=state.rot; $$("rot_n").value=state.rot.toFixed(0);
      draw(); refreshFooter(); save();
    });

    // sliders
    const syncRangeNum = (rangeId, numId, onChange) => {
      const r=$$(rangeId), n=$$(numId);
      const fmt = (id,val)=>{
        if(id.includes("scale")) return val.toFixed(3);
        if(id.includes("opacity")) return val.toFixed(2);
        if(id.includes("rot")) return val.toFixed(0);
        return String(val);
      };
      const clampTo = (v,min,max)=>Math.min(max,Math.max(min,v));
      r.addEventListener("input", ()=>{ const v=parseFloat(r.value); n.value=fmt(numId, v); onChange(v); save(); });
      n.addEventListener("change", ()=>{ let v=parseFloat(n.value); if(isNaN(v)) return; let min=parseFloat(r.min), max=parseFloat(r.max); v = clampTo(v,min,max); r.value=String(v); n.value=fmt(numId, v); onChange(v); save(); });
    };

    syncRangeNum("opacity","opacity_n",(v)=>{ state.opacity=v; draw(); });
    syncRangeNum("scale","scale_n",(v)=>{ state.scale=v; draw(); refreshFooter(); });
    syncRangeNum("rot","rot_n",(v)=>{ state.rot=v; draw(); });

    $$("step_n").addEventListener("change", ()=>{
      const v = parseInt($$("step_n").value,10);
      if(!isNaN(v) && v>=1 && v<=200){ state.step=v; save(); }
      $$("step_n").value = String(state.step);
    });

    // dpad
    const move = (dx,dy)=>{ state.x += dx; state.y += dy; draw(); save(); };
    const hold = (el,fn)=>{
      let t, rep;
      const start=()=>{ fn(); t=setTimeout(()=>{ rep=setInterval(fn, 40); }, 300); };
      const stop=()=>{ clearTimeout(t); clearInterval(rep); };
      el.addEventListener("mousedown", start);
      el.addEventListener("mouseup", stop);
      el.addEventListener("mouseleave", stop);
      el.addEventListener("touchstart", e=>{ e.preventDefault(); start(); }, {passive:false});
      el.addEventListener("touchend", stop);
    };
    hold($$("up"),   ()=>move(0, -state.step));
    hold($$("down"), ()=>move(0,  state.step));
    hold($$("left"), ()=>move(-state.step, 0));
    hold($$("right"),()=>move( state.step, 0));
    $$("center").addEventListener("click", ()=>{ state.x=0; state.y=0; draw(); save(); });

    refreshFooter();
  }

  function refreshFooter(){
    try{
      const scaleMeta = shadow?.getElementById("scale_meta");
      if(scaleMeta) scaleMeta.textContent = `${state.scale.toFixed(3)}×`;
      const meta = shadow?.getElementById("imgmeta");
      if(meta) meta.textContent = state.imageInfo?.name ? `${state.imageInfo.name} ${state.imageInfo.w}×${state.imageInfo.h}` : "No image loaded";
    }catch{}
  }

  function applyState(next){
    Object.assign(state, next || {});
    const $ = (id)=>shadow?.getElementById(id);
    if($){
      const s=$("scale"), sn=$("scale_n");
      if(s){ s.value = String(state.scale); }
      if(sn){ sn.value = state.scale.toFixed(3); }
      const r=$("rot"), rn=$("rot_n");
      if(r){ r.value = String(state.rot); }
      if(rn){ rn.value = state.rot.toFixed(0); }
      const op=$("opacity"), opn=$("opacity_n");
      if(op){ op.value = String(state.opacity); }
      if(opn){ opn.value = state.opacity.toFixed(2); }
      const step=$("step_n"); if(step) step.value = String(state.step);
      const tog=$("toggle"); if(tog) tog.textContent = state.on?"Hide":"Show";
      const minBtn = $("min"); if(minBtn){ /* handled by applyMinimized during build */ }
      refreshFooter();
    }
    applyLockSideEffects();
    draw();
  }

  function keepAlive(){
    const ensure=()=>{
      if(!document.body) return;
      if(cvs && !cvs.isConnected){ document.body.appendChild(cvs); }
      if(guiHost && !guiHost.isConnected){ document.body.appendChild(guiHost); }
      if(lockProxy && !lockProxy.isConnected){ document.body.appendChild(lockProxy); }
    };
    return setInterval(ensure, 1000);
  }

  // cleanup
  singleton.destroy = function destroy(fromPrev){
    try{ clearInterval(keepAliveId); }catch{}
    try{
      if(cvs){ window.removeEventListener("resize", cvs.__onResize || (()=>{})); cvs.remove(); }
      if(guiHost) guiHost.remove();
      if(lockProxy) lockProxy.remove();
    }catch{}
    if(!fromPrev) toast("Overlay removed");
  };

  // tiny API
  singleton.setLock = function(v){
    state.lock = !!v;
    applyLockSideEffects();
    save();
    toast(state.lock ? "Camera locked" : "Camera unlocked");
  };

  (async function init(){
    await waitForBody();
    document.body.appendChild(lockProxy); // keep under canvas
    makeCanvas();
    buildGUI();
    keepAliveId = keepAlive();
    applyLockSideEffects();

    document.addEventListener("paste", async e=>{
      const items=e.clipboardData?.items||[];
      for(const it of items){
        if(it.type && it.type.startsWith("image/")){
          const f=it.getAsFile(); if(f) await loadFromFile(f);
          e.preventDefault(); break;
        }
      }
    }, true);

    log("wplace overlay ready");
    toast("Overlay ready. Load image to begin");
  })();
})();