@mtillmann/circlepacker
Version:
simple circle-packing library
2 lines (1 loc) • 7.34 kB
JavaScript
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).MTCP={})}(this,(function(t){"use strict";var i={pack(t,i,s,a){let e=s.length;const o=[],n={width:i,height:t.data.length/i/4};for(;e>0;){e--;const i=s[e];let r=1e3;for(;!i.x&&r-- >0;){const s=Math.random()*n.width,e=Math.random()*n.height;this.isCircleInside(t,n.width,s,e,i.radius,a.higherAccuracy,a.minAlpha)&&(this.touchesPlacedCircle(s,e,i.radius,o,a.spacing)||(i.x=s,i.y=e,o.push(i)))}}return o},isFilled:(t,i,s,a,e)=>(i=Math.round(i),s=Math.round(s),t.data[4*(a*s+i)+3]>e),isCircleInside(t,i,s,a,e,o,n){if(!this.isFilled(t,s,a-e,i,n))return!1;if(!this.isFilled(t,s,a+e,i,n))return!1;if(!this.isFilled(t,s+e,a,i,n))return!1;if(!this.isFilled(t,s-e,a,i,n))return!1;if(o){const e=Math.cos(Math.PI/4);if(!this.isFilled(t,s+e,a+e,i,n))return!1;if(!this.isFilled(t,s-e,a+e,i,n))return!1;if(!this.isFilled(t,s-e,a-e,i,n))return!1;if(!this.isFilled(t,s+e,a-e,i,n))return!1}return!0},touchesPlacedCircle(t,i,s,a,e){return a.some((a=>this.dist(t,i,a.x,a.y)<a.radius+s+e))},dist(t,i,s,a){const e=t-s,o=i-a;return Math.sqrt(e*e+o*o)}},s=class{defaultOptions={spacing:1,numCircles:1e3,minRadius:1,maxRadius:10,higherAccuracy:!1,colors:"auto",minAlpha:1,background:"transparent",useMainThread:!1,reuseWorker:!0};defaultExportOptions={scale:globalThis.devicePixelRatio||1,quality:1,format:"image/png"};options;spareCircles=[];placedCircles=[];dims={width:0,height:0};worker=null;constructor(t={}){this.options={...this.defaultOptions,...t},["transparent",null,"",!1,void 0].includes(this.options.background)&&(this.options.background=!1),"auto"===this.options.colors&&(this.options.colors=[]);for(let t=0;t<this.options.numCircles;t++)this.spareCircles.push({radius:this.options.minRadius+Math.random()*Math.random()*(this.options.maxRadius-this.options.minRadius)});this.spareCircles.sort(((t,i)=>t.radius-i.radius))}getCircleColor(t,i,s){if(0===this.options.colors.length){i=Math.min(Math.round(i),this.dims.width-1),s=Math.round(s);const a=4*(this.dims.width*s+i);if(t.data[a+3]/255<this.options.minAlpha)return!1;return`rgb(${t.data[a]},${t.data[a+1]},${t.data[a+2]})`}return this.options.colors[Math.floor(Math.random()*this.options.colors.length)]}async pack(t,s){let a=[];return"undefined"==typeof Worker||this.options.useMainThread?a=i.pack(t,s,this.spareCircles,this.options):(this.worker||(this.worker=new Worker("data:text/javascript;base64,dmFyIENvcmVfZGVmYXVsdD17cGFjayhpLHQsZSxzKXtsZXQgcj1lLmxlbmd0aDtjb25zdCBhPVtdLGQ9e3dpZHRoOnQsaGVpZ2h0OmkuZGF0YS5sZW5ndGgvdC80fTtmb3IoO3I+MDspe3ItLTtjb25zdCB0PWVbcl07bGV0IGg9MWUzO2Zvcig7IXQueCYmaC0tID4wOyl7Y29uc3QgZT1NYXRoLnJhbmRvbSgpKmQud2lkdGgscj1NYXRoLnJhbmRvbSgpKmQuaGVpZ2h0O3RoaXMuaXNDaXJjbGVJbnNpZGUoaSxkLndpZHRoLGUscix0LnJhZGl1cyxzLmhpZ2hlckFjY3VyYWN5LHMubWluQWxwaGEpJiYodGhpcy50b3VjaGVzUGxhY2VkQ2lyY2xlKGUscix0LnJhZGl1cyxhLHMuc3BhY2luZyl8fCh0Lng9ZSx0Lnk9cixhLnB1c2godCkpKX19cmV0dXJuIGF9LGlzRmlsbGVkOihpLHQsZSxzLHIpPT4odD1NYXRoLnJvdW5kKHQpLGU9TWF0aC5yb3VuZChlKSxpLmRhdGFbNCoocyplK3QpKzNdPnIpLGlzQ2lyY2xlSW5zaWRlKGksdCxlLHMscixhLGQpe2lmKCF0aGlzLmlzRmlsbGVkKGksZSxzLXIsdCxkKSlyZXR1cm4hMTtpZighdGhpcy5pc0ZpbGxlZChpLGUscytyLHQsZCkpcmV0dXJuITE7aWYoIXRoaXMuaXNGaWxsZWQoaSxlK3Iscyx0LGQpKXJldHVybiExO2lmKCF0aGlzLmlzRmlsbGVkKGksZS1yLHMsdCxkKSlyZXR1cm4hMTtpZihhKXtjb25zdCByPU1hdGguY29zKE1hdGguUEkvNCk7aWYoIXRoaXMuaXNGaWxsZWQoaSxlK3IscytyLHQsZCkpcmV0dXJuITE7aWYoIXRoaXMuaXNGaWxsZWQoaSxlLXIscytyLHQsZCkpcmV0dXJuITE7aWYoIXRoaXMuaXNGaWxsZWQoaSxlLXIscy1yLHQsZCkpcmV0dXJuITE7aWYoIXRoaXMuaXNGaWxsZWQoaSxlK3Iscy1yLHQsZCkpcmV0dXJuITF9cmV0dXJuITB9LHRvdWNoZXNQbGFjZWRDaXJjbGUoaSx0LGUscyxyKXtyZXR1cm4gcy5zb21lKChzPT50aGlzLmRpc3QoaSx0LHMueCxzLnkpPHMucmFkaXVzK2UrcikpfSxkaXN0KGksdCxlLHMpe2NvbnN0IHI9aS1lLGE9dC1zO3JldHVybiBNYXRoLnNxcnQocipyK2EqYSl9fTtzZWxmLm9ubWVzc2FnZT1pPT57cG9zdE1lc3NhZ2UoQ29yZV9kZWZhdWx0LnBhY2soaS5kYXRhLmltYWdlRGF0YSxpLmRhdGEuaW1hZ2VXaWR0aCxpLmRhdGEuc3BhcmVDaXJjbGVzLGkuZGF0YS5vcHRpb25zKSl9Ow==")),a=await new Promise((i=>{this.worker.onmessage=t=>i(t.data),this.worker.postMessage({imageData:t,imageWidth:s,spareCircles:this.spareCircles,options:this.options})})),this.options.reuseWorker||this.worker.terminate()),this.dims={width:s,height:t.data.length/s/4},this.placedCircles=a.reduce(((i,s)=>{const a=this.getCircleColor(t,s.x,s.y);return a&&(s.color=a,i.push(s)),i}),[]),this.placedCircles}asSVGString(){return`<svg width="${this.dims.width}" height="${this.dims.height}" viewBox="0 0 ${this.dims.width} ${this.dims.height}" xmlns="http://www.w3.org/2000/svg">`+(this.options.background?`<rect x="0" y="0" width="${this.dims.width}" height="${this.dims.height}" fill="${this.options.background}" />`:"")+this.placedCircles.map((t=>{const{x:i,y:s,radius:a,color:e}=t;return`<circle cx="${i}" cy="${s}" r="${a}" fill="${e}" />`})).join("")+"</svg>"}asSVG(){const t=this.asSVGString();return(new DOMParser).parseFromString(t,"image/svg+xml").documentElement}asCanvas(t={}){const i=(t={...this.defaultExportOptions,...t}).scale,s=document.createElement("canvas");s.width=this.dims.width*i,s.height=this.dims.height*i;const a=s.getContext("2d");this.options.background&&(a.fillStyle=this.options.background,a.fillRect(0,0,s.width,s.height));for(const t of this.placedCircles){const{x:s,y:e,radius:o,color:n}=t;a.fillStyle=String(n),a.beginPath(),a.arc(Number(s*i),Number(e*i),o*i,0,2*Math.PI),a.closePath(),a.fill()}return s}asImageData(t={}){t={...this.defaultExportOptions,...t};const i=this.asCanvas(t);return i.getContext("2d").getImageData(0,0,i.width,i.height)}async asBlob(t={}){t={...this.defaultExportOptions,...t};const i=this.asCanvas(t);return await new Promise(((s,a)=>{i.toBlob((t=>{t?s(t):a(new Error("Blob creation failed"))}),t.format,t.quality)}))}async asBlobURL(t={}){return URL.createObjectURL(await this.asBlob(t))}asDataURL(t={}){return t={...this.defaultExportOptions,...t},this.asCanvas().toDataURL(t.format,t.quality)}asArray(){return this.placedCircles}};async function a(t,i={}){t=t instanceof URL?t.href:t;const s=document.createElement("img");return await new Promise((i=>{s.onload=i,s.src=t})),await e(s,i)}async function e(t,i={}){const s=document.createElement("canvas");s.width=t.width,s.height=t.height;const a=s.getContext("2d");return a.drawImage(t,0,0),await n(a,i)}async function o(t,i,a={}){const e=new s(a);return await e.pack(t,i),e}async function n(t,i={}){return await o(t.getImageData(0,0,t.canvas.width,t.canvas.height),t.canvas.width,i)}async function r(t=200,i=200,s="black",a={}){const e=document.createElement("canvas");e.width=t,e.height=i;const o=e.getContext("2d");return o.fillStyle=s,o.fillRect(0,0,t,i),await n(o,a)}t.CirclePacker=s,t.fromBlob=async function(t,i={}){const s=URL.createObjectURL(t);return await a(s,i)},t.fromCanvas=async function(t,i={}){const s=t.getContext("2d");return await n(s,i)},t.fromCircle=async function(t=100,i="black",s={}){const a=document.createElement("canvas");a.width=2*t,a.height=2*t;const e=a.getContext("2d");return e.fillStyle=i,e.beginPath(),e.arc(t,t,t,0,2*Math.PI),e.closePath(),e.fill(),await n(e,s)},t.fromContext2D=n,t.fromImage=e,t.fromImageData=o,t.fromRect=r,t.fromSquare=async function(t=200,i="black",s={}){return await r(t,t,i,s)},t.fromURL=a,Object.defineProperty(t,"__esModule",{value:!0})}));