"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