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",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
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="">
|
||||
<article class="border-b-1 h-100 flex items-center justify-center"><p class="text-9xl font-terminal-retro">HERO SECTION</p></article>
|
||||
<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>
|
||||
<!-- Video Section -->
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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