UNPKG

@guinetik/penrose-js

Version:

A JavaScript library for generating beautiful Penrose tilings

1 lines 6.08 kB
class Complex{constructor(o,t=0){this.real=o,this.imag=t}static fromPolar(o,t){return new Complex(o*Math.cos(t),o*Math.sin(t))}add(o){return new Complex(this.real+o.real,this.imag+o.imag)}subtract(o){return new Complex(this.real-o.real,this.imag-o.imag)}multiply(o){return new Complex(this.real*o.real-this.imag*o.imag,this.real*o.imag+this.imag*o.real)}divide(o){return new Complex(this.real/o,this.imag/o)}scale(o){return new Complex(this.real*o,this.imag*o)}abs(){return Math.sqrt(this.real*this.real+this.imag*this.imag)}}const PHI=(Math.sqrt(5)+1)/2;function hexToRgb(o){const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(o);return t?[parseInt(t[1],16)/255,parseInt(t[2],16)/255,parseInt(t[3],16)/255,1]:[0,0,0,1]}function getRandomColor(){let o="#";for(let t=0;t<6;t++)o+="0123456789ABCDEF"[Math.floor(16*Math.random())];return o}function generatePenroseTriangles(o){let t=[];for(let o=0;o<10;o++){const e=Complex.fromPolar(1,(2*o-1)*Math.PI/10),r=Complex.fromPolar(1,(2*o+1)*Math.PI/10);o%2==0?t.push(["thin",new Complex(0),r,e]):t.push(["thin",new Complex(0),e,r])}for(let e=0;e<o;e++){const o=[];for(const[e,r,a,n]of t)if("thin"===e){const t=r.add(a.subtract(r).scale(1/PHI));o.push(["thin",n,t,a]),o.push(["thicc",t,n,r])}else{const t=a.add(r.subtract(a).scale(1/PHI)),e=a.add(n.subtract(a).scale(1/PHI));o.push(["thicc",e,n,r]),o.push(["thicc",t,e,a]),o.push(["thin",e,t,r])}t=o}return t}function calculateLineWidth(o){return o>3?Math.pow(o,-3):Math.pow(o,-5)}const DEFAULT_OPTIONS={divisions:5,zoomType:"in",width:800,height:800,color1:[1,0,0,1],color2:[0,0,1,1],color3:[0,0,0,1],backgroundColor:[1,1,1,1]};class Penrose{constructor(o={}){this.options={...DEFAULT_OPTIONS,...o}}generatePenroseTiling(o){throw new Error("Not implemented - use a concrete subclass")}_processOptions(o){const t={...this.options,...o};return"string"==typeof t.color1&&(t.color1=hexToRgb(t.color1)),"string"==typeof t.color2&&(t.color2=hexToRgb(t.color2)),"string"==typeof t.color3&&(t.color3=hexToRgb(t.color3)),"string"==typeof t.backgroundColor&&(t.backgroundColor=hexToRgb(t.backgroundColor)),t}}class PenroseBitmap extends Penrose{generatePenroseTiling(o){const t=this._processOptions(o),{divisions:e,zoomType:r,width:a,height:n,color1:i,color2:l,color3:s,backgroundColor:h}=t,c=new Uint8ClampedArray(a*n*4);for(let o=0;o<c.length;o+=4)c[o]=255*h[0],c[o+1]=255*h[1],c[o+2]=255*h[2],c[o+3]=255*(h[3]||1);const g="in"===r?1:2,m=Math.max(a,n),f=m/g,u=m/g,d=.5*g,M=.5*g,x=generatePenroseTriangles(e);function p(o){return{x:Math.floor((o.real*f+d*f)*a/m),y:Math.floor((o.imag*u+M*u)*n/m)}}for(const[o,t,e,r]of x){const s=p(t),h=p(e),g=p(r),m="thin"===o?i:l;this._fillTriangle(c,s,h,g,m,a,n)}if(s&&s[3]>0)for(const[o,t,e,r]of x){const o=p(t),i=p(e),l=p(r);this._drawLine(c,o,i,s,a,n),this._drawLine(c,i,l,s,a,n),this._drawLine(c,l,o,s,a,n)}return c}_fillTriangle(o,t,e,r,a,n,i){t.y>e.y&&([t,e]=[e,t]),t.y>r.y&&([t,r]=[r,t]),e.y>r.y&&([e,r]=[r,e]);const l=Math.round(255*a[0]),s=Math.round(255*a[1]),h=Math.round(255*a[2]),c=Math.round(255*(a[3]||1));if(e.y===r.y)this._fillFlatBottomTriangle(o,t,e,r,l,s,h,c,n,i);else if(t.y===e.y)this._fillFlatTopTriangle(o,t,e,r,l,s,h,c,n,i);else{const a={x:Math.floor(t.x+(e.y-t.y)/(r.y-t.y)*(r.x-t.x)),y:e.y};this._fillFlatBottomTriangle(o,t,e,a,l,s,h,c,n,i),this._fillFlatTopTriangle(o,e,a,r,l,s,h,c,n,i)}}_fillFlatBottomTriangle(o,t,e,r,a,n,i,l,s,h){const c=(e.x-t.x)/(e.y-t.y||1),g=(r.x-t.x)/(r.y-t.y||1);let m=t.x,f=t.x;for(let r=t.y;r<=e.y;r++){if(r>=0&&r<h){const t=Math.max(0,Math.min(Math.floor(m),s-1)),e=Math.max(0,Math.min(Math.floor(f),s-1));for(let h=Math.min(t,e);h<=Math.max(t,e);h++){const t=4*(r*s+h);t>=0&&t<o.length-3&&(o[t]=a,o[t+1]=n,o[t+2]=i,o[t+3]=l)}}m+=c,f+=g}}_fillFlatTopTriangle(o,t,e,r,a,n,i,l,s,h){const c=(r.x-t.x)/(r.y-t.y||1),g=(r.x-e.x)/(r.y-e.y||1);let m=r.x,f=r.x;for(let e=r.y;e>t.y;e--)if(e>=0&&e<h){m-=c,f-=g;const t=Math.max(0,Math.min(Math.floor(m),s-1)),r=Math.max(0,Math.min(Math.floor(f),s-1));for(let h=Math.min(t,r);h<=Math.max(t,r);h++){const t=4*(e*s+h);t>=0&&t<o.length-3&&(o[t]=a,o[t+1]=n,o[t+2]=i,o[t+3]=l)}}}_drawLine(o,t,e,r,a,n){const i=Math.round(255*r[0]),l=Math.round(255*r[1]),s=Math.round(255*r[2]),h=Math.round(255*(r[3]||1));let c=t.x,g=t.y,m=e.x,f=e.y;const u=Math.abs(m-c),d=Math.abs(f-g),M=c<m?1:-1,x=g<f?1:-1;let p=u-d;for(;;){if(c>=0&&c<a&&g>=0&&g<n){const t=4*(g*a+c);if(t>=0&&t<o.length-3){const e=h/255;o[t]=Math.round(o[t]*(1-e)+i*e),o[t+1]=Math.round(o[t+1]*(1-e)+l*e),o[t+2]=Math.round(o[t+2]*(1-e)+s*e),o[t+3]=255}}if(c===m&&g===f)break;const t=2*p;t>-d&&(p-=d,c+=M),t<u&&(p+=u,g+=x)}}}class PenroseCanvas extends Penrose{generatePenroseTiling(o){if(!o.ctx)throw new Error("Canvas context (ctx) is required for PenroseCanvas");const t=this._processOptions(o),{ctx:e,divisions:r,zoomType:a,width:n,height:i,color1:l,color2:s,color3:h,backgroundColor:c}=t;e.setTransform(1,0,0,1,0,0),e.clearRect(0,0,n,i),c&&(e.fillStyle=`rgba(${Math.round(255*c[0])}, ${Math.round(255*c[1])}, ${Math.round(255*c[2])}, ${c[3]||1})`,e.fillRect(0,0,n,i));const g="in"===a?1:2,m=Math.max(n,i);e.scale(m/g,m/g),e.translate(.5*g,.5*g);const f=generatePenroseTriangles(r);e.beginPath();for(const[o,t,r,a]of f)"thin"===o&&(e.moveTo(t.real,t.imag),e.lineTo(r.real,r.imag),e.lineTo(a.real,a.imag),e.closePath());e.fillStyle=`rgba(${Math.round(255*l[0])}, ${Math.round(255*l[1])}, ${Math.round(255*l[2])}, ${l[3]||1})`,e.fill(),e.beginPath();for(const[o,t,r,a]of f)"thicc"===o&&(e.moveTo(t.real,t.imag),e.lineTo(r.real,r.imag),e.lineTo(a.real,a.imag),e.closePath());e.fillStyle=`rgba(${Math.round(255*s[0])}, ${Math.round(255*s[1])}, ${Math.round(255*s[2])}, ${s[3]||1})`,e.fill(),e.beginPath();for(const[o,t,r,a]of f)e.moveTo(r.real,r.imag),e.lineTo(t.real,t.imag),e.lineTo(a.real,a.imag);e.strokeStyle=`rgba(${Math.round(255*h[0])}, ${Math.round(255*h[1])}, ${Math.round(255*h[2])}, ${h[3]||1})`;const u=calculateLineWidth(r);return e.lineWidth=u,e.lineJoin="round",e.stroke(),e.setTransform(1,0,0,1,0,0),e}}export{Complex,DEFAULT_OPTIONS,PHI,Penrose,PenroseBitmap,PenroseCanvas,getRandomColor,hexToRgb};