aperiodical banner design
This commit is contained in:
commit
e800edc47e
2 changed files with 808 additions and 0 deletions
81
index.html
Normal file
81
index.html
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<script src="script.js" defer></script>
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"drawing" 50vh
|
||||||
|
"controls" 1fr
|
||||||
|
/ 1fr;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="drawing">
|
||||||
|
<svg viewBox="-9 -68 162 27" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<path id="spectre"
|
||||||
|
d="M 0 1 C 0.6 0.6699999999999999 0.6 0.32999999999999996 0 0 C 0.33 -0.6 0.67 -0.6 1 0 C 1.684615242270663 0.014211616751135248 1.854615242270663 -0.2802370205355739 1.5 -0.8660254037844386 C 2.0857883832488646 -1.2206406460551018 2.380237020535574 -1.0506406460551019 2.366025403784439 -0.36602540378443865 C 1.7660254037844387 -0.03602540378443864 1.7660254037844387 0.3039745962155614 2.366025403784439 0.6339745962155614 C 2.696025403784439 0.033974596215561426 3.0360254037844387 0.033974596215561426 3.366025403784439 0.6339745962155614 C 3.011410161513776 1.2197629794644262 3.1814101615137758 1.5142116167511352 3.866025403784439 1.5 C 3.880237020535574 2.184615242270663 3.5857883832488646 2.354615242270663 3 2 C 3.014211616751135 1.3153847577293367 2.719762979464426 1.1453847577293368 2.133974596215561 1.5 C 2.4885898384862246 2.085788383248865 2.3185898384862247 2.3802370205355743 1.6339745962155614 2.3660254037844393 C 1.3039745962155613 1.7660254037844392 0.9639745962155614 1.7660254037844392 0.6339745962155614 2.3660254037844393 C 0.3039745962155614 2.9660254037844394 -0.03602540378443864 2.9660254037844394 -0.3660254037844386 2.3660254037844393 C -0.011410161513775163 1.7802370205355742 -0.18141016151377531 1.4857883832488645 -0.866025403784439 1.5 C -0.8802370205355741 0.8153847577293366 -0.5857883832488648 0.6453847577293367 0 1"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="0.1"
|
||||||
|
stroke-opacity="0.5"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="board" style="transform: translate(0,-120px) rotate(90deg)"></g>
|
||||||
|
<text x="86" fill="white" y="-45" font-size="8px">The Aperiodical</text>
|
||||||
|
</svg>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="controls">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Viewbox</legend>
|
||||||
|
<label for="vx1">min x</label>
|
||||||
|
<input type="range" min="-500" max="500" value="-9" id="vx1">
|
||||||
|
<label for="vy1">min y</label>
|
||||||
|
<input type="range" min="-500" max="500" value="-68" id="vy1">
|
||||||
|
<label for="vx2">max x</label>
|
||||||
|
<input type="range" min="-500" max="500" value="153" id="vx2">
|
||||||
|
<label for="vy2">max y</label>
|
||||||
|
<input type="range" min="-500" max="500" value="-41" id="vy2">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Colours</legend>
|
||||||
|
<label for="col1">Colour 1</label>
|
||||||
|
<input type="color" id="col1" value="#00ff80">
|
||||||
|
<label for="col2">Colour 2</label>
|
||||||
|
<input type="color" id="col2" value="#5500ff">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Tiling</legend>
|
||||||
|
<label for="num_iterations">Number of iterations</label>
|
||||||
|
<input type="number" min="1" max="5" value="4" id="num_iterations">
|
||||||
|
<label for="colouring_rule">Colouring rule</label>
|
||||||
|
<textarea id="colouring_rule">
|
||||||
|
const sat = t;
|
||||||
|
const hue = lerp(150,260,y*lerp(0.9,1.1,Math.random()));
|
||||||
|
const lum = lerp(50,80,((1-x)+5*(1-y))/6);
|
||||||
|
return `hsl(${hue},${sat}%,${lum}%)`;
|
||||||
|
</textarea>
|
||||||
|
</fieldset>
|
||||||
|
<button type="button" id="rebuild">Rebuild</button>
|
||||||
|
<button type="button" id="finish">Finish</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
727
script.js
Normal file
727
script.js
Normal file
|
@ -0,0 +1,727 @@
|
||||||
|
// derived from https://cs.uwaterloo.ca/~csk/spectre/spectre.js
|
||||||
|
|
||||||
|
|
||||||
|
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 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 ) ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
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 );
|
||||||
|
vertex( tp.x, tp.y );
|
||||||
|
}
|
||||||
|
endShape( CLOSE );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<use href="#spectre" transform="matrix(${matS.join(',')})"/>`;
|
||||||
|
|
||||||
|
//const points = this.pts.map(({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`).join(' ');
|
||||||
|
/*
|
||||||
|
const tp = this.pts[0];
|
||||||
|
|
||||||
|
var s = `<path transform="matrix(${matS.join(',')})" d="M ${tp.x} ${tp.y}`;
|
||||||
|
|
||||||
|
for( let idx = 1; idx < this.pts.length; idx += 3 ) {
|
||||||
|
const a = this.pts[idx];
|
||||||
|
const b = this.pts[idx+1];
|
||||||
|
const c = this.pts[idx+2];
|
||||||
|
|
||||||
|
s = s + ` C ${a.x} ${a.y} ${b.x} ${b.y} ${c.x} ${c.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s + `"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="0.1"
|
||||||
|
stroke-opacity="0.2"
|
||||||
|
fill="currentColor" />`;
|
||||||
|
*/
|
||||||
|
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(`<g transform="matrix(${matS.join(',')})">`);
|
||||||
|
const quad_points = this.quad.map(({x,y}) => `${x},${y}`).join(' ');
|
||||||
|
// stream.push(`<polygon fill="blue" fill-opacity="0.2" points="${quad_points}"></polygon>`);
|
||||||
|
for( let g of this.geoms ) {
|
||||||
|
g.geom.streamSVG( g.xform, stream );
|
||||||
|
}
|
||||||
|
stream.push('</g>');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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<settings.num_iterations;i++) {
|
||||||
|
sys = buildSupertiles( sys );
|
||||||
|
}
|
||||||
|
|
||||||
|
sys = sys['Delta'];
|
||||||
|
window.sys = sys;
|
||||||
|
|
||||||
|
const drawing = [];
|
||||||
|
const board = svg.querySelector('#board');
|
||||||
|
board.innerHTML = '';
|
||||||
|
sys.streamSVG(ident, 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=='use') {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(board,50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
const svg = document.querySelector('svg');
|
||||||
|
const viewbox = svg.getBoundingClientRect();
|
||||||
|
Array.from(document.querySelectorAll('#board g,use')).filter(g=>{
|
||||||
|
const b = g.getBoundingClientRect();
|
||||||
|
return (b.x+b.width<viewbox.x || b.y+b.height<viewbox.y || b.x>viewbox.x+viewbox.width || b.y>viewbox.y+viewbox.height);
|
||||||
|
}).forEach(g => g.parentElement.removeChild(g))
|
||||||
|
Array.from(svg.querySelectorAll('text')).forEach(text => {
|
||||||
|
text.parentElement.removeChild(text);
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(svg.outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const settings = get_settings();
|
||||||
|
const svg = document.querySelector('svg');
|
||||||
|
//const bounds = sys.bounds(ident);
|
||||||
|
svg.setAttribute('viewBox',`${settings.vx1} ${settings.vy1} ${settings.vx2-settings.vx1} ${settings.vy2-settings.vy1}`);
|
||||||
|
|
||||||
|
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 : 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<point_index ? -1 : 1);
|
||||||
|
for(let i=point_index;i!=ni;i = (i+s+14)%14) {
|
||||||
|
path.push(thing.points[i]);
|
||||||
|
}
|
||||||
|
thing = target.thing;
|
||||||
|
point_index = target.toi;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let t of flattened) {
|
||||||
|
const polygon = document.createElement('polygon');
|
||||||
|
polygon.setAttribute('points', t.points.map(svg_point).join(' '));
|
||||||
|
polygon.style.fill = 'none';
|
||||||
|
polygon.style.stroke = 'black';
|
||||||
|
// svg.appendChild(polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = path.map(svg_point).join(' ');
|
||||||
|
svg.innerHTML += `<polyline points="${points}" fill="none" stroke="blue" stroke-width="3">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i of document.querySelectorAll('input')) {
|
||||||
|
i.addEventListener('input', setup);
|
||||||
|
i.addEventListener('change', setup)
|
||||||
|
}
|
||||||
|
|
||||||
|
setup();
|
||||||
|
|
||||||
|
document.getElementById('rebuild').addEventListener('click', () => rebuild());
|
||||||
|
document.getElementById('finish').addEventListener('click', () => finish());
|
Loading…
Add table
Add a link
Reference in a new issue