SOURCE

console 命令行工具 X clear

                    
>
console
"use strict";

window.addEventListener("load",function() {

  const nbCells = 5 ; // min number of cells in height or width
  const bgColor = '#004';
  const rayBallMin = 0.3; // relative to apoHex
  const rayBallMax = 0.8; // relative to apoHex (< 1)
  const speed = 0.03;

  let canv, ctx;   // canvas and context : global variables (I know :( )
  let ctxGrid, canvGrid;
  let maxx, maxy;  // canvas sizes (in pixels)
  let nbx, nby;    // number of columns / rows
  let grid;
  let rayHex;       // Hexagon side
  let apoHex;       // apothem
  let nbBalls;
  let balls;

// for animation
  let events = [];
  let mouse = {x:0, y:0};
  let explorers; // array of alive Explorers

// 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 mtan = Math.tan;

  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 vertices;
let orgx, orgy;

Hexagon = function (kx, ky) {

  this.kx = kx;
  this.ky = ky;
  this.neighbours = [];

} // 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

/* position of hexagon vertices, relative to its center */
  vertices = [[],[],[],[],[],[]] ;
// x coordinates, from left to right
  vertices[3][0] = - (rayHex + 0.5);
  vertices[2][0] = vertices[4][0] = - (rayHex + 0.5) / 2;
  vertices[1][0] = vertices[5][0] = + (rayHex + 0.5) / 2;
  vertices[0][0] = (rayHex + 0.5);
// y coordinates, from top to bottom
  vertices[4][1] = vertices[5][1] = - (rayHex + 0.5) * rac3s2;
  vertices[0][1] = vertices[3][1] = 0;
  vertices[1][1] = vertices[2][1] = (rayHex + 0.5) * rac3s2;

  Hexagon.dirx = [rac3s2, 0, - rac3s2, - rac3s2, 0, rac3s2];
  Hexagon.diry = [0.5, 1, 0.5, - 0.5, -1, - 0.5];
} // 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 = [[],[],[],[],[],[]] ;
  this.middles = [[],[],[],[],[],[]];

// x coordinates of this hexagon vertices
  this.vertices[3][0] = this.xc + vertices[3][0];
  this.vertices[2][0] = this.vertices[4][0] = this.xc + vertices[2][0];
  this.vertices[1][0] = this.vertices[5][0] = this.xc + vertices[1][0];;
  this.vertices[0][0] = this.xc + vertices[0][0];;
// y coordinates of this hexagon vertices
  this.vertices[4][1] = this.vertices[5][1] = this.yc + vertices[4][1];
  this.vertices[0][1] = this.vertices[3][1] = this.yc + vertices[0][1];
  this.vertices[1][1] = this.vertices[2][1] = this.yc + vertices[1][1];

  for (let k = 0; k < 6; ++k) {
    this.middles[k] = [(this.vertices[k][0] + this.vertices[(k + 1) % 6][0]) / 2,
                       (this.vertices[k][1] + this.vertices[(k + 1) % 6][1]) / 2];
  } // for k

} // Hexagon.prototype.size

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Hexagon.prototype.drawHexagon = function(fill, stroke, width = 1) {

  if (! this.vertices) this.size();

  ctxGrid.globalCompositeOperation = 'source-over'; // normal
  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]);
  if (fill) {
    ctxGrid.fillStyle = fill;
    ctxGrid.fill();
  }
  if (stroke) {
    ctxGrid.strokeStyle = stroke;
    ctxGrid.lineWidth = 2;
    ctxGrid.stroke();
  }
  ctxGrid.globalCompositeOperation = 'destination-out';
  for (let side = 0; side < 6 ; ++side) {
    if (this.neighbour(side)) {
      ctxGrid.beginPath();
      ctxGrid.fillStyle = 'rgba(0,0,0,1)'; // seems to not matter
      ctxGrid.arc(this.middles[side][0], this.middles[side][1], 3,0, m2PI);
      ctxGrid.fill();
    }
  }
} // Hexagon.prototype.drawHexagon

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Hexagon.prototype.drawSide = function(side, hue) {

let s2 = (side + 1) % 6;

  if (! this.vertices) this.size();

  let ctxGrid = ctx;
    ctxGrid.beginPath();

    ctxGrid.moveTo (this.vertices[side][0], this.vertices[side][1]);
    ctxGrid.lineTo (this.vertices[s2][0], this.vertices[s2][1]);
    ctxGrid.strokeStyle = `hsl(${hue},100%,60%)`;
    ctxGrid.lineWidth = 1;
//    ctxGrid.fillStyle = `hsl(${hue},100%,60%)`;
    ctxGrid.stroke();
} // Hexagon.prototype.drawHexagon

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

/* 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

} // scope for Hexagon

//------------------------------------------------------------------------
//------------------------------------------------------------------------

function Ball() {
/* creates a random ball */
  this.radius = apoHex * alea(rayBallMin, rayBallMax);
  do {
    this.kx = intAlea(nbx);
    this.ky = intAlea(nby);
    this.cell = grid[this.ky][this.kx];
    this.comesFrom = intAlea(6); // takes a random 'comesFrom' side
  } while (this.cell.neighbour(this.comesFrom) === false || this.cell.occupied);
  this.state = 0; // initial state : center of cell, not chosen direction yet
  this.retries = 0;
  this.cell.occupied = true;
  this.hue = intAlea(360);
  this.hueNoise = Noise1DOneShot(1000, -1, 1);
} //

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Ball.prototype.move = function() {

  let dir, neigh, nTries;
  let xc, yc;
  let r1, r2;

  this.hue += this.hueNoise();
  this.hue = (this.hue + 360) % 360;
  
  ctx.fillStyle = `hsl(${this.hue},100%,50%)`;

  switch (this.state) {
    case 0:

      ctx.beginPath();
      ctx.arc(this.cell.xc, this.cell.yc, this.radius, 0, m2PI);
      ctx.fill();

      nTries = 0;
      do {
        dir = (this.comesFrom + 3 + [-2, -1, -1, 0, 0, 0, 1, 1, 2][intAlea(9)]) % 6;
        neigh = this.cell.neighbour(dir);
        if (neigh.occupied) neigh = false; // not acceptable
        ++ nTries;
      } while (neigh === false && nTries < 30);
      if (neigh === false) {
        dir = this.comesFrom;
        neigh = this.cell.neighbour(dir);
        if (neigh.occupied) break;
      }
      this.state++;
      this.dC = 0;      // distance to center of hexagon
      this.dir = dir;
      neigh.occupied = true;
      break;

    case 1:
      this.dC += rayHex * speed; // movement
      if (this.dC + this.radius >= apoHex) {
        this.dC = (apoHex - this.radius);
        this.state = 2;
        this.alphaCross = 0;
      }
      xc = this.cell.xc + Hexagon.dirx[this.dir] * this.dC;
      yc = this.cell.yc + Hexagon.diry[this.dir] * this.dC;
      ctx.beginPath();
      ctx.arc(xc, yc, this.radius, 0, m2PI);
      ctx.fill();
      break;

    case 2:         // crossing side
      this.alphaCross += speed / 2; // more slowly !
      if (this.alphaCross >= 1) {
        this.state = 3;
        this.alphaCross = 1;
        this.dC = apoHex + this.radius;
      }
      r1 = this.radius * msqrt(1 - this.alphaCross * this.alphaCross);
      r2 = this.radius * this.alphaCross;
      ctx.beginPath();
      if (r1 > 0.5) {
        xc = this.cell.middles[this.dir][0] - r1 * Hexagon.dirx[this.dir];
        yc = this.cell.middles[this.dir][1] - r1 * Hexagon.diry[this.dir];
        ctx.arc(xc, yc, r1, 0, m2PI);
      }
      if (r2 > 0.5) {
        xc = this.cell.middles[this.dir][0] + r2 * Hexagon.dirx[this.dir];
        yc = this.cell.middles[this.dir][1] + r2 * Hexagon.diry[this.dir];
        ctx.arc(xc, yc, r2, 0, m2PI);
      }
      ctx.fill();
      break;
    case 3: // move towards center of next cell
      this.dC += rayHex * speed; // movement
      if (this.dC >= 2 * apoHex) {
        this.dC = (2 * apoHex);
      }
      xc = this.cell.xc + Hexagon.dirx[this.dir] * this.dC;
      yc = this.cell.yc + Hexagon.diry[this.dir] * this.dC;
      ctx.beginPath();
      ctx.arc(xc, yc, this.radius, 0, m2PI);
      ctx.fill();
      if (this.dC >= 2 * apoHex) {
        this.cell.occupied = false;
        this.cell = this.cell.neighbour(this.dir);
        this.kx = this.cell.kx;
        this.ky = this.cell.ky;
        this.comesFrom = (this.dir + 3) % 6;
        this.state = 0;
      }
      break;

  } // switch (this.state)
} // move

//------------------------------------------------------------------------
//------------------------------------------------------------------------

function createGrid() {
/* create the grid of Hexagons
  and defines the number of dots on each side of the hexagons
  but does NOT define the crossings between dots inside an hexagon
*/
  let hexa;
  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;
    } // for kx
  } // for ky
} // createGrid

//------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// returns false if nothing can be done, true if preparation done

function startOver() {

  let rayHexX, rayHexY;

  maxx = window.innerWidth;
  maxy = window.innerHeight;

  let orgLeft = mmax (((window.innerWidth ) - maxx) / 2, 0);
  let orgTop = mmax (((window.innerHeight ) - maxy) / 2, 0);
  canvGrid.style.left = canv.style.left = orgLeft + 'px';
  canvGrid.style.top = canv.style.top = orgTop + 'px';

  canvGrid.width = canv.width = maxx;
  canvGrid.height = canv.height = maxy;
  ctxGrid.lineCap = ctx.lineCap = 'round';   // placed here because reset when canvas resized

// number of columns / rows
// computed to have (0,0) in top leftmost corner
// and for all hexagons to be fully contained in canvas

  rayHexX = mfloor((maxx - 6) / (nbCells + mfloor((nbCells + 1) / 2)));
  rayHexY = mfloor((maxy - 6) / rac3s2 / (nbCells * 2 + 1));

  rayHex = mmin(rayHexX, rayHexY);
  apoHex = rayHex * rac3s2; // apothem

  nbx = mfloor(((maxx / rayHex) - 0.5) / 1.5);
  nby = mfloor(maxy / rayHex / rac3 - 0.5); //

  if (nbx <= 2 || nby <= 2) return false; // nothing to do

  nbBalls = mfloor(nbx * nby / 3);

  if (nbBalls < 1 ) return false;

  Hexagon.dimensions();

  createGrid();

  ctxGrid.clearRect(0, 0, maxx, maxy);
  grid.forEach(line => {
    line.forEach(cell => {
      cell.drawHexagon(false, '#fff',5);
    }); // line.forEach
  }) // grid.forEach

  balls = [];
  for (let k = 0; k < nbBalls; ++k) {
    balls[k] = new Ball();
  }

  return true; // ok

} // startOver

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

function clickCanvas(event) {
  if (event.target.tagName == 'CANVAS') {
    events.push({event: 'click'});
    mouse.x = event.clientX;
    mouse.y = event.clientY;
  }
}
//------------------------------------------------------------------------
let animate;
{
  let animState = 0;
  let ball;

  animate = function(tStamp) {

    const event = events.shift();

    window.requestAnimationFrame(animate);

    switch (animState) {
      case 0 :
        if (startOver()) {
          ++animState;
          mouse.x = maxx/2; mouse.y = maxy / 2;
        }

        break;

      case 1 :
        ctx.clearRect(0, 0, maxx, maxy);
        balls.forEach ( ball => {
          ball.move();
        });
        if (!event || event.event !== 'click') break; // waiting for click
        animState = 0;
        break;
    } // switch animState
  } // animate
} // scope for animate

//------------------------------------------------------------------------
// beginning of execution

  {
    canv = document.createElement('canvas');
    canv.style.position = "absolute";
    document.body.appendChild(canv);
    ctx = canv.getContext('2d');
    canv.setAttribute('title','click me');
  } // canvas creation
  {
    canvGrid = document.createElement('canvas');
    canvGrid.style.position = "absolute";
    document.body.appendChild(canvGrid);
    ctxGrid = canvGrid.getContext('2d');
    canvGrid.style.zIndex = 1;
  } // canvas creation

  window.addEventListener('click',clickCanvas);

// launch animation

  window.requestAnimationFrame(animate);
}); // window load listener
<!-- click to restart -->
body {
  font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif;
  background-color: #044;
  margin:0;
  padding:0;
  border-width:0;
  cursor: pointer;
}