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)}}}