// derived from https://cs.uwaterloo.ca/~csk/spectre/spectre.js const {PI, cos, sin} = Math; const ident = [1,0,0,0,1,0]; let pan = {x: -100, y: 100}; function radians(degrees) { return degrees * PI / 180; } let to_screen = [20, 0, 0, 0, -20, 0]; let lw_scale = 1; let sys; let scale_centre; let scale_start; let scale_ts; let reset_but; let tile_sel; let shape_sel; let colscheme_sel; let subst_button; let translate_button; let scale_button; let dragging = false; let uibox = true; 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) ]; 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 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]); } class Shape { constructor( pts, quad) { 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; this.pts.push( p ); } } streamSVG( S, stream ) { const tpts = this.pts.map(p => transPt( S, p )); const [a,c,e,b,d,f] = S; const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3)); const s = ``; stream.push( s ); } bounds(S) { const points = this.pts.map(p => transPt(S,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 => transPt(S,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( mul( S, g.xform ) ); } } 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)); stream.push(``); const quad_points = this.quad.map(({x,y}) => `${x},${y}`).join(' '); // stream.push(``); for( let g of this.geoms ) { g.geom.streamSVG( g.xform, stream ); } stream.push(''); } bounds(S) { const sub_bounds = this.geoms.map(g => g.geom.bounds(mul(S,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)), }; } * flatten(S) { for(let g of this.geoms) { yield* g.geom.flatten(mul(S, g.xform)); } } } function tiles(level, label) { let quad; let out = []; let transform; if(level == 0) { transform = ident; quad = base_quad; switch(label) { case 'Delta': case 'Theta': case 'Lambda': case 'Xi': case 'Pi': case 'Sigma': case 'Phi': case 'Psi': out.push(ident); case 'Gamma': const mystic = new Meta(); out.push(ident); out.push(mul( ttrans( spectre[8].x, spectre[8].y ), trot( PI / 6 ) )); } } else { /* * Each of the subtiles is identical, but rotated and translated. * * Produce transformation matrices Ts for each of the subtiles: they're formed by rotating the quad and then matching up a pair of points. * * The whole thing is then reflected. * * The layout of subtiles depends on the larger tile. * */ 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); // 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] ]; let Ts = [ident]; let total_ang = 0; let rot = ident; let tquad = [...subquad]; 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)); } const ttt = transTo( tquad[to], transPt( Ts[Ts.length-1], subquad[from] ) ); Ts.push( mul( ttt, 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'], '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 = [ transPt( Ts[6], subquad[2] ), transPt( Ts[5], subquad[1] ), transPt( Ts[3], subquad[2] ), transPt( Ts[0], 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' ), mul( ttrans( spectre[8].x, spectre[8].y ), trot( 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 pt( x + 0.5*y, -hr3*y ); } function hexPt2( x, y ) { return pt( 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' ), ttrans( 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' ), mul( ttrans( turtle[9].x, turtle[9].y ), trot( PI/3 ) ) ); mystic.quad = turtle_keys; ret['Gamma'] = mystic; } return ret; } 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) ]; 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 = [-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 = trot( radians( total_ang ) ); for( i = 0; i < 4; ++i ) { tquad[i] = transPt( rot, quad[i] ); } } const ttt = transTo( tquad[to], transPt( Ts[Ts.length-1], quad[from] ) ); Ts.push( mul( ttt, rot ) ); } for( let idx = 0; idx < Ts.length; ++idx ) { Ts[idx] = mul( R, 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 = [ transPt( Ts[6], quad[2] ), transPt( Ts[5], quad[1] ), transPt( Ts[3], quad[2] ), transPt( Ts[0], 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() { return Object.fromEntries( Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value]) ); } function rebuild() { const svg = document.querySelector('svg'); const settings = get_settings(); let sys = buildSpectreBase(false); for(let i=0;i finish()); function make_download() { const svg = document.querySelector('svg').cloneNode(true); document.body.append(svg); Array.from(svg.querySelectorAll('.spectre')).forEach(s => { s.setAttribute('fill', getComputedStyle(s).fill); }); for(let e of svg.querySelectorAll('#guides, #draggables circle')) { e.parentElement.removeChild(e); } for(let img of svg.querySelectorAll('image.template')) { img.setAttribute('width', getComputedStyle(img).width); } document.body.removeChild(svg); const f = new File([svg.outerHTML],'aperiodic-monotile-clothes.svg',{type:'image/svg+xml'}); const url = URL.createObjectURL(f); document.getElementById('link').setAttribute('href',url); } function set_colours() { const svg = document.querySelector('svg'); const fd = new FormData(document.querySelector('form')); console.log(fd); svg.style.setProperty('--y0-lum', document.getElementById('y0-lum').cy.baseVal.value); svg.style.setProperty('--y1-lum', document.getElementById('y1-lum').cy.baseVal.value); svg.style.setProperty('--y0-hue', document.getElementById('y0-hue').cy.baseVal.value); svg.style.setProperty('--y1-hue', document.getElementById('y1-hue').cy.baseVal.value); 'back front front-pocket hood label-panel left-sleeve right-sleeve'.split(' ').forEach(w => { const img = document.getElementById(`hoodie-${w}`); svg.style.setProperty(`--hoodie-${w}-x`, img.x.baseVal.value); svg.style.setProperty(`--hoodie-${w}-y`, img.y.baseVal.value); }); svg.style.setProperty('--hue-interpolation', fd.get('hue-interpolation')); svg.dataset.template = fd.get('template-name'); 'min-hue max-hue sat-scale randomisation template-scale'.split(' ').forEach(w => { const n = document.getElementById(w).valueAsNumber; svg.style.setProperty(`--${w}`, n); const o = document.querySelector(`output[for="${w}"]`); if(o) { o.textContent = n; } }); } Array.from(document.querySelectorAll('form :is(input,select)')).map(i => { i.addEventListener('input', set_colours); }); set_colours(); function getsvg(event) { let t = event.target; while(t && t.tagName.toLowerCase() != 'svg') { t = t.parentElement; } return t; } function getcoords(svg, event) { const point = svg.createSVGPoint(); point.x = event.clientX; point.y = event.clientY; const position = point.matrixTransform(svg.querySelector('#display').getScreenCTM().inverse()); return position; } function init_draggables() { let dragging = false; let off = null; const svg = document.querySelector('svg'); function coord_attributes(element) { switch(element.tagName.toLowerCase()) { case 'circle': return ['cx', 'cy']; default: return ['x', 'y']; } } function get_element_centre(element) { const [xattr, yattr] = coord_attributes(element); return {x: element[xattr].baseVal.value, y: element[yattr].baseVal.value}; } function set_element_centre(element, p) { const [xattr, yattr] = coord_attributes(element); element.setAttribute(xattr, p.x - off.x); element.setAttribute(yattr, p.y - off.y); } svg.addEventListener('pointerdown', e => { if(e.buttons == 1 && e.target.classList.contains('draggable')) { dragging = e.target; const p = getcoords(svg, e); const {x,y} = get_element_centre(dragging); off = {x: p.x - x, y: p.y - y}; } else { dragging = svg; const z = svg.querySelector('#display').getScreenCTM().a; off = {x: e.clientX - pan.x*z, y: e.clientY - pan.y*z}; } }); document.body.addEventListener('pointermove', e => { if(!dragging) { return; } if(dragging.classList.contains('draggable')) { const p = getcoords(svg, e); set_element_centre(dragging, p); set_colours(); } else { const z = svg.querySelector('#display').getScreenCTM().a; pan.x = (e.clientX - off.x) / z; pan.y = (e.clientY - off.y) / z; setup(); } }); document.body.addEventListener('pointerup', () => { dragging = false; }); svg.addEventListener('wheel', e => { const dy = e.deltaY / 1000; const zoom_input = document.getElementById('zoom'); zoom_input.value = zoom_input.valueAsNumber - dy; setup(); e.preventDefault(); }); set_colours(); } init_draggables();