Code adapted for Turtletoy from github.com/isohedral/hatviz
An aperiodic monotile
David Smith, Joseph Samuel Myers, Craig S. Kaplan, and Chaim Goodman-Strauss, 2023
cs.uwaterloo.ca/~csk/hat/
Log in to post a comment.
// Forked from "Aperiodic Hat Monotiles" by reinder // https://turtletoy.net/turtle/61a52c764a // Aperiodic Hat Monotiles. Created by Reinder Nijhoff 2023 // @reindernijhoff // // https://turtletoy.net/turtle/61a52c764a // // Code adapted from: https://github.com/isohedral/hatviz // BSD 3-Clause License // Copyright (c) 2023, Craig S. Kaplan // const turtle = new Turtle(); //const levels = 3; // min=1, max=6, step=1 const levels = 1; // min=1, max=6, step=1 //const scale = 6; // min=0.5, max=10, step=0.001 const scale = 10; // min=0.5, max=10, step=0.001 const draw = 1; // min=1, max=3, step=1 (Draw Hats, Draw Supertiles, Draw All) const hatching = 1; // min=0, max=1, step=1 (No, Yes) const t_morph = 2; // min=0, max=6, step=.01 const polygons = new Polygons(); const iterator = createEinsteinTiling(levels, draw, scale); function walk(i) { const d = iterator.next(); if (!d.done) { const v = d.value; const poly = polygons.create(); poly.addPoints(...v.polygon); poly.addOutline(); if (hatching && v.geom.children.length === 0) { switch(v.geom.fill) { case 0: poly.addHatching( .8,1.0); break; case 1: poly.addHatching(-.8,0.7); break; case 2: poly.addHatching( .1,0.3); break; } } polygons.draw(turtle, poly, false); return true; } else { return false; } } // // An aperiodic monotile // David Smith, Joseph Samuel Myers, Craig S. Kaplan, and Chaim Goodman-Strauss, 2023 // https://cs.uwaterloo.ca/~csk/hat/ // // Code adapted from: https://github.com/isohedral/hatviz // BSD 3-Clause License // Copyright (c) 2023, Craig S. Kaplan // function* createEinsteinTiling(levels, draw = 1, scale = 1) { const r3 = Math.sqrt(3); const hr3 = r3 / 2; const ident = [1, 0, 0, 0, 1, 0]; const scl2 = (a,b) => [a[0]*b, a[1]*b]; const add2 = (a,b) => [a[0]+b[0], a[1]+b[1]]; const sub2 = (a,b) => [a[0]-b[0], a[1]-b[1]]; const rot2 = (a) => [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; const hexPt = (x, y) => [x + 0.5 * y, hr3 * y]; const intersect = (p1, q1, p2, q2) => { const d = (q2[1] - p2[1]) * (q1[0] - p1[0]) - (q2[0] - p2[0]) * (q1[1] - p1[1]); const uA = ((q2[0] - p2[0]) * (p1[1] - p2[1]) - (q2[1] - p2[1]) * (p1[0] - p2[0])) / d; return [p1[0] + uA * (q1[0] - p1[0]), p1[1] + uA * (q1[1] - p1[1])]; } // Affine matrix inverse const inv = (T) => { const det = T[0] * T[4] - T[1] * T[3]; return [ T[4] / det, -T[1] / det, (T[1] * T[5] - T[2] * T[4]) / det, -T[3] / det, T[0] / det, (T[2] * T[3] - T[0] * T[5]) / det ]; } // Affine matrix multiply const mul = (A, B) => [ A[0] * B[0] + A[1] * B[3], A[0] * B[1] + A[1] * B[4], A[0] * B[2] + A[1] * B[5] + A[2], A[3] * B[0] + A[4] * B[3], A[3] * B[1] + A[4] * B[4], A[3] * B[2] + A[4] * B[5] + A[5] ]; // Rotation matrix const trot = (a) => [Math.cos(a), -Math.sin(a), 0, Math.sin(a), Math.cos(a), 0]; // Translation matrix const ttrans = (p) => [1, 0, p[0], 0, 1, p[1]]; const rotAbout = (p, ang) => mul(ttrans(p), mul(trot(ang), ttrans([-p[0], -p[1]]))); // Matrix * point const transPt = (M, P) => [M[0] * P[0] + M[1] * P[1] + M[2], M[3] * P[0] + M[4] * P[1] + M[5]]; // Match unit interval to line segment p->q const matchSeg = (p, q) => [q[0] - p[0], p[1] - q[1], p[0], q[1] - p[1], q[0] - p[0], p[1]]; // Match line segment p1->q1 to line segment p2->q2 const matchTwo = (p1, q1, p2, q2) => mul(matchSeg(p2, q2), inv(matchSeg(p1, q1))); class Geom { constructor(pgon, fill = 0) { this.shape = pgon; this.fill = fill this.width = 1.0; this.children = []; } addChild(T, geom) { this.children.push({ T: T, geom: geom }); } evalChild(n, i) { return transPt(this.children[n].T, this.children[n].geom.shape[i]); } recentre() { let tr = [0, 0]; this.shape.forEach( p => tr = add2(tr, p)); tr = scl2( tr, -1 / this.shape.length); this.shape = this.shape.map (p => add2(p, tr)); const M = ttrans(tr); for (let ch of this.children) { ch.T = mul(M, ch.T); } } } //// START ADDED CODE function morph_tile_outline(morph_t = 2) { const hat_outline = [ hexPt(0, 0), hexPt(-1, -1), hexPt(0, -2), hexPt(2, -2), hexPt(2, -1), hexPt(4, -2), hexPt(5, -1), hexPt(4, 0), hexPt(3, 0), hexPt(2, 2), hexPt(0, 3), hexPt(0, 2), hexPt(-1, 2) ]; // criteria taken from figure 2.3 in https://arxiv.org/pdf/2303.10798.pdf // and it's also mentioned in https://isohedral.ca/aperiodic-monotiles/ // as https://isohedral.ca/wp-content/uploads/2023/12/tile_ab-1024x188.png const criteria = [ [0, 1], //t_morph = 0 Chevron [1, 4], //t_morph = 1 [1, 3**.5], //t_morph = 2 Hat [1, 1], //t_morph = 3 [3**.5, 1], //t_morph = 4 Turtle [4, 1], //t_morph = 5 [1, 0], //t_morph = 6 Comet ]; const lerp2 = (a,b,t) => a.map((v, i) => v*(1-t) + b[i]*t); const startIndex = t_morph== 6? criteria.length - 2: ((t_morph * (criteria.length - 1) / 6)|0); const endIndex = t_morph== 6? criteria.length - 1: ((t_morph * (criteria.length - 1) / 6)|0) + 1; const [a, b] = lerp2(criteria[startIndex], criteria[endIndex], t_morph == 6? 1: (t_morph * (criteria.length - 1) / 6) % 1); const tile_edgetypes = [b, a, 2 * a, a, b, b, a, a, b, b, a, a, b]; const tile_outline = hat_outline.map((e,i,a) => sub2(a[(i+1)%a.length], e)) //map hat_outline vertices to vectors representing vectors from vertice to next vertice .map((e,i) => scl2(e, tile_edgetypes[i] / Math.hypot(...e))) //scale each edge to ratio in tile_edgetypes .reduce((a, c) => [...a, add2(a[a.length-1], c)], [[0,0]]); //and map those scaled edges back to vertices tile_outline.pop(); // conversion to vectors and back causes last point identical to start point to be added to tile_outline const areaScalar = getVerticeScalarByArea(getPolgyonArea(tile_outline), getPolgyonArea(hat_outline)); return tile_outline.map(pt => scl2(pt, areaScalar)); } //Shoelace https://en.wikipedia.org/wiki/Shoelace_formula function getPolgyonArea(vertices) { const shoelace = (a, b) => a[0] * b[1] - a[1] * b [0]; return vertices.reduce((a, c, i) => a + shoelace(c, vertices[(i+1)%vertices.length]), 0); } function getVerticeScalarByArea(area, normalizeToArea = 1) { return (normalizeToArea / Math.abs(area))**.5; } function getPolygonCenter(polygon) { const minmax = polygon.reduce((a, c) => [[Math.min(c[0], a[0][0]), Math.max(c[0], a[0][1])], [Math.min(c[1], a[1][0]), Math.max(c[1], a[1][1])]], [[Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER], [Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER]]); return [minmax[0][0] + (minmax[0][1] - minmax[0][0])/2, minmax[1][0] + (minmax[1][1] - minmax[1][0])/2]; } const hat_outline = morph_tile_outline(t_morph); //// END ADDED CODE /* const hat_outline = [ hexPt(0, 0), hexPt(-1, -1), hexPt(0, -2), hexPt(2, -2), hexPt(2, -1), hexPt(4, -2), hexPt(5, -1), hexPt(4, 0), hexPt(3, 0), hexPt(2, 2), hexPt(0, 3), hexPt(0, 2), hexPt(-1, 2) ]; */ const H1_hat = new Geom(hat_outline, 0); const H_hat = new Geom(hat_outline, 1); const T_hat = new Geom(hat_outline, 2); const P_hat = new Geom(hat_outline, 3); const F_hat = new Geom(hat_outline, 4); function constructPatch(H, T, P, F) { const rules = [ ['H'], [0, 0, 'P', 2], [1, 0, 'H', 2], [2, 0, 'P', 2], [3, 0, 'H', 2], [4, 4, 'P', 2], [0, 4, 'F', 3], [2, 4, 'F', 3], [4, 1, 3, 2, 'F', 0], [8, 3, 'H', 0], [9, 2, 'P', 0], [10, 2, 'H', 0], [11, 4, 'P', 2], [12, 0, 'H', 2], [13, 0, 'F', 3], [14, 2, 'F', 1], [15, 3, 'H', 4], [8, 2, 'F', 1], [17, 3, 'H', 0], [18, 2, 'P', 0], [19, 2, 'H', 2], [20, 4, 'F', 3], [20, 0, 'P', 2], [22, 0, 'H', 2], [23, 4, 'F', 3], [23, 0, 'F', 3], [16, 0, 'P', 2], [9, 4, 0, 2, 'T', 2], [4, 0, 'F', 3] ]; ret = new Geom([], null, null); ret.width = H.width; shapes = { 'H': H, 'T': T, 'P': P, 'F': F }; for (let r of rules) { if (r.length == 1) { ret.addChild(ident, shapes[r[0]]); } else if (r.length == 4) { const poly = ret.children[r[0]].geom.shape; const T = ret.children[r[0]].T; const P = transPt(T, poly[(r[1] + 1) % poly.length]); const Q = transPt(T, poly[r[1]]); const nshp = shapes[r[2]]; const npoly = nshp.shape; ret.addChild(matchTwo(npoly[r[3]], npoly[(r[3] + 1) % npoly.length], P, Q), nshp); } else { const chP = ret.children[r[0]]; const chQ = ret.children[r[2]]; const P = transPt(chQ.T, chQ.geom.shape[r[3]]); const Q = transPt(chP.T, chP.geom.shape[r[1]]); const nshp = shapes[r[4]]; const npoly = nshp.shape; ret.addChild(matchTwo(npoly[r[5]], npoly[(r[5] + 1) % npoly.length], P, Q), nshp); } } return ret; } function constructMetatiles(patch) { const bps1 = patch.evalChild(8, 2); const bps2 = patch.evalChild(21, 2); const rbps = transPt(rotAbout(bps1, -2.0 * Math.PI / 3.0), bps2); const p72 = patch.evalChild(7, 2); const p252 = patch.evalChild(25, 2); const llc = intersect(bps1, rbps, patch.evalChild(6, 2), p72); let w = sub2(patch.evalChild(6, 2), llc); const new_H_outline = [llc, bps1]; w = transPt(trot(-Math.PI / 3), w); new_H_outline.push(add2(new_H_outline[1], w)); new_H_outline.push(patch.evalChild(14, 2)); w = transPt(trot(-Math.PI / 3), w); new_H_outline.push(sub2(new_H_outline[3], w)); new_H_outline.push(patch.evalChild(6, 2)); const new_H = new Geom(new_H_outline); new_H.width = patch.width * 2; for (let ch of [0, 9, 16, 27, 26, 6, 1, 8, 10, 15]) { new_H.addChild(patch.children[ch].T, patch.children[ch].geom); } const new_P_outline = [p72, add2(p72, sub2(bps1, llc)), bps1, llc]; const new_P = new Geom(new_P_outline); new_P.width = patch.width * 2; for (let ch of [7, 2, 3, 4, 28]) { new_P.addChild(patch.children[ch].T, patch.children[ch].geom); } const new_F_outline = [bps2, patch.evalChild(24, 2), patch.evalChild(25, 0), p252, add2(p252, sub2(llc, bps1))]; const new_F = new Geom(new_F_outline); new_F.width = patch.width * 2; for (let ch of [21, 20, 22, 23, 24, 25]) { new_F.addChild(patch.children[ch].T, patch.children[ch].geom); } const AAA = new_H_outline[2]; const BBB = add2(new_H_outline[1], sub2(new_H_outline[4], new_H_outline[5])); const CCC = transPt(rotAbout(BBB, -Math.PI / 3), AAA); const new_T_outline = [BBB, CCC, AAA]; const new_T = new Geom(new_T_outline); new_T.width = patch.width * 2; new_T.addChild(patch.children[11].T, patch.children[11].geom); new_H.recentre(); new_P.recentre(); new_F.recentre(); new_T.recentre(); return [new_H, new_T, new_P, new_F] } // init start tiles const H_outline = [[0, 0], [4, 0], [4.5, hr3], [2.5, 5 * hr3], [1.5, 5 * hr3], [-0.5, hr3]]; const H_init = new Geom(H_outline); H_init.width = 2; H_init.addChild(matchTwo(hat_outline[5], hat_outline[7], H_outline[5], H_outline[0]), H_hat); H_init.addChild(matchTwo(hat_outline[9], hat_outline[11], H_outline[1], H_outline[2]), H_hat); H_init.addChild(matchTwo(hat_outline[5], hat_outline[7], H_outline[3], H_outline[4]), H_hat); H_init.addChild(mul(ttrans([2.5, hr3]), mul([-0.5, -hr3, 0, hr3, -0.5, 0], [0.5, 0, 0, 0, -0.5, 0])), H1_hat); const T_outline = [[0, 0], [3, 0], [1.5, 3 * hr3]]; const T_init = new Geom(T_outline); T_init.width = 2; T_init.addChild([0.5, 0, 0.5, 0, 0.5, hr3], T_hat); const P_outline = [[0, 0], [4, 0], [3, 2 * hr3], [-1, 2 * hr3]]; const P_init = new Geom(P_outline); P_init.width = 2; P_init.addChild([0.5, 0, 1.5, 0, 0.5, hr3], P_hat); P_init.addChild(mul(ttrans([0, 2 * hr3]), mul([0.5, hr3, 0, -hr3, 0.5, 0], [0.5, 0.0, 0.0, 0.0, 0.5, 0.0])), P_hat); const F_outline = [[0, 0], [3, 0], [3.5, hr3], [3, 2 * hr3], [-1, 2 * hr3]]; const F_init = new Geom(F_outline); F_init.width = 2; F_init.addChild([0.5, 0, 1.5, 0, 0.5, hr3], F_hat); F_init.addChild(mul(ttrans([0, 2 * hr3]), mul([0.5, hr3, 0, -hr3, 0.5, 0], [0.5, 0.0, 0.0, 0.0, 0.5, 0.0])), F_hat); let tiles = [H_init, T_init, P_init, F_init]; // create all tiles for level for (let i = 0; i < levels; i++) { const patch = constructPatch(...tiles); tiles = constructMetatiles(patch); } // generator code const queue = [{ T: [scale, 0, 0, 0, scale, 0], geom: tiles[0], level: levels }]; do { const t = queue.pop(); if (t.level >= 0) { for (let g of t.geom.children) { queue.push({ T: mul(t.T, g.T), geom: g.geom, level: t.level - 1 }); } } if ( (t.level < 0 && draw == 1) || (t.level >= 0 && draw == 2) || draw == 3) { yield { geom: t.geom, polygon: t.geom.shape.map(p => transPt(t.T, p)) }; } } while (queue.length > 0); } //////////////////////////////////////////////////////////////// // Polygon Clipping utility code - Created by Reinder Nijhoff 2019 // (Polygon binning by Lionel Lemarie 2021) // https://turtletoy.net/turtle/a5befa1f8d //////////////////////////////////////////////////////////////// function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t,s){const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++)e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]),e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);e.boolean(this,!1),this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{reducedPolygonList=function(n){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h)}}}