import { Directive, ElementRef, Input, OnDestroy, HostListener, OnInit } from "@angular/core"; /* Variables to be used outside of directive scope To improve performance. */ 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.initVariables(); } ngOnInit() { this.setCanvasSize(); 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; } } }