Magnetic Mouse Trails
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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magnetic Mouse Trails</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #0f172a;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="magneticCanvas"></canvas>
<script>
const canvas = document.getElementById('magneticCanvas');
const ctx = canvas.getContext('2d');
let width, height;
let particles = [];
const config = {
particleCount: 500, // Reduced for wider compatibility, can handle thousands
repelRadius: 150,
returnSpeed: 0.1,
color: '#38bdf8'
};
let mouse = { x: -1000, y: -1000, vx: 0, vy: 0 };
let lastMouse = { x: -1000, y: -1000 };
// Idle drift state: lastInputTime is performance.now() of the
// last real mouse/touch input (-2000 = idle from load). idlePos
// is the eased autonomous attractor used while idle.
let lastInputTime = -2000;
let idleTime = 0;
let idlePos = { x: -1000, y: -1000 };
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
init();
}
class Particle {
constructor(x, y) {
this.ox = x; // Origin X
this.oy = y; // Origin Y
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.size = Math.random() * 2 + 1;
// Randomize color slightly around the base blue
const brightness = Math.floor(Math.random() * 50) + 200;
this.color = `rgba(56, 189, 248, ${Math.random() * 0.5 + 0.5})`;
}
update() {
// Calculate distance to mouse
const dx = mouse.x - this.x;
const dy = mouse.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Magnetic Repulsion
if (dist < config.repelRadius) {
const force = (config.repelRadius - dist) / config.repelRadius;
const angle = Math.atan2(dy, dx);
// We push the particle AWAY from the mouse
// and add some of the mouse velocity for a "swirl" effect
this.vx -= Math.cos(angle) * force * 5 + mouse.vx * force * 0.1;
this.vy -= Math.sin(angle) * force * 5 + mouse.vy * force * 0.1;
}
// Spring back to origin (magnetic attraction to home base)
this.vx += (this.ox - this.x) * config.returnSpeed;
this.vy += (this.oy - this.y) * config.returnSpeed;
// Friction
this.vx *= 0.85;
this.vy *= 0.85;
this.x += this.vx;
this.y += this.vy;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
function init() {
particles = [];
// Create a grid of particles
const spacing = 20;
const cols = Math.floor(width / spacing);
const rows = Math.floor(height / spacing);
// Adjust offset to center grid
const offsetX = (width - cols * spacing) / 2;
const offsetY = (height - rows * spacing) / 2;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
// Skip some for organic feel
if (Math.random() > 0.2) {
particles.push(new Particle(offsetX + i * spacing, offsetY + j * spacing));
}
}
}
}
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
function animate() {
// Fading trail effect via low opacity clearRect alternative
ctx.fillStyle = 'rgba(15, 23, 42, 0.3)';
ctx.fillRect(0, 0, width, height);
// Idle drift: after 2s without real input, ease the
// effective attractor along a slow Lissajous path so the
// dot field stays alive. A real mousemove updates mouse and
// lastInputTime in the handlers, taking over immediately.
// Skipped under prefers-reduced-motion (animate() runs a
// single static frame there anyway).
if (!reducedMotion) {
if (performance.now() - lastInputTime > 2000) {
idleTime += 0.008;
const tx = width * (0.5 + 0.34 * Math.sin(idleTime * 0.9));
const ty = height * (0.5 + 0.30 * Math.sin(idleTime * 1.27 + 1.3));
if (idlePos.x < -500) { idlePos.x = tx; idlePos.y = ty; }
idlePos.x += (tx - idlePos.x) * 0.04;
idlePos.y += (ty - idlePos.y) * 0.04;
mouse.x = idlePos.x;
mouse.y = idlePos.y;
} else {
// Keep the idle attractor at the cursor so the next
// idle phase hands off smoothly from its position.
idlePos.x = mouse.x;
idlePos.y = mouse.y;
}
}
// Calculate mouse velocity for swirling
mouse.vx = mouse.x - lastMouse.x;
mouse.vy = mouse.y - lastMouse.y;
lastMouse.x = mouse.x;
lastMouse.y = mouse.y;
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
}
if (!reducedMotion) requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
lastInputTime = performance.now();
});
window.addEventListener('mouseout', () => {
mouse.x = -1000;
mouse.y = -1000;
lastInputTime = -2000;
});
// Touch support for mobile interaction
window.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent scrolling
mouse.x = e.touches[0].clientX;
mouse.y = e.touches[0].clientY;
lastInputTime = performance.now();
}, { passive: false });
window.addEventListener('touchend', () => {
mouse.x = -1000;
mouse.y = -1000;
lastInputTime = -2000;
});
resize();
animate();
</script>
</body>
</html>About this animation
Viscous fluid particles that exhibit magnetic attraction and smooth repulsions based on cursor movement.
Under the hood
Friction & Attraction Physics Vectors
These particles use two distinct physics rules. First: Magnetic repulsion via
Math.atan2(dy, dx) pushes the particle away from the mouse. Second: An elastic attraction variable pulls the particle smoothly back to its original origin point. A low-opacity fillRect across the canvas ensures a gorgeous trailing burn-in effect. After 2 seconds without mouse or touch input, the attractor eases along a slow Lissajous path on its own until real input takes over again.Related