From 2744d7822f86122030a4932019d615b6d1f75fbe Mon Sep 17 00:00:00 2001 From: SwingTheVine Date: Fri, 13 Feb 2026 20:09:01 -0500 Subject: [PATCH] Created rudementary total pixel calculator --- dist/BlueMarble.user.js | 4 +- docs/README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/BlueMarble.meta.js | 2 +- src/Template.js | 188 ++++++++++++++++++++++++++++++++-------- src/templateManager.js | 6 +- src/utils.js | 34 ++++++++ 8 files changed, 199 insertions(+), 43 deletions(-) diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js index 3f57592..f37f1a5 100644 --- a/dist/BlueMarble.user.js +++ b/dist/BlueMarble.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Blue Marble // @namespace https://github.com/SwingTheVine/ -// @version 0.88.5 +// @version 0.88.25 // @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 @@ -24,4 +24,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),o=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i),s=class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.t=null,this.i="bm-o",this.o=null,this.l=null,this.m=[]}h(t){this.t=t}u(){return this.m.length>0&&(this.l=this.m.pop()),this}p(t){t?.appendChild(this.o),this.o=null,this.l=null,this.m=[]}v(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"div",{},n)),this}$(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"p",{},n)),this}S(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"small",{},n)),this}M(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"img",{},n)),this}T(n,i={},s=()=>{}){return s(this,o(this,t,e).call(this,"h"+n,{},i)),this}O(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"hr",{},n)),this}C(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"br",{},n)),this}N(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.u(),i(this,s,a),this}D(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"button",{},n)),this}k(n={},i=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const a={textContent:"?",className:"bm-D",onclick:()=>{this.I(this.i,s)}};return i(this,o(this,t,e).call(this,"button",a,n)),this}B(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"input",{},n)),this}W(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.u();const l=o(this,t,e).call(this,"button",{textContent:s});return this.u(),this.u(),r.setAttribute("tabindex","-1"),r.setAttribute("aria-hidden","true"),l.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{l.style.maxWidth=`${l.offsetWidth}px`,r.files.length>0?l.textContent=r.files[0].name:l.textContent=s}),i(this,a,r,l),this}_(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"textarea",{},n)),this}I(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}P(t,e){let n,i=!1,o=0,s=null,a=0,r=0,l=0,c=0;if(t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),!t||!e)return void this.X(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`);const m=()=>{if(i){const e=Math.abs(a-l),n=Math.abs(r-c);(e>.5||n>.5)&&(a=l,r=c,t.style.transform=`translate(${a}px, ${r}px)`,t.style.left="0px",t.style.top="0px",t.style.right=""),s=requestAnimationFrame(m)}};let h=null;const u=(u,d)=>{i=!0,h=t.getBoundingClientRect(),n=u-h.left,o=d-h.top;const b=window.getComputedStyle(t).transform;if(b&&"none"!==b){const t=new DOMMatrix(b);a=t.m41,r=t.m42}else a=h.left,r=h.top;l=a,c=r,document.body.style.userSelect="none",e.classList.add("dragging"),s&&cancelAnimationFrame(s),m()},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&&h&&(l=t.clientX-n,c=t.clientY-o)},{passive:!0}),document.addEventListener("touchmove",function(t){if(i&&h){const e=t?.touches?.[0];if(!e)return;l=e.clientX-n,c=e.clientY-o,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",d),document.addEventListener("touchend",d),document.addEventListener("touchcancel",d)}G(t){(0,console.info)(`${this.name}: ${t}`),this.I(this.i,"Status: "+t,!0)}X(t){(0,console.error)(`${this.name}: ${t}`),this.I(this.i,"Error: "+t,!0)}};function a(...t){(0,console.error)(...t)}function r(t,e){if(0===t)return e[0];let n="";const i=e.length;for(;t>0;)n=e[t%i]+n,t=Math.floor(t/i);return n}function l(t){let e="";for(let n=0;n0)for(const t in e){const n=t,i=e[t];if(console.log(n),e.hasOwnProperty(t)){const t=n.split(" "),e=Number(t?.[0]),o=t?.[1]||"0",s=i.name||`Template ${e||""}`,a=i.tiles,r={};for(const t in a)if(console.log(t),a.hasOwnProperty(t)){const e=c(a[t]),n=new Blob([e],{type:"image/png"}),i=await createImageBitmap(n);r[t]=i}const l=new f({displayName:s,J:e||this.q?.length||0,U:o||""});l.j=r,this.q.push(l),console.log(this.q),console.log("^^^ This ^^^")}}},d=new WeakSet,b=async function(t=navigator.userAgent){return(t=t||"").includes("OPR/")||t.includes("Opera")?"Opera":t.includes("Edg/")?"Edge":t.includes("Vivaldi")?"Vivaldi":t.includes("YaBrowser")?"Yandex":t.includes("Kiwi")?"Kiwi":t.includes("Brave")?"Brave":t.includes("Firefox/")?"Firefox":t.includes("Chrome/")?"Chrome":t.includes("Safari/")?"Safari":navigator.brave&&"function"==typeof navigator.brave.isBrave&&await navigator.brave.isBrave()?"Brave":"Unknown"},p=function(t=navigator.userAgent){return/Windows NT 11/i.test(t=t||"")?"Windows 11":/Windows NT 10/i.test(t)?"Windows 10":/Windows NT 6\.3/i.test(t)?"Windows 8.1":/Windows NT 6\.2/i.test(t)?"Windows 8":/Windows NT 6\.1/i.test(t)?"Windows 7":/Windows NT 6\.0/i.test(t)?"Windows Vista":/Windows NT 5\.1|Windows XP/i.test(t)?"Windows XP":/Mac OS X 10[_\.]15/i.test(t)?"macOS Catalina":/Mac OS X 10[_\.]14/i.test(t)?"macOS Mojave":/Mac OS X 10[_\.]13/i.test(t)?"macOS High Sierra":/Mac OS X 10[_\.]12/i.test(t)?"macOS Sierra":/Mac OS X 10[_\.]11/i.test(t)?"OS X El Capitan":/Mac OS X 10[_\.]10/i.test(t)?"OS X Yosemite":/Mac OS X 10[_\.]/i.test(t)?"macOS":/Android/i.test(t)?"Android":/iPhone|iPad|iPod/i.test(t)?"iOS":/Linux/i.test(t)?"Linux":"Unknown"};var w=GM_info.script.name.toString(),g=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-E",w),e.setAttribute("bm-B","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement?.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-E")||"Blue Marble",n=t?.getAttribute("bm-B")||"",i=new Map;window.addEventListener("message",t=>{const{source:o,endpoint:s,blobID:a,blobData:r,blink:l}=t.data,c=Date.now()-l;if(console.groupCollapsed(`%c${e}%c: ${i.size} Recieved IMAGE message about blob "${a}"`,n,""),console.log(`Blob fetch took %c${String(Math.floor(c/6e4)).padStart(2,"0")}:${String(Math.floor(c/1e3)%60).padStart(2,"0")}.${String(c%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",l=a.headers.get("content-type")||"";if(l.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(l.includes("image/")&&!r.includes("openfreemap")&&!r.includes("maps")){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 l=crypto.randomUUID();i.set(l,t=>{s(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),console.log(`%c${e}%c: ${i.size} Processed blob "${l}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:l,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 y=GM_getResourceText("CSS-BM-File");GM_addStyle(y);var v=document.createElement("link");v.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",v.rel="preload",v.as="style",v.onload=function(){this.onload=null,this.rel="stylesheet"},document.head?.appendChild(v),new class{constructor(){this.K=null,this.Z=null,this.tt="#bm-h"}et(t){return this.Z=t,this.K=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.tt)}),this}nt(){return this.K}observe(t,e=!1,n=!1){t.observe(this.Z,{childList:e,subtree:n})}};var x=new s(w,g),$=(new s(w,g),new class{constructor(t,e,n){i(this,m),this.name=t,this.version=e,this.o=n,this.it="1.0.0",this.ot=null,this.st="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.H=1e3,this.rt=3,this.lt=null,this.ct=null,this.ht="bm-C",this.ut="div#map canvas.maplibregl-canvas",this.dt=null,this.bt="",this.q=[],this.R=null,this.ft=!0}wt(){if(document.body.contains(this.lt))return this.lt;document.getElementById(this.ht)?.remove();const t=document.querySelector(this.ut),e=document.createElement("canvas");return e.id=this.ht,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.lt=e,window.addEventListener("move",this.gt),window.addEventListener("zoom",this.yt),window.addEventListener("resize",this.vt),this.lt}async xt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.it,templates:{}}}async $t(t,e,n){this.R||(this.R=await this.xt(),console.log("Creating JSON...")),this.o.G(`Creating template at ${n.join(", ")}...`);const i=new f({displayName:e,J:0,U:r(this.ot||0,this.st),file:t,coords:n}),{A:s,F:a}=await i.V(this.H);i.j=s,this.R.templates[`${i.J} ${i.U}`]={name:i.displayName,coords:n.join(", "),enabled:!0,tiles:a},this.q=[],this.q.push(i),this.o.G(`Template created at ${n.join(", ")}!`),console.log(Object.keys(this.R.templates).length),console.log(this.R),console.log(this.q),console.log(JSON.stringify(this.R)),await o(this,m,h).call(this)}St(){}async Mt(){this.R||(this.R=await this.xt(),console.log("Creating JSON..."))}async Tt(t,e){if(!this.ft)return t;const n=this.H*this.rt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0"),console.log(`Searching for templates in tile: "${e}"`);const i=this.q;console.log(i),i.sort((t,e)=>t.J-e.J),console.log(i);const o=i.map(t=>{const n=Object.keys(t.j).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>{const n=e.split(",");return{Ot:t.j[e],Ct:[n[0],n[1]],Nt:[n[2],n[3]]}});return i?.[0]}).filter(Boolean);console.log(o);const s=o?.length||0;if(console.log(`templateCount = ${s}`),!(s>0))return this.o.G(`Displaying ${s} templates.`),t;{const t=i.filter(t=>Object.keys(t.j).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.L||0),0),n=(new Intl.NumberFormat).format(t);this.o.G(`Displaying ${s} template${1==s?"":"s"}.\nTotal pixels: ${n}`)}const a=await createImageBitmap(t),r=new OffscreenCanvas(n,n),l=r.getContext("2d");l.imageSmoothingEnabled=!1,l.beginPath(),l.rect(0,0,n,n),l.clip(),l.clearRect(0,0,n,n),l.drawImage(a,0,0,n,n);for(const t of o)return console.log("Template:"),console.log(t),l.drawImage(t.Ot,Number(t.Nt[0])*this.rt,Number(t.Nt[1])*this.rt),await r.convertToBlob({type:"image/png"})}Dt(t){console.log("Importing JSON..."),console.log(t),"BlueMarble"==t?.whoami&&o(this,m,u).call(this,t)}kt(t){this.ft=t}}(w,g,x)),S=new class{constructor(t){i(this,d),this.It=t,this.Bt=!1,this.Wt=[],this._t=[]}Pt(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.X("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.It.ot=i.id,t.I("bm-u",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.I("bm-p",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.I("bm-i",`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))),l=new URLSearchParams(n.endpoint.split("?")[1]),c=[l.get("x"),l.get("y")];if(this.Wt.length&&(!o.length||!c.length))return void t.X("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Wt=[...o,...c];const m=(s=o,a=c,[parseInt(s[0])%4*1e3+parseInt(a[0]),parseInt(s[1])%4*1e3+parseInt(a[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${m[0]}, ${m[1]}`)){let e=document.querySelector("#bm-h");const n=`(Tl X: ${o[0]}, Tl Y: ${o[1]}, Px X: ${c[0]}, Px Y: ${c[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-h",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.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,b=n.blobData,p=await this.It.Tt(b,u);window.postMessage({source:"blue-marble",blobID:d,blobData:p,blink:n.blink});break;case"robots":this.Bt="false"==i.userscript?.toString().toLowerCase();break}var s,a})}async Xt(t){console.log("Sending heartbeat to telemetry server...");let e=GM_getValue("bmUserSettings","{}");if(e=JSON.parse(e),!e||!e.telemetry||!e.uuid)return void console.log("Telemetry is disabled, not sending heartbeat.");const n=navigator.userAgent;let i=await o(this,d,b).call(this,n),s=o(this,d,p).call(this,n);GM_xmlhttpRequest({method:"POST",url:"https://telemetry.thebluecorner.net/heartbeat",headers:{"Content-Type":"application/json"},data:JSON.stringify({uuid:e.uuid,version:t,browser:i,os:s}),onload:t=>{200!==t.status&&a("Failed to send heartbeat:",t.statusText)},onerror:t=>{a("Error sending heartbeat:",t)}})}}($);x.h(S);var M=JSON.parse(GM_getValue("bmTemplates","{}"));console.log(M),$.Dt(M);var T=JSON.parse(GM_getValue("bmUserSettings","{}"));if(console.log(T),console.log(Object.keys(T).length),0==Object.keys(T).length){const t=crypto.randomUUID();console.log(t),GM.setValue("bmUserSettings",JSON.stringify({uuid:t}))}if(setInterval(()=>S.Xt(g),18e5),console.log(`Telemetry is ${!(null==T?.telemetry)}`),null==T?.telemetry||T?.telemetry>1){const t=new s(w,g);t.h(S),t.v({id:"bm-d",style:"top: 0px; left: 0px; width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; z-index: 9999;"}).v({id:"bm-7",style:"display: flex; flex-direction: column; align-items: center;"}).v({id:"bm-1",style:"margin-top: 10%;"}).T(1,{textContent:`${w} Telemetry`}).u().u().v({id:"bm-e",style:"max-width: 50%; overflow-y: auto; max-height: 80vh;"}).O().u().C().u().v({style:"width: fit-content; margin: auto; text-align: center;"}).D({id:"bm-8",textContent:"More Information"},(t,e)=>{e.onclick=()=>{window.open("https://github.com/SwingTheVine/Wplace-TelemetryServer#telemetry-data","_blank","noopener noreferrer")}}).u().u().C().u().v({style:"width: fit-content; margin: auto; text-align: center;"}).D({id:"bm-5",textContent:"Enable Telemetry",style:"margin-right: 2ch;"},(t,e)=>{e.onclick=()=>{const t=JSON.parse(GM_getValue("bmUserSettings","{}"));t.telemetry=1,GM.setValue("bmUserSettings",JSON.stringify(t));const e=document.getElementById("bm-d");e&&(e.style.display="none")}}).u().D({id:"bm-2",textContent:"Disable Telemetry"},(t,e)=>{e.onclick=()=>{const t=JSON.parse(GM_getValue("bmUserSettings","{}"));t.telemetry=0,GM.setValue("bmUserSettings",JSON.stringify(t));const e=document.getElementById("bm-d");e&&(e.style.display="none")}}).u().u().C().u().$({textContent:"We collect anonymous telemetry data such as your browser, OS, and script version to make the experience better for everyone. The data is never shared personally. The data is never sold. You can turn this off by pressing the 'Disable' button, but keeping it on helps us improve features and reliability faster. Thank you for supporting the Blue Marble!"}).u().$({textContent:'You can disable telemetry by pressing the "Disable" button below.'}).u().u().u().p(document.body)}!function(){let t=!1;x.v({id:"bm-A",style:"top: 10px; right: 75px;"}).v({id:"bm-j"}).v({id:"bm-z"}).u().M({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-A"),o=document.querySelector("#bm-j"),s=document.querySelector("#bm-z"),a=document.querySelector("#bm-k"),r=document.querySelector("#bm-q"),l=document.querySelector("#bm-r"),c=document.querySelector("#bm-s"),m=document.querySelector("#bm-l"),h=document.querySelectorAll("#bm-k input");t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-A h1","#bm-f","#bm-A hr","#bm-c > *:not(#bm-k)","#bm-a","#bm-6",`#${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"),l&&(l.style.display="none"),c&&(c.style.display="none"),m&&(m.style.display="none"),h.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=""),l&&(l.style.display="",l.style.marginTop=""),c&&(c.style.display="",c.style.marginTop=""),m&&(m.style.display="",m.style.marginTop=""),h.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)"})}).u().T(1,{textContent:w}).u().u().O().u().v({id:"bm-f"}).$({id:"bm-u",textContent:"Username:"}).u().$({id:"bm-p",textContent:"Droplets:"}).u().$({id:"bm-i",textContent:"Next level in..."}).u().u().O().u().v({id:"bm-c"}).v({id:"bm-k"}).D({id:"bm-q",className:"bm-D",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.t?.Wt;e?.[0]?(t.I("bm-v",e?.[0]||""),t.I("bm-w",e?.[1]||""),t.I("bm-x",e?.[2]||""),t.I("bm-y",e?.[3]||"")):t.X("Coordinates are malformed! Did you try clicking on the canvas first?")}}).u().B({type:"number",id:"bm-v",placeholder:"Tl X",min:0,max:2047,step:1,required:!0},(t,e)=>{e.addEventListener("paste",t=>{let e=(t.clipboardData||window.clipboardData).getData("text").split(" ").filter(t=>t).map(Number).filter(t=>!isNaN(t));if(4!==e.length)return;let n=selectAllCoordinateInputs(document);for(let t=0;tpersistCoords();e.addEventListener("input",n),e.addEventListener("change",n)}).u().B({type:"number",id:"bm-w",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0},(t,e)=>{const n=()=>persistCoords();e.addEventListener("input",n),e.addEventListener("change",n)}).u().B({type:"number",id:"bm-x",placeholder:"Px X",min:0,max:2047,step:1,required:!0},(t,e)=>{const n=()=>persistCoords();e.addEventListener("input",n),e.addEventListener("change",n)}).u().B({type:"number",id:"bm-y",placeholder:"Px Y",min:0,max:2047,step:1,required:!0},(t,e)=>{const n=()=>persistCoords();e.addEventListener("input",n),e.addEventListener("change",n)}).u().u().W({id:"bm-a",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).u().v({id:"bm-4"}).D({id:"bm-s",textContent:"Enable"},(t,e)=>{e.onclick=()=>{t.t?.It?.kt(!0),t.G("Enabled templates!")}}).u().D({id:"bm-r",textContent:"Create"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-a"),n=document.querySelector("#bm-v");if(!n.checkValidity())return n.reportValidity(),void t.X("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-w");if(!i.checkValidity())return i.reportValidity(),void t.X("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-x");if(!o.checkValidity())return o.reportValidity(),void t.X("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-y");if(!s.checkValidity())return s.reportValidity(),void t.X("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?($.$t(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(o.value),Number(s.value)]),t.G("Drew to canvas!")):t.X("No file selected!")}}).u().D({id:"bm-l",textContent:"Disable"},(t,e)=>{e.onclick=()=>{t.t?.It?.kt(!1),t.G("Disabled templates!")}}).u().u()._({id:x.i,placeholder:`Status: Sleeping...\nVersion: ${g}`,readOnly:!0}).u().v({id:"bm-6"}).v().D({id:"bm-m",className:"bm-D",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).u().D({id:"bm-n",className:"bm-D",innerHTML:"🌐",title:"Official Blue Marble Website"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://bluemarble.lol/","_blank","noopener noreferrer")})}).u().u().S({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).u().u().u().p(document.body)}(),x.P("#bm-A","#bm-z"),S.Pt(x),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-t");if(!i){i=document.createElement("button"),i.id="bm-t",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 ↑"};const t=n.parentNode.parentNode.parentNode.parentNode.querySelector("h2");t.parentNode?.appendChild(i)}}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${w}%c (${g}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file +(()=>{var e,t,n=e=>{throw TypeError(e)},i=(e,t,i)=>t.has(e)?n("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),o=(e,t,i)=>(((e,t)=>{t.has(e)||n("Cannot access private method")})(e,t),i),s=class{constructor(t,n){i(this,e),this.name=t,this.version=n,this.t=null,this.i="bm-o",this.o=null,this.m=null,this.l=[]}u(e){this.t=e}h(){return this.l.length>0&&(this.m=this.l.pop()),this}p(e){e?.appendChild(this.o),this.o=null,this.m=null,this.l=[]}v(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"div",{},n)),this}S(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"p",{},n)),this}$(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"small",{},n)),this}M(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"img",{},n)),this}T(n,i={},s=()=>{}){return s(this,o(this,e,t).call(this,"h"+n,{},i)),this}k(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"hr",{},n)),this}D(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"br",{},n)),this}O(n={},i=()=>{}){const s=o(this,e,t).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const r=o(this,e,t).call(this,"input",{type:"checkbox"},n);return s.insertBefore(r,s.firstChild),this.h(),i(this,s,r),this}C(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"button",{},n)),this}B(n={},i=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const r={textContent:"?",className:"bm-D",onclick:()=>{this.N(this.i,s)}};return i(this,o(this,e,t).call(this,"button",r,n)),this}I(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"input",{},n)),this}G(n={},i=()=>{}){const s=n.textContent??"";delete n.textContent;const r=o(this,e,t).call(this,"div"),a=o(this,e,t).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.h();const m=o(this,e,t).call(this,"button",{textContent:s});return this.h(),this.h(),a.setAttribute("tabindex","-1"),a.setAttribute("aria-hidden","true"),m.addEventListener("click",()=>{a.click()}),a.addEventListener("change",()=>{m.style.maxWidth=`${m.offsetWidth}px`,a.files.length>0?m.textContent=a.files[0].name:m.textContent=s}),i(this,r,a,m),this}L(n={},i=()=>{}){return i(this,o(this,e,t).call(this,"textarea",{},n)),this}N(e,t,n=!1){const i=document.getElementById(e.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=t:n?i.textContent=t:i.innerHTML=t)}P(e,t){let n,i=!1,o=0,s=null,r=0,a=0,m=0,l=0;if(e=document.querySelector("#"==e?.[0]?e:"#"+e),t=document.querySelector("#"==t?.[0]?t:"#"+t),!e||!t)return void this.W(`Can not drag! ${e?"":"moveMe"} ${e||t?"":"and "}${t?"":"iMoveThings "}was not found!`);const c=()=>{if(i){const t=Math.abs(r-m),n=Math.abs(a-l);(t>.5||n>.5)&&(r=m,a=l,e.style.transform=`translate(${r}px, ${a}px)`,e.style.left="0px",e.style.top="0px",e.style.right=""),s=requestAnimationFrame(c)}};let u=null;const d=(d,h)=>{i=!0,u=e.getBoundingClientRect(),n=d-u.left,o=h-u.top;const b=window.getComputedStyle(e).transform;if(b&&"none"!==b){const e=new DOMMatrix(b);r=e.m41,a=e.m42}else r=u.left,a=u.top;m=r,l=a,document.body.style.userSelect="none",t.classList.add("dragging"),s&&cancelAnimationFrame(s),c()},h=()=>{i=!1,s&&(cancelAnimationFrame(s),s=null),document.body.style.userSelect="",t.classList.remove("dragging")};t.addEventListener("mousedown",function(e){e.preventDefault(),d(e.clientX,e.clientY)}),t.addEventListener("touchstart",function(e){const t=e?.touches?.[0];t&&(d(t.clientX,t.clientY),e.preventDefault())},{passive:!1}),document.addEventListener("mousemove",function(e){i&&u&&(m=e.clientX-n,l=e.clientY-o)},{passive:!0}),document.addEventListener("touchmove",function(e){if(i&&u){const t=e?.touches?.[0];if(!t)return;m=t.clientX-n,l=t.clientY-o,e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",h),document.addEventListener("touchend",h),document.addEventListener("touchcancel",h)}_(e){(0,console.info)(`${this.name}: ${e}`),this.N(this.i,"Status: "+e,!0)}W(e){(0,console.error)(`${this.name}: ${e}`),this.N(this.i,"Error: "+e,!0)}};function r(...e){(0,console.error)(...e)}function a(e,t){if(0===e)return t[0];let n="";const i=t.length;for(;e>0;)n=t[e%i]+n,e=Math.floor(e/i);return n}function m(e){let t="";for(let n=0;ns.set(e.id,0));for(let e=0;e>16&255,u=e>>8&255,d=255&e;if(e>>>24==0)return 0;for(let e=0;en)continue;const b=t*t+r*r+h*h;b0)for(const e in t){const n=e,i=t[e];if(console.log(n),t.hasOwnProperty(e)){const e=n.split(" "),t=Number(e?.[0]),o=e?.[1]||"0",s=i.name||`Template ${t||""}`,r=i.tiles,a={};for(const e in r)if(console.log(e),r.hasOwnProperty(e)){const t=l(r[e]),n=new Blob([t],{type:"image/png"}),i=await createImageBitmap(n);a[e]=i}const m=new v({displayName:s,U:t||this.ee?.length||0,R:o||""});m.X=a,this.ee.push(m),console.log(this.ee),console.log("^^^ This ^^^")}}},g=new WeakSet,f=async function(e=navigator.userAgent){return(e=e||"").includes("OPR/")||e.includes("Opera")?"Opera":e.includes("Edg/")?"Edge":e.includes("Vivaldi")?"Vivaldi":e.includes("YaBrowser")?"Yandex":e.includes("Kiwi")?"Kiwi":e.includes("Brave")?"Brave":e.includes("Firefox/")?"Firefox":e.includes("Chrome/")?"Chrome":e.includes("Safari/")?"Safari":navigator.brave&&"function"==typeof navigator.brave.isBrave&&await navigator.brave.isBrave()?"Brave":"Unknown"},w=function(e=navigator.userAgent){return/Windows NT 11/i.test(e=e||"")?"Windows 11":/Windows NT 10/i.test(e)?"Windows 10":/Windows NT 6\.3/i.test(e)?"Windows 8.1":/Windows NT 6\.2/i.test(e)?"Windows 8":/Windows NT 6\.1/i.test(e)?"Windows 7":/Windows NT 6\.0/i.test(e)?"Windows Vista":/Windows NT 5\.1|Windows XP/i.test(e)?"Windows XP":/Mac OS X 10[_\.]15/i.test(e)?"macOS Catalina":/Mac OS X 10[_\.]14/i.test(e)?"macOS Mojave":/Mac OS X 10[_\.]13/i.test(e)?"macOS High Sierra":/Mac OS X 10[_\.]12/i.test(e)?"macOS Sierra":/Mac OS X 10[_\.]11/i.test(e)?"OS X El Capitan":/Mac OS X 10[_\.]10/i.test(e)?"OS X Yosemite":/Mac OS X 10[_\.]/i.test(e)?"macOS":/Android/i.test(e)?"Android":/iPhone|iPad|iPod/i.test(e)?"iOS":/Linux/i.test(e)?"Linux":"Unknown"};var x=GM_info.script.name.toString(),S=GM_info.script.version.toString();!function(e){const t=document.createElement("script");t.setAttribute("bm-E",x),t.setAttribute("bm-B","color: cornflowerblue;"),t.textContent=`(${e})();`,document.documentElement?.appendChild(t),t.remove()}(()=>{const e=document.currentScript,t=e?.getAttribute("bm-E")||"Blue Marble",n=e?.getAttribute("bm-B")||"",i=new Map;window.addEventListener("message",e=>{const{source:o,endpoint:s,blobID:r,blobData:a,blink:m}=e.data,l=Date.now()-m;if(console.groupCollapsed(`%c${t}%c: ${i.size} Recieved IMAGE message about blob "${r}"`,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&&r&&a&&!s){const e=i.get(r);"function"==typeof e?e(a):function(...e){(0,console.warn)(...e)}(`%c${t}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",r),i.delete(r)}});const o=window.fetch;window.fetch=async function(...e){const s=await o.apply(this,e),r=s.clone(),a=(e[0]instanceof Request?e[0]?.url:e[0])||"ignore",m=r.headers.get("content-type")||"";if(m.includes("application/json"))console.log(`%c${t}%c: Sending JSON message about endpoint "${a}"`,n,""),r.json().then(e=>{window.postMessage({source:"blue-marble",endpoint:a,jsonData:e},"*")}).catch(e=>{console.error(`%c${t}%c: Failed to parse JSON: `,n,"",e)});else if(m.includes("image/")&&!a.includes("openfreemap")&&!a.includes("maps")){const e=Date.now(),o=await r.blob();return console.log(`%c${t}%c: ${i.size} Sending IMAGE message about endpoint "${a}"`,n,""),new Promise(s=>{const m=crypto.randomUUID();i.set(m,e=>{s(new Response(e,{headers:r.headers,status:r.status,statusText:r.statusText})),console.log(`%c${t}%c: ${i.size} Processed blob "${m}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:a,blobID:m,blobData:o,blink:e})}).catch(o=>{const s=Date.now();console.error(`%c${t}%c: Failed to Promise blob!`,n,""),console.groupCollapsed(`%c${t}%c: Details of failed blob Promise:`,n,""),console.log(`Endpoint: ${a}\nThere are ${i.size} blobs processing...\nBlink: ${e.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 $=GM_getResourceText("CSS-BM-File");GM_addStyle($);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.te=null,this.ne=null,this.ie="#bm-h"}oe(e){return this.ne=e,this.te=new MutationObserver(e=>{for(const t of e)for(const e of t.addedNodes)e instanceof HTMLElement&&e.matches?.(this.ie)}),this}se(){return this.te}observe(e,t=!1,n=!1){e.observe(this.ne,{childList:t,subtree:n})}};var T=new s(x,S),k=(new s(x,S),new class{constructor(e,t,n){i(this,h),this.name=e,this.version=t,this.o=n,this.re="1.0.0",this.ae=null,this.me="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.A=1e3,this.le=3,this.ce=function(){const e=y;e.unshift({id:-1,premium:!1,name:"Erased",rgb:[222,250,206]}),e.unshift({id:-2,premium:!1,name:"Other",rgb:[0,0,0]});const t=new Uint32Array(e.length),n=new Uint32Array(e.length),i=new Uint32Array(e.length),o=new Uint32Array(e.length);for(let s=0;se.U-t.U),console.log(i);const o=i.map(e=>{const n=Object.keys(e.X).filter(e=>e.startsWith(t));if(0===n.length)return null;const i=n.map(t=>{const n=t.split(",");return{Oe:e.X[t],Ce:[n[0],n[1]],Be:[n[2],n[3]]}});return i?.[0]}).filter(Boolean);console.log(o);const s=o?.length||0;if(console.log(`templateCount = ${s}`),!(s>0))return this.o._(`Displaying ${s} templates.`),e;{const e=i.filter(e=>Object.keys(e.X).filter(e=>e.startsWith(t)).length>0).reduce((e,t)=>e+(t.J||0),0),n=(new Intl.NumberFormat).format(e);this.o._(`Displaying ${s} template${1==s?"":"s"}.\nTotal pixels: ${n}`)}const r=await createImageBitmap(e),a=new OffscreenCanvas(n,n),m=a.getContext("2d");m.imageSmoothingEnabled=!1,m.beginPath(),m.rect(0,0,n,n),m.clip(),m.clearRect(0,0,n,n),m.drawImage(r,0,0,n,n);for(const e of o)console.log("Template:"),console.log(e),m.drawImage(e.Oe,Number(e.Be[0])*this.le,Number(e.Be[1])*this.le);return await a.convertToBlob({type:"image/png"})}Ne(e){console.log("Importing JSON..."),console.log(e),"BlueMarble"==e?.whoami&&o(this,h,p).call(this,e)}Ie(e){this.we=e}}(x,S,T)),D=new class{constructor(e){i(this,g),this.Ge=e,this.Le=!1,this.Pe=[],this.We=[]}_e(e){window.addEventListener("message",async t=>{const n=t.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const o=n.endpoint?.split("?")[0].split("/").filter(e=>e&&isNaN(Number(e))).filter(e=>e&&!e.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 e.W("You are not logged in!\nCould not fetch userdata.");const t=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(a(i.id,"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~")),this.Ge.ae=i.id,e.N("bm-u",`Username: ${function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}(i.name)}`),e.N("bm-p",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),e.N("bm-i",`Next level in ${(new Intl.NumberFormat).format(t)} pixel${1==t?"":"s"}`);break;case"pixel":const o=n.endpoint.split("?")[0].split("/").filter(e=>e&&!isNaN(Number(e))),m=new URLSearchParams(n.endpoint.split("?")[1]),l=[m.get("x"),m.get("y")];if(this.Pe.length&&(!o.length||!l.length))return void e.W("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Pe=[...o,...l];const c=(s=o,r=l,[parseInt(s[0])%4*1e3+parseInt(r[0]),parseInt(s[1])%4*1e3+parseInt(r[1])]),u=document.querySelectorAll("span");for(const e of u)if(e.textContent.trim().includes(`${c[0]}, ${c[1]}`)){let t=document.querySelector("#bm-h");const n=`(Tl X: ${o[0]}, Tl Y: ${o[1]}, Px X: ${l[0]}, Px Y: ${l[1]})`;t?t.textContent=n:(t=document.createElement("span"),t.id="bm-h",t.textContent=n,t.style="margin-left: calc(var(--spacing)*3); font-size: small;",e.parentNode.parentNode.insertAdjacentElement("afterend",t))}break;case"tiles":let d=n.endpoint.split("/");d=[parseInt(d[d.length-2]),parseInt(d[d.length-1].replace(".png",""))];const h=n.blobID,b=n.blobData,p=await this.Ge.De(b,d);window.postMessage({source:"blue-marble",blobID:h,blobData:p,blink:n.blink});break;case"robots":this.Le="false"==i.userscript?.toString().toLowerCase();break}var s,r})}async Ue(e){console.log("Sending heartbeat to telemetry server...");let t=GM_getValue("bmUserSettings","{}");if(t=JSON.parse(t),!t||!t.telemetry||!t.uuid)return void console.log("Telemetry is disabled, not sending heartbeat.");const n=navigator.userAgent;let i=await o(this,g,f).call(this,n),s=o(this,g,w).call(this,n);GM_xmlhttpRequest({method:"POST",url:"https://telemetry.thebluecorner.net/heartbeat",headers:{"Content-Type":"application/json"},data:JSON.stringify({uuid:t.uuid,version:e,browser:i,os:s}),onload:e=>{200!==e.status&&r("Failed to send heartbeat:",e.statusText)},onerror:e=>{r("Error sending heartbeat:",e)}})}}(k);T.u(D);var O=JSON.parse(GM_getValue("bmTemplates","{}"));console.log(O),k.Ne(O);var C=JSON.parse(GM_getValue("bmUserSettings","{}"));if(console.log(C),console.log(Object.keys(C).length),0==Object.keys(C).length){const e=crypto.randomUUID();console.log(e),GM.setValue("bmUserSettings",JSON.stringify({uuid:e}))}if(setInterval(()=>D.Ue(S),18e5),console.log(`Telemetry is ${!(null==C?.telemetry)}`),null==C?.telemetry||C?.telemetry>1){const e=new s(x,S);e.u(D),e.v({id:"bm-d",style:"top: 0px; left: 0px; width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; z-index: 9999;"}).v({id:"bm-7",style:"display: flex; flex-direction: column; align-items: center;"}).v({id:"bm-1",style:"margin-top: 10%;"}).T(1,{textContent:`${x} Telemetry`}).h().h().v({id:"bm-e",style:"max-width: 50%; overflow-y: auto; max-height: 80vh;"}).k().h().D().h().v({style:"width: fit-content; margin: auto; text-align: center;"}).C({id:"bm-8",textContent:"More Information"},(e,t)=>{t.onclick=()=>{window.open("https://github.com/SwingTheVine/Wplace-TelemetryServer#telemetry-data","_blank","noopener noreferrer")}}).h().h().D().h().v({style:"width: fit-content; margin: auto; text-align: center;"}).C({id:"bm-5",textContent:"Enable Telemetry",style:"margin-right: 2ch;"},(e,t)=>{t.onclick=()=>{const e=JSON.parse(GM_getValue("bmUserSettings","{}"));e.telemetry=1,GM.setValue("bmUserSettings",JSON.stringify(e));const t=document.getElementById("bm-d");t&&(t.style.display="none")}}).h().C({id:"bm-2",textContent:"Disable Telemetry"},(e,t)=>{t.onclick=()=>{const e=JSON.parse(GM_getValue("bmUserSettings","{}"));e.telemetry=0,GM.setValue("bmUserSettings",JSON.stringify(e));const t=document.getElementById("bm-d");t&&(t.style.display="none")}}).h().h().D().h().S({textContent:"We collect anonymous telemetry data such as your browser, OS, and script version to make the experience better for everyone. The data is never shared personally. The data is never sold. You can turn this off by pressing the 'Disable' button, but keeping it on helps us improve features and reliability faster. Thank you for supporting the Blue Marble!"}).h().S({textContent:'You can disable telemetry by pressing the "Disable" button below.'}).h().h().h().p(document.body)}!function(){let e=!1;T.v({id:"bm-A",style:"top: 10px; right: 75px;"}).v({id:"bm-j"}).v({id:"bm-z"}).h().M({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(t,n)=>{n.addEventListener("click",()=>{e=!e;const i=document.querySelector("#bm-A"),o=document.querySelector("#bm-j"),s=document.querySelector("#bm-z"),r=document.querySelector("#bm-k"),a=document.querySelector("#bm-q"),m=document.querySelector("#bm-r"),l=document.querySelector("#bm-s"),c=document.querySelector("#bm-l"),u=document.querySelectorAll("#bm-k input");e||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-A h1","#bm-f","#bm-A hr","#bm-c > *:not(#bm-k)","#bm-a","#bm-6",`#${t.i}`].forEach(t=>{document.querySelectorAll(t).forEach(t=>{t.style.display=e?"none":""})}),e?(r&&(r.style.display="none"),a&&(a.style.display="none"),m&&(m.style.display="none"),l&&(l.style.display="none"),c&&(c.style.display="none"),u.forEach(e=>{e.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")):(r&&(r.style.display="",r.style.flexDirection="",r.style.justifyContent="",r.style.alignItems="",r.style.gap="",r.style.textAlign="",r.style.margin=""),a&&(a.style.display=""),m&&(m.style.display="",m.style.marginTop=""),l&&(l.style.display="",l.style.marginTop=""),c&&(c.style.display="",c.style.marginTop=""),u.forEach(e=>{e.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=e?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)"})}).h().T(1,{textContent:x}).h().h().k().h().v({id:"bm-f"}).S({id:"bm-u",textContent:"Username:"}).h().S({id:"bm-p",textContent:"Droplets:"}).h().S({id:"bm-i",textContent:"Next level in..."}).h().h().k().h().v({id:"bm-c"}).v({id:"bm-k"}).C({id:"bm-q",className:"bm-D",style:"margin-top: 0;",innerHTML:''},(e,t)=>{t.onclick=()=>{const t=e.t?.Pe;t?.[0]?(e.N("bm-v",t?.[0]||""),e.N("bm-w",t?.[1]||""),e.N("bm-x",t?.[2]||""),e.N("bm-y",t?.[3]||"")):e.W("Coordinates are malformed! Did you try clicking on the canvas first?")}}).h().I({type:"number",id:"bm-v",placeholder:"Tl X",min:0,max:2047,step:1,required:!0},(e,t)=>{t.addEventListener("paste",e=>{let t=(e.clipboardData||window.clipboardData).getData("text").split(" ").filter(e=>e).map(Number).filter(e=>!isNaN(e));if(4!==t.length)return;let n=selectAllCoordinateInputs(document);for(let e=0;epersistCoords();t.addEventListener("input",n),t.addEventListener("change",n)}).h().I({type:"number",id:"bm-w",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0},(e,t)=>{const n=()=>persistCoords();t.addEventListener("input",n),t.addEventListener("change",n)}).h().I({type:"number",id:"bm-x",placeholder:"Px X",min:0,max:2047,step:1,required:!0},(e,t)=>{const n=()=>persistCoords();t.addEventListener("input",n),t.addEventListener("change",n)}).h().I({type:"number",id:"bm-y",placeholder:"Px Y",min:0,max:2047,step:1,required:!0},(e,t)=>{const n=()=>persistCoords();t.addEventListener("input",n),t.addEventListener("change",n)}).h().h().G({id:"bm-a",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).h().v({id:"bm-4"}).C({id:"bm-s",textContent:"Enable"},(e,t)=>{t.onclick=()=>{e.t?.Ge?.Ie(!0),e._("Enabled templates!")}}).h().C({id:"bm-r",textContent:"Create"},(e,t)=>{t.onclick=()=>{const t=document.querySelector("#bm-a"),n=document.querySelector("#bm-v");if(!n.checkValidity())return n.reportValidity(),void e.W("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-w");if(!i.checkValidity())return i.reportValidity(),void e.W("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-x");if(!o.checkValidity())return o.reportValidity(),void e.W("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-y");if(!s.checkValidity())return s.reportValidity(),void e.W("Coordinates are malformed! Did you try clicking on the canvas first?");t?.files[0]?(k.Me(t.files[0],t.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(o.value),Number(s.value)]),e._("Drew to canvas!")):e.W("No file selected!")}}).h().C({id:"bm-l",textContent:"Disable"},(e,t)=>{t.onclick=()=>{e.t?.Ge?.Ie(!1),e._("Disabled templates!")}}).h().h().L({id:T.i,placeholder:`Status: Sleeping...\nVersion: ${S}`,readOnly:!0}).h().v({id:"bm-6"}).v().C({id:"bm-m",className:"bm-D",innerHTML:"🎨",title:"Template Color Converter"},(e,t)=>{t.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).h().C({id:"bm-n",className:"bm-D",innerHTML:"🌐",title:"Official Blue Marble Website"},(e,t)=>{t.addEventListener("click",()=>{window.open("https://bluemarble.lol/","_blank","noopener noreferrer")})}).h().h().$({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).h().h().h().p(document.body)}(),T.P("#bm-A","#bm-z"),D._e(T),new MutationObserver((e,t)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-t");if(!i){i=document.createElement("button"),i.id="bm-t",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const e=this.parentNode.parentNode.parentNode.parentNode,t="Move ↑"==this.textContent;e.parentNode.className=e.parentNode.className.replace(t?"bottom":"top",t?"top":"bottom"),e.style.borderTopLeftRadius=t?"0px":"var(--radius-box)",e.style.borderTopRightRadius=t?"0px":"var(--radius-box)",e.style.borderBottomLeftRadius=t?"var(--radius-box)":"0px",e.style.borderBottomRightRadius=t?"var(--radius-box)":"0px",this.textContent=t?"Move ↓":"Move ↑"};const e=n.parentNode.parentNode.parentNode.parentNode.querySelector("h2");e.parentNode?.appendChild(i)}}).observe(document.body,{childList:!0,subtree:!0}),function(...e){(0,console.log)(...e)}(`%c${x}%c (${S}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index c0fd638..5b2fd90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,7 +51,7 @@ Contact Me Blue Marble Website WakaTime -Total Patches +Total Patches Total Lines of Code Total Comments Compression diff --git a/package-lock.json b/package-lock.json index 5b8124f..013c769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wplace-bluemarble", - "version": "0.88.5", + "version": "0.88.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wplace-bluemarble", - "version": "0.88.5", + "version": "0.88.25", "devDependencies": { "esbuild": "^0.25.0", "jsdoc": "^4.0.5", diff --git a/package.json b/package.json index b6d8425..e3a9d19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wplace-bluemarble", - "version": "0.88.5", + "version": "0.88.25", "type": "module", "homepage": "https://bluemarble.lol/", "repository": { diff --git a/src/BlueMarble.meta.js b/src/BlueMarble.meta.js index 7e46847..71e1287 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.88.5 +// @version 0.88.25 // @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/Template.js b/src/Template.js index 3752b9e..1700f5f 100644 --- a/src/Template.js +++ b/src/Template.js @@ -1,4 +1,4 @@ -import { uint8ToBase64, colorpalette } from "./utils"; +import { uint8ToBase64 } from "./utils"; /** An instance of a template. * Handles all mathematics, manipulation, and analysis regarding a single template. @@ -42,17 +42,21 @@ export default class Template { } /** Creates chunks of the template for each tile. - * + * @param {Number} tileSize - Size of the tile as determined by templateManager + * @param {Object} paletteBM - An collection of Uint32Arrays containing the palette BM uses + * @param {Number} paletteTolerance - How close an RGB color has to be in order to be considered a palette color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value. * @returns {Object} Collection of template bitmaps & buffers organized by tile coordinates * @since 0.65.4 */ - async createTemplateTiles() { + async createTemplateTiles(tileSize, paletteBM, paletteTolerance) { console.log('Template coordinates:', this.coords); const shreadSize = 3; // Scale image factor for pixel art enhancement (must be odd) const bitmap = await createImageBitmap(this.file); // Create efficient bitmap from uploaded file const imageWidth = bitmap.width; const imageHeight = bitmap.height; + + this.tileSize = tileSize; // Tile size predetermined by the templateManager // Calculate total pixel count using standard width × height formula // TODO: Use non-transparent pixels instead of basic width times height @@ -67,6 +71,26 @@ export default class Template { const canvas = new OffscreenCanvas(this.tileSize, this.tileSize); const context = canvas.getContext('2d', { willReadFrequently: true }); + + // Prep the canvas for drawing the entire template (so we can find total pixels) + canvas.width = imageWidth; + canvas.height = imageHeight; + context.imageSmoothingEnabled = false; // Nearest neighbor + + context.drawImage(bitmap, 0, 0); // Draws the template to the canvas + + let timer = Date.now(); + this.#calculateTotalPixelsFromTemplateData(context.getImageData(0, 0, imageWidth, imageHeight), paletteBM, paletteTolerance); // Calculates total pixels from the template buffer retrieved from the canvas context image data + console.log(`Calculating total pixels took ${(Date.now() - timer) / 1000.0} seconds`); + + timer = Date.now(); + + // Creates a mask where the middle pixel is white, and everything else is transparent + const canvasMask = new OffscreenCanvas(3, 3); + const contextMask = canvasMask.getContext("2d"); + contextMask.clearRect(0, 0, 3, 3); + contextMask.fillStyle = "white"; + contextMask.fillRect(1, 1, 1, 1); // For every tile... for (let pixelY = this.coords[3]; pixelY < imageHeight + this.coords[3]; ) { @@ -124,44 +148,56 @@ export default class Template { // window.open(url, '_blank'); // Opens a new tab with blob // setTimeout(() => URL.revokeObjectURL(url), 60000); // Destroys the blob 1 minute later - const imageData = context.getImageData(0, 0, canvasWidth, canvasHeight); // Data of the image on the canvas + context.save(); // Saves the current context of the canvas + context.globalCompositeOperation = "destination-in"; // The existing canvas content is kept where both the new shape and existing canvas content overlap. Everything else is made transparent. + // For our purposes, this means any non-transparent pixels on the mask will be kept - for (let y = 0; y < canvasHeight; y++) { - for (let x = 0; x < canvasWidth; x++) { - // For every pixel... - const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel - // If the pixel is the color #deface, draw a translucent gray checkerboard pattern - if ( - imageData.data[pixelIndex] === 222 && - imageData.data[pixelIndex + 1] === 250 && - imageData.data[pixelIndex + 2] === 206 - ) { - if ((x + y) % 2 === 0) { // Formula for checkerboard pattern - imageData.data[pixelIndex] = 0; - imageData.data[pixelIndex + 1] = 0; - imageData.data[pixelIndex + 2] = 0; - imageData.data[pixelIndex + 3] = 32; // Translucent black - } else { // Transparent negative space - imageData.data[pixelIndex + 3] = 0; - } - } else if (x % shreadSize !== 1 || y % shreadSize !== 1) { // Otherwise only draw the middle pixel - imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel - } - } - } + // Fills the canvas with the mask + context.fillStyle = context.createPattern(canvasMask, "repeat"); + context.fillRect(0, 0, canvasWidth, canvasHeight); + + context.restore(); // Restores the context of the canvas to the previous save + + const imageData = context.getImageData(0, 0, canvasWidth, canvasHeight); // Data of the image on the canvas + + // TODO: Make Erased pixels calculated when showing the template, not generating it for the first time. + // For every pixel... + // for (let y = 0; y < canvasHeight; y++) { + // for (let x = 0; x < canvasWidth; x++) { + + // const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel + + // // If the pixel is the color #deface, draw a translucent gray checkerboard pattern + // if ( + // imageData.data[pixelIndex] === 222 && + // imageData.data[pixelIndex + 1] === 250 && + // imageData.data[pixelIndex + 2] === 206 + // ) { + // if ((x + y) % 2 === 0) { // Formula for checkerboard pattern + // imageData.data[pixelIndex] = 0; + // imageData.data[pixelIndex + 1] = 0; + // imageData.data[pixelIndex + 2] = 0; + // imageData.data[pixelIndex + 3] = 32; // Translucent black + // } else { // Transparent negative space + // imageData.data[pixelIndex + 3] = 0; + // } + // } else if (x % shreadSize !== 1 || y % shreadSize !== 1) { // Otherwise make all non-middle pixels transparent + // imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel + // } + // } + // } console.log(`Shreaded pixels for ${pixelX}, ${pixelY}`, imageData); context.putImageData(imageData, 0, 0); // Creates the "0000,0000,000,000" key name - const templateTileName = `${(this.coords[0] + Math.floor(pixelX / 1000)) - .toString() - .padStart(4, '0')},${(this.coords[1] + Math.floor(pixelY / 1000)) - .toString() - .padStart(4, '0')},${(pixelX % 1000) - .toString() - .padStart(3, '0')},${(pixelY % 1000).toString().padStart(3, '0')}`; + const templateTileName = `${ + (this.coords[0] + Math.floor(pixelX / 1000)).toString().padStart(4, '0')},${ + (this.coords[1] + Math.floor(pixelY / 1000)).toString().padStart(4, '0')},${ + (pixelX % 1000).toString().padStart(3, '0')},${ + (pixelY % 1000).toString().padStart(3, '0') + }`; templateTiles[templateTileName] = await createImageBitmap(canvas); // Creates the bitmap @@ -178,8 +214,92 @@ export default class Template { pixelY += drawSizeY; } + console.log(`Parsing template took ${(Date.now() - timer) / 1000.0} seconds`); console.log('Template Tiles: ', templateTiles); console.log('Template Tiles Buffers: ', templateTilesBuffers); return { templateTiles, templateTilesBuffers }; } + + /** Calculates the total pixels for each color for the template. + * + * @param {ImageData} imageData - The pre-shreaded template "casted" onto a canvas + * @param {Object} paletteBM - The palette Blue Marble uses for colors + * @param {Number} paletteTolerance - How close an RGB color has to be in order to be considered a palette color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value. + * @since 0.88.6 + */ + async #calculateTotalPixelsFromTemplateData(imageData, paletteBM, paletteTolerance) { + + const buffer32Arr = new Uint32Array(imageData.data.buffer); // RGB values as a Uint32Array. Each index represents 1 pixel. + + // Makes a copy of the color palette Blue Marble uses, turns it into a Map, and adds data to count the amount of each color + const _colorpalette = new Map(); // Temp color palette + paletteBM.palette.forEach(color => _colorpalette.set(color.id, 0)); + //paletteBM.palette.forEach(color => _colorpalette.set(color.id, { ...color, amount: 0 })); + + // For every pixel... + for (let pixelIndex = 0; pixelIndex < buffer32Arr.length; pixelIndex++) { + + // Finds the best matching + const bestColorID = this.#findClosestPixelColorID(buffer32Arr[pixelIndex], paletteBM, paletteTolerance); + + // Adds one to the "amount" value for that pixel in the temporary color palette Map + _colorpalette.set(bestColorID, _colorpalette.get(bestColorID) + 1); + // This works since the Map keys are the color ID, which can be negative. + } + + console.log(_colorpalette); + } + + /** Takes a 32-bit integer of an RGB value and finds the closest palette color. + * This uses squared Euclidean distance calculations to find the closest color in 3D space. + * @param {Number} pixelColor32 - Pixel to find the color of + * @param {Object} paletteBM - The palette Blue Marble uses for colors + * @param {Number} paletteTolerance - How close an RGB color has to be in order to be considered a palette color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value. + * @returns {Number} The ID value of the color that matches. + * @since 0.88.10 + */ + #findClosestPixelColorID(pixelColor32, paletteBM, paletteTolerance) { + + let bestIndex = Infinity; // Best matching index palette color + let bestDistance = Infinity; // The distance to the best matching index palette color + const { palette: palette, RGB: _, R: paletteR, G: paletteG, B: paletteB } = paletteBM; // Gets the full color palette as Array as well as each R, G, and B palette as a Uint32Array + + const pixelR = (pixelColor32 >> 16) & 0xFF; // Red value for the pixel + const pixelG = (pixelColor32 >> 8) & 0xFF; // Green value for the pixel + const pixelB = pixelColor32 & 0xFF; // Blue value for the pixel + + // If the pixel we want to find the palette color of is transparent, then return the transparent index early + if ((pixelColor32 >>> 24) == 0) {return 0;} + + // For every palette color... + for (let paletteColorIndex = 0; paletteColorIndex < palette.length; paletteColorIndex++) { + // ...find how close the pixel is in 3D space to each palette color, then return the closest palette color. + + // Skip all colors in the pallete where the color ID is 0 (Transparent color) or less than 0 (Blue Marble custom color) + if (palette[paletteColorIndex].id <= 0) {continue;} + + // The difference in RGB values between the pixel color and the palette color for each of the 3 channels + const deltaR = paletteR[paletteColorIndex] - pixelR; + const deltaG = paletteG[paletteColorIndex] - pixelG; + const deltaB = paletteB[paletteColorIndex] - pixelB; + + // If the palette color is outside of the tolerance, skip this color + if ((Math.abs(deltaR) + Math.abs(deltaG) + Math.abs(deltaB)) > paletteTolerance) {continue;} + // This is is the Manhattan distance. We don't need to do any of the calculations below if this exceeds the tolerance. + // The tolerance check here is the sum of the difference across the RGB channels. + // E.g. "123,45,6" minus "123,44,5" is 2, which is within tolerance. "123,45,6" minus "23,45,6" is 100, which is outside tolerance. + + // Squared Euclidean distance in space between palette color and pixel color + const distance = (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB); + + // If this palette color is the closest color YET, then update the "best" variables + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = paletteColorIndex; + } + } + + // Returns the ID of the best matching color in the palette, or returns the color ID for "Other" (which is -2) + return (bestIndex == Infinity) ? -2 : palette[bestIndex].id; + } } diff --git a/src/templateManager.js b/src/templateManager.js index 71b6636..7386c6c 100644 --- a/src/templateManager.js +++ b/src/templateManager.js @@ -1,5 +1,5 @@ import Template from "./Template"; -import { base64ToUint8, numberToEncoded } from "./utils"; +import { base64ToUint8, colorpaletteForBlueMarble, consoleLog, numberToEncoded } from "./utils"; /** Manages the template system. * This class handles all external requests for template modification, creation, and analysis. @@ -50,6 +50,8 @@ export default class TemplateManager { this.encodingBase = '!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; // Characters to use for encoding/decoding this.tileSize = 1000; // The number of pixels in a tile. Assumes the tile is square this.drawMult = 3; // The enlarged size for each pixel. E.g. when "3", a 1x1 pixel becomes a 1x1 pixel inside a 3x3 area. MUST BE ODD + this.paletteBM = colorpaletteForBlueMarble(); // Retrieves the color palette BM will use as an Object containing multiple Uint32Arrays + this.paletteTolerance = 3; // Tolerance for how close an RGB value has to be in order to be considered a color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value. // Template this.canvasTemplate = null; // Our canvas @@ -138,7 +140,7 @@ export default class TemplateManager { coords: coords }); //template.chunked = await template.createTemplateTiles(this.tileSize); // Chunks the tiles - const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize); // Chunks the tiles + const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize, this.paletteBM, this.paletteTolerance); // Chunks the tiles template.chunked = templateTiles; // Stores the chunked tile bitmaps // Appends a child into the templates object diff --git a/src/utils.js b/src/utils.js index 5f67f60..cfbf6ca 100644 --- a/src/utils.js +++ b/src/utils.js @@ -142,6 +142,40 @@ export function selectAllCoordinateInputs(document) { return coords; } +/** Processes the palette used for Blue Marble. + * Each ID is sorted from smallest to largest. + * Color ID's are integers, which can be negative. + * Custom colors have been added for the Blue Marble purposes. + * Wplace palette colors have not been modified. + * @since 0.88.6 + */ +export function colorpaletteForBlueMarble() { + + const colorpaletteBM = colorpalette; // Makes a copy + + // Adds the Blue Marble color for "erased" and "other" pixels to the palette list + colorpaletteBM.unshift({ "id": -1, "premium": false, "name": "Erased", "rgb": [222, 250, 206] }); + colorpaletteBM.unshift({ "id": -2, "premium": false, "name": "Other", "rgb": [ 0, 0, 0] }); + + const paletteRGB32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of all 3 channels for each color + const paletteR32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just red channel for each color + const paletteG32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just green channel for each color + const paletteB32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just blue channel for each color + + // For each color... + for (let color = 0; color < colorpaletteBM.length; color++) { + + const [red, green, blue] = colorpaletteBM[color].rgb; // Retrieves the RGB values of the color + + paletteRGB32[color] = (red) | (green << 8) | (blue << 16); // Takes 3 ints of RGB of color and puts in 32 bits + paletteR32[color] = red; // Red channel of color + paletteG32[color] = green; // Green channel of color + paletteB32[color] = blue; // Blue channel of color + } + + return {palette: colorpaletteBM, RGB: paletteRGB32, R: paletteR32, G: paletteG32, B: paletteB32} +} + /** The color palette used by wplace.live * @since 0.78.0 * @examples