From c53bfed433caa153f8a36ac10a7e5552bff11369 Mon Sep 17 00:00:00 2001 From: vishnuvardhan33 <93069382+vishnuvardhan33@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:39:36 +0530 Subject: [PATCH] counts and colors for 81 --- dist/BlueMarble.user.js | 2 +- package-lock.json | 4 +- src/Template.js | 79 +++++++++++- src/main.js | 148 +++++++++++++++++++++- src/templateManager.js | 269 +++++++++++++++++++++++++++++++++++----- src/utils.js | 13 +- 6 files changed, 476 insertions(+), 39 deletions(-) diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js index 2e8ad4e..48d6866 100644 --- a/dist/BlueMarble.user.js +++ b/dist/BlueMarble.user.js @@ -22,4 +22,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),o=class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.t=null,this.i="bm-b",this.o=null,this.l=null,this.h=[]}u(t){this.t=t}m(){return this.h.length>0&&(this.l=this.h.pop()),this}p(t){t?.appendChild(this.o),this.o=null,this.l=null,this.h=[]}v(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}M(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}$(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}C(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}D(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}T(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}I(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}k(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.m(),i(this,o,a),this}N(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}S(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-q",onclick:()=>{this.B(this.i,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}O(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}L(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 !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;"},n);this.m();const c=s(this,t,e).call(this,"button",{textContent:o});return this.m(),this.m(),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=o}),i(this,a,r,c),this}H(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}B(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}j(t,e){let n,i=!1,s=0,o=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.q(`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=""),o=requestAnimationFrame(h)}};let u=null;const m=(m,d)=>{i=!0,u=t.getBoundingClientRect(),n=m-u.left,s=d-u.top;const p=window.getComputedStyle(t).transform;if(p&&"none"!==p){const t=new DOMMatrix(p);a=t.m41,r=t.m42}else a=u.left,r=u.top;c=a,l=r,document.body.style.userSelect="none",e.classList.add("dragging"),o&&cancelAnimationFrame(o),h()},d=()=>{i=!1,o&&(cancelAnimationFrame(o),o=null),document.body.style.userSelect="",e.classList.remove("dragging")};e.addEventListener("mousedown",function(t){t.preventDefault(),m(t.clientX,t.clientY)}),e.addEventListener("touchstart",function(t){const e=t?.touches?.[0];e&&(m(e.clientX,e.clientY),t.preventDefault())},{passive:!1}),document.addEventListener("mousemove",function(t){i&&u&&(c=t.clientX-n,l=t.clientY-s)},{passive:!0}),document.addEventListener("touchmove",function(t){if(i&&u){const e=t?.touches?.[0];if(!e)return;c=e.clientX-n,l=e.clientY-s,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",d),document.addEventListener("touchend",d),document.addEventListener("touchcancel",d)}A(t){(0,console.info)(`${this.name}: ${t}`),this.B(this.i,"Status: "+t,!0)}q(t){(0,console.error)(`${this.name}: ${t}`),this.B(this.i,"Error: "+t,!0)}};function a(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 r(t){let e="";for(let n=0;n0)for(const t in e){const n=t,i=e[t];if(e.hasOwnProperty(t)){const t=n.split(" "),e=Number(t?.[0]),s=t?.[1]||"0",o=i.name||`Template ${e||""}`,a=i.tiles,r={};for(const t in a)if(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 m({displayName:o,_:e||this.X?.length||0,F:s||""});l.P=r,this.X.push(l)}}};var d=GM_info.script.name.toString(),p=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-r",d),e.setAttribute("bm-o","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement?.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-r")||"Blue Marble",n=t?.getAttribute("bm-o")||"",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")&&!o.includes("maps")){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 b=GM_getResourceText("CSS-BM-File");GM_addStyle(b);var f=document.createElement("link");f.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",f.rel="preload",f.as="style",f.onload=function(){this.onload=null,this.rel="stylesheet"},document.head?.appendChild(f),new class{constructor(){this.Z=null,this.K=null,this.tt="#bm-5"}et(t){return this.K=t,this.Z=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.Z}observe(t,e=!1,n=!1){t.observe(this.K,{childList:e,subtree:n})}};var w=new o(d,p),v=(new o(d,p),new class{constructor(t,e,n){i(this,l),this.name=t,this.version=e,this.o=n,this.it="1.0.0",this.st=null,this.ot="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.R=1e3,this.rt=3,this.ct=null,this.lt=null,this.ht="bm-p",this.ut="div#map canvas.maplibregl-canvas",this.dt=null,this.bt="",this.X=[],this.W=null,this.ft=!0}wt(){if(document.body.contains(this.ct))return this.ct;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.ct=e,window.addEventListener("move",this.vt),window.addEventListener("zoom",this.yt),window.addEventListener("resize",this.xt),this.ct}async gt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.it,templates:{}}}async Mt(t,e,n){this.W||(this.W=await this.gt()),this.o.A(`Creating template at ${n.join(", ")}...`);const i=new m({displayName:e,_:0,F:a(this.st||0,this.ot),file:t,coords:n}),{Y:o,J:r}=await i.U(this.R);i.P=o,this.W.templates[`${i._} ${i.F}`]={name:i.displayName,coords:n.join(", "),enabled:!0,tiles:r},this.X=[],this.X.push(i);const c=(new Intl.NumberFormat).format(i.G);this.o.A(`Template created at ${n.join(", ")}! Total pixels: ${c}`),await s(this,l,h).call(this)}$t(){}async Ct(){this.W||(this.W=await this.gt())}async Dt(t,e){if(!this.ft)return t;const n=this.R*this.rt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0");const i=this.X;i.sort((t,e)=>t._-e._);const s=i.map(t=>{const n=Object.keys(t.P).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>{const n=e.split(",");return{Tt:t.P[e],It:[n[0],n[1]],kt:[n[2],n[3]]}});return i?.[0]}).filter(Boolean),o=s?.length||0;if(o>0){const t=i.filter(t=>Object.keys(t.P).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.G||0),0),n=(new Intl.NumberFormat).format(t);this.o.A(`Displaying ${o} template${1==o?"":"s"}.\nTotal pixels: ${n}`)}else this.o.A(`Displaying ${o} templates.`);const a=await createImageBitmap(t),r=new OffscreenCanvas(n,n),c=r.getContext("2d");c.imageSmoothingEnabled=!1,c.beginPath(),c.rect(0,0,n,n),c.clip(),c.clearRect(0,0,n,n),c.drawImage(a,0,0,n,n);for(const t of s)c.drawImage(t.Tt,Number(t.kt[0])*this.rt,Number(t.kt[1])*this.rt);return await r.convertToBlob({type:"image/png"})}Nt(t){"BlueMarble"==t?.whoami&&s(this,l,u).call(this,t)}St(t){this.ft=t}}(d,p,w)),y=new class{constructor(t){this.Bt=t,this.Ot=!1,this.Lt=[],this.zt=[]}Ht(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.q("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.Bt.st=i.id,t.B("bm-h",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.B("bm-c",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.B("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.Lt.length&&(!s.length||!c.length))return void t.q("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Lt=[...s,...c];const l=(o=s,a=c,[parseInt(o[0])%4*1e3+parseInt(a[0]),parseInt(o[1])%4*1e3+parseInt(a[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${l[0]}, ${l[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 m=n.blobID,d=n.blobData,p=await this.Bt.Dt(d,u);window.postMessage({source:"blue-marble",blobID:m,blobData:p,blink:n.blink});break;case"robots":this.Ot="false"==i.userscript?.toString().toLowerCase()}var o,a})}}(v);w.u(y);var x=JSON.parse(GM_getValue("bmTemplates","{}"));v.Nt(x),function(){let t=!1;w.v({id:"bm-n",style:"top: 10px; right: 75px;"}).v({id:"bm-7"}).v({id:"bm-i"}).m().C({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-n"),s=document.querySelector("#bm-7"),o=document.querySelector("#bm-i"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-d"),c=document.querySelector("#bm-e"),l=document.querySelector("#bm-f"),h=document.querySelector("#bm-9"),u=document.querySelectorAll("#bm-8 input");t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-n h1","#bm-4","#bm-n 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&&(l.style.display="none"),h&&(h.style.display="none"),u.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",s.style.textAlign="center",s.style.margin="0",s.style.marginBottom="0",o&&(o.style.display="",o.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&&(l.style.display="",l.style.marginTop=""),h&&(h.style.display="",h.style.marginTop=""),u.forEach(t=>{t.style.display=""}),n.style.marginLeft="",i.style.padding="10px",s.style.textAlign="",s.style.margin="",s.style.marginBottom="",o&&(o.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)"})}).m().D(1,{textContent:d}).m().m().T().m().v({id:"bm-4"}).M({id:"bm-h",textContent:"Username:"}).m().M({id:"bm-c",textContent:"Droplets:"}).m().M({id:"bm-6",textContent:"Next level in..."}).m().m().T().m().v({id:"bm-3"}).v({id:"bm-8"}).N({id:"bm-d",className:"bm-q",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.t?.Lt;e?.[0]?(t.B("bm-j",e?.[0]||""),t.B("bm-k",e?.[1]||""),t.B("bm-l",e?.[2]||""),t.B("bm-m",e?.[3]||"")):t.q("Coordinates are malformed! Did you try clicking on the canvas first?")}}).m().O({type:"number",id:"bm-j",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-k",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-l",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-m",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).m().m().L({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).m().v({id:"bm-0"}).N({id:"bm-f",textContent:"Enable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!0),t.A("Enabled templates!")}}).m().N({id:"bm-e",textContent:"Create"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-j");if(!n.checkValidity())return n.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-k");if(!i.checkValidity())return i.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-l");if(!s.checkValidity())return s.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-m");if(!o.checkValidity())return o.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(v.Mt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(s.value),Number(o.value)]),t.A("Drew to canvas!")):t.q("No file selected!")}}).m().N({id:"bm-9",textContent:"Disable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!1),t.A("Disabled templates!")}}).m().m().H({id:w.i,placeholder:`Status: Sleeping...\nVersion: ${p}`,readOnly:!0}).m().v({id:"bm-1"}).v().N({id:"bm-a",className:"bm-q",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).m().m().$({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).m().m().m().p(document.body)}(),w.j("#bm-n","#bm-i"),y.Ht(w),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-g");if(!i){i=document.createElement("button"),i.id="bm-g",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${d}%c (${p}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file +(()=>{var t,e,n=t=>{throw TypeError(t)},o=(t,e,o)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,o),i=(t,e,o)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),o),s=class{constructor(e,n){o(this,t),this.name=e,this.version=n,this.t=null,this.o="bm-b",this.i=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.i),this.i=null,this.l=null,this.m=[]}$(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"div",{},n)),this}v(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"p",{},n)),this}M(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"small",{},n)),this}D(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"img",{},n)),this}S(n,o={},s=()=>{}){return s(this,i(this,t,e).call(this,"h"+n,{},o)),this}k(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"hr",{},n)),this}C(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"br",{},n)),this}T(n={},o=()=>{}){const s=i(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=i(this,t,e).call(this,"input",{type:"checkbox"},n);return s.insertBefore(a,s.firstChild),this.u(),o(this,s,a),this}N(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"button",{},n)),this}O(n={},o=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const a={textContent:"?",className:"bm-q",onclick:()=>{this.B(this.o,s)}};return o(this,i(this,t,e).call(this,"button",a,n)),this}I(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"input",{},n)),this}L(n={},o=()=>{}){const s=n.textContent??"";delete n.textContent;const a=i(this,t,e).call(this,"div"),r=i(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 c=i(this,t,e).call(this,"button",{textContent:s});return this.u(),this.u(),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}),o(this,a,r,c),this}G(n={},o=()=>{}){return o(this,i(this,t,e).call(this,"textarea",{},n)),this}B(t,e,n=!1){const o=document.getElementById(t.replace(/^#/,""));o&&(o instanceof HTMLInputElement?o.value=e:n?o.textContent=e:o.innerHTML=e)}P(t,e){let n,o=!1,i=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.F(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`);const m=()=>{if(o){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(m)}};let h=null;const u=(u,b)=>{o=!0,h=t.getBoundingClientRect(),n=u-h.left,i=b-h.top;const d=window.getComputedStyle(t).transform;if(d&&"none"!==d){const t=new DOMMatrix(d);a=t.m41,r=t.m42}else a=h.left,r=h.top;c=a,l=r,document.body.style.userSelect="none",e.classList.add("dragging"),s&&cancelAnimationFrame(s),m()},b=()=>{o=!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){o&&h&&(c=t.clientX-n,l=t.clientY-i)},{passive:!0}),document.addEventListener("touchmove",function(t){if(o&&h){const e=t?.touches?.[0];if(!e)return;c=e.clientX-n,l=e.clientY-i,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",b),document.addEventListener("touchend",b),document.addEventListener("touchcancel",b)}j(t){(0,console.info)(`${this.name}: ${t}`),this.B(this.o,"Status: "+t,!0)}F(t){(0,console.error)(`${this.name}: ${t}`),this.B(this.o,"Error: "+t,!0)}};function a(t,e){if(0===t)return e[0];let n="";const o=e.length;for(;t>0;)n=e[t%o]+n,t=Math.floor(t/o);return n}function r(t){let e="";for(let n=0;n=32)}}catch(t){}var m,h,u,b=class{constructor({displayName:t="My template",q:e=0,A:n="",url:o="",file:i=null,coords:s=null,J:a=null,H:r=1e3}={}){this.displayName=t,this.q=e,this.A=n,this.url=o,this.file=i,this.coords=s,this.J=a,this.H=r,this.Y=0,this._=0,this.X=0,this.V={},this.W=new Set,this.U=null;const c=Array.isArray(l)?l:[];this.Z=new Set(c.filter(t=>"transparent"!==(t?.name||"").toLowerCase()&&Array.isArray(t?.rgb)).map(t=>`${t.rgb[0]},${t.rgb[1]},${t.rgb[2]}`)),this.K=new Map(c.filter(t=>Array.isArray(t?.rgb)).map(t=>[`${t.rgb[0]},${t.rgb[1]},${t.rgb[2]}`,{id:t.id,R:!!t.R,name:t.name}]))}async tt(){console.log("Template coordinates:",this.coords);const t=await createImageBitmap(this.file),e=t.width,n=t.height,o=e*n;console.log(`Template pixel analysis - Dimensions: ${e}Γ—${n} = ${o.toLocaleString()} pixels`),this.Y=o;try{const o=new OffscreenCanvas(e,n).getContext("2d",{et:!0});o.imageSmoothingEnabled=!1,o.clearRect(0,0,e,n),o.drawImage(t,0,0);const i=o.getImageData(0,0,e,n).data;let s=0,a=0;const r=new Map;for(let t=0;t0){for(const t in e){const n=t,o=e[t];if(console.log(n),e.hasOwnProperty(t)){const t=n.split(" "),i=Number(t?.[0]),s=t?.[1]||"0",a=o.name||`Template ${i||""}`,r=o.tiles,l={};let m=0;const h=new Map;for(const t in r)if(console.log(t),r.hasOwnProperty(t)){const e=c(r[t]),n=new Blob([e],{type:"image/png"}),o=await createImageBitmap(n);l[t]=o;try{const t=o.width,e=o.height,n=new OffscreenCanvas(t,e).getContext("2d",{et:!0});n.imageSmoothingEnabled=!1,n.clearRect(0,0,t,e),n.drawImage(o,0,0);const i=n.getImageData(0,0,t,e).data;for(let n=0;n{u.W?.add(t.split(",").slice(0,2).join(","))})}catch(t){}try{const t=e?.[n]?.palette;if(t)for(const[e,n]of Object.entries(t))u.V[e]?u.V[e].enabled=!!n?.enabled:u.V[e]={count:n?.count||0,enabled:!!n?.enabled}}catch(t){}u.U=n,this.rt.push(u),console.log(this.rt),console.log("^^^ This ^^^")}}try{const t=document.querySelector("#bm-y");t&&(t.style.display=""),window.postMessage({source:"blue-marble",ct:"bm-A"},"*")}catch(t){}}};var d=GM_info.script.name.toString(),p=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-r",d),e.setAttribute("bm-o","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement?.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-r")||"Blue Marble",n=t?.getAttribute("bm-o")||"",o=new Map;window.addEventListener("message",t=>{const{source:i,endpoint:s,blobID:a,blobData:r,blink:c}=t.data,l=Date.now()-c;if(console.groupCollapsed(`%c${e}%c: ${o.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(o),console.groupEnd(),"blue-marble"==i&&a&&r&&!s){const t=o.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),o.delete(a)}});const i=window.fetch;window.fetch=async function(...t){const s=await i.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")&&!r.includes("maps")){const t=Date.now(),i=await a.blob();return console.log(`%c${e}%c: ${o.size} Sending IMAGE message about endpoint "${r}"`,n,""),new Promise(s=>{const c=crypto.randomUUID();o.set(c,t=>{s(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),console.log(`%c${e}%c: ${o.size} Processed blob "${c}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:c,blobData:i,blink:t})}).catch(i=>{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 ${o.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:",i),console.groupEnd()})}return s}});var g=GM_getResourceText("CSS-BM-File");GM_addStyle(g);var f=document.createElement("link");f.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",f.rel="preload",f.as="style",f.onload=function(){this.onload=null,this.rel="stylesheet"},document.head?.appendChild(f),new class{constructor(){this.lt=null,this.ht=null,this.ut="#bm-5"}bt(t){return this.ht=t,this.lt=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.ut)}),this}dt(){return this.lt}observe(t,e=!1,n=!1){t.observe(this.ht,{childList:e,subtree:n})}};var w=new s(d,p),$=(new s(d,p),new class{constructor(t,e,n){o(this,m),this.name=t,this.version=e,this.i=n,this.gt="1.0.0",this.ft=null,this.wt="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.H=1e3,this.st=3,this.$t=null,this.yt=null,this.xt="bm-p",this.vt="div#map canvas.maplibregl-canvas",this.Mt=null,this.Dt="",this.rt=[],this.it=null,this.St=!0,this.kt=new Map}Ct(){if(document.body.contains(this.$t))return this.$t;document.getElementById(this.xt)?.remove();const t=document.querySelector(this.vt),e=document.createElement("canvas");return e.id=this.xt,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.$t=e,window.addEventListener("move",this.Tt),window.addEventListener("zoom",this.Nt),window.addEventListener("resize",this.Ot),this.$t}async Bt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.gt,templates:{}}}async It(t,e,n){this.it||(this.it=await this.Bt(),console.log("Creating JSON...")),this.i.j(`Creating template at ${n.join(", ")}...`);const o=new b({displayName:e,q:0,A:a(this.ft||0,this.wt),file:t,coords:n}),{nt:s,ot:r}=await o.tt(this.H);o.J=s;const c=`${o.q} ${o.A}`;o.U=c,this.it.templates[c]={name:o.displayName,coords:n.join(", "),enabled:!0,tiles:r,palette:o.V},this.rt=[],this.rt.push(o);const l=(new Intl.NumberFormat).format(o.Y);this.i.j(`Template created at ${n.join(", ")}! Total pixels: ${l}`);try{const t=document.querySelector("#bm-y");t&&(t.style.display=""),window.postMessage({source:"blue-marble",ct:"bm-A"},"*")}catch(t){}console.log(Object.keys(this.it.templates).length),console.log(this.it),console.log(this.rt),console.log(JSON.stringify(this.it)),await i(this,m,h).call(this)}Lt(){}async Gt(){this.it||(this.it=await this.Bt(),console.log("Creating JSON..."))}async Pt(t,e){if(!this.St)return t;const n=this.H*this.st;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0"),console.log(`Searching for templates in tile: "${e}"`);const o=this.rt;if(console.log(o),o.sort((t,e)=>t.q-e.q),console.log(o),!o.some(t=>!!t?.J&&(t.W&&t.W.size>0?t.W.has(e):Object.keys(t.J).some(t=>t.startsWith(e)))))return t;const i=o.map(t=>{const n=Object.keys(t.J).filter(t=>t.startsWith(e));if(0===n.length)return null;const o=n.map(e=>{const n=e.split(",");return{Ft:t.J[e],jt:[n[0],n[1]],Et:[n[2],n[3]]}});return o?.[0]}).filter(Boolean);console.log(i);const s=i?.length||0;console.log(`templateCount = ${s}`);let a=0,r=0,c=0;const l=await createImageBitmap(t),m=new OffscreenCanvas(n,n),h=m.getContext("2d");h.imageSmoothingEnabled=!1,h.beginPath(),h.rect(0,0,n,n),h.clip(),h.clearRect(0,0,n,n),h.drawImage(l,0,0,n,n);let u=null;try{u=h.getImageData(0,0,n,n).data}catch(t){}for(const t of i){if(console.log("Template:"),console.log(t),u)try{const e=t.Ft.width,o=t.Ft.height,i=new OffscreenCanvas(e,o).getContext("2d",{et:!0});i.imageSmoothingEnabled=!1,i.clearRect(0,0,e,o),i.drawImage(t.Ft,0,0);const s=i.getImageData(0,0,e,o).data,l=Number(t.Et[0])*this.st,m=Number(t.Et[1])*this.st;for(let t=0;t=n||h>=n)continue;const b=4*(t*e+o),d=s[b],p=s[b+1],g=s[b+2];if(s[b+3]<64)continue;if(222===d&&250===p&&206===g)continue;try{const t=this.rt?.[0];if(t?.Z&&!t.Z.has(`${d},${p},${g}`))continue}catch(t){}c++;const f=4*(h*n+i),w=u[f],$=u[f+1],y=u[f+2];u[f+3]<64||(w===d&&$===p&&y===g?a++:r++)}}catch(t){console.warn("Failed to compute per-tile painted/wrong stats:",t)}try{const e=this.rt?.[0],n=e?.V||{};if(Object.values(n).some(t=>!1===t?.enabled)){const o=t.Ft.width,i=t.Ft.height,s=new OffscreenCanvas(o,i),a=s.getContext("2d",{et:!0});a.imageSmoothingEnabled=!1,a.clearRect(0,0,o,i),a.drawImage(t.Ft,0,0);const r=a.getImageData(0,0,o,i),c=r.data;for(let t=0;t0){const t=e;this.kt.set(t,{Rt:a,required:c,qt:r});let n=0,o=0,i=0;for(const t of this.kt.values())n+=t.Rt||0,o+=t.required||0,i+=t.qt||0;const l=this.rt.reduce((t,e)=>t+(e._||e.Y||0),0),m=l>0?l:o,h=(new Intl.NumberFormat).format(n),u=(new Intl.NumberFormat).format(m),b=(new Intl.NumberFormat).format(i);this.i.j(`Displaying ${s} template${1==s?"":"s"}.\nPainted ${h} / ${u} β€’ Wrong ${b}`)}else this.i.j(`Displaying ${s} templates.`);return await m.convertToBlob({type:"image/png"})}At(t){console.log("Importing JSON..."),console.log(t),"BlueMarble"==t?.whoami&&i(this,m,u).call(this,t)}Jt(t){this.St=t}}(d,p,w)),y=new class{constructor(t){this.zt=t,this.Ht=!1,this.Yt=[],this._t=[]}Xt(t){window.addEventListener("message",async e=>{const n=e.data,o=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const i=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;","",i),i){case"me":if(o.status&&"2"!=o.status?.toString()[0])return void t.F("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(o.level)*Math.pow(30,.65),1/.65)-o.pixelsPainted);console.log(o.id),(o.id||0===o.id)&&console.log(a(o.id,"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~")),this.zt.ft=o.id,t.B("bm-h",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(o.name)}`),t.B("bm-c",`Droplets: ${(new Intl.NumberFormat).format(o.droplets)}`),t.B("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const i=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.Yt.length&&(!i.length||!l.length))return void t.F("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Yt=[...i,...l];const m=(s=i,r=l,[parseInt(s[0])%4*1e3+parseInt(r[0]),parseInt(s[1])%4*1e3+parseInt(r[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${m[0]}, ${m[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${i[0]}, Tl Y: ${i[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 b=n.blobID,d=n.blobData,p=await this.zt.Pt(d,u);window.postMessage({source:"blue-marble",blobID:b,blobData:p,blink:n.blink});break;case"robots":this.Ht="false"==o.userscript?.toString().toLowerCase();break}var s,r})}}($);w.h(y);var x=JSON.parse(GM_getValue("bmTemplates","{}"));console.log(x),$.At(x),function(){let t=!1,e={};try{e=JSON.parse(GM_getValue("bmCoords","{}"))||{}}catch(t){e={}}const n=()=>{try{const t=Number(document.querySelector("#bm-j")?.value||""),e=Number(document.querySelector("#bm-k")?.value||""),n={Vt:t,Wt:e,px:Number(document.querySelector("#bm-l")?.value||""),Ut:Number(document.querySelector("#bm-m")?.value||"")};GM.setValue("bmCoords",JSON.stringify(n))}catch(t){}};w.$({id:"bm-n",style:"top: 10px; right: 75px;"}).$({id:"bm-7"}).$({id:"bm-i"}).u().D({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 o=document.querySelector("#bm-n"),i=document.querySelector("#bm-7"),s=document.querySelector("#bm-i"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-d"),c=document.querySelector("#bm-e"),l=document.querySelector("#bm-f"),m=document.querySelector("#bm-9"),h=document.querySelectorAll("#bm-8 input");t||(o.style.width="auto",o.style.maxWidth="300px",o.style.minWidth="200px",o.style.padding="10px"),["#bm-n h1","#bm-4","#bm-n hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.o}`,"#bm-y"].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&&(l.style.display="none"),m&&(m.style.display="none"),h.forEach(t=>{t.style.display="none"}),o.style.width="60px",o.style.height="76px",o.style.maxWidth="60px",o.style.minWidth="60px",o.style.padding="8px",n.style.marginLeft="3px",i.style.textAlign="center",i.style.margin="0",i.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&&(l.style.display="",l.style.marginTop=""),m&&(m.style.display="",m.style.marginTop=""),h.forEach(t=>{t.style.display=""}),n.style.marginLeft="",o.style.padding="10px",i.style.textAlign="",i.style.margin="",i.style.marginBottom="",s&&(s.style.marginBottom="0.5em"),o.style.width="",o.style.height=""),n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)"})}).u().S(1,{textContent:d}).u().u().k().u().$({id:"bm-4"}).v({id:"bm-h",textContent:"Username:"}).u().v({id:"bm-c",textContent:"Droplets:"}).u().v({id:"bm-6",textContent:"Next level in..."}).u().u().k().u().$({id:"bm-3"}).$({id:"bm-8"}).N({id:"bm-d",className:"bm-q",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.t?.Yt;e?.[0]?(t.B("bm-j",e?.[0]||""),t.B("bm-k",e?.[1]||""),t.B("bm-l",e?.[2]||""),t.B("bm-m",e?.[3]||""),n()):t.F("Coordinates are malformed! Did you try clicking on the canvas first?")}}).u().I({type:"number",id:"bm-j",placeholder:"Tl X",min:0,max:2047,step:1,required:!0,value:e.Vt??""},(t,e)=>{const o=()=>n();e.addEventListener("input",o),e.addEventListener("change",o)}).u().I({type:"number",id:"bm-k",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0,value:e.Wt??""},(t,e)=>{const o=()=>n();e.addEventListener("input",o),e.addEventListener("change",o)}).u().I({type:"number",id:"bm-l",placeholder:"Px X",min:0,max:2047,step:1,required:!0,value:e.px??""},(t,e)=>{const o=()=>n();e.addEventListener("input",o),e.addEventListener("change",o)}).u().I({type:"number",id:"bm-m",placeholder:"Px Y",min:0,max:2047,step:1,required:!0,value:e.Ut??""},(t,e)=>{const o=()=>n();e.addEventListener("input",o),e.addEventListener("change",o)}).u().u().$({id:"bm-y",style:"max-height: 140px; overflow: auto; border: 1px solid rgba(255,255,255,0.1); padding: 4px; border-radius: 4px; display: none;"}).$({style:"display: flex; gap: 6px; margin-bottom: 6px;"}).N({id:"bm-v",textContent:"Enable All"},(t,e)=>{e.onclick=()=>{const e=$.rt[0];e?.V&&(Object.values(e.V).forEach(t=>t.enabled=!0),buildColorFilterList(),t.j("Enabled all colors"))}}).u().N({id:"bm-u",textContent:"Disable All"},(t,e)=>{e.onclick=()=>{const e=$.rt[0];e?.V&&(Object.values(e.V).forEach(t=>t.enabled=!1),buildColorFilterList(),t.j("Disabled all colors"))}}).u().u().$({id:"bm-D"}).u().u().L({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).u().$({id:"bm-0"}).N({id:"bm-f",textContent:"Enable"},(t,e)=>{e.onclick=()=>{t.t?.zt?.Jt(!0),t.j("Enabled templates!")}}).u().N({id:"bm-e",textContent:"Create"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-j");if(!n.checkValidity())return n.reportValidity(),void t.F("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.F("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-l");if(!i.checkValidity())return i.reportValidity(),void t.F("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-m");if(!s.checkValidity())return s.reportValidity(),void t.F("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?($.It(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(o.value),Number(i.value),Number(s.value)]),t.j("Drew to canvas!")):t.F("No file selected!")}}).u().N({id:"bm-9",textContent:"Disable"},(t,e)=>{e.onclick=()=>{t.t?.zt?.Jt(!1),t.j("Disabled templates!")}}).u().u().G({id:w.o,placeholder:`Status: Sleeping...\nVersion: ${p}`,readOnly:!0}).u().$({id:"bm-1"}).$().N({id:"bm-a",className:"bm-q",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).u().u().M({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).u().u().u().p(document.body),window.buildColorFilterList=function(){const t=document.querySelector("#bm-D"),e=$.rt?.[0];if(!t||!e?.V)return void(t&&(t.innerHTML="No template colors to display."));t.innerHTML="";const n=Object.entries(e.V).sort((t,e)=>e[1].count-t[1].count);for(const[e,o]of n){const[n,i,s]=e.split(",").map(Number),a=document.createElement("div");a.style.display="flex",a.style.alignItems="center",a.style.gap="8px",a.style.margin="4px 0";const r=document.createElement("div");r.style.width="14px",r.style.height="14px",r.style.border="1px solid rgba(255,255,255,0.5)",r.style.background=`rgb(${n},${i},${s})`;const c=document.createElement("span");c.style.fontSize="12px";let l=`${o.count.toLocaleString()}`;try{const t=$.rt?.[0]?.K?.get(e);if(t&&"number"==typeof t.id){const e=t?.name||`rgb(${n},${i},${s})`,o=t.R?"β˜… ":"";l=`#${t.id} ${o}${e} β€’ ${l}`}}catch(t){}c.textContent=l;const m=document.createElement("input");m.type="checkbox",m.checked=!!o.enabled,m.addEventListener("change",()=>{o.enabled=m.checked,w.j(`${m.checked?"Enabled":"Disabled"} ${e}`);try{const t=$.rt?.[0],e=t?.U;t&&e&&$.it?.templates?.[e]&&($.it.templates[e].palette=t.V,GM.setValue("bmTemplates",JSON.stringify($.it)))}catch(t){}}),a.appendChild(m),a.appendChild(r),a.appendChild(c),t.appendChild(a)}},window.addEventListener("message",t=>{if("bm-A"===t?.data?.ct)try{buildColorFilterList()}catch(t){}}),setTimeout(()=>{try{if($.rt?.length>0){const t=document.querySelector("#bm-y");t&&(t.style.display=""),buildColorFilterList()}}catch(t){}},0)}(),w.P("#bm-n","#bm-i"),y.Xt(w),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let o=document.querySelector("#bm-g");if(!o){o=document.createElement("button"),o.id="bm-g",o.textContent="Move ↑",o.className="btn btn-soft",o.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(o)}}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${d}%c (${p}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fbb34ba..353ccca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wplace-bluemarble", - "version": "0.78.0", + "version": "0.81.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wplace-bluemarble", - "version": "0.78.0", + "version": "0.81.0", "devDependencies": { "esbuild": "^0.25.0", "jsdoc": "^4.0.4", diff --git a/src/Template.js b/src/Template.js index 0832c1b..f858f9e 100644 --- a/src/Template.js +++ b/src/Template.js @@ -1,4 +1,4 @@ -import { uint8ToBase64 } from "./utils"; +import { uint8ToBase64, colorpalette } from "./utils"; /** An instance of a template. * Handles all mathematics, manipulation, and analysis regarding a single template. @@ -39,6 +39,25 @@ export default class Template { this.chunked = chunked; this.tileSize = tileSize; this.pixelCount = 0; // Total pixel count in template + this.requiredPixelCount = 0; // Total number of non-transparent, non-#deface pixels + this.defacePixelCount = 0; // Number of #deface pixels (represents Transparent color in-game) + this.colorPalette = {}; // key: "r,g,b" -> { count: number, enabled: boolean } + this.tilePrefixes = new Set(); // Set of "xxxx,yyyy" tiles this template touches + this.storageKey = null; // Key used inside templatesJSON to persist settings + + // Build allowed color set from site palette (exclude special Transparent entry by name) + const allowed = Array.isArray(colorpalette) ? colorpalette : []; + this.allowedColorsSet = new Set( + allowed + .filter(c => (c?.name || '').toLowerCase() !== 'transparent' && Array.isArray(c?.rgb)) + .map(c => `${c.rgb[0]},${c.rgb[1]},${c.rgb[2]}`) + ); + // Map rgb-> {id, premium} + this.rgbToMeta = new Map( + allowed + .filter(c => Array.isArray(c?.rgb)) + .map(c => [ `${c.rgb[0]},${c.rgb[1]},${c.rgb[2]}`, { id: c.id, premium: !!c.premium, name: c.name } ]) + ); } /** Creates chunks of the template for each tile. @@ -62,6 +81,54 @@ export default class Template { // Store pixel count in instance property for access by template manager and UI components this.pixelCount = totalPixels; + // ==================== REQUIRED/DEFACE PIXEL COUNTING ==================== + // Build a 1Γ— scale canvas to inspect original pixels and count required vs deface + try { + const inspectCanvas = new OffscreenCanvas(imageWidth, imageHeight); + const inspectCtx = inspectCanvas.getContext('2d', { willReadFrequently: true }); + inspectCtx.imageSmoothingEnabled = false; + inspectCtx.clearRect(0, 0, imageWidth, imageHeight); + inspectCtx.drawImage(bitmap, 0, 0); + const inspectData = inspectCtx.getImageData(0, 0, imageWidth, imageHeight).data; + + let required = 0; + let deface = 0; + const paletteMap = new Map(); + for (let y = 0; y < imageHeight; y++) { + for (let x = 0; x < imageWidth; x++) { + const idx = (y * imageWidth + x) * 4; + const r = inspectData[idx]; + const g = inspectData[idx + 1]; + const b = inspectData[idx + 2]; + const a = inspectData[idx + 3]; + if (a === 0) { continue; } // Ignored transparent pixel + if (r === 222 && g === 250 && b === 206) { // #deface + deface++; + continue; // Do not include in required count so progress reflects paintable pixels + } + const key = `${r},${g},${b}`; + if (!this.allowedColorsSet.has(key)) { continue; } // Skip non-palette colors + required++; + paletteMap.set(key, (paletteMap.get(key) || 0) + 1); + } + } + + this.requiredPixelCount = required; + this.defacePixelCount = deface; + + // Persist palette with all colors enabled by default + const paletteObj = {}; + for (const [key, count] of paletteMap.entries()) { + paletteObj[key] = { count, enabled: true }; + } + this.colorPalette = paletteObj; + } catch (err) { + // Fail-safe: if OffscreenCanvas not available or any error, fall back to widthΓ—height + this.requiredPixelCount = Math.max(0, this.pixelCount); + this.defacePixelCount = 0; + console.warn('Failed to compute required/deface counts. Falling back to total pixels.', err); + } + const templateTiles = {}; // Holds the template tiles const templateTilesBuffers = {}; // Holds the buffers of the template tiles @@ -146,6 +213,14 @@ export default class Template { } } 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 + } else { + // Center pixel: keep only if in allowed site palette + const r = imageData.data[pixelIndex]; + const g = imageData.data[pixelIndex + 1]; + const b = imageData.data[pixelIndex + 2]; + if (!this.allowedColorsSet.has(`${r},${g},${b}`)) { + imageData.data[pixelIndex + 3] = 0; // hide non-palette colors + } } } } @@ -164,6 +239,8 @@ export default class Template { .padStart(3, '0')},${(pixelY % 1000).toString().padStart(3, '0')}`; templateTiles[templateTileName] = await createImageBitmap(canvas); // Creates the bitmap + // Record tile prefix for fast lookup later + this.tilePrefixes.add(templateTileName.split(',').slice(0,2).join(',')); const canvasBlob = await canvas.convertToBlob(); const canvasBuffer = await canvasBlob.arrayBuffer(); diff --git a/src/main.js b/src/main.js index e09c2ce..007b0ea 100644 --- a/src/main.js +++ b/src/main.js @@ -242,6 +242,19 @@ function observeBlack() { */ function buildOverlayMain() { let isMinimized = false; // Overlay state tracker (false = maximized, true = minimized) + // Load last saved coordinates (if any) + let savedCoords = {}; + try { savedCoords = JSON.parse(GM_getValue('bmCoords', '{}')) || {}; } catch (_) { savedCoords = {}; } + const persistCoords = () => { + try { + const tx = Number(document.querySelector('#bm-input-tx')?.value || ''); + const ty = Number(document.querySelector('#bm-input-ty')?.value || ''); + const px = Number(document.querySelector('#bm-input-px')?.value || ''); + const py = Number(document.querySelector('#bm-input-py')?.value || ''); + const data = { tx, ty, px, py }; + GM.setValue('bmCoords', JSON.stringify(data)); + } catch (_) {} + }; overlayMain.addDiv({'id': 'bm-overlay', 'style': 'top: 10px; right: 75px;'}) .addDiv({'id': 'bm-contain-header'}) @@ -296,7 +309,8 @@ function buildOverlayMain() { '#bm-contain-automation > *:not(#bm-contain-coords)', // Automation section excluding coordinates '#bm-input-file-template', // Template file upload interface '#bm-contain-buttons-action', // Action buttons container - `#${instance.outputStatusId}` // Status log textarea for user feedback + `#${instance.outputStatusId}`, // Status log textarea for user feedback + '#bm-contain-colorfilter' // Color filter UI ]; // Apply visibility changes to all toggleable elements @@ -475,13 +489,54 @@ function buildOverlayMain() { instance.updateInnerHTML('bm-input-ty', coords?.[1] || ''); instance.updateInnerHTML('bm-input-px', coords?.[2] || ''); instance.updateInnerHTML('bm-input-py', coords?.[3] || ''); + persistCoords(); } } ).buildElement() - .addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement() - .addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement() - .addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement() - .addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement() + .addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.tx ?? '')}, (instance, input) => { + const handler = () => persistCoords(); + input.addEventListener('input', handler); + input.addEventListener('change', handler); + }).buildElement() + .addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.ty ?? '')}, (instance, input) => { + const handler = () => persistCoords(); + input.addEventListener('input', handler); + input.addEventListener('change', handler); + }).buildElement() + .addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.px ?? '')}, (instance, input) => { + const handler = () => persistCoords(); + input.addEventListener('input', handler); + input.addEventListener('change', handler); + }).buildElement() + .addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.py ?? '')}, (instance, input) => { + const handler = () => persistCoords(); + input.addEventListener('input', handler); + input.addEventListener('change', handler); + }).buildElement() + .buildElement() + // Color filter UI + .addDiv({'id': 'bm-contain-colorfilter', 'style': 'max-height: 140px; overflow: auto; border: 1px solid rgba(255,255,255,0.1); padding: 4px; border-radius: 4px; display: none;'}) + .addDiv({'style': 'display: flex; gap: 6px; margin-bottom: 6px;'}) + .addButton({'id': 'bm-button-colors-enable-all', 'textContent': 'Enable All'}, (instance, button) => { + button.onclick = () => { + const t = templateManager.templatesArray[0]; + if (!t?.colorPalette) { return; } + Object.values(t.colorPalette).forEach(v => v.enabled = true); + buildColorFilterList(); + instance.handleDisplayStatus('Enabled all colors'); + }; + }).buildElement() + .addButton({'id': 'bm-button-colors-disable-all', 'textContent': 'Disable All'}, (instance, button) => { + button.onclick = () => { + const t = templateManager.templatesArray[0]; + if (!t?.colorPalette) { return; } + Object.values(t.colorPalette).forEach(v => v.enabled = false); + buildColorFilterList(); + instance.handleDisplayStatus('Disabled all colors'); + }; + }).buildElement() + .buildElement() + .addDiv({'id': 'bm-colorfilter-list'}).buildElement() .buildElement() .addInputFile({'id': 'bm-input-file-template', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement() .addDiv({'id': 'bm-contain-buttons-template'}) @@ -541,6 +596,89 @@ function buildOverlayMain() { .buildElement() .buildElement() .buildOverlay(document.body); + + // ------- Helper: Build the color filter list ------- + window.buildColorFilterList = function buildColorFilterList() { + const listContainer = document.querySelector('#bm-colorfilter-list'); + const t = templateManager.templatesArray?.[0]; + if (!listContainer || !t?.colorPalette) { + if (listContainer) { listContainer.innerHTML = 'No template colors to display.'; } + return; + } + + listContainer.innerHTML = ''; + const entries = Object.entries(t.colorPalette) + .sort((a,b) => b[1].count - a[1].count); // sort by frequency desc + + for (const [rgb, meta] of entries) { + const [r,g,b] = rgb.split(',').map(Number); + + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + row.style.margin = '4px 0'; + + const swatch = document.createElement('div'); + swatch.style.width = '14px'; + swatch.style.height = '14px'; + swatch.style.border = '1px solid rgba(255,255,255,0.5)'; + swatch.style.background = `rgb(${r},${g},${b})`; + + const label = document.createElement('span'); + label.style.fontSize = '12px'; + let labelText = `${meta.count.toLocaleString()}`; + try { + const tMeta = templateManager.templatesArray?.[0]?.rgbToMeta?.get(rgb); + if (tMeta && typeof tMeta.id === 'number') { + const displayName = tMeta?.name || `rgb(${r},${g},${b})`; + const starLeft = tMeta.premium ? 'β˜… ' : ''; + labelText = `#${tMeta.id} ${starLeft}${displayName} β€’ ${labelText}`; + } + } catch (_) {} + label.textContent = labelText; + + const toggle = document.createElement('input'); + toggle.type = 'checkbox'; + toggle.checked = !!meta.enabled; + toggle.addEventListener('change', () => { + meta.enabled = toggle.checked; + overlayMain.handleDisplayStatus(`${toggle.checked ? 'Enabled' : 'Disabled'} ${rgb}`); + try { + const t = templateManager.templatesArray?.[0]; + const key = t?.storageKey; + if (t && key && templateManager.templatesJSON?.templates?.[key]) { + templateManager.templatesJSON.templates[key].palette = t.colorPalette; + // persist immediately + GM.setValue('bmTemplates', JSON.stringify(templateManager.templatesJSON)); + } + } catch (_) {} + }); + + row.appendChild(toggle); + row.appendChild(swatch); + row.appendChild(label); + listContainer.appendChild(row); + } + }; + + // Listen for template creation/import completion to (re)build palette list + window.addEventListener('message', (event) => { + if (event?.data?.bmEvent === 'bm-rebuild-color-list') { + try { buildColorFilterList(); } catch (_) {} + } + }); + + // If a template was already loaded from storage, show the color UI and build list + setTimeout(() => { + try { + if (templateManager.templatesArray?.length > 0) { + const colorUI = document.querySelector('#bm-contain-colorfilter'); + if (colorUI) { colorUI.style.display = ''; } + buildColorFilterList(); + } + } catch (_) {} + }, 0); } function buildOverlayTabTemplate() { diff --git a/src/templateManager.js b/src/templateManager.js index 703738a..299daf2 100644 --- a/src/templateManager.js +++ b/src/templateManager.js @@ -61,6 +61,7 @@ export default class TemplateManager { this.templatesArray = []; // All Template instnaces currently loaded (Template) this.templatesJSON = null; // All templates currently loaded (JSON) this.templatesShouldBeDrawn = true; // Should ALL templates be drawn to the canvas? + this.tileProgress = new Map(); // Tracks per-tile progress stats {painted, required, wrong} } /** Retrieves the pixel art canvas. @@ -143,11 +144,14 @@ export default class TemplateManager { // Appends a child into the templates object // The child's name is the number of templates already in the list (sort order) plus the encoded player ID - this.templatesJSON.templates[`${template.sortID} ${template.authorID}`] = { + const storageKey = `${template.sortID} ${template.authorID}`; + template.storageKey = storageKey; + this.templatesJSON.templates[storageKey] = { "name": template.displayName, // Display name of template "coords": coords.join(', '), // The coords of the template "enabled": true, - "tiles": templateTilesBuffers // Stores the chunked tile buffers + "tiles": templateTilesBuffers, // Stores the chunked tile buffers + "palette": template.colorPalette // Persist palette and enabled flags }; this.templatesArray = []; // Remove this to enable multiple templates (2/2) @@ -159,6 +163,14 @@ export default class TemplateManager { const pixelCountFormatted = new Intl.NumberFormat().format(template.pixelCount); this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}! Total pixels: ${pixelCountFormatted}`); + // Ensure color filter UI is visible when a template is created + try { + const colorUI = document.querySelector('#bm-contain-colorfilter'); + if (colorUI) { colorUI.style.display = ''; } + // Deferred palette list rendering; actual DOM is built in main via helper + window.postMessage({ source: 'blue-marble', bmEvent: 'bm-rebuild-color-list' }, '*'); + } catch (_) { /* no-op */ } + console.log(Object.keys(this.templatesJSON.templates).length); console.log(this.templatesJSON); console.log(this.templatesArray); @@ -223,6 +235,18 @@ export default class TemplateManager { console.log(templateArray); + // Early exit if none of the active templates touch this tile + const anyTouches = templateArray.some(t => { + if (!t?.chunked) { return false; } + // Fast path via recorded tile prefixes if available + if (t.tilePrefixes && t.tilePrefixes.size > 0) { + return t.tilePrefixes.has(tileCoords); + } + // Fallback: scan chunked keys + return Object.keys(t.chunked).some(k => k.startsWith(tileCoords)); + }); + if (!anyTouches) { return tileBlob; } + // Retrieves the relavent template tile blobs const templatesToDraw = templateArray .map(template => { @@ -253,31 +277,10 @@ export default class TemplateManager { const templateCount = templatesToDraw?.length || 0; // Number of templates to draw on this tile console.log(`templateCount = ${templateCount}`); - if (templateCount > 0) { - - // Calculate total pixel count for templates actively being displayed in this tile - const totalPixels = templateArray - .filter(template => { - // Filter templates to include only those with tiles matching current coordinates - // This ensures we count pixels only for templates actually being rendered - const matchingTiles = Object.keys(template.chunked).filter(tile => - tile.startsWith(tileCoords) - ); - return matchingTiles.length > 0; - }) - .reduce((sum, template) => sum + (template.pixelCount || 0), 0); - - // Format pixel count with locale-appropriate thousands separators for better readability - // Examples: "1,234,567" (US), "1.234.567" (DE), "1 234 567" (FR) - const pixelCountFormatted = new Intl.NumberFormat().format(totalPixels); - - // Display status information about the templates being rendered - this.overlay.handleDisplayStatus( - `Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nTotal pixels: ${pixelCountFormatted}` - ); - } else { - this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`); - } + // We'll compute per-tile painted/wrong/required counts when templates exist for this tile + let paintedCount = 0; + let wrongCount = 0; + let requiredCount = 0; const tileBitmap = await createImageBitmap(tileBlob); @@ -294,13 +297,161 @@ export default class TemplateManager { context.clearRect(0, 0, drawSize, drawSize); // Draws transparent background context.drawImage(tileBitmap, 0, 0, drawSize, drawSize); + // Grab a snapshot of the tile pixels BEFORE we draw any template overlays + let tilePixels = null; + try { + tilePixels = context.getImageData(0, 0, drawSize, drawSize).data; + } catch (_) { + // If reading fails for any reason, we will skip stats + } + // For each template in this tile, draw them. for (const template of templatesToDraw) { console.log(`Template:`); console.log(template); - // Draws the each template on the tile based on it's relative position - context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult); + // Compute stats by sampling template center pixels against tile pixels, + // honoring color enable/disable from the active template's palette + if (tilePixels) { + try { + const tempW = template.bitmap.width; + const tempH = template.bitmap.height; + const tempCanvas = new OffscreenCanvas(tempW, tempH); + const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); + tempCtx.imageSmoothingEnabled = false; + tempCtx.clearRect(0, 0, tempW, tempH); + tempCtx.drawImage(template.bitmap, 0, 0); + const tImg = tempCtx.getImageData(0, 0, tempW, tempH); + const tData = tImg.data; + + const offsetX = Number(template.pixelCoords[0]) * this.drawMult; + const offsetY = Number(template.pixelCoords[1]) * this.drawMult; + + for (let y = 0; y < tempH; y++) { + for (let x = 0; x < tempW; x++) { + // Only evaluate the center pixel of each shread block + if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; } + const gx = x + offsetX; + const gy = y + offsetY; + if (gx < 0 || gy < 0 || gx >= drawSize || gy >= drawSize) { continue; } + const tIdx = (y * tempW + x) * 4; + const tr = tData[tIdx]; + const tg = tData[tIdx + 1]; + const tb = tData[tIdx + 2]; + const ta = tData[tIdx + 3]; + // Ignore transparent and semi-transparent (deface checkerboard uses alpha 32) + if (ta < 64) { continue; } + // Ignore #deface explicitly if it sneaks through with higher alpha + if (tr === 222 && tg === 250 && tb === 206) { continue; } + // Ignore non-palette colors (match against allowed set when available) + try { + const activeTemplate = this.templatesArray?.[0]; + if (activeTemplate?.allowedColorsSet && !activeTemplate.allowedColorsSet.has(`${tr},${tg},${tb}`)) { + continue; + } + } catch (_) {} + + requiredCount++; + + // Strict center-pixel matching. Treat transparent tile pixels as unpainted (not wrong) + const tileIdx = (gy * drawSize + gx) * 4; + const pr = tilePixels[tileIdx]; + const pg = tilePixels[tileIdx + 1]; + const pb = tilePixels[tileIdx + 2]; + const pa = tilePixels[tileIdx + 3]; + + if (pa < 64) { + // Unpainted -> neither painted nor wrong + } else if (pr === tr && pg === tg && pb === tb) { + paintedCount++; + } else { + wrongCount++; + } + } + } + } catch (e) { + console.warn('Failed to compute per-tile painted/wrong stats:', e); + } + } + + // Draw the template overlay for visual guidance, honoring color filter + try { + const activeTemplate = this.templatesArray?.[0]; + const palette = activeTemplate?.colorPalette || {}; + const hasDisabled = Object.values(palette).some(v => v?.enabled === false); + if (!hasDisabled) { + context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult); + } else { + const tempW = template.bitmap.width; + const tempH = template.bitmap.height; + const filterCanvas = new OffscreenCanvas(tempW, tempH); + const filterCtx = filterCanvas.getContext('2d', { willReadFrequently: true }); + filterCtx.imageSmoothingEnabled = false; + filterCtx.clearRect(0, 0, tempW, tempH); + filterCtx.drawImage(template.bitmap, 0, 0); + const img = filterCtx.getImageData(0, 0, tempW, tempH); + const data = img.data; + for (let y = 0; y < tempH; y++) { + for (let x = 0; x < tempW; x++) { + if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; } + const idx = (y * tempW + x) * 4; + const r = data[idx]; + const g = data[idx + 1]; + const b = data[idx + 2]; + const a = data[idx + 3]; + if (a < 1) { continue; } + const key = `${r},${g},${b}`; + // Hide if color is not in allowed palette or explicitly disabled + const inSitePalette = activeTemplate?.allowedColorsSet ? activeTemplate.allowedColorsSet.has(key) : true; + const enabled = palette?.[key]?.enabled !== false; + if (!inSitePalette || !enabled) { + data[idx + 3] = 0; // hide disabled color center pixel + } + } + } + filterCtx.putImageData(img, 0, 0); + context.drawImage(filterCanvas, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult); + } + } catch (_) { + // Fallback to drawing raw bitmap if filtering fails + context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult); + } + } + + // Save per-tile stats and compute global aggregates across all processed tiles + if (templateCount > 0) { + const tileKey = tileCoords; // already padded string "xxxx,yyyy" + this.tileProgress.set(tileKey, { + painted: paintedCount, + required: requiredCount, + wrong: wrongCount, + }); + + // Aggregate painted/wrong across tiles we've processed + let aggPainted = 0; + let aggRequiredTiles = 0; + let aggWrong = 0; + for (const stats of this.tileProgress.values()) { + aggPainted += stats.painted || 0; + aggRequiredTiles += stats.required || 0; + aggWrong += stats.wrong || 0; + } + + // Determine total required across all templates + // Prefer precomputed per-template required counts; fall back to sum of processed tiles + const totalRequiredTemplates = this.templatesArray.reduce((sum, t) => + sum + (t.requiredPixelCount || t.pixelCount || 0), 0); + const totalRequired = totalRequiredTemplates > 0 ? totalRequiredTemplates : aggRequiredTiles; + + const paintedStr = new Intl.NumberFormat().format(aggPainted); + const requiredStr = new Intl.NumberFormat().format(totalRequired); + const wrongStr = new Intl.NumberFormat().format(aggWrong); + + this.overlay.handleDisplayStatus( + `Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nPainted ${paintedStr} / ${requiredStr} β€’ Wrong ${wrongStr}` + ); + } else { + this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`); } return await canvas.convertToBlob({ type: 'image/png' }); @@ -349,6 +500,8 @@ export default class TemplateManager { //const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4] const tilesbase64 = templateValue.tiles; const templateTiles = {}; // Stores the template bitmap tiles for each tile. + let requiredPixelCount = 0; // Global required pixel count for this imported template + const paletteMap = new Map(); // Accumulates color counts across tiles (center pixels only) for (const tile in tilesbase64) { console.log(tile); @@ -359,6 +512,36 @@ export default class TemplateManager { const templateBlob = new Blob([templateUint8Array], { type: "image/png" }); // Uint8Array -> Blob const templateBitmap = await createImageBitmap(templateBlob) // Blob -> Bitmap templateTiles[tile] = templateBitmap; + + // Count required pixels in this bitmap (center pixels with alpha >= 64 and not #deface) + try { + const w = templateBitmap.width; + const h = templateBitmap.height; + const c = new OffscreenCanvas(w, h); + const cx = c.getContext('2d', { willReadFrequently: true }); + cx.imageSmoothingEnabled = false; + cx.clearRect(0, 0, w, h); + cx.drawImage(templateBitmap, 0, 0); + const data = cx.getImageData(0, 0, w, h).data; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + // Only count center pixels of 3x blocks + if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; } + const idx = (y * w + x) * 4; + const r = data[idx]; + const g = data[idx + 1]; + const b = data[idx + 2]; + const a = data[idx + 3]; + if (a < 64) { continue; } + if (r === 222 && g === 250 && b === 206) { continue; } + requiredPixelCount++; + const key = `${r},${g},${b}`; + paletteMap.set(key, (paletteMap.get(key) || 0) + 1); + } + } + } catch (e) { + console.warn('Failed to count required pixels for imported tile', e); + } } } @@ -370,11 +553,39 @@ export default class TemplateManager { //coords: coords }); template.chunked = templateTiles; + template.requiredPixelCount = requiredPixelCount; + // Construct colorPalette from paletteMap + const paletteObj = {}; + for (const [key, count] of paletteMap.entries()) { paletteObj[key] = { count, enabled: true }; } + template.colorPalette = paletteObj; + // Populate tilePrefixes for fast-scoping + try { Object.keys(templateTiles).forEach(k => { template.tilePrefixes?.add(k.split(',').slice(0,2).join(',')); }); } catch (_) {} + // Merge persisted palette (enabled/disabled) if present + try { + const persisted = templates?.[templateKey]?.palette; + if (persisted) { + for (const [rgb, meta] of Object.entries(persisted)) { + if (!template.colorPalette[rgb]) { + template.colorPalette[rgb] = { count: meta?.count || 0, enabled: !!meta?.enabled }; + } else { + template.colorPalette[rgb].enabled = !!meta?.enabled; + } + } + } + } catch (_) {} + // Store storageKey for later writes + template.storageKey = templateKey; this.templatesArray.push(template); console.log(this.templatesArray); console.log(`^^^ This ^^^`); } } + // After importing templates from storage, reveal color UI and request palette list build + try { + const colorUI = document.querySelector('#bm-contain-colorfilter'); + if (colorUI) { colorUI.style.display = ''; } + window.postMessage({ source: 'blue-marble', bmEvent: 'bm-rebuild-color-list' }, '*'); + } catch (_) { /* no-op */ } } } diff --git a/src/utils.js b/src/utils.js index 312979f..5b522be 100644 --- a/src/utils.js +++ b/src/utils.js @@ -391,4 +391,15 @@ export const colorpalette = [ "name": "Light Stone", "rgb": [205, 197, 158] } -]; \ No newline at end of file +]; + +// Annotate palette entries with ID (index) and premium flag. +try { + for (let i = 0; i < colorpalette.length; i++) { + const c = colorpalette[i]; + if (c && typeof c === 'object') { + c.id = i; + c.premium = i >= 32; // Premium colors are indices 32–63 (0-based) + } + } +} catch (_) { /* no-op */ } \ No newline at end of file