mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
Added threejs video holo component visualiser, and text scramble on hero page
This commit is contained in:
73
package-lock.json
generated
73
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tailwindcss-primeui": "^0.6.1",
|
"tailwindcss-primeui": "^0.6.1",
|
||||||
|
"three": "^0.176.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
@@ -2653,6 +2655,13 @@
|
|||||||
"node": ">=0.1.90"
|
"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": {
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
|
"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": "^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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -6718,6 +6734,36 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -7112,6 +7158,13 @@
|
|||||||
"@xtuc/long": "4.2.2"
|
"@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": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -13503,6 +13563,13 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
@@ -17171,6 +17238,12 @@
|
|||||||
"tslib": "^2"
|
"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": {
|
"node_modules/thunky": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tailwindcss-primeui": "^0.6.1",
|
"tailwindcss-primeui": "^0.6.1",
|
||||||
|
"three": "^0.176.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
|||||||
BIN
public/cyber_hands.mp4
Normal file
BIN
public/cyber_hands.mp4
Normal file
Binary file not shown.
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
<section class="">
|
<section class="">
|
||||||
<article class="border-b-1 h-100 flex items-center justify-center"><p class="text-9xl font-terminal-retro">HERO SECTION</p></article>
|
<!-- Video Section -->
|
||||||
<article class="border-b-1 h-100 flex items-center justify-center checkered-bg-dark"><p class="font-terminal-nier text-9xl">CREATIVE DEVELOPER</p></article>
|
<article class="border-b h-[40vh] sm:h-[60vh] md:h-[50vh] flex items-center justify-center">
|
||||||
|
<app-holo-video-container containerClasses="w-full aspect-video" videoSrc="cyber_hands.mp4" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Display Text Section -->
|
||||||
|
<article class="flex-1 flex items-center justify-center checkered-bg-dark py-4 sm:py-8 md:py-12">
|
||||||
|
<p class="font-terminal-nier scramble-text-glow
|
||||||
|
text-6xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl 2xl:text-9xl
|
||||||
|
text-center px-4 leading-tight">
|
||||||
|
{{ displayText }}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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({
|
@Component({
|
||||||
selector: 'app-hero-display',
|
selector: 'app-hero-display',
|
||||||
imports: [],
|
imports: [HoloVideoContainerComponent],
|
||||||
templateUrl: './hero-display.component.html',
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="block w-full h-full" [ngClass]="containerClasses">
|
||||||
|
<div class="bg-slate-900 rounded-lg overflow-hidden w-full h-full">
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<canvas #canvas class="block w-full h-full cursor-grab active:cursor-grabbing"></canvas>
|
||||||
|
|
||||||
|
@if (isLoadingValue) {
|
||||||
|
<div class="absolute inset-0 bg-black/80 flex items-center justify-center flex-col text-green-400 text-lg font-sans z-10">
|
||||||
|
<div class="w-10 h-10 border-3 border-gray-600 border-t-green-400 rounded-full animate-spin mb-4"></div>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="absolute top-2 right-2 bg-black/80 border border-gray-600 rounded p-2 backdrop-blur-sm z-10">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="video/*"
|
||||||
|
(change)="onVideoFileSelect($event)"
|
||||||
|
#videoInput
|
||||||
|
class="hidden">
|
||||||
|
<button
|
||||||
|
class="bg-gray-700 text-white border border-gray-500 px-3 py-1 rounded text-xs transition-all duration-300 hover:bg-gray-600 hover:border-gray-400 active:translate-y-px disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:bg-gray-700 disabled:hover:border-gray-500"
|
||||||
|
(click)="videoInput.click()"
|
||||||
|
[disabled]="isLoadingValue">
|
||||||
|
📁
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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<HTMLCanvasElement>;
|
||||||
|
@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<void> {
|
||||||
|
this.video.srcObject = null;
|
||||||
|
this.video.src = src;
|
||||||
|
this.video.load();
|
||||||
|
await this.video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoadingValue(): boolean {
|
||||||
|
return this.isLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user