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) { +
+
+ Loading... +
+ } + +
+ + +
+
+
+
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(); + } +}