UNPKG

@omnedia/ngx-particles

Version:

A simple component library to create a container with an animated background.

271 lines (266 loc) 11.4 kB
import { isPlatformBrowser, CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { signal, PLATFORM_ID, HostListener, Input, ViewChild, Inject, ChangeDetectionStrategy, Component } from '@angular/core'; class NgxParticlesComponent { platformId; canvasRef; wrapperRef; quantity = 100; size = 0.4; color = "#9c9c9c"; staticity = 50; ease = 50; particleSpeed = 1; vx = 0; vy = 0; mousePosition = { x: 0, y: 0 }; mouse = { x: 0, y: 0 }; circles = []; onClick(event) { this.mousePosition = { x: event.clientX, y: event.clientY, }; this.onMouseMove(); } isInView = signal(false); isAnimating = signal(false); animationFrameId; intersectionObserver; constructor(platformId) { this.platformId = platformId; } ngAfterViewInit() { this.setCanvasSize(); this.animate(); if (isPlatformBrowser(this.platformId)) { this.intersectionObserver = new IntersectionObserver(([entry]) => { this.renderContents(entry.isIntersecting); }); this.intersectionObserver.observe(this.canvasRef.nativeElement); } window.addEventListener("resize", () => this.setCanvasSize()); } ngOnDestroy() { window.removeEventListener("resize", () => this.setCanvasSize()); if (this.intersectionObserver) { this.intersectionObserver.disconnect(); } if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } } renderContents(isIntersecting) { if (isIntersecting && !this.isInView()) { this.isInView.set(true); if (!this.isAnimating()) { this.animationFrameId = requestAnimationFrame(() => this.animate()); } } else if (!isIntersecting) { this.isInView.set(false); } } setCanvasSize() { this.canvasRef.nativeElement.width = this.wrapperRef.nativeElement.getBoundingClientRect().width; this.canvasRef.nativeElement.height = this.wrapperRef.nativeElement.getBoundingClientRect().height; this.circles = []; this.drawParticles(); } drawParticles() { this.clearContext(); for (let i = 0; i < this.quantity; i++) { const circle = this.circleParams(); this.drawCircle(circle); } } circleParams() { const x = Math.floor(Math.random() * this.canvasRef.nativeElement.width); const y = Math.floor(Math.random() * this.canvasRef.nativeElement.height); const translateX = 0; const translateY = 0; const pSize = Math.floor(Math.random() * 2) + this.size; const alpha = 0; const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); const dx = (Math.random() - 0.5) * 0.1; const dy = (Math.random() - 0.5) * 0.1; const magnetism = 0.1 + Math.random() * 4; return { x, y, translateX, translateY, size: pSize, alpha, targetAlpha, dx, dy, magnetism, }; } drawCircle(circle, update = false) { const context = this.canvasRef.nativeElement.getContext("2d"); if (!context) { return; } const { x, y, translateX, translateY, size, alpha } = circle; const rgb = this.hexToRgb(this.color); context.translate(translateX, translateY); context.beginPath(); context.arc(x, y, size, 0, 2 * Math.PI); context.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; context.fill(); context.setTransform(1, 0, 0, 1, 0, 0); if (!update) { this.circles.push(circle); } } animate() { if (!this.isInView()) { this.isAnimating.set(false); return; } this.isAnimating.set(true); this.clearContext(); this.circles.forEach((circle, i) => { // Handle the alpha value const edge = [ circle.x + circle.translateX - circle.size, // distance from left edge this.canvasRef.nativeElement.width - circle.x - circle.translateX - circle.size, // distance from right edge circle.y + circle.translateY - circle.size, // distance from top edge this.canvasRef.nativeElement.height - circle.y - circle.translateY - circle.size, // distance from bottom edge ]; const closestEdge = edge.reduce((a, b) => Math.min(a, b)); const remapClosestEdge = parseFloat(this.remapValue(closestEdge, 0, 20, 0, 1).toFixed(2)); if (remapClosestEdge > 1) { circle.alpha += 0.02; if (circle.alpha > circle.targetAlpha) { circle.alpha = circle.targetAlpha; } } else { circle.alpha = circle.targetAlpha * remapClosestEdge; } circle.x += (circle.dx + this.vx) * this.particleSpeed; circle.y += (circle.dy + this.vy) * this.particleSpeed; circle.translateX += (this.mouse.x / (this.staticity / circle.magnetism) - circle.translateX) / this.ease; circle.translateY += (this.mouse.y / (this.staticity / circle.magnetism) - circle.translateY) / this.ease; this.drawCircle(circle, true); // circle gets out of the canvas if (circle.x < -circle.size || circle.x > this.canvasRef.nativeElement.width + circle.size || circle.y < -circle.size || circle.y > this.canvasRef.nativeElement.height + circle.size) { // remove the circle from the array this.circles.splice(i, 1); // create a new circle const newCircle = this.circleParams(); this.drawCircle(newCircle); // update the circle position } }); this.animationFrameId = requestAnimationFrame(() => this.animate()); } clearContext() { const context = this.canvasRef.nativeElement.getContext("2d"); if (!context) { return; } context.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height); } onMouseMove() { const rect = this.canvasRef.nativeElement.getBoundingClientRect(); const { w, h } = { w: this.canvasRef.nativeElement.width, h: this.canvasRef.nativeElement.height, }; const x = this.mousePosition.x - rect.left - w / 2; const y = this.mousePosition.y - rect.top - h / 2; const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; if (inside) { this.mouse = { x: x, y: y }; } } remapValue(value, start1, end1, start2, end2) { const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; return remapped > 0 ? remapped : 0; } hexToRgb(hex) { hex = hex.replace("#", ""); if (hex.length === 3) { hex = hex .split("") .map((char) => char + char) .join(""); } const hexInt = parseInt(hex, 16); const red = (hexInt >> 16) & 255; const green = (hexInt >> 8) & 255; const blue = hexInt & 255; return [red, green, blue]; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgxParticlesComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.2", type: NgxParticlesComponent, isStandalone: true, selector: "om-particles", inputs: { quantity: "quantity", size: "size", color: ["circleColor", "color"], staticity: "staticity", ease: "ease", particleSpeed: "particleSpeed", vx: "vx", vy: "vy" }, host: { listeners: { "mousemove": "onClick($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["OmParticlesCanvas"], descendants: true }, { propertyName: "wrapperRef", first: true, predicate: ["OmParticlesWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"om-particles\" #OmParticlesWrapper>\r\n <div class=\"om-particles-background\">\r\n <canvas #OmParticlesCanvas></canvas>\r\n </div>\r\n\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".om-particles{position:relative;width:100%;height:100%}.om-particles .om-particles-background{position:absolute;width:100%;height:100%}.om-particles .om-particles-background canvas{width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgxParticlesComponent, decorators: [{ type: Component, args: [{ selector: "om-particles", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"om-particles\" #OmParticlesWrapper>\r\n <div class=\"om-particles-background\">\r\n <canvas #OmParticlesCanvas></canvas>\r\n </div>\r\n\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".om-particles{position:relative;width:100%;height:100%}.om-particles .om-particles-background{position:absolute;width:100%;height:100%}.om-particles .om-particles-background canvas{width:100%;height:100%}\n"] }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }], propDecorators: { canvasRef: [{ type: ViewChild, args: ["OmParticlesCanvas"] }], wrapperRef: [{ type: ViewChild, args: ["OmParticlesWrapper"] }], quantity: [{ type: Input, args: ["quantity"] }], size: [{ type: Input, args: ["size"] }], color: [{ type: Input, args: ["circleColor"] }], staticity: [{ type: Input, args: ["staticity"] }], ease: [{ type: Input, args: ["ease"] }], particleSpeed: [{ type: Input, args: ["particleSpeed"] }], vx: [{ type: Input, args: ["vx"] }], vy: [{ type: Input, args: ["vy"] }], onClick: [{ type: HostListener, args: ["mousemove", ["$event"]] }] } }); /* * Public API Surface of ngx-particles */ /** * Generated bundle index. Do not edit. */ export { NgxParticlesComponent }; //# sourceMappingURL=omnedia-ngx-particles.mjs.map