Skip to content

Instantly share code, notes, and snippets.

@Xaypanya
Created October 3, 2025 05:24
Show Gist options
  • Select an option

  • Save Xaypanya/2477a657709ed5c783d52a894e5f74b0 to your computer and use it in GitHub Desktop.

Select an option

Save Xaypanya/2477a657709ed5c783d52a894e5f74b0 to your computer and use it in GitHub Desktop.
Interactive Particle Image Hover Effect
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hover → Particle</title>
<style>
:root {
--w: 520px; /* canvas width (tweakable) */
--density: 6; /* smaller = more particles */
--particle-size: 3; /* pixel size of each particle */
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
background: #0b0f14;
color: #cbd5e1;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
display: grid;
place-items: center;
}
.card {
width: min(var(--w), 90vw);
background: #0f172a;
border: 1px solid #1f2937;
border-radius: 16px;
padding: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,.25);
}
.stage {
position: relative;
overflow: hidden;
border-radius: 12px;
background: radial-gradient(1200px 500px at 40% -10%, rgba(59,130,246,.12), transparent 60%),
radial-gradient(800px 400px at 80% 110%, rgba(16,185,129,.12), transparent 60%);
}
canvas { display: block; width: 100%; height: auto; }
.hint {
text-align: center;
font-size: 12px;
opacity: .7;
margin-top: 10px;
}
.badge {
position: absolute; inset: 10px auto auto 10px;
font-size: 11px; letter-spacing: .06em;
padding: 6px 8px; border-radius: 999px;
background: rgba(17,24,39,.7); border: 1px solid rgba(255,255,255,.08);
backdrop-filter: blur(6px);
}
</style>
</head>
<body>
<div class="card">
<div class="stage">
<span class="badge">hover to disperse • particles reform as you move</span>
<canvas id="c"></canvas>
</div>
<div class="hint">Image: fluffy cat (PNG). You can swap the URL inside the script.</div>
</div>
<script>
// === CONFIG ===
const IMG_URL = "https://static.vecteezy.com/system/resources/thumbnails/047/493/988/small_2x/hairy-fluffy-cat-playing-png.png";
const DENSITY = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--density')) || 6;
const PARTICLE_SIZE = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--particle-size')) || 3;
const MAX_PARTICLES = 20000;
const EXPLOSION_SPEED = 1.5;
const RETURN_SPEED = 0.10;
const JITTER = 0.4;
const ALPHA_THRESHOLD = 40;
const INFLUENCE_RADIUS = 80;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const sampler = document.createElement('canvas');
const sctx = sampler.getContext('2d');
let w, h, device = Math.min(window.devicePixelRatio || 1, 2);
let particles = [];
let mouse = null; // start with no mouse
function resizeTo(widthPx) {
w = Math.floor(widthPx);
h = Math.floor(widthPx * img.naturalHeight / img.naturalWidth);
canvas.width = Math.floor(w * device);
canvas.height = Math.floor(h * device);
sampler.width = Math.floor(w);
sampler.height = Math.floor(h);
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
}
class P {
constructor(x, y, color) {
this.x = x; this.y = y;
this.px = x; this.py = y;
this.vx = 0; this.vy = 0;
this.c = color;
}
draw() {
ctx.fillStyle = this.c;
ctx.fillRect(
Math.floor(this.px * device),
Math.floor(this.py * device),
PARTICLE_SIZE * device,
PARTICLE_SIZE * device
);
}
}
function buildParticles() {
particles = [];
sctx.clearRect(0, 0, sampler.width, sampler.height);
sctx.drawImage(img, 0, 0, sampler.width, sampler.height);
let imgData;
try {
imgData = sctx.getImageData(0, 0, sampler.width, sampler.height).data;
} catch (e) {
console.warn('CORS blocked pixel sampling.', e);
}
const step = Math.max(1, DENSITY);
const w0 = sampler.width;
const h0 = sampler.height;
for (let y = 0; y < h0; y += step) {
for (let x = 0; x < w0; x += step) {
if (particles.length >= MAX_PARTICLES) break;
let col = '#ffffff';
if (imgData) {
const idx = (y * w0 + x) * 4;
const r = imgData[idx], g = imgData[idx + 1], b = imgData[idx + 2], a = imgData[idx + 3];
if (a < ALPHA_THRESHOLD) continue;
col = `rgb(${r},${g},${b})`;
} else {
if (((x + y) % (step * 2)) !== 0) continue;
}
particles.push(new P(x, y, col));
}
}
}
function renderIdle() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(device, device);
ctx.drawImage(img, 0, 0, w, h);
ctx.restore();
}
function renderParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
for (const p of particles) p.draw();
ctx.restore();
}
let raf;
function tick() {
for (const p of particles) {
if (mouse) {
const dx = p.px - mouse.x;
const dy = p.py - mouse.y;
const d = Math.hypot(dx, dy) + 0.001;
if (d < INFLUENCE_RADIUS) {
const repel = (INFLUENCE_RADIUS - d) / INFLUENCE_RADIUS;
p.vx += (dx / d) * repel * 1.5 + (Math.random() - .5) * JITTER;
p.vy += (dy / d) * repel * 1.5 + (Math.random() - .5) * JITTER;
} else {
p.vx += (p.x - p.px) * RETURN_SPEED;
p.vy += (p.y - p.py) * RETURN_SPEED;
}
} else {
// no mouse → particles go back to home position and stay
p.vx += (p.x - p.px) * RETURN_SPEED;
p.vy += (p.y - p.py) * RETURN_SPEED;
}
p.vx *= 0.9; p.vy *= 0.9;
p.px += p.vx; p.py += p.vy;
}
renderParticles();
raf = requestAnimationFrame(tick);
}
function onMove(e) {
const rect = canvas.getBoundingClientRect();
mouse = {
x: (e.clientX - rect.left) * (w / rect.width),
y: (e.clientY - rect.top) * (h / rect.height)
};
}
function onLeave() {
mouse = null; // stop influencing when mouse leaves
}
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const targetW = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--w')) || 520;
resizeTo(targetW);
buildParticles();
renderIdle();
cancelAnimationFrame(raf);
raf = requestAnimationFrame(tick);
};
img.src = IMG_URL;
let t;
window.addEventListener('resize', () => {
clearTimeout(t);
t = setTimeout(() => {
const rect = canvas.parentElement.getBoundingClientRect();
const targetW = Math.min(rect.width, 900);
resizeTo(targetW);
buildParticles();
renderIdle();
}, 120);
});
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseleave', onLeave);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment