<!--
  Click and drag to attract
  Right click to repulse
  Mouse-wheel click to create a time dilation field
  Use the Controls to decrease or increase
  the particle count to tweak performance.
-->
<canvas id="swarm"></canvas>
/*
  Click and drag to attract
  Right click to repulse
  Mouse-wheel click to create a time dilation field
  Use the Controls to decrease or increase
  the particle count to tweak performance.
*/

body, html {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

body {
  canvas {
    display: block;
    cursor: crosshair;
  }
}
View Compiled
/*
  Click and drag to attract
  Right click to repulse
  Mouse-wheel click to create a time dilation field
  Use the Controls to decrease or increase
  the particle count to tweak performance.
*/

+(function(root) {
  'use strict';
  var Vector3D = function Vector3D(x, y, z) {
    this.set(x, y, z);
  }, v3dp = Vector3D.prototype;

  v3dp.dot2d = function(x, y) {
    return ((this.x * x) + (this.y * y));
  };

  v3dp.dot3d = function(x, y, z) {
    return ((this.x * x) + (this.y * y) + (this.z * z));
  };

  v3dp.set = function(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
    
    return this;
  };

  v3dp.add = function(other) {
    if(typeof other === "number") {
      this.x += other, this.y += other, this.z += other;
      return this;
    }
    this.x += other.x, this.y += other.y, this.z += other.z;
    return this;
  };

  v3dp.sub = function(other) {
    if(typeof other === "number") {
      this.x -= other, this.y -= other, this.z -= other;
      return this;
    }
    this.x -= other.x, this.y -= other.y, this.z -= other.z;
    return this;
  };

  v3dp.mul = function(other) {
    if(typeof other === "number") {
      this.x *= other, this.y *= other, this.z *= other;
      return this;
    }
    this.x *= other.x, this.y *= other.y, this.z *= other.z;
    return this;
  };

  v3dp.div = function(other) {
    if(typeof other === "number") {
      this.x /= other, this.y /= other, this.z /= other;
      return this;
    }
    this.x /= other.x, this.y /= other.y, this.z /= other.z;
    return this;
  };

  v3dp.move = function(dest) {
    if(dest instanceof Vector3D) {
      dest.x = this.x, dest.y = this.y, dest.z = this.z;
    }
    return this;
  };

  v3dp.within2d = function(bounds) {
    return (this.x >= 0 && this.x < bounds.x && this.y >= 0 && this.y < bounds.y);
  };

  v3dp.wrap2d = function(bounds) {
    if(this.x > bounds.x) {
      this.x = 0;
      return true;
    }

    if(this.x < 0) {
      this.x = bounds.x;
      return true;
    }

    if(this.y > bounds.y) {
      this.y = 0;
      return true;
    }

    if(this.y < 0) {
      this.y = bounds.y;
      return true;
    }
  };

  v3dp.eq = function(other) {
    return (other instanceof Vector3D) && this.x === other.x && this.y === other.y && this.z === other.z;
  };
  
  v3dp.distance = function(other) {
    var dx = (this.x - other.x),
        dy = (this.y - other.y);
    
    return Math.sqrt(dx * dx + dy * dy);
  };
  
  v3dp.clone = function() {
    return new Vector3D(this.x, this.y, this.z);
  };

  root.Vector3D = Vector3D;
}(window));

+(function(root) {
  'use strict';
  // a simple non-optimized Perlin Simplex Noise. I wrote this
  // to understand Simplex Noise a bit more.

  // fully self-contained state, so you can influence the outcome
  // of each simplex noise state
  var Perlin = function Perlin() {
    this.grad3 = [
      new Vector3D(1,1,0), new Vector3D(-1,1,0), new Vector3D(1,-1,0), new Vector3D(-1,-1,0),
      new Vector3D(1,0,1), new Vector3D(-1,0,1), new Vector3D(1,0,-1), new Vector3D(-1,0,-1),
      new Vector3D(0,1,1), new Vector3D(0,-1,1), new Vector3D(0,1,-1), new Vector3D(0,-1,-1)
    ];

    this.p = [
      0x97, 0xa0, 0x89, 0x5b, 0x5a, 0x0f, 0x83, 0x0d, 0xc9, 0x5f, 0x60, 0x35, 0xc2, 0xe9, 0x07, 0xe1, 
      0x8c, 0x24, 0x67, 0x1e, 0x45, 0x8e, 0x08, 0x63, 0x25, 0xf0, 0x15, 0x0a, 0x17, 0xbe, 0x06, 0x94, 
      0xf7, 0x78, 0xea, 0x4b, 0x00, 0x1a, 0xc5, 0x3e, 0x5e, 0xfc, 0xdb, 0xcb, 0x75, 0x23, 0x0b, 0x20, 
      0x39, 0xb1, 0x21, 0x58, 0xed, 0x95, 0x38, 0x57, 0xae, 0x14, 0x7d, 0x88, 0xab, 0xa8, 0x44, 0xaf, 
      0x4a, 0xa5, 0x47, 0x86, 0x8b, 0x30, 0x1b, 0xa6, 0x4d, 0x92, 0x9e, 0xe7, 0x53, 0x6f, 0xe5, 0x7a, 
      0x3c, 0xd3, 0x85, 0xe6, 0xdc, 0x69, 0x5c, 0x29, 0x37, 0x2e, 0xf5, 0x28, 0xf4, 0x66, 0x8f, 0x36, 
      0x41, 0x19, 0x3f, 0xa1, 0x01, 0xd8, 0x50, 0x49, 0xd1, 0x4c, 0x84, 0xbb, 0xd0, 0x59, 0x12, 0xa9, 
      0xc8, 0xc4, 0x87, 0x82, 0x74, 0xbc, 0x9f, 0x56, 0xa4, 0x64, 0x6d, 0xc6, 0xad, 0xba, 0x03, 0x40, 
      0x34, 0xd9, 0xe2, 0xfa, 0x7c, 0x7b, 0x05, 0xca, 0x26, 0x93, 0x76, 0x7e, 0xff, 0x52, 0x55, 0xd4, 
      0xcf, 0xce, 0x3b, 0xe3, 0x2f, 0x10, 0x3a, 0x11, 0xb6, 0xbd, 0x1c, 0x2a, 0xdf, 0xb7, 0xaa, 0xd5, 
      0x77, 0xf8, 0x98, 0x02, 0x2c, 0x9a, 0xa3, 0x46, 0xdd, 0x99, 0x65, 0x9b, 0xa7, 0x2b, 0xac, 0x09, 
      0x81, 0x16, 0x27, 0xfd, 0x13, 0x62, 0x6c, 0x6e, 0x4f, 0x71, 0xe0, 0xe8, 0xb2, 0xb9, 0x70, 0x68, 
      0xda, 0xf6, 0x61, 0xe4, 0xfb, 0x22, 0xf2, 0xc1, 0xee, 0xd2, 0x90, 0x0c, 0xbf, 0xb3, 0xa2, 0xf1, 
      0x51, 0x33, 0x91, 0xeb, 0xf9, 0x0e, 0xef, 0x6b, 0x31, 0xc0, 0xd6, 0x1f, 0xb5, 0xc7, 0x6a, 0x9d, 
      0xb8, 0x54, 0xcc, 0xb0, 0x73, 0x79, 0x32, 0x2d, 0x7f, 0x04, 0x96, 0xfe, 0x8a, 0xec, 0xcd, 0x5d, 
      0xde, 0x72, 0x43, 0x1d, 0x18, 0x48, 0xf3, 0x8d, 0x80, 0xc3, 0x4e, 0x42, 0xd7, 0x3d, 0x9c, 0xb4
    ];

    this.permutation = new Array(512);
    this.gradP       = new Array(512);

    // skew and unskew factors for 2D or 3D, can be modified per state!
    this.F2 = (0.5 * (Math.sqrt(3) - 1));
    this.G2 = ((3 - Math.sqrt(3)) / 6);
    this.F3 = (1 / 3);
    this.G3 = (1 / 6);
  }, pp = Perlin.prototype;

  pp.init = function(prng) {
    if(typeof prng !== "function") {
      throw new TypeError("prng needs to be a function returning an int between 0 and 255");
    }

    for(var i = 0; i < 256; i += 1) {
      var randval = (this.p[i] ^ prng());
      this.permutation[i] = this.permutation[i + 256] = randval;
      this.gradP[i] = this.gradP[i + 256] = this.grad3[randval % this.grad3.length];
    }
  };

  // I removed the pp.simplex2d function, because I don't need it in this project
  // pp.simplex2d = function(x, y) {};

  pp.simplex3d = function(x, y, z) {
    var n0, n1, n2, n3, i1, j1, k1, i2, j2, k2,
        x1, y1, z1, x2, y2, z2, x3, y3, z3,
        gi0, gi1, gi2, gi3, t0, t1, t2, t3,
        s = ((x + y + z) * this.F3),
        i = Math.floor(x + s), j = Math.floor(y + s), k = Math.floor(z + s),
        t = ((i + j + k) * this.G3),
        x0 = (x - i + t), y0 = (y - j + t), z0 = (z - k + t);

    if(x0 >= y0) {
      if(y0 >= z0)      { 
        i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; 
      } else if(x0 >= z0) { 
        i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; 
      } else { 
        i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; 
      }
    } else {
      if(y0 < z0) { 
        i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; 
      } else if(x0 < z0) { 
        i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; 
      } else { 
        i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; 
      }
    }

    x1 = (x0 - i1 + this.G3), y1 = (y0 - j1 + this.G3), z1 = (z0 - k1 + this.G3);
    x2 = (x0 - i2 + 2 * this.G3), y2 = (y0 - j2 + 2 * this.G3), z2 = (z0 - k2 + 2 * this.G3);
    x3 = (x0 - 1 + 3 * this.G3), y3 = (y0 - 1 + 3 * this.G3), z3 = (z0 - 1 + 3 * this.G3);

    i &= 255, j &= 255, k &= 255;

    gi0 = this.gradP[i + this.permutation[j + this.permutation[k]]];
    gi1 = this.gradP[i + i1 + this.permutation[j + j1 + this.permutation[k + k1]]];
    gi2 = this.gradP[i + i2 + this.permutation[j + j2 + this.permutation[k + k2]]];
    gi3 = this.gradP[i + 1 + this.permutation[j + 1 + this.permutation[k + 1]]];

    t0 = (0.6 - x0 * x0 - y0 * y0 - z0 * z0);
    t1 = (0.6 - x1 * x1 - y1 * y1 - z1 * z1);
    t2 = (0.6 - x2 * x2 - y2 * y2 - z2 * z2);
    t3 = (0.6 - x3 * x3 - y3 * y3 - z3 * z3);
    n0 = (t0 < 0 ? 0 : (t0 *= t0, t0 * t0 * gi0.dot3d(x0, y0, z0)));
    n1 = (t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * gi1.dot3d(x1, y1, z1)));
    n2 = (t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * gi2.dot3d(x2, y2, z2)));
    n3 = (t3 < 0 ? 0 : (t3 *= t3, t3 * t3 * gi3.dot3d(x3, y3, z3)));

    return (32 * (n0 + n1 + n2 + n3));
  };

  root.Perlin = Perlin;
}(window));

;(function(root) {
  'use strict';

  var MouseMonitor = function(element) {
    this.position = new Vector3D(0, 0, 0);
    this.state    = {left: false, middle: false, right: false};
    this.element  = element;
    
    var that = this;
    element.addEventListener('mousemove', function(event) {
      var dot, eventDoc, doc, body, pageX, pageY;
      event = event || window.event;
      if (event.pageX == null && event.clientX != null) {
        eventDoc = (event.target && event.target.ownerDocument) || document;
        doc = eventDoc.documentElement;
        body = eventDoc.body;
        event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
        event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0 );
      }

      that.position.x = event.pageX;
      that.position.y = event.pageY;
    });

    element.addEventListener('contextmenu', function(event) {
      return event.preventDefault();
    });
    
    element.addEventListener('mousedown', function(event) {
      if(event.which === 1) that.state.left = true;
      if(event.which === 2) that.state.middle = true;
      if(event.which === 3) that.state.right = true;
      
      return event.preventDefault();
    });

    element.addEventListener('mouseup', function(event) {
      that.state.left = that.state.middle = that.state.right = false;
      
      return event.preventDefault();
    });
  };
  
  root.MouseMonitor = MouseMonitor;
}(window));

+(function(root) {
  'use strict';
  
  var Particle = function Particle(generator, bounds, rctx, mon) {
    this.p = new Vector3D(); // position
    this.t = new Vector3D(); // trail to
    this.v = new Vector3D(); // velocity
    this.g = generator; // simplex noise generator
    this.b = bounds;    // window bounds for wrapping
    this.r = rctx;      // random context
    this.m = mon;       // mouse position monitor
    
    this.reset();
  }, pp = Particle.prototype;

  pp.reset = function() {
    // new random position
    this.p.x = this.t.x = Math.floor(this.r.random() * this.b.x);
    this.p.y = this.t.y = Math.floor(this.r.random() * this.b.y);
    
    // reset velocity
    this.v.set(1, 1, 0);
    
    // iteration and life
    this.i = 0; 
    this.l = this.r.random(1000, 10000); // life time before particle respawns
  };

  pp.step = function() {
    if(this.i++ > this.l) {
      this.reset();
    }
    
    var xx = (this.p.x / 200),
        yy = (this.p.y / 200),
        zz = (Date.now() / 5000),
        a  = (this.r.random() * Math.Tau),
        rnd= (this.r.random()  / 4);

    // calculate the new velocity based on the noise
    // random velocity in a random direction
    this.v.x += (rnd * Math.sin(a) + this.g.simplex3d(xx, yy, -zz)); // sin or cos, no matter
    this.v.y += (rnd * Math.cos(a) + this.g.simplex3d(xx, yy, zz));  // opposite zz's matters
    
    if(this.m.state.left) {
      // add a difference between mouse pos and particle pos (a fraction of it) to the velocity.
      this.v.add(this.m.position.clone().sub(this.p).mul(.00085));
    }
    
    // repulse the particles if the right mouse button is down and the distance between
    // the mouse and particle is below an arbitrary value between 200 and 250.
    if(this.m.state.right && this.p.distance(this.m.position) < this.r.random(200, 250)) {
      this.v.add(this.p.clone().sub(this.m.position).mul(.02));
    }
    
    // time dilation field, stuff moves at 10% here, depending on distance
    if(this.m.state.middle) {
      var d = this.p.distance(this.m.position),
          l = this.r.random(200, 250);
      
      if(d < l) {
        this.v.mul(d / l);
      }
    }
    
    // keep a copy of the current position, for a nice line between then and now and add velocity
    this.p.move(this.t).add(this.v.mul(.94)); // slow down the velocity slightly

    // wrap around the edges
    if(this.p.wrap2d(this.b)) {
      this.p.move(this.t);
    }
  };

  // plot the line, but do not stroke yet.
  pp.render = function(context) {
    context.moveTo(this.t.x, this.t.y);
    context.lineTo(this.p.x, this.p.y);
  };

  root.Particle = Particle;
}(window));

window.addEventListener('load', function() {
  var rctx = new SmallPRNG(+new Date()), // random generator, see ref
      p = new Perlin(), // simplex noise generator
      canvas = document.getElementById("swarm"),
      context = canvas.getContext("2d"),
      stats = new Stats(),
      monitor = new MouseMonitor(canvas),
      gui = new dat.GUI(),
      hue = 0, particles = [], resize,
      width, height, bounds = new Vector3D(0, 0, 0),
      settings = {
        particleNum: 5000,
        fadeOverlay: true,
        rotateColor: true,
        staticColor: {r: 0, g: 75, b: 50},
        staticColorString: 'rgba(0, 75, 50, 0.55)'
      };

  stats.setMode(0); // Start off with FPS mode

  // Place the statistics at the bottom right.
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.right = '5px';
  stats.domElement.style.bottom = '5px';

  document.body.appendChild(stats.domElement);
  
  // dat.gui stuff, 2 folders with a few properties
  var f1 = gui.addFolder('Particles'),
      f2 = gui.addFolder('Colors');
  
  f1.add(settings, 'particleNum', 1000, 15000).step(10).name("Particles").onChange(function() {
    if(settings.particleNum < particles.length) {
      var toDelete = (particles.length - settings.particleNum);
      particles.splice(particles.length - toDelete, toDelete);
    } else {
      for(var i = particles.length; i < settings.particleNum; i += 1) {
        particles.push(new Particle(p, bounds, rctx, monitor));
      }
    }
  });
  
  f2.add(settings, 'fadeOverlay').name("Fade Clear").onChange(function() {
    if(settings.fadeOverlay) {
      resize();
    }
  });
  
  f2.add(settings, 'rotateColor').name("Rotate Color");
  f2.addColor(settings, 'staticColor').name("Static Color").onChange(function() {
    settings.staticColorString = 'rgba(' + 
      Math.floor(settings.staticColor.r) + ', ' + 
      Math.floor(settings.staticColor.g) + ', ' + 
      Math.floor(settings.staticColor.b) + ', ' + .55 + ')';
  });
  
  f1.open();
  f2.open();
  gui.close();
  
  // seed perlin with random bytes from SmallPRNG
  p.init(function() {
    // called for each permutation (256 times)
    return rctx.random(0, 255);
  });

  resize = function() {
    // resize the canvas
    canvas.width  = width  = bounds.x = window.innerWidth;
    canvas.height = height = bounds.y = window.innerHeight;
    
    // remove this and see weird gorgeous stuffs, the history of particles.
    context.fillStyle = '#ffffff';
    context.fillRect(0, 0, width, height);
  }; resize();

  window.addEventListener('resize', resize); 

  // generate a few particles
  for(var i = 0; i < settings.particleNum; i += 1) {
    particles.push(new Particle(p, bounds, rctx, monitor));
  }
  
  +(function render() {
    requestAnimFrame(render);

    stats.begin();
      context.beginPath();
        // render each particle and trail
        for(var i = 0; i < particles.length; i += 1) {
          particles[i].step(), particles[i].render(context);
        }

        context.globalCompositeOperation = 'source-over';
        if(settings.fadeOverlay) {
          context.fillStyle = 'rgba(0, 0, 0, .085)';
        } else {
          context.fillStyle = 'rgba(0, 0, 0, 1)';
        }
        context.fillRect(0, 0, width, height);

        context.globalCompositeOperation = 'lighter';
        if(settings.rotateColor) {
          context.strokeStyle = 'hsla(' + hue + ', 75%, 50%, .55)';
        } else {
          context.strokeStyle = settings.staticColorString;
        }
        context.stroke();
      context.closePath();
    
    stats.end();
    
    hue = ((hue + .5) % 360);
  }());
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //codepen.io/ImagineProgramming/pen/bcmyD.js
  2. //s3-us-west-2.amazonaws.com/s.cdpn.io/188512/codepen-utilities.min.js
  3. //cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.js
  4. //cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js