Devlog 12: Making Clicks Feel Good With PixiJS Particles
The clicking worked, but it felt flat. You'd tap the button and a number would go up. That's it. No juice, no feedback, no satisfaction.
I needed particles. Little bursts of color that spawn from your finger when you tap, making each vote feel impactful.
Why PixiJS?
I tried CSS animations first. They looked okay on desktop but tanked frame rates on mobile. CSS is great for simple stuff, but for 100+ particles spawning and animating simultaneously, you need hardware acceleration.
PixiJS uses WebGL, which runs on the GPU. This means I can render way more particles while maintaining 60fps. These particles follow organic curved trajectories using cubic Bezier curves, creating satisfying arcs instead of boring linear motion.
Setting Up the Canvas
PixiJS creates a canvas that overlays the voting area:
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
transparent: true,
antialias: true,
resolution: window.devicePixelRatio || 1,
});
container.appendChild(app.view);
The transparent: true part is key because I want particles to appear over the existing UI, not replace it.
Object Pooling
My first version created new particle objects on every click. This caused garbage collection pauses that made the game stutter.
The fix: pre-create a pool of particles and reuse them:
const POOL_SIZE = 200;
const particlePool: PIXI.Graphics[] = [];
for (let i = 0; i < POOL_SIZE; i++) {
const particle = new PIXI.Graphics();
particle.beginFill(0xFFFFFF);
particle.drawCircle(0, 0, 3);
particle.endFill();
particle.alpha = 0;
particlePool.push(particle);
app.stage.addChild(particle);
}
Now when you click, I just grab an inactive particle from the pool, update its properties, and make it visible. No allocation, no garbage collection.
Spawning Particles
When you click, I spawn 8 particles in a radial burst:
function spawnClickParticles(x: number, y: number, side: 'a' | 'b') {
const color = side === 'a' ? 0xFF6B6B : 0x4ECDC4;
for (let i = 0; i < 8; i++) {
const particle = getAvailableParticle();
if (!particle) break;
particle.x = x;
particle.y = y;
particle.alpha = 1;
particle.tint = color;
// Radial velocity
const angle = (Math.PI * 2 * i) / 8;
const speed = 2 + Math.random() * 3;
particle.velocityX = Math.cos(angle) * speed;
particle.velocityY = Math.sin(angle) * speed;
particle.life = 1.0;
}
}
Each particle shoots out at a different angle, creating a satisfying burst effect. I randomize the speed slightly so they don't all move in perfect sync (which looks robotic).
Animation Loop
PixiJS has a built-in ticker that runs every frame. I use it to update all active particles:
app.ticker.add((delta) => {
particlePool.forEach(particle => {
if (particle.alpha === 0) return;
// Move
particle.x += particle.velocityX * delta;
particle.y += particle.velocityY * delta;
// Gravity
particle.velocityY += 0.2 * delta;
// Fade out
particle.life -= 0.02 * delta;
particle.alpha = Math.max(0, particle.life);
});
});
This creates a nice arc as particles fall with gravity while fading out. The whole animation takes about 2 seconds.
Performance Optimization
I had to be careful about performance here. On a cheap Android phone, I was seeing frame drops when lots of particles were active.
The fixes:
- Particle pooling (no allocations during gameplay)
- Early return (skip inactive particles in the loop)
- GPU rendering (PixiJS handles this)
- Limit particle count (max 200 active at once)
Now it runs at 60fps even on a 3-year-old budget phone.
The Difference It Makes
Before particles: clicking felt mechanical and boring. Numbers went up, but there was no visual reward.
After particles: every click gives you instant, satisfying feedback. The burst of color reinforces that your action mattered.
It's such a small detail, but it completely changes how the game feels. People comment on how "satisfying" the clicking is, and particles are a huge part of that.
Takeaway
Visual feedback is everything for interaction-heavy apps. A number incrementing isn't enough. You need motion, color, something that acknowledges the user's input.
PixiJS was overkill for what I'm doing (simple circles), but the performance benefits on mobile made it worth the extra complexity. CSS animations just can't compete with WebGL for particle systems.
Object pooling is one of those game dev tricks that seems weird until you need it, then it's essential. Pre-allocating and reusing objects saves so much performance.
These particle effects complement other animation techniques I've implemented, including smooth vote counter interpolation which makes the numbers count up naturally even though votes arrive in batches. Together, they create a polished, satisfying user experience.
Next week I'm writing about testing strategies and how I caught bugs before they hit production.