Canvas particle animation mobile touch event with elements positioned on top

Viewed 76

I have this canvas particle animation that follows the mouse event correctly but it does not follow a touch event on mobile. I realized that it's because I have this animation beneath the rest of the content which is positioned on top. When there is something on top of the canvas that it's hitting, it does not trigger the touch event. I'm hoping someone can help me figure out how to avoid this issue so that the animation will still follow the user underneath the page content.

JS

var PI2 = Math.PI * 2;
var HALF_PI = Math.PI / 2;

var isTouch = 'ontouchstart' in window;
var isSafari =  !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);

function Canvas(options) {
  options = _.clone(options || {});
  this.options = _.defaults(options, this.options);
  
  this.el = this.options.el;
  this.ctx = this.el.getContext('2d');
  
  this.dpr = window.devicePixelRatio || 1;
  
  this.updateDimensions();
  window.addEventListener('resize', this.updateDimensions.bind(this), false);
  this.resetTarget();
  
  if(isTouch){
      // touch
    this.el.addEventListener('touchstart', this.touchMove.bind(this), false);
    this.el.addEventListener('touchmove', this.touchMove.bind(this), false);
//     this.el.addEventListener('touchend', this.resetTarget.bind(this), false);
  } else {
    // Mouse
    window.addEventListener('mousemove', this.mouseMove.bind(this), false);
    window.addEventListener('mouseout', this.resetTarget.bind(this), false);
  }
  
  this.setupParticles();

  this.loop();
}

Canvas.prototype.updateDimensions = function() {
  this.width = this.el.width = _.result(this.options, 'width') * this.dpr;
  this.height = this.el.height = _.result(this.options, 'height') * this.dpr;
  this.el.style.width = _.result(this.options, 'width') + 'px';
  this.el.style.height = _.result(this.options, 'height') + 'px';
}

// Update the orb target
Canvas.prototype.mouseMove = function(event) {
  this.target = new Vector(event.clientX * this.dpr, event.clientY* this.dpr);
}

// Reset to center when we mouse out
Canvas.prototype.resetTarget = function() {
  this.target = new Vector(this.width / 2, this.height /2);
}

// Touch Eent
Canvas.prototype.touchMove = function(event) {
  if(event.touches.length === 1) {  event.preventDefault(); }

  this.target = new Vector(event.touches[0].pageX * this.dpr, event.touches[0].pageY * this.dpr);
}

// Defaults
Canvas.prototype.options = {
  count: 11,
  speed: 0.001,
  width: 400,
  height: 400,
  size: 5,
  radius: 1,
  background: '240, 240, 240, 0.6',
  maxDistance: 100
}

Canvas.prototype.setupParticles = function() {
  this.particles = [];
  var index = -1;
  var between = PI2 / this.options.count;
  while(++index < this.options.count) {
    var x;
    var y;
    var angle;
    var max = Math.max(this.width, this.height);
    
    angle = (index + 1) * between;
    
    x = Math.cos(angle) * max;
    x += this.width / 2;

    y = Math.sin(angle) * max;
    y += this.height / 2;
    
    var particle = new Particle({
      x: x,
      y: y,
      radius: this.options.radius,
      size: this.options.size,
      angle: angle,
      color: this.options.color
    });
    
    this.particles.push(particle);
  }
}

Canvas.prototype.findClosest = function() {
  var index = -1;
  var pointsLength = this.particles.length;

  while(++index < pointsLength) {
    var closestIndex = -1;
    this.particles[index].closest = [];
    
    while(++closestIndex < pointsLength) {
      var closest = this.particles[closestIndex];
      var distance = this.particles[index].position.distanceTo(closest.position);
      if(distance < this.options.maxDistance) {
        var vector = new Vector(closest.position.x, closest.position.y);
        vector.opacity = 1 - (distance / this.options.maxDistance);
        vector.distance = distance;
        this.particles[index].closest.push(vector);
      }
    }
  }
}

Canvas.prototype.loop = function() {
//   this.clear();
  if(isTouch || isSafari) {
    this.ghost();
  } else {
    this.ghostGradient();
  }    
  if(this.options.maxDistance > 0) {
    this.findClosest();
  }    
  this.draw();
  
  window.requestAnimationFrame(_.bind(this.loop, this));
}

Canvas.prototype.clear = function() {
  this.ctx.clearRect(0, 0 , this.width, this.height);
}

Canvas.prototype.ghost = function() {
  this.ctx.globalCompositeOperation = "source-over";
  this.ctx.rect(0, 0 , this.width, this.height);
  if(typeof this.options.background === 'string') {
    this.ctx.fillStyle = "rgba(" + this.options.background + ")";
  } else  {
    this.ctx.fillStyle = "rgba(" + this.options.background[0] + ")";
  }
    
  this.ctx.fill();
}

Canvas.prototype.ghostGradient = function() {
  var gradient;
  
  if(typeof this.options.background === 'string') {
    this.ctx.fillStyle = 'rgba(' + this.options.background + ')';   
  } else {
    var gradient = this.ctx.createLinearGradient(0, 0, 0, this.height);
    
    var length = this.options.background.length;
    for(var i = 0; i < length; i++){
      gradient.addColorStop((i+1) / length, 'rgba(' + this.options.background[i] + ')');
    }
    this.ctx.fillStyle = gradient;
  }
  
  this.ctx.globalOpacity = 0.1;
  this.ctx.globalCompositeOperation = "darken";
  this.ctx.fillRect(0, 0 , this.width, this.height);
}

// Draw
Canvas.prototype.draw = function() {
  var index = -1;
  var length = this.particles.length;
  while(++index < length) {
    var point = this.particles[index];
    var color = point.color || this.options.color;
    point.update(this.target, index);
    
    this.ctx.globalAlpha = 0.3;
    this.ctx.globalCompositeOperation = "lighten";
    this.ctx.fillStyle = 'rgb(' + color + ')';
    this.ctx.beginPath();
    this.ctx.arc(point.position.x, point.position.y, point.size, 0, PI2, false);
    this.ctx.closePath();
    this.ctx.fill();
    
    if(this.options.maxDistance > 0) {
      this.drawLines(point, color);
    }
  }  
}

// Draw connecting lines
Canvas.prototype.drawLines = function (point, color) {
  color = color || this.options.color;
  var index = -1;
  var length = point.closest.length;
  this.ctx.globalAlpha = 0.2;
  this.ctx.globalCompositeOperation = "screen";
  this.ctx.lineCap = 'round';
  while(++index < length) {
    this.ctx.lineWidth = (point.size * 2) *  point.closest[index].opacity;
    this.ctx.strokeStyle = 'rgba(250,250,250, ' + point.closest[index].opacity + ')';
    this.ctx.beginPath();
    this.ctx.moveTo(point.position.x, point.position.y);
    this.ctx.lineTo(point.closest[index].x, point.closest[index].y);
    this.ctx.stroke();
  }
}

function Particle(options) {
  options = _.clone(options || {});
  this.options = _.defaults(options, this.options);
  
  this.position = this.shift = new Vector(this.options.x, this.options.y);
  
  this.speed = this.options.speed || 0.01 + Math.random() * 0.04;
  
  this.angle = this.options.angle || 0;
    
  if(this.options.color) {
    var color = this.options.color.split(',');
    var colorIndex = -1;
    while(++colorIndex < 3) {      
      color[colorIndex] = Math.round(parseInt(color[colorIndex], 10) + (Math.random()*100)-50);
      
      // Clamp
      color[colorIndex] = Math.min(color[colorIndex], 255);
      color[colorIndex] = Math.max(color[colorIndex], 0);
    }
    this.color = color.join(', ');
  } 
  
  // Size
  this.options.size = this.options.size || 7;
  this.size = 1 + Math.random() * this.options.size;
  this.targetSize = this.options.targetSize || this.options.size;
  
  this.orbit = this.options.radius * 0.5 + (this.options.radius * 0.5 * Math.random());
}

Particle.prototype.update = function(target, index) {
  this.angle += this.speed;

  this.shift.x += (target.x - this.shift.x) * this.speed;
  this.shift.y += (target.y - this.shift.y) * this.speed;

  this.position.x = this.shift.x + Math.cos(index + this.angle) * this.orbit;
  this.position.y = this.shift.y + Math.sin(index + this.angle) * this.orbit;
  
  if(!isSafari) {
    this.size += (this.targetSize - this.size) * 0.03;

    if(Math.round(this.size) === Math.round(this.targetSize)) {
      this.targetSize = 1 + Math.random() * this.options.size;
    }
  }
}

function Vector(x, y) {
  this.x = x || 0;
  this.y = y || 0;
}

Vector.prototype.distanceTo = function(vector, abs) {
  var distance = Math.sqrt(Math.pow(this.x - vector.x, 2) + Math.pow(this.y - vector.y, 2));
  return abs || false ? Math.abs(distance) : distance;
};

new Canvas({
  el: document.getElementById('canvas'),

  count: 25,
  speed: 0.3,
  radius: 6,
  width: function() { return window.innerWidth; },
  height: function() { return window.innerHeight; },
  size: 15,
  color: '30, 180, 1',
  maxDistance: 100,
  background: ['250,250,250,1', '215,216,215,0.8']
})

CSS

html, body {
  min-height: 100%;
  height: 100%;
}

#canvas {
  width: 100%;
  height: 100%;
  position: fixed;
  z-index: 100;
}
.page-content {
  position: relative;
  z-index: 900;
  display: flex;
  flex-direction: column;
}

HTML

<body>
    <div id="container">
      <canvas id="canvas"></canvas>
      <div class="page-content">
      </div>
    </div>
</body>
0 Answers
Related