diff --git a/index.html b/index.html index 86c671f..f031783 100644 --- a/index.html +++ b/index.html @@ -1,28 +1,44 @@ + Aperiodic monotile - + @@ -30,7 +46,7 @@ textarea {
Viewbox - +
-
- Colours - -
-
- Tiling + +
- - diff --git a/script.js b/script.js index b8147dd..f883d7e 100644 --- a/script.js +++ b/script.js @@ -3,131 +3,135 @@ const {PI, cos, sin} = Math; -const ident = [1,0,0,0,1,0]; - function radians(degrees) { return degrees * PI / 180; } -let to_screen = [20, 0, 0, 0, -20, 0]; -let lw_scale = 1; +let num_pieces = 0; -let sys; +class Point { + constructor(x,y) { + this.x = x; + this.y = y; + } -let scale_centre; -let scale_start; -let scale_ts; + add( q ) { + return { x : this.x + q.x, y : this.y + q.y }; + } -let reset_but; -let tile_sel; -let shape_sel; -let colscheme_sel; + sub( q ) { + return { x : this.x - q.x, y : this.y - q.y }; + } -let subst_button; -let translate_button; -let scale_button; -let dragging = false; -let uibox = true; + frame( p, q, a, b ) { + return{ x : this.x + a*p.x + b*q.x, y : this.y + a*p.y + b*q.y }; + } + +} + +class Matrix { + constructor(mat) { + this.mat = mat; + } + + determinant() { + const T = this.mat; + const det = T[0]*T[4] - T[1]*T[3]; + } + + inverse() { + const T = this.mat; + const det = this.determinant(); + return new Matrix([ + 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 + ]); + } + + mul( other ) { + const A = this.mat; + const B = other.mat; + return new Matrix([ + 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 + static rotation( ang ) { + const c = cos( ang ); + const s = sin( ang ); + return new Matrix([c, -s, 0, s, c, 0]); + } + + static scale(x,y) { + return new Matrix([x,0,0,0,y,0]); + } + + static translate( tx, ty ) { + return new Matrix([1, 0, tx, 0, 1, ty]); + } + + // Translation matrix moving p to q + static translateTo( p, q ) { + return Matrix.translate( q.x - p.x, q.y - p.y ); + } + + transform( P ) { + const M = this.mat; + return new Point(M[0]*P.x + M[1]*P.y + M[2], M[3]*P.x + M[4]*P.y + M[5]); + } + +} + +const ident = new Matrix([1,0,0,0,1,0]); const spectre = [ - pt(0, 0), - pt(1.0, 0.0), - pt(1.5, -0.8660254037844386), - pt(2.366025403784439, -0.36602540378443865), - pt(2.366025403784439, 0.6339745962155614), - pt(3.366025403784439, 0.6339745962155614), - pt(3.866025403784439, 1.5), - pt(3.0, 2.0), - pt(2.133974596215561, 1.5), - pt(1.6339745962155614, 2.3660254037844393), - pt(0.6339745962155614, 2.3660254037844393), - pt(-0.3660254037844386, 2.3660254037844393), - pt(-0.866025403784439, 1.5), - pt(0.0, 1.0) + new Point(0, 0), + new Point(1.0, 0.0), + new Point(1.5, -0.8660254037844386), + new Point(2.366025403784439, -0.36602540378443865), + new Point(2.366025403784439, 0.6339745962155614), + new Point(3.366025403784439, 0.6339745962155614), + new Point(3.866025403784439, 1.5), + new Point(3.0, 2.0), + new Point(2.133974596215561, 1.5), + new Point(1.6339745962155614, 2.3660254037844393), + new Point(0.6339745962155614, 2.3660254037844393), + new Point(-0.3660254037844386, 2.3660254037844393), + new Point(-0.866025403784439, 1.5), + new Point(0.0, 1.0) ]; const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]]; - -const tile_names = [ - 'Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi' ]; - -const svg_point = ({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`; - -function lerp(a,b,t) { - return t*b + (1-t)*a; +function getsvg(event) { + let t = event.target; + while(t && t.tagName.toLowerCase()!='svg') { + t = t.parentElement; + } + return t; } -function pt( x, y ) -{ - return { x : x, y : y }; -} - -// Affine matrix inverse -function 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 -function mul( A, B ) -{ - return [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]]; -} - -function padd( p, q ) -{ - return { x : p.x + q.x, y : p.y + q.y }; -} - -function psub( p, q ) -{ - return { x : p.x - q.x, y : p.y - q.y }; -} - -function pframe( o, p, q, a, b ) -{ - return { x : o.x + a*p.x + b*q.x, y : o.y + a*p.y + b*q.y }; -} - -// Rotation matrix -function trot( ang ) -{ - const c = cos( ang ); - const s = sin( ang ); - return [c, -s, 0, s, c, 0]; -} - -//Scale matrix -function tscale(x,y) { - return [x,0,0,0,y,0]; -} - -// Translation matrix -function ttrans( tx, ty ) -{ - return [1, 0, tx, 0, 1, ty]; -} - -// Translation matrix moving p to q -function transTo( p, q ) -{ - return ttrans( q.x - p.x, q.y - p.y ); -} - - -// Matrix * point -function transPt( M, P ) -{ - return pt(M[0]*P.x + M[1]*P.y + M[2], M[3]*P.x + M[4]*P.y + M[5]); +function getcoords(event) { + const t = getsvg(event); + if(!t) { + return; + } + const point = t.createSVGPoint() + point.x = event.clientX + point.y = event.clientY + const position = point.matrixTransform(t.getScreenCTM().inverse()) + return position; } // Match unit interval to line segment p->q @@ -139,16 +143,14 @@ function matchSeg( p, q ) // Match line segment p1->q1 to line segment p2->q2 function matchTwo( p1, q1, p2, q2 ) { - return mul( matchSeg( p2, q2 ), inv( matchSeg( p1, q1 ) ) ); + return matchSeg( p2, q2 ).mul(matchSeg( p1, q1 ).inverse()); }; function drawPolygon( shape, T, f, s, w ) { - console.log(shape,T,f,s,w); - beginShape(); for( let p of shape ) { - const tp = transPt( T, p ); + const tp = T.transform( p ); vertex( tp.x, tp.y ); } endShape( CLOSE ); @@ -160,56 +162,32 @@ class Shape this.pts = pts; this.quad = quad; - let blah = true; - this.pts = [pts[pts.length-1]]; for( const p of pts ) { const prev = this.pts[this.pts.length-1]; - const v = psub( p, prev ); - const w = pt( -v.y, v.x ); - if( blah ) { - this.pts.push( pframe( prev, v, w, 0.33, 0.6 ) ); - this.pts.push( pframe( prev, v, w, 0.67, 0.6 ) ); - } else { - this.pts.push( pframe( prev, v, w, 0.33, -0.6 ) ); - this.pts.push( pframe( prev, v, w, 0.67, -0.6 ) ); - } - blah = !blah; + const v = p.sub(prev); + const w = new Point( -v.y, v.x ); + this.pts.push( prev.frame(v, w, 0.5, -0.3 ) ); + this.pts.push( prev.frame(v, w, 0.5, 0.3 ) ); this.pts.push( p ); } } - streamSVG( S, stream ) - { - const tpts = this.pts.map(p => transPt( S, p )); + streamSVG( S, stream ) { + const tpts = this.pts.map(p => S.transform( p )); - const [a,c,e,b,d,f] = S; + const [a,c,e,b,d,f] = S.mat; const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3)); - //const s = ``; - - const points = this.pts.map(({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`).join(' '); - const tp = this.pts[0]; - - var s = ``; - stream.push( s ); + num_pieces += 1; + stream.push(` + +${num_pieces} + +`); } bounds(S) { - const points = this.pts.map(p => transPt(S,p)); + const points = this.pts.map(p => S.transform(p)); return { minx: Math.min(...points.map(p => p.x)), miny: Math.min(...points.map(p => p.y)), @@ -219,46 +197,39 @@ fill="currentColor" />`; } * flatten(S) { - const points = this.pts.map(p => transPt(S,p)); + const points = this.pts.map(p => S.transform(p)); const ymax = Math.max(...points.map(p => p.y)); yield {points, ymax, shape: this}; } } -class Meta -{ - constructor() - { +class Meta { + constructor() { this.geoms = []; this.quad = []; } - addChild( g, T ) - { + addChild( g, T ) { this.geoms.push( { geom : g, xform: T } ); } - draw( S ) - { + draw( S ) { for( let g of this.geoms ) { - g.geom.draw( mul( S, g.xform ) ); + g.geom.draw( S.mul( g.xform ) ); } } - streamSVG( S, stream ) - { + streamSVG( S, stream ) { const {minx,miny,maxx,maxy} = this.bounds(S); - const [a,c,e,b,d,f] = S; - const matS = [a,b,c,d,e,f];//.map(p=>p.toFixed(3)); for( let g of this.geoms ) { - g.geom.streamSVG( mul(S,g.xform), stream ); + g.geom.streamSVG( S.mul(g.xform), stream ); } } bounds(S) { - const sub_bounds = this.geoms.map(g => g.geom.bounds(mul(S,g.xform))); + const sub_bounds = this.geoms.map(g => g.geom.bounds(S.mul(g.xform))); return { minx: Math.min(...sub_bounds.map(b=>b.minx)), miny: Math.min(...sub_bounds.map(b=>b.miny)), @@ -269,7 +240,7 @@ class Meta * flatten(S) { for(let g of this.geoms) { - yield* g.geom.flatten(mul(S, g.xform)); + yield* g.geom.flatten(S.mul(g.xform)); } } } @@ -294,7 +265,7 @@ function tiles(level, label) { case 'Gamma': const mystic = new Meta(); out.push(ident); - out.push(mul( ttrans( spectre[8].x, spectre[8].y ), trot( PI / 6 ) )); + out.push(Matrix.translate( spectre[8].x, spectre[8].y ).mul( Matrix.rotation( PI / 6 ) )); } } else { /* @@ -307,19 +278,33 @@ function tiles(level, label) { * The layout of subtiles depends on the larger tile. * */ - const labels = ['Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi']; + const labels = [ + 'Delta', + 'Theta', + 'Lambda', + 'Xi', + 'Pi', + 'Sigma', + 'Phi', + 'Psi' + ]; const sublevels = Object.fromEntries(labels.map(label => tiles(level-1, label))); const subquad = sublevels['Delta'].quad; - const reflection = tscale(-1,1); + const reflection = Matrix.scale(-1,1); // How to get from each subtile to the next. const t_rules = [ - [60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1], - [0, 2, 0], [60, 3, 1], [-120, 3, 3] ]; + [60, 3, 1], + [0, 2, 0], + [60, 3, 1], + [60, 3, 1], + [0, 2, 0], + [60, 3, 1], + [-120, 3, 3] + ]; let Ts = [ident]; @@ -329,12 +314,12 @@ function tiles(level, label) { for( const [ang,from,to] of t_rules ) { total_ang += ang; if( ang != 0 ) { - rot = trot( radians( total_ang ) ); - tquad = subquad.map(q => transPt(rot,q)); + rot = Matrix.rotation( radians( total_ang ) ); + tquad = subquad.map(q => rot.transform(q)); } - const ttt = transTo( tquad[to], transPt( Ts[Ts.length-1], subquad[from] ) ); - Ts.push( mul( ttt, rot ) ); + const ttt = Matrix.translateTo( tquad[to], Ts[Ts.length-1].transform( subquad[from] ) ); + Ts.push( ttt.mul( rot ) ); } Ts = Ts.map(t => mul(reflection, t)); @@ -350,12 +335,13 @@ function tiles(level, label) { 'Pi' : ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'], 'Sigma' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'], 'Phi' : ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','Gamma'], - 'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] }; + 'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] + }; const super_quad = [ - transPt( Ts[6], subquad[2] ), - transPt( Ts[5], subquad[1] ), - transPt( Ts[3], subquad[2] ), - transPt( Ts[0], subquad[1] ) ]; + Ts[6].transform(subquad[2] ), + Ts[5].transform(subquad[1] ), + Ts[3].transform(subquad[2] ), + Ts[0].transform(subquad[1] ) ]; } return {quad, tiles: out}; } @@ -373,7 +359,7 @@ function buildSpectreBase() const mystic = new Meta(); mystic.addChild( new Shape( spectre, base_quad, 'Gamma1' ), ident ); mystic.addChild( new Shape( spectre, base_quad, 'Gamma2' ), - mul( ttrans( spectre[8].x, spectre[8].y ), trot( PI / 6 ) ) ); + Matrix.translate( spectre[8].x, spectre[8].y ).mul( Matrix.rotation( PI / 6 ) ) ); mystic.quad = base_quad; ret['Gamma'] = mystic; @@ -385,14 +371,12 @@ function buildHatTurtleBase( hat_dominant ) const r3 = 1.7320508075688772; const hr3 = 0.8660254037844386; - function hexPt( x, y ) - { - return pt( x + 0.5*y, -hr3*y ); + function hexPt( x, y ) { + return new Point( x + 0.5*y, -hr3*y ); } - function hexPt2( x, y ) - { - return pt( x + hr3*y, -0.5*y ); + function hexPt2( x, y ) { + return new Point( x + hr3*y, -0.5*y ); } const hat = [ @@ -424,7 +408,7 @@ function buildHatTurtleBase( hat_dominant ) const mystic = new Meta(); mystic.addChild( new Shape( hat, hat_keys, 'Gamma1' ), ident ); mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma2' ), - ttrans( hat[8].x, hat[8].y ) ); + Matrix.translate( hat[8].x, hat[8].y ) ); mystic.quad = hat_keys; ret['Gamma'] = mystic; } else { @@ -436,7 +420,7 @@ function buildHatTurtleBase( hat_dominant ) const mystic = new Meta(); mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma1' ), ident ); mystic.addChild( new Shape( hat, hat_keys, 'Gamma2' ), - mul( ttrans( turtle[9].x, turtle[9].y ), trot( PI/3 ) ) ); + Matrix.translate( turtle[9].x, turtle[9].y ).mul( Matrix.rotation( PI/3 ) ) ); mystic.quad = turtle_keys; ret['Gamma'] = mystic; } @@ -449,12 +433,12 @@ function buildHexBase() const hr3 = 0.8660254037844386; const hex = [ - pt(0, 0), - pt(1.0, 0.0), - pt(1.5, hr3), - pt(1, 2*hr3), - pt(0, 2*hr3), - pt(-0.5, hr3) + new Point(0, 0), + new Point(1.0, 0.0), + new Point(1.5, hr3), + new Point(1, 2*hr3), + new Point(0, 2*hr3), + new Point(-0.5, hr3) ]; const hex_keys = [ hex[1], hex[2], hex[3], hex[5] ]; @@ -476,7 +460,7 @@ function buildSupertiles( sys ) // supertiles. const quad = sys['Delta'].quad; - const R = [-1,0,0,0,1,0]; + const R = new Matrix([-1,0,0,0,1,0]); const t_rules = [ [60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1], @@ -489,19 +473,19 @@ function buildSupertiles( sys ) for( const [ang,from,to] of t_rules ) { total_ang += ang; if( ang != 0 ) { - rot = trot( radians( total_ang ) ); + rot = Matrix.rotation( radians( total_ang ) ); for( i = 0; i < 4; ++i ) { - tquad[i] = transPt( rot, quad[i] ); + tquad[i] = rot.transform(quad[i] ); } } - const ttt = transTo( tquad[to], - transPt( Ts[Ts.length-1], quad[from] ) ); - Ts.push( mul( ttt, rot ) ); + const ttt = Matrix.translateTo( tquad[to], + Ts[Ts.length-1].transform(quad[from] ) ); + Ts.push( ttt.mul( rot ) ); } for( let idx = 0; idx < Ts.length; ++idx ) { - Ts[idx] = mul( R, Ts[idx] ); + Ts[idx] = R.mul( Ts[idx] ); } // Now build the actual supertiles, labelling appropriately. @@ -516,10 +500,10 @@ function buildSupertiles( sys ) 'Phi' : ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','Gamma'], 'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] }; const super_quad = [ - transPt( Ts[6], quad[2] ), - transPt( Ts[5], quad[1] ), - transPt( Ts[3], quad[2] ), - transPt( Ts[0], quad[1] ) ]; + Ts[6].transform(quad[2] ), + Ts[5].transform(quad[1] ), + Ts[3].transform(quad[2] ), + Ts[0].transform(quad[1] ) ]; const ret = {}; @@ -575,16 +559,13 @@ function hexToHSL(H) { let last_num_iterations; function get_settings() { - Array.from(document.querySelectorAll('input[type="range"]')).forEach(i => { - const o = document.querySelector(`output[for="${i.id}"]`); - o.textContent = i.value; - }); return Object.fromEntries( Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value]) ); } function rebuild() { + num_pieces = 0; const svg = document.querySelector('svg'); const settings = get_settings(); let sys = buildSpectreBase(false); @@ -611,127 +592,82 @@ function rebuild() { const b = g.getBoundingClientRect(); const x = (b.x - viewbox.x) / viewbox.width; const y = (b.y - viewbox.y) / viewbox.height; - const rule = new Function('t','x','y', settings.colouring_rule); - g.style.color = rule(t,x,y); + g.style.color = '#ccc'; } } visit(board,50); -} -function finish() { - const svg = document.querySelector('svg'); - const viewbox = svg.getBoundingClientRect(); - Array.from(document.querySelectorAll('#board use, #board path')).filter(g=>{ - const b = g.getBoundingClientRect(); - return (b.xviewbox.x+viewbox.width || b.y+b.height>viewbox.y+viewbox.height); - }).forEach(g => g.parentElement.removeChild(g)) + svg.addEventListener('click', e => { + console.log('click'); + if(!last_click) { + return; + } + let tile = e.target; + while(tile && !tile.classList.contains('tile')) { + tile = tile.parentElement; + } + if(!tile) { + return; + } - const transforms = Array.from(document.querySelectorAll('#board use')).map(g=>{ - const {a,b,c,d,e,f} = g.transform.baseVal[0].matrix; - return [ - [a,c,0,e], - [b,d,0,f], - [0,0,1,0] - ]; + for(let el of svg.querySelectorAll('.highlight')) { + el.classList.remove('highlight'); + } + tile.classList.add('highlight'); }); - console.log(transforms); - navigator.clipboard.writeText(svg.outerHTML); } -function setup() { +function update_display() { const settings = get_settings(); const svg = document.querySelector('svg'); //const bounds = sys.bounds(ident); - svg.setAttribute('viewBox',`${settings.ox - settings.width/2} ${settings.oy - settings.height/2} ${settings.width} ${settings.height}`); + svg.setAttribute('viewBox',`${settings.ox - settings.scale/2} ${settings.oy - settings.scale/2} ${settings.scale} ${settings.scale}`); const mx = spectre.map(p=>p.x).reduce((a,b)=>a+b)/spectre.length; const my = spectre.map(p=>p.y).reduce((a,b)=>a+b)/spectre.length; - console.log(Math.max(...spectre.map(p=>p.x)) - Math.min(...spectre.map(p=>p.x))); - console.log(Math.max(...spectre.map(p=>p.y)) - Math.min(...spectre.map(p=>p.y))); - document.getElementById('spectre').setAttribute('transform',`translate(${mx},${my}) scale(${settings.scale}) translate(${-mx},${-my})`); + document.getElementById('spectre').setAttribute('transform',`translate(${mx},${my}) translate(${-mx},${-my})`); if(settings.num_iterations != last_num_iterations) { rebuild(); last_num_iterations = settings.num_iterations; } - - - return; - - function point_key({x,y}) { - return `${x.toFixed(3)},${y.toFixed(3)}`; - } - - const point_map = new Map(); - const flattened = Array.from(sys.flatten(ident)); - for(let thing of flattened) { - const {points} = thing; - points.forEach((p,i) => { - const key = point_key(p); - if(!point_map.has(key)) { - point_map.set(key,[]); - } - point_map.get(key).push({thing, i}); - }); - } - window.point_map = point_map; - for(let thing of flattened) { - thing.neighbours = []; - thing.points.forEach((p,i) => { - const key = point_key(p); - for(let n of point_map.get(key)) { - if(n.thing != thing) { - thing.neighbours.push({thing: n.thing, toi: n.i, fromi: i}); - } - } - }); - } - window.flattened = flattened; - - function edge_distance(a,b) { - const d = Math.abs(a-b); - return d>7 ? 14-d : d; - } - - let point_index = 0; - let thing = flattened[0]; - const path = [thing.points[point_index]]; - let step = 0; - while(step++<1000) { - const maxy = Math.max(...thing.neighbours.map(t => t.thing.ymax)); - const potentials = thing.neighbours.filter(t => t.thing.ymax==maxy); - if(!potentials.length) { - break; - } - potentials.sort((a,b) => { a = edge_distance(point_index,a.fromi); b = edge_distance(point_index,b.fromi); return a
b ? 1 : 0}); - const target = potentials[0]; - const ni = target.fromi; - const d = Math.abs(ni-point_index); - const s = (d>7 ? -1 : 1) * (ni`; } for(let i of document.querySelectorAll('input')) { - i.addEventListener('input', setup); - i.addEventListener('change', setup) + i.addEventListener('input', update_display); + i.addEventListener('change', update_display) } -setup(); +update_display(); + +let opos; +let dragging; +let pan = {x:0, y:0}; +let npan = pan; +let last_click = false; + +const svg = document.querySelector('svg'); +svg.addEventListener('pointerdown', e => { + opos = getcoords(e); + dragging = true; + console.log('dragstart'); +}); + +svg.addEventListener('pointermove', e => { + if(!dragging) { + return; + } + const pos = getcoords(e); + npan = {x: pan.x + pos.x - opos.x, y: pan.y + pos.y - opos.y}; + document.getElementById('board').setAttribute('transform', `translate(${npan.x} ${npan.y})`); +}); + +svg.addEventListener('pointerup', e => { + dragging = false; + const [dx,dy] = [npan.x - pan.x, npan.y - pan.y]; + const d = Math.sqrt(dx*dx + dy*dy); + console.log(d); + last_click = d < 0.5; + pan = npan; + console.log('dragend'); +}); -document.getElementById('rebuild').addEventListener('click', () => rebuild()); -document.getElementById('finish').addEventListener('click', () => finish());