14

I have an Angular app that I want to use particles.js in however I have no clue how to add it and get it working.

I've added it to the .angular-cli.json

  "scripts": [
    "../node_modules/particles.js/particles.js"
  ],

And I've imported it into my component

import * as  particlesJS from 'particles.js';

And attempted to initialize it using

  particlesJS.load('particles-js', 'assets/particles.json', function() {
    console.log('callback - particles.js config loaded');
  });

Has anyone got this working?

Steve Fitzsimons
  • 3,754
  • 7
  • 27
  • 66
  • Here is a description on how to utilize external javascript libraries in Angular2+ [link](https://stackoverflow.com/questions/44170294/including-external-hosted-javascript-files-in-angular2) – Ludwig May 31 '17 at 20:13
  • This works OK with the likes of JQuery but I cant get it to work with Particles.js – Steve Fitzsimons Jun 01 '17 at 08:19

5 Answers5

13

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"]
Thanh-Quy Nguyen
  • 2,995
  • 7
  • 30
  • 46
Daniel Danielecki
  • 8,508
  • 6
  • 68
  • 94
12

Here is how to do that:

  1. 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>
    
  2. 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>
    
  3. Make the package visible by adding a simple type definition (in your component or in the typings.d.ts)

    declare var particlesJS: any;
    
  4. 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

Ludwig
  • 1,242
  • 10
  • 8
  • 1
    This worked a treat thanks. Followed it step by step and couldn't figure out why it wasn't working, took a while for it to click that the particles where white on a white background in my app – Steve Fitzsimons Jun 01 '17 at 18:30
  • Why is declaring it important ? – frunkad Oct 24 '18 at 15:21
  • @frunkad If you don't have typings then you need to declare. Otherwise the transpiler claims about unknown things. – Ludwig Oct 24 '18 at 16:47
9

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;
      }
  }
}
Ado Ren
  • 3,511
  • 4
  • 21
  • 36
  • Hi Ado, did you make an npm package for this or do you recommend that we just clone your repo? Thanks! – Thanh-Quy Nguyen Nov 15 '19 at 12:02
  • 1
    Hey Thanh, updated the repo to used an optimized directive over the previous standalone component. No need to make a package just copy paste file content into a new directive. See repo for steps. – Ado Ren Nov 22 '19 at 21:33
  • Hi there, My particles don't appear and then when I right click and inspect in chrome they suddenly appear - any idea what that could be? – Timothy Louw Mar 03 '20 at 10:49
  • Turns out it was the HostListener("window:resize") that was triggering the setCanvasSize() function when I inspected in chrome - I moved the setCanvasSize call in the constructor to ngOninit and now the canvas size is set without the window needing to be resized. I'm on angular 9 so I'm not sure if that has something to do with it. – Timothy Louw Mar 03 '20 at 12:09
2

Just want to add a comment to the Ado Ren's solution ( I'm not able to comment on his solution because I have not enough reputation)

It works well on Angular 8 but with little changes

Change 28th row in particles.component.ts file :

From :  @ViewChild('particles') particlesCanvas: ElementRef;
To   :  @ViewChild('particles', {static: true}) particlesCanvas: ElementRef;

Without this little change the error is thrown in angular 8

ERROR in app/particles/particles.component.ts(28,4): error TS2554: Expected 2 arguments, but got 1.

Also, you should change the background color of particles component from the white to something else. Otherwise, you won't see them as they are white too.

Vic VKh
  • 169
  • 1
  • 2
  • 11
  • 1
    updated repo to now use a directive and is optimized. Works on angular 8+ :) Thanks for your comment – Ado Ren Nov 22 '19 at 21:35
-3

I was getting the error:

Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them.

Solution: Go to the particle.js file and modify it:


    -//Object.deepExtend = function (destination, source) {
    +Object.deepExtend = function deepExtendFunction(destination, source) {
      for (var property in source) {
        if (source[property] && source[property].constructor &&
         source[property].constructor === Object) {
          destination[property] = destination[property] || {}; 
    -       //arguments.callee(destination[property], source[property]);
    +      deepExtendFunction(destination[property], source[property]);
        } else {
          destination[property] = source[property];
        }
      }
      return destination;
    };

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
  • This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](https://stackoverflow.com/questions/ask). To get notified when this question gets new answers, you can [follow this question](https://meta.stackexchange.com/q/345661). Once you have enough [reputation](https://stackoverflow.com/help/whats-reputation), you can also [add a bounty](https://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question. - [From Review](/review/low-quality-posts/29433255) – Stephan Vierkant Jul 25 '21 at 19:02
  • @StephanVierkant: Why not? It seems to expand on the problem by offering an error message, but then goes on o offer a solution. – Jeremy Caney Jul 26 '21 at 01:03