diff --git a/index.html b/index.html index f031783..685d407 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,7 @@ textarea { } .tile { - color: #ccc; + color: #eee; } .tile.highlight { @@ -60,10 +60,6 @@ textarea {
Viewbox - - - - diff --git a/script.js b/script.js index f883d7e..c28d352 100644 --- a/script.js +++ b/script.js @@ -15,15 +15,15 @@ class Point { this.y = y; } - add( q ) { + add(q) { return { x : this.x + q.x, y : this.y + q.y }; } - sub( q ) { + sub(q) { return { x : this.x - q.x, y : this.y - q.y }; } - frame( p, q, a, b ) { + frame(p, q, a, b) { return{ x : this.x + a*p.x + b*q.x, y : this.y + a*p.y + b*q.y }; } @@ -52,7 +52,7 @@ class Matrix { ]); } - mul( other ) { + mul(other) { const A = this.mat; const B = other.mat; return new Matrix([ @@ -67,9 +67,9 @@ class Matrix { } // Rotation matrix - static rotation( ang ) { - const c = cos( ang ); - const s = sin( ang ); + static rotation(ang) { + const c = cos(ang); + const s = sin(ang); return new Matrix([c, -s, 0, s, c, 0]); } @@ -77,16 +77,16 @@ class Matrix { return new Matrix([x,0,0,0,y,0]); } - static translate( tx, ty ) { + 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 ); + static translateTo(p, q) { + return Matrix.translate(q.x - p.x, q.y - p.y); } - transform( P ) { + 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]); } @@ -117,7 +117,7 @@ const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]]; function getsvg(event) { let t = event.target; while(t && t.tagName.toLowerCase()!='svg') { - t = t.parentElement; + t = t.parentElement; } return t; } @@ -134,114 +134,52 @@ function getcoords(event) { return position; } -// Match unit interval to line segment p->q -function matchSeg( p, q ) -{ - return [q.x-p.x, p.y-q.y, p.x, q.y-p.y, q.x-p.x, p.y]; -}; +class Tile { + constructor(pts, quad) { + this.pts = pts; + this.quad = quad; -// Match line segment p1->q1 to line segment p2->q2 -function matchTwo( p1, q1, p2, q2 ) -{ - return matchSeg( p2, q2 ).mul(matchSeg( p1, q1 ).inverse()); -}; + this.pts = [pts[pts.length-1]]; + for(const p of pts) { + const prev = this.pts[this.pts.length-1]; + 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); + } + } -function drawPolygon( shape, T, f, s, w ) -{ - beginShape(); - for( let p of shape ) { - const tp = T.transform( p ); - vertex( tp.x, tp.y ); - } - endShape( CLOSE ); -} - -class Shape -{ - constructor( pts, quad) { - this.pts = pts; - this.quad = quad; - - this.pts = [pts[pts.length-1]]; - for( const p of pts ) { - const prev = this.pts[this.pts.length-1]; - 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 => S.transform( p )); + streamSVG(S, stream) { + const tpts = this.pts.map(p => S.transform(p)); const [a,c,e,b,d,f] = S.mat; const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3)); num_pieces += 1; - stream.push(` - -${num_pieces} - + stream.push(` + + ${num_pieces} + `); - } - - bounds(S) { - 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)), - maxx: Math.max(...points.map(p => p.x)), - maxy: Math.max(...points.map(p => p.y)), - }; } - * flatten(S) { - 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() { - this.geoms = []; - this.quad = []; - } - - addChild( g, T ) { - this.geoms.push( { geom : g, xform: T } ); - } - - draw( S ) { - for( let g of this.geoms ) { - g.geom.draw( S.mul( g.xform ) ); - } - } - - streamSVG( S, stream ) { - const {minx,miny,maxx,maxy} = this.bounds(S); - - for( let g of this.geoms ) { - g.geom.streamSVG( S.mul(g.xform), stream ); - } - - } - - bounds(S) { - 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)), - maxx: Math.max(...sub_bounds.map(b=>b.maxx)), - maxy: Math.max(...sub_bounds.map(b=>b.maxy)), - }; +class Metatile { + constructor() { + this.geoms = []; + this.quad = []; } - * flatten(S) { + addChild(g, T) { + this.geoms.push({ geom : g, xform: T }); + } + + streamSVG(S, stream) { for(let g of this.geoms) { - yield* g.geom.flatten(S.mul(g.xform)); + g.geom.streamSVG(S.mul(g.xform), stream); } + } } @@ -263,9 +201,9 @@ function tiles(level, label) { case 'Psi': out.push(ident); case 'Gamma': - const mystic = new Meta(); + const mystic = new Metatile(); out.push(ident); - out.push(Matrix.translate( spectre[8].x, spectre[8].y ).mul( Matrix.rotation( PI / 6 ) )); + out.push(Matrix.translate(spectre[8].x, spectre[8].y).mul(Matrix.rotation(PI / 6))); } } else { /* @@ -283,7 +221,7 @@ function tiles(level, label) { 'Theta', 'Lambda', 'Xi', - 'Pi', + 'Pi', 'Sigma', 'Phi', 'Psi' @@ -306,25 +244,25 @@ function tiles(level, label) { [-120, 3, 3] ]; - + let Ts = [ident]; let total_ang = 0; let rot = ident; let tquad = [...subquad]; - for( const [ang,from,to] of t_rules ) { + for(const [ang,from,to] of t_rules) { total_ang += ang; - if( ang != 0 ) { - rot = Matrix.rotation( radians( total_ang ) ); + if(ang != 0) { + rot = Matrix.rotation(radians(total_ang)); tquad = subquad.map(q => rot.transform(q)); } - const ttt = Matrix.translateTo( tquad[to], Ts[Ts.length-1].transform( subquad[from] ) ); - Ts.push( ttt.mul( 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)); - + // Now build the actual supertiles, labelling appropriately. const super_rules = { 'Gamma' : ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'], @@ -338,224 +276,15 @@ function tiles(level, label) { 'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] }; const super_quad = [ - Ts[6].transform(subquad[2] ), - Ts[5].transform(subquad[1] ), - Ts[3].transform(subquad[2] ), - Ts[0].transform(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}; } -function buildSpectreBase() -{ - const ret = {}; - - for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi'] ) { - ret[lab] = new Shape( spectre, base_quad, lab ); - } - - const mystic = new Meta(); - mystic.addChild( new Shape( spectre, base_quad, 'Gamma1' ), ident ); - mystic.addChild( new Shape( spectre, base_quad, 'Gamma2' ), - Matrix.translate( spectre[8].x, spectre[8].y ).mul( Matrix.rotation( PI / 6 ) ) ); - mystic.quad = base_quad; - ret['Gamma'] = mystic; - - return ret; -} - -function buildHatTurtleBase( hat_dominant ) -{ - const r3 = 1.7320508075688772; - const hr3 = 0.8660254037844386; - - function hexPt( x, y ) { - return new Point( x + 0.5*y, -hr3*y ); - } - - function hexPt2( x, y ) { - return new Point( x + hr3*y, -0.5*y ); - } - - const hat = [ - hexPt(-1, 2), hexPt(0, 2), hexPt(0, 3), hexPt(2, 2), hexPt(3, 0), - hexPt(4, 0), hexPt(5,-1), hexPt(4,-2), hexPt(2,-1), hexPt(2,-2), - hexPt( 1, -2), hexPt(0,-2), hexPt(-1,-1), hexPt(0, 0) ]; - - const turtle = [ - hexPt(0,0), hexPt(2,-1), hexPt(3,0), hexPt(4,-1), hexPt(4,-2), - hexPt(6,-3), hexPt(7,-5), hexPt(6,-5), hexPt(5,-4), hexPt(4,-5), - hexPt(2,-4), hexPt(0,-3), hexPt(-1,-1), hexPt(0,-1) - ]; - - const hat_keys = [ - hat[3], hat[5], hat[7], hat[11] - ]; - const turtle_keys = [ - turtle[3], turtle[5], turtle[7], turtle[11] - ]; - - const ret = {}; - - if( hat_dominant ) { - for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi'] ) { - ret[lab] = new Shape( hat, hat_keys, lab ); - } - - const mystic = new Meta(); - mystic.addChild( new Shape( hat, hat_keys, 'Gamma1' ), ident ); - mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma2' ), - Matrix.translate( hat[8].x, hat[8].y ) ); - mystic.quad = hat_keys; - ret['Gamma'] = mystic; - } else { - for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi'] ) { - ret[lab] = new Shape( turtle, turtle_keys, lab ); - } - - const mystic = new Meta(); - mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma1' ), ident ); - mystic.addChild( new Shape( hat, hat_keys, 'Gamma2' ), - Matrix.translate( turtle[9].x, turtle[9].y ).mul( Matrix.rotation( PI/3 ) ) ); - mystic.quad = turtle_keys; - ret['Gamma'] = mystic; - } - - return ret; -} - -function buildHexBase() -{ - const hr3 = 0.8660254037844386; - - const hex = [ - 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] ]; - - const ret = {}; - - for( lab of ['Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', - 'Pi', 'Sigma', 'Phi', 'Psi'] ) { - ret[lab] = new Shape( hex, hex_keys, lab ); - } - - return ret; -} - -function buildSupertiles( sys ) -{ - // First, use any of the nine-unit tiles in sys to obtain - // a list of transformation matrices for placing tiles within - // supertiles. - - const quad = sys['Delta'].quad; - 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], - [0, 2, 0], [60, 3, 1], [-120, 3, 3] ]; - - const Ts = [ident]; - let total_ang = 0; - let rot = ident; - const tquad = [...quad]; - for( const [ang,from,to] of t_rules ) { - total_ang += ang; - if( ang != 0 ) { - rot = Matrix.rotation( radians( total_ang ) ); - for( i = 0; i < 4; ++i ) { - tquad[i] = rot.transform(quad[i] ); - } - } - - 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] = R.mul( Ts[idx] ); - } - - // Now build the actual supertiles, labelling appropriately. - const super_rules = { - 'Gamma' : ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'], - 'Delta' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'], - 'Theta' : ['Psi','Delta','Pi','Phi','Sigma','Pi','Phi','Gamma'], - 'Lambda' : ['Psi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'], - 'Xi' : ['Psi','Delta','Pi','Phi','Sigma','Psi','Phi','Gamma'], - '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'] }; - const super_quad = [ - Ts[6].transform(quad[2] ), - Ts[5].transform(quad[1] ), - Ts[3].transform(quad[2] ), - Ts[0].transform(quad[1] ) ]; - - const ret = {}; - - for( const [lab, subs] of Object.entries( super_rules ) ) { - const sup = new Meta(); - for( let idx = 0; idx < 8; ++idx ) { - if( subs[idx] == 'null' ) { - continue; - } - sup.addChild( sys[subs[idx]], Ts[idx] ); - } - sup.quad = super_quad; - - ret[lab] = sup; - } - - return ret; -} - -/* modified from https://css-tricks.com/converting-color-spaces-in-javascript/ - */ -function hexToHSL(H) { - const [r,g,b] = [0,1,2].map(i=>H.slice(2*i+1,2*i+3)).map(n=>parseInt(n,16)/255); - let cmin = Math.min(r,g,b), - cmax = Math.max(r,g,b), - delta = cmax - cmin, - h = 0, - s = 0, - l = 0; - - if (delta == 0) - h = 0; - else if (cmax == r) - h = ((g - b) / delta) % 6; - else if (cmax == g) - h = (b - r) / delta + 2; - else - h = (r - g) / delta + 4; - - h = Math.round(h * 60); - - if (h < 0) - h += 360; - - l = (cmax + cmin) / 2; - s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); - s = +(s * 100).toFixed(1); - l = +(l * 100).toFixed(1); - - return {h,s,l}; -} - let last_num_iterations; function get_settings() { @@ -564,70 +293,175 @@ function get_settings() { ); } -function rebuild() { - num_pieces = 0; - const svg = document.querySelector('svg'); - const settings = get_settings(); - let sys = buildSpectreBase(false); - - for(let i=0;i { - console.log('click'); - if(!last_click) { - return; - } - let tile = e.target; - while(tile && !tile.classList.contains('tile')) { - tile = tile.parentElement; - } - if(!tile) { - return; + buildSpectreBase() { + const ret = {}; + + for(let lab of ['Delta', 'Theta', 'Lambda', 'Xi', + 'Pi', 'Sigma', 'Phi', 'Psi']) { + ret[lab] = new Tile(spectre, base_quad, lab); } - for(let el of svg.querySelectorAll('.highlight')) { - el.classList.remove('highlight'); + const mystic = new Metatile(); + mystic.addChild(new Tile(spectre, base_quad, 'Gamma1'), ident); + mystic.addChild(new Tile(spectre, base_quad, 'Gamma2'), + Matrix.translate(spectre[8].x, spectre[8].y).mul(Matrix.rotation(PI / 6))); + mystic.quad = base_quad; + ret['Gamma'] = mystic; + + return ret; + } + + buildSupertiles(sys) { + // First, use any of the nine-unit tiles in sys to obtain + // a list of transformation matrices for placing tiles within + // supertiles. + + const quad = sys['Delta'].quad; + 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], + [0, 2, 0], [60, 3, 1], [-120, 3, 3] ]; + + const Ts = [ident]; + let total_ang = 0; + let rot = ident; + const tquad = [...quad]; + for(const [ang,from,to] of t_rules) { + total_ang += ang; + if(ang != 0) { + rot = Matrix.rotation(radians(total_ang)); + for(let i = 0; i < 4; ++i) { + tquad[i] = rot.transform(quad[i]); + } + } + + const ttt = Matrix.translateTo(tquad[to], + Ts[Ts.length-1].transform(quad[from])); + Ts.push(ttt.mul(rot)); } - tile.classList.add('highlight'); - }); + + for(let idx = 0; idx < Ts.length; ++idx) { + Ts[idx] = R.mul(Ts[idx]); + } + + // Now build the actual supertiles, labelling appropriately. + const super_rules = { + 'Gamma' : ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'], + 'Delta' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'], + 'Theta' : ['Psi','Delta','Pi','Phi','Sigma','Pi','Phi','Gamma'], + 'Lambda' : ['Psi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'], + 'Xi' : ['Psi','Delta','Pi','Phi','Sigma','Psi','Phi','Gamma'], + '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'] }; + const super_quad = [ + Ts[6].transform(quad[2]), + Ts[5].transform(quad[1]), + Ts[3].transform(quad[2]), + Ts[0].transform(quad[1]) ]; + + const ret = {}; + + for(const [lab, subs] of Object.entries(super_rules)) { + const sup = new Metatile(); + for(let idx = 0; idx < 8; ++idx) { + if(subs[idx] == 'null') { + continue; + } + sup.addChild(sys[subs[idx]], Ts[idx]); + } + sup.quad = super_quad; + + ret[lab] = sup; + } + + return ret; + } + + + draw() { + const {sys} = this; + + const drawing = []; + + const svg = document.querySelector('svg'); + + const board = svg.querySelector('#board'); + board.innerHTML = ''; + const R = new Matrix([-1,0,0,0,1,0]); + const m = this.settings.num_iterations % 2 == 0 ? ident : R; + sys.streamSVG(m, drawing); + board.innerHTML = drawing.join(' '); + + const viewbox = svg.getBoundingClientRect(); + function visit(g, t) { + for(let c of g.children) { + visit(c, t+Math.random()*10-5); + } + if(g.tagName=='path') { + const b = g.getBoundingClientRect(); + const x = (b.x - viewbox.x) / viewbox.width; + const y = (b.y - viewbox.y) / viewbox.height; + g.style.color = '#ccc'; + } + } + visit(board,50); + + svg.addEventListener('click', e => { + if(!last_click) { + return; + } + let tile = e.target; + while(tile && !tile.classList.contains('tile')) { + tile = tile.parentElement; + } + if(!tile) { + return; + } + + for(let el of svg.querySelectorAll('.highlight')) { + el.classList.remove('highlight'); + } + tile.classList.add('highlight'); + }); + } } function update_display() { const settings = get_settings(); const svg = document.querySelector('svg'); - //const bounds = sys.bounds(ident); - svg.setAttribute('viewBox',`${settings.ox - settings.scale/2} ${settings.oy - settings.scale/2} ${settings.scale} ${settings.scale}`); + svg.setAttribute('viewBox',`${-settings.scale/2} ${-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; document.getElementById('spectre').setAttribute('transform',`translate(${mx},${my}) translate(${-mx},${-my})`); - + if(settings.num_iterations != last_num_iterations) { - rebuild(); + const builder = new Builder(settings); + builder.build(); + builder.draw(); last_num_iterations = settings.num_iterations; } } @@ -649,7 +483,6 @@ const svg = document.querySelector('svg'); svg.addEventListener('pointerdown', e => { opos = getcoords(e); dragging = true; - console.log('dragstart'); }); svg.addEventListener('pointermove', e => { @@ -665,9 +498,7 @@ 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'); });