<canvas id="canvas"></canvas>
body, html {
  margin: 0;
  overflow: hidden;
}

canvas {
  display: block;
  cursor: crosshair;
}
/*
  Johan Karlsson, 2020
  https://twitter.com/DonKarlssonSan
  MIT License, see Details View
*/

class Circle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.r = 10;
    this.done = false;
  }
  
  draw() {
    this.drawCircle(this.x, this.y, this.r * 0.92, this.r / 8);

    let d = mouseVector.sub(new Vector(this.x, this.y));
    let p = d.div(d.getLength()).mult(this.r / 4);
    this.drawCircle(this.x + p.x, this.y + p.y, this.r * 0.6, this.r / 16);
   
    p.multTo(2.2);
    this.drawCircle(this.x + p.x, this.y + p.y, this.r * 0.3, this.r / 24);
  }

  drawCircle(x, y, r, lineWidth) {
    ctx.lineWidth = lineWidth;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI*2);
    ctx.stroke();
  }
}

let canvas;
let ctx;
let w, h;
let circles;
let mouseVector;
let mouseHasLeft;

function setup() {
  mouseHasLeft = true;
  canvas = document.querySelector("#canvas");
  ctx = canvas.getContext("2d");
  reset();
  window.addEventListener("resize", () => {
    reset();
  });
  canvas.addEventListener("click", reset);
  canvas.addEventListener("mousemove", mouseMove);
  canvas.addEventListener("mouseleave", mouseLeave);
  canvas.addEventListener("mouseenter", mouseEnter)
  mouseVector = new Vector(w * 0.75, h * 0.75);
}

function reset() {
  w = canvas.width = window.innerWidth;
  h = canvas.height = window.innerHeight;
  resetCircles();
  packCircles();
}

function clear() {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, w, h);
  ctx.strokeStyle = "white";
}

function dist(x1, y1, x2, y2) {
  return Math.hypot(x1 - x2, y1 - y2);
}

function addCircles() {
  let nrOfTries = 0;
  let wasAdded;
  do {
    wasAdded = false;
    let x = Math.random() * w;
    let y = Math.random() * h;
    if(validPos(x, y)) {
      wasAdded = true;
      let c = new Circle(x, y);
      circles.push(c);
    }
    nrOfTries++;
  } while (!wasAdded && nrOfTries < 50)
}

function validPos(x, y) {
  for(let i = 0; i < circles.length; i++) {
    let current = circles[i];
    let d = dist(x, y, current.x, current.y);
    if(d - 10 < current.r) {
      return false;
    }
  }
  return true;
}

function canGrow(circle) {
  for(let i = 0; i < circles.length; i++) {
    let current = circles[i];
    if(circle !== current) {
      let d = dist(circle.x, circle.y, current.x, current.y);
      if(d - 4 <= circle.r + current.r) {
        return false;
      } 
    }
  }
  return true;
}

function resetCircles() {
  circles = [];
}
  
function packCircles() {
  let nrOfTries = w * h / 400;
  for(let i = 0; i < nrOfTries; i++) {
    if(i % 4 === 0) {
      addCircles();
    }
    circles.filter(c => !c.done).forEach(c => {
      if(canGrow(c)) {
        c.r += 2;
      } else {
        c.done = true;
      }
    });
  }
}
  
function drawCircles() {
  circles.forEach(c => c.draw());
}
  
function draw(now) {
  requestAnimationFrame(draw);
  clear();
  fakeMoveMouse(now);
  drawCircles();
}
  
function mouseMove(event) {
  mouseVector.x = event.clientX;
  mouseVector.y = event.clientY;
}
  
function mouseLeave() {
  mouseHasLeft = true;
}

function mouseEnter() {
  mouseHasLeft = false;
}

function fakeMoveMouse(now) {
  if(mouseHasLeft) {
    mouseVector.x = Math.cos(now/1000) * w/3 + w/2;
    mouseVector.y = Math.sin(now/1000) * h/3 + h/2;
  }
}
 
setup();
draw(1);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/DonKarlssonSan/pen/JreEJO.js