"use strict";
window.onload = function() {
const rayHexMin = 80;
const rayHexMax = 160;
const amplNoise = 30;
const minPerX = 1;
const maxPerX = 5;
const minPerY = 1;
const maxPerY = 5;
let canv, ctx; // canvas and context
let maxx, maxy;
let grid;
let nbx, nby;
let rayHex;
let tbSides;
/* for animation */
let events = [];
// shortcuts for Math.…
const mrandom = Math.random;
const mfloor = Math.floor;
const mround = Math.round;
const mceil = Math.ceil;
const mabs = Math.abs;
const mmin = Math.min;
const mmax = Math.max;
const mPI = Math.PI;
const mPIS2 = Math.PI / 2;
const m2PI = Math.PI * 2;
const msin = Math.sin;
const mcos = Math.cos;
const matan2 = Math.atan2;
const mhypot = Math.hypot;
const msqrt = Math.sqrt;
const rac3 = msqrt(3);
const rac3s2 = rac3 / 2;
const mPIS3 = Math.PI / 3;
//-----------------------------------------------------------------
function alea (min, max) {
// random number [min..max[ . If no max is provided, [0..min[
if (typeof max == 'undefined') return min * mrandom();
return min + (max - min) * mrandom();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function intAlea (min, max) {
// random integer number [min..max[ . If no max is provided, [0..min[
if (typeof max == 'undefined') {
max = min; min = 0;
}
return mfloor(min + (max - min) * mrandom());
} // intAlea
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function Noise1DOneShot (period, min = 0, max = 1, random) {
/* returns a 1D single-shot noise generator.
the (optional) random function must return a value between 0 and 1
the returned function has no parameter, and will return a new number every tiime it is called.
If the random function provides reproductible values (and is not used elsewhere), this
one will return reproductible values too.
period should be > 1. The bigger period is, the smoother output noise is
*/
random = random || Math.random;
let currx = random(); // start with random offset
let y0 = min + (max - min) * random(); // 'previous' value
let y1 = min + (max - min) * random(); // 'next' value
let dx = 1 / period;
return function() {
currx += dx;
if (currx > 1) {
currx -= 1;
y0 = y1;
y1 = min + (max - min) * random();
}
let z = (3 - 2 * currx) * currx * currx;
return z * y1 + (1 - z) * y0;
}
} // Noise1DOneShot
//------------------------------------------------------------------------
// class Hexagon
let Hexagon;
{ // scope for Hexagon
let orgx, orgy;
Hexagon = function (kx, ky) {
this.kx = kx;
this.ky = ky;
this.neighbours = [];
this.orient = intAlea(6);
} // function Hexagon
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/* static method */
Hexagon.dimensions = function () {
// coordinates of center of hexagon [0][0]
orgx = (maxx - rayHex * (1.5 * nbx + 0.5)) / 2 + rayHex; // obvious, no ?
orgy = (maxy - (rayHex * rac3 * (nby + 0.5))) / 2 + rayHex * rac3; // yet more obvious
} // Hexagon.dimensions
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.size = function() {
/* computes screen sizes / positions
*/
// centre
this.xc = orgx + this.kx * 1.5 * rayHex;
this.yc = orgy + this.ky * rayHex * rac3;
if (this.kx & 1) this.yc -= rayHex * rac3s2; // odd columns
this.vertices = [[],[],[],[],[],[]] ;
// x coordinates, from left to right
this.vertices[3][0] = this.xc - rayHex;
this.vertices[2][0] = this.vertices[4][0] = this.xc - rayHex / 2;
this.vertices[1][0] = this.vertices[5][0] = this.xc + rayHex / 2;
this.vertices[0][0] = this.xc + rayHex;
// y coordinates, from top to bottom
this.vertices[4][1] = this.vertices[5][1] = this.yc - rayHex * rac3s2;
this.vertices[0][1] = this.vertices[3][1] = this.yc;
this.vertices[1][1] = this.vertices[2][1] = this.yc + rayHex * rac3s2;
// get a 2nd copy of table to avoid many % 6 calculations later
this.vertices = this.vertices.concat(this.vertices);
this.extCenters = [[],[],[],[],[],[]] ;
let dxc = rayHex;
let dyc = rayHex / rac3;
this.rad1 = dyc; // radius fir circles with center in extCenters
for (let k = 0; k < 6; ++k) {
this.extCenters[k][0] = this.xc + dxc * mcos(k * mPIS3) - dyc * msin(k * mPIS3);
this.extCenters[k][1] = this.yc + dxc * msin(k * mPIS3) + dyc * mcos(k * mPIS3);
}
// get a 2nd copy of table to avoid many % 6 calculations later
this.extCenters = this.extCenters.concat(this.extCenters);
this.extCentersB = [[],[],[],[],[],[]] ;
// x coordinates, from left to right
this.extCentersB[3][0] = this.xc - 2 * rayHex;
this.extCentersB[2][0] = this.extCentersB[4][0] = this.xc - rayHex;
this.extCentersB[1][0] = this.extCentersB[5][0] = this.xc + rayHex;
this.extCentersB[0][0] = this.xc + 2 * rayHex;
// y coordinates, from top to bottom
this.extCentersB[4][1] = this.extCentersB[5][1] = this.yc - rayHex * rac3;
this.extCentersB[3][1] = this.extCentersB[0][1] = this.yc;
this.extCentersB[2][1] = this.extCentersB[1][1] = this.yc + rayHex * rac3;
// get a 2nd copy of table to avoid many % 6 calculations later
this.extCentersB = this.extCentersB.concat(this.extCentersB);
} // Hexagon.prototype.size
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawHexagon = function(hue) {
if (! this.vertices) this.size();
let ctxGrid = ctx;
ctxGrid.beginPath();
ctxGrid.moveTo (this.vertices[0][0], this.vertices[0][1]);
ctxGrid.lineTo (this.vertices[1][0], this.vertices[1][1]);
ctxGrid.lineTo (this.vertices[2][0], this.vertices[2][1]);
ctxGrid.lineTo (this.vertices[3][0], this.vertices[3][1]);
ctxGrid.lineTo (this.vertices[4][0], this.vertices[4][1]);
ctxGrid.lineTo (this.vertices[5][0], this.vertices[5][1]);
ctxGrid.lineTo (this.vertices[0][0], this.vertices[0][1]);
ctxGrid.strokeStyle = '#8FF';
ctxGrid.lineWidth = 0.5;
ctxGrid.strokeStyle = `hsl(${hue},100%,60%)`;
ctxGrid.stroke();
} // Hexagon.prototype.drawHexagon
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc1 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R;
xc = x0 = rayHex * (1 - alpha / 2);
y0 = alpha * rayHex * rac3s2;
yc = xc / rac3
R = rayHex * (1 - 2 * alpha) / rac3;
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert - 3) * mPI / 6,(2 * vert + 5) * mPI / 6 , true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc6 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R;
xc = x0 = rayHex * (1 - alpha / 2);
y0 = -alpha * rayHex * rac3s2;
yc = -xc / rac3
R = rayHex * (1 - 2 * alpha) / rac3;
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert + 3) * mPI / 6,(2 * vert - 5) * mPI / 6);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc2 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, angle;
xc = x0 = rayHex;
y0 = 0;
yc = ((0.5 + alpha) * (0.5 + alpha) + 0.75) / rac3 * rayHex;
R = yc;
angle = matan2(rayHex * ( 1/2 + alpha), yc - rayHex * rac3s2);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert - 3) * mPI / 6,(2 * vert - 3) * mPI / 6 - angle , true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc7 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, angle;
x0 = rayHex * (0.5 - alpha);
y0 = rayHex * rac3s2;
xc = rayHex;
yc = ((0.5 + alpha) * (0.5 + alpha) + 0.75) / rac3 * rayHex;
R = yc;
angle = matan2(rayHex * ( 1/2 + alpha), yc - rayHex * rac3s2);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R,(2 * vert - 3) * mPI / 6 - angle , (2 * vert - 3) * mPI / 6);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc3 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, xb, yb, angle;
xc = x0 = rayHex;
y0 = 0;
xb = rayHex * (- 1 / 2 - alpha / 2);
yb = (1 - alpha) * rac3s2 * rayHex;
yc = (yb * yb + xb * xb - x0 * x0 + 2 * x0 * xc - 2 * xb * xc) / 2 / yb;
R = yc;
angle = matan2(rayHex - xb, yc - yb);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert - 3) * mPI / 6,(2 * vert - 3) * mPI / 6 - angle , true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc8 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, xb, yb, angle;
x0 = -(1 + alpha) / 2 * rayHex;
y0 = (1 - alpha) * rac3s2 * rayHex;
xc = rayHex;
xb = rayHex * (- 1 / 2 - alpha / 2);
yb = (1 - alpha) * rac3s2 * rayHex;
yc = (yb * yb + xb * xb - rayHex * rayHex + 2 * rayHex * xc - 2 * xb * xc) / 2 / yb;
R = yc;
angle = matan2(rayHex - xb, yc - yb);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert - 3) * mPI / 6 - angle , (2 * vert - 3) * mPI / 6);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc4 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, xb, yb, angle;
xc = x0 = rayHex;
y0 = 0;
yc = - ((0.5 + alpha) * (0.5 + alpha) + 0.75) / rac3 * rayHex;
R = - yc;
angle = matan2(rayHex * ( 1/2 + alpha), R - rayHex * rac3s2);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert + 3) * mPI / 6,(2 * vert + 3) * mPI / 6 + angle);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc9 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, angle;
x0 = (0.5 - alpha) * rayHex;
y0 = - rayHex * rac3s2;
xc = rayHex;
yc = - ((0.5 + alpha) * (0.5 + alpha) + 0.75) / rac3 * rayHex;
R = - yc;
angle = matan2(rayHex * ( 1/2 + alpha), R - rayHex * rac3s2);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R,(2 * vert + 3) * mPI / 6 + angle, (2 * vert + 3) * mPI / 6, true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc5 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, xb, yb, angle;
xc = x0 = rayHex;
y0 = 0;
xb = rayHex * (- 1 / 2 - alpha / 2);
yb = (alpha - 1) * rac3s2 * rayHex;
yc = (yb * yb + xb * xb - x0 * x0 + 2 * x0 * xc - 2 * xb * xc) / 2 / yb;
R = - yc;
angle = matan2(rayHex - xb, R + yb);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R, (2 * vert + 3) * mPI / 6,(2 * vert + 3) * mPI / 6 + angle);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawArc10 = function (vert, alpha, first = false) {
let x0, y0, xc, yc, R, xb, yb, angle;
x0 = -(1 + alpha) / 2 * rayHex;
y0 = -(1 - alpha) * rayHex * rac3s2;
xc = rayHex;
xb = rayHex * (- 1 / 2 - alpha / 2);
yb = (alpha - 1) * rac3s2 * rayHex;
yc = (yb * yb + xb * xb - rayHex * rayHex + 2 * rayHex * xc - 2 * xb * xc) / 2 / yb;
R = - yc;
angle = matan2(rayHex - xb, R + yb);
[x0, y0] = rotate([x0, y0], 2 * vert);
[xc, yc] = rotate([xc, yc], 2 * vert);
if (first) ctx.moveTo (this.xc + x0, this.yc + y0);
ctx.arc (this.xc + xc, this.yc + yc, R,(2 * vert + 3) * mPI / 6 + angle, (2 * vert + 3) * mPI / 6, true);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/* returns a cell's neighbour
keep track of it for future request
defines itself as its neighbour's neighbour to reduce calculations
returns false if no neighbour
*/
Hexagon.prototype.neighbour = function(side) {
let neigh = this.neighbours[side];
if (neigh instanceof(Hexagon)) return neigh; // known neighbour
if (neigh === false) return false; // known for no neighbour
// do not know yet
if (this.kx & 1) {
neigh = {kx: this.kx + [1, 0, -1, -1, 0, 1][side],
ky: this.ky + [0, 1, 0, -1, -1, -1][side]};
} else {
neigh = {kx: this.kx + [1, 0, -1, -1, 0, 1][side],
ky: this.ky + [1, 1, 1, 0, -1, 0][side]};
}
if (neigh.kx < 0 || neigh.ky <0 || neigh.kx >= nbx || neigh.ky >= nby) {
this.neighbours[side] = false;
return false;
}
neigh = grid[neigh.ky][neigh.kx];
this.neighbours[side] = neigh;
neigh.neighbours[(side + 3) % 6] = this;
return neigh;
} // Hexagon.prototype.neighbour
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hexagon.prototype.drawSide = function(side, alpha, first){
switch (this.styles[side]) {
case 'a' :
this.drawArc6((side + 1) % 6, alpha, first);
break;
case 'b' :
this.drawArc7((side + 5) % 6, 1 - alpha, first);
this.drawArc2((side + 5) % 6, alpha);
break;
case 'c' :
this.drawArc8((side + 4) % 6, 1 - alpha, first);
this.drawArc3((side + 4) % 6, alpha);
break;
case 'd' :
this.drawArc9((side + 2) % 6, alpha, first);
this.drawArc4((side + 2) % 6, 1 - alpha);
break;
case 'e' :
this.drawArc10((side + 3) % 6, alpha, first);
this.drawArc5((side + 3) % 6, 1 - alpha);
break;
}
} // Hexagon.prototype.drawSide
} // scope for Hexagon
//------------------------------------------------------------------------
function rotate (p, k) {
// turn the given point after a rotation of k / 12 turns (k * PI / 6) around the origin
let s = msin (k * mPI / 6);
let c = mcos (k * mPI / 6);
return [p[0] * c - p[1] * s,
p[0] * s + p[1] * c];
} // rotate
//------------------------------------------------------------------------
function createGrid() {
/* create the grid of Hexagons
but does NOT define the crossings between dots inside an hexagon
*/
let hexa;
let tbPer = [];
let tbKind = [];
let perx = intAlea(minPerX, maxPerX + 1);
let pery = intAlea(minPerY, maxPerY + 1);
if (intAlea(3) == 0) { // aperiodic
perx = nbx;
pery = nby;
}
for (let ky = 0; ky < pery; ++ky) {
tbPer[ky] = [];
tbKind[ky] = [];
for (let kx = 0; kx < perx; ++kx) {
tbPer[ky][kx] = intAlea(6); // random orientation
tbKind[ky][kx] = intAlea(3);
} // for kx
} // for ky
grid = [];
for (let ky = 0; ky < nby; ++ky) {
grid[ky] = []
for (let kx = 0; kx < nbx; ++kx) {
hexa = new Hexagon(kx, ky);
grid[ky][kx] = hexa;
grid[ky][kx].orient = tbPer[ky % pery][kx % perx];
grid[ky][kx].kind = tbKind[ky % pery][kx % perx];
} // for kx
} // for ky
} // createGrid
//------------------------------------------------------------------------
/* creates a Side and initialize it with a first cell
in most cases, a second cell will be added later, excepts on the edges of the grid.
*/
function Side(cell, side, styl) {
let fdHue;
this.cell1 = cell;
this.side1 = side;
this.hue = intAlea(360);
this.satur = intAlea(90,100);
if (! cell.styles) cell.styles = [];
cell.styles[side] = styl;
tbSides.push (this);
}
Side.prototype.addCell = function (cell, side, styl) {
this.cell2 = cell;
this.side2 = side; // normally useless, but can make things more easy
if (! cell.styles) cell.styles = [];
cell.styles[side] = styl;
}
Side.prototype.drawSide = function(alpha) {
/* alpha in range [0..0.5]
*/
ctx.beginPath();
this.cell1.drawSide(this.side1, alpha, true);
if (this.cell2) {
this.cell2.drawSide(this.side2, alpha);
}
ctx.closePath();
ctx.strokeStyle = `hsl(${this.hue},${this.satur}%,${20+120*alpha}%)`;
ctx.lineWidth = 1;
ctx.stroke();
}
//------------------------------------------------------------------------
function drawAll (alpha) {
tbSides.forEach(side => {
side.drawSide(alpha);
});
}
//------------------------------------------------------------------------
function createSides() {
let cellneigh, side, styl, styleList;
tbSides = [];
grid.forEach ((line, ky) => {
line.forEach ((cell, kx) => {
cell.sides = [];
styleList = ['abceda','abcabc', 'edaeda'][cell.kind];
for (let kside = 0; kside < 6 ; ++ kside) {
styl = styleList [(kside + cell.orient) % 6];
cellneigh = cell.neighbour(kside);
if (!cellneigh) cell.sides[kside] = new Side(cell, kside, styl);
else if (cellneigh.sides && cellneigh.sides [(kside + 3) % 6]) {
side = cellneigh.sides [(kside + 3) % 6];
side.addCell(cell, kside, styl);
cell.sides[kside] = side;
} else cell.sides[kside] = new Side(cell, kside, styl);
} // for side
}); // grid.forEach
}); // grid.forEach
}
//-----------------------------------------------------------------------------
let animate;
{ // scope for animate
let animState = 0;
let alpha = 0;
animate = function(tStamp) {
let event = events.pop();
requestAnimationFrame(animate)
if (event) {
switch (event.event) {
case 'reset' :
animState = 0;
break;
} // switch (event)
} // if (event)
let tinit = performance.now();
switch (animState) {
case 0:
if (startOver()) {
alpha = 0.499;
++animState;
}
break;
case 1:
drawAll(alpha);
alpha -= 0.5/rayHex;
if (alpha <= 0) {
++animState;
alpha = 0.001;
}
break;
case 2:
drawAll(alpha);
alpha += 0.5/rayHex;
if (alpha >= 0.5) {
alpha = 0.499;
++animState;
}
break;
} // switch (animState)
} // animate
} // scope for animate
//-----------------------------------------------------------------
function startOver() {
// canvas dimensions
maxx = window.innerWidth;
maxy = window.innerHeight;
let orgLeft = mmax (((window.innerWidth ) - maxx) / 2, 0);
let orgTop = mmax (((window.innerHeight ) - maxy) / 2, 0);
canv.style.left = orgLeft + 'px';
canv.style.top = orgTop + 'px';
if (maxx != canv.width) canv.width = maxx;
if (maxy != canv.height) canv.height = maxy;
canv.width = maxx;
canv.height = maxy;
ctx.lineCap = 'round'; // placed here because reset when canvas resized
ctx.lineJoin = 'round';
// number of columns / rows
// computed to have (0,0) in top leftmost corner
// and for all hexagons to be fully contained in canvas
rayHex = alea(rayHexMin, rayHexMax);
nbx = mfloor(((maxx / rayHex) - 0.5) / 1.5);
nby = mfloor(maxy / rayHex / rac3 - 0.5); //
nbx += 3; // to have canvas fully covered by hexagons
nby += 3;
if (nbx <= 3 || nby <= 3) return false; // nothing to do
Hexagon.dimensions();
createGrid();
createSides();
grid.forEach(line => {
line.forEach(cell => {
cell.size();
});
});
return true;
} // startOver
//-----------------------------------------------------------------
// beginning of execution
{
canv = document.createElement('canvas');
canv.style.position="absolute";
document.body.appendChild(canv);
ctx = canv.getContext('2d');
canv.addEventListener('click', ()=> {events.push({event:'reset'})});
canv.setAttribute ('title', 'click me');
} // canvas creation
events.push({event:'reset'});
requestAnimationFrame(animate)
} // window.onload
body {
font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif;
background-color: #000;
color: #fff;
margin: 0;
padding: 0;
border-width:0;
cursor: pointer;
}
console