@omnedia/ngx-particles
Version:
A simple component library to create a container with an animated background.
271 lines (266 loc) • 11.5 kB
JavaScript
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, ...(ngDevMode ? [{ debugName: "isInView" }] : []));
isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
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: "21.0.0", ngImport: i0, type: NgxParticlesComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.0", 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: "21.0.0", 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