2023-07-13 08:12:36 +01:00
|
|
|
// derived from https://cs.uwaterloo.ca/~csk/spectre/spectre.js
|
|
|
|
|
|
|
|
|
|
|
|
const {PI, cos, sin} = Math;
|
|
|
|
|
|
|
|
function radians(degrees) {
|
|
|
|
return degrees * PI / 180;
|
|
|
|
}
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
let num_pieces = 0;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
class Point {
|
|
|
|
constructor(x,y) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
add(q) {
|
2025-05-07 16:18:06 +01:00
|
|
|
return { x : this.x + q.x, y : this.y + q.y };
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
sub(q) {
|
2025-05-07 16:18:06 +01:00
|
|
|
return { x : this.x - q.x, y : this.y - q.y };
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
frame(p, q, a, b) {
|
2025-05-07 16:18:06 +01:00
|
|
|
return{ x : this.x + a*p.x + b*q.x, y : this.y + a*p.y + b*q.y };
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
class Matrix {
|
|
|
|
constructor(mat) {
|
|
|
|
this.mat = mat;
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
determinant() {
|
|
|
|
const T = this.mat;
|
|
|
|
const det = T[0]*T[4] - T[1]*T[3];
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
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
|
|
|
|
]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
mul(other) {
|
2025-05-07 16:18:06 +01:00
|
|
|
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]
|
|
|
|
]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
// Rotation matrix
|
2025-05-07 16:46:54 +01:00
|
|
|
static rotation(ang) {
|
|
|
|
const c = cos(ang);
|
|
|
|
const s = sin(ang);
|
2025-05-07 16:18:06 +01:00
|
|
|
return new Matrix([c, -s, 0, s, c, 0]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
static scale(x,y) {
|
|
|
|
return new Matrix([x,0,0,0,y,0]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
static translate(tx, ty) {
|
2025-05-07 16:18:06 +01:00
|
|
|
return new Matrix([1, 0, tx, 0, 1, ty]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
// Translation matrix moving p to q
|
2025-05-07 16:46:54 +01:00
|
|
|
static translateTo(p, q) {
|
|
|
|
return Matrix.translate(q.x - p.x, q.y - p.y);
|
2025-05-07 16:18:06 +01:00
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
transform(P) {
|
2025-05-07 16:18:06 +01:00
|
|
|
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]);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const ident = new Matrix([1,0,0,0,1,0]);
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const spectre = [
|
|
|
|
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)
|
|
|
|
];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
function getsvg(event) {
|
|
|
|
let t = event.target;
|
|
|
|
while(t && t.tagName.toLowerCase()!='svg') {
|
2025-05-07 16:46:54 +01:00
|
|
|
t = t.parentElement;
|
2025-05-07 16:18:06 +01:00
|
|
|
}
|
|
|
|
return t;
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
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;
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
class Tile {
|
|
|
|
constructor(pts, quad) {
|
|
|
|
this.pts = pts;
|
|
|
|
this.quad = quad;
|
|
|
|
|
|
|
|
this.pts = [pts[pts.length-1]];
|
|
|
|
for(const p of pts) {
|
|
|
|
const prev = this.pts[this.pts.length-1];
|
|
|
|
const v = p.sub(prev);
|
|
|
|
const w = new Point(-v.y, v.x);
|
|
|
|
this.pts.push(prev.frame(v, w, 0.5, -0.3));
|
|
|
|
this.pts.push(prev.frame(v, w, 0.5, 0.3));
|
|
|
|
this.pts.push(p);
|
|
|
|
}
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
streamSVG(S, stream) {
|
|
|
|
const tpts = this.pts.map(p => S.transform(p));
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const [a,c,e,b,d,f] = S.mat;
|
2023-07-13 08:12:36 +01:00
|
|
|
const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3));
|
2025-05-07 16:18:06 +01:00
|
|
|
num_pieces += 1;
|
2025-05-07 16:46:54 +01:00
|
|
|
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>
|
2025-05-07 16:18:06 +01:00
|
|
|
</g>`);
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
class Metatile {
|
|
|
|
constructor() {
|
|
|
|
this.geoms = [];
|
|
|
|
this.quad = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
addChild(g, T) {
|
|
|
|
this.geoms.push({ geom : g, xform: T });
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
streamSVG(S, stream) {
|
2023-07-13 08:12:36 +01:00
|
|
|
for(let g of this.geoms) {
|
2025-05-07 16:46:54 +01:00
|
|
|
g.geom.streamSVG(S.mul(g.xform), stream);
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
2025-05-07 16:46:54 +01:00
|
|
|
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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':
|
2025-05-07 16:46:54 +01:00
|
|
|
const mystic = new Metatile();
|
2023-07-13 08:12:36 +01:00
|
|
|
out.push(ident);
|
2025-05-07 16:46:54 +01:00
|
|
|
out.push(Matrix.translate(spectre[8].x, spectre[8].y).mul(Matrix.rotation(PI / 6)));
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
} 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.
|
|
|
|
*
|
|
|
|
*/
|
2025-05-07 16:18:06 +01:00
|
|
|
const labels = [
|
|
|
|
'Delta',
|
|
|
|
'Theta',
|
|
|
|
'Lambda',
|
|
|
|
'Xi',
|
2025-05-07 16:46:54 +01:00
|
|
|
'Pi',
|
2025-05-07 16:18:06 +01:00
|
|
|
'Sigma',
|
|
|
|
'Phi',
|
|
|
|
'Psi'
|
|
|
|
];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
|
|
|
const sublevels = Object.fromEntries(labels.map(label => tiles(level-1, label)));
|
|
|
|
const subquad = sublevels['Delta'].quad;
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const reflection = Matrix.scale(-1,1);
|
2023-07-13 08:12:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
// How to get from each subtile to the next.
|
|
|
|
const t_rules = [
|
2025-05-07 16:18:06 +01:00
|
|
|
[60, 3, 1],
|
|
|
|
[0, 2, 0],
|
|
|
|
[60, 3, 1],
|
|
|
|
[60, 3, 1],
|
|
|
|
[0, 2, 0],
|
|
|
|
[60, 3, 1],
|
|
|
|
[-120, 3, 3]
|
|
|
|
];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
|
2023-07-13 08:12:36 +01:00
|
|
|
let Ts = [ident];
|
|
|
|
let total_ang = 0;
|
|
|
|
let rot = ident;
|
|
|
|
let tquad = [...subquad];
|
2025-05-07 16:46:54 +01:00
|
|
|
for(const [ang,from,to] of t_rules) {
|
2023-07-13 08:12:36 +01:00
|
|
|
total_ang += ang;
|
2025-05-07 16:46:54 +01:00
|
|
|
if(ang != 0) {
|
|
|
|
rot = Matrix.rotation(radians(total_ang));
|
2025-05-07 16:18:06 +01:00
|
|
|
tquad = subquad.map(q => rot.transform(q));
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const ttt = Matrix.translateTo(tquad[to], Ts[Ts.length-1].transform(subquad[from]));
|
|
|
|
Ts.push(ttt.mul(rot));
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Ts = Ts.map(t => mul(reflection, t));
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
|
2023-07-13 08:12:36 +01:00
|
|
|
// 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'],
|
2025-05-07 16:18:06 +01:00
|
|
|
'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma']
|
|
|
|
};
|
2023-07-13 08:12:36 +01:00
|
|
|
const super_quad = [
|
2025-05-07 16:46:54 +01:00
|
|
|
Ts[6].transform(subquad[2]),
|
|
|
|
Ts[5].transform(subquad[1]),
|
|
|
|
Ts[3].transform(subquad[2]),
|
|
|
|
Ts[0].transform(subquad[1]) ];
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
return {quad, tiles: out};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
let last_num_iterations;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
function get_settings() {
|
|
|
|
return Object.fromEntries(
|
|
|
|
Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value])
|
|
|
|
);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
class Builder {
|
|
|
|
constructor(settings) {
|
|
|
|
this.settings = settings;
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
build() {
|
|
|
|
const {settings} = this;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
num_pieces = 0;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
let sys = this.buildSpectreBase();
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
for(let i=0;i<settings.num_iterations;i++) {
|
|
|
|
sys = this.buildSupertiles(sys);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
sys = sys['Delta'];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
this.sys = sys;
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
buildSpectreBase() {
|
|
|
|
const ret = {};
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
for(let lab of ['Delta', 'Theta', 'Lambda', 'Xi',
|
|
|
|
'Pi', 'Sigma', 'Phi', 'Psi']) {
|
|
|
|
ret[lab] = new Tile(spectre, base_quad, lab);
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const mystic = new Metatile();
|
|
|
|
mystic.addChild(new Tile(spectre, base_quad, 'Gamma1'), ident);
|
|
|
|
mystic.addChild(new Tile(spectre, base_quad, 'Gamma2'),
|
|
|
|
Matrix.translate(spectre[8].x, spectre[8].y).mul(Matrix.rotation(PI / 6)));
|
|
|
|
mystic.quad = base_quad;
|
|
|
|
ret['Gamma'] = mystic;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
return ret;
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
buildSupertiles(sys) {
|
|
|
|
// First, use any of the nine-unit tiles in sys to obtain
|
|
|
|
// a list of transformation matrices for placing tiles within
|
|
|
|
// supertiles.
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const quad = sys['Delta'].quad;
|
|
|
|
const R = new Matrix([-1,0,0,0,1,0]);
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const t_rules = [
|
|
|
|
[60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1],
|
|
|
|
[0, 2, 0], [60, 3, 1], [-120, 3, 3] ];
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const Ts = [ident];
|
|
|
|
let total_ang = 0;
|
|
|
|
let rot = ident;
|
|
|
|
const tquad = [...quad];
|
|
|
|
for(const [ang,from,to] of t_rules) {
|
|
|
|
total_ang += ang;
|
|
|
|
if(ang != 0) {
|
|
|
|
rot = Matrix.rotation(radians(total_ang));
|
|
|
|
for(let i = 0; i < 4; ++i) {
|
|
|
|
tquad[i] = rot.transform(quad[i]);
|
|
|
|
}
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
const ttt = Matrix.translateTo(tquad[to],
|
|
|
|
Ts[Ts.length-1].transform(quad[from]));
|
|
|
|
Ts.push(ttt.mul(rot));
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
for(let idx = 0; idx < Ts.length; ++idx) {
|
|
|
|
Ts[idx] = R.mul(Ts[idx]);
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
2025-05-07 16:46:54 +01:00
|
|
|
|
|
|
|
// Now build the actual supertiles, labelling appropriately.
|
|
|
|
const super_rules = {
|
|
|
|
'Gamma' : ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'],
|
|
|
|
'Delta' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
|
|
|
|
'Theta' : ['Psi','Delta','Pi','Phi','Sigma','Pi','Phi','Gamma'],
|
|
|
|
'Lambda' : ['Psi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
|
|
|
|
'Xi' : ['Psi','Delta','Pi','Phi','Sigma','Psi','Phi','Gamma'],
|
|
|
|
'Pi' : ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'],
|
|
|
|
'Sigma' : ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'],
|
|
|
|
'Phi' : ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','Gamma'],
|
|
|
|
'Psi' : ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] };
|
|
|
|
const super_quad = [
|
|
|
|
Ts[6].transform(quad[2]),
|
|
|
|
Ts[5].transform(quad[1]),
|
|
|
|
Ts[3].transform(quad[2]),
|
|
|
|
Ts[0].transform(quad[1]) ];
|
|
|
|
|
|
|
|
const ret = {};
|
|
|
|
|
|
|
|
for(const [lab, subs] of Object.entries(super_rules)) {
|
|
|
|
const sup = new Metatile();
|
|
|
|
for(let idx = 0; idx < 8; ++idx) {
|
|
|
|
if(subs[idx] == 'null') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
sup.addChild(sys[subs[idx]], Ts[idx]);
|
|
|
|
}
|
|
|
|
sup.quad = super_quad;
|
|
|
|
|
|
|
|
ret[lab] = sup;
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
2025-05-07 16:46:54 +01:00
|
|
|
|
|
|
|
return ret;
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
|
2025-05-07 16:46:54 +01:00
|
|
|
draw() {
|
|
|
|
const {sys} = this;
|
|
|
|
|
|
|
|
const drawing = [];
|
|
|
|
|
|
|
|
const svg = document.querySelector('svg');
|
|
|
|
|
|
|
|
const board = svg.querySelector('#board');
|
|
|
|
board.innerHTML = '';
|
|
|
|
const R = new Matrix([-1,0,0,0,1,0]);
|
|
|
|
const m = this.settings.num_iterations % 2 == 0 ? ident : R;
|
|
|
|
sys.streamSVG(m, drawing);
|
|
|
|
board.innerHTML = drawing.join(' ');
|
|
|
|
|
|
|
|
const viewbox = svg.getBoundingClientRect();
|
|
|
|
function visit(g, t) {
|
|
|
|
for(let c of g.children) {
|
|
|
|
visit(c, t+Math.random()*10-5);
|
|
|
|
}
|
|
|
|
if(g.tagName=='path') {
|
|
|
|
const b = g.getBoundingClientRect();
|
|
|
|
const x = (b.x - viewbox.x) / viewbox.width;
|
|
|
|
const y = (b.y - viewbox.y) / viewbox.height;
|
|
|
|
g.style.color = '#ccc';
|
|
|
|
}
|
2025-05-07 16:18:06 +01:00
|
|
|
}
|
2025-05-07 16:46:54 +01:00
|
|
|
visit(board,50);
|
|
|
|
|
|
|
|
svg.addEventListener('click', e => {
|
|
|
|
if(!last_click) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let tile = e.target;
|
|
|
|
while(tile && !tile.classList.contains('tile')) {
|
|
|
|
tile = tile.parentElement;
|
|
|
|
}
|
|
|
|
if(!tile) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for(let el of svg.querySelectorAll('.highlight')) {
|
|
|
|
el.classList.remove('highlight');
|
|
|
|
}
|
|
|
|
tile.classList.add('highlight');
|
|
|
|
});
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
function update_display() {
|
2023-07-13 08:12:36 +01:00
|
|
|
const settings = get_settings();
|
|
|
|
const svg = document.querySelector('svg');
|
2025-05-07 16:46:54 +01:00
|
|
|
svg.setAttribute('viewBox',`${-settings.scale/2} ${-settings.scale/2} ${settings.scale} ${settings.scale}`);
|
2025-05-07 11:32:22 +01:00
|
|
|
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;
|
2025-05-07 16:18:06 +01:00
|
|
|
document.getElementById('spectre').setAttribute('transform',`translate(${mx},${my}) translate(${-mx},${-my})`);
|
2025-05-07 16:46:54 +01:00
|
|
|
|
2023-07-13 08:12:36 +01:00
|
|
|
if(settings.num_iterations != last_num_iterations) {
|
2025-05-07 16:46:54 +01:00
|
|
|
const builder = new Builder(settings);
|
|
|
|
builder.build();
|
|
|
|
builder.draw();
|
2023-07-13 08:12:36 +01:00
|
|
|
last_num_iterations = settings.num_iterations;
|
|
|
|
}
|
2025-05-07 16:18:06 +01:00
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
for(let i of document.querySelectorAll('input')) {
|
|
|
|
i.addEventListener('input', update_display);
|
|
|
|
i.addEventListener('change', update_display)
|
|
|
|
}
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
update_display();
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
let opos;
|
|
|
|
let dragging;
|
|
|
|
let pan = {x:0, y:0};
|
|
|
|
let npan = pan;
|
|
|
|
let last_click = false;
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
const svg = document.querySelector('svg');
|
|
|
|
svg.addEventListener('pointerdown', e => {
|
|
|
|
opos = getcoords(e);
|
|
|
|
dragging = true;
|
|
|
|
});
|
2023-07-13 08:12:36 +01:00
|
|
|
|
2025-05-07 16:18:06 +01:00
|
|
|
svg.addEventListener('pointermove', e => {
|
|
|
|
if(!dragging) {
|
|
|
|
return;
|
2023-07-13 08:12:36 +01:00
|
|
|
}
|
2025-05-07 16:18:06 +01:00
|
|
|
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);
|
|
|
|
last_click = d < 0.5;
|
|
|
|
pan = npan;
|
|
|
|
});
|
2023-07-13 08:12:36 +01:00
|
|
|
|