diff --git a/package-lock.json b/package-lock.json
index 67a548f..a0bd80e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"prettier": "^3.5.3",
"rxjs": "~7.8.0",
"tailwindcss-primeui": "^0.6.1",
+ "three": "^0.176.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
@@ -29,6 +30,7 @@
"@tailwindcss/postcss": "^4.1.7",
"@types/jasmine": "~5.1.0",
"@types/jest": "^29.5.14",
+ "@types/three": "^0.176.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"autoprefixer": "^10.4.21",
@@ -2653,6 +2655,13 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@discoveryjs/json-ext": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
@@ -6376,6 +6385,13 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -6718,6 +6734,36 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.176.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.176.0.tgz",
+ "integrity": "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "^0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.22",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
+ "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -7112,6 +7158,13 @@
"@xtuc/long": "4.2.2"
}
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.60",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
+ "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -10011,6 +10064,13 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -13503,6 +13563,13 @@
"node": ">= 8"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -17171,6 +17238,12 @@
"tslib": "^2"
}
},
+ "node_modules/three": {
+ "version": "0.176.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz",
+ "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
+ "license": "MIT"
+ },
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
diff --git a/package.json b/package.json
index 79115b9..3a250c2 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"prettier": "^3.5.3",
"rxjs": "~7.8.0",
"tailwindcss-primeui": "^0.6.1",
+ "three": "^0.176.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
@@ -31,6 +32,7 @@
"@tailwindcss/postcss": "^4.1.7",
"@types/jasmine": "~5.1.0",
"@types/jest": "^29.5.14",
+ "@types/three": "^0.176.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"autoprefixer": "^10.4.21",
diff --git a/public/cyber_hands.mp4 b/public/cyber_hands.mp4
new file mode 100644
index 0000000..60bb367
Binary files /dev/null and b/public/cyber_hands.mp4 differ
diff --git a/src/app/features/hero-display/hero-display.component.css b/src/app/features/hero-display/hero-display.component.css
index e69de29..aa4df63 100644
--- a/src/app/features/hero-display/hero-display.component.css
+++ b/src/app/features/hero-display/hero-display.component.css
@@ -0,0 +1,17 @@
+/* hero-display.component.css */
+
+.scramble-text-glow {
+ text-shadow:
+ 0 0 5px rgba(255, 255, 255, 0.3),
+ 0 0 10px rgba(255, 255, 255, 0.2),
+ 0 0 15px rgba(255, 255, 255, 0.1);
+ transition: text-shadow 0.3s ease;
+}
+
+/* Enhanced glow for binary characters */
+.binary-glow {
+ text-shadow:
+ 0 0 3px rgba(255, 255, 255, 0.4),
+ 0 0 6px rgba(255, 255, 255, 0.3),
+ 0 0 9px rgba(255, 255, 255, 0.2);
+}
diff --git a/src/app/features/hero-display/hero-display.component.html b/src/app/features/hero-display/hero-display.component.html
index bbbbb39..aa4e7d0 100644
--- a/src/app/features/hero-display/hero-display.component.html
+++ b/src/app/features/hero-display/hero-display.component.html
@@ -1,4 +1,15 @@
- HERO SECTION
- CREATIVE DEVELOPER
+
+
+
+
+
+
+
+
+ {{ displayText }}
+
+
diff --git a/src/app/features/hero-display/hero-display.component.ts b/src/app/features/hero-display/hero-display.component.ts
index 22d157c..f486423 100644
--- a/src/app/features/hero-display/hero-display.component.ts
+++ b/src/app/features/hero-display/hero-display.component.ts
@@ -1,11 +1,198 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { HoloVideoContainerComponent } from '../../shared/ui/holo-video-container/holo-video-container.component';
@Component({
selector: 'app-hero-display',
- imports: [],
+ imports: [HoloVideoContainerComponent],
templateUrl: './hero-display.component.html',
- styleUrl: './hero-display.component.css'
+ styleUrl: './hero-display.component.css',
})
-export class HeroDisplayComponent {
+export class HeroDisplayComponent implements OnInit, OnDestroy {
+ displayText: string = '';
+ private messageQueue: string[] = [
+ 'Web Developer',
+ 'Coding Enjoyer',
+ 'Software Artisan',
+ ];
+
+ private currentMessage: string = '';
+ private isGlitching: boolean = false;
+ private frameRequest: number | null = null;
+ private processTimeout: any = null;
+ private isInitialMount: boolean = true;
+
+ ngOnInit(): void {
+ this.initialMountAnimation();
+ }
+
+ ngOnDestroy(): void {
+ this.cleanup();
+ }
+
+ private cleanup(): void {
+ if (this.frameRequest) {
+ cancelAnimationFrame(this.frameRequest);
+ }
+ if (this.processTimeout) {
+ clearTimeout(this.processTimeout);
+ }
+ }
+
+ private initialMountAnimation(): void {
+ const firstMessage = this.messageQueue[0];
+ let currentIndex = 0;
+
+ const animateNextLetter = (): void => {
+ if (currentIndex <= firstMessage.length) {
+ let output = '';
+
+ // Build the confirmed part
+ for (let i = 0; i < currentIndex; i++) {
+ output += firstMessage[i];
+ }
+
+ // Add intense scrambling for upcoming letters (very fast scramble)
+ const scrambleLength = Math.min(6, firstMessage.length - currentIndex);
+ for (let i = 0; i < scrambleLength; i++) {
+ output += Math.random() > 0.5 ? '0' : '1';
+ }
+
+ this.displayText = output;
+
+ if (currentIndex < firstMessage.length) {
+ currentIndex++;
+ setTimeout(animateNextLetter, 35 + Math.random() * 25); // Much faster: 35-60ms
+ } else {
+ // Mount animation complete, start normal cycle
+ this.displayText = firstMessage;
+ this.currentMessage = firstMessage;
+ this.messageQueue.shift(); // Remove the first message since we used it
+ this.isInitialMount = false;
+
+ // Start the regular cycle after a short pause
+ setTimeout(() => {
+ this.processQueue();
+ }, 2000);
+ }
+ }
+ };
+
+ animateNextLetter();
+ }
+
+ private processQueue(): void {
+ if (this.messageQueue.length === 0) {
+ this.messageQueue = [
+ 'Web Developer',
+ 'Coding Enjoyer',
+ 'Software Artisan',
+ ];
+ }
+
+ const nextMessage = this.messageQueue.shift()!;
+ this.startScrambleAnimation(nextMessage);
+
+ this.processTimeout = setTimeout(() => {
+ this.processQueue();
+ }, 6000); // Reduced from 10000 to 6000 (faster rotation)
+ }
+
+ private startScrambleAnimation(nextMessage: string): void {
+ const length = Math.max(this.displayText.length, nextMessage.length);
+ let complete = 0;
+
+ const update = (): void => {
+ let output = '';
+ const scrambleChars = 3 + Math.random() * 5;
+
+ for (let i = 0; i < length; i++) {
+ const scramble = i < scrambleChars + complete && Math.random() > 0.8;
+
+ if (i < nextMessage.length) {
+ if (scramble) {
+ output += Math.random() > 0.5 ? '0' : '1';
+ } else if (i < complete) {
+ output += nextMessage[i];
+ } else {
+ output += this.displayText[i] || (Math.random() > 0.5 ? '0' : '1');
+ }
+ }
+ }
+
+ this.displayText = output;
+
+ if (complete < nextMessage.length) {
+ complete += 0.5 + Math.floor(Math.random() * 2);
+ setTimeout(update, 40 + Math.random() * 60);
+ } else {
+ this.displayText = nextMessage;
+ this.currentMessage = nextMessage;
+ this.isGlitching = true;
+ this.glitchText();
+ }
+ };
+
+ this.isGlitching = false;
+ if (this.frameRequest) {
+ cancelAnimationFrame(this.frameRequest);
+ }
+ update();
+ }
+
+ private glitchText(): void {
+ if (!this.isGlitching) return;
+
+ const probability = Math.random();
+
+ if (probability < 0.05) {
+ // Reduced from 0.2 to 0.05
+ const scrambledText = this.currentMessage
+ .split('')
+ .map(() => (Math.random() > 0.5 ? '0' : '1'))
+ .join('');
+ this.displayText = scrambledText;
+
+ setTimeout(() => {
+ this.displayText = this.currentMessage;
+ }, 25);
+ } else if (probability < 0.15) {
+ // Reduced from 0.5 to 0.15
+ const textArray = this.currentMessage.split('');
+ for (let i = 0; i < Math.floor(textArray.length * 0.2); i++) {
+ // Reduced from 0.4 to 0.2
+ const idx = Math.floor(Math.random() * textArray.length);
+ textArray[idx] = Math.random() > 0.5 ? '0' : '1';
+ }
+ this.displayText = textArray.join('');
+
+ setTimeout(() => {
+ this.displayText = this.currentMessage;
+ }, 20);
+ }
+
+ const jitterProbability = Math.random();
+ if (jitterProbability < 0.1) {
+ // Reduced from 0.5 to 0.1
+ setTimeout(() => {
+ const textArray = this.displayText.split('');
+ for (let i = 0; i < 2; i++) {
+ // Reduced from 4 to 2 characters
+ const idx = Math.floor(Math.random() * textArray.length);
+ if (textArray[idx] === '0' || textArray[idx] === '1') {
+ textArray[idx] = textArray[idx] === '0' ? '1' : '0';
+ }
+ }
+ this.displayText = textArray.join('');
+
+ setTimeout(() => {
+ this.displayText = this.currentMessage;
+ }, 15);
+ }, 15);
+ }
+
+ this.frameRequest = requestAnimationFrame(() => {
+ setTimeout(() => this.glitchText(), Math.random() * 500); // Increased from 150 to 500
+ });
+ }
}
diff --git a/src/app/shared/ui/holo-video-container/holo-video-container.component.css b/src/app/shared/ui/holo-video-container/holo-video-container.component.css
new file mode 100644
index 0000000..6818c4e
--- /dev/null
+++ b/src/app/shared/ui/holo-video-container/holo-video-container.component.css
@@ -0,0 +1,45 @@
+:host {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+/* Custom spinner border width for Tailwind */
+.border-3 {
+ border-width: 3px;
+}
+
+/* Backdrop blur fallback for older browsers */
+.backdrop-blur-sm {
+ backdrop-filter: blur(4px);
+}
+
+/* Custom shadow glow effect */
+.shadow-cyan-500\/30 {
+ box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
+}
+
+/* Responsive container for smaller screens */
+@media (max-width: 840px) {
+ .w-\[800px\] {
+ width: 100%;
+ max-width: 800px;
+ }
+
+ .h-\[600px\] {
+ height: 60vw;
+ max-height: 600px;
+ }
+}
+
+@media (max-width: 768px) {
+ .absolute.top-5.right-5 {
+ top: 0.625rem;
+ right: 0.625rem;
+ left: 0.625rem;
+ }
+
+ .absolute.top-5.right-5 button {
+ width: 100%;
+ justify-content: center;
+ }
+}
diff --git a/src/app/shared/ui/holo-video-container/holo-video-container.component.html b/src/app/shared/ui/holo-video-container/holo-video-container.component.html
new file mode 100644
index 0000000..b9e1570
--- /dev/null
+++ b/src/app/shared/ui/holo-video-container/holo-video-container.component.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ @if (isLoadingValue) {
+
+ }
+
+
+
+
+
+
+
+
diff --git a/src/app/shared/ui/holo-video-container/holo-video-container.component.ts b/src/app/shared/ui/holo-video-container/holo-video-container.component.ts
new file mode 100644
index 0000000..c9b941a
--- /dev/null
+++ b/src/app/shared/ui/holo-video-container/holo-video-container.component.ts
@@ -0,0 +1,639 @@
+import {
+ Component,
+ ElementRef,
+ OnInit,
+ OnDestroy,
+ ViewChild,
+ Input,
+ signal,
+ effect,
+ HostListener,
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import * as THREE from 'three';
+
+@Component({
+ selector: 'app-holo-video-container',
+ imports: [CommonModule],
+ templateUrl: './holo-video-container.component.html',
+ styleUrl: './holo-video-container.component.css',
+})
+export class HoloVideoContainerComponent implements OnInit, OnDestroy {
+ @ViewChild('canvas', { static: true })
+ canvasRef!: ElementRef;
+ @Input() videoSrc?: string;
+ @Input() containerClasses: string = 'w-full h-96'; // Default with height
+
+ isLoading = signal(false);
+
+ // Three.js objects
+ private scene!: THREE.Scene;
+ private camera!: THREE.PerspectiveCamera;
+ private renderer!: THREE.WebGLRenderer;
+ private points!: THREE.Points;
+ private geometry!: THREE.BufferGeometry;
+ private material!: THREE.PointsMaterial;
+ private glowLayers: Array<{
+ points: THREE.Points;
+ geometry: THREE.BufferGeometry;
+ }> = [];
+
+ // Side decorations
+ private sideElements: THREE.Group[] = [];
+
+ private animationId: number = 0;
+ private video!: HTMLVideoElement;
+ private canvas2D!: HTMLCanvasElement;
+ private ctx2D!: CanvasRenderingContext2D;
+
+ readonly GRID_WIDTH = 320; // Increased from 240 for wider coverage
+ readonly GRID_HEIGHT = 180;
+ readonly POINTS_COUNT = this.GRID_WIDTH * this.GRID_HEIGHT;
+
+ // Mouse and effects
+ private mouseX = 0;
+ private mouseY = 0;
+ private isMouseOverCanvas = false;
+ private glitchTime = 0;
+ private glitchIntensity = 0;
+ private resizeObserver!: ResizeObserver;
+
+ constructor() {
+ effect(() => {
+ if (this.material) {
+ this.material.size = 8.0; // Increased from 5.0 to 8.0
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ this.initThreeJS();
+ this.createPointCloud();
+ this.createSideDecorations();
+ this.setupVideo();
+ this.setupEventListeners();
+ this.setupResizeObserver();
+ this.animate();
+ this.onWindowResize(); // Initial resize
+
+ if (this.videoSrc) {
+ this.loadVideoFromSrc(this.videoSrc);
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.animationId) {
+ cancelAnimationFrame(this.animationId);
+ }
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+ this.renderer?.dispose();
+ }
+
+ private initThreeJS(): void {
+ const canvas = this.canvasRef.nativeElement;
+ this.scene = new THREE.Scene();
+ this.scene.background = new THREE.Color(0x0a0d14);
+
+ // Initialize camera with even wider FOV for closer view
+ this.camera = new THREE.PerspectiveCamera(40, 1, 1, 10000);
+ this.camera.position.set(0, 0, 800);
+ this.camera.lookAt(0, 0, 0);
+
+ this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
+ this.renderer.setPixelRatio(window.devicePixelRatio);
+ }
+
+ private setupResizeObserver(): void {
+ const canvas = this.canvasRef.nativeElement;
+
+ this.resizeObserver = new ResizeObserver((entries) => {
+ for (let entry of entries) {
+ const { width, height } = entry.contentRect;
+ if (width > 0 && height > 0) {
+ this.handleResize(width, height);
+ }
+ }
+ });
+
+ // Observe the canvas element
+ this.resizeObserver.observe(canvas);
+ }
+
+ private handleResize(width: number, height: number): void {
+ // Update camera aspect ratio
+ this.camera.aspect = width / height;
+ this.camera.updateProjectionMatrix();
+
+ // Update renderer size
+ this.renderer.setSize(width, height, false);
+
+ // Calculate optimal camera distance
+ const fov = this.camera.fov * (Math.PI / 180);
+ const gridHeight = this.GRID_HEIGHT * 12; // Updated to match new scale
+ const gridWidth = this.GRID_WIDTH * 12; // Updated to match new scale
+
+ // Calculate visible dimensions at camera distance
+ const vFov = fov;
+ const hFov = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect);
+
+ // Calculate distance to fit width or height
+ const distanceHeight = gridHeight / 2 / Math.tan(vFov / 2);
+ const distanceWidth = gridWidth / 2 / Math.tan(hFov / 2);
+
+ // Use the distance that fits both dimensions with minimal margin
+ this.camera.position.z = Math.max(distanceHeight, distanceWidth) * 0.95;
+
+ // Update side decorations position based on aspect ratio
+ if (this.sideElements.length >= 2) {
+ const sideOffset = this.camera.aspect * 1000;
+ this.sideElements[0].position.x = -sideOffset;
+ this.sideElements[1].position.x = sideOffset;
+ }
+ }
+
+ @HostListener('window:resize')
+ onWindowResize(): void {
+ const canvas = this.canvasRef.nativeElement;
+ const rect = canvas.getBoundingClientRect();
+
+ if (rect.width > 0 && rect.height > 0) {
+ this.handleResize(rect.width, rect.height);
+ }
+ }
+
+ private createPointCloud(): void {
+ this.geometry = new THREE.BufferGeometry();
+ const positions = new Float32Array(this.POINTS_COUNT * 3);
+ const colors = new Float32Array(this.POINTS_COUNT * 3);
+ const originalPositions = new Float32Array(this.POINTS_COUNT * 3);
+
+ let index = 0;
+ for (let y = 0; y < this.GRID_HEIGHT; y++) {
+ for (let x = 0; x < this.GRID_WIDTH; x++) {
+ const posX = (x - this.GRID_WIDTH / 2) * 12; // Reduced from 16 to compensate for wider grid
+ const posY = -(y - this.GRID_HEIGHT / 2) * 12; // Reduced from 16 to maintain proportion
+
+ positions[index * 3] = posX;
+ positions[index * 3 + 1] = posY;
+ positions[index * 3 + 2] = 0;
+
+ originalPositions[index * 3] = posX;
+ originalPositions[index * 3 + 1] = posY;
+ originalPositions[index * 3 + 2] = 0;
+
+ colors[index * 3] = colors[index * 3 + 1] = colors[index * 3 + 2] = 1;
+ index++;
+ }
+ }
+
+ this.geometry.setAttribute(
+ 'position',
+ new THREE.BufferAttribute(positions, 3),
+ );
+ this.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
+ this.geometry.setAttribute(
+ 'originalPosition',
+ new THREE.BufferAttribute(originalPositions, 3),
+ );
+
+ this.material = new THREE.PointsMaterial({
+ size: 8.0, // Increased from 5.0 to 8.0
+ vertexColors: true,
+ transparent: true,
+ opacity: 1.0,
+ alphaTest: 0.5,
+ depthWrite: false,
+ blending: THREE.AdditiveBlending,
+ sizeAttenuation: true,
+ });
+
+ this.points = new THREE.Points(this.geometry, this.material);
+ this.scene.add(this.points);
+ this.createGlowLayers();
+ }
+
+ private createGlowLayers(): void {
+ for (let layer = 1; layer <= 3; layer++) {
+ const glowGeometry = this.geometry.clone();
+ const glowMaterial = new THREE.PointsMaterial({
+ size: 8.0 * (1 + layer * 0.5), // Increased base size from 5.0 to 8.0
+ vertexColors: true,
+ transparent: true,
+ opacity: 0.3 / layer,
+ alphaTest: 0.1,
+ depthWrite: false,
+ blending: THREE.AdditiveBlending,
+ sizeAttenuation: true,
+ });
+
+ const glowPoints = new THREE.Points(glowGeometry, glowMaterial);
+ this.scene.add(glowPoints);
+ this.glowLayers.push({ points: glowPoints, geometry: glowGeometry });
+ }
+ }
+
+ private createSideDecorations(): void {
+ // Create vertical scan lines on both sides
+ const createScanLines = (xPosition: number) => {
+ const group = new THREE.Group();
+
+ // Main vertical lines
+ for (let i = 0; i < 5; i++) {
+ const geometry = new THREE.BufferGeometry();
+ const positions = new Float32Array([
+ xPosition + (i - 2) * 30,
+ -1000,
+ 0,
+ xPosition + (i - 2) * 30,
+ 1000,
+ 0,
+ ]);
+ geometry.setAttribute(
+ 'position',
+ new THREE.BufferAttribute(positions, 3),
+ );
+
+ const material = new THREE.LineBasicMaterial({
+ color: new THREE.Color(0.3, 0.6, 0.8),
+ transparent: true,
+ opacity: 0.3,
+ linewidth: 1,
+ });
+
+ const line = new THREE.Line(geometry, material);
+ group.add(line);
+ }
+
+ // Floating particles
+ const particleGeometry = new THREE.BufferGeometry();
+ const particleCount = 50;
+ const positions = new Float32Array(particleCount * 3);
+ const opacities = new Float32Array(particleCount);
+
+ for (let i = 0; i < particleCount; i++) {
+ positions[i * 3] = xPosition + (Math.random() - 0.5) * 150;
+ positions[i * 3 + 1] = (Math.random() - 0.5) * 1500;
+ positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
+ opacities[i] = Math.random();
+ }
+
+ particleGeometry.setAttribute(
+ 'position',
+ new THREE.BufferAttribute(positions, 3),
+ );
+ particleGeometry.setAttribute(
+ 'opacity',
+ new THREE.BufferAttribute(opacities, 1),
+ );
+
+ const particleMaterial = new THREE.PointsMaterial({
+ size: 3,
+ color: new THREE.Color(0.5, 0.8, 1.0),
+ transparent: true,
+ opacity: 0.6,
+ blending: THREE.AdditiveBlending,
+ vertexColors: false,
+ sizeAttenuation: true,
+ });
+
+ const particles = new THREE.Points(particleGeometry, particleMaterial);
+ group.add(particles);
+
+ // Add grid pattern
+ const gridGroup = new THREE.Group();
+ const gridSize = 100;
+ const gridDivisions = 10;
+
+ for (let j = -5; j <= 5; j++) {
+ const lineGeometry = new THREE.BufferGeometry();
+ const linePositions = new Float32Array([
+ xPosition - gridSize,
+ (j * gridSize) / 5,
+ -50,
+ xPosition + gridSize,
+ (j * gridSize) / 5,
+ -50,
+ ]);
+ lineGeometry.setAttribute(
+ 'position',
+ new THREE.BufferAttribute(linePositions, 3),
+ );
+
+ const lineMaterial = new THREE.LineBasicMaterial({
+ color: new THREE.Color(0.2, 0.4, 0.6),
+ transparent: true,
+ opacity: 0.2,
+ });
+
+ const gridLine = new THREE.Line(lineGeometry, lineMaterial);
+ gridGroup.add(gridLine);
+ }
+
+ group.add(gridGroup);
+ return group;
+ };
+
+ // Create decorations for left and right sides
+ const leftDecoration = createScanLines(-2000);
+ const rightDecoration = createScanLines(2000);
+
+ this.sideElements.push(leftDecoration, rightDecoration);
+ this.scene.add(leftDecoration);
+ this.scene.add(rightDecoration);
+ }
+
+ private animateSideElements(time: number): void {
+ this.sideElements.forEach((group, index) => {
+ // Animate vertical lines
+ group.children.forEach((child, childIndex) => {
+ if (child instanceof THREE.Line && childIndex < 5) {
+ child.material.opacity =
+ 0.2 + Math.sin(time * 0.001 + childIndex) * 0.1;
+ }
+
+ // Animate particles
+ if (child instanceof THREE.Points) {
+ child.rotation.y = time * 0.0001;
+ const positions = child.geometry.attributes.position
+ .array as Float32Array;
+ for (let i = 0; i < positions.length; i += 3) {
+ positions[i + 1] += Math.sin(time * 0.001 + i) * 0.5;
+ if (positions[i + 1] > 750) positions[i + 1] = -750;
+ }
+ child.geometry.attributes.position.needsUpdate = true;
+ }
+
+ // Animate grid
+ if (child instanceof THREE.Group) {
+ child.rotation.z = Math.sin(time * 0.0005) * 0.05;
+ }
+ });
+ });
+ }
+
+ private setupVideo(): void {
+ this.video = document.createElement('video');
+ this.video.width = this.GRID_WIDTH;
+ this.video.height = this.GRID_HEIGHT;
+ this.video.autoplay = true;
+ this.video.muted = true;
+ this.video.loop = true;
+
+ this.canvas2D = document.createElement('canvas');
+ this.canvas2D.width = this.GRID_WIDTH;
+ this.canvas2D.height = this.GRID_HEIGHT;
+ this.ctx2D = this.canvas2D.getContext('2d')!;
+
+ this.createAnimatedPattern();
+ }
+
+ private createAnimatedPattern(): void {
+ const canvas = document.createElement('canvas');
+ canvas.width = this.GRID_WIDTH;
+ canvas.height = this.GRID_HEIGHT;
+ const ctx = canvas.getContext('2d')!;
+
+ let time = 0;
+ const animate = () => {
+ const imageData = ctx.createImageData(this.GRID_WIDTH, this.GRID_HEIGHT);
+ const data = imageData.data;
+
+ for (let y = 0; y < this.GRID_HEIGHT; y++) {
+ for (let x = 0; x < this.GRID_WIDTH; x++) {
+ const index = (y * this.GRID_WIDTH + x) * 4;
+ const centerX = this.GRID_WIDTH / 2;
+ const centerY = this.GRID_HEIGHT / 2;
+ const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
+ const wave = Math.sin(distance * 0.1 + time * 0.05) * 0.5 + 0.5;
+ const noise =
+ Math.sin(x * 0.1 + time * 0.02) * Math.cos(y * 0.1 + time * 0.03);
+ const intensity = Math.max(
+ 0,
+ Math.min(255, (wave + noise * 0.3) * 255),
+ );
+
+ data[index] = intensity;
+ data[index + 1] = intensity * 0.8;
+ data[index + 2] = intensity * 0.6;
+ data[index + 3] = 255;
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+ time++;
+ requestAnimationFrame(animate);
+ };
+
+ animate();
+ const stream = canvas.captureStream(30);
+ this.video.srcObject = stream;
+ this.video.play();
+ }
+
+ private updatePointCloud(): void {
+ if (!this.video.videoWidth || !this.video.videoHeight) return;
+
+ this.glitchTime++;
+ if (Math.random() < 0.003) {
+ this.glitchIntensity = Math.random() * 0.7 + 0.3;
+ setTimeout(() => (this.glitchIntensity = 0), Math.random() * 1000 + 500);
+ }
+
+ this.ctx2D.drawImage(this.video, 0, 0, this.GRID_WIDTH, this.GRID_HEIGHT);
+ const imageData = this.ctx2D.getImageData(
+ 0,
+ 0,
+ this.GRID_WIDTH,
+ this.GRID_HEIGHT,
+ );
+ const data = imageData.data;
+
+ const positions = this.geometry.attributes['position']
+ .array as Float32Array;
+ const colors = this.geometry.attributes['color'].array as Float32Array;
+ const originalPositions = this.geometry.attributes['originalPosition']
+ .array as Float32Array;
+
+ const rect = this.canvasRef.nativeElement.getBoundingClientRect();
+ const mouseWorldX =
+ ((this.mouseX / rect.width) * 2 - 1) * (this.GRID_WIDTH * 6); // Adjusted for new scale
+ const mouseWorldY =
+ -((this.mouseY / rect.height) * 2 - 1) * (this.GRID_HEIGHT * 6); // Adjusted for new scale
+
+ let index = 0;
+ for (let y = 0; y < this.GRID_HEIGHT; y++) {
+ for (let x = 0; x < this.GRID_WIDTH; x++) {
+ const pixelIndex = (y * this.GRID_WIDTH + x) * 4;
+ const r = data[pixelIndex];
+ const g = data[pixelIndex + 1];
+ const b = data[pixelIndex + 2];
+ const brightness = (r + g + b) / 3;
+
+ if (brightness <= 40) {
+ positions[index * 3] = positions[index * 3 + 1] = 0;
+ positions[index * 3 + 2] = -10000;
+ colors[index * 3] = colors[index * 3 + 1] = colors[index * 3 + 2] = 0;
+ index++;
+ continue;
+ }
+
+ let depth = brightness / 255;
+ depth = Math.pow(depth, 2.2);
+ const baseZ = depth * 400 - 200;
+
+ const originalX = originalPositions[index * 3];
+ const originalY = originalPositions[index * 3 + 1];
+
+ const distanceToMouse = Math.sqrt(
+ Math.pow(originalX - mouseWorldX, 2) +
+ Math.pow(originalY - mouseWorldY, 2),
+ );
+
+ let scatterX = 0,
+ scatterY = 0,
+ scatterZ = 0;
+ if (this.isMouseOverCanvas && distanceToMouse < 50) {
+ const scatterFactor = 1 - distanceToMouse / 50;
+ const scatterIntensity = scatterFactor * 20;
+ const deltaX = originalX - mouseWorldX;
+ const deltaY = originalY - mouseWorldY;
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+
+ if (distance > 0) {
+ scatterX = (deltaX / distance) * scatterIntensity;
+ scatterY = (deltaY / distance) * scatterIntensity;
+ scatterZ = scatterIntensity * 0.2;
+ }
+ }
+
+ // Glitch effect
+ let glitchX = 0,
+ glitchY = 0;
+ if (
+ this.glitchIntensity > 0 &&
+ Math.random() < this.glitchIntensity * 0.1
+ ) {
+ glitchX = (Math.random() - 0.5) * this.glitchIntensity * 25;
+ glitchY = (Math.random() - 0.5) * this.glitchIntensity * 25;
+ }
+
+ positions[index * 3] = originalX + scatterX + glitchX;
+ positions[index * 3 + 1] = originalY + scatterY + glitchY;
+ positions[index * 3 + 2] = baseZ + scatterZ;
+
+ // Apply holographic colors
+ this.applyHolographicColors(colors, index, brightness, distanceToMouse);
+ index++;
+ }
+ }
+
+ this.geometry.attributes['position'].needsUpdate = true;
+ this.geometry.attributes['color'].needsUpdate = true;
+ this.updateGlowLayers();
+ }
+
+ private applyHolographicColors(
+ colors: Float32Array,
+ index: number,
+ brightness: number,
+ distanceToMouse: number,
+ ): void {
+ let finalColor = [0.5, 0.5, 0.5]; // Default grey
+
+ if (brightness > 220) finalColor = [1.0, 1.0, 1.0];
+ else if (brightness > 180) finalColor = [0.9, 0.9, 0.9];
+ else if (brightness > 140) finalColor = [0.7, 0.7, 0.7];
+ else if (brightness > 100) finalColor = [0.5, 0.5, 0.5];
+ else if (brightness > 60) finalColor = [0.3, 0.3, 0.3];
+ else finalColor = [0.1, 0.1, 0.1];
+
+ // Add subtle color variations
+ const variation = (index % 7) / 20;
+ finalColor[0] += variation * 0.1;
+ finalColor[1] += variation * 0.05;
+ finalColor[2] += variation * 0.15;
+
+ // Mouse glow effect
+ if (this.isMouseOverCanvas && distanceToMouse < 50) {
+ const glowFactor = (1 - distanceToMouse / 50) * 0.6;
+ finalColor[0] = Math.min(1.0, finalColor[0] + glowFactor * 0.7);
+ finalColor[1] = Math.min(1.0, finalColor[1] + glowFactor * 0.8);
+ finalColor[2] = Math.min(1.0, finalColor[2] + glowFactor * 1.0);
+ }
+
+ colors[index * 3] = finalColor[0];
+ colors[index * 3 + 1] = finalColor[1];
+ colors[index * 3 + 2] = finalColor[2];
+ }
+
+ private updateGlowLayers(): void {
+ this.glowLayers.forEach((layer, layerIndex) => {
+ const layerPositions = layer.geometry.attributes['position']
+ .array as Float32Array;
+ const layerColors = layer.geometry.attributes['color']
+ .array as Float32Array;
+ const mainPositions = this.geometry.attributes['position']
+ .array as Float32Array;
+ const mainColors = this.geometry.attributes['color']
+ .array as Float32Array;
+
+ for (let i = 0; i < mainPositions.length; i += 3) {
+ layerPositions[i] = mainPositions[i];
+ layerPositions[i + 1] = mainPositions[i + 1];
+ layerPositions[i + 2] = mainPositions[i + 2] - (layerIndex + 1) * 2;
+ }
+
+ for (let i = 0; i < mainColors.length; i += 3) {
+ const glowIntensity = 0.8 / (layerIndex + 1);
+ layerColors[i] = mainColors[i] * glowIntensity;
+ layerColors[i + 1] = mainColors[i + 1] * glowIntensity;
+ layerColors[i + 2] = mainColors[i + 2] * glowIntensity;
+ }
+
+ layer.geometry.attributes['position'].needsUpdate = true;
+ layer.geometry.attributes['color'].needsUpdate = true;
+ });
+ }
+
+ private setupEventListeners(): void {
+ const canvas = this.canvasRef.nativeElement;
+
+ canvas.addEventListener('mousemove', (event) => {
+ const rect = canvas.getBoundingClientRect();
+ this.mouseX = event.clientX - rect.left;
+ this.mouseY = event.clientY - rect.top;
+ });
+
+ canvas.addEventListener(
+ 'mouseenter',
+ () => (this.isMouseOverCanvas = true),
+ );
+ canvas.addEventListener(
+ 'mouseleave',
+ () => (this.isMouseOverCanvas = false),
+ );
+
+ // Removed wheel event listener - no more zooming
+ }
+
+ private animate(): void {
+ this.animationId = requestAnimationFrame(() => this.animate());
+ const time = Date.now();
+ this.updatePointCloud();
+ this.animateSideElements(time);
+ this.renderer.render(this.scene, this.camera);
+ }
+
+ private async loadVideoFromSrc(src: string): Promise {
+ this.video.srcObject = null;
+ this.video.src = src;
+ this.video.load();
+ await this.video.play();
+ }
+
+ get isLoadingValue(): boolean {
+ return this.isLoading();
+ }
+}