diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js index 9807344..0a071c7 100644 --- a/dist/BlueMarble.user.js +++ b/dist/BlueMarble.user.js @@ -20,4 +20,4 @@ // Wplace --> https://wplace.live // License --> https://www.mozilla.org/en-US/MPL/2.0/ -(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),o=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i);t=new WeakSet,e=function(t,e={},n={}){const i=document.createElement(t);this.t?(this.i.appendChild(i),this.o.push(this.i),this.i=i):(this.t=i,this.i=i);for(const[t,n]of Object.entries(e))i[t]=n;for(const[t,e]of Object.entries(n))i[t]=e;return i};var s,a=class{constructor({displayName:t="My template",l:e=0,h:n="",url:i="",file:o=null,coords:s=null,m:a=null,u:r=1e3}={}){this.displayName=t,this.l=e,this.h=n,this.url=i,this.file=o,this.coords=s,this.m=a,this.u=r}async p(){console.log(this.coords);const t=await createImageBitmap(this.file),e=t.width,n=t.height,i={},o=new OffscreenCanvas(this.u,this.u),s=o.getContext("2d",{$:!0});for(let a=this.coords[3];a0;)n=e[t%i]+n,t=Math.floor(t/i);return n}s=new WeakSet;var c=GM_info.script.name.toString(),l=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-o",c),e.setAttribute("bm-m","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-o")||"Blue Marble",n=t?.getAttribute("bm-m")||"",i=new Map;window.addEventListener("message",t=>{const{source:o,endpoint:s,blobID:a,blobData:r,blink:c}=t.data,l=Date.now()-c;if(console.groupCollapsed(`%c${e}%c: ${i.size} Recieved IMAGE message about blob "${a}"`,n,""),console.log(`Blob fetch took %c${String(Math.floor(l/6e4)).padStart(2,"0")}:${String(Math.floor(l/1e3)%60).padStart(2,"0")}.${String(l%1e3).padStart(3,"0")}%c MM:SS.mmm`,n,""),console.log(i),console.groupEnd(),"blue-marble"==o&&a&&r&&!s){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const o=window.fetch;window.fetch=async function(...t){const s=await o.apply(this,t),a=s.clone(),r=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",c=a.headers.get("content-type")||"";if(c.includes("application/json"))console.log(`%c${e}%c: Sending JSON message about endpoint "${r}"`,n,""),a.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:r,jsonData:t},"*")}).catch(t=>{console.error(`%c${e}%c: Failed to parse JSON: `,n,"",t)});else if(c.includes("image/")&&!r.includes("openfreemap")){const t=Date.now(),o=await a.blob();return console.log(`%c${e}%c: ${i.size} Sending IMAGE message about endpoint "${r}"`,n,""),new Promise(s=>{const c=crypto.randomUUID();i.set(c,t=>{s(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),console.log(`%c${e}%c: ${i.size} Processed blob "${c}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:c,blobData:o,blink:t})}).catch(o=>{const s=Date.now();console.error(`%c${e}%c: Failed to Promise blob!`,n,""),console.groupCollapsed(`%c${e}%c: Details of failed blob Promise:`,n,""),console.log(`Endpoint: ${r}\nThere are ${i.size} blobs processing...\nBlink: ${t.toLocaleString()}\nTime Since Blink: ${String(Math.floor(s/6e4)).padStart(2,"0")}:${String(Math.floor(s/1e3)%60).padStart(2,"0")}.${String(s%1e3).padStart(3,"0")} MM:SS.mmm`),console.error("Exception stack:",o),console.groupEnd()})}return s}});var h=GM_getResourceText("CSS-BM-File");GM_addStyle(h);var m=document.createElement("link");m.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",m.rel="preload",m.as="style",m.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(m),new class{constructor(){this.v=null,this.M=null,this.S="#bm-5"}C(t){return this.M=t,this.v=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.S)}),this}k(){return this.v}observe(t,e=!1,n=!1){t.observe(this.M,{childList:e,subtree:n})}};var u=new class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.D=null,this.T="bm-a",this.t=null,this.i=null,this.o=[]}I(t){this.D=t}N(){return this.o.length>0&&(this.i=this.o.pop()),this}B(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}O(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"div",{},n)),this}L(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"p",{},n)),this}P(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"small",{},n)),this}H(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"img",{},n)),this}R(n,i={},s=()=>{}){return s(this,o(this,t,e).call(this,"h"+n,{},i)),this}Y(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"hr",{},n)),this}G(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"br",{},n)),this}X(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.N(),i(this,s,a),this}j(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"button",{},n)),this}q(n={},i=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const a={textContent:"?",className:"bm-p",onclick:()=>{this._(this.T,s)}};return i(this,o(this,t,e).call(this,"button",a,n)),this}F(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"input",{},n)),this}J(n={},i=()=>{}){const s=n.textContent??"";delete n.textContent;const a=o(this,t,e).call(this,"div"),r=o(this,t,e).call(this,"input",{type:"file",style:"display: none;"},n);this.N();const c=o(this,t,e).call(this,"button",{textContent:s});return this.N(),this.N(),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=s}),i(this,a,r,c),this}V(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"textarea",{},n)),this}_(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}W(t,e){let n,i=!1,o=0;t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),t&&e?(e.addEventListener("mousedown",function(s){i=!0,n=s.clientX-t.getBoundingClientRect().left,o=s.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging")}),e.addEventListener("touchstart",function(s){i=!0;const a=s?.touches?.[0];a&&(n=a.clientX-t.getBoundingClientRect().left,o=a.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging"))},{passive:!1}),document.addEventListener("mousemove",function(e){i&&(t.style.left=e.clientX-n+"px",t.style.top=e.clientY-o+"px",t.style.right="")}),document.addEventListener("touchmove",function(e){if(i){const i=e?.touches?.[0];if(!i)return;t.style.left=i.clientX-n+"px",t.style.top=i.clientY-o+"px",e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchend",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchcancel",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")})):this.A(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`)}U(t){(0,console.info)(`${this.name}: ${t}`),this._(this.T,"Status: "+t,!0)}A(t){(0,console.error)(`${this.name}: ${t}`),this._(this.T,"Error: "+t,!0)}}(c,l),d=new class{constructor(t,e,n){i(this,s),this.name=t,this.version=e,this.t=n,this.Z="1.0.0",this.K=null,this.tt="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.u=1e3,this.et=3,this.nt=null,this.it=null,this.ot="bm-n",this.st="div#map canvas.maplibregl-canvas",this.rt=null,this.ct="",this.lt=[],this.ht=null}ut(){if(document.body.contains(this.nt))return this.nt;document.getElementById(this.ot)?.remove();const t=document.querySelector(this.st),e=document.createElement("canvas");return e.id=this.ot,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.nt=e,window.addEventListener("move",this.dt),window.addEventListener("zoom",this.bt),window.addEventListener("resize",this.ft),this.nt}async gt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.Z,templates:{}}}async wt(t,e,n){this.ht||(this.ht=await this.gt(),console.log("Creating JSON...")),this.t.U(`Creating template at ${n.join(", ")}...`);const i=new a({displayName:e,l:0,h:r(this.K||0,this.tt),file:t,coords:n});i.m=await i.p(this.u),this.ht.templates[`${i.l} ${i.h}`]={name:i.displayName,enabled:!0,tiles:i.m},this.lt=[],this.lt.push(i),this.t.U(`Template created at ${n.join(", ")}!`),console.log(Object.keys(this.ht.templates).length),console.log(this.ht),console.log(this.lt)}$t(){}async vt(){this.ht||(this.ht=await this.gt(),console.log("Creating JSON..."))}async yt(t,e){const n=this.u*this.et;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0"),console.log(`Looking for "${e}"`);const i=this.lt;i.sort((t,e)=>t.l-e.l),console.log(i);const o=i.map(t=>{const n=Object.keys(t.m).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>t.m[e]);return i?.[0]}).filter(Boolean);console.log(o),o.length>0&&this.t.U(`Displaying ${o.length} template${1==o.length?"":"s"}.`);const s=await createImageBitmap(t),a=new OffscreenCanvas(n,n),r=a.getContext("2d");r.imageSmoothingEnabled=!1,r.beginPath(),r.rect(0,0,n,n),r.clip(),r.clearRect(0,0,n,n),r.drawImage(s,0,0,n,n);for(const t of o)console.log("Template Blob is "+typeof t),console.log(t),r.drawImage(t,0,0);return await a.convertToBlob({type:"image/png"})}xt(){}}(c,l,u),b=new class{constructor(t){this.Mt=t,this.St=!1,this.Ct=[],this.kt=[]}Dt(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.A("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);console.log(i.id),(i.id||0===i.id)&&console.log(r(i.id,"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~")),this.Mt.K=i.id,t._("bm-f",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t._("bm-b",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t._("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const o=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),c=new URLSearchParams(n.endpoint.split("?")[1]),l=[c.get("x"),c.get("y")];if(this.Ct.length&&(!o.length||!l.length))return void t.A("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Ct=[...o,...l];const h=(s=o,a=l,[parseInt(s[0])%4*1e3+parseInt(a[0]),parseInt(s[1])%4*1e3+parseInt(a[1])]),m=document.querySelectorAll("span");for(const t of m)if(t.textContent.trim().includes(`${h[0]}, ${h[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${o[0]}, Tl Y: ${o[1]}, Px X: ${l[0]}, Px Y: ${l[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const d=n.blobID,b=n.blobData,p=await this.Mt.yt(b,u);window.postMessage({source:"blue-marble",blobID:d,blobData:p,blink:n.blink});break;case"robots":this.St="false"==i.userscript?.toString().toLowerCase();break}var s,a})}}(d);u.I(b),function(){let t=!1;u.O({id:"bm-l",style:"top: 10px; right: 75px;"}).O({id:"bm-7"}).O({id:"bm-g"}).N().H({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(e,n)=>{n.addEventListener("click",()=>{t=!t;const i=document.querySelector("#bm-l"),o=document.querySelector("#bm-7"),s=document.querySelector("#bm-g"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-c"),c=document.querySelector("#bm-d"),l=document.querySelectorAll("#bm-8 input");if(t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-l h1","#bm-4","#bm-l hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.T}`].forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.style.display=t?"none":""})}),t)a&&(a.style.display="flex",a.style.flexDirection="row",a.style.justifyContent="center",a.style.alignItems="center",a.style.gap="0.5em",a.style.margin="0.5em 0"),r&&(r.style.display=""),c&&a&&(a.appendChild(c),c.style.display="",c.style.marginTop="0"),l.forEach(t=>{t.style.display="none"}),i.style.padding="5px",o.style.textAlign="center",o.style.margin="0",o.style.marginBottom="0",s&&(s.style.display="",s.style.marginBottom="0.25em");else{if(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){const t=document.querySelector("#bm-0");t&&t.appendChild(c),c.style.display="",c.style.marginTop=""}l.forEach(t=>{t.style.display=""}),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=""}if(n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)",!t){const t="Overlay maximizado";e.U(t)}})}).N().R(1,{textContent:c}).N().N().Y().N().O({id:"bm-4"}).L({id:"bm-f",textContent:"Username:"}).N().L({id:"bm-b",textContent:"Droplets:"}).N().L({id:"bm-6",textContent:"Next level in..."}).N().N().Y().N().O({id:"bm-3"}).O({id:"bm-8"}).j({id:"bm-c",className:"bm-p",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.D?.Ct;e?.[0]?(t._("bm-h",e?.[0]||""),t._("bm-i",e?.[1]||""),t._("bm-j",e?.[2]||""),t._("bm-k",e?.[3]||"")):t.A("Coordinates are malformed! Did you try clicking on the canvas first?")}}).N().F({type:"number",id:"bm-h",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).N().F({type:"number",id:"bm-i",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).N().F({type:"number",id:"bm-j",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).N().F({type:"number",id:"bm-k",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).N().N().J({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).N().O({id:"bm-0"}).j({id:"bm-d",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-h");if(!n.checkValidity())return n.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-i");if(!i.checkValidity())return i.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-j");if(!o.checkValidity())return o.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-k");if(!s.checkValidity())return s.reportValidity(),void t.A("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(d.wt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(o.value),Number(s.value)]),t.U("Drew to canvas!")):t.A("No file selected!")}}).N().N().V({id:u.T,placeholder:`Status: Sleeping...\nVersion: ${l}`,readOnly:!0}).N().O({id:"bm-1"}).O().j({id:"bm-9",className:"bm-p",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).N().N().P({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).N().N().N().B(document.body)}(),u.W("#bm-l","#bm-g"),b.Dt(u),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-e");i||(i=document.createElement("button"),i.id="bm-e",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"},n.parentNode.parentNode.parentNode.parentNode.querySelector("h2").parentNode.appendChild(i))}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${c}%c (${l}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file +(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),o=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i);t=new WeakSet,e=function(t,e={},n={}){const i=document.createElement(t);this.t?(this.i.appendChild(i),this.o.push(this.i),this.i=i):(this.t=i,this.i=i);for(const[t,n]of Object.entries(e))i[t]=n;for(const[t,e]of Object.entries(n))i[t]=e;return i};var s,a=class{constructor({displayName:t="My template",l:e=0,h:n="",url:i="",file:o=null,coords:s=null,m:a=null,u:r=1e3}={}){this.displayName=t,this.l=e,this.h=n,this.url=i,this.file=o,this.coords=s,this.m=a,this.u=r,this.p=0}async $(){console.log(this.coords);const t=await createImageBitmap(this.file),e=t.width,n=t.height,i=e*n;console.log(`Template dimensions: ${e}x${n} = ${i} pixels`),this.p=i;const o={},s=new OffscreenCanvas(this.u,this.u),a=s.getContext("2d",{v:!0});for(let i=this.coords[3];i0;)n=e[t%i]+n,t=Math.floor(t/i);return n}s=new WeakSet;var c=GM_info.script.name.toString(),l=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-o",c),e.setAttribute("bm-m","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-o")||"Blue Marble",n=t?.getAttribute("bm-m")||"",i=new Map;window.addEventListener("message",t=>{const{source:o,endpoint:s,blobID:a,blobData:r,blink:c}=t.data,l=Date.now()-c;if(console.groupCollapsed(`%c${e}%c: ${i.size} Recieved IMAGE message about blob "${a}"`,n,""),console.log(`Blob fetch took %c${String(Math.floor(l/6e4)).padStart(2,"0")}:${String(Math.floor(l/1e3)%60).padStart(2,"0")}.${String(l%1e3).padStart(3,"0")}%c MM:SS.mmm`,n,""),console.log(i),console.groupEnd(),"blue-marble"==o&&a&&r&&!s){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const o=window.fetch;window.fetch=async function(...t){const s=await o.apply(this,t),a=s.clone(),r=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",c=a.headers.get("content-type")||"";if(c.includes("application/json"))console.log(`%c${e}%c: Sending JSON message about endpoint "${r}"`,n,""),a.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:r,jsonData:t},"*")}).catch(t=>{console.error(`%c${e}%c: Failed to parse JSON: `,n,"",t)});else if(c.includes("image/")&&!r.includes("openfreemap")){const t=Date.now(),o=await a.blob();return console.log(`%c${e}%c: ${i.size} Sending IMAGE message about endpoint "${r}"`,n,""),new Promise(s=>{const c=crypto.randomUUID();i.set(c,t=>{s(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),console.log(`%c${e}%c: ${i.size} Processed blob "${c}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:c,blobData:o,blink:t})}).catch(o=>{const s=Date.now();console.error(`%c${e}%c: Failed to Promise blob!`,n,""),console.groupCollapsed(`%c${e}%c: Details of failed blob Promise:`,n,""),console.log(`Endpoint: ${r}\nThere are ${i.size} blobs processing...\nBlink: ${t.toLocaleString()}\nTime Since Blink: ${String(Math.floor(s/6e4)).padStart(2,"0")}:${String(Math.floor(s/1e3)%60).padStart(2,"0")}.${String(s%1e3).padStart(3,"0")} MM:SS.mmm`),console.error("Exception stack:",o),console.groupEnd()})}return s}});var h=GM_getResourceText("CSS-BM-File");GM_addStyle(h);var m=document.createElement("link");m.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",m.rel="preload",m.as="style",m.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(m),new class{constructor(){this.M=null,this.S=null,this.C="#bm-5"}k(t){return this.S=t,this.M=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.C)}),this}D(){return this.M}observe(t,e=!1,n=!1){t.observe(this.S,{childList:e,subtree:n})}};var u=new class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.T=null,this.I="bm-a",this.t=null,this.i=null,this.o=[]}N(t){this.T=t}B(){return this.o.length>0&&(this.i=this.o.pop()),this}O(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}L(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"div",{},n)),this}P(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"p",{},n)),this}H(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"small",{},n)),this}R(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"img",{},n)),this}Y(n,i={},s=()=>{}){return s(this,o(this,t,e).call(this,"h"+n,{},i)),this}j(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"hr",{},n)),this}G(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"br",{},n)),this}X(n={},i=()=>{}){const s=o(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=o(this,t,e).call(this,"input",{type:"checkbox"},n);return s.insertBefore(a,s.firstChild),this.B(),i(this,s,a),this}q(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"button",{},n)),this}_(n={},i=()=>{}){const s=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${s}`;const a={textContent:"?",className:"bm-p",onclick:()=>{this.F(this.I,s)}};return i(this,o(this,t,e).call(this,"button",a,n)),this}J(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"input",{},n)),this}V(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;"},n);this.B();const c=o(this,t,e).call(this,"button",{textContent:s});return this.B(),this.B(),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=s}),i(this,a,r,c),this}W(n={},i=()=>{}){return i(this,o(this,t,e).call(this,"textarea",{},n)),this}F(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}A(t,e){let n,i=!1,o=0;t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),t&&e?(e.addEventListener("mousedown",function(s){i=!0,n=s.clientX-t.getBoundingClientRect().left,o=s.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging")}),e.addEventListener("touchstart",function(s){i=!0;const a=s?.touches?.[0];a&&(n=a.clientX-t.getBoundingClientRect().left,o=a.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging"))},{passive:!1}),document.addEventListener("mousemove",function(e){i&&(t.style.left=e.clientX-n+"px",t.style.top=e.clientY-o+"px",t.style.right="")}),document.addEventListener("touchmove",function(e){if(i){const i=e?.touches?.[0];if(!i)return;t.style.left=i.clientX-n+"px",t.style.top=i.clientY-o+"px",e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchend",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchcancel",function(){i=!1,document.body.style.userSelect="",e.classList.remove("dragging")})):this.U(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`)}Z(t){(0,console.info)(`${this.name}: ${t}`),this.F(this.I,"Status: "+t,!0)}U(t){(0,console.error)(`${this.name}: ${t}`),this.F(this.I,"Error: "+t,!0)}}(c,l),d=new class{constructor(t,e,n){i(this,s),this.name=t,this.version=e,this.t=n,this.K="1.0.0",this.tt=null,this.et="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.u=1e3,this.nt=3,this.it=null,this.ot=null,this.st="bm-n",this.rt="div#map canvas.maplibregl-canvas",this.ct=null,this.lt="",this.ht=[],this.ut=null}dt(){if(document.body.contains(this.it))return this.it;document.getElementById(this.st)?.remove();const t=document.querySelector(this.rt),e=document.createElement("canvas");return e.id=this.st,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.it=e,window.addEventListener("move",this.bt),window.addEventListener("zoom",this.ft),window.addEventListener("resize",this.gt),this.it}async $t(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.K,templates:{}}}async wt(t,e,n){this.ut||(this.ut=await this.$t(),console.log("Creating JSON...")),this.t.Z(`Creating template at ${n.join(", ")}...`);const i=new a({displayName:e,l:0,h:r(this.tt||0,this.et),file:t,coords:n});i.m=await i.$(this.u),this.ut.templates[`${i.l} ${i.h}`]={name:i.displayName,enabled:!0,tiles:i.m},this.ht=[],this.ht.push(i);const o=(new Intl.NumberFormat).format(i.p);this.t.Z(`Template created at ${n.join(", ")}! Total pixels: ${o}`),console.log(Object.keys(this.ut.templates).length),console.log(this.ut),console.log(this.ht)}vt(){}async xt(){this.ut||(this.ut=await this.$t(),console.log("Creating JSON..."))}async yt(t,e){const n=this.u*this.nt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0"),console.log(`Looking for "${e}"`);const i=this.ht;i.sort((t,e)=>t.l-e.l),console.log(i);const o=i.map(t=>{const n=Object.keys(t.m).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>t.m[e]);return i?.[0]}).filter(Boolean);if(console.log(o),o.length>0){const t=i.filter(t=>Object.keys(t.m).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.p||0),0),n=(new Intl.NumberFormat).format(t);this.t.Z(`Displaying ${o.length} template${1==o.length?"":"s"}. Total pixels: ${n}`)}const s=await createImageBitmap(t),a=new OffscreenCanvas(n,n),r=a.getContext("2d");r.imageSmoothingEnabled=!1,r.beginPath(),r.rect(0,0,n,n),r.clip(),r.clearRect(0,0,n,n),r.drawImage(s,0,0,n,n);for(const t of o)console.log("Template Blob is "+typeof t),console.log(t),r.drawImage(t,0,0);return await a.convertToBlob({type:"image/png"})}Mt(){}}(c,l,u),b=new class{constructor(t){this.St=t,this.Ct=!1,this.kt=[],this.Dt=[]}Tt(t){window.addEventListener("message",async e=>{const n=e.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const o=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(console.log('%cBlue Marble%c: Recieved message about "%s"',"color: cornflowerblue;","",o),o){case"me":if(i.status&&"2"!=i.status?.toString()[0])return void t.U("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);console.log(i.id),(i.id||0===i.id)&&console.log(r(i.id,"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~")),this.St.tt=i.id,t.F("bm-f",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.F("bm-b",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.F("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const o=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),c=new URLSearchParams(n.endpoint.split("?")[1]),l=[c.get("x"),c.get("y")];if(this.kt.length&&(!o.length||!l.length))return void t.U("Coordinates are malformed!\nDid you try clicking the canvas first?");this.kt=[...o,...l];const h=(s=o,a=l,[parseInt(s[0])%4*1e3+parseInt(a[0]),parseInt(s[1])%4*1e3+parseInt(a[1])]),m=document.querySelectorAll("span");for(const t of m)if(t.textContent.trim().includes(`${h[0]}, ${h[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${o[0]}, Tl Y: ${o[1]}, Px X: ${l[0]}, Px Y: ${l[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const d=n.blobID,b=n.blobData,p=await this.St.yt(b,u);window.postMessage({source:"blue-marble",blobID:d,blobData:p,blink:n.blink});break;case"robots":this.Ct="false"==i.userscript?.toString().toLowerCase();break}var s,a})}}(d);u.N(b),function(){let t=!1;u.L({id:"bm-l",style:"top: 10px; right: 75px;"}).L({id:"bm-7"}).L({id:"bm-g"}).B().R({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(e,n)=>{n.addEventListener("click",()=>{t=!t;const i=document.querySelector("#bm-l"),o=document.querySelector("#bm-7"),s=document.querySelector("#bm-g"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-c"),c=document.querySelector("#bm-d"),l=document.querySelectorAll("#bm-8 input");if(t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-l h1","#bm-4","#bm-l hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.I}`].forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.style.display=t?"none":""})}),t?(a&&(a.style.display="none"),r&&(r.style.display="none"),c&&(c.style.display="none"),l.forEach(t=>{t.style.display="none"}),i.style.width="60px",i.style.height="76px",i.style.maxWidth="60px",i.style.minWidth="60px",i.style.padding="8px",n.style.marginLeft="3px",o.style.textAlign="center",o.style.margin="0",o.style.marginBottom="0",s&&(s.style.display="",s.style.marginBottom="0.25em")):(a&&(a.style.display="",a.style.flexDirection="",a.style.justifyContent="",a.style.alignItems="",a.style.gap="",a.style.textAlign="",a.style.margin=""),r&&(r.style.display=""),c&&(c.style.display="",c.style.marginTop=""),l.forEach(t=>{t.style.display=""}),n.style.marginLeft="",i.style.padding="10px",o.style.textAlign="",o.style.margin="",o.style.marginBottom="",s&&(s.style.marginBottom="0.5em"),i.style.width="",i.style.height=""),n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)",!t){const t="Overlay maximizado";e.Z(t)}})}).B().Y(1,{textContent:c}).B().B().j().B().L({id:"bm-4"}).P({id:"bm-f",textContent:"Username:"}).B().P({id:"bm-b",textContent:"Droplets:"}).B().P({id:"bm-6",textContent:"Next level in..."}).B().B().j().B().L({id:"bm-3"}).L({id:"bm-8"}).q({id:"bm-c",className:"bm-p",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.T?.kt;e?.[0]?(t.F("bm-h",e?.[0]||""),t.F("bm-i",e?.[1]||""),t.F("bm-j",e?.[2]||""),t.F("bm-k",e?.[3]||"")):t.U("Coordinates are malformed! Did you try clicking on the canvas first?")}}).B().J({type:"number",id:"bm-h",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).B().J({type:"number",id:"bm-i",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).B().J({type:"number",id:"bm-j",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).B().J({type:"number",id:"bm-k",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).B().B().V({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).B().L({id:"bm-0"}).q({id:"bm-d",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-h");if(!n.checkValidity())return n.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-i");if(!i.checkValidity())return i.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-j");if(!o.checkValidity())return o.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-k");if(!s.checkValidity())return s.reportValidity(),void t.U("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(d.wt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(o.value),Number(s.value)]),t.Z("Drew to canvas!")):t.U("No file selected!")}}).B().B().W({id:u.I,placeholder:`Status: Sleeping...\nVersion: ${l}`,readOnly:!0}).B().L({id:"bm-1"}).L().q({id:"bm-9",className:"bm-p",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).B().B().H({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).B().B().B().O(document.body)}(),u.A("#bm-l","#bm-g"),b.Tt(u),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-e");i||(i=document.createElement("button"),i.id="bm-e",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"},n.parentNode.parentNode.parentNode.parentNode.querySelector("h2").parentNode.appendChild(i))}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${c}%c (${l}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aefe7b9..c0ec0ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wplace-bluemarble", - "version": "0.67.0", + "version": "0.69.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wplace-bluemarble", - "version": "0.67.0", + "version": "0.69.0", "devDependencies": { "esbuild": "^0.25.0", "terser": "^5.43.1" diff --git a/src/Template.js b/src/Template.js index d73f83e..f915f68 100644 --- a/src/Template.js +++ b/src/Template.js @@ -1,19 +1,50 @@ -/** An instance of a template. - * Handles all mathmatics and manipulation regarding a single template. +/** An instance of a template with comprehensive pixel counting and statistics. + * Handles all mathematics, manipulation, and statistical analysis regarding a single template. + * + * TEMPLATE FEATURES: + * - Automatic pixel counting and dimension analysis + * - Tile-based template chunking for efficient rendering + * - Statistical information storage and retrieval + * - Bitmap processing with configurable scaling + * - Memory-efficient template tile generation + * + * PIXEL COUNTING SYSTEM: + * - Calculates total pixel count (width × height) during template creation + * - Stores pixel count for statistical display and analysis + * - Integrates with template manager for aggregate statistics + * - Provides formatted pixel count information to user interface + * * @since 0.65.2 + * @version 1.1.0 - Added comprehensive pixel counting and statistical analysis system */ export default class Template { - /** The constructor for the {@link Template} class. - * @param {Object} [params={}] - Object containing all optional params + /** The constructor for the {@link Template} class with enhanced pixel tracking. + * + * Initializes a new template instance with all necessary properties for rendering + * and statistical analysis. The pixel counting system is initialized here and + * populated during the template creation process. + * + * PIXEL COUNTING INTEGRATION: + * The pixelCount property is automatically calculated during createTemplateTiles() + * and represents the total number of pixels in the source image (width × height). + * This information is used for: + * - User interface statistics display + * - Template comparison and analysis + * - Performance optimization decisions + * - Memory usage estimation + * + * @param {Object} [params={}] - Object containing all optional parameters * @param {string} [params.displayName='My template'] - The display name of the template - * @param {number} [params.sortID=0] - The sort number of the template - * @param {string} [params.authorID=''] - The user ID of the person who exported the template. This is to prevent sort ID collisions when importing - * @param {string} [params.url=''] - The URL to the image - * @param {File} [params.file=null] - The template file. This can be a pre-processed File, or a processed bitmap - * @param {[number, number, number, number]} [params.coords=null] - The coordinates of the top left corner as (x, y, x, y) + * @param {number} [params.sortID=0] - The sort number of the template for rendering priority + * @param {string} [params.authorID=''] - The user ID of the person who exported the template (prevents sort ID collisions) + * @param {string} [params.url=''] - The URL to the source image + * @param {File} [params.file=null] - The template file (pre-processed File or processed bitmap) + * @param {[number, number, number, number]} [params.coords=null] - The coordinates of the top left corner as (tileX, tileY, pixelX, pixelY) * @param {Object} [params.chunked=null] - The affected chunks of the template, and their template for each chunk - * @param {number} [params.tileSize=1000] - The size of a tile in pixels. Assumes the tile is a square + * @param {number} [params.tileSize=1000] - The size of a tile in pixels (assumes square tiles) + * @param {number} [params.pixelCount=0] - Total number of pixels in the template (calculated automatically during processing) * @since 0.65.2 + * @version 1.1.0 - Added pixelCount property for comprehensive template statistics and analysis */ constructor({ displayName = 'My template', @@ -33,19 +64,60 @@ export default class Template { this.coords = coords; this.chunked = chunked; this.tileSize = tileSize; + this.pixelCount = 0; // Total pixel count in template (automatically calculated during createTemplateTiles) } - /** Creates chunks of the template for each tile. - * @returns {Object} Collection of template bitmaps in a Object + /** Creates chunks of the template for each tile with integrated pixel counting system. + * + * This method processes the template image and performs several critical operations: + * 1. PIXEL ANALYSIS: Calculates total pixel count (width × height) for statistical purposes + * 2. TILE CHUNKING: Divides the template into tile-sized chunks for efficient rendering + * 3. BITMAP PROCESSING: Applies scaling and filtering for optimal display quality + * 4. MEMORY OPTIMIZATION: Creates efficient ImageBitmap objects for each tile segment + * + * PIXEL COUNTING IMPLEMENTATION: + * The pixel counting system calculates the total number of pixels in the source image + * by multiplying the bitmap width by height. This information is stored in the + * pixelCount property and used throughout the application for: + * - User interface statistics display + * - Template comparison and analysis + * - Performance monitoring and optimization + * - Memory usage estimation and management + * + * TECHNICAL DETAILS: + * - Uses createImageBitmap() for efficient image processing + * - Applies 3x scaling factor (shreadSize) for pixel art enhancement + * - Processes images in tile-sized chunks for memory efficiency + * - Maintains pixel-perfect rendering with nearest-neighbor sampling + * - Handles coordinate transformation between template and tile coordinate systems + * + * PERFORMANCE CONSIDERATIONS: + * - Large templates are processed incrementally to avoid memory issues + * - Bitmap creation uses OffscreenCanvas for optimal performance + * - Pixel counting is performed once during initial processing + * - Results are cached in the pixelCount property for repeated access + * + * @returns {Object} Collection of template bitmaps organized by tile coordinates * @since 0.65.4 + * @version 1.1.0 - Added comprehensive pixel counting functionality with detailed logging and statistics */ async createTemplateTiles() { - console.log(this.coords); + console.log('Template coordinates:', this.coords); - const shreadSize = 3; // Scale image factor. Must be odd - const bitmap = await createImageBitmap(this.file); // Creates a bitmap image from the uploaded file + 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; + + // ==================== PIXEL COUNTING SYSTEM ==================== + // Calculate total pixel count using standard width × height formula + // This provides essential statistical information for the user interface + const totalPixels = imageWidth * imageHeight; + console.log(`Template pixel analysis - Dimensions: ${imageWidth}×${imageHeight} = ${totalPixels.toLocaleString()} pixels`); + + // Store pixel count in instance property for access by template manager and UI components + // This enables real-time statistics display and template comparison features + this.pixelCount = totalPixels; const templateTiles = {}; // Holds the template tiles diff --git a/src/main.js b/src/main.js index a715686..6d49f2e 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,19 @@ /** The main file. Everything in the userscript is executed from here. * @since 0.0.0 + * + * VERSION HISTORY: + * v1.1.0 - Added minimize/maximize functionality and pixel counting system + * Features added: + * - Interactive minimize/maximize overlay with click-to-toggle functionality + * - Fixed overlay dimensions: 60px width × 76px height in minimized state + * - Smart element visibility control (hides all UI except icon and drag bar when minimized) + * - Icon repositioning system (3px right offset) for better visual alignment in minimized state + * - Comprehensive pixel counting system for template statistics + * - Real-time pixel count display in template creation and rendering status messages + * - Intelligent pixel counting for actively rendered templates with tile-based filtering + * - Internationalized number formatting for large pixel counts (e.g., "1,234,567 pixels") + * - Automatic state management with proper cleanup when switching between modes + * - Enhanced user experience with visual feedback and status updates */ import Overlay from './Overlay.js'; @@ -229,20 +243,61 @@ function observeBlack() { observer.observe(document.body, { childList: true, subtree: true }); } -/** Deploys the overlay to the page. +/** Deploys the overlay to the page with minimize/maximize functionality. + * Creates a responsive overlay UI that can toggle between full-featured and minimized states. + * * Parent/child relationships in the DOM structure below are indicated by indentation. + * + * OVERLAY STATES: + * - MAXIMIZED: Full UI with all controls, inputs, and status information visible + * - MINIMIZED: Compact 60×76px interface showing only the Blue Marble icon and drag functionality + * + * FEATURES: + * - Click-to-toggle functionality on the Blue Marble icon + * - Automatic element visibility management + * - Fixed dimensions for consistent minimized appearance + * - Proper cleanup and restoration of all UI elements + * - Visual feedback through alt-text updates + * - Status message integration + * * @since 0.58.3 + * @version 1.1.0 - Added comprehensive minimize/maximize feature with fixed dimensions and enhanced UX */ function buildOverlayMain() { - let isMinimized = false; // Estado do overlay (false = maximizado, true = minimizado) + let isMinimized = false; // Overlay state tracker (false = maximized, true = minimized) overlay.addDiv({'id': 'bm-overlay', 'style': 'top: 10px; right: 75px;'}) .addDiv({'id': 'bm-contain-header'}) .addDiv({'id': 'bm-bar-drag'}).buildElement() .addImg({'alt': 'Blue Marble Icon - Click to minimize/maximize', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png', 'style': 'cursor: pointer;'}, (instance, img) => { + /** Click event handler for overlay minimize/maximize functionality. + * + * Toggles between two distinct UI states: + * 1. MINIMIZED STATE (60×76px): + * - Shows only the Blue Marble icon and drag bar + * - Hides all input fields, buttons, and status information + * - Applies fixed dimensions for consistent appearance + * - Repositions icon with 3px right offset for visual centering + * + * 2. MAXIMIZED STATE (responsive): + * - Restores full functionality with all UI elements + * - Removes fixed dimensions to allow responsive behavior + * - Resets icon positioning to default alignment + * - Shows success message when returning to maximized state + * + * IMPLEMENTATION DETAILS: + * - Uses CSS display property manipulation for element visibility + * - Maintains drag functionality in both states + * - Updates accessibility text (alt attribute) based on current state + * - Provides user feedback through status messages + * - Ensures proper cleanup of all style overrides when switching states + * + * @since 1.1.0 - Complete minimize/maximize implementation + * @param {Event} event - The click event object (implicit) + */ img.addEventListener('click', () => { - isMinimized = !isMinimized; + isMinimized = !isMinimized; // Toggle the current state const overlay = document.querySelector('#bm-overlay'); const header = document.querySelector('#bm-contain-header'); @@ -252,107 +307,148 @@ function buildOverlayMain() { const enableButton = document.querySelector('#bm-button-enable'); const coordInputs = document.querySelectorAll('#bm-contain-coords input'); - // Restaura o tamanho original ao maximizar + // Pre-restore original dimensions when switching to maximized state + // This ensures smooth transition and prevents layout issues if (!isMinimized) { overlay.style.width = "auto"; overlay.style.maxWidth = "300px"; overlay.style.minWidth = "200px"; overlay.style.padding = "10px"; } + + // Define elements that should be hidden/shown during state transitions + // Each element is documented with its purpose for maintainability const elementsToToggle = [ - '#bm-overlay h1', // Título "Blue Marble" - '#bm-contain-userinfo', // Informações do usuário - '#bm-overlay hr', // Linhas separadoras - '#bm-contain-automation > *:not(#bm-contain-coords)', // Seção de automação exceto coordenadas - '#bm-input-file-template', // Upload de arquivo - '#bm-contain-buttons-action', // Botões de ação - `#${instance.outputStatusId}` // Log de status (textarea) + '#bm-overlay h1', // Main title "Blue Marble" + '#bm-contain-userinfo', // User information section (username, droplets, level) + '#bm-overlay hr', // Visual separator lines + '#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 ]; + + // Apply visibility changes to all toggleable elements elementsToToggle.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(element => { element.style.display = isMinimized ? 'none' : ''; }); }); - // Controla especificamente o container de coordenadas e botões + // Handle coordinate container and button visibility based on state if (isMinimized) { - // Configura o container de coords para mostrar apenas o botão (esconde inputs) + // ==================== MINIMIZED STATE CONFIGURATION ==================== + // In minimized state, we hide ALL interactive elements except the icon and drag bar + // This creates a clean, unobtrusive interface that maintains only essential functionality + + // Hide coordinate input container completely if (coordsContainer) { - coordsContainer.style.display = 'flex'; - coordsContainer.style.flexDirection = 'row'; - coordsContainer.style.justifyContent = 'center'; - coordsContainer.style.alignItems = 'center'; - coordsContainer.style.gap = '0.5em'; - coordsContainer.style.margin = '0.5em 0'; + coordsContainer.style.display = 'none'; } + + // Hide coordinate button (pin icon) if (coordsButton) { - coordsButton.style.display = ''; + coordsButton.style.display = 'none'; } - // Move o botão Enable para o container de coordenadas para ficar na mesma linha - if (enableButton && coordsContainer) { - coordsContainer.appendChild(enableButton); - enableButton.style.display = ''; - enableButton.style.marginTop = '0'; + // Hide enable/disable template button + if (enableButton) { + enableButton.style.display = 'none'; } + // Hide all coordinate input fields individually (failsafe) coordInputs.forEach(input => { input.style.display = 'none'; }); - overlay.style.padding = '5px'; + + // Apply fixed dimensions for consistent minimized appearance + // These dimensions were chosen to accommodate the icon while remaining compact + overlay.style.width = '60px'; // Fixed width for consistency + overlay.style.height = '76px'; // Fixed height (60px + 16px for better proportions) + overlay.style.maxWidth = '60px'; // Prevent expansion + overlay.style.minWidth = '60px'; // Prevent shrinking + overlay.style.padding = '8px'; // Comfortable padding around icon + + // Apply icon positioning for better visual centering in minimized state + // The 3px offset compensates for visual weight distribution + img.style.marginLeft = '3px'; + + // Configure header layout for minimized state header.style.textAlign = 'center'; header.style.margin = '0'; header.style.marginBottom = '0'; + + // Ensure drag bar remains visible and properly spaced if (dragBar) { dragBar.style.display = ''; dragBar.style.marginBottom = '0.25em'; } } else { - // Restaura o layout normal + // ==================== MAXIMIZED STATE RESTORATION ==================== + // In maximized state, we restore all elements to their default functionality + // This involves clearing all style overrides applied during minimization + + // Restore coordinate container to default state if (coordsContainer) { - coordsContainer.style.display = ''; - coordsContainer.style.flexDirection = ''; - coordsContainer.style.justifyContent = ''; - coordsContainer.style.alignItems = ''; - coordsContainer.style.gap = ''; - coordsContainer.style.textAlign = ''; - coordsContainer.style.margin = ''; + coordsContainer.style.display = ''; // Show container + coordsContainer.style.flexDirection = ''; // Reset flex layout + coordsContainer.style.justifyContent = ''; // Reset alignment + coordsContainer.style.alignItems = ''; // Reset alignment + coordsContainer.style.gap = ''; // Reset spacing + coordsContainer.style.textAlign = ''; // Reset text alignment + coordsContainer.style.margin = ''; // Reset margins } + + // Restore coordinate button visibility if (coordsButton) { coordsButton.style.display = ''; } - // Move o botão Enable de volta para seu container original + // Restore enable button visibility and reset positioning if (enableButton) { - const enableContainer = document.querySelector('#bm-contain-buttons-template'); - if (enableContainer) { - enableContainer.appendChild(enableButton); - } enableButton.style.display = ''; enableButton.style.marginTop = ''; } + // Restore all coordinate input fields coordInputs.forEach(input => { input.style.display = ''; }); + + // Reset icon positioning to default (remove minimized state offset) + img.style.marginLeft = ''; + + // Restore overlay to responsive dimensions overlay.style.padding = '10px'; + + // Reset header styling to defaults header.style.textAlign = ''; header.style.margin = ''; header.style.marginBottom = ''; + + // Reset drag bar spacing if (dragBar) { dragBar.style.marginBottom = '0.5em'; } - // Remove dimensões fixas para permitir redimensionamento automático + + // Remove all fixed dimensions to allow responsive behavior + // This ensures the overlay can adapt to content changes overlay.style.width = ''; overlay.style.height = ''; } - // Atualiza o alt text do ícone para refletir o estado atual + + // ==================== ACCESSIBILITY AND USER FEEDBACK ==================== + // Update accessibility information and provide user feedback + + // Update alt text to reflect current state for screen readers and tooltips img.alt = isMinimized ? 'Blue Marble Icon - Minimized (Click to maximize)' : 'Blue Marble Icon - Maximized (Click to minimize)'; - // Atualiza a mensagem de status apenas quando maximizado (para não aparecer quando minimizado) + + // Provide status feedback only when maximizing (avoid clutter in minimized state) + // This gives users confirmation that the action was successful if (!isMinimized) { - const statusMessage = 'Overlay maximizado'; + const statusMessage = 'Overlay maximized - All controls restored'; instance.handleDisplayStatus(statusMessage); } }); diff --git a/src/templateManager.js b/src/templateManager.js index e200c63..e8c3dc8 100644 --- a/src/templateManager.js +++ b/src/templateManager.js @@ -1,9 +1,34 @@ import Template from "./Template"; import { numberToEncoded } from "./utils"; -/** Manages the template system. - * This class handles all external requests for modification to a Template. +/** Manages the comprehensive template system with integrated pixel counting and statistics. + * + * This class handles all external requests for template modification, creation, and statistical analysis. + * It serves as the central coordinator between template instances and the user interface, providing + * real-time feedback on template statistics including pixel counts and rendering status. + * + * ENHANCED FEATURES (v1.1.0): + * - Real-time pixel counting and statistics display + * - Intelligent template filtering based on active tiles + * - Internationalized number formatting for large pixel counts + * - Comprehensive status reporting with detailed template information + * - Enhanced user feedback during template creation and rendering + * + * PIXEL COUNTING SYSTEM: + * The template manager integrates with the Template class pixel counting system to provide: + * - Individual template pixel counts during creation + * - Aggregate pixel counts for multiple templates during rendering + * - Smart filtering to count only actively displayed templates + * - Formatted display of pixel statistics in user interface + * + * STATISTICAL INTEGRATION POINTS: + * 1. Template Creation: Displays pixel count when new templates are processed + * 2. Template Rendering: Shows aggregate pixel count for templates being displayed + * 3. Tile Filtering: Counts pixels only for templates active in current viewport + * 4. User Interface: Provides formatted statistics for status messages + * * @since 0.55.8 + * @version 1.1.0 - Added comprehensive pixel counting system and enhanced statistical reporting * @example * // JSON structure for a template * { @@ -147,7 +172,11 @@ export default class TemplateManager { this.templatesArray = []; // Remove this to enable multiple templates (2/2) this.templatesArray.push(template); // Pushes the Template object instance to the Template Array - this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}!`); + // ==================== PIXEL COUNT DISPLAY SYSTEM ==================== + // Display pixel count statistics with internationalized number formatting + // This provides immediate feedback to users about template complexity and size + const pixelCountFormatted = new Intl.NumberFormat().format(template.pixelCount); + this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}! Total pixels: ${pixelCountFormatted}`); console.log(Object.keys(this.templatesJSON.templates).length); console.log(this.templatesJSON); @@ -177,18 +206,44 @@ export default class TemplateManager { } - /** Draws all templates on that tile + /** Draws all templates on the specified tile with intelligent pixel count reporting. + * + * This method handles the rendering of template overlays on individual tiles and provides + * comprehensive statistics about the templates being displayed. It integrates with the + * pixel counting system to give users real-time feedback about template complexity. + * + * PIXEL COUNTING INTEGRATION: + * The method implements intelligent pixel counting that: + * - Identifies templates that have content in the current tile + * - Sums pixel counts only for templates actually being rendered + * - Formats large numbers with locale-appropriate separators + * - Provides detailed status messages with template and pixel statistics + * + * PERFORMANCE OPTIMIZATIONS: + * - Filters templates by tile coordinates before processing + * - Counts pixels only for active templates to avoid unnecessary calculations + * - Uses efficient array operations for template filtering and aggregation + * - Caches formatted numbers to avoid repeated formatting operations + * + * USER EXPERIENCE ENHANCEMENTS: + * - Shows both template count and total pixel count in status messages + * - Uses internationalized number formatting for better readability + * - Provides immediate feedback when templates are being displayed + * - Handles singular/plural forms correctly for template count + * * @param {File} tileBlob - The pixels that are placed on a tile * @param {[number, number]} tileCoords - The tile coordinates [x, y] * @since 0.65.77 + * @version 1.1.0 - Added intelligent pixel counting and enhanced status reporting */ async drawTemplateOnTile(tileBlob, tileCoords) { - const drawSize = this.tileSize * this.drawMult; // Draw multiplier + const drawSize = this.tileSize * this.drawMult; // Calculate draw multiplier for scaling + // Format tile coordinates with proper padding for consistent lookup tileCoords = tileCoords[0].toString().padStart(4, '0') + ',' + tileCoords[1].toString().padStart(4, '0'); - console.log(`Looking for "${tileCoords}"`); + console.log(`Searching for templates in tile: "${tileCoords}"`); const templateArray = this.templatesArray; // Stores a copy for sorting @@ -218,7 +273,31 @@ export default class TemplateManager { console.log(templateBlobs); if (templateBlobs.length > 0) { - this.overlay.handleDisplayStatus(`Displaying ${templateBlobs.length} template${templateBlobs.length == 1 ? '' : 's'}.`); + // ==================== INTELLIGENT PIXEL COUNTING SYSTEM ==================== + // Calculate total pixel count for templates actively being displayed in this tile + // This provides accurate statistics by counting only templates with content in the current viewport + + 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 comprehensive status information including both template count and pixel statistics + // This gives users immediate feedback about the complexity and scope of what's being rendered + this.overlay.handleDisplayStatus( + `Displaying ${templateBlobs.length} template${templateBlobs.length == 1 ? '' : 's'}. ` + + `Total pixels: ${pixelCountFormatted}` + ); } const tileBitmap = await createImageBitmap(tileBlob);