diff --git a/dist/BlueMarble.user.css b/dist/BlueMarble.user.css
index e873a30..61adbd7 100644
--- a/dist/BlueMarble.user.css
+++ b/dist/BlueMarble.user.css
@@ -1 +1 @@
-#bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through}
+#bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto;will-change:transform;backface-visibility:hidden;-webkit-backface-visibility:hidden;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-l:has(#bm-g.dragging){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#bm-g.dragging{pointer-events:auto}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-2,input[type=file][id*=template]{display:none!important;visibility:hidden!important;position:absolute!important;left:-9999px!important;top:-9999px!important;width:0!important;height:0!important;opacity:0!important;z-index:-9999!important;pointer-events:none!important}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through}
diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js
index 376d763..82e98d8 100644
--- a/dist/BlueMarble.user.js
+++ b/dist/BlueMarble.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
-// @name Blue Marble.
+// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
-// @version 0.70.0
+// @version 0.72.0
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0
@@ -20,4 +20,4 @@
// Wplace --> https://wplace.live
// License --> https://www.mozilla.org/en-US/MPL/2.0/
-(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),s=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i);t=new WeakSet,e=function(t,e={},n={}){const i=document.createElement(t);this.t?(this.i.appendChild(i),this.o.push(this.i),this.i=i):(this.t=i,this.i=i);for(const[t,n]of Object.entries(e))i[t]=n;for(const[t,e]of Object.entries(n))i[t]=e;return i};var o,a=class{constructor({displayName:t="My template",h:e=0,l:n="",url:i="",file:s=null,coords:o=null,u:a=null,m:r=1e3}={}){this.displayName=t,this.h=e,this.l=n,this.url=i,this.file=s,this.coords=o,this.u=a,this.m=r}async p(){const t=await createImageBitmap(this.file),e=t.width,n=t.height,i={},s=new OffscreenCanvas(this.m,this.m),o=s.getContext("2d",{v:!0});for(let a=this.coords[3];a0;)n=e[t%i]+n,t=Math.floor(t/i);return n}o=new WeakSet;var c=GM_info.script.name.toString(),h=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-o",c),e.setAttribute("bm-m","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-o")||"Blue Marble",n=t?.getAttribute("bm-m")||"",i=new Map;window.addEventListener("message",t=>{const{source:s,endpoint:o,blobID:a,blobData:r,blink:c}=t.data;if(Date.now(),"blue-marble"==s&&a&&r&&!o){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const s=window.fetch;window.fetch=async function(...t){const e=await s.apply(this,t),n=e.clone(),o=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",a=n.headers.get("content-type")||"";if(a.includes("application/json"))n.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:o,jsonData:t},"*")}).catch(t=>{});else if(a.includes("image/")&&!o.includes("openfreemap")){const t=Date.now(),e=await n.blob();return new Promise(s=>{const a=crypto.randomUUID();i.set(a,t=>{s(new Response(t,{headers:n.headers,status:n.status,statusText:n.statusText}))}),window.postMessage({source:"blue-marble",endpoint:o,blobID:a,blobData:e,blink:t})}).catch(t=>{Date.now()})}return e}});var l=GM_getResourceText("CSS-BM-File");GM_addStyle(l);var u=document.createElement("link");u.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",u.rel="preload",u.as="style",u.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(u),new class{constructor(){this.M=null,this.$=null,this.D="#bm-5"}C(t){return this.$=t,this.M=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.D)}),this}T(){return this.M}observe(t,e=!1,n=!1){t.observe(this.$,{childList:e,subtree:n})}};var d=new class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.I=null,this.k="bm-a",this.t=null,this.i=null,this.o=[]}N(t){this.I=t}S(){return this.o.length>0&&(this.i=this.o.pop()),this}B(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}O(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}L(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}H(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}q(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}P(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}R(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}_(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}j(n={},i=()=>{}){const o=s(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=s(this,t,e).call(this,"input",{type:"checkbox"},n);return o.insertBefore(a,o.firstChild),this.S(),i(this,o,a),this}V(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}Y(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-p",onclick:()=>{this.F(this.k,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}G(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}U(n={},i=()=>{}){const o=n.textContent??"";delete n.textContent;const a=s(this,t,e).call(this,"div"),r=s(this,t,e).call(this,"input",{type:"file",style:"display: none;"},n);this.S();const c=s(this,t,e).call(this,"button",{textContent:o});return this.S(),this.S(),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=o}),i(this,a,r,c),this}W(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}F(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}X(t,e){let n,i=!1,s=0;t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),t&&e?(e.addEventListener("mousedown",function(o){i=!0,n=o.clientX-t.getBoundingClientRect().left,s=o.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging")}),e.addEventListener("touchstart",function(o){i=!0;const a=o?.touches?.[0];a&&(n=a.clientX-t.getBoundingClientRect().left,s=a.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging"))},{passive:!1}),document.addEventListener("mousemove",function(e){i&&(t.style.left=e.clientX-n+"px",t.style.top=e.clientY-s+"px",t.style.right="")}),document.addEventListener("touchmove",function(e){if(i){const i=e?.touches?.[0];if(!i)return;t.style.left=i.clientX-n+"px",t.style.top=i.clientY-s+"px",t.style.right="",e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchend",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchcancel",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")})):this.A(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`)}J(t){(0,console.info)(`${this.name}: ${t}`),this.F(this.k,"Status: "+t,!0)}A(t){(0,console.error)(`${this.name}: ${t}`),this.F(this.k,"Error: "+t,!0)}}(c,h),m=new class{constructor(t,e,n){i(this,o),this.name=t,this.version=e,this.t=n,this.Z="1.0.0",this.K=null,this.tt="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.m=1e3,this.et=3,this.nt=null,this.it=null,this.st="bm-n",this.ot="div#map canvas.maplibregl-canvas",this.rt=null,this.ct="",this.ht=[],this.lt=null}ut(){if(document.body.contains(this.nt))return this.nt;document.getElementById(this.st)?.remove();const t=document.querySelector(this.ot),e=document.createElement("canvas");return e.id=this.st,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.nt=e,window.addEventListener("move",this.dt),window.addEventListener("zoom",this.bt),window.addEventListener("resize",this.ft),this.nt}async wt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.Z,templates:{}}}async gt(t,e,n){this.lt||(this.lt=await this.wt()),this.t.J(`Creating template at ${n.join(", ")}...`);const i=new a({displayName:e,h:0,l:r(this.K||0,this.tt),file:t,coords:n});i.u=await i.p(this.m),this.lt.templates[`${i.h} ${i.l}`]={name:i.displayName,enabled:!0,tiles:i.u},this.ht=[],this.ht.push(i),this.t.J(`Template created at ${n.join(", ")}!`)}vt(){}async yt(){this.lt||(this.lt=await this.wt())}async xt(t,e){const n=this.m*this.et;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0");const i=this.ht;i.sort((t,e)=>t.h-e.h);const s=i.map(t=>{const n=Object.keys(t.u).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>t.u[e]);return i?.[0]}).filter(Boolean);s.length>0&&this.t.J(`Displaying ${s.length} template${1==s.length?"":"s"}.`);const o=await createImageBitmap(t),a=new OffscreenCanvas(n,n),r=a.getContext("2d");r.imageSmoothingEnabled=!1,r.beginPath(),r.rect(0,0,n,n),r.clip(),r.clearRect(0,0,n,n),r.drawImage(o,0,0,n,n);for(const t of s)r.drawImage(t,0,0);return await a.convertToBlob({type:"image/png"})}Mt(){}}(c,h,d),p=new class{constructor(t){this.$t=t,this.Dt=!1,this.Ct=[],this.Tt=[]}It(t){window.addEventListener("message",async e=>{const n=e.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const s=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(s){case"me":if(i.status&&"2"!=i.status?.toString()[0])return void t.A("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);i.id||i.id,this.$t.K=i.id,t.F("bm-f",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.F("bm-b",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.F("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const s=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),r=new URLSearchParams(n.endpoint.split("?")[1]),c=[r.get("x"),r.get("y")];if(this.Ct.length&&(!s.length||!c.length))return void t.A("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Ct=[...s,...c];const h=(o=s,a=c,[parseInt(o[0])%4*1e3+parseInt(a[0]),parseInt(o[1])%4*1e3+parseInt(a[1])]),l=document.querySelectorAll("span");for(const t of l)if(t.textContent.trim().includes(`${h[0]}, ${h[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${s[0]}, Tl Y: ${s[1]}, Px X: ${c[0]}, Px Y: ${c[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const d=n.blobID,m=n.blobData,p=await this.$t.xt(m,u);window.postMessage({source:"blue-marble",blobID:d,blobData:p,blink:n.blink});break;case"robots":this.Dt="false"==i.userscript?.toString().toLowerCase()}var o,a})}}(m);d.N(p),d.O({id:"bm-l",style:"top: 10px; right: 75px;"}).O({id:"bm-7"}).O({id:"bm-g"}).S().q({alt:"Blue Marble Icon",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png"}).S().P(1,{textContent:c}).S().S().R().S().O({id:"bm-4"}).L({id:"bm-f",textContent:"Username:"}).S().L({id:"bm-b",textContent:"Droplets:"}).S().L({id:"bm-6",textContent:"Next level in..."}).S().S().R().S().O({id:"bm-3"}).O({id:"bm-8"}).V({id:"bm-c",className:"bm-p",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.I?.Ct;e?.[0]?(t.F("bm-h",e?.[0]||""),t.F("bm-i",e?.[1]||""),t.F("bm-j",e?.[2]||""),t.F("bm-k",e?.[3]||"")):t.A("Coordinates are malformed! Did you try clicking on the canvas first?")}}).S().G({type:"number",id:"bm-h",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).S().G({type:"number",id:"bm-i",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).S().G({type:"number",id:"bm-j",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).S().G({type:"number",id:"bm-k",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).S().S().U({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).S().O({id:"bm-0"}).V({id:"bm-d",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-h");if(!n.checkValidity())return n.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-i");if(!i.checkValidity())return i.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-j");if(!s.checkValidity())return s.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-k");if(!o.checkValidity())return o.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(m.gt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(s.value),Number(o.value)]),t.J("Drew to canvas!")):t.A("No file selected!")}}).S().S().W({id:d.k,placeholder:`Status: Sleeping...\nVersion: ${h}`,readOnly:!0}).S().O({id:"bm-1"}).O().V({id:"bm-9",className:"bm-p",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).S().S().H({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).S().S().S().B(document.body),d.X("#bm-l","#bm-g"),p.It(d),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-e");i||(i=document.createElement("button"),i.id="bm-e",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"},n.parentNode.parentNode.parentNode.parentNode.querySelector("h2").parentNode.appendChild(i))}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${c}%c (${h}) userscript has loaded!`,"color: cornflowerblue;","")})();
+(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),o=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i);t=new WeakSet,e=function(t,e={},n={}){const i=document.createElement(t);this.t?(this.i.appendChild(i),this.o.push(this.i),this.i=i):(this.t=i,this.i=i);for(const[t,n]of Object.entries(e))i[t]=n;for(const[t,e]of Object.entries(n))i[t]=e;return i};var s,a=class{constructor({displayName:t="My template",l:e=0,h:n="",url:i="",file:o=null,coords:s=null,m:a=null,u:r=1e3}={}){this.displayName=t,this.l=e,this.h=n,this.url=i,this.file=o,this.coords=s,this.m=a,this.u=r,this.p=0}async $(){console.log("Template coordinates:",this.coords);const t=await createImageBitmap(this.file),e=t.width,n=t.height,i=e*n;console.log(`Template pixel analysis - Dimensions: ${e}×${n} = ${i.toLocaleString()} pixels`),this.p=i;const o={},s=new OffscreenCanvas(this.u,this.u),a=s.getContext("2d",{v:!0});for(let i=this.coords[3];i0;)n=e[t%i]+n,t=Math.floor(t/i);return n}s=new WeakSet;var c=GM_info.script.name.toString(),l=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-o",c),e.setAttribute("bm-m","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-o")||"Blue Marble",n=t?.getAttribute("bm-m")||"",i=new Map;window.addEventListener("message",t=>{const{source:o,endpoint:s,blobID:a,blobData:r,blink:c}=t.data,l=Date.now()-c;if(console.groupCollapsed(`%c${e}%c: ${i.size} Recieved IMAGE message about blob "${a}"`,n,""),console.log(`Blob fetch took %c${String(Math.floor(l/6e4)).padStart(2,"0")}:${String(Math.floor(l/1e3)%60).padStart(2,"0")}.${String(l%1e3).padStart(3,"0")}%c MM:SS.mmm`,n,""),console.log(i),console.groupEnd(),"blue-marble"==o&&a&&r&&!s){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const o=window.fetch;window.fetch=async function(...t){const s=await o.apply(this,t),a=s.clone(),r=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",c=a.headers.get("content-type")||"";if(c.includes("application/json"))console.log(`%c${e}%c: Sending JSON message about endpoint "${r}"`,n,""),a.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:r,jsonData:t},"*")}).catch(t=>{console.error(`%c${e}%c: Failed to parse JSON: `,n,"",t)});else if(c.includes("image/")&&!r.includes("openfreemap")){const t=Date.now(),o=await a.blob();return console.log(`%c${e}%c: ${i.size} Sending IMAGE message about endpoint "${r}"`,n,""),new Promise(s=>{const c=crypto.randomUUID();i.set(c,t=>{s(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),console.log(`%c${e}%c: ${i.size} Processed blob "${c}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:c,blobData:o,blink:t})}).catch(o=>{const s=Date.now();console.error(`%c${e}%c: Failed to Promise blob!`,n,""),console.groupCollapsed(`%c${e}%c: Details of failed blob Promise:`,n,""),console.log(`Endpoint: ${r}\nThere are ${i.size} blobs processing...\nBlink: ${t.toLocaleString()}\nTime Since Blink: ${String(Math.floor(s/6e4)).padStart(2,"0")}:${String(Math.floor(s/1e3)%60).padStart(2,"0")}.${String(s%1e3).padStart(3,"0")} MM:SS.mmm`),console.error("Exception stack:",o),console.groupEnd()})}return s}});var h=GM_getResourceText("CSS-BM-File");GM_addStyle(h);var m=document.createElement("link");m.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",m.rel="preload",m.as="style",m.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(m),new class{constructor(){this.M=null,this.S=null,this.D="#bm-5"}C(t){return this.S=t,this.M=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.D)}),this}T(){return this.M}observe(t,e=!1,n=!1){t.observe(this.S,{childList:e,subtree:n})}};var u=new class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.k=null,this.I="bm-a",this.t=null,this.i=null,this.o=[]}N(t){this.k=t}B(){return this.o.length>0&&(this.i=this.o.pop()),this}O(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}P(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"div",{},n)),this}H(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"p",{},n)),this}L(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"small",{},n)),this}F(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"img",{},n)),this}R(n,i={},s=()=>{}){return s(this,o(this,t,e).call(this,"h"+n,{},i)),this}Y(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"hr",{},n)),this}j(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"br",{},n)),this}q(n={},i=()=>{}){const s=o(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=o(this,t,e).call(this,"input",{type:"checkbox"},n);return s.insertBefore(a,s.firstChild),this.B(),i(this,s,a),this}G(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"button",{},n)),this}X(n={},i=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const a={textContent:"?",className:"bm-p",onclick:()=>{this.A(this.I,s)}};return i(this,o(this,t,e).call(this,"button",a,n)),this}_(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"input",{},n)),this}J(n={},i=()=>{}){const s=n.textContent??"";delete n.textContent;const a=o(this,t,e).call(this,"div"),r=o(this,t,e).call(this,"input",{type:"file",style:"display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;"},n);this.B();const c=o(this,t,e).call(this,"button",{textContent:s});return this.B(),this.B(),r.setAttribute("tabindex","-1"),r.setAttribute("aria-hidden","true"),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=s}),i(this,a,r,c),this}V(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"textarea",{},n)),this}A(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}W(t,e){let n,i=!1,o=0,s=null,a=0,r=0,c=0,l=0;if(t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),!t||!e)return void this.U(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`);const h=()=>{if(i){const e=Math.abs(a-c),n=Math.abs(r-l);(e>.5||n>.5)&&(a=c,r=l,t.style.transform=`translate(${a}px, ${r}px)`,t.style.left="0px",t.style.top="0px",t.style.right=""),s=requestAnimationFrame(h)}};let m=null;const u=(u,d)=>{i=!0,m=t.getBoundingClientRect(),n=u-m.left,o=d-m.top;const p=window.getComputedStyle(t).transform;if(p&&"none"!==p){const t=new DOMMatrix(p);a=t.m41,r=t.m42}else a=m.left,r=m.top;c=a,l=r,document.body.style.userSelect="none",e.classList.add("dragging"),s&&cancelAnimationFrame(s),h()},d=()=>{i=!1,s&&(cancelAnimationFrame(s),s=null),document.body.style.userSelect="",e.classList.remove("dragging")};e.addEventListener("mousedown",function(t){t.preventDefault(),u(t.clientX,t.clientY)}),e.addEventListener("touchstart",function(t){const e=t?.touches?.[0];e&&(u(e.clientX,e.clientY),t.preventDefault())},{passive:!1}),document.addEventListener("mousemove",function(t){i&&m&&(c=t.clientX-n,l=t.clientY-o)},{passive:!0}),document.addEventListener("touchmove",function(t){if(i&&m){const e=t?.touches?.[0];if(!e)return;c=e.clientX-n,l=e.clientY-o,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",d),document.addEventListener("touchend",d),document.addEventListener("touchcancel",d)}Z(t){(0,console.info)(`${this.name}: ${t}`),this.A(this.I,"Status: "+t,!0)}U(t){(0,console.error)(`${this.name}: ${t}`),this.A(this.I,"Error: "+t,!0)}}(c,l),d=new class{constructor(t,e,n){i(this,s),this.name=t,this.version=e,this.t=n,this.K="1.0.0",this.tt=null,this.et="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.u=1e3,this.nt=3,this.it=null,this.ot=null,this.st="bm-n",this.rt="div#map canvas.maplibregl-canvas",this.ct=null,this.lt="",this.ht=[],this.ut=null}dt(){if(document.body.contains(this.it))return this.it;document.getElementById(this.st)?.remove();const t=document.querySelector(this.rt),e=document.createElement("canvas");return e.id=this.st,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.it=e,window.addEventListener("move",this.bt),window.addEventListener("zoom",this.ft),window.addEventListener("resize",this.wt),this.it}async $t(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.K,templates:{}}}async gt(t,e,n){this.ut||(this.ut=await this.$t(),console.log("Creating JSON...")),this.t.Z(`Creating template at ${n.join(", ")}...`);const i=new a({displayName:e,l:0,h:r(this.tt||0,this.et),file:t,coords:n});i.m=await i.$(this.u),this.ut.templates[`${i.l} ${i.h}`]={name:i.displayName,enabled:!0,tiles:i.m},this.ht=[],this.ht.push(i);const o=(new Intl.NumberFormat).format(i.p);this.t.Z(`Template created at ${n.join(", ")}! Total pixels: ${o}`),console.log(Object.keys(this.ut.templates).length),console.log(this.ut),console.log(this.ht)}vt(){}async xt(){this.ut||(this.ut=await this.$t(),console.log("Creating JSON..."))}async yt(t,e){const n=this.u*this.nt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0"),console.log(`Searching for templates in tile: "${e}"`);const i=this.ht;i.sort((t,e)=>t.l-e.l),console.log(i);const o=i.map(t=>{const n=Object.keys(t.m).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>t.m[e]);return i?.[0]}).filter(Boolean);if(console.log(o),o.length>0){const t=i.filter(t=>Object.keys(t.m).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.p||0),0),n=(new Intl.NumberFormat).format(t);this.t.Z(`Displaying ${o.length} template${1==o.length?"":"s"}. Total pixels: ${n}`)}const s=await createImageBitmap(t),a=new OffscreenCanvas(n,n),r=a.getContext("2d");r.imageSmoothingEnabled=!1,r.beginPath(),r.rect(0,0,n,n),r.clip(),r.clearRect(0,0,n,n),r.drawImage(s,0,0,n,n);for(const t of o)console.log("Template Blob is "+typeof t),console.log(t),r.drawImage(t,0,0);return await a.convertToBlob({type:"image/png"})}Mt(){}}(c,l,u),p=new class{constructor(t){this.St=t,this.Dt=!1,this.Ct=[],this.Tt=[]}kt(t){window.addEventListener("message",async e=>{const n=e.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const o=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(console.log('%cBlue Marble%c: Recieved message about "%s"',"color: cornflowerblue;","",o),o){case"me":if(i.status&&"2"!=i.status?.toString()[0])return void t.U("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);console.log(i.id),(i.id||0===i.id)&&console.log(r(i.id,"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~")),this.St.tt=i.id,t.A("bm-f",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.A("bm-b",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.A("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const o=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),c=new URLSearchParams(n.endpoint.split("?")[1]),l=[c.get("x"),c.get("y")];if(this.Ct.length&&(!o.length||!l.length))return void t.U("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Ct=[...o,...l];const h=(s=o,a=l,[parseInt(s[0])%4*1e3+parseInt(a[0]),parseInt(s[1])%4*1e3+parseInt(a[1])]),m=document.querySelectorAll("span");for(const t of m)if(t.textContent.trim().includes(`${h[0]}, ${h[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${o[0]}, Tl Y: ${o[1]}, Px X: ${l[0]}, Px Y: ${l[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const d=n.blobID,p=n.blobData,b=await this.St.yt(p,u);window.postMessage({source:"blue-marble",blobID:d,blobData:b,blink:n.blink});break;case"robots":this.Dt="false"==i.userscript?.toString().toLowerCase();break}var s,a})}}(d);u.N(p),function(){let t=!1;u.P({id:"bm-l",style:"top: 10px; right: 75px;"}).P({id:"bm-7"}).P({id:"bm-g"}).B().F({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(e,n)=>{n.addEventListener("click",()=>{t=!t;const i=document.querySelector("#bm-l"),o=document.querySelector("#bm-7"),s=document.querySelector("#bm-g"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-c"),c=document.querySelector("#bm-d"),l=document.querySelectorAll("#bm-8 input");t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-l h1","#bm-4","#bm-l hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.I}`].forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.style.display=t?"none":""})}),t?(a&&(a.style.display="none"),r&&(r.style.display="none"),c&&(c.style.display="none"),l.forEach(t=>{t.style.display="none"}),i.style.width="60px",i.style.height="76px",i.style.maxWidth="60px",i.style.minWidth="60px",i.style.padding="8px",n.style.marginLeft="3px",o.style.textAlign="center",o.style.margin="0",o.style.marginBottom="0",s&&(s.style.display="",s.style.marginBottom="0.25em")):(a&&(a.style.display="",a.style.flexDirection="",a.style.justifyContent="",a.style.alignItems="",a.style.gap="",a.style.textAlign="",a.style.margin=""),r&&(r.style.display=""),c&&(c.style.display="",c.style.marginTop=""),l.forEach(t=>{t.style.display=""}),n.style.marginLeft="",i.style.padding="10px",o.style.textAlign="",o.style.margin="",o.style.marginBottom="",s&&(s.style.marginBottom="0.5em"),i.style.width="",i.style.height=""),n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)"})}).B().R(1,{textContent:c}).B().B().Y().B().P({id:"bm-4"}).H({id:"bm-f",textContent:"Username:"}).B().H({id:"bm-b",textContent:"Droplets:"}).B().H({id:"bm-6",textContent:"Next level in..."}).B().B().Y().B().P({id:"bm-3"}).P({id:"bm-8"}).G({id:"bm-c",className:"bm-p",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.k?.Ct;e?.[0]?(t.A("bm-h",e?.[0]||""),t.A("bm-i",e?.[1]||""),t.A("bm-j",e?.[2]||""),t.A("bm-k",e?.[3]||"")):t.U("Coordinates are malformed! Did you try clicking on the canvas first?")}}).B()._({type:"number",id:"bm-h",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).B()._({type:"number",id:"bm-i",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).B()._({type:"number",id:"bm-j",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).B()._({type:"number",id:"bm-k",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).B().B().J({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).B().P({id:"bm-0"}).G({id:"bm-d",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-h");if(!n.checkValidity())return n.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-i");if(!i.checkValidity())return i.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-j");if(!o.checkValidity())return o.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-k");if(!s.checkValidity())return s.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(d.gt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(o.value),Number(s.value)]),t.Z("Drew to canvas!")):t.U("No file selected!")}}).B().B().V({id:u.I,placeholder:`Status: Sleeping...\nVersion: ${l}`,readOnly:!0}).B().P({id:"bm-1"}).P().G({id:"bm-9",className:"bm-p",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).B().B().L({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).B().B().B().O(document.body)}(),u.W("#bm-l","#bm-g"),p.kt(u),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-e");i||(i=document.createElement("button"),i.id="bm-e",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"},n.parentNode.parentNode.parentNode.parentNode.querySelector("h2").parentNode.appendChild(i))}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${c}%c (${l}) userscript has loaded!`,"color: cornflowerblue;","")})();
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 21c4f68..0c2bc27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
- "version": "0.69.1",
+ "version": "0.72.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
- "version": "0.69.1",
+ "version": "0.72.0",
"devDependencies": {
"esbuild": "^0.25.0",
"terser": "^5.43.1"
diff --git a/package.json b/package.json
index 5b8a829..2c05718 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
- "version": "0.70.0",
+ "version": "0.72.0",
"type": "module",
"scripts": {
"build": "node build/build.js",
diff --git a/src/BlueMarble.meta.js b/src/BlueMarble.meta.js
index 99c025c..2237f52 100644
--- a/src/BlueMarble.meta.js
+++ b/src/BlueMarble.meta.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
-// @version 0.70.0
+// @version 0.72.0
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0
diff --git a/src/Overlay.js b/src/Overlay.js
index ea206fe..8fe3e13 100644
--- a/src/Overlay.js
+++ b/src/Overlay.js
@@ -430,9 +430,9 @@ export default class Overlay {
return this;
}
- /** Adds a file input to the overlay. This includes a container and a button.
+ /** Adds a file input to the overlay with enhanced visibility controls.
* This input element will have properties shared between all file input elements in the overlay.
- * You can override the shared properties by using a callback.
+ * Uses multiple hiding methods to prevent browser native text from appearing during minimize/maximize.
* @param {Object.} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.
* @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input.
* @returns {Overlay} Overlay class instance (this)
@@ -451,7 +451,10 @@ export default class Overlay {
*/
addInputFile(additionalProperties = {}, callback = () => {}) {
- const properties = {'type': 'file', 'style': 'display: none;'}; // Shared file input DOM properties
+ const properties = {
+ 'type': 'file',
+ 'style': 'display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;'
+ }; // Complete file input hiding to prevent native browser text interference
const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content
delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input
@@ -463,11 +466,15 @@ export default class Overlay {
this.buildElement(); // Signifies that we are done adding children to the button
this.buildElement(); // Signifies that we are done adding children to the container
+ // Prevent file input from being accessible or visible in any way
+ input.setAttribute('tabindex', '-1');
+ input.setAttribute('aria-hidden', 'true');
+
button.addEventListener('click', () => {
input.click(); // Clicks the file input
});
- // Changes the button text content (and trims the file name)
+ // Update button text when file is selected
input.addEventListener('change', () => {
button.style.maxWidth = `${button.offsetWidth}px`;
if (input.files.length > 0) {
@@ -533,14 +540,20 @@ export default class Overlay {
}
}
- /** Handles dragging of the overlay.
+ /** Handles dragging of the overlay with performance optimizations.
+ * Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms.
* @param {string} moveMe - The ID of the element to be moved
- * @param {string} iMoveThings - The ID of the element to be moved
+ * @param {string} iMoveThings - The ID of the drag handle element
* @since 0.8.2
*/
handleDrag(moveMe, iMoveThings) {
let isDragging = false;
let offsetX, offsetY = 0;
+ let animationFrame = null;
+ let currentX = 0;
+ let currentY = 0;
+ let targetX = 0;
+ let targetY = 0;
// Retrieves the elements (allows either '#id' or 'id' to be passed in)
moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe);
@@ -552,67 +565,110 @@ export default class Overlay {
return; // Kills itself
}
- // What to do when the mouse is pressed down on the element that moves things
- iMoveThings.addEventListener('mousedown', function(event) {
+ // Smooth animation loop using requestAnimationFrame for optimal performance
+ const updatePosition = () => {
+ if (isDragging) {
+ // Only update DOM if position changed significantly (reduce repaints)
+ const deltaX = Math.abs(currentX - targetX);
+ const deltaY = Math.abs(currentY - targetY);
+
+ if (deltaX > 0.5 || deltaY > 0.5) {
+ currentX = targetX;
+ currentY = targetY;
+
+ // Use CSS transform for GPU acceleration instead of left/top
+ moveMe.style.transform = `translate(${currentX}px, ${currentY}px)`;
+ moveMe.style.left = '0px';
+ moveMe.style.top = '0px';
+ moveMe.style.right = '';
+ }
+
+ animationFrame = requestAnimationFrame(updatePosition);
+ }
+ };
+
+ // Cache initial position to avoid expensive getBoundingClientRect calls during drag
+ let initialRect = null;
+
+ const startDrag = (clientX, clientY) => {
isDragging = true;
- offsetX = event.clientX - moveMe.getBoundingClientRect().left;
- offsetY = event.clientY - moveMe.getBoundingClientRect().top;
- document.body.style.userSelect = 'none'; // Prevents text selection while dragging
- iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
+ initialRect = moveMe.getBoundingClientRect();
+ offsetX = clientX - initialRect.left;
+ offsetY = clientY - initialRect.top;
+
+ // Get current position from transform or use element position
+ const computedStyle = window.getComputedStyle(moveMe);
+ const transform = computedStyle.transform;
+
+ if (transform && transform !== 'none') {
+ const matrix = new DOMMatrix(transform);
+ currentX = matrix.m41;
+ currentY = matrix.m42;
+ } else {
+ currentX = initialRect.left;
+ currentY = initialRect.top;
+ }
+
+ targetX = currentX;
+ targetY = currentY;
+
+ document.body.style.userSelect = 'none';
+ iMoveThings.classList.add('dragging');
+
+ // Start animation loop
+ if (animationFrame) {
+ cancelAnimationFrame(animationFrame);
+ }
+ updatePosition();
+ };
+
+ const endDrag = () => {
+ isDragging = false;
+ if (animationFrame) {
+ cancelAnimationFrame(animationFrame);
+ animationFrame = null;
+ }
+ document.body.style.userSelect = '';
+ iMoveThings.classList.remove('dragging');
+ };
+
+ // Mouse down - start dragging
+ iMoveThings.addEventListener('mousedown', function(event) {
+ event.preventDefault();
+ startDrag(event.clientX, event.clientY);
});
- // What to do when the touch starts on the element that moves things
+ // Touch start - start dragging
iMoveThings.addEventListener('touchstart', function(event) {
- isDragging = true;
const touch = event?.touches?.[0];
if (!touch) {return;}
- offsetX = touch.clientX - moveMe.getBoundingClientRect().left; // Distance between the left edge of the overlay, and the cursor
- offsetY = touch.clientY - moveMe.getBoundingClientRect().top; // Distance between the top edge of the overlay, and the cursor
- document.body.style.userSelect = 'none'; // Prevents text selection while dragging
- iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
- }, { passive: false }); // Prevents scrolling from being captured
+ startDrag(touch.clientX, touch.clientY);
+ event.preventDefault();
+ }, { passive: false });
- // What to do when the mouse is moved while dragging
+ // Mouse move - update target position
document.addEventListener('mousemove', function(event) {
- if (isDragging) {
- moveMe.style.left = (event.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
- moveMe.style.top = (event.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
- moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
+ if (isDragging && initialRect) {
+ targetX = event.clientX - offsetX;
+ targetY = event.clientY - offsetY;
}
- });
+ }, { passive: true });
- // What to do when the touch moves while dragging
+ // Touch move - update target position
document.addEventListener('touchmove', function(event) {
- if (isDragging) {
+ if (isDragging && initialRect) {
const touch = event?.touches?.[0];
if (!touch) {return;}
- moveMe.style.left = (touch.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
- moveMe.style.top = (touch.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
- moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
- event.preventDefault(); // prevent scrolling while dragging
+ targetX = touch.clientX - offsetX;
+ targetY = touch.clientY - offsetY;
+ event.preventDefault();
}
- }, { passive: false }); // Prevents scrolling from being captured
+ }, { passive: false });
- // What to do when the mouse is released
- document.addEventListener('mouseup', function() {
- isDragging = false;
- document.body.style.userSelect = ''; // Restores text selection capability after dragging
- iMoveThings.classList.remove('dragging'); // Removes the dragging class
- });
-
- // What to do when the touch ends
- document.addEventListener('touchend', function() {
- isDragging = false;
- document.body.style.userSelect = ''; // Restores text selection capability after dragging
- iMoveThings.classList.remove('dragging'); // Removes the dragging class
- });
-
- // What to do when the touch is cancelled
- document.addEventListener('touchcancel', function() {
- isDragging = false;
- document.body.style.userSelect = ''; // Restores text selection capability after dragging
- iMoveThings.classList.remove('dragging'); // Removes the dragging class
- });
+ // End drag events
+ document.addEventListener('mouseup', endDrag);
+ document.addEventListener('touchend', endDrag);
+ document.addEventListener('touchcancel', endDrag);
}
/** Handles status display.
diff --git a/src/main.js b/src/main.js
index c8dacba..35d2216 100644
--- a/src/main.js
+++ b/src/main.js
@@ -2,6 +2,12 @@
* @since 0.0.0
*
* VERSION HISTORY:
+ * 0.72.0 - Performance and UI improvements
+ * - Smooth drag performance with requestAnimationFrame and GPU acceleration
+ * - Fixed file upload button text bug during UI state changes
+ * - Added minimize/maximize animations with CSS transitions
+ * - Hardware-accelerated transformations and smart DOM updates
+ *
* 0.71.0 - Added minimize/maximize functionality and pixel counting system
* Features added:
* - Interactive minimize/maximize overlay with click-to-toggle functionality
diff --git a/src/overlay.css b/src/overlay.css
index c239ac3..7a0a12e 100644
--- a/src/overlay.css
+++ b/src/overlay.css
@@ -11,12 +11,18 @@
transition: all 0.3s ease;
max-width: 300px;
width: auto;
+ /* Performance optimizations for smooth dragging */
+ will-change: transform;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+ transform-style: preserve-3d;
+ -webkit-transform-style: preserve-3d;
}
/* Smooth transitions for minimize/maximize functionality */
#bm-contain-userinfo,
#bm-overlay hr,
-#bm-contain-automation,
+#bm-contain-automation,
#bm-contain-buttons-action {
transition: opacity 0.2s ease, height 0.2s ease;
}
@@ -49,6 +55,20 @@ div#bm-overlay {
cursor: grabbing;
}
+/* Disable interactions during drag for better performance */
+#bm-overlay:has(#bm-bar-drag.dragging) {
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
+
+/* Keep drag bar interactive when dragging */
+#bm-bar-drag.dragging {
+ pointer-events: auto;
+}
+
/* The container for the overlay header */
#bm-contain-header {
margin-bottom: 0.5em;
@@ -188,6 +208,21 @@ div:has(> #bm-input-file-template) > button {
text-overflow: ellipsis;
}
+/* Force complete invisibility of file input to prevent native browser text */
+#bm-input-file-template,
+input[type="file"][id*="template"] {
+ display: none !important;
+ visibility: hidden !important;
+ position: absolute !important;
+ left: -9999px !important;
+ top: -9999px !important;
+ width: 0 !important;
+ height: 0 !important;
+ opacity: 0 !important;
+ z-index: -9999 !important;
+ pointer-events: none !important;
+}
+
/* Output status area */
#bm-output-status {
font-size: small;