Almost the same as my other pen Growing lines, but based on a tessellation of hexagons instead of squares, and in black on white only
Created
September 29, 2021 17:26
-
-
Save shshaw/856576bd6ed1f31f6e42e019271864df to your computer and use it in GitHub Desktop.
growing lines hex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
window.addEventListener("load",function() { | |
const ANIM_DURATION = 3000; // ms for average drawing | |
const MAX_FRAME_DURATION = 20; | |
const RADIUSMIN = 0.03; | |
const RADIUSMAX = 0.08; | |
const LWIDTH = 0.02; | |
let rndSeed = Math.random(); | |
let canv, ctx; // canvas and context | |
let maxx, maxy; // canvas dimensions | |
let nbx, nby; | |
let uiv; | |
let grid; | |
let rndStruct; | |
let lRef; | |
let cntStarters; | |
let mouse; | |
// 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 mPIS3 = Math.PI / 3; | |
const m2PI = Math.PI * 2; | |
const m2PIS3 = Math.PI * 2 / 3; | |
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; | |
let blackOnWhite; | |
//------------------------------------------------------------------------ | |
//------------------------------------------------------------------------ | |
/* based on a function found at https://www.grc.com/otg/uheprng.htm | |
and customized to my needs | |
use : | |
x = Mash('1213'); // returns a resettable, reproductible pseudo-random number generator function | |
x = Mash(); // like line above, but uses Math.random() for a seed | |
x(); // returns pseudo-random number in range [0..1[; | |
x.reset(); // re-initializes the sequence with the same seed. Even if Mash was invoked without seed, will generate the same sequence. | |
x.seed; // retrieves the internal seed actually used. May be useful if no seed or non-string seed provided to Mash | |
be careful : this internal seed is a String, even if it may look like a number. Changing or omitting any single digit will produce a completely different sequence | |
x.intAlea(min, max) returns integer in the range [min..max[ (or [0..min[ if max not provided) | |
x.alea(min, max) returns float in the range [min..max[ (or [0..min[ if max not provided) | |
*/ | |
/* ============================================================================ | |
This is based upon Johannes Baagoe's carefully designed and efficient hash | |
function for use with JavaScript. It has a proven "avalanche" effect such | |
that every bit of the input affects every bit of the output 50% of the time, | |
which is good. See: http://baagoe.com/en/RandomMusings/hash/avalanche.xhtml | |
============================================================================ | |
*/ | |
/* seed may be almost anything not evaluating to false */ | |
function Mash(seed) { | |
let n = 0xefc8249d; | |
let intSeed = (seed || Math.random()).toString(); | |
function mash (data) { | |
if (data) { | |
data = data.toString(); | |
for (var i = 0; i < data.length; i++) { | |
n += data.charCodeAt(i); | |
var h = 0.02519603282416938 * n; | |
n = h >>> 0; | |
h -= n; | |
h *= n; | |
n = h >>> 0; | |
h -= n; | |
n += h * 0x100000000; // 2^32 | |
} | |
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 | |
} else n = 0xefc8249d; | |
}; | |
mash (intSeed); // initial value based on seed | |
let mmash = () => mash('A'); // could as well be 'B' or '!' or any non falsy value | |
mmash.reset = () => {mash(); mash(intSeed)} | |
Object.defineProperty(mmash, 'seed', {get: ()=> intSeed}); | |
mmash.intAlea = function (min, max) { | |
if (typeof max == 'undefined') { | |
max = min; min = 0; | |
} | |
return mfloor(min + (max - min) * this()); | |
} | |
mmash.alea = function (min, max) { | |
// random number [min..max[ . If no max is provided, [0..min[ | |
if (typeof max == 'undefined') return min * this(); | |
return min + (max - min) * this(); | |
} | |
return mmash; | |
} // Mash | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function hslString(hsl) { | |
return `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`; | |
} // hslString | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function lerp(p1, p2, alpha) { | |
const omalpha = 1 - alpha; | |
return [p1[0] * omalpha + p2[0] * alpha, | |
p1[1] * omalpha + p2[1] * alpha]; | |
} // lerp | |
//------------------------------------------------------------------------ | |
function Hexagon (kx, ky) { | |
this.kx = kx; | |
this.ky = ky; | |
this.hPoints = new Array(12).fill(0).map((v,k) => new HalfPoint(this, k)); | |
this.arcs = []; // no arc between points yet | |
} // Hexagon | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
Hexagon.prototype.calculateArc = function(arc) { | |
if (! this.pos) this.calculatePoints(); | |
const kp0 = arc.p0.khp; | |
const kp1 = arc.p1.khp; | |
let khp0 = kp0, khp1 = kp1; | |
// is starting point is odd, take symmetric to start from even point | |
if (kp0 & 1) { | |
khp0 = 11 - kp0; | |
khp1 = 11 - kp1; | |
} | |
khp1 = (khp1 - khp0 + 12) % 12; // relative index from khp0 to khp1; | |
const coeffBeg = Hexagon.coeffBeg [khp1] * uiv.radius; | |
const coeffEnd = Hexagon.coeffEnd [khp1] * uiv.radius; | |
const side0 = mfloor(kp0 / 2); | |
const side1 = mfloor(kp1 / 2); | |
const p0 = this.pos[kp0]; | |
const p1 = this.pos[kp1]; | |
const pax = p0[0] + Hexagon.perp[side0][0] * coeffBeg; | |
const pay = p0[1] + Hexagon.perp[side0][1] * coeffBeg; | |
const pbx = p1[0] + Hexagon.perp[side1][0] * coeffEnd; | |
const pby = p1[1] + Hexagon.perp[side1][1] * coeffEnd; | |
arc.bez = [p0[0], p0[1], pax, pay, pbx, pby, p1[0], p1[1]]; | |
} // Hexagon.prototype.calculateArc | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
Hexagon.prototype.drawArc = function(arc) { | |
if (! arc.bez) this.calculateArc(arc); | |
let bez = arc.bez; | |
ctx.moveTo (bez[0], bez[1]); | |
ctx.bezierCurveTo (bez[2], bez[3], bez[4], bez[5], bez[6], bez[7]); | |
} // Hexagon.prototype.drawArc | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
Hexagon.prototype.calculatePoints = function() { | |
this.xc = (maxx - (nbx - 1) * 1.5 * uiv.radius) / 2 + 1.5 * uiv. radius * this.kx; | |
let y0 = (maxy - (nby - 0.5) * rac3 * uiv.radius) / 2; | |
if ((this.kx & 1) == 0) y0 += uiv.radius * rac3s2; | |
this.yc = y0 + this.ky * uiv.radius * rac3; | |
this.pos = Hexagon.positions.map(p => [this.xc + p[0] * uiv.radius, this.yc + p[1] * uiv.radius]); | |
} // Hexagon.prototype.calculatePoints | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
Hexagon.k0 = 0.15; | |
Hexagon.vertices = [[1, 0], [0.5, rac3s2], [-0.5, rac3s2], [-1, 0], [-0.5, -rac3s2], [0.5, -rac3s2]]; | |
Hexagon.positions = [lerp(Hexagon.vertices[0],Hexagon.vertices[1] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[0],Hexagon.vertices[1] , 0.5 + Hexagon.k0), | |
lerp(Hexagon.vertices[1],Hexagon.vertices[2] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[1],Hexagon.vertices[2] , 0.5 + Hexagon.k0), | |
lerp(Hexagon.vertices[2],Hexagon.vertices[3] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[2],Hexagon.vertices[3] , 0.5 + Hexagon.k0), | |
lerp(Hexagon.vertices[3],Hexagon.vertices[4] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[3],Hexagon.vertices[4] , 0.5 + Hexagon.k0), | |
lerp(Hexagon.vertices[4],Hexagon.vertices[5] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[4],Hexagon.vertices[5] , 0.5 + Hexagon.k0), | |
lerp(Hexagon.vertices[5],Hexagon.vertices[0] , 0.5 - Hexagon.k0), | |
lerp(Hexagon.vertices[5],Hexagon.vertices[0] , 0.5 + Hexagon.k0)]; | |
Hexagon.coeffBeg = [0, 0.2, 0.3, 0.4, 0.5, 0.6, 1, 0.7, 0.5, 0.5, 0.4, 0.2]; | |
Hexagon.coeffEnd = [0, 0.2, 0.3, 0.3, 0.5, 0.6, 0.7, 0.7, 0.6, 0.6, 0.3, 0.3]; | |
Hexagon.perp = [[-rac3s2, -0.5], | |
[0, -1], | |
[rac3s2, -0.5], | |
[rac3s2, 0.5], | |
[0, 1], | |
[-rac3s2, 0.5]]; // perpendicular to sides - towards center | |
//------------------------------------------------------------------------ | |
function HalfPoint(parent, khp) { | |
this.parent = parent; // an Hexagon | |
this.khp = khp; // index of point in its parentFig's hPoints | |
this.side = mfloor(khp / 2); | |
this.state = 0; // 0 : undecided; 1: entry; 2: exit; 3: blocked | |
// this.other will be added later for the other half of same point. Maybe | |
} // HalfPoint | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
HalfPoint.prototype.attach = function(other) { | |
/* sets other as the second half of this HalfPoint */ | |
if (this.other) { | |
if (this.other != other) throw ('inconsistent attachment'); | |
return; // already connected | |
} | |
this.other = other; // connect both ways at the same time | |
other.other = this; | |
} // HalfPoint.prototype.attach | |
//------------------------------------------------------------------------ | |
function prepareGo () { | |
/* create table of hues for families */ | |
const hue0 = rndStruct.intAlea(360); | |
const starters = []; // list of possible starting points | |
let currp; | |
let k, kcurrp, ktarg, ptarg, targets, currCell; | |
/* chooses random starting point */ | |
if (! mouse) { | |
currp = grid[rndStruct.intAlea(nby)][rndStruct.intAlea(nbx)].hPoints[rndStruct.intAlea(8)]; | |
} else { | |
/* chose (very approximately) the starting point at mouse location */ | |
let kx = mfloor(mouse.x / maxx * nbx); | |
let ky = mfloor(mouse.y / maxy * nby); | |
kx = mmax(0, mmin(kx, nbx - 1)); // limit range of values | |
ky = mmax(0, mmin(ky, nby - 1)); | |
currp = grid[ky][kx].hPoints[rndStruct.intAlea(12)]; | |
} | |
addStarter(starters, currp); | |
if (currp.other) { | |
addStarter(starters, currp.other); | |
} | |
return starters; | |
} | |
//------------------------------------------------------------------------ | |
function goon(st) { | |
/* chooses random points as starting points | |
grows branches from theses points | |
*/ | |
const starters = st; | |
let currp ; | |
let k, kcurrp, ktarg, ptarg, targets, currCell; | |
let arc; | |
while (starters.length) { | |
kcurrp = rndStruct.intAlea(starters.length); | |
currp = starters[kcurrp]; // random point in list | |
currCell = currp.parent; | |
targets = []; | |
for (k = 1; k < 12; ++k) { // test possible connection with other points in this Hexagon | |
if ((currp.khp & 1) && (k == 5) || | |
!(currp.khp & 1) && (k == 7)) continue; // avoid straight lines | |
ktarg = (currp.khp + k) % 12; | |
if (currCell.hPoints[ktarg].state != 0) continue; // this other point not available | |
// test if any other arc would intersect | |
if (currCell.arcs.find(arc => { | |
if ((arc.p0.khp - currp.khp) * | |
(arc.p0.khp - ktarg) * | |
(arc.p1.khp - currp.khp) * | |
(arc.p1.khp - ktarg) < 0) return true; // intersects | |
// } | |
return false; // does not intersect | |
})) continue; // would intersect | |
targets.push(ktarg); // would not intersect | |
} // for k | |
if (targets.length == 0) { | |
currCell.state = 3; // blocked | |
starters.splice(kcurrp, 1); // no longer a possible starting point | |
} else { | |
ktarg = targets[rndStruct.intAlea(targets.length)]; // random pick | |
ptarg = currCell.hPoints[ktarg]; | |
ptarg.state = 2; // mark as exit point | |
arc = {p0: currp, p1:ptarg}; | |
currCell.arcs.push(arc); | |
ctx.beginPath() | |
currCell.drawArc(arc); | |
ctx.strokeStyle = blackOnWhite ? '#000': '#fff'; | |
ctx.lineWidth = uiv.lineWidth; | |
ctx.stroke(); | |
if (ptarg.other) { | |
if (ptarg.other.state != 0) throw('ptarg.other.state != 0'); | |
addStarter(starters, ptarg.other ); | |
} | |
return true; // we've done something | |
} | |
} // while | |
return false; // starters list is empty | |
} // goon | |
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
function addStarter (starters, hp) { | |
starters.push(hp); | |
hp.state = 1; // this point becomes a starting point | |
++cntStarters; | |
} // addStarter | |
//------------------------------------------------------------------------ | |
function attachHalfPoints() { | |
let kxn, kyn, khpn; | |
let dkx, dky; | |
grid.forEach ((line, ky) => { | |
line.forEach ((hex, kx) => { | |
hex.hPoints.forEach((hp, khp) => { | |
dkx = [1, 0, -1, -1, 0, 1][hp.side]; | |
dky = [[1,1,1,0,-1,0],[0,1,0,-1,-1,-1]][kx & 1][hp.side]; | |
kyn = ky + dky; | |
if (kyn < 0 || kyn >= nby) return; // no neighbor | |
kxn = kx + dkx; | |
if (kxn < 0 || kxn >= nbx) return; // no neighbor | |
hp.attach(grid[kyn][kxn].hPoints[[7,6,9,8,11,10,1,0,3,2,5,4][khp]]); | |
}); // sq.hPoints.forEach | |
}); // line.forEach | |
}); // grid.forEach | |
} // attachHalfPoints | |
//------------------------------------------------------------------------ | |
function readUI() { | |
uiv = {}; | |
uiv.radius = mmax(rndStruct.alea(RADIUSMIN, RADIUSMAX) * lRef, 20); | |
uiv.lineWidth = mmax(uiv.radius * rndStruct.alea(0.05, 0.15), 0.5); | |
} // readUI | |
//------------------------------------------------------------------------ | |
let animate; | |
{ // scope for animate | |
let animState = 0; | |
let st; | |
let tStart; | |
let speedFr; | |
animate = function(tStamp) { | |
let event; | |
let tinit, dur; | |
event = events.pop(); | |
if (event && event.event == 'reset') animState = 0; | |
if (event && event.event == 'click') animState = 0; | |
window.requestAnimationFrame(animate) | |
switch (animState) { | |
case 0 : | |
if (startOver()) { | |
st = prepareGo(); | |
++animState; | |
tStart = performance.now(); | |
speedFr = nbx * nby * 10 / ANIM_DURATION; // nb of segments / millisec | |
} | |
break; | |
case 1 : | |
tinit = performance.now(); | |
do { | |
if (!goon(st)) { | |
++animState; | |
break; | |
} | |
if (cntStarters > (performance.now() - tStart) * speedFr) break ; // done enough for this time | |
if ((performance.now() - tinit) > MAX_FRAME_DURATION) break; | |
} while (true) | |
break; | |
case 2: | |
++animState; | |
break; | |
} // switch | |
} // animate | |
} // scope for animate | |
//------------------------------------------------------------------------ | |
//------------------------------------------------------------------------ | |
function startOver() { | |
// canvas dimensions | |
maxx = window.innerWidth; | |
maxy = window.innerHeight; | |
lRef = msqrt(maxx * maxy); | |
canv.width = maxx; | |
canv.height = maxy; | |
ctx.lineJoin = 'round'; | |
ctx.lineCap = 'round'; | |
rndStruct = Mash(rndSeed); | |
++rndSeed; | |
readUI(); | |
/* rem : width = (1.5 * nx + 0.5) * radius | |
height = (ny + 0.5) * radius * sqrt(3) | |
*/ | |
nbx = mfloor((maxx / uiv.radius - 0.5) / 1.5); | |
nby = mfloor(maxy / uiv.radius / rac3 - 0.5); | |
nbx += 2; | |
nby += 2; | |
blackOnWhite = (rndStruct() > 0.5); | |
blackOnWhite = true; // better ! | |
ctx.fillStyle = blackOnWhite ? '#fff': '#000'; | |
ctx.fillRect(0,0,maxx,maxy); | |
grid = new Array(nby).fill(0).map((v, ky) => new Array(nbx).fill(0).map((v, kx) => new Hexagon(kx, ky))); | |
grid.forEach(line => { | |
line.forEach(cell=> { | |
cell.calculatePoints(); | |
}); | |
}); | |
attachHalfPoints(); | |
cntStarters = 0; | |
return true; | |
} // startOver | |
//------------------------------------------------------------------------ | |
function mouseClick (event) { | |
events.push({event:'click', x: event.clientX, y: event.clientY});; | |
if (! mouse) mouse = {}; | |
mouse.x = event.clientX; | |
mouse.y = event.clientY; | |
} // mouseClick | |
//------------------------------------------------------------------------ | |
//------------------------------------------------------------------------ | |
// beginning of execution | |
{ | |
canv = document.createElement('canvas'); | |
canv.style.position="absolute"; | |
document.body.appendChild(canv); | |
ctx = canv.getContext('2d'); | |
canv.setAttribute ('title','click me'); | |
} // création CANVAS | |
canv.addEventListener('click',mouseClick); | |
events = [{event:'reset'}]; | |
requestAnimationFrame (animate); | |
}); // window load listener |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif; | |
background-color: #000; | |
margin:0; | |
padding:0; | |
border-width:0; | |
cursor: pointer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment