library / interactive / click-response
live - 60fps 1920 x 1080
click-response.html - self-contained
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Click Response</title>
  <!-- Anime.js library for animations -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/1.0.0/anime.min.js"></script>
  <style>
    canvas {
      display: block;
      width: 100vw;
      height: 100vh;
    }
  </style>
</head>
<body>
  <canvas id="c"></canvas>
  <script>
    // Recreate the click response animation from CodePen
    var c = document.getElementById("c");
    var ctx = c.getContext("2d");
    var cH;
    var cW;
    var bgColor = "#FF6138";
    var animations = [];
    // Color cycling helper
    var colorPicker = (function() {
      var colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
      var index = 0;
      function next() {
        index = index++ < colors.length - 1 ? index : 0;
        return colors[index];
      }
      function current() {
        return colors[index];
      }
      return { next: next, current: current };
    })();
    function removeAnimation(animation) {
      var index = animations.indexOf(animation);
      if (index > -1) animations.splice(index, 1);
    }
    function calcPageFillRadius(x, y) {
      var l = Math.max(x - 0, cW - x);
      var h = Math.max(y - 0, cH - y);
      return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
    }
    function addClickListeners() {
      document.addEventListener("touchstart", handleEvent);
      document.addEventListener("mousedown", handleEvent);
    }
    function handleEvent(e) {
      if (e.touches) {
        e.preventDefault();
        e = e.touches[0];
      }
      var currentColor = colorPicker.current();
      var nextColor = colorPicker.next();
      var targetR = calcPageFillRadius(e.pageX, e.pageY);
      var rippleSize = Math.min(200, cW * 0.4);
      var minCoverDuration = 750;
      var pageFill = new Circle({ x: e.pageX, y: e.pageY, r: 0, fill: nextColor });
      var fillAnimation = anime({
        targets: pageFill,
        r: targetR,
        duration: Math.max(targetR / 2, minCoverDuration),
        easing: "easeOutQuart",
        complete: function() {
          bgColor = pageFill.fill;
          removeAnimation(fillAnimation);
        },
      });
      var ripple = new Circle({
        x: e.pageX,
        y: e.pageY,
        r: 0,
        fill: currentColor,
        stroke: { width: 3, color: currentColor },
        opacity: 1,
      });
      var rippleAnimation = anime({
        targets: ripple,
        r: rippleSize,
        opacity: 0,
        easing: "easeOutExpo",
        duration: 900,
        complete: removeAnimation,
      });
      var particles = [];
      for (var i = 0; i < 32; i++) {
        var particle = new Circle({
          x: e.pageX,
          y: e.pageY,
          fill: currentColor,
          r: anime.random(24, 48),
        });
        particles.push(particle);
      }
      var particlesAnimation = anime({
        targets: particles,
        x: function(particle) {
          return particle.x + anime.random(rippleSize, -rippleSize);
        },
        y: function(particle) {
          return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
        },
        r: 0,
        easing: "easeOutExpo",
        duration: anime.random(1000, 1300),
        complete: removeAnimation,
      });
      animations.push(fillAnimation, rippleAnimation, particlesAnimation);
    }
    function extend(a, b) {
      for (var key in b) {
        if (b.hasOwnProperty(key)) {
          a[key] = b[key];
        }
      }
      return a;
    }
    var Circle = function(opts) {
      extend(this, opts);
    };
    Circle.prototype.draw = function() {
      ctx.globalAlpha = this.opacity || 1;
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
      if (this.stroke) {
        ctx.strokeStyle = this.stroke.color;
        ctx.lineWidth = this.stroke.width;
        ctx.stroke();
      }
      if (this.fill) {
        ctx.fillStyle = this.fill;
        ctx.fill();
      }
      ctx.closePath();
      ctx.globalAlpha = 1;
    };
    var animationLoop = anime({
      duration: Infinity,
      update: function() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cW, cH);
        animations.forEach(function(anim) {
          anim.animatables.forEach(function(animatable) {
            animatable.target.draw();
          });
        });
      },
    });
    var resizeCanvas = function() {
      cW = window.innerWidth;
      cH = window.innerHeight;
      var pixelRatio = window.devicePixelRatio || 1;
      c.width = cW * pixelRatio;
      c.height = cH * pixelRatio;
      ctx.scale(pixelRatio, pixelRatio);
    };
    (function init() {
      resizeCanvas();
      window.addEventListener("resize", resizeCanvas);
      addClickListeners();
      handleInactiveUser();
    })();
    function handleInactiveUser() {
      var inactive = setTimeout(function() {
        fauxClick(cW / 2, cH / 2);
      }, 2000);
      function clearInactiveTimeout() {
        clearTimeout(inactive);
        document.removeEventListener("mousedown", clearInactiveTimeout);
        document.removeEventListener("touchstart", clearInactiveTimeout);
      }
      document.addEventListener("mousedown", clearInactiveTimeout);
      document.addEventListener("touchstart", clearInactiveTimeout);
    }
    function fauxClick(x, y) {
      var faux = new Event("mousedown");
      faux.pageX = x;
      faux.pageY = y;
      document.dispatchEvent(faux);
    }
  </script>
</body>
</html>

About this animation

Ripple effect on mouse click.

Under the hood

Event-Listener Ripple Math

This script acts on the window.addEventListener('click') event. Upon firing, it captures the user's clientX and clientY mouse coordinates and spawns an ephemeral ripple element (or canvas ring) that expands via CSS transform: scale() before unmounting itself for memory cleanup.

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