Click Response
Live previewPerformance varies by device
No account needed to copy or download. Check dependencies and thelicense status before publishing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Click Response</title>
<!-- Anime.js library for animations -->
<script src="../vendor/anime-1.0.0.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 = [];
var lastInteraction = Date.now();
// 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) {
// Real user input resets the idle clock; ambient faux clicks
// (marked __isFaux) do not, so real clicks always take priority.
if (!e.__isFaux) lastInteraction = Date.now();
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() {
// Ambient idle ripples: while the user has not interacted
// recently, fire a faux click every ~2.1s at a random point in
// the central area so the field stays alive. Real clicks update
// lastInteraction in handleEvent and pause the ambient cycle.
// Gated off under prefers-reduced-motion (static frame held).
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
var IDLE_AFTER = 2400;
var AMBIENT_EVERY = 2100;
var lastAmbient = 0;
setInterval(function() {
var now = Date.now();
if (now - lastInteraction < IDLE_AFTER) return;
if (now - lastAmbient < AMBIENT_EVERY) return;
lastAmbient = now;
fauxClick(
cW * (0.25 + Math.random() * 0.5),
cH * (0.25 + Math.random() * 0.5)
);
}, 250);
}
function fauxClick(x, y) {
var faux = new Event("mousedown");
faux.pageX = x;
faux.pageY = y;
faux.__isFaux = true;
document.dispatchEvent(faux);
}
</script>
</body>
</html>About this animation
Ripple effect on mouse click.
Under the hood
Anime.js Canvas Click Burst
A
document.addEventListener('mousedown') (and touchstart) handler reads pageX and pageY and uses Anime.js to tween three Canvas 2D circle effects drawn with ctx.arc(): a page-filling circle sized by calcPageFillRadius(), a fading ripple ring, and a burst of 32 particles. The tweens use easeOutQuart and easeOutExpo, finished animations are removed from the render list, and after 2.4 seconds without input an idle timer fires a faux click every ~2.1 seconds at a random central point, pausing whenever a real click resets the idle clock.Related