making it work as a reference while making a real tiling

I've gathered the vector and matrix functions in Point and Matrix
classes.

Going to make the building functions part of a builder class.

You can drag to pan the view.

Clicking a tile highlights it. Each tile is labelled with a number, but
it gets reflected when there's an odd number of iterations - need to fix
that.
This commit is contained in:
Christian Lawson-Perfect 2025-05-07 16:18:06 +01:00
parent c34514e939
commit bdf1890ac0
2 changed files with 276 additions and 353 deletions

View file

@ -1,28 +1,44 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Aperiodic monotile</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<script src="script.js" defer></script> <script src="script.js?t=202505071454" defer></script>
<style> <style>
* {
box-sizing: border-box;
}
svg { svg {
max-width: 100%; width: 100%;
max-height: 100%; height: 100%;
border: 1px solid black; border: 1px solid black;
touch-action: pan-x pan-y;
} }
body { body {
overscroll-behavior: contain;
padding: 0; padding: 0;
margin: 0; margin: 0;
display: grid; display: grid;
height: 100svh;
grid-template: grid-template:
"drawing" 50vh "drawing" 80svh
"controls" 1fr "controls" min-content
/ 1fr; / 1fr;
} }
textarea { textarea {
width: 100%; width: 100%;
height: 20em; height: 20em;
} }
.tile {
color: #ccc;
}
.tile.highlight {
color: hsl(240,100%,80%);
}
</style> </style>
</head> </head>
<body> <body>
@ -30,7 +46,7 @@ textarea {
<svg viewBox="0 0 190 195" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 190 195" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<path id="spectre" <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" d="M 0 1 C -0.3 0.5 0.3 0.5 0 0 C 0.5 -0.3 0.5 0.3 1 0 C 0.9901923788646685 -0.5830127018922193 1.5098076211353315 -0.2830127018922193 1.5 -0.8660254037844386 C 2.0830127018922195 -0.8758330249197702 1.7830127018922195 -0.356217782649107 2.366025403784439 -0.36602540378443865 C 2.6660254037844386 0.13397459621556135 2.066025403784439 0.13397459621556135 2.366025403784439 0.6339745962155614 C 2.866025403784439 0.3339745962155614 2.866025403784439 0.9339745962155614 3.366025403784439 0.6339745962155614 C 3.8758330249197703 0.9169872981077808 3.3562177826491073 1.2169872981077807 3.866025403784439 1.5 C 3.5830127018922195 2.0098076211353315 3.2830127018922197 1.4901923788646685 3 2 C 2.4169872981077805 2.0098076211353315 2.7169872981077803 1.4901923788646685 2.133974596215561 1.5 C 2.143782217350893 2.0830127018922195 1.6241669750802294 1.7830127018922197 1.6339745962155614 2.3660254037844393 C 1.1339745962155614 2.666025403784439 1.1339745962155614 2.0660254037844394 0.6339745962155614 2.3660254037844393 C 0.1339745962155614 2.666025403784439 0.1339745962155614 2.0660254037844394 -0.3660254037844386 2.3660254037844393 C -0.8758330249197706 2.08301270189222 -0.35621778264910703 1.7830127018922195 -0.866025403784439 1.5 C -0.5830127018922195 0.9901923788646683 -0.2830127018922195 1.5098076211353317 0 1"
stroke="black" stroke="black"
stroke-width="0.1" stroke-width="0.1"
stroke-opacity="1" stroke-opacity="1"
@ -44,44 +60,15 @@ textarea {
<section id="controls"> <section id="controls">
<fieldset> <fieldset>
<legend>Viewbox</legend> <legend>Viewbox</legend>
<label for="ox">min x</label> <label for="ox">ox</label>
<input type="number" min="-500" max="500" value="0" id="ox"> <input type="number" min="-500" max="500" value="0" id="ox">
<output for="ox"></output> <label for="oy">oy</label>
<label for="oy">min y</label>
<input type="number" min="-500" max="500" value="0" id="oy"> <input type="number" min="-500" max="500" value="0" id="oy">
<output for="oy"></output> <label for="scale">scale</label>
<label for="width">max x</label> <input type="range" min="1" max="100" value="19" id="scale">
<input type="number" min="-500" max="500" value="190" id="width">
<output for="width"></output>
<label for="height">max y</label>
<input type="number" min="-500" max="500" value="190" id="height">
<output for="height"></output>
<label for="scale">Piece scale</label>
<input type="range" min="0" max="1" value="1" step="0.01" id="scale">
<output for="scale"></output>
</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> <label for="num_iterations">Number of iterations</label>
<input type="number" min="1" max="5" value="4" id="num_iterations"> <input type="number" min="1" max="5" value="2" 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> </fieldset>
<button type="button" id="rebuild">Rebuild</button>
<button type="button" id="finish">Finish</button>
</section> </section>
</body> </body>

562
script.js
View file

@ -3,131 +3,135 @@
const {PI, cos, sin} = Math; const {PI, cos, sin} = Math;
const ident = [1,0,0,0,1,0];
function radians(degrees) { function radians(degrees) {
return degrees * PI / 180; return degrees * PI / 180;
} }
let to_screen = [20, 0, 0, 0, -20, 0]; let num_pieces = 0;
let lw_scale = 1;
let sys; class Point {
constructor(x,y) {
this.x = x;
this.y = y;
}
let scale_centre; add( q ) {
let scale_start; return { x : this.x + q.x, y : this.y + q.y };
let scale_ts; }
let reset_but; sub( q ) {
let tile_sel; return { x : this.x - q.x, y : this.y - q.y };
let shape_sel; }
let colscheme_sel;
let subst_button; frame( p, q, a, b ) {
let translate_button; return{ x : this.x + a*p.x + b*q.x, y : this.y + a*p.y + b*q.y };
let scale_button; }
let dragging = false;
let uibox = true; }
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 = [ const spectre = [
pt(0, 0), new Point(0, 0),
pt(1.0, 0.0), new Point(1.0, 0.0),
pt(1.5, -0.8660254037844386), new Point(1.5, -0.8660254037844386),
pt(2.366025403784439, -0.36602540378443865), new Point(2.366025403784439, -0.36602540378443865),
pt(2.366025403784439, 0.6339745962155614), new Point(2.366025403784439, 0.6339745962155614),
pt(3.366025403784439, 0.6339745962155614), new Point(3.366025403784439, 0.6339745962155614),
pt(3.866025403784439, 1.5), new Point(3.866025403784439, 1.5),
pt(3.0, 2.0), new Point(3.0, 2.0),
pt(2.133974596215561, 1.5), new Point(2.133974596215561, 1.5),
pt(1.6339745962155614, 2.3660254037844393), new Point(1.6339745962155614, 2.3660254037844393),
pt(0.6339745962155614, 2.3660254037844393), new Point(0.6339745962155614, 2.3660254037844393),
pt(-0.3660254037844386, 2.3660254037844393), new Point(-0.3660254037844386, 2.3660254037844393),
pt(-0.866025403784439, 1.5), new Point(-0.866025403784439, 1.5),
pt(0.0, 1.0) new Point(0.0, 1.0)
]; ];
const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]]; const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]];
function getsvg(event) {
const tile_names = [ let t = event.target;
'Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', while(t && t.tagName.toLowerCase()!='svg') {
'Pi', 'Sigma', 'Phi', 'Psi' ]; t = t.parentElement;
}
const svg_point = ({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`; return t;
function lerp(a,b,t) {
return t*b + (1-t)*a;
} }
function pt( x, y ) function getcoords(event) {
{ const t = getsvg(event);
return { x : x, y : y }; if(!t) {
} return;
}
// Affine matrix inverse const point = t.createSVGPoint()
function inv( T ) { point.x = event.clientX
const det = T[0]*T[4] - T[1]*T[3]; point.y = event.clientY
return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det, const position = point.matrixTransform(t.getScreenCTM().inverse())
-T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det]; return position;
};
// 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 // 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 // Match line segment p1->q1 to line segment p2->q2
function matchTwo( p1, q1, 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 ) function drawPolygon( shape, T, f, s, w )
{ {
console.log(shape,T,f,s,w);
beginShape(); beginShape();
for( let p of shape ) { for( let p of shape ) {
const tp = transPt( T, p ); const tp = T.transform( p );
vertex( tp.x, tp.y ); vertex( tp.x, tp.y );
} }
endShape( CLOSE ); endShape( CLOSE );
@ -160,56 +162,32 @@ class Shape
this.pts = pts; this.pts = pts;
this.quad = quad; this.quad = quad;
let blah = true;
this.pts = [pts[pts.length-1]]; this.pts = [pts[pts.length-1]];
for( const p of pts ) { for( const p of pts ) {
const prev = this.pts[this.pts.length-1]; const prev = this.pts[this.pts.length-1];
const v = psub( p, prev ); const v = p.sub(prev);
const w = pt( -v.y, v.x ); const w = new Point( -v.y, v.x );
if( blah ) { this.pts.push( prev.frame(v, w, 0.5, -0.3 ) );
this.pts.push( pframe( prev, v, w, 0.33, 0.6 ) ); this.pts.push( prev.frame(v, w, 0.5, 0.3 ) );
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 ); this.pts.push( p );
} }
} }
streamSVG( S, stream ) streamSVG( S, stream ) {
{ const tpts = this.pts.map(p => S.transform( p ));
const tpts = this.pts.map(p => transPt( S, 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 matS = [a,b,c,d,e,f].map(p=>p.toFixed(3));
//const s = `<use href="#spectre" transform="matrix(${matS.join(',')}) "/>`; num_pieces += 1;
stream.push(`<g class="tile">
const points = this.pts.map(({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`).join(' '); <use href="#spectre" transform="matrix(${matS.join(',')}) "/>
const tp = this.pts[0]; <text font-size="0.5" dominant-baseline="middle" text-anchor="middle" transform="matrix(${matS.join(',')}) translate(1 1.6)">${num_pieces}</text>
<text font-size="1" dominant-baseline="middle" text-anchor="middle" transform="matrix(${matS.join(',')}) translate(1 1)"></text>
var s = `<path transform="matrix(${matS.join(',')})" d="M ${tp.x} ${tp.y}`; </g>`);
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 + ` L ${c.x} ${c.y}`;
}
s = s + `"
stroke="white"
stroke-width="0.1"
stroke-opacity="1"
fill="currentColor" />`;
stream.push( s );
} }
bounds(S) { bounds(S) {
const points = this.pts.map(p => transPt(S,p)); const points = this.pts.map(p => S.transform(p));
return { return {
minx: Math.min(...points.map(p => p.x)), minx: Math.min(...points.map(p => p.x)),
miny: Math.min(...points.map(p => p.y)), miny: Math.min(...points.map(p => p.y)),
@ -219,46 +197,39 @@ fill="currentColor" />`;
} }
* flatten(S) { * 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)); const ymax = Math.max(...points.map(p => p.y));
yield {points, ymax, shape: this}; yield {points, ymax, shape: this};
} }
} }
class Meta class Meta {
{ constructor() {
constructor()
{
this.geoms = []; this.geoms = [];
this.quad = []; this.quad = [];
} }
addChild( g, T ) addChild( g, T ) {
{
this.geoms.push( { geom : g, xform: T } ); this.geoms.push( { geom : g, xform: T } );
} }
draw( S ) draw( S ) {
{
for( let g of this.geoms ) { 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 {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 ) { for( let g of this.geoms ) {
g.geom.streamSVG( mul(S,g.xform), stream ); g.geom.streamSVG( S.mul(g.xform), stream );
} }
} }
bounds(S) { 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 { return {
minx: Math.min(...sub_bounds.map(b=>b.minx)), minx: Math.min(...sub_bounds.map(b=>b.minx)),
miny: Math.min(...sub_bounds.map(b=>b.miny)), miny: Math.min(...sub_bounds.map(b=>b.miny)),
@ -269,7 +240,7 @@ class Meta
* flatten(S) { * flatten(S) {
for(let g of this.geoms) { 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': case 'Gamma':
const mystic = new Meta(); const mystic = new Meta();
out.push(ident); 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 { } else {
/* /*
@ -307,19 +278,33 @@ function tiles(level, label) {
* The layout of subtiles depends on the larger tile. * The layout of subtiles depends on the larger tile.
* *
*/ */
const labels = ['Delta', 'Theta', 'Lambda', 'Xi', const labels = [
'Pi', 'Sigma', 'Phi', 'Psi']; 'Delta',
'Theta',
'Lambda',
'Xi',
'Pi',
'Sigma',
'Phi',
'Psi'
];
const sublevels = Object.fromEntries(labels.map(label => tiles(level-1, label))); const sublevels = Object.fromEntries(labels.map(label => tiles(level-1, label)));
const subquad = sublevels['Delta'].quad; 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. // How to get from each subtile to the next.
const t_rules = [ const t_rules = [
[60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1], [60, 3, 1],
[0, 2, 0], [60, 3, 1], [-120, 3, 3] ]; [0, 2, 0],
[60, 3, 1],
[60, 3, 1],
[0, 2, 0],
[60, 3, 1],
[-120, 3, 3]
];
let Ts = [ident]; let Ts = [ident];
@ -329,12 +314,12 @@ function tiles(level, label) {
for( const [ang,from,to] of t_rules ) { for( const [ang,from,to] of t_rules ) {
total_ang += ang; total_ang += ang;
if( ang != 0 ) { if( ang != 0 ) {
rot = trot( radians( total_ang ) ); rot = Matrix.rotation( radians( total_ang ) );
tquad = subquad.map(q => transPt(rot,q)); tquad = subquad.map(q => rot.transform(q));
} }
const ttt = transTo( tquad[to], transPt( Ts[Ts.length-1], subquad[from] ) ); const ttt = Matrix.translateTo( tquad[to], Ts[Ts.length-1].transform( subquad[from] ) );
Ts.push( mul( ttt, rot ) ); Ts.push( ttt.mul( rot ) );
} }
Ts = Ts.map(t => mul(reflection, t)); Ts = Ts.map(t => mul(reflection, t));
@ -350,12 +335,13 @@ function tiles(level, label) {
'Pi' : ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'], 'Pi' : ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'],
'Sigma' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'], 'Sigma' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'],
'Phi' : ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','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 = [ const super_quad = [
transPt( Ts[6], subquad[2] ), Ts[6].transform(subquad[2] ),
transPt( Ts[5], subquad[1] ), Ts[5].transform(subquad[1] ),
transPt( Ts[3], subquad[2] ), Ts[3].transform(subquad[2] ),
transPt( Ts[0], subquad[1] ) ]; Ts[0].transform(subquad[1] ) ];
} }
return {quad, tiles: out}; return {quad, tiles: out};
} }
@ -373,7 +359,7 @@ function buildSpectreBase()
const mystic = new Meta(); const mystic = new Meta();
mystic.addChild( new Shape( spectre, base_quad, 'Gamma1' ), ident ); mystic.addChild( new Shape( spectre, base_quad, 'Gamma1' ), ident );
mystic.addChild( new Shape( spectre, base_quad, 'Gamma2' ), 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; mystic.quad = base_quad;
ret['Gamma'] = mystic; ret['Gamma'] = mystic;
@ -385,14 +371,12 @@ function buildHatTurtleBase( hat_dominant )
const r3 = 1.7320508075688772; const r3 = 1.7320508075688772;
const hr3 = 0.8660254037844386; const hr3 = 0.8660254037844386;
function hexPt( x, y ) function hexPt( x, y ) {
{ return new Point( x + 0.5*y, -hr3*y );
return pt( x + 0.5*y, -hr3*y );
} }
function hexPt2( x, y ) function hexPt2( x, y ) {
{ return new Point( x + hr3*y, -0.5*y );
return pt( x + hr3*y, -0.5*y );
} }
const hat = [ const hat = [
@ -424,7 +408,7 @@ function buildHatTurtleBase( hat_dominant )
const mystic = new Meta(); const mystic = new Meta();
mystic.addChild( new Shape( hat, hat_keys, 'Gamma1' ), ident ); mystic.addChild( new Shape( hat, hat_keys, 'Gamma1' ), ident );
mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma2' ), 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; mystic.quad = hat_keys;
ret['Gamma'] = mystic; ret['Gamma'] = mystic;
} else { } else {
@ -436,7 +420,7 @@ function buildHatTurtleBase( hat_dominant )
const mystic = new Meta(); const mystic = new Meta();
mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma1' ), ident ); mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma1' ), ident );
mystic.addChild( new Shape( hat, hat_keys, 'Gamma2' ), 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; mystic.quad = turtle_keys;
ret['Gamma'] = mystic; ret['Gamma'] = mystic;
} }
@ -449,12 +433,12 @@ function buildHexBase()
const hr3 = 0.8660254037844386; const hr3 = 0.8660254037844386;
const hex = [ const hex = [
pt(0, 0), new Point(0, 0),
pt(1.0, 0.0), new Point(1.0, 0.0),
pt(1.5, hr3), new Point(1.5, hr3),
pt(1, 2*hr3), new Point(1, 2*hr3),
pt(0, 2*hr3), new Point(0, 2*hr3),
pt(-0.5, hr3) new Point(-0.5, hr3)
]; ];
const hex_keys = [ hex[1], hex[2], hex[3], hex[5] ]; const hex_keys = [ hex[1], hex[2], hex[3], hex[5] ];
@ -476,7 +460,7 @@ function buildSupertiles( sys )
// supertiles. // supertiles.
const quad = sys['Delta'].quad; 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 = [ const t_rules = [
[60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1], [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 ) { for( const [ang,from,to] of t_rules ) {
total_ang += ang; total_ang += ang;
if( ang != 0 ) { if( ang != 0 ) {
rot = trot( radians( total_ang ) ); rot = Matrix.rotation( radians( total_ang ) );
for( i = 0; i < 4; ++i ) { for( i = 0; i < 4; ++i ) {
tquad[i] = transPt( rot, quad[i] ); tquad[i] = rot.transform(quad[i] );
} }
} }
const ttt = transTo( tquad[to], const ttt = Matrix.translateTo( tquad[to],
transPt( Ts[Ts.length-1], quad[from] ) ); Ts[Ts.length-1].transform(quad[from] ) );
Ts.push( mul( ttt, rot ) ); Ts.push( ttt.mul( rot ) );
} }
for( let idx = 0; idx < Ts.length; ++idx ) { 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. // Now build the actual supertiles, labelling appropriately.
@ -516,10 +500,10 @@ function buildSupertiles( sys )
'Phi' : ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','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 = [ const super_quad = [
transPt( Ts[6], quad[2] ), Ts[6].transform(quad[2] ),
transPt( Ts[5], quad[1] ), Ts[5].transform(quad[1] ),
transPt( Ts[3], quad[2] ), Ts[3].transform(quad[2] ),
transPt( Ts[0], quad[1] ) ]; Ts[0].transform(quad[1] ) ];
const ret = {}; const ret = {};
@ -575,16 +559,13 @@ function hexToHSL(H) {
let last_num_iterations; let last_num_iterations;
function get_settings() { 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( return Object.fromEntries(
Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value]) Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value])
); );
} }
function rebuild() { function rebuild() {
num_pieces = 0;
const svg = document.querySelector('svg'); const svg = document.querySelector('svg');
const settings = get_settings(); const settings = get_settings();
let sys = buildSpectreBase(false); let sys = buildSpectreBase(false);
@ -611,127 +592,82 @@ function rebuild() {
const b = g.getBoundingClientRect(); const b = g.getBoundingClientRect();
const x = (b.x - viewbox.x) / viewbox.width; const x = (b.x - viewbox.x) / viewbox.width;
const y = (b.y - viewbox.y) / viewbox.height; const y = (b.y - viewbox.y) / viewbox.height;
const rule = new Function('t','x','y', settings.colouring_rule); g.style.color = '#ccc';
g.style.color = rule(t,x,y);
} }
} }
visit(board,50); visit(board,50);
}
function finish() { svg.addEventListener('click', e => {
const svg = document.querySelector('svg'); console.log('click');
const viewbox = svg.getBoundingClientRect(); if(!last_click) {
Array.from(document.querySelectorAll('#board use, #board path')).filter(g=>{ return;
const b = g.getBoundingClientRect(); }
return (b.x<viewbox.x || b.y<viewbox.y || b.x+b.width>viewbox.x+viewbox.width || b.y+b.height>viewbox.y+viewbox.height); let tile = e.target;
}).forEach(g => g.parentElement.removeChild(g)) while(tile && !tile.classList.contains('tile')) {
tile = tile.parentElement;
}
if(!tile) {
return;
}
const transforms = Array.from(document.querySelectorAll('#board use')).map(g=>{ for(let el of svg.querySelectorAll('.highlight')) {
const {a,b,c,d,e,f} = g.transform.baseVal[0].matrix; el.classList.remove('highlight');
return [ }
[a,c,0,e], tile.classList.add('highlight');
[b,d,0,f],
[0,0,1,0]
];
}); });
console.log(transforms);
navigator.clipboard.writeText(svg.outerHTML);
} }
function setup() { function update_display() {
const settings = get_settings(); const settings = get_settings();
const svg = document.querySelector('svg'); const svg = document.querySelector('svg');
//const bounds = sys.bounds(ident); //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 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; 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))); document.getElementById('spectre').setAttribute('transform',`translate(${mx},${my}) translate(${-mx},${-my})`);
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})`);
if(settings.num_iterations != last_num_iterations) { if(settings.num_iterations != last_num_iterations) {
rebuild(); rebuild();
last_num_iterations = settings.num_iterations; 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')) { for(let i of document.querySelectorAll('input')) {
i.addEventListener('input', setup); i.addEventListener('input', update_display);
i.addEventListener('change', setup) 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());