let to_screen = [20, 0, 0, 0, -20, 0];
let lw_scale = 1;
let tiles;
let level;
let scale_centre;
let scale_start;
let scale_ts;
let reset_button;
let subst_button;
let translate_button;
let scale_button;
let draw_hats;
let draw_super;
let radio;
let dragging = false;
let uibox = true;
let box_height = 10;
let svg_serial = 0;
const cols = {};
let black;
function getSVGID()
{
const ret = 't' + String(svg_serial).padStart( 5, '0' );
++svg_serial;
return ret;
}
function drawPolygon( shape, T, f, s, w )
{
if( f != null ) {
fill( f );
} else {
noFill();
}
if( s != null ) {
stroke( s );
strokeWeight( w * lw_scale );
} else {
noStroke();
}
beginShape();
for( let p of shape ) {
const tp = transPt( T, p );
vertex( tp.x, tp.y );
}
endShape( CLOSE );
}
function polygonToSVG( shape, id, f, s, w )
{
let verts = '';
for( let p of shape ) {
if( verts.length > 0 ) {
verts = verts + ' ';
}
verts = verts + p.x + ',' + p.y;
}
let ids = '';
if( id != null ) {
ids = ` id="${id}"`;
}
let str = ' stroke="none"';
if( s != null ) {
str = ` stroke="rgb(${red(s)},${green(s)},${blue(s)})" stroke-width="${w}"`;
}
let fil = ' fill="none"';
if( f != null ) {
fil = ` fill="rgb(${red(f)},${green(f)},${blue(f)})"`;
}
return ` `;
}
function getSVGInstance( id, T )
{
return ` `;
}
// The base level of the scene, a single hat tile, including a label
// for colouring
class HatTile
{
constructor( label )
{
this.label = label;
this.svg_id = null;
}
draw( S, level )
{
drawPolygon(
hat_outline, S, cols[this.label].color(), black, 1 );
}
resetSVG()
{
this.svg_id = null;
}
buildSVGDefs( stream, sc )
{
if( this.svg_id != null ) {
return;
}
this.svg_id = getSVGID();
stream.push( polygonToSVG( hat_outline, this.svg_id,
cols[this.label].color(), black, lw_scale/sc ) );
}
getSVGStrokeID()
{
return null;
}
getSVGFillID()
{
return this.svg_id;
}
getText( stream, T )
{
// Write out the top two rows of an affine transformation matrix
// giving the location of this hat, together with the type of
// this tile.
stream.push( `${this.label} ${T[0]} ${T[1]} ${T[2]} ${T[3]} ${T[4]} ${T[5]}` )
}
}
// A group that collects a list of transformed children and an outline
class MetaTile
{
constructor( shape, width )
{
this.shape = shape;
this.width = width;
this.children = [];
this.svg_id = null;
}
addChild( T, geom )
{
this.children.push( { T : T, geom : geom } );
}
evalChild( n, i )
{
return transPt( this.children[n].T, this.children[n].geom.shape[i] );
}
draw( S, level )
{
if( level > 0 ) {
for( let g of this.children ) {
g.geom.draw( mul( S, g.T ), level - 1 );
}
} else {
drawPolygon( this.shape, S, null, black, this.width );
}
}
recentre()
{
let cx = 0;
let cy = 0;
for( let p of this.shape ) {
cx += p.x;
cy += p.y;
}
cx /= this.shape.length;
cy /= this.shape.length;
const tr = pt( -cx, -cy );
for( let idx = 0; idx < this.shape.length; ++idx ) {
this.shape[idx] = padd( this.shape[idx], tr );
}
const M = ttrans( -cx, -cy );
for( let ch of this.children ) {
ch.T = mul( M, ch.T );
}
}
resetSVG()
{
for( let ch of this.children ) {
ch.geom.resetSVG();
}
this.svg_id = null;
}
buildSVGDefs( stream, sc )
{
if( this.svg_id != null ) {
return;
}
this.svg_id = getSVGID();
for( let ch of this.children ) {
ch.geom.buildSVGDefs( stream, sc );
}
// Construct a fill group that must live at a logical lowest
// layer in the draw order.
stream.push( ` ` );
for( let ch of this.children ) {
const fid = ch.geom.getSVGFillID();
if( fid != null ) {
stream.push( getSVGInstance( fid, ch.T ) );
}
}
stream.push( ' ' );
// Construct a stroke group that must live above all fill groups.
stream.push( ` ` );
for( let ch of this.children ) {
const sid = ch.geom.getSVGStrokeID();
if( sid != null ) {
stream.push( getSVGInstance( sid, ch.T ) );
}
}
stream.push( polygonToSVG( this.shape,
null, null, black, this.width*lw_scale/sc ) );
stream.push( ' ' );
}
getSVGStrokeID()
{
return `${this.svg_id}s`;
}
getSVGFillID()
{
return `${this.svg_id}f`;
}
getText( stream, T )
{
for( let g of this.children ) {
g.geom.getText( stream, mul( T, g.T ) );
}
}
}
const H1_hat = new HatTile( 'H1' );
const H_hat = new HatTile( 'H' );
const T_hat = new HatTile( 'T' );
const P_hat = new HatTile( 'P' );
const F_hat = new HatTile( 'F' );
const H_init = (function () {
const H_outline = [
pt( 0, 0 ), pt( 4, 0 ), pt( 4.5, hr3 ),
pt( 2.5, 5 * hr3 ), pt( 1.5, 5 * hr3 ), pt( -0.5, hr3 ) ];
const meta = new MetaTile( H_outline, 2 );
meta.addChild(
matchTwo(
hat_outline[5], hat_outline[7], H_outline[5], H_outline[0] ),
H_hat );
meta.addChild(
matchTwo(
hat_outline[9], hat_outline[11], H_outline[1], H_outline[2] ),
H_hat );
meta.addChild(
matchTwo(
hat_outline[5], hat_outline[7], H_outline[3], H_outline[4] ),
H_hat );
meta.addChild(
mul( ttrans( 2.5, hr3 ),
mul(
[-0.5,-hr3,0,hr3,-0.5,0],
[0.5,0,0,0,-0.5,0] ) ),
H1_hat );
return meta; }());
const T_init = (function () {
const T_outline = [
pt( 0, 0 ), pt( 3, 0 ), pt( 1.5, 3 * hr3 ) ];
const meta = new MetaTile( T_outline, 2 );
meta.addChild(
[0.5, 0, 0.5, 0, 0.5, hr3],
T_hat );
return meta; }());
const P_init = (function () {
const P_outline = [
pt( 0, 0 ), pt( 4, 0 ),
pt( 3, 2 * hr3 ), pt( -1, 2 * hr3 ) ];
const meta = new MetaTile( P_outline, 2 );
meta.addChild(
[0.5, 0, 1.5, 0, 0.5, hr3],
P_hat );
meta.addChild(
mul( ttrans( 0, 2 * hr3 ),
mul( [0.5, hr3, 0, -hr3, 0.5, 0],
[0.5, 0.0, 0.0, 0.0, 0.5, 0.0] ) ),
P_hat );
return meta; }());
const F_init = (function () {
const F_outline = [
pt( 0, 0 ), pt( 3, 0 ),
pt( 3.5, hr3 ), pt( 3, 2 * hr3 ), pt( -1, 2 * hr3 ) ];
const meta = new MetaTile( F_outline, 2 );
meta.addChild(
[0.5, 0, 1.5, 0, 0.5, hr3],
F_hat );
meta.addChild(
mul( ttrans( 0, 2 * hr3 ),
mul( [0.5, hr3, 0, -hr3, 0.5, 0],
[0.5, 0.0, 0.0, 0.0, 0.5, 0.0] ) ),
F_hat );
return meta; }());
function constructPatch( H, T, P, F )
{
const rules = [
['H'],
[0, 0, 'P', 2],
[1, 0, 'H', 2],
[2, 0, 'P', 2],
[3, 0, 'H', 2],
[4, 4, 'P', 2],
[0, 4, 'F', 3],
[2, 4, 'F', 3],
[4, 1, 3, 2, 'F', 0],
[8, 3, 'H', 0],
[9, 2, 'P', 0],
[10, 2, 'H', 0],
[11, 4, 'P', 2],
[12, 0, 'H', 2],
[13, 0, 'F', 3],
[14, 2, 'F', 1],
[15, 3, 'H', 4],
[8, 2, 'F', 1],
[17, 3, 'H', 0],
[18, 2, 'P', 0],
[19, 2, 'H', 2],
[20, 4, 'F', 3],
[20, 0, 'P', 2],
[22, 0, 'H', 2],
[23, 4, 'F', 3],
[23, 0, 'F', 3],
[16, 0, 'P', 2],
[9, 4, 0, 2, 'T', 2],
[4, 0, 'F', 3]
];
ret = new MetaTile( [], H.width );
shapes = { 'H' : H, 'T' : T, 'P' : P, 'F' : F };
for( let r of rules ) {
if( r.length == 1 ) {
ret.addChild( ident, shapes[r[0]] );
} else if( r.length == 4 ) {
const poly = ret.children[r[0]].geom.shape;
const T = ret.children[r[0]].T;
const P = transPt( T, poly[(r[1]+1)%poly.length] );
const Q = transPt( T, poly[r[1]] );
const nshp = shapes[r[2]];
const npoly = nshp.shape;
ret.addChild(
matchTwo( npoly[r[3]], npoly[(r[3]+1)%npoly.length], P, Q ),
nshp );
} else {
const chP = ret.children[r[0]];
const chQ = ret.children[r[2]];
const P = transPt( chQ.T, chQ.geom.shape[r[3]] );
const Q = transPt( chP.T, chP.geom.shape[r[1]] );
const nshp = shapes[r[4]];
const npoly = nshp.shape;
ret.addChild(
matchTwo( npoly[r[5]], npoly[(r[5]+1)%npoly.length], P, Q ),
nshp );
}
}
return ret;
}
function constructMetatiles( patch )
{
const bps1 = patch.evalChild( 8, 2 );
const bps2 = patch.evalChild( 21, 2 );
const rbps = transPt( rotAbout( bps1, -2.0*PI/3.0 ), bps2 );
const p72 = patch.evalChild( 7, 2 );
const p252 = patch.evalChild( 25, 2 );
const llc = intersect( bps1, rbps,
patch.evalChild( 6, 2 ), p72 );
let w = psub( patch.evalChild( 6, 2 ), llc );
const new_H_outline = [llc, bps1];
w = transPt( trot( -PI/3 ), w );
new_H_outline.push( padd( new_H_outline[1], w ) );
new_H_outline.push( patch.evalChild( 14, 2 ) );
w = transPt( trot( -PI/3 ), w );
new_H_outline.push( psub( new_H_outline[3], w ) );
new_H_outline.push( patch.evalChild( 6, 2 ) );
const new_H = new MetaTile( new_H_outline, patch.width * 2 );
for( let ch of [0, 9, 16, 27, 26, 6, 1, 8, 10, 15] ) {
new_H.addChild( patch.children[ch].T, patch.children[ch].geom );
}
const new_P_outline = [ p72, padd( p72, psub( bps1, llc ) ), bps1, llc ];
const new_P = new MetaTile( new_P_outline, patch.width * 2 );
for( let ch of [7,2,3,4,28] ) {
new_P.addChild( patch.children[ch].T, patch.children[ch].geom );
}
const new_F_outline = [
bps2, patch.evalChild( 24, 2 ), patch.evalChild( 25, 0 ),
p252, padd( p252, psub( llc, bps1 ) ) ];
const new_F = new MetaTile( new_F_outline, patch.width * 2 );
for( let ch of [21,20,22,23,24,25] ) {
new_F.addChild( patch.children[ch].T, patch.children[ch].geom );
}
const AAA = new_H_outline[2];
const BBB = padd( new_H_outline[1],
psub( new_H_outline[4], new_H_outline[5] ) );
const CCC = transPt( rotAbout( BBB, -PI/3 ), AAA );
const new_T_outline = [BBB,CCC,AAA];
const new_T = new MetaTile( new_T_outline, patch.width * 2 );
new_T.addChild( patch.children[11].T, patch.children[11].geom );
new_H.recentre();
new_P.recentre();
new_F.recentre();
new_T.recentre();
return [new_H, new_T, new_P, new_F]
}
function isButtonActive( but )
{
return but.elt.style.border.length > 0;
}
function setButtonActive( but, b )
{
but.elt.style.border = (b ? "3px solid black" : "");
}
function addButton( name, f )
{
const ret = createButton( name );
ret.position( 10, box_height );
ret.size( 125, 25 );
ret.mousePressed( f );
box_height += 30;
return ret;
}
function setup() {
createCanvas( windowWidth, windowHeight );
tiles = [H_init, T_init, P_init, F_init];
level = 1;
black = color( 'black' );
reset_button = addButton( "Reset", function() {
tiles = [H_init, T_init, P_init, F_init];
level = 1;
radio.selected( 'H' );
to_screen = [20, 0, 0, 0, -20, 0];
lw_scale = 1;
setButtonActive( draw_hats, true );
setButtonActive( draw_super, true );
loop();
} );
subst_button = addButton( "Build Supertiles", function() {
const patch = constructPatch( ...tiles );
tiles = constructMetatiles( patch );
++level;
loop();
} );
box_height += 10;
radio = createRadio();
radio.mousePressed( function() { loop() } );
radio.position( 10, box_height );
for( let s of ['H', 'T', 'P', 'F'] ) {
let o = radio.option( s );
o.onclick = loop;
}
radio.selected( 'H' );
box_height += 40;
const cp_info = {
'H1' : [0, 137, 212],
'H' : [148, 205, 235],
'T' : [251, 251, 251],
'P' : [250, 250, 250],
'F' : [191, 191, 191]
};
let count = 0;
for( let [name, col] of Object.entries( cp_info ) ) {
const label = createSpan( name );
label.position( 10 + 70*count, box_height );
const cp = createColorPicker( color( ...col ) );
cp.mousePressed( function() { loop() } );
cp.position( 10 + 70*count, box_height + 20 );
cols[name] = cp;
++count;
if( count == 2 ) {
count = 0;
box_height += 50;
}
}
if( count == 1 ) {
box_height += 50;
}
box_height += 20;
translate_button = addButton( "Translate", function() {
setButtonActive( translate_button, true );
setButtonActive( scale_button, false );
loop();
} );
scale_button = addButton( "Scale", function() {
setButtonActive( translate_button, false );
setButtonActive( scale_button, true );
loop();
} );
setButtonActive( translate_button, true );
box_height += 10;
draw_hats = addButton( "Draw Hats", function() {
setButtonActive( draw_hats, !isButtonActive( draw_hats ) );
loop();
} );
draw_super = addButton( "Draw Supertiles", function() {
setButtonActive( draw_super, !isButtonActive( draw_super ) );
loop();
} );
setButtonActive( draw_hats, true );
setButtonActive( draw_super, true );
box_height += 10;
addButton( "Save PNG", function () {
uibox = false;
draw();
save( "output.png" );
uibox = true;
draw();
} );
addButton( "Save SVG", function () {
svg_serial = 0;
for( let t of tiles ) {
t.resetSVG();
}
const stream = [];
stream.push( `' );
saveStrings( stream, 'output', 'svg' );
} );
addButton( "Save Matrices", function() {
const stream = [];
const idx = {'H':0, 'T':1, 'P':2, 'F':3}[radio.value()];
tiles[idx].getText( stream, ident );
saveStrings( stream, 'output', 'txt' );
} );
box_height -= 5; // remove half the padding
}
function draw()
{
background( 255 );
push();
translate( width/2, height/2 );
const idx = {'H':0, 'T':1, 'P':2, 'F':3}[radio.value()];
if( isButtonActive( draw_hats ) ) {
tiles[idx].draw( to_screen, level );
}
if( isButtonActive( draw_super ) ) {
for( let lev = level - 1; lev >= 0; --lev ) {
tiles[idx].draw( to_screen, lev );
}
}
pop();
if( uibox ) {
stroke( 0 );
strokeWeight( 0.5 );
fill( 255, 220 );
rect( 5, 5, 135, box_height);
}
noLoop();
}
function windowResized()
{
resizeCanvas( windowWidth, windowHeight );
}
function mousePressed()
{
dragging = true;
if( isButtonActive( scale_button ) ) {
scale_centre = transPt( inv( to_screen ), pt( width/2, height/2 ) );
scale_start = pt( mouseX, mouseY );
scale_ts = [...to_screen];
}
loop();
}
function mouseDragged()
{
if( dragging ) {
if( isButtonActive( translate_button ) ) {
to_screen = mul( ttrans( mouseX - pmouseX, mouseY - pmouseY ),
to_screen );
} else if( isButtonActive( scale_button ) ) {
let sc = dist( mouseX, mouseY, width/2, height/2 ) /
dist( scale_start.x, scale_start.y, width/2, height/2 );
to_screen = mul(
mul( ttrans( scale_centre.x, scale_centre.y ),
mul( [sc, 0, 0, 0, sc, 0],
ttrans( -scale_centre.x, -scale_centre.y ) ) ),
scale_ts );
lw_scale = mag( to_screen[0], to_screen[1] ) / 20.0;
}
loop();
return false;
}
}
function mouseReleased()
{
dragging = false;
loop();
}