Multiple Instances
You can create multiple ParticleText.js instances on a single page, each with its own canvas, configuration, and behavior. This guide covers best practices for managing multiple instances, optimizing performance, and coordinating animations across multiple canvases.
Basic Setup
Section titled “Basic Setup”Creating multiple instances is straightforward - just call initParticleJS() for each canvas element:
// Create three independent instancesconst instance1 = initParticleJS('#canvas1', {text: 'FIRST',colors: ['#695aa6']});
const instance2 = initParticleJS('#canvas2', {text: 'SECOND',colors: ['#FF6B6B']});
const instance3 = initParticleJS('#canvas3', {text: 'THIRD',colors: ['#4ECDC4']});
// Each instance operates independentlyconsole.log(instance1.particleList.length); // Particle count for firstconsole.log(instance2.isAnimating); // Animation state for secondKey points:
- Each instance is independent with its own particles, animation loop, and state
- Store references to instances if you need to control them later
- Each canvas needs a unique selector
Managing Instance References
Section titled “Managing Instance References”Store in Variables
Section titled “Store in Variables”// Store individual referencesconst heroCanvas = initParticleJS('#hero', {text: 'HERO',colors: ['#695aa6'],maxParticles: 3000});
const footerCanvas = initParticleJS('#footer', {text: 'FOOTER',colors: ['#4ECDC4'],maxParticles: 1000});
// Control individuallyheroCanvas.destroy(); // Stop hero animationfooterCanvas.startAnimation(); // Start footer animationStore in Array
Section titled “Store in Array”// Store multiple instances in arrayconst instances = [];
instances.push(initParticleJS('#canvas1', {text: 'ONE',colors: ['#FF6B6B']}));
instances.push(initParticleJS('#canvas2', {text: 'TWO',colors: ['#4ECDC4']}));
instances.push(initParticleJS('#canvas3', {text: 'THREE',colors: ['#FFD93D']}));
// Control all at onceinstances.forEach(instance => instance.destroy());
// Check status of allconst allAnimating = instances.every(instance => instance.isAnimating);console.log('All animating:', allAnimating);Store in Map
Section titled “Store in Map”// Store instances in a map for easy lookupconst instanceMap = new Map();
instanceMap.set('hero', initParticleJS('#hero', {text: 'HERO',colors: ['#695aa6']}));
instanceMap.set('sidebar', initParticleJS('#sidebar', {text: 'MENU',colors: ['#FF6B6B']}));
instanceMap.set('footer', initParticleJS('#footer', {text: 'FOOTER',colors: ['#4ECDC4']}));
// Access by nameinstanceMap.get('hero').destroy();instanceMap.get('sidebar').startAnimation();
// Clean up allinstanceMap.forEach(instance => instance.destroy());instanceMap.clear();Store in Object
Section titled “Store in Object”// Store instances in object with semantic keysconst particles = {hero: initParticleJS('#hero', { text: 'WELCOME', colors: ['#695aa6'], maxParticles: 3000}),
feature1: initParticleJS('#feature1', { text: 'FAST', colors: ['#FF6B6B'], maxParticles: 2000}),
feature2: initParticleJS('#feature2', { text: 'EASY', colors: ['#4ECDC4'], maxParticles: 2000}),
footer: initParticleJS('#footer', { text: 'CONTACT', colors: ['#FFD93D'], maxParticles: 1500})};
// Access semanticallyparticles.hero.destroy();particles.footer.startAnimation();
// Clean upObject.values(particles).forEach(instance => instance.destroy());Common Patterns
Section titled “Common Patterns”Comparison Layout
Section titled “Comparison Layout”Side-by-side canvases for demonstrating different configurations:
// HTML structure// <div class="comparison">// <canvas id="config-a"></canvas>// <canvas id="config-b"></canvas>// </div>
const configA = initParticleJS('#config-a', {text: 'CONFIG A',colors: ['#FF6B6B'],maxParticles: 1500,explosionRadius: { xs: 80, lg: 120 }});
const configB = initParticleJS('#config-b', {text: 'CONFIG B',colors: ['#4ECDC4'],maxParticles: 3000,explosionRadius: { xs: 150, lg: 200 }});
// Styling for comparisondocument.querySelector('.comparison').style.display = 'grid';document.querySelector('.comparison').style.gridTemplateColumns = '1fr 1fr';document.querySelector('.comparison').style.gap = '20px';Gallery Layout
Section titled “Gallery Layout”Grid of multiple particle text items:
// HTML structure// <div class="gallery">// <canvas class="gallery-item" id="item-1"></canvas>// <canvas class="gallery-item" id="item-2"></canvas>// <canvas class="gallery-item" id="item-3"></canvas>// <canvas class="gallery-item" id="item-4"></canvas>// </div>
const galleryConfig = {maxParticles: 1500,trackCursorOnlyInsideCanvas: true, // Important for galleries!explosionRadius: { xs: 80, lg: 120 }};
const galleryItems = [{ id: 'item-1', text: 'DESIGN', color: '#FF6B6B' },{ id: 'item-2', text: 'CODE', color: '#4ECDC4' },{ id: 'item-3', text: 'TEST', color: '#FFD93D' },{ id: 'item-4', text: 'SHIP', color: '#95E1D3' }];
const instances = galleryItems.map(item => {return initParticleJS(`#${item.id}`, { text: item.text, colors: [item.color], ...galleryConfig});});
// CSS Grid stylingdocument.querySelector('.gallery').style.cssText = `display: grid;grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));gap: 20px;`;Section-Based Layout
Section titled “Section-Based Layout”Different sections of a page with their own particle effects:
// Hero sectionconst hero = initParticleJS('#hero-canvas', {text: 'WELCOME',colors: ['#695aa6'],maxParticles: 3000,fontSize: 120,explosionRadius: { xs: 100, lg: 150 }});
// Features section (multiple small canvases)const features = [{ selector: '#feature-fast', text: 'FAST', color: '#FF6B6B' },{ selector: '#feature-easy', text: 'EASY', color: '#4ECDC4' },{ selector: '#feature-free', text: 'FREE', color: '#FFD93D' }].map(feature => {return initParticleJS(feature.selector, { text: feature.text, colors: [feature.color], maxParticles: 1500, fontSize: 80, trackCursorOnlyInsideCanvas: true});});
// Footerconst footer = initParticleJS('#footer-canvas', {text: 'CONTACT',colors: ['#4ECDC4'],maxParticles: 1000,fontSize: 60,particleFriction: 0.98 // Faster settling for footer});Stacked/Overlapping Canvases
Section titled “Stacked/Overlapping Canvases”Layered canvases with different z-index:
// HTML structure// <div class="stack">// <canvas id="background-layer"></canvas>// <canvas id="foreground-layer"></canvas>// </div>
// Background layer - subtle, slowconst background = initParticleJS('#background-layer', {text: 'BACKGROUND',colors: ['#695aa6'],maxParticles: 2000,particleFriction: 0.98,explosionRadius: { xs: 150, lg: 200 }});
// Foreground layer - prominent, interactiveconst foreground = initParticleJS('#foreground-layer', {text: 'FRONT',colors: ['#FF6B6B'],maxParticles: 1500,particleFriction: 0.90,explosionRadius: { xs: 100, lg: 150 }});
// CSS positioningconst stack = document.querySelector('.stack');stack.style.position = 'relative';
document.querySelector('#background-layer').style.cssText = `position: absolute;top: 0;left: 0;opacity: 0.3;z-index: 1;`;
document.querySelector('#foreground-layer').style.cssText = `position: absolute;top: 0;left: 0;z-index: 2;`;Performance Optimization
Section titled “Performance Optimization”Reduce Particle Count Per Canvas
Section titled “Reduce Particle Count Per Canvas”// ❌ Bad: Each canvas uses full particle budgetconst bad1 = initParticleJS('#canvas1', {text: 'FIRST',maxParticles: 5000 // Too many for multiple instances});
const bad2 = initParticleJS('#canvas2', {text: 'SECOND',maxParticles: 5000 // Total: 10,000 particles!});
// ✅ Good: Distribute particle budget across canvasesconst good1 = initParticleJS('#canvas1', {text: 'FIRST',maxParticles: 2000 // Reasonable for multiple instances});
const good2 = initParticleJS('#canvas2', {text: 'SECOND',maxParticles: 2000 // Total: 4,000 particles});
// ✅ Even better: Vary based on importanceconst hero = initParticleJS('#hero', {text: 'HERO',maxParticles: 2500 // Most important});
const sidebar = initParticleJS('#sidebar', {text: 'MENU',maxParticles: 1000 // Less important});
const footer = initParticleJS('#footer', {text: 'FOOTER',maxParticles: 800 // Least important});// Total: 4,300 particles - good distributionStagger Initialization
Section titled “Stagger Initialization”// ❌ Bad: Initialize all at once - causes CPU spikeconst instance1 = initParticleJS('#canvas1', config1);const instance2 = initParticleJS('#canvas2', config2);const instance3 = initParticleJS('#canvas3', config3);const instance4 = initParticleJS('#canvas4', config4);// CPU spike during initialization
// ✅ Good: Stagger initializationconst instances = [];
// Initialize first one immediatelyinstances.push(initParticleJS('#canvas1', config1));
// Stagger the restsetTimeout(() => {instances.push(initParticleJS('#canvas2', config2));}, 100);
setTimeout(() => {instances.push(initParticleJS('#canvas3', config3));}, 200);
setTimeout(() => {instances.push(initParticleJS('#canvas4', config4));}, 300);
// ✅ Better: Use loop with delayconst configs = [{ selector: '#canvas1', config: config1 },{ selector: '#canvas2', config: config2 },{ selector: '#canvas3', config: config3 },{ selector: '#canvas4', config: config4 }];
const instances2 = [];configs.forEach((item, index) => {setTimeout(() => { instances2.push(initParticleJS(item.selector, item.config)); console.log(`Initialized canvas ${index + 1}`);}, index * 100); // 100ms between each});Lazy Load Off-Screen Instances
Section titled “Lazy Load Off-Screen Instances”// Only initialize instances when they enter viewportconst canvases = [{ selector: '#canvas1', config: { text: 'ONE', colors: ['#FF6B6B'] } },{ selector: '#canvas2', config: { text: 'TWO', colors: ['#4ECDC4'] } },{ selector: '#canvas3', config: { text: 'THREE', colors: ['#FFD93D'] } }];
const instances = new Map();
canvases.forEach(({ selector, config }) => {const element = document.querySelector(selector);
// Create IntersectionObserver for each canvasconst observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Initialize when entering viewport if (!instances.has(selector)) { console.log('Initializing', selector); const instance = initParticleJS(selector, config); instances.set(selector, instance); } else { // Restart animation if already initialized instances.get(selector).startAnimation(); } } else { // Stop animation when leaving viewport const instance = instances.get(selector); if (instance) { instance.destroy(); } } });}, { threshold: 0.1, rootMargin: '50px' // Start loading 50px before entering});
observer.observe(element);});Use Restricted Cursor Tracking
Section titled “Use Restricted Cursor Tracking”// ❌ Bad: Global cursor tracking for all instancesconst bad1 = initParticleJS('#canvas1', {text: 'FIRST',colors: ['#FF6B6B'],trackCursorOnlyInsideCanvas: false // Reacts to cursor everywhere});
const bad2 = initParticleJS('#canvas2', {text: 'SECOND',colors: ['#4ECDC4'],trackCursorOnlyInsideCanvas: false // Also reacts to cursor everywhere});// Both canvases react even when cursor is nowhere near them!
// ✅ Good: Restricted tracking for each instanceconst good1 = initParticleJS('#canvas1', {text: 'FIRST',colors: ['#FF6B6B'],trackCursorOnlyInsideCanvas: true // Only reacts inside canvas});
const good2 = initParticleJS('#canvas2', {text: 'SECOND',colors: ['#4ECDC4'],trackCursorOnlyInsideCanvas: true // Only reacts inside canvas});// Each canvas only reacts when cursor is over it// Much better performance and clearer UXProgressive Enhancement
Section titled “Progressive Enhancement”// Load high-quality instances on desktop, simpler on mobileconst isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const instanceConfigs = [{ selector: '#hero', desktop: { text: 'HERO', maxParticles: 3000, fontSize: 120 }, mobile: { text: 'HERO', maxParticles: 1200, fontSize: 80 }},{ selector: '#feature1', desktop: { text: 'FAST', maxParticles: 2000, fontSize: 100 }, mobile: { text: 'FAST', maxParticles: 800, fontSize: 60 }},{ selector: '#feature2', desktop: { text: 'EASY', maxParticles: 2000, fontSize: 100 }, mobile: { text: 'EASY', maxParticles: 800, fontSize: 60 }}];
const instances = instanceConfigs.map(item => {const config = isMobile ? item.mobile : item.desktop;return initParticleJS(item.selector, { ...config, colors: ['#695aa6']});});
// Total particles:// Desktop: 7,000 particles (acceptable)// Mobile: 2,800 particles (good performance)Coordinating Multiple Instances
Section titled “Coordinating Multiple Instances”Start All Instances Together
Section titled “Start All Instances Together”// Initialize without auto-animationconst instances = [initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'], autoAnimate: false}),initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'], autoAnimate: false}),initParticleJS('#canvas3', { text: 'THREE', colors: ['#FFD93D'], autoAnimate: false})];
// Start all animations togetherfunction startAll() {instances.forEach(instance => instance.startAnimation());console.log('All animations started');}
// Trigger on user actiondocument.getElementById('start-btn').addEventListener('click', startAll);
// Or after page loadwindow.addEventListener('load', () => {setTimeout(startAll, 500); // Start after 500ms delay});Stop All Instances Together
Section titled “Stop All Instances Together”const instances = [initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] }),initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] }),initParticleJS('#canvas3', { text: 'THREE', colors: ['#FFD93D'] })];
function stopAll() {instances.forEach(instance => { instance.destroy();});console.log('All animations stopped');}
// Stop on page unloadwindow.addEventListener('beforeunload', stopAll);
// Stop on visibility changedocument.addEventListener('visibilitychange', () => {if (document.hidden) { stopAll();} else { instances.forEach(instance => instance.startAnimation());}});
// Stop on button clickdocument.getElementById('stop-btn').addEventListener('click', stopAll);Sequential Animation
Section titled “Sequential Animation”// Animate instances one after anotherconst configs = [{ selector: '#canvas1', text: 'FIRST', color: '#FF6B6B' },{ selector: '#canvas2', text: 'SECOND', color: '#4ECDC4' },{ selector: '#canvas3', text: 'THIRD', color: '#FFD93D' }];
// Initialize all without animationconst instances = configs.map(config => {return initParticleJS(config.selector, { text: config.text, colors: [config.color], autoAnimate: false});});
// Start animations sequentiallyfunction startSequentially(delay = 500) {instances.forEach((instance, index) => { setTimeout(() => { instance.startAnimation(); console.log(`Started animation ${index + 1}`); }, index * delay);});}
// Trigger on scrolllet hasAnimated = false;window.addEventListener('scroll', () => {if (!hasAnimated && window.scrollY > 300) { startSequentially(); hasAnimated = true;}});Pause/Resume All
Section titled “Pause/Resume All”const instances = [initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] }),initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] }),initParticleJS('#canvas3', { text: 'THREE', colors: ['#FFD93D'] })];
let isPaused = false;
function toggleAll() {if (isPaused) { // Resume all instances.forEach(instance => instance.startAnimation()); isPaused = false; console.log('Resumed all animations');} else { // Pause all instances.forEach(instance => instance.destroy()); isPaused = true; console.log('Paused all animations');}}
// Toggle buttondocument.getElementById('toggle-btn').addEventListener('click', toggleAll);
// Keyboard shortcutdocument.addEventListener('keydown', (e) => {if (e.key === ' ') { // Spacebar e.preventDefault(); toggleAll();}});Conditional Animation
Section titled “Conditional Animation”// Animate only specific instances based on conditionsconst instances = {hero: initParticleJS('#hero', { text: 'HERO', colors: ['#695aa6'], autoAnimate: false}),
feature1: initParticleJS('#feature1', { text: 'FAST', colors: ['#FF6B6B'], autoAnimate: false}),
feature2: initParticleJS('#feature2', { text: 'EASY', colors: ['#4ECDC4'], autoAnimate: false}),
footer: initParticleJS('#footer', { text: 'FOOTER', colors: ['#FFD93D'], autoAnimate: false})};
// Function to start specific instancesfunction startInstances(names) {names.forEach(name => { if (instances[name]) { instances[name].startAnimation(); console.log(`Started ${name}`); }});}
// Function to stop specific instancesfunction stopInstances(names) {names.forEach(name => { if (instances[name]) { instances[name].destroy(); console.log(`Stopped ${name}`); }});}
// Use conditionallyconst isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {// Only animate hero on mobilestartInstances(['hero']);} else {// Animate all on desktopstartInstances(['hero', 'feature1', 'feature2', 'footer']);}
// Or based on performanceif (navigator.hardwareConcurrency >= 8) {// High-end device: animate allstartInstances(Object.keys(instances));} else {// Low-end device: only animate herostartInstances(['hero']);}Responsive Behavior
Section titled “Responsive Behavior”Reinitialize on Resize
Section titled “Reinitialize on Resize”let instances = [];
function initializeInstances() {// Clean up existing instancesinstances.forEach(instance => instance.destroy());instances = [];
const isMobile = window.innerWidth < 768;
if (isMobile) { // Mobile: fewer instances instances.push( initParticleJS('#hero', { text: 'HERO', colors: ['#695aa6'], maxParticles: 1200 }) );} else { // Desktop: more instances instances.push( initParticleJS('#hero', { text: 'HERO', colors: ['#695aa6'], maxParticles: 3000 }), initParticleJS('#feature1', { text: 'FAST', colors: ['#FF6B6B'], maxParticles: 2000 }), initParticleJS('#feature2', { text: 'EASY', colors: ['#4ECDC4'], maxParticles: 2000 }) );}}
// Initialize on loadinitializeInstances();
// Reinitialize on resize (debounced)let resizeTimeout;window.addEventListener('resize', () => {clearTimeout(resizeTimeout);resizeTimeout = setTimeout(initializeInstances, 300);});Breakpoint-Based Instances
Section titled “Breakpoint-Based Instances”const instanceManager = {instances: new Map(),currentBreakpoint: null,
configs: { mobile: [ { selector: '#hero', text: 'HERO', colors: ['#695aa6'], maxParticles: 1200 } ],
tablet: [ { selector: '#hero', text: 'HERO', colors: ['#695aa6'], maxParticles: 2000 }, { selector: '#feature1', text: 'FAST', colors: ['#FF6B6B'], maxParticles: 1500 } ],
desktop: [ { selector: '#hero', text: 'HERO', colors: ['#695aa6'], maxParticles: 3000 }, { selector: '#feature1', text: 'FAST', colors: ['#FF6B6B'], maxParticles: 2000 }, { selector: '#feature2', text: 'EASY', colors: ['#4ECDC4'], maxParticles: 2000 }, { selector: '#footer', text: 'FOOTER', colors: ['#FFD93D'], maxParticles: 1000 } ]},
getBreakpoint() { const width = window.innerWidth; if (width < 768) return 'mobile'; if (width < 1024) return 'tablet'; return 'desktop';},
initialize() { const breakpoint = this.getBreakpoint();
if (breakpoint === this.currentBreakpoint) { return; // No change needed }
// Clean up old instances this.instances.forEach(instance => instance.destroy()); this.instances.clear();
// Create new instances for current breakpoint const configs = this.configs[breakpoint]; configs.forEach(config => { const instance = initParticleJS(config.selector, config); this.instances.set(config.selector, instance); });
this.currentBreakpoint = breakpoint; console.log(`Initialized ${configs.length} instances for ${breakpoint}`);}};
// InitializeinstanceManager.initialize();
// Reinitialize on resizelet resizeTimeout;window.addEventListener('resize', () => {clearTimeout(resizeTimeout);resizeTimeout = setTimeout(() => { instanceManager.initialize();}, 300);});Memory Management
Section titled “Memory Management”Clean Up on Navigation
Section titled “Clean Up on Navigation”// Store all instances globallyconst particleInstances = [];
// Create instancesfunction createInstances() {particleInstances.push( initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] }), initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] }), initParticleJS('#canvas3', { text: 'THREE', colors: ['#FFD93D'] }));}
// Clean up functionfunction cleanupInstances() {particleInstances.forEach(instance => { instance.destroy();});particleInstances.length = 0; // Clear arrayconsole.log('All instances cleaned up');}
// InitializecreateInstances();
// Clean up on page unloadwindow.addEventListener('beforeunload', cleanupInstances);
// For Single Page Applications (SPA)// Clean up when navigating awayrouter.beforeEach((to, from, next) => {cleanupInstances();next();});
// For frameworks// ReactuseEffect(() => {createInstances();return cleanupInstances; // Cleanup on unmount}, []);
// Vueexport default {mounted() { createInstances();},beforeUnmount() { cleanupInstances();}};Prevent Memory Leaks
Section titled “Prevent Memory Leaks”// ❌ Bad: Creating instances without cleanupfunction badPattern() {// Creating new instances every time without destroying old onessetInterval(() => { initParticleJS('#canvas1', { text: 'LEAK', colors: ['#FF6B6B'] }); // Previous instances keep running - memory leak!}, 5000);}
// ✅ Good: Proper cleanup before creating new instancesfunction goodPattern() {let instance = null;
function reinitialize() { // Clean up old instance first if (instance) { instance.destroy(); }
// Create new instance instance = initParticleJS('#canvas1', { text: 'SAFE', colors: ['#FF6B6B'] });}
// Safe to call repeatedlysetInterval(reinitialize, 5000);}
// ✅ Better: Manage multiple instancesclass InstanceManager {constructor() { this.instances = new Map();}
create(selector, config) { // Clean up if already exists this.destroy(selector);
// Create new instance const instance = initParticleJS(selector, config); this.instances.set(selector, instance); return instance;}
destroy(selector) { const instance = this.instances.get(selector); if (instance) { instance.destroy(); this.instances.delete(selector); }}
destroyAll() { this.instances.forEach(instance => instance.destroy()); this.instances.clear();}
get(selector) { return this.instances.get(selector);}
has(selector) { return this.instances.has(selector);}}
// Usageconst manager = new InstanceManager();
manager.create('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] });manager.create('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] });
// Clean up specific instancemanager.destroy('#canvas1');
// Clean up allmanager.destroyAll();Framework Integration
Section titled “Framework Integration”import { useEffect, useRef, useState } from 'react';
function MultipleParticleText() {const canvasRefs = useRef([]);const instancesRef = useRef([]);
const configs = [ { text: 'ONE', colors: ['#FF6B6B'], maxParticles: 2000 }, { text: 'TWO', colors: ['#4ECDC4'], maxParticles: 2000 }, { text: 'THREE', colors: ['#FFD93D'], maxParticles: 2000 }];
useEffect(() => { // Initialize all instances canvasRefs.current.forEach((canvas, index) => { if (canvas) { const instance = initParticleJS(canvas, configs[index]); instancesRef.current.push(instance); } });
// Cleanup on unmount return () => { instancesRef.current.forEach(instance => { instance.destroy(); }); instancesRef.current = []; };}, []);
const handleStopAll = () => { instancesRef.current.forEach(instance => instance.destroy());};
const handleStartAll = () => { instancesRef.current.forEach(instance => instance.startAnimation());};
return ( <div> <div className="controls"> <button onClick={handleStartAll}>Start All</button> <button onClick={handleStopAll}>Stop All</button> </div>
{configs.map((config, index) => ( <canvas key={index} ref={el => canvasRefs.current[index] = el} width={800} height={200} /> ))} </div>);}<template><div> <div class="controls"> <button @click="startAll">Start All</button> <button @click="stopAll">Stop All</button> </div>
<canvas v-for="(config, index) in configs" :key="index" :ref="el => canvasRefs[index] = el" width="800" height="200" /></div></template>
<script setup>import { ref, onMounted, onBeforeUnmount } from 'vue';
const canvasRefs = ref([]);const instances = ref([]);
const configs = [{ text: 'ONE', colors: ['#FF6B6B'], maxParticles: 2000 },{ text: 'TWO', colors: ['#4ECDC4'], maxParticles: 2000 },{ text: 'THREE', colors: ['#FFD93D'], maxParticles: 2000 }];
onMounted(() => {canvasRefs.value.forEach((canvas, index) => { if (canvas) { const instance = initParticleJS(canvas, configs[index]); instances.value.push(instance); }});});
onBeforeUnmount(() => {instances.value.forEach(instance => instance.destroy());instances.value = [];});
const startAll = () => {instances.value.forEach(instance => instance.startAnimation());};
const stopAll = () => {instances.value.forEach(instance => instance.destroy());};</script>Vanilla JS with Class
Section titled “Vanilla JS with Class”class ParticleTextManager {constructor(configs) { this.configs = configs; this.instances = []; this.initialized = false;}
initialize() { if (this.initialized) { console.warn('Already initialized'); return; }
this.configs.forEach(config => { const instance = initParticleJS(config.selector, config.options); this.instances.push({ name: config.name, selector: config.selector, instance: instance }); });
this.initialized = true; console.log(`Initialized ${this.instances.length} instances`);}
destroy() { this.instances.forEach(item => { item.instance.destroy(); }); this.instances = []; this.initialized = false; console.log('Destroyed all instances');}
startAll() { this.instances.forEach(item => { item.instance.startAnimation(); });}
stopAll() { this.instances.forEach(item => { item.instance.destroy(); });}
get(name) { const found = this.instances.find(item => item.name === name); return found ? found.instance : null;}
getStatus() { return this.instances.map(item => ({ name: item.name, isAnimating: item.instance.isAnimating, particleCount: item.instance.particleList.length }));}}
// Usageconst manager = new ParticleTextManager([{ name: 'hero', selector: '#hero', options: { text: 'HERO', colors: ['#695aa6'], maxParticles: 3000 }},{ name: 'footer', selector: '#footer', options: { text: 'FOOTER', colors: ['#4ECDC4'], maxParticles: 1000 }}]);
manager.initialize();
// Control specific instanceconst heroInstance = manager.get('hero');heroInstance.destroy();
// Check statusconsole.log(manager.getStatus());
// Cleanupwindow.addEventListener('beforeunload', () => {manager.destroy();});Best Practices
Section titled “Best Practices”1. Use Restricted Cursor Tracking
Section titled “1. Use Restricted Cursor Tracking”// ✅ Good: Each instance only reacts to its own canvasconst instances = [{ selector: '#canvas1', text: 'ONE' },{ selector: '#canvas2', text: 'TWO' },{ selector: '#canvas3', text: 'THREE' }].map(config => {return initParticleJS(config.selector, { text: config.text, colors: ['#695aa6'], trackCursorOnlyInsideCanvas: true // Critical for multiple instances!});});
// ❌ Bad: All instances react to cursor globallyconst badInstances = [{ selector: '#canvas1', text: 'ONE' },{ selector: '#canvas2', text: 'TWO' }].map(config => {return initParticleJS(config.selector, { text: config.text, colors: ['#695aa6'] // trackCursorOnlyInsideCanvas defaults to false // Both canvases react even when cursor is far away});});2. Budget Your Particles
Section titled “2. Budget Your Particles”// ✅ Good: Total particle budget is reasonableconst totalBudget = 5000;const instanceCount = 3;const perInstance = Math.floor(totalBudget / instanceCount);
const instances = Array(instanceCount).fill(null).map((_, i) => {return initParticleJS(`#canvas${i + 1}`, { text: `CANVAS ${i + 1}`, colors: ['#695aa6'], maxParticles: perInstance // ~1666 per instance});});// Total: ~5000 particles (good)
// ❌ Bad: Each instance uses full budgetconst badInstances = Array(3).fill(null).map((_, i) => {return initParticleJS(`#canvas${i + 1}`, { text: `CANVAS ${i + 1}`, colors: ['#695aa6'], maxParticles: 5000 // Full budget per instance});});// Total: 15,000 particles (too many)3. Clean Up Properly
Section titled “3. Clean Up Properly”// ✅ Good: Proper cleanup lifecycleclass Application {constructor() { this.instances = [];}
init() { this.instances = [ initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] }), initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] }) ];}
destroy() { this.instances.forEach(instance => instance.destroy()); this.instances = [];}
restart() { this.destroy(); this.init();}}
const app = new Application();app.init();
// Cleanup on navigationwindow.addEventListener('beforeunload', () => {app.destroy();});
// ❌ Bad: No cleanupfunction badInit() {initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'] });initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'] });// No way to access or clean up these instances later}4. Stagger Initialization
Section titled “4. Stagger Initialization”// ✅ Good: Staggered initialization prevents CPU spikeasync function initializeStaggered(configs, delay = 100) {const instances = [];
for (const config of configs) { const instance = initParticleJS(config.selector, config.options); instances.push(instance);
// Wait before next initialization await new Promise(resolve => setTimeout(resolve, delay));}
return instances;}
const configs = [{ selector: '#canvas1', options: { text: 'ONE', colors: ['#FF6B6B'] } },{ selector: '#canvas2', options: { text: 'TWO', colors: ['#4ECDC4'] } },{ selector: '#canvas3', options: { text: 'THREE', colors: ['#FFD93D'] } }];
initializeStaggered(configs).then(instances => {console.log('All instances initialized:', instances);});
// ❌ Bad: Initialize all at onceconst badInstances = configs.map(config => {return initParticleJS(config.selector, config.options);});// CPU spike during initialization5. Provide Visual Feedback
Section titled “5. Provide Visual Feedback”// ✅ Good: Clear visual boundaries for each canvasconst instances = [{ selector: '#canvas1', text: 'ONE', color: '#FF6B6B' },{ selector: '#canvas2', text: 'TWO', color: '#4ECDC4' },{ selector: '#canvas3', text: 'THREE', color: '#FFD93D' }].map(config => {const instance = initParticleJS(config.selector, { text: config.text, colors: [config.color], trackCursorOnlyInsideCanvas: true});
// Add visual feedbackconst canvas = document.querySelector(config.selector);canvas.style.border = `3px solid ${config.color}`;canvas.style.borderRadius = '8px';canvas.style.cursor = 'crosshair';
// Hover effectscanvas.addEventListener('mouseenter', () => { canvas.style.boxShadow = `0 0 20px ${config.color}66`;});
canvas.addEventListener('mouseleave', () => { canvas.style.boxShadow = 'none';});
return instance;});
// Users can clearly see which canvas is which// and which one they're interacting withTroubleshooting
Section titled “Troubleshooting”Performance Issues
Section titled “Performance Issues”// Issue: Too many particles causing lagconsole.log('Total particles:',instances.reduce((sum, instance) => sum + instance.particleList.length, 0));
// Solution 1: Reduce per-instance countinstances.forEach(instance => {console.log('Instance has', instance.particleList.length, 'particles');// If too many, reinitialize with lower maxParticles});
// Solution 2: Stop non-visible instancesconst observer = new IntersectionObserver((entries) => {entries.forEach(entry => { const instance = instanceMap.get(entry.target.id); if (entry.isIntersecting) { instance.startAnimation(); } else { instance.destroy(); }});});
// Solution 3: Use restricted tracking// Ensure trackCursorOnlyInsideCanvas: true for all instancesInstances Interfering with Each Other
Section titled “Instances Interfering with Each Other”// Issue: Canvases reacting when cursor is elsewhere
// Check cursor tracking settinginstances.forEach((instance, i) => {console.log(`Instance ${i} trackCursorOnlyInsideCanvas:`, instance.trackCursorOnlyInsideCanvas);});
// Solution: Enable restricted trackinginstances.forEach(instance => {instance.destroy();});
instances = [initParticleJS('#canvas1', { text: 'ONE', colors: ['#FF6B6B'], trackCursorOnlyInsideCanvas: true // Fix!}),initParticleJS('#canvas2', { text: 'TWO', colors: ['#4ECDC4'], trackCursorOnlyInsideCanvas: true // Fix!})];Memory Leaks
Section titled “Memory Leaks”// Issue: Instances not being cleaned up
// Check for lingering instancesconsole.log('Active instances:', instances.length);instances.forEach((instance, i) => {console.log(`Instance ${i} isAnimating:`, instance.isAnimating);});
// Solution: Proper cleanupfunction cleanupAll() {instances.forEach(instance => { if (instance.isAnimating) { instance.destroy(); }});instances.length = 0; // Clear array}
// Add cleanup listenerswindow.addEventListener('beforeunload', cleanupAll);
// For SPA navigationif (typeof router !== 'undefined') {router.beforeEach((to, from, next) => { cleanupAll(); next();});}Initialization Failures
Section titled “Initialization Failures”// Issue: Some instances fail to initialize
// Check which instances failedconst results = configs.map(config => {try { const instance = initParticleJS(config.selector, config.options); return { success: true, selector: config.selector, instance };} catch (error) { console.error(`Failed to initialize ${config.selector}:`, error); return { success: false, selector: config.selector, error };}});
// Filter successful instancesconst instances = results.filter(result => result.success).map(result => result.instance);
console.log(`Initialized ${instances.length} of ${configs.length} instances`);
// Report failuresconst failures = results.filter(result => !result.success);if (failures.length > 0) {console.warn('Failed to initialize:', failures);}Related Documentation
Section titled “Related Documentation”- Performance Optimization - Optimize performance across multiple instances
- Max Particles - Control particle count per instance
- Restricted Cursor Tracking - Limit cursor interaction zones
- Manual Animation Control - Start/stop instances programmatically
- Error Handling - Handle initialization failures
- Slow Browser Detection - Automatic performance adaptation