library / particles & systems / particle-collision-reaction
live - 60fps 1920 x 1080
particle-collision-reaction.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Particle Collision Reaction</title>
<style>
  body, html {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #050505;
  }
  .collision-canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    filter: brightness(1.2);
  }
</style>
</head>
<body>
<canvas class="collision-canvas"></canvas>
<script>
  const canvas = document.querySelector('.collision-canvas');
  const ctx = canvas.getContext('2d');
  let width, height;
  let particles = [];
  let sparks = [];

  function setSize() {
    width = canvas.width = window.innerWidth;
    height = canvas.height = window.innerHeight;
  }

  class Particle {
    constructor(type) {
      this.type = type; // 0 for left, 1 for right
      this.reset();
    }
    reset(){
      this.x = (this.type === 0) ? -5 : width + 5;
      this.y = Math.random() * height;
      this.vx = (this.type === 0) ? 2 + Math.random() * 2 : -2 - Math.random() * 2;
      this.vy = Math.random() - 0.5;
      this.hue = (this.type === 0) ? 180 : 330;
      this.life = 100 + Math.random() * 100;
    }
    update(){
        this.x += this.vx;
        this.y += this.vy;
        this.life--;
        // Check collision zone and life
        const distToCenter = Math.abs(this.x - width/2);
        if (this.life <= 0 || (distToCenter < 50 && Math.random() > 0.8)) {
            for(let i=0; i < 5; i++) {
                sparks.push(new Spark(this.x, this.y, this.hue));
            }
            this.reset();
        }
    }
    draw() {
      ctx.fillStyle = `hsl(${this.hue}, 100%, 70%)`;
      ctx.beginPath();
      ctx.arc(this.x, this.y, 1.5, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  class Spark {
      constructor(x, y, hue){
          this.x = x;
          this.y = y;
          const angle = Math.random() * Math.PI * 2;
          const speed = Math.random() * 3 + 1;
          this.vx = Math.cos(angle) * speed;
          this.vy = Math.sin(angle) * speed;
          this.life = 50;
          this.hue = hue + Math.random()*40-20;
      }
      update(){
          this.x += this.vx;
          this.y += this.vy;
          this.life--;
      }
      draw(){
          const alpha = this.life / 50;
          ctx.fillStyle = `hsla(${this.hue}, 100%, 80%, ${alpha})`;
          ctx.beginPath();
          ctx.arc(this.x, this.y, 1, 0, Math.PI * 2);
          ctx.fill();
      }
  }
  
  function init() {
    setSize();
    particles = [];
    sparks = [];
    for(let i=0; i<150; i++){
        particles.push(new Particle(0));
        particles.push(new Particle(1));
    }
  }
  
  function animate(){
      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = 'rgba(0,0,0,0.1)';
      ctx.fillRect(0,0,width,height);
      
      ctx.globalCompositeOperation = 'lighter';

      particles.forEach(p => { p.update(); p.draw(); });
      
      for(let i=sparks.length-1; i>=0; i--){
          sparks[i].update();
          sparks[i].draw();
          if(sparks[i].life <= 0) sparks.splice(i,1);
      }
      requestAnimationFrame(animate);
  }

  window.addEventListener('resize', init);
  init();
  animate();
</script>
</body>
</html>

About this animation

Particles colliding and reacting.

Under the hood

Elastic Collision Math

An incredibly impressive display of rigid body physics. When two particles intersect, the script calculates conservation of momentum and energy based on the particle mass and angle of incidence to perfectly ricochet them across the screen via Math.atan2.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger