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>
<html lang="en">
<head>
<title>Aperiodic monotile</title>
<meta charset="utf-8">
<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>
* {
box-sizing: border-box;
}
svg {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
border: 1px solid black;
touch-action: pan-x pan-y;
}
body {
overscroll-behavior: contain;
padding: 0;
margin: 0;
display: grid;
height: 100svh;
grid-template:
"drawing" 50vh
"controls" 1fr
"drawing" 80svh
"controls" min-content
/ 1fr;
}
textarea {
width: 100%;
height: 20em;
}
.tile {
color: #ccc;
}
.tile.highlight {
color: hsl(240,100%,80%);
}
</style>
</head>
<body>
@ -30,7 +46,7 @@ textarea {
<svg viewBox="0 0 190 195" 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"
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-width="0.1"
stroke-opacity="1"
@ -44,44 +60,15 @@ textarea {
<section id="controls">
<fieldset>
<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">
<output for="ox"></output>
<label for="oy">min y</label>
<label for="oy">oy</label>
<input type="number" min="-500" max="500" value="0" id="oy">
<output for="oy"></output>
<label for="width">max x</label>
<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="scale">scale</label>
<input type="range" min="1" max="100" value="19" id="scale">
<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>
<input type="number" min="1" max="5" value="2" id="num_iterations">
</fieldset>
<button type="button" id="rebuild">Rebuild</button>
<button type="button" id="finish">Finish</button>
</section>
</body>

562
script.js
View file

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