How to import and use particles.js in an Angular/Angular2/Angular4 app
Using the original package, without the https://github.com/audrenbdb/angular-particlesjs you can go as follows:
Install it using npm i particles.js
app.component.html
<div id="particles-js"></div>
app.component.ts
import { Component, OnInit } from '@angular/core';
import { ParticlesConfig } from './particles-config';
declare let particlesJS: any; // Required to be properly interpreted by TypeScript.
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public ngOnInit(): void {
this.invokeParticles();
}
public invokeParticles(): void {
particlesJS('particles-js', ParticlesConfig, function() {});
}
}
particles-config.ts
export const ParticlesConfig = {
particles: {
number: {
value: 70,
density: {
enable: true,
value_area: 1400
}
},
color: {
value: '#283593'
},
shape: {
type: 'polygon',
stroke: {
width: 1,
color: '#283593'
},
polygon: {
nb_sides: 6
}
},
opacity: {
value: 1,
random: true,
anim: {
enable: true,
speed: 0.8,
opacity_min: 0.25,
sync: true
}
},
size: {
value: 2,
random: true,
anim: {
enable: true,
speed: 10,
size_min: 1.25,
sync: true
}
},
line_linked: {
enable: true,
distance: 150,
color: '#283593',
opacity: 1,
width: 1
},
move: {
enable: true,
speed: 8,
direction: 'none',
random: true,
straight: false,
out_mode: 'out',
bounce: true,
attract: {
enable: true,
rotateX: 2000,
rotateY: 2000
}
}
},
interactivity: {
detect_on: 'canvas',
events: {
onhover: {
enable: true,
mode: 'grab'
},
onclick: {
enable: true,
mode: 'repulse'
},
resize: true
},
modes: {
grab: {
distance: 200,
line_linked: {
opacity: 3
}
},
repulse: {
distance: 250,
duration: 2
}
}
},
retina_detect: true
};
app.component.scss (optional, to show it as full height)
#particles-js {
height: 100vh;
}
angular.json
"scripts": ["node_modules/particles.js/particles.js"]
Here is how to do that:
Just import the particles.js in your index.html (cdn or local)
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
Put in the div anchor into your component template (you could also put it to index.html or somewhere else)
<div id="particles-js"></div>
Make the package visible by adding a simple type definition (in your component or in the typings.d.ts)
declare var particlesJS: any;
Initialize it in ngOnInit (or somewhere else)
particlesJS.load('particles-js', 'particles.json', null);
I have made a little plunker example: http://plnkr.co/edit/GLRvYgNPJue4KqdMuAJB?p=preview
Update 22/11/2019
I have ported an angular version of particles.js into a directive. Inside the div you want to cover with particles, add a canvas element with repulse-particles
. This is a very trimmed version of the library, but there are still quite a few options.
Performance wise, this directive is an upgrade over the original library with the use of a quadtree and several big optimizations. You can now have fun with 1k particles ~
Another tweak is the support of touch mobile device.
See example in the repo or in the stackblitz demo below.
Edit to your likings :)
Repo & stackblitz editor
https://github.com/audrenbdb/angular-particlesjs
https://stackblitz.com/edit/angular-dt2cjg
Demo
https://audrenbdb.github.io/particles/angular/index.html
Full directive code
import { Directive, ElementRef, Input, OnDestroy, HostListener, OnInit } from "@angular/core";
/*
Variables set outside of directive scope
To improve performances.
*/
const TAU: number = Math.PI * 2;
const QUADTREE_CAPACITY: number = 4;
let linkBatches: number = 10;
let mouse: {x: number,y: number} = {x: 0, y: 0};
/*
Variables to be initiated
*/
let linkDistance: number;
let linkDistance2: number;
let repulseDistance: number;
let particleSpeed: number;
let particleSize: number;
let bounce: boolean;
let quadTree: QuadTree;
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
@Directive({
selector: "[repulse-particles]"
})
export class ParticlesDirective implements OnDestroy, OnInit {
@Input() number: number = 80;
@Input() speed: number = 6;
@Input() linkWidth: number = .5;
@Input() linkDistance: number = 140;
@Input() size: number = 3;
@Input() repulseDistance: number = 140;
@Input() particleHex: string = "#FFF";
@Input() linkHex: string = "#FFF";
@Input() bounce: boolean = true;
@Input() densityArea: number = 800;
particlesNumber: number;
particlesList: Particle[] = [];
links: Link[][] = [];
linkBatchAlphas: number[] = [];
linkPool: Link[] = [];
candidates: Particle[] = [];
boundary: Bounds;
animationFrame;
constructor(
public el: ElementRef,
) {
canvas = this.el.nativeElement;
canvas.style.height = "100%";
canvas.style.width = "100%";
ctx = canvas.getContext("2d");
for (var i = 1/(linkBatches + 1); i < 1; i += 1/(linkBatches + 1)) {
this.links.push([]);
this.linkBatchAlphas.push(i);
}
this.setCanvasSize();
this.initVariables();
}
ngOnInit() {
this.animate();
}
@HostListener("window:resize") onResize() {
this.setCanvasSize();
}
@HostListener("mouseleave") onMouseLeave() {
this.stopMouse()
}
@HostListener("touchend") onTouchEnd() {
this.stopMouse()
}
@HostListener("mousemove", ["$event"]) onMouseMove(e) {
this.setMousePos(e.offsetX, e.offsetY);
}
@HostListener("touchmove", ["$event"]) onTouchMove(e) {
this.setMousePos(e.touches[0].clientX, e.touches[0].clientY);
}
@HostListener("change") ngOnChanges() {
this.initVariables();
this.resetParticles();
}
setMousePos(x, y) {
mouse.x = x;
mouse.y = y;
}
stopMouse() {
mouse.x = null;
}
initVariables() {
linkDistance = this.linkDistance;
linkDistance2 = (0.7 * linkDistance) ** 2;
repulseDistance = this.repulseDistance;
particleSpeed = this.speed;
particleSize = this.size;
bounce = this.bounce;
if (this.densityArea) this.scaleDensity();
}
animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.updateParticles();
this.updateLinks();
this.animationFrame = requestAnimationFrame(this.animate.bind(this));
}
updateParticles() {
quadTree.close();
ctx.fillStyle = this.particleHex;
ctx.beginPath();
for (const p of this.particlesList) p.update(ctx, true);
ctx.fill();
}
updateLinks() {
let i: number;
let link: Link;
let alphaIdx = 0;
for (const p1 of this.particlesList) {
p1.explored = true;
const count = quadTree.query(p1, 0, this.candidates);
for (i = 0; i < count; i++) {
const p2 = this.candidates[i];
if (!p2.explored) {
link = this.linkPool.length ? this.linkPool.pop() : new Link();
link.init(p1, p2);
this.links[link.batchId].push(link);
}
}
}
ctx.lineWidth = this.linkWidth;
ctx.strokeStyle = this.linkHex;
for (const l of this.links) {
ctx.globalAlpha = this.linkBatchAlphas[alphaIdx++];
ctx.beginPath();
while (l.length) this.linkPool.push(l.pop().addPath(ctx));
ctx.stroke();
}
ctx.globalAlpha = 1;
}
resetParticles() {
this.particlesList = [];
for (let i = 0; i < this.particlesNumber; i++) {
this.particlesList.push(new Particle(canvas, particleSize))
}
quadTree = new QuadTree();
for (const p of this.particlesList) p.reset(canvas);
}
scaleDensity() {
var area = canvas.width * canvas.height / 1000;
this.particlesNumber = (area * this.number / this.densityArea) | 0;
}
setCanvasSize() {
canvas.height = canvas.offsetHeight;
canvas.width = canvas.offsetWidth;
if (this.densityArea) this.scaleDensity();
this.resetParticles();
}
ngOnDestroy(): void {
cancelAnimationFrame(this.animationFrame);
}
}
class Link {
p1: Particle;
p2: Particle;
alpha: number;
batchId: number;
constructor() { }
init(p1: Particle, p2: Particle) {
this.p1 = p1;
this.p2 = p2;
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
this.alpha = 1 - (dx * dx + dy * dy) / linkDistance2;
this.batchId = this.alpha * linkBatches | 0;
this.batchId = this.batchId >= linkBatches ? linkBatches : this.batchId;
}
addPath(ctx) {
ctx.moveTo(this.p1.x, this.p1.y);
ctx.lineTo(this.p2.x, this.p2.y);
return this;
}
}
class Particle {
r: number;
speedScale: number;
x: number;
y: number;
vx: number;
vy: number;
quad: QuadTree;
explored: boolean;
constructor (canvas, r) {
this.r = r;
this.speedScale = particleSpeed / 2;
this.reset(canvas, r);
}
reset(canvas, r = this.r) {
const W = canvas.width - r * 2;
const H = canvas.height - r * 2;
this.x = Math.random() * W + r;
this.y = Math.random() * H + r;
this.vx = Math.random() - 0.5;
this.vy = Math.random() - 0.5;
this.quad = undefined;
this.explored = false;
}
addPath(ctx) {
ctx.moveTo(this.x + this.r, this.y);
ctx.arc(this.x, this.y, this.r, 0, TAU);
}
near(p) {
return ((p.x - this.x) ** 2 + (p.y - this.y) ** 2) <= linkDistance2;
}
intersects(range) {
const xd = Math.abs(range.x - this.x);
const yd = Math.abs(range.y - this.y);
const r = linkDistance;
const w = range.w;
const h = range.h;
if (xd > r + w || yd > r + h) { return false }
if (xd <= w || yd <= h) { return true }
return ((xd - w) ** 2 + (yd - h) ** 2) <= linkDistance2;
}
update(ctx, repulse = true) {
this.explored = false;
const r = this.r;
let W, H;
this.x += this.vx * this.speedScale;
this.y += this.vy * this.speedScale;
if (bounce) {
W = ctx.canvas.width - r;
H = ctx.canvas.height - r;
if (this.x > W || this.x < 0) {
this.vx = -this.vx;
}
if (this.y > H || this.y < 0) {
this.vy = -this.vy;
}
} else {
W = ctx.canvas.width + r;
H = ctx.canvas.height + r;
if (this.x > W) {
this.x = 0;
this.y = Math.random() * (H - r);
} else if (this.x < -r) {
this.x = W - r;
this.y = Math.random() * (H - r);
}
if (this.y > H) {
this.y = 0
this.x = Math.random() * (W - r);
} else if (this.y < -r) {
this.y = H - r;
this.x = Math.random() * (W - r);
}
}
repulse && mouse.x && this.repulse();
this.addPath(ctx);
quadTree.insert(this);
this.quad && (this.quad.drawn = false)
}
repulse() {
var dx = this.x - mouse.x;
var dy = this.y - mouse.y;
const dist = (dx * dx + dy * dy) ** 0.5;
var rf = ((1 - (dist / repulseDistance) ** 2) * 100);
rf = (rf < 0 ? 0 : rf > 50 ? 50 : rf) / dist;
var posX = this.x + dx * rf;
var posY = this.y + dy * rf;
if (bounce) {
if (posX - particleSize > 0 && posX + particleSize < canvas.width) this.x = posX;
if (posY - particleSize > 0 && posY + particleSize < canvas.height) this.y = posY;
} else {
this.x = posX;
this.y = posY;
}
}
}
class Bounds {
x: number;
y: number;
w: number;
h: number;
left: number;
right: number;
top: number;
bottom: number;
diagonal: number;
constructor(x, y, w, h) { this.init(x, y, w, h) }
init(x,y,w,h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.left = x - w;
this.right = x + w;
this.top = y - h;
this.bottom = y + h;
this.diagonal = (w * w + h * h);
}
contains(p) {
return (p.x >= this.left && p.x <= this.right && p.y >= this.top && p.y <= this.bottom);
}
near(p) {
if (!this.contains(p)) {
const dx = p.x - this.x;
const dy = p.y - this.y;
const dist = (dx * dx + dy * dy) - this.diagonal - linkDistance2;
return dist < 0;
}
return true;
}
}
class QuadTree {
boundary: Bounds;
divided: boolean;
points: Particle[];
pointCount: number;
drawn: boolean;
depth: number;
NE: QuadTree;
NW: QuadTree;
SE: QuadTree;
SW: QuadTree;
constructor(boundary: Bounds = new Bounds(canvas.width / 2,canvas.height / 2,canvas.width / 2 ,canvas.height / 2), depth = 0) {
this.boundary = boundary;
this.divided = false;
this.points = depth > 1 ? [] : null;
this.pointCount = 0
this.drawn = false;
this.depth = depth;
if(depth === 0) { // BM67 Fix on resize
this.subdivide();
this.NE.subdivide();
this.NW.subdivide();
this.SE.subdivide();
this.SW.subdivide();
}
}
addPath() {
const b = this.boundary;
ctx.rect(b.left, b.top, b.w * 2, b.h * 2);
this.drawn = true;
}
addToSubQuad(particle) {
if (this.NE.insert(particle)) { return true }
if (this.NW.insert(particle)) { return true }
if (this.SE.insert(particle)) { return true }
if (this.SW.insert(particle)) { return true }
particle.quad = undefined;
}
insert(particle) {
if (this.depth > 0 && !this.boundary.contains(particle)) { return false }
if (this.depth > 1 && this.pointCount < QUADTREE_CAPACITY) {
this.points[this.pointCount++] = particle;
particle.quad = this;
return true;
}
if (!this.divided) { this.subdivide() }
return this.addToSubQuad(particle);
}
subdivide() {
if (!this.NW) {
const x = this.boundary.x;
const y = this.boundary.y;
const w = this.boundary.w / 2;
const h = this.boundary.h / 2;
const depth = this.depth + 1;
this.NE = new QuadTree(new Bounds(x + w, y - h, w, h), depth);
this.NW = new QuadTree(new Bounds(x - w, y - h, w, h), depth);
this.SE = new QuadTree(new Bounds(x + w, y + h, w, h), depth);
this.SW = new QuadTree(new Bounds(x - w, y + h, w, h), depth);
} else {
this.NE.pointCount = 0;
this.NW.pointCount = 0;
this.SE.pointCount = 0;
this.SW.pointCount = 0;
}
this.divided = true;
}
query(part, fc, found) {
var i = this.pointCount;
if (this.depth === 0 || this.boundary.near(part)) {
if (this.depth > 1) {
while (i--) {
const p = this.points[i];
if (!p.explored && part.near(p)) { found[fc++] = p }
}
if (this.divided) {
fc = this.NE.pointCount ? this.NE.query(part, fc, found) : fc;
fc = this.NW.pointCount ? this.NW.query(part, fc, found) : fc;
fc = this.SE.pointCount ? this.SE.query(part, fc, found) : fc;
fc = this.SW.pointCount ? this.SW.query(part, fc, found) : fc;
}
} else if(this.divided) {
fc = this.NE.query(part, fc, found);
fc = this.NW.query(part, fc, found);
fc = this.SE.query(part, fc, found);
fc = this.SW.query(part, fc, found);
}
}
return fc;
}
close() {
if (this.divided) {
this.NE.close();
this.NW.close();
this.SE.close();
this.SW.close();
}
if (this.depth === 2 && this.divided) {
this.NE.pointCount = 0;
this.NW.pointCount = 0;
this.SE.pointCount = 0;
this.SW.pointCount = 0;
} else if (this.depth > 2) {
this.divided = false;
}
}
}