<canvas id="c"></canvas>

<footer> 
  <h3>Made with <i class='fa fa-heart' ></i> by <a href="http://www.velasquezdaniel.com" target='_blank'>Daniel Velasquez</a>
</footer>
  
<!--   No longer used controls
<div id='controls'> 
  <div id='c-mouse' class='cc'>
    <div class='header'>
      <h3 class='header-title'>Mouse</h3>
      <h3 class='header-value' id='mouse-value'>200</h3>
    </div>
    <input type="range" min="50" max="600" value="400" class="slider" id="mouse">
  </div>
  <div id='c-speed' class='cc'>
    <div class='header'>
      <h3 class='header-title'>Speed</h3>
      <h3 class='header-value' id='speed-value'>53</h3>
    </div>
    <input type="range" min="1" max="30" value="30" class="slider" id="speed">
  </div>
  <div id='c-lines' class='cc'>
    <div class='header'>
      <h3 class='header-title'>Lines</h3>
      <h3 class='header-value' id='lines-value'>53</h3>
    </div>
    <input type="range" min="2" max="20" value="6" class="slider" id="lines">
    
  </div>
  <div id='c-points' class='cc'>
    <div class='header'>
      <h3 class='header-title'>Points<i class="fa fa-question-circle info"><h4 class='info-puf'>Every line is formed by many points.<br />The more points you have, the more bents the line can have.</h4></i></h3>
      <h3 class='header-value' id='points-value'>53</h3>
    </div>
    <input type="range" min="3" max="100" value="50" class="slider" id="points">
    
  </div>
  <div id='c-padding' class='cc'>
    <div class='header'>
      <h3 class='header-title'>padding</h3>
      <h3 class='header-value' id='padding-value'>53</h3>
    </div>
    <input type="range" min="5" max="45" value="5" class="slider" id="padding">
  </div>
  <div class='debug cc'>
  <div id='debug-fps' class='debug-item'>
    <div class='header'>
      <h3 class='header-title'>FPS</h3>
    </div>
    <input type="checkbox" name="fps" value="fps" id='debug-fps' class='check'>
  </div>
  <div id='c-debug-points' class='debug-item'>
    <div class='header'>
      <h3 class='header-title'>Points</h3>
    </div>
  <input type="checkbox" name="points" value="points" id='debug-points' class='check'>
    
  </div>
  </div>
</div> -->
* {
  box-sizing: border-box;
}
html {
  font-size: 12px;
}
body, html {
  width: 100%;
  height: 100%;
  overflow: hidden;
  padding: 0;
  margin: 0;
  position: relative;
}
footer {
  position: absolute;
  width: 100%;
  bottom: 0px;
  display: flex;
  align-items: center;
  justify-content: center;
  a, i {
    color: #ff5050;
    text-decoration: none;
  }
  a:hover {
    color: red;
  }
  h3 {
    font-size: 1.25rem;
    letter-spacing: 0.05em;
    font-weight: 400;
    color: white;
    margin: 0;
    padding: 0.75em 1.5em;
    background: rgba(10,9,14,0.8);
    border-radius: 2px;
  }
}
.cc {
  width: 100%;
  padding: 0 20px;
}
#controls {
  position: absolute;
  right: 0;
  top: 0;
  background: rgba(10,9,14,0.8);
  box-shadow: 0 2px 4px rgba(0,0,0,0.4);
  min-width: 200px;
  max-height: 100vh;
  border-radius: 2px;
  padding: 20px 0;
  & > div {
    margin-bottom: 20px;
  }
  & > div:last-of-type {
    margin-bottom: 0px;
    width: 100%;
  }
}
.slider {
  width: 100%;
  height: 25px;
  -webkit-appearance: none;
  background: #d3d3d3;
  outline: none;
  opacity: 0.7;
  -webkit-transition: .2s;
  transition: opacity .2s;
}
.slider:hover {
    opacity: 1;
}

.slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 25px;
    height: 25px;
    background: #ff5050;
    cursor: pointer;
}
.slider::-moz-range-thumb {
    width: 25px;
    height: 25px;
    background: #4CAF50;
    cursor: pointer;
}
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  &-title, &-value {
    position: relative;
    display: inline-block;
    float: left;
    margin: 0;
    color: white;
    font-weight: 400;
    letter-spacing: 0.1em;
    text-transform: capitalize;
  }
  &-value {
    font-weight: 200;
    text-align: right;
  }
}
.debug {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: row;
  &-item {
    width: 50%;
    text-align: center;
    & .header {
      margin-bottom: 10px;
      &-title {
        width: 100%;
      }
    }
  }
}
.check {
  width: 20px;
  height: 20px;
  &:hover {
    cursor: pointer;
    
  }
}

.info {
  position: relative;
  left: 5px;
  &-puf {
    opacity: 0;
    font-family: sans-serif;
    position: absolute;
    line-height: 1.6em;
    top: -30px;
    right: calc(100% + 15px);
    margin: 0;
    white-space: nowrap;
    background: #282828;
    border-radius: 2px;
    z-index: 50;
    padding: 0.5em 2em;
    text-transform: none;
    transition: all 0.2s ease-in-out;
    user-select: none;
    pointer-events: none;
    font-weight: 400;
  }
  &:hover {
    cursor: pointer;
    
  }
  &:hover &-puf {
    opacity: 1;
    user-select: none;
    pointer-events: none;
  }
}

@media screen and (min-width: 800px){
  html {
    font-size: 12px;
  }
}
@media screen and (min-width: 1200px){
}
View Compiled
let canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;


// Helper Functions
// Get number in the middle
function mid(){
  const args = Array.from(arguments);
  if(args.length < 3) return args[0] || 0;
  const sorted = args.slice().sort((a,b)=> a - b);
  return sorted[Math.round((sorted.length - 1) / 2)];
}
// Phytagoshananigans theory
const PY = (x,y) => Math.sqrt(Math.pow(Math.abs(x),2) + Math.pow(Math.abs(y),2),2); 
// Fps calculator
const fpsHelper = function(onSecond){
  let lastSec = Date.now();
  let frames = 0;
  let fps = 0;
  return {
    onFrame: ()=>{
      if(((Date.now() - lastSec) / 1000) > 1) {
        lastSec = Date.now();
        fps = frames;
        frames = 0;
        if(onSecond)
        onSecond(fps);
      } else {
        frames += 1;
      }
    },
    getFPS: () => {
      return fps;
    }
  }
}
// Initialization
const mouse = {
  x: W / 2,
  y: H / 2
}
// pading is in percentage
const config = {
  nPoints: 20,
  nLines: 20,
  radius: 100,
  padding: 40,
  showFPS: false,
  showPoints: false,
  maxSpeed: 30,
}
window.onload = function(){
var gui = new dat.GUI({ closed: true, name:"config" });
gui.add(config,"nPoints",3,50).step(1).onFinishChange(function(val){
  debouncedInit();
});
gui.add(config,"nLines",3,50).step(1).onFinishChange(function(val){
  console.log('finish',val,config)
  debouncedInit();
});;
gui.add(config,"radius",50,300).step(1);
gui.add(config,"padding",5,45).step(1).onFinishChange(function(val){
  console.log(val)
  debouncedUpdateX();
});
gui.add(config,"showFPS");
gui.add(config,"showPoints");
gui.add(config,"maxSpeed", 5, 100);

  
}
let pointsPerLine = 20;
let linesInScreen = 20;
let lines = [];
let homesX = [];
let homesY = [];
let padding = 40;
let max = 30;
let radius = 200;
let fpsObj = fpsHelper();
let debug = {
  fps: false,
  dots: false,
};

let rAF = null;

// Actual Code 
// Update Line's dots Position
function updateLine(line,homeY){
  let point, desiredX, desiredY, desiredH, desiredForce, desiredAngle, hvx, hvy, mvx, mvy, x, y, homeX, vx, vy;
  let radius = config.radius;
  let maxSpeed = config.maxSpeed;
  for(var j = line.length - 1; j >= 0; j--){
    point = line[j];
    x = point.x;
    y = point.y;
    hvx = 0, hvy = 0;
    // Home forces
    homeX = homesX[j];
    if(x !== homeX || y !== homeY) {
      desiredX = homeX - x;
      desiredY = homeY - y;
      desiredH = PY(desiredX,desiredY);
      desiredForce = Math.max(desiredH * 0.2,1);
      desiredAngle = Math.atan2(desiredY,desiredX);
      hvx = desiredForce * Math.cos(desiredAngle);
      hvy = desiredForce * Math.sin(desiredAngle);
    }
    // Mouse Forces
    mvx = 0, mvy = 0;
    desiredX = x - mouse.x;
    desiredY = y - mouse.y;
    if(!(desiredX > radius || desiredY > radius || desiredY < -radius || desiredX < -radius)) {
    desiredAngle = Math.atan2(desiredY,desiredX);
    desiredH = PY(desiredX,desiredY);
    desiredForce =  Math.max(0,Math.min(radius - desiredH,radius));
    mvx = desiredForce * Math.cos(desiredAngle);
    mvy = desiredForce * Math.sin(desiredAngle);
    }
    // Combine and limit
    vx = Math.round(mid((mvx + hvx) * 0.9, maxSpeed, -maxSpeed));
    vy = Math.round(mid((mvy + hvy) * 0.9, maxSpeed, -maxSpeed));
    
    // Dont let point get too far from home
    
    if(vx != 0) {
      point.x += vx;
    }
    if(vy != 0){
      point.y += vy;
    }
    line[j] = point;
  }
  
  return line;
}
function timer(){
  ctx.clearRect(0,0,W,H);
  if(config.showFPS){
     fpsObj.onFrame();
      ctx.fillStyle = '#282828';
      ctx.textAlign="start"; 
      ctx.textBaseline="top"; 
     ctx.font="50px Helvetica";
     ctx.fillText(fpsObj.getFPS(),50,50);
  }
  let line, xc,yc, cur, curX, curY, next, dot;
  for(var i = lines.length - 1; i >= 0; i--){
    // Update before rendering
    line = updateLine(lines[i],homesY[i]);
    lines[i] = line;
    ctx.beginPath();
    ctx.strokeStyle = '#d2d2d2';
    ctx.moveTo(line[line.length - 1].x,line[line.length - 1].y);
    for(var j = line.length - 2; j > 1; j--){
      cur  = line[j];
      curX = cur.x;
      curY = cur.y;
      next = line[j - 1];
      xc = (curX + next.x) / 2;
      yc = (curY + next.y) / 2;
      // ctx.bezierCurveTo(curX,curY,xc,yc,xc,yc);
      // ctx.bezierCurveTo(xc,yc,xc,yc,next.x,next.y);
      // ctx.bezierCurveTo(curX,curY,curX,curY,next.x,next.y);
      // ctx.bezierCurveTo(curX,curY,curX,curY,xc,yc);
      // ctx.bezierCurveTo(curX,curY,xc,yc,next.x,next.y);
      // ctx.bezierCurveTo(xc,yc,xc,yc,curX,curY);
      ctx.quadraticCurveTo(curX,curY,xc,yc);
      if(i===0){
        // console.log(cur.x, cur.y);
      }
    }
    // ctx.bezierCurveTo(line[j].x,line[j].y, line[j - 1].x, line[j - 1].y, line[j - 1].x, line[j - 1].y);
    ctx.quadraticCurveTo(line[j].x,line[j].y, line[j - 1].x, line[j - 1].y);
    ctx.stroke();
    if(config.showPoints) {
      for(j = line.length - 1; j >= 0; j--){
        dot =  line[j];
        ctx.beginPath();
        ctx.fillStyle ='red';
        ctx.arc(dot.x, dot.y, 1, 0, 2 * Math.PI);
        ctx.fill();
      }
    }
  
    }
  rAF = requestAnimationFrame(timer)
  
}
function point(x,y){
  return {
    x: x,
    y: y,
    hy: y,
    hx: x,
  }
}
function updateX(){
  let line, calcPad;
  if(rAF) {
    cancelAnimationFrame(rAF);
    rAF = null;
  }
  calcPad = (W * config.padding) / 100;
  homesX = [];
  for(var i = config.nLines; i >= 0; i--){
    x = calcPad + (((W - calcPad * 2) / config.nLines) * i);
    homesX.push(x);
  }
  timer();
}
function init(){
  // Cancel if neededs
  if(rAF) {
    cancelAnimationFrame(rAF);
    rAF = null;
  }
  lines = [];
  homesX = [];
  homesY = [];
  let line = [], y = 0, x = 0;
  let calcPad = (W * config.padding) / 100;
  for(var i = config.nLines; i >= 0; i--){
    line = [];
    // Include padding in calculatio
    y = calcPad + (((H - calcPad * 2) / config.nLines) * i);
    homesY.push(y);
    for(var j = config.nPoints; j >= 0; j--){
      let x = Math.round((W / config.nPoints ) * j);
      line.push(point(x,y));
      console.log(x,y)
      
      if(i === 0) {
        homesX.push(x);
      }
    }
    if(i == 0){
      // homesY.reverse();
    }
    lines.push(line);
  }
  timer();
}

// Input Handles

const debouncedInit = _.debounce(init, 200)
const debouncedUpdateX = _.debounce(updateX, 200)

// Events
window.addEventListener('mousemove',(e)=>{
  mouse.x = e.clientX;
  mouse.y = e.clientY;
});

window.addEventListener('resize', (e) => {
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width = W;
  canvas.height = H;
  init();
});

init();
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js
  2. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js
  3. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.3/dat.gui.min.js