Skip to content

Manual Animation Control

ParticleText.js provides complete programmatic control over the animation lifecycle. Instead of starting automatically, you can control when animations start, stop, and even render individual frames manually. This is essential for creating sophisticated interactions, optimizing performance, and coordinating with other page elements.

By default, ParticleText.js starts animating immediately after initialization. Setting autoAnimate: false disables this behavior, giving you full control.

// Starts animating immediately
const instance = initParticleJS('#canvas', {
text: 'AUTO',
colors: ['#695aa6']
// autoAnimate: true (default)
});
// Animation loop is running
console.log(instance.isAnimating); // true
// Waits for your command
const instance = initParticleJS('#canvas', {
text: 'MANUAL',
colors: ['#695aa6'],
autoAnimate: false // Don't start automatically
});
// Animation not running yet
console.log(instance.isAnimating); // false
// Start when you're ready
instance.startAnimation();

Starts the animation loop. Safe to call multiple times - will not create duplicate animation loops.

const instance = initParticleJS('#canvas', {
text: 'START',
colors: ['#695aa6'],
autoAnimate: false
});
// Start the animation
instance.startAnimation();
// Safe to call again - no effect
instance.startAnimation();
instance.startAnimation(); // Still only one loop running

When to use:

  • Delayed animation start
  • Restart after stopping
  • Conditional animation based on user action
  • Animation triggered by scroll position

Stops the animation loop and cleans up resources. This cancels the requestAnimationFrame and prevents memory leaks.

const instance = initParticleJS('#canvas', {
text: 'STOP',
colors: ['#695aa6']
});
// Stop the animation
instance.destroy();
console.log(instance.isAnimating); // false
// Can restart if needed
instance.startAnimation();

When to use:

  • Pause animation temporarily
  • Clean up when component unmounts
  • Stop animation when tab is not visible
  • Performance optimization

Renders a single frame without starting the animation loop. Useful for manual frame-by-frame control or static rendering.

const instance = initParticleJS('#canvas', {
text: 'FRAME',
colors: ['#695aa6'],
autoAnimate: false // Don't animate
});
// Render a single frame
instance.forceRequestAnimationFrame();
// Render another frame
instance.forceRequestAnimationFrame();
// Each call renders one frame, no continuous loop

When to use:

  • Frame-by-frame animation
  • Step debugging
  • Synchronize with external timing
  • Screenshot/export functionality

Boolean property indicating whether the animation loop is currently running.

const instance = initParticleJS('#canvas', {
text: 'STATUS',
colors: ['#695aa6'],
autoAnimate: false
});
console.log(instance.isAnimating); // false
instance.startAnimation();
console.log(instance.isAnimating); // true
instance.destroy();
console.log(instance.isAnimating); // false

Array containing all particle objects. Useful for debugging or custom manipulation.

const instance = initParticleJS('#canvas', {
text: 'PARTICLES',
colors: ['#695aa6']
});
console.log('Particle count:', instance.particleList.length);
// Access individual particles
instance.particleList.forEach((particle, index) => {
console.log(`Particle ${index}:`, {
x: particle.x,
y: particle.y,
color: particle.color,
radius: particle.r
});
});

Start animation only when user clicks a button:

const instance = initParticleJS('#canvas', {
text: 'CLICK TO START',
colors: ['#695aa6'],
autoAnimate: false
});
document.getElementById('start-btn').addEventListener('click', () => {
instance.startAnimation();
});

Start animation when element enters viewport:

const instance = initParticleJS('#canvas', {
text: 'SCROLL',
colors: ['#695aa6'],
autoAnimate: false
});
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
instance.startAnimation();
} else {
instance.destroy(); // Stop when not visible
}
});
});
observer.observe(document.getElementById('canvas'));

Pause animation when tab is not visible:

const instance = initParticleJS('#canvas', {
text: 'VISIBILITY',
colors: ['#695aa6']
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
instance.destroy();
console.log('Tab hidden - animation paused');
} else {
instance.startAnimation();
console.log('Tab visible - animation resumed');
}
});

Only animate on desktop (performance optimization):

const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const instance = initParticleJS('#canvas', {
text: 'RESPONSIVE',
colors: ['#695aa6'],
autoAnimate: !isMobile // Auto-start only on desktop
});
// Mobile users can click to enable
if (isMobile) {
document.getElementById('enable-btn').addEventListener('click', () => {
instance.startAnimation();
});
}

Control multiple canvas instances together:

const canvas1 = initParticleJS('#canvas1', {
text: 'ONE',
colors: ['#FF6B6B'],
autoAnimate: false
});
const canvas2 = initParticleJS('#canvas2', {
text: 'TWO',
colors: ['#4ECDC4'],
autoAnimate: false
});
const canvas3 = initParticleJS('#canvas3', {
text: 'THREE',
colors: ['#FFD93D'],
autoAnimate: false
});
// Start all at once
function startAll() {
canvas1.startAnimation();
canvas2.startAnimation();
canvas3.startAnimation();
}
// Stop all at once
function stopAll() {
canvas1.destroy();
canvas2.destroy();
canvas3.destroy();
}
document.getElementById('start-all').onclick = startAll;
document.getElementById('stop-all').onclick = stopAll;

Control animation frame-by-frame (e.g., timeline scrubber):

const instance = initParticleJS('#canvas', {
text: 'SCRUB',
colors: ['#695aa6'],
autoAnimate: false
});
const slider = document.getElementById('timeline-slider');
slider.addEventListener('input', (e) => {
const frame = parseInt(e.target.value);
// Render specific number of frames
for (let i = 0; i < frame; i++) {
instance.forceRequestAnimationFrame();
}
});

Run animation for specific duration:

const instance = initParticleJS('#canvas', {
text: 'TIMED',
colors: ['#695aa6'],
autoAnimate: false
});
function animateForDuration(ms) {
instance.startAnimation();
setTimeout(() => {
instance.destroy();
console.log(`Animation ran for ${ms}ms`);
}, ms);
}
// Animate for 5 seconds
animateForDuration(5000);

Integrate with custom game loop or animation framework:

const instance = initParticleJS('#canvas', {
text: 'CUSTOM',
colors: ['#695aa6'],
autoAnimate: false // We'll control the loop
});
let lastTime = 0;
const targetFPS = 30; // Custom frame rate
const frameInterval = 1000 / targetFPS;
function customLoop(currentTime) {
const deltaTime = currentTime - lastTime;
if (deltaTime >= frameInterval) {
// Render ParticleText frame
instance.forceRequestAnimationFrame();
// Your other animations
updateOtherAnimations(deltaTime);
lastTime = currentTime;
}
requestAnimationFrame(customLoop);
}
requestAnimationFrame(customLoop);

Create a pause/resume button:

const instance = initParticleJS('#canvas', {
text: 'TOGGLE',
colors: ['#695aa6']
});
let isPaused = false;
document.getElementById('toggle-btn').addEventListener('click', () => {
if (isPaused) {
instance.startAnimation();
isPaused = false;
console.log('Resumed');
} else {
instance.destroy();
isPaused = true;
console.log('Paused');
}
});

Stop animation when battery is low:

const instance = initParticleJS('#canvas', {
text: 'BATTERY',
colors: ['#695aa6']
});
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
function updateAnimationState() {
if (battery.level < 0.2 || !battery.charging) {
instance.destroy();
console.log('Low battery - animation paused');
} else {
instance.startAnimation();
}
}
battery.addEventListener('levelchange', updateAnimationState);
battery.addEventListener('chargingchange', updateAnimationState);
updateAnimationState();
});
}

Don’t start animation until canvas is near viewport:

const instance = initParticleJS('#canvas', {
text: 'LAZY',
colors: ['#695aa6'],
autoAnimate: false
});
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
instance.startAnimation();
observer.disconnect(); // Only start once
}
});
}, {
rootMargin: '100px' // Start 100px before entering viewport
});
observer.observe(document.getElementById('canvas'));

Reduce frame rate on slow devices:

const instance = initParticleJS('#canvas', {
text: 'THROTTLE',
colors: ['#695aa6'],
autoAnimate: false
});
const isSlowDevice = /iPhone 6|iPhone 7|Android 4/i.test(navigator.userAgent);
const frameSkip = isSlowDevice ? 2 : 1; // Render every 2nd frame on slow devices
let frameCount = 0;
function throttledLoop() {
frameCount++;
if (frameCount % frameSkip === 0) {
instance.forceRequestAnimationFrame();
}
requestAnimationFrame(throttledLoop);
}
throttledLoop();

Properly clean up when component unmounts:

// React example
useEffect(() => {
const instance = initParticleJS('#canvas', {
text: 'CLEANUP',
colors: ['#695aa6']
});
// Cleanup function
return () => {
instance.destroy();
console.log('Animation cleaned up');
};
}, []);
// Vue example
export default {
mounted() {
this.instance = initParticleJS('#canvas', {
text: 'CLEANUP',
colors: ['#695aa6']
});
},
beforeUnmount() {
this.instance.destroy();
}
};
// Vanilla JS - page navigation
window.addEventListener('beforeunload', () => {
instance.destroy();
});
const instance = initParticleJS('#canvas', {
text: 'DEBUG',
colors: ['#695aa6']
});
// Monitor state changes
setInterval(() => {
console.log({
isAnimating: instance.isAnimating,
particleCount: instance.particleList.length,
currentBreakpoint: instance.getCurrentBreakpoint()
});
}, 1000);
let frameCount = 0;
let lastTime = performance.now();
const instance = initParticleJS('#canvas', {
text: 'FPS',
colors: ['#695aa6']
});
setInterval(() => {
const currentTime = performance.now();
const elapsed = currentTime - lastTime;
const fps = Math.round((frameCount * 1000) / elapsed);
console.log(`FPS: ${fps}`);
frameCount = 0;
lastTime = currentTime;
}, 1000);
// Count frames
const originalRender = instance.forceRequestAnimationFrame;
instance.forceRequestAnimationFrame = function() {
frameCount++;
originalRender.call(instance);
};
// ✅ Good: Clean up when done
const instance = initParticleJS('#canvas', {
text: 'CLEANUP',
colors: ['#695aa6']
});
// When component unmounts or page changes
instance.destroy();
// ❌ Bad: Memory leak - animation keeps running
const instance = initParticleJS('#canvas', {
text: 'LEAK',
colors: ['#695aa6']
});
// Never calls destroy() - animation loop continues forever
// ✅ Good: Check before action
if (!instance.isAnimating) {
instance.startAnimation();
}
// ❌ Unnecessary: startAnimation() already checks
// But doesn't hurt - method is safe to call multiple times
instance.startAnimation();
instance.startAnimation(); // Safe but redundant
// ✅ Good: Respect tab visibility
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
instance.destroy();
} else {
instance.startAnimation();
}
});
// ❌ Bad: Waste resources when tab is hidden
// Animation runs even when user can't see it
// ✅ Good: Manage all instances
const instances = [];
instances.push(initParticleJS('#canvas1', { /* ... */ }));
instances.push(initParticleJS('#canvas2', { /* ... */ }));
instances.push(initParticleJS('#canvas3', { /* ... */ }));
// Stop all at once
function cleanupAll() {
instances.forEach(instance => instance.destroy());
}
// ❌ Bad: Lose reference to instances
initParticleJS('#canvas1', { /* ... */ }); // Can't control later
initParticleJS('#canvas2', { /* ... */ }); // Can't control later
// ✅ Good: Defensive programming
try {
const instance = initParticleJS('#canvas', {
text: 'SAFE',
colors: ['#695aa6']
});
if (instance && instance.startAnimation) {
instance.startAnimation();
}
} catch (error) {
console.error('Animation failed:', error);
}
// ❌ Bad: Assumes success
const instance = initParticleJS('#canvas', { /* ... */ });
instance.startAnimation(); // Might fail if canvas not found

Animation doesn’t start?

  • Check autoAnimate is not false
  • Verify startAnimation() is called
  • Ensure canvas element exists
  • Check browser console for errors

Can’t stop animation?

  • Call destroy() method
  • Check isAnimating property
  • Ensure you have reference to instance

Multiple animation loops running?

  • startAnimation() is safe to call multiple times
  • Only one loop will run per instance
  • Check you’re not creating multiple instances

Memory leak issues?

  • Always call destroy() when done
  • Use visibility API to pause hidden tabs
  • Clean up in component unmount hooks

Frame rate too low?

  • Use autoAnimate: false and custom loop
  • Implement frame skipping for slow devices
  • Reduce particle count with maxParticles