import {afterNextRender, Component, ElementRef, NgZone, OnDestroy, ViewChild} from '@angular/core'; import {Dot} from '../../models/dot'; import {DeviceDetectionService} from '../../service/device-detection-service'; @Component({ selector: 'app-dot-background', imports: [], templateUrl: './dot-background.html', styleUrl: './dot-background.scss', }) export class DotBackground implements OnDestroy { @ViewChild('canvas') canvasRef!: ElementRef; private ctx!: CanvasRenderingContext2D; private dots: Dot[] = []; private mouse = { x: -1000, y: -1000 }; private animationId = 0; private initialized = false; private readonly INIT_DOT_COUNT = 12; private readonly MAX_DOT_COUNT = 100; private readonly MAX_DOT_COUNT_MOBILE = 40; private readonly COLORS = ['#6366f1', '#8b5cf6', '#a855f7', '#3b82f6']; private readonly MOUSE_RADIUS = 150; private ballSpawnId = 0; private ballSpawnNextColor = 0; constructor(private ngZone: NgZone, private mobileService: DeviceDetectionService) { afterNextRender(() => { this.init(); }); } ngOnDestroy() { if (!this.initialized) return; cancelAnimationFrame(this.animationId); window.removeEventListener('resize', this.resize); window.removeEventListener('mousemove', this.onMouseMove); } private init() { const canvas = this.canvasRef.nativeElement; this.ctx = canvas.getContext('2d')!; this.resize(); this.initDots(); window.addEventListener('resize', this.resize); window.addEventListener('mousemove', this.onMouseMove); window.addEventListener('click', this.onMouseClick); this.initialized = true; this.ngZone.runOutsideAngular(() => this.animate()); } private resize = () => { const canvas = this.canvasRef.nativeElement; const width = window.innerWidth; const height = window.innerHeight; const dx = Math.abs(width - canvas.width) / width; const dy = Math.abs(height - canvas.height) / height; if (!this.mobileService.mobileCheck() || dy > 0.2 || dx > 0.05) { //sync canvas size to screen size canvas.width = width; canvas.height = height; for (const dot of this.dots) { dot.x = Math.max(dot.radius, Math.min(width - dot.radius, dot.x)); dot.y = Math.max(dot.radius, Math.min(height - dot.radius, dot.y)); } } }; private onMouseMove = (e: MouseEvent) => { const canvas = this.canvasRef.nativeElement; // map real res to canvas res this.mouse.x = e.clientX / window.innerWidth * canvas.width; this.mouse.y = e.clientY / window.innerHeight * canvas.height; }; private onMouseClick = () => { const dot = this.spawnDot(); dot.x = this.mouse.x; dot.y = this.mouse.y; }; private spawnDot(): Dot { const dotId = this.ballSpawnId++; const max_count = this.mobileService.mobileCheck() ? this.MAX_DOT_COUNT_MOBILE : this.MAX_DOT_COUNT; let dot; if (dotId < max_count) { dot = { x: 0, y: 0, vx: 0, vy: 0, radius: 1, color: "#000000", }; this.dots.push(dot); } else { dot = this.dots[dotId % this.dots.length]; } this.populateDot(dot); return dot; } private populateDot(dot: Dot) { const {width, height} = this.canvasRef.nativeElement; dot.x = Math.random() * width; dot.y = Math.random() * height; dot.vx = (Math.random() - 0.5) * 0.5; dot.vy = (Math.random() - 0.5) * 0.5; dot.radius = Math.random() * 60 + 40; dot.color = this.COLORS[this.ballSpawnNextColor++ % this.COLORS.length]; } private initDots() { for (let i = 0; i < this.INIT_DOT_COUNT; i++) { this.spawnDot(); } } private animate = () => { const canvas = this.canvasRef.nativeElement; const { width, height } = canvas; this.ctx.clearRect(0, 0, width, height); for (const dot of this.dots) { const dx = dot.x - this.mouse.x; const dy = dot.y - this.mouse.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist != 0 && dist < this.MOUSE_RADIUS) { const force = (this.MOUSE_RADIUS - dist) / this.MOUSE_RADIUS; dot.vx += (dx / dist) * force * 0.5; dot.vy += (dy / dist) * force * 0.5; } dot.x += dot.vx; dot.y += dot.vy; const speed = Math.sqrt(dot.vx * dot.vx + dot.vy * dot.vy); if (speed > 0.3) { dot.vx *= 0.98; dot.vy *= 0.98; } else if (speed <= 0.3) { dot.vx *= 1.02; dot.vy *= 1.02; } else if (speed === 0) { dot.vx = Math.random() * 0.2 - 0.1; dot.vy = Math.random() * 0.2 - 0.1; } // Bounce off edges (accounting for radius) if (dot.x < dot.radius || dot.x > width - dot.radius) dot.vx *= -1; if (dot.y < dot.radius || dot.y > height - dot.radius) dot.vy *= -1; // Clamp to bounds dot.x = Math.max(dot.radius, Math.min(width - dot.radius, dot.x)); dot.y = Math.max(dot.radius, Math.min(height - dot.radius, dot.y)); const gradient = this.ctx.createRadialGradient( dot.x, dot.y, 0, dot.x, dot.y, dot.radius ); gradient.addColorStop(0, dot.color + '40'); gradient.addColorStop(1, 'transparent'); this.ctx.beginPath(); this.ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2); this.ctx.fillStyle = gradient; this.ctx.fill(); } this.animationId = requestAnimationFrame(this.animate); }; }