From 7f7ff008e2cd1110429a4999788ffb49ccd81d6d Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Wed, 21 Jan 2026 07:28:26 +0100 Subject: [PATCH] added background --- src/app/app.html | 350 +----------------- src/app/app.routes.ts | 5 +- src/app/app.scss | 12 + src/app/app.ts | 3 +- .../dot-background/dot-background.html | 1 + .../dot-background/dot-background.scss | 9 + .../dot-background/dot-background.spec.ts | 23 ++ .../dot-background/dot-background.ts | 131 +++++++ src/app/models/dot.ts | 8 + src/app/models/skill-category.ts | 8 + src/app/models/skill.ts | 5 + 11 files changed, 213 insertions(+), 342 deletions(-) create mode 100644 src/app/components/dot-background/dot-background.html create mode 100644 src/app/components/dot-background/dot-background.scss create mode 100644 src/app/components/dot-background/dot-background.spec.ts create mode 100644 src/app/components/dot-background/dot-background.ts create mode 100644 src/app/models/dot.ts create mode 100644 src/app/models/skill-category.ts create mode 100644 src/app/models/skill.ts diff --git a/src/app/app.html b/src/app/app.html index e0118a1..6f150cc 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,342 +1,12 @@ - - - - - - - - + +
+
- - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - + +
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..ce27a40 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,6 @@ import { Routes } from '@angular/router'; +import {Skills} from './components/pages/skills/skills'; -export const routes: Routes = []; +export const routes: Routes = [ + {path: '', component: Skills} +]; diff --git a/src/app/app.scss b/src/app/app.scss index e69de29..5ca46fc 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -0,0 +1,12 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + background: #0a0a0f; + color: #fff; + -webkit-font-smoothing: antialiased; +} diff --git a/src/app/app.ts b/src/app/app.ts index dc855a1..1dd31ff 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,9 +1,10 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import {DotBackground} from './components/dot-background/dot-background'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, DotBackground], templateUrl: './app.html', styleUrl: './app.scss' }) diff --git a/src/app/components/dot-background/dot-background.html b/src/app/components/dot-background/dot-background.html new file mode 100644 index 0000000..c2e2ad0 --- /dev/null +++ b/src/app/components/dot-background/dot-background.html @@ -0,0 +1 @@ + diff --git a/src/app/components/dot-background/dot-background.scss b/src/app/components/dot-background/dot-background.scss new file mode 100644 index 0000000..8401c4b --- /dev/null +++ b/src/app/components/dot-background/dot-background.scss @@ -0,0 +1,9 @@ +canvas { + position: fixed; + inset: 0; + z-index: -1; + background: #0a0a0f; + + width: 100%; + height: 100%; +} diff --git a/src/app/components/dot-background/dot-background.spec.ts b/src/app/components/dot-background/dot-background.spec.ts new file mode 100644 index 0000000..7aa702d --- /dev/null +++ b/src/app/components/dot-background/dot-background.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DotBackground } from './dot-background'; + +describe('DotBackground', () => { + let component: DotBackground; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DotBackground] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DotBackground); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/dot-background/dot-background.ts b/src/app/components/dot-background/dot-background.ts new file mode 100644 index 0000000..3ede884 --- /dev/null +++ b/src/app/components/dot-background/dot-background.ts @@ -0,0 +1,131 @@ +import {afterNextRender, AfterViewInit, Component, ElementRef, NgZone, OnDestroy, ViewChild} from '@angular/core'; +import {Dot} from '../../models/dot'; + +@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 DOT_COUNT = 12; + private readonly COLORS = ['#6366f1', '#8b5cf6', '#a855f7', '#3b82f6']; + private readonly MOUSE_RADIUS = 150; + + constructor(private ngZone: NgZone) { + 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); + + this.initialized = true; + this.ngZone.runOutsideAngular(() => this.animate()); + } + + private resize = () => { + const canvas = this.canvasRef.nativeElement; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + // Clamp dots to new bounds + for (const dot of this.dots) { + dot.x = Math.min(dot.x, canvas.width - dot.radius); + dot.y = Math.min(dot.y, canvas.height - dot.radius); + dot.x = Math.max(dot.x, dot.radius); + dot.y = Math.max(dot.y, dot.radius); + } + }; + + private onMouseMove = (e: MouseEvent) => { + this.mouse.x = e.clientX; + this.mouse.y = e.clientY; + }; + + private initDots() { + const { width, height } = this.canvasRef.nativeElement; + for (let i = 0; i < this.DOT_COUNT; i++) { + this.dots.push({ + x: Math.random() * width, + y: Math.random() * height, + vx: (Math.random() - 0.5) * 0.5, + vy: (Math.random() - 0.5) * 0.5, + radius: Math.random() * 60 + 40, + color: this.COLORS[i % this.COLORS.length], + }); + } + } + + 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 < 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; + } + + // 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); + }; +} diff --git a/src/app/models/dot.ts b/src/app/models/dot.ts new file mode 100644 index 0000000..c248254 --- /dev/null +++ b/src/app/models/dot.ts @@ -0,0 +1,8 @@ +export interface Dot { + x: number; + y: number; + vx: number; + vy: number; + radius: number; + color: string; +} diff --git a/src/app/models/skill-category.ts b/src/app/models/skill-category.ts new file mode 100644 index 0000000..edb3257 --- /dev/null +++ b/src/app/models/skill-category.ts @@ -0,0 +1,8 @@ +import {Skill} from './skill'; + +export interface SkillCategory { + title: string; + category: string; + skills: Skill[]; + gridArea: string; +} diff --git a/src/app/models/skill.ts b/src/app/models/skill.ts new file mode 100644 index 0000000..df17cca --- /dev/null +++ b/src/app/models/skill.ts @@ -0,0 +1,5 @@ +export interface Skill { + name: string; + icon: string; + url: string; +}