Finished hero with text scramble on init and occasional glitching, changed header layout on mobile for better UX, finished minimal footer

This commit is contained in:
AdamBtech
2025-05-23 18:07:50 +02:00
parent 8576ee1d40
commit 642f0adb73
8 changed files with 520 additions and 401 deletions

View File

@@ -1,5 +1,7 @@
<footer> <footer class="bg-nier-dark border-t border-nier-accent">
<section class="checkered-background border-t-5 border-nier-accent h-15 flex items-center bg-nier-light text-nier-light"> <div class="container mx-auto px-4 py-6">
<p class="text-1xl font-noto-jp">Made by Adam Benyekkou 2025</p> <p class="text-center text-nier-light font-noto-jp text-sm">
</section> © 2025 Adam Benyekkou. All rights reserved.
</p>
</div>
</footer> </footer>

View File

@@ -1,7 +1,6 @@
<div class="typewriter-container"> <div class="typewriter-container">
@for (item of displayedTexts(); track item.id) { @for (item of currentTexts(); track item.id) {
<div class="typewriter-item"> <div class="typewriter-item" [@slideIn]>
<div class="text-scroller-text font-noto-jp text-sm"> <div class="text-scroller-text font-noto-jp text-sm">
{{ item.displayed }} {{ item.displayed }}
@if (item.isTyping) { @if (item.isTyping) {

View File

@@ -6,6 +6,8 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
signal, signal,
computed,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { import {
trigger, trigger,
@@ -16,19 +18,28 @@ import {
} from '@angular/animations'; } from '@angular/animations';
interface TextItem { interface TextItem {
id: number; readonly id: number;
text: string; readonly text: string;
displayed: string; readonly displayed: string;
isTyping: boolean; readonly isTyping: boolean;
isComplete: boolean; readonly isComplete: boolean;
} }
// Constants for better maintainability
const TYPING_DELAYS = {
GLITCH: { min: 150, max: 200, probability: 0.03 },
RAPID: { min: 5, max: 15, probability: 0.5 },
SLOW: { min: 50, max: 80, probability: 0.15 },
STUTTER: { delay: 200, probability: 0.02 },
} as const;
@Component({ @Component({
selector: 'app-header-text-animate-section', selector: 'app-header-text-animate-section',
standalone: true, standalone: true,
imports: [], imports: [],
templateUrl: './header-text-animate-section.component.html', templateUrl: './header-text-animate-section.component.html',
styleUrl: './header-text-animate-section.component.css', styleUrl: './header-text-animate-section.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [ animations: [
trigger('textChange', [ trigger('textChange', [
state( state(
@@ -45,13 +56,24 @@ interface TextItem {
transform: 'translateY(20px)', transform: 'translateY(20px)',
}), }),
), ),
transition('visible => hidden', [animate('0.5s ease-out')]), transition('visible => hidden', animate('300ms ease-out')),
transition('hidden => visible', [animate('0.5s ease-in')]), transition('hidden => visible', animate('300ms ease-in')),
]),
// Add fade-in animation for new text items
trigger('slideIn', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(10px)' }),
animate(
'200ms ease-out',
style({ opacity: 1, transform: 'translateY(0)' }),
),
]),
]), ]),
], ],
}) })
export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy { export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
phrases = input<string[]>([ // Input signals with better defaults
phrases = input<readonly string[]>([
'Uploading guinea pig consciousness to the cloud...', 'Uploading guinea pig consciousness to the cloud...',
'Error: Sarcasm module overloaded. Rebooting...', 'Error: Sarcasm module overloaded. Rebooting...',
'Downloading personalities... 404: Personality not found.', 'Downloading personalities... 404: Personality not found.',
@@ -62,73 +84,75 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
'Cybernetic guinea pigs have seized control of Sector 7...', 'Cybernetic guinea pigs have seized control of Sector 7...',
'ERROR: Reality.dll has crashed. Would you like to submit a bug report?', 'ERROR: Reality.dll has crashed. Would you like to submit a bug report?',
'Synthetic sushi generation complete. Tastes like chicken.exe...', 'Synthetic sushi generation complete. Tastes like chicken.exe...',
]); ] as const);
interval = input<number>(3000); // Default interval of 3 seconds interval = input<number>(2500);
typingSpeed = input<number>(30); // Time in ms between each character (faster than before) typingSpeed = input<number>(25);
maxDisplayedTexts = input<number>(4); // Maximum number of texts to display maxDisplayedTexts = input<number>(4);
// Signals for internal state // State signals
displayedTexts = signal<TextItem[]>([]); private readonly displayedTexts = signal<readonly TextItem[]>([]);
nextId = signal<number>(0); private readonly nextId = signal<number>(0);
phraseIndex = signal<number>(0); private readonly phraseIndex = signal<number>(0);
isTypingInProgress = signal<boolean>(false); private readonly isTypingInProgress = signal<boolean>(false);
private typewriterTimeouts: number[] = []; // Computed signals for better performance
private nextTextTimeout: any; readonly currentTexts = computed(() => this.displayedTexts());
readonly hasTexts = computed(() => this.displayedTexts().length > 0);
// Timeout management
private readonly activeTimeouts = new Set<number>();
private nextTextTimeout?: number;
constructor() { constructor() {
// Use effect to react to changes in phrases // Initialize first text when phrases are available
effect(() => { effect(() => {
const phrasesList = this.phrases(); const phrasesList = this.phrases();
if (phrasesList.length > 0 && this.displayedTexts().length === 0) { if (phrasesList.length > 0 && this.displayedTexts().length === 0) {
this.startNextText(); this.scheduleNextText(0); // Start immediately
} }
}); });
} }
ngOnInit(): void { ngOnInit(): void {
if (this.phrases().length) { // Component initialization handled in constructor effect
this.startTextRotation();
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.stopTextRotation(); this.cleanup();
this.clearTypewriterTimeouts();
} }
private startTextRotation(): void { private cleanup(): void {
// Start with the first text // Clear all timeouts
this.startNextText(); this.activeTimeouts.forEach((id) => window.clearTimeout(id));
} this.activeTimeouts.clear();
private stopTextRotation(): void {
if (this.nextTextTimeout) { if (this.nextTextTimeout) {
clearTimeout(this.nextTextTimeout); window.clearTimeout(this.nextTextTimeout);
this.nextTextTimeout = undefined;
} }
} }
private clearTypewriterTimeouts(): void { private scheduleNextText(delay: number = this.interval()): void {
this.typewriterTimeouts.forEach((id) => window.clearTimeout(id)); if (this.nextTextTimeout) {
this.typewriterTimeouts = []; window.clearTimeout(this.nextTextTimeout);
}
this.nextTextTimeout = window.setTimeout(() => {
this.startNextText();
}, delay);
} }
private startNextText(): void { private startNextText(): void {
// If we're already typing, don't start a new one
if (this.isTypingInProgress()) return; if (this.isTypingInProgress()) return;
const phrasesList = this.phrases(); const phrasesList = this.phrases();
if (phrasesList.length === 0) return; if (phrasesList.length === 0) return;
// Get the next phrase to display const currentIndex = this.phraseIndex();
const index = this.phraseIndex(); const nextText = phrasesList[currentIndex];
const nextText = phrasesList[index];
// Update the index for the next time // Create immutable text item
this.phraseIndex.update((idx) => (idx + 1) % phrasesList.length);
// Create a new text item
const newItem: TextItem = { const newItem: TextItem = {
id: this.nextId(), id: this.nextId(),
text: nextText, text: nextText,
@@ -137,44 +161,40 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
isComplete: false, isComplete: false,
}; };
// Update the nextId for the next time // Update state atomically
this.phraseIndex.update((idx) => (idx + 1) % phrasesList.length);
this.nextId.update((id) => id + 1); this.nextId.update((id) => id + 1);
this.isTypingInProgress.set(true);
// Add the new text to the displayed texts list // Update displayed texts with proper cleanup
this.displayedTexts.update((texts) => { this.displayedTexts.update((texts) => {
// If we already have the maximum number of texts, remove the oldest one const maxTexts = this.maxDisplayedTexts();
if (texts.length >= this.maxDisplayedTexts()) { if (texts.length >= maxTexts) {
return [...texts.slice(1), newItem]; return [...texts.slice(texts.length - maxTexts + 1), newItem];
} }
// Otherwise, just add the new text
return [...texts, newItem]; return [...texts, newItem];
}); });
// Set typing in progress // Start typing animation
this.isTypingInProgress.set(true); this.animateText(newItem.id);
// Start the typewriter effect
this.typeText(newItem.id);
} }
private typeText(textId: number): void { private animateText(textId: number): void {
const texts = this.displayedTexts(); const texts = this.displayedTexts();
const textIndex = texts.findIndex((t) => t.id === textId); const textIndex = texts.findIndex((t) => t.id === textId);
if (textIndex === -1) return; if (textIndex === -1) return;
const textItem = texts[textIndex]; const textItem = texts[textIndex];
const fullText = textItem.text; const { text, displayed } = textItem;
const currentLength = textItem.displayed.length; const nextCharIndex = displayed.length;
if (currentLength < fullText.length) { if (nextCharIndex < text.length) {
// Add one character // Update displayed text
const newDisplayed = fullText.substring(0, currentLength + 1); const newDisplayed = text.substring(0, nextCharIndex + 1);
// Update the text this.displayedTexts.update((currentTexts) => {
this.displayedTexts.update((texts) => { const updatedTexts = [...currentTexts];
const updatedTexts = [...texts];
updatedTexts[textIndex] = { updatedTexts[textIndex] = {
...textItem, ...textItem,
displayed: newDisplayed, displayed: newDisplayed,
@@ -182,63 +202,76 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
return updatedTexts; return updatedTexts;
}); });
// Hectic typing effect with much more randomization // Schedule next character with dynamic timing
// Occasionally pause, sometimes type very fast, sometimes slower const delay = this.calculateTypingDelay();
let nextDelay: number;
// Random chance for different typing behaviors
const randomFactor = Math.random();
if (randomFactor < 0.05) {
// Longer pause (glitch effect) - reduced probability and duration
nextDelay = 200 + Math.random() * 150;
} else if (randomFactor < 0.5) {
// Rapid typing burst - increased probability
nextDelay = 5 + Math.random() * 15;
} else if (randomFactor < 0.65) {
// Slow typing - reduced probability and duration
nextDelay = 70 + Math.random() * 50;
} else {
// Normal typing speed with some variation - faster base speed
nextDelay = this.typingSpeed() + Math.random() * 40 - 20;
}
// Occasionally "glitch" and add brief pause before continuing - reduced probability
if (Math.random() < 0.03) {
// Add a brief stutter (simulate buffering) - shorter pause
nextDelay += 250;
}
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
this.typeText(textId); this.animateText(textId);
}, nextDelay); }, delay);
this.typewriterTimeouts.push(timeoutId); this.activeTimeouts.add(timeoutId);
} else { } else {
// Text is complete // Text complete
this.displayedTexts.update((texts) => { this.completeText(textIndex, textItem);
const updatedTexts = [...texts];
updatedTexts[textIndex] = {
...textItem,
isTyping: false,
isComplete: true,
};
return updatedTexts;
});
// Typing is no longer in progress
this.isTypingInProgress.set(false);
// Schedule the next text after the interval
this.nextTextTimeout = setTimeout(() => {
this.clearTypewriterTimeouts(); // Clear any remaining timeouts
this.startNextText();
}, this.interval());
} }
} }
// Helper method to track items private calculateTypingDelay(): number {
trackByTextId(index: number, item: TextItem): number { const random = Math.random();
return item.id; const baseSpeed = this.typingSpeed();
// Apply different typing patterns based on probability
if (random < TYPING_DELAYS.GLITCH.probability) {
return (
TYPING_DELAYS.GLITCH.min +
Math.random() * (TYPING_DELAYS.GLITCH.max - TYPING_DELAYS.GLITCH.min)
);
}
if (random < TYPING_DELAYS.RAPID.probability) {
return (
TYPING_DELAYS.RAPID.min +
Math.random() * (TYPING_DELAYS.RAPID.max - TYPING_DELAYS.RAPID.min)
);
}
if (
random <
TYPING_DELAYS.RAPID.probability + TYPING_DELAYS.SLOW.probability
) {
return (
TYPING_DELAYS.SLOW.min +
Math.random() * (TYPING_DELAYS.SLOW.max - TYPING_DELAYS.SLOW.min)
);
}
// Normal typing with variation
let delay = baseSpeed + (Math.random() * 30 - 15);
// Add occasional stutter effect
if (Math.random() < TYPING_DELAYS.STUTTER.probability) {
delay += TYPING_DELAYS.STUTTER.delay;
}
return Math.max(delay, 1); // Ensure positive delay
} }
private completeText(textIndex: number, textItem: TextItem): void {
// Mark text as complete
this.displayedTexts.update((texts) => {
const updatedTexts = [...texts];
updatedTexts[textIndex] = {
...textItem,
isTyping: false,
isComplete: true,
};
return updatedTexts;
});
// Reset typing state and schedule next text
this.isTypingInProgress.set(false);
this.scheduleNextText();
}
// Optimized tracking function
trackByTextId = (index: number, item: TextItem): number => item.id;
} }

View File

@@ -1,15 +1,31 @@
<section class=""> <section class="h-screen relative">
<!-- Video Section --> <app-holo-video-container
<article class="border-b h-[40vh] sm:h-[60vh] md:h-[50vh] flex items-center justify-center"> containerClasses="absolute inset-0 w-full h-full"
<app-holo-video-container containerClasses="w-full aspect-video" videoSrc="cyber_hands.mp4" /> videoSrc="cyber_hands.mp4" />
</article>
<!-- Display Text Section --> <!-- Changed from justify-center to justify-start with top padding -->
<article class="flex-1 flex items-center justify-center checkered-bg-dark pt-4 sm:pt-8 md:pt-12"> <div class="absolute inset-0 flex flex-col items-center justify-start pt-20 sm:pt-24 md:pt-28 lg:pt-32 xl:pt-36 2xl:pt-40 pointer-events-none px-6 sm:px-8 md:px-12 lg:px-16 xl:px-20">
<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 <div class="mb-8 sm:mb-12 md:mb-16 lg:mb-20 xl:mb-24 2xl:mb-32">
text-center px-4 leading-tight"> <h1 class="font-terminal-nier scramble-text-glow text-cyan-200
{{ displayText }} text-6xl sm:text-7xl md:text-8xl lg:text-9xl xl:text-[10rem] 2xl:text-[12rem]
</p> text-center
</article> leading-[1.1] sm:leading-[1.05] md:leading-[1.0] lg:leading-[0.95] xl:leading-[0.9] 2xl:leading-[0.85]
font-black tracking-[-0.02em] pointer-events-auto
drop-shadow-[0_0_30px_rgba(34,211,238,0.6)]
max-w-[90vw] sm:max-w-[85vw] md:max-w-[80vw] lg:max-w-none break-words whitespace-pre-line">
{{ nameText }}
</h1>
</div>
<div class="max-w-[95vw] sm:max-w-[90vw] md:max-w-[85vw] lg:max-w-[80vw] xl:max-w-none">
<p class="font-terminal-nier scramble-text-glow text-cyan-100
text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl 2xl:text-9xl
text-center leading-[0.8] font-bold tracking-wide uppercase pointer-events-auto
drop-shadow-[0_0_25px_rgba(34,211,238,0.5)]
break-words px-4 sm:px-6 md:px-8 lg:px-10">
{{ displayText }}
</p>
</div>
</div>
</section> </section>

View File

@@ -9,6 +9,7 @@ import { HoloVideoContainerComponent } from '../../shared/ui/holo-video-containe
}) })
export class HeroDisplayComponent implements OnInit, OnDestroy { export class HeroDisplayComponent implements OnInit, OnDestroy {
displayText: string = ''; displayText: string = '';
nameText: string = '';
private messageQueue: string[] = [ private messageQueue: string[] = [
'Web Developer', 'Web Developer',
@@ -17,8 +18,11 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
]; ];
private currentMessage: string = ''; private currentMessage: string = '';
private currentName: string = 'ADAM\nBENYEKKOU';
private isGlitching: boolean = false; private isGlitching: boolean = false;
private isNameGlitching: boolean = false;
private frameRequest: number | null = null; private frameRequest: number | null = null;
private nameFrameRequest: number | null = null;
private processTimeout: any = null; private processTimeout: any = null;
private isInitialMount: boolean = true; private isInitialMount: boolean = true;
@@ -34,12 +38,57 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
if (this.frameRequest) { if (this.frameRequest) {
cancelAnimationFrame(this.frameRequest); cancelAnimationFrame(this.frameRequest);
} }
if (this.nameFrameRequest) {
cancelAnimationFrame(this.nameFrameRequest);
}
if (this.processTimeout) { if (this.processTimeout) {
clearTimeout(this.processTimeout); clearTimeout(this.processTimeout);
} }
} }
private initialMountAnimation(): void { private initialMountAnimation(): void {
// Start both animations simultaneously
this.animateNameOnInit();
this.animateDisplayTextOnInit();
}
private animateNameOnInit(): void {
const targetName = this.currentName;
let currentIndex = 0;
const animateNextLetter = (): void => {
if (currentIndex <= targetName.length) {
let output = '';
// Build the confirmed part
for (let i = 0; i < currentIndex; i++) {
output += targetName[i];
}
// Add intense scrambling for upcoming letters
const scrambleLength = Math.min(8, targetName.length - currentIndex);
for (let i = 0; i < scrambleLength; i++) {
output += Math.random() > 0.5 ? '0' : '1';
}
this.nameText = output;
if (currentIndex < targetName.length) {
currentIndex++;
setTimeout(animateNextLetter, 45 + Math.random() * 35); // Slightly slower for name
} else {
// Name animation complete, start glitching
this.nameText = targetName;
this.isNameGlitching = true;
this.glitchName();
}
}
};
animateNextLetter();
}
private animateDisplayTextOnInit(): void {
const firstMessage = this.messageQueue[0]; const firstMessage = this.messageQueue[0];
let currentIndex = 0; let currentIndex = 0;
@@ -52,7 +101,7 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
output += firstMessage[i]; output += firstMessage[i];
} }
// Add intense scrambling for upcoming letters (very fast scramble) // Add intense scrambling for upcoming letters
const scrambleLength = Math.min(6, firstMessage.length - currentIndex); const scrambleLength = Math.min(6, firstMessage.length - currentIndex);
for (let i = 0; i < scrambleLength; i++) { for (let i = 0; i < scrambleLength; i++) {
output += Math.random() > 0.5 ? '0' : '1'; output += Math.random() > 0.5 ? '0' : '1';
@@ -62,12 +111,12 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
if (currentIndex < firstMessage.length) { if (currentIndex < firstMessage.length) {
currentIndex++; currentIndex++;
setTimeout(animateNextLetter, 35 + Math.random() * 25); // Much faster: 35-60ms setTimeout(animateNextLetter, 35 + Math.random() * 25);
} else { } else {
// Mount animation complete, start normal cycle // Mount animation complete, start normal cycle
this.displayText = firstMessage; this.displayText = firstMessage;
this.currentMessage = firstMessage; this.currentMessage = firstMessage;
this.messageQueue.shift(); // Remove the first message since we used it this.messageQueue.shift();
this.isInitialMount = false; this.isInitialMount = false;
// Start the regular cycle after a short pause // Start the regular cycle after a short pause
@@ -95,7 +144,7 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
this.processTimeout = setTimeout(() => { this.processTimeout = setTimeout(() => {
this.processQueue(); this.processQueue();
}, 6000); // Reduced from 10000 to 6000 (faster rotation) }, 6000);
} }
private startScrambleAnimation(nextMessage: string): void { private startScrambleAnimation(nextMessage: string): void {
@@ -146,7 +195,6 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
const probability = Math.random(); const probability = Math.random();
if (probability < 0.05) { if (probability < 0.05) {
// Reduced from 0.2 to 0.05
const scrambledText = this.currentMessage const scrambledText = this.currentMessage
.split('') .split('')
.map(() => (Math.random() > 0.5 ? '0' : '1')) .map(() => (Math.random() > 0.5 ? '0' : '1'))
@@ -157,10 +205,8 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
this.displayText = this.currentMessage; this.displayText = this.currentMessage;
}, 25); }, 25);
} else if (probability < 0.15) { } else if (probability < 0.15) {
// Reduced from 0.5 to 0.15
const textArray = this.currentMessage.split(''); const textArray = this.currentMessage.split('');
for (let i = 0; i < Math.floor(textArray.length * 0.2); i++) { 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); const idx = Math.floor(Math.random() * textArray.length);
textArray[idx] = Math.random() > 0.5 ? '0' : '1'; textArray[idx] = Math.random() > 0.5 ? '0' : '1';
} }
@@ -173,11 +219,9 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
const jitterProbability = Math.random(); const jitterProbability = Math.random();
if (jitterProbability < 0.1) { if (jitterProbability < 0.1) {
// Reduced from 0.5 to 0.1
setTimeout(() => { setTimeout(() => {
const textArray = this.displayText.split(''); const textArray = this.displayText.split('');
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
// Reduced from 4 to 2 characters
const idx = Math.floor(Math.random() * textArray.length); const idx = Math.floor(Math.random() * textArray.length);
if (textArray[idx] === '0' || textArray[idx] === '1') { if (textArray[idx] === '0' || textArray[idx] === '1') {
textArray[idx] = textArray[idx] === '0' ? '1' : '0'; textArray[idx] = textArray[idx] === '0' ? '1' : '0';
@@ -192,7 +236,64 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
} }
this.frameRequest = requestAnimationFrame(() => { this.frameRequest = requestAnimationFrame(() => {
setTimeout(() => this.glitchText(), Math.random() * 500); // Increased from 150 to 500 setTimeout(() => this.glitchText(), Math.random() * 500);
});
}
private glitchName(): void {
if (!this.isNameGlitching) return;
const probability = Math.random();
if (probability < 0.03) {
// Less frequent than displayText glitching
const scrambledName = this.currentName
.split('')
.map((char) => (char === '\n' ? '\n' : Math.random() > 0.5 ? '0' : '1'))
.join('');
this.nameText = scrambledName;
setTimeout(() => {
this.nameText = this.currentName;
}, 30);
} else if (probability < 0.08) {
const nameArray = this.currentName.split('');
for (let i = 0; i < Math.floor(nameArray.length * 0.15); i++) {
const idx = Math.floor(Math.random() * nameArray.length);
if (nameArray[idx] !== '\n') {
// Don't replace line breaks
nameArray[idx] = Math.random() > 0.5 ? '0' : '1';
}
}
this.nameText = nameArray.join('');
setTimeout(() => {
this.nameText = this.currentName;
}, 25);
}
// Subtle character jitter
const jitterProbability = Math.random();
if (jitterProbability < 0.05) {
setTimeout(() => {
const nameArray = this.nameText.split('');
for (let i = 0; i < 1; i++) {
// Just 1 character at a time for name
const idx = Math.floor(Math.random() * nameArray.length);
if (nameArray[idx] === '0' || nameArray[idx] === '1') {
nameArray[idx] = nameArray[idx] === '0' ? '1' : '0';
}
}
this.nameText = nameArray.join('');
setTimeout(() => {
this.nameText = this.currentName;
}, 20);
}, 20);
}
this.nameFrameRequest = requestAnimationFrame(() => {
setTimeout(() => this.glitchName(), Math.random() * 800); // Slower glitching for name
}); });
} }
} }

View File

@@ -35,12 +35,12 @@
[interval]="2500" [interval]="2500"
/> />
</div> </div>
<div class="flex flex-wrap justify-center py-3 space-y-2 sm:space-y-0 sm:space-x-4 border-b border-nier-accent/30"> <div class="flex flex-wrap justify-center gap-2 py-3 border-b border-nier-accent/30">
<app-header-nav-links />
</div>
<div class="flex flex-wrap justify-center gap-2 py-3">
<app-header-contact-links /> <app-header-contact-links />
</div> </div>
<div class="flex flex-wrap justify-center py-3 space-y-2 sm:space-y-0 sm:space-x-4">
<app-header-nav-links />
</div>
</section> </section>
<!-- Tablet Layout (768px - 1023px) --> <!-- Tablet Layout (768px - 1023px) -->
@@ -136,4 +136,3 @@
</div> </div>
</section> </section>
</header> </header>

View File

@@ -10,20 +10,6 @@
</div> </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>
</div> </div>
</div>

View File

@@ -22,7 +22,7 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
@ViewChild('canvas', { static: true }) @ViewChild('canvas', { static: true })
canvasRef!: ElementRef<HTMLCanvasElement>; canvasRef!: ElementRef<HTMLCanvasElement>;
@Input() videoSrc?: string; @Input() videoSrc?: string;
@Input() containerClasses: string = 'w-full h-96'; // Default with height @Input() containerClasses: string = 'w-full h-96';
isLoading = signal(false); isLoading = signal(false);
@@ -38,19 +38,16 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
geometry: THREE.BufferGeometry; geometry: THREE.BufferGeometry;
}> = []; }> = [];
// Side decorations
private sideElements: THREE.Group[] = [];
private animationId: number = 0; private animationId: number = 0;
private video!: HTMLVideoElement; private video!: HTMLVideoElement;
private canvas2D!: HTMLCanvasElement; private canvas2D!: HTMLCanvasElement;
private ctx2D!: CanvasRenderingContext2D; private ctx2D!: CanvasRenderingContext2D;
readonly GRID_WIDTH = 320; // Increased from 240 for wider coverage readonly GRID_WIDTH = 320;
readonly GRID_HEIGHT = 180; readonly GRID_HEIGHT = 180;
readonly POINTS_COUNT = this.GRID_WIDTH * this.GRID_HEIGHT; readonly POINTS_COUNT = this.GRID_WIDTH * this.GRID_HEIGHT;
// Mouse and effects // Enhanced mouse and effects
private mouseX = 0; private mouseX = 0;
private mouseY = 0; private mouseY = 0;
private isMouseOverCanvas = false; private isMouseOverCanvas = false;
@@ -58,10 +55,21 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
private glitchIntensity = 0; private glitchIntensity = 0;
private resizeObserver!: ResizeObserver; private resizeObserver!: ResizeObserver;
// Enhanced scatter properties
private scatterIntensityMap = new Float32Array(this.POINTS_COUNT);
private scatterDecayMap = new Float32Array(this.POINTS_COUNT);
private readonly MAX_SCATTER_DISTANCE = 600; // Increased scatter range
private readonly SCATTER_STRENGTH = 45; // Much stronger scatter
private readonly GLOW_INTENSITY = 4.0; // Intense white glow multiplier
// Optimization: Reuse arrays
private tempPositions = new Float32Array(this.POINTS_COUNT * 3);
private tempColors = new Float32Array(this.POINTS_COUNT * 3);
constructor() { constructor() {
effect(() => { effect(() => {
if (this.material) { if (this.material) {
this.material.size = 8.0; // Increased from 5.0 to 8.0 this.material.size = 6.0;
} }
}); });
} }
@@ -69,12 +77,11 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.initThreeJS(); this.initThreeJS();
this.createPointCloud(); this.createPointCloud();
this.createSideDecorations();
this.setupVideo(); this.setupVideo();
this.setupEventListeners(); this.setupEventListeners();
this.setupResizeObserver(); this.setupResizeObserver();
this.animate(); this.animate();
this.onWindowResize(); // Initial resize this.onWindowResize();
if (this.videoSrc) { if (this.videoSrc) {
this.loadVideoFromSrc(this.videoSrc); this.loadVideoFromSrc(this.videoSrc);
@@ -88,6 +95,14 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
if (this.resizeObserver) { if (this.resizeObserver) {
this.resizeObserver.disconnect(); this.resizeObserver.disconnect();
} }
// Cleanup Three.js resources
this.geometry?.dispose();
this.material?.dispose();
this.glowLayers.forEach((layer) => {
layer.geometry.dispose();
(layer.points.material as THREE.Material).dispose();
});
this.renderer?.dispose(); this.renderer?.dispose();
} }
@@ -96,13 +111,16 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
this.scene = new THREE.Scene(); this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0d14); 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 = new THREE.PerspectiveCamera(40, 1, 1, 10000);
this.camera.position.set(0, 0, 800); this.camera.position.set(0, 0, 800);
this.camera.lookAt(0, 0, 0); this.camera.lookAt(0, 0, 0);
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); this.renderer = new THREE.WebGLRenderer({
this.renderer.setPixelRatio(window.devicePixelRatio); canvas,
antialias: true,
powerPreference: 'high-performance',
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
} }
private setupResizeObserver(): void { private setupResizeObserver(): void {
@@ -117,40 +135,25 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
} }
}); });
// Observe the canvas element
this.resizeObserver.observe(canvas); this.resizeObserver.observe(canvas);
} }
private handleResize(width: number, height: number): void { private handleResize(width: number, height: number): void {
// Update camera aspect ratio
this.camera.aspect = width / height; this.camera.aspect = width / height;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
// Update renderer size
this.renderer.setSize(width, height, false); this.renderer.setSize(width, height, false);
// Calculate optimal camera distance
const fov = this.camera.fov * (Math.PI / 180); const fov = this.camera.fov * (Math.PI / 180);
const gridHeight = this.GRID_HEIGHT * 12; // Updated to match new scale const gridHeight = this.GRID_HEIGHT * 12;
const gridWidth = this.GRID_WIDTH * 12; // Updated to match new scale const gridWidth = this.GRID_WIDTH * 12;
// Calculate visible dimensions at camera distance
const vFov = fov; const vFov = fov;
const hFov = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect); 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 distanceHeight = gridHeight / 2 / Math.tan(vFov / 2);
const distanceWidth = gridWidth / 2 / Math.tan(hFov / 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; 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') @HostListener('window:resize')
@@ -172,8 +175,8 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
let index = 0; let index = 0;
for (let y = 0; y < this.GRID_HEIGHT; y++) { for (let y = 0; y < this.GRID_HEIGHT; y++) {
for (let x = 0; x < this.GRID_WIDTH; x++) { 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 posX = (x - this.GRID_WIDTH / 2) * 12;
const posY = -(y - this.GRID_HEIGHT / 2) * 12; // Reduced from 16 to maintain proportion const posY = -(y - this.GRID_HEIGHT / 2) * 12;
positions[index * 3] = posX; positions[index * 3] = posX;
positions[index * 3 + 1] = posY; positions[index * 3 + 1] = posY;
@@ -183,7 +186,12 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
originalPositions[index * 3 + 1] = posY; originalPositions[index * 3 + 1] = posY;
originalPositions[index * 3 + 2] = 0; originalPositions[index * 3 + 2] = 0;
colors[index * 3] = colors[index * 3 + 1] = colors[index * 3 + 2] = 1; colors[index * 3] = colors[index * 3 + 1] = colors[index * 3 + 2] = 0.8;
// Initialize scatter maps
this.scatterIntensityMap[index] = 0;
this.scatterDecayMap[index] = 0;
index++; index++;
} }
} }
@@ -199,7 +207,7 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
); );
this.material = new THREE.PointsMaterial({ this.material = new THREE.PointsMaterial({
size: 8.0, // Increased from 5.0 to 8.0 size: 6.0,
vertexColors: true, vertexColors: true,
transparent: true, transparent: true,
opacity: 1.0, opacity: 1.0,
@@ -215,13 +223,14 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
} }
private createGlowLayers(): void { private createGlowLayers(): void {
// Enhanced glow layers for stronger effect
for (let layer = 1; layer <= 3; layer++) { for (let layer = 1; layer <= 3; layer++) {
const glowGeometry = this.geometry.clone(); const glowGeometry = this.geometry.clone();
const glowMaterial = new THREE.PointsMaterial({ const glowMaterial = new THREE.PointsMaterial({
size: 8.0 * (1 + layer * 0.5), // Increased base size from 5.0 to 8.0 size: 6.0 * (1 + layer * 0.6), // Larger glow sizes
vertexColors: true, vertexColors: true,
transparent: true, transparent: true,
opacity: 0.3 / layer, opacity: 0.4 / layer, // Increased opacity
alphaTest: 0.1, alphaTest: 0.1,
depthWrite: false, depthWrite: false,
blending: THREE.AdditiveBlending, blending: THREE.AdditiveBlending,
@@ -234,145 +243,6 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
} }
} }
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 { private setupVideo(): void {
this.video = document.createElement('video'); this.video = document.createElement('video');
this.video.width = this.GRID_WIDTH; this.video.width = this.GRID_WIDTH;
@@ -406,17 +276,18 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
const centerX = this.GRID_WIDTH / 2; const centerX = this.GRID_WIDTH / 2;
const centerY = this.GRID_HEIGHT / 2; const centerY = this.GRID_HEIGHT / 2;
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 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 wave = Math.sin(distance * 0.08 + time * 0.04) * 0.5 + 0.5;
const noise = const noise =
Math.sin(x * 0.1 + time * 0.02) * Math.cos(y * 0.1 + time * 0.03); Math.sin(x * 0.05 + time * 0.015) *
Math.cos(y * 0.05 + time * 0.02);
const intensity = Math.max( const intensity = Math.max(
0, 0,
Math.min(255, (wave + noise * 0.3) * 255), Math.min(255, (wave + noise * 0.4) * 255),
); );
data[index] = intensity; data[index] = intensity;
data[index + 1] = intensity * 0.8; data[index + 1] = intensity * 0.85;
data[index + 2] = intensity * 0.6; data[index + 2] = intensity * 0.7;
data[index + 3] = 255; data[index + 3] = 255;
} }
} }
@@ -435,10 +306,11 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
private updatePointCloud(): void { private updatePointCloud(): void {
if (!this.video.videoWidth || !this.video.videoHeight) return; if (!this.video.videoWidth || !this.video.videoHeight) return;
// Enhanced glitch system
this.glitchTime++; this.glitchTime++;
if (Math.random() < 0.003) { if (Math.random() < 0.008) {
this.glitchIntensity = Math.random() * 0.7 + 0.3; this.glitchIntensity = Math.random() * 1.2 + 0.6;
setTimeout(() => (this.glitchIntensity = 0), Math.random() * 1000 + 500); setTimeout(() => (this.glitchIntensity = 0), Math.random() * 500 + 150);
} }
this.ctx2D.drawImage(this.video, 0, 0, this.GRID_WIDTH, this.GRID_HEIGHT); this.ctx2D.drawImage(this.video, 0, 0, this.GRID_WIDTH, this.GRID_HEIGHT);
@@ -458,9 +330,22 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
const rect = this.canvasRef.nativeElement.getBoundingClientRect(); const rect = this.canvasRef.nativeElement.getBoundingClientRect();
const mouseWorldX = const mouseWorldX =
((this.mouseX / rect.width) * 2 - 1) * (this.GRID_WIDTH * 6); // Adjusted for new scale ((this.mouseX / rect.width) * 2 - 1) * (this.GRID_WIDTH * 6);
const mouseWorldY = const mouseWorldY =
-((this.mouseY / rect.height) * 2 - 1) * (this.GRID_HEIGHT * 6); // Adjusted for new scale -((this.mouseY / rect.height) * 2 - 1) * (this.GRID_HEIGHT * 6);
const isGlitching = this.glitchIntensity > 0;
// Update scatter decay for all points
for (let i = 0; i < this.POINTS_COUNT; i++) {
if (this.scatterDecayMap[i] > 0) {
this.scatterDecayMap[i] *= 0.92; // Slower decay for longer effect
if (this.scatterDecayMap[i] < 0.01) {
this.scatterDecayMap[i] = 0;
this.scatterIntensityMap[i] = 0;
}
}
}
let index = 0; let index = 0;
for (let y = 0; y < this.GRID_HEIGHT; y++) { for (let y = 0; y < this.GRID_HEIGHT; y++) {
@@ -491,40 +376,99 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
Math.pow(originalY - mouseWorldY, 2), Math.pow(originalY - mouseWorldY, 2),
); );
// ENHANCED: Much stronger scatter effect with persistent intensity
let scatterX = 0, let scatterX = 0,
scatterY = 0, scatterY = 0,
scatterZ = 0; scatterZ = 0;
if (this.isMouseOverCanvas && distanceToMouse < 50) { let currentScatterIntensity = this.scatterIntensityMap[index];
const scatterFactor = 1 - distanceToMouse / 50;
const scatterIntensity = scatterFactor * 20; if (
this.isMouseOverCanvas &&
distanceToMouse < this.MAX_SCATTER_DISTANCE
) {
// Calculate scatter factor with explosive falloff
const scatterFactor = Math.pow(
1 - distanceToMouse / this.MAX_SCATTER_DISTANCE,
1.5,
);
const newScatterIntensity = scatterFactor * this.SCATTER_STRENGTH;
// Update scatter intensity (with momentum for explosive transitions)
if (newScatterIntensity > currentScatterIntensity) {
this.scatterIntensityMap[index] = newScatterIntensity;
this.scatterDecayMap[index] = 1.0; // Full decay strength
currentScatterIntensity = newScatterIntensity;
}
const deltaX = originalX - mouseWorldX; const deltaX = originalX - mouseWorldX;
const deltaY = originalY - mouseWorldY; const deltaY = originalY - mouseWorldY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 0) { if (distance > 0) {
scatterX = (deltaX / distance) * scatterIntensity; // Explosive scatter with dramatic randomness
scatterY = (deltaY / distance) * scatterIntensity; const randomFactor = 0.6 + Math.random() * 0.8; // Increased randomness
scatterZ = scatterIntensity * 0.2; const explosionMultiplier = 1.5 + Math.random() * 0.5; // Extra explosion force
scatterX =
(deltaX / distance) *
currentScatterIntensity *
randomFactor *
explosionMultiplier;
scatterY =
(deltaY / distance) *
currentScatterIntensity *
randomFactor *
explosionMultiplier;
scatterZ = currentScatterIntensity * 0.6 * randomFactor;
// Add chaotic perpendicular scatter for spiral explosion
const perpX = -deltaY / distance;
const perpY = deltaX / distance;
const perpScatter =
(Math.random() - 0.5) * currentScatterIntensity * 0.5;
scatterX += perpX * perpScatter;
scatterY += perpY * perpScatter;
}
} else if (currentScatterIntensity > 0) {
// Apply decaying scatter effect
const decayFactor = this.scatterDecayMap[index];
const deltaX = originalX - mouseWorldX;
const deltaY = originalY - mouseWorldY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 0) {
scatterX =
(deltaX / distance) * currentScatterIntensity * decayFactor;
scatterY =
(deltaY / distance) * currentScatterIntensity * decayFactor;
scatterZ = currentScatterIntensity * 0.4 * decayFactor;
} }
} }
// Glitch effect // Enhanced glitch effect
let glitchX = 0, let glitchX = 0,
glitchY = 0; glitchY = 0,
if ( glitchZ = 0;
this.glitchIntensity > 0 && if (isGlitching) {
Math.random() < this.glitchIntensity * 0.1 glitchX = (Math.random() - 0.5) * this.glitchIntensity * 35;
) { glitchY = (Math.random() - 0.5) * this.glitchIntensity * 35;
glitchX = (Math.random() - 0.5) * this.glitchIntensity * 25; glitchZ = (Math.random() - 0.5) * this.glitchIntensity * 30;
glitchY = (Math.random() - 0.5) * this.glitchIntensity * 25;
} }
positions[index * 3] = originalX + scatterX + glitchX; positions[index * 3] = originalX + scatterX + glitchX;
positions[index * 3 + 1] = originalY + scatterY + glitchY; positions[index * 3 + 1] = originalY + scatterY + glitchY;
positions[index * 3 + 2] = baseZ + scatterZ; positions[index * 3 + 2] = baseZ + scatterZ + glitchZ;
// Apply enhanced colors with strong white glow for scattered points
this.applyEnhancedHolographicColors(
colors,
index,
brightness,
distanceToMouse,
currentScatterIntensity,
isGlitching,
);
// Apply holographic colors
this.applyHolographicColors(colors, index, brightness, distanceToMouse);
index++; index++;
} }
} }
@@ -534,33 +478,68 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
this.updateGlowLayers(); this.updateGlowLayers();
} }
private applyHolographicColors( private applyEnhancedHolographicColors(
colors: Float32Array, colors: Float32Array,
index: number, index: number,
brightness: number, brightness: number,
distanceToMouse: number, distanceToMouse: number,
scatterIntensity: number,
isGlitching: boolean,
): void { ): void {
let finalColor = [0.5, 0.5, 0.5]; // Default grey let finalColor = [0.5, 0.5, 0.5];
if (brightness > 220) finalColor = [1.0, 1.0, 1.0]; // Base color mapping
else if (brightness > 180) finalColor = [0.9, 0.9, 0.9]; if (brightness > 220) finalColor = [0.95, 0.95, 0.95];
else if (brightness > 180) finalColor = [0.85, 0.85, 0.85];
else if (brightness > 140) finalColor = [0.7, 0.7, 0.7]; 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 > 100) finalColor = [0.55, 0.55, 0.55];
else if (brightness > 60) finalColor = [0.3, 0.3, 0.3]; else if (brightness > 60) finalColor = [0.4, 0.4, 0.4];
else finalColor = [0.1, 0.1, 0.1]; else finalColor = [0.25, 0.25, 0.25];
// Add subtle color variations // Subtle color variations
const variation = (index % 7) / 20; const variation = (index % 5) / 25;
finalColor[0] += variation * 0.1; finalColor[0] += variation * 0.08;
finalColor[1] += variation * 0.05; finalColor[1] += variation * 0.04;
finalColor[2] += variation * 0.15; finalColor[2] += variation * 0.12;
// Mouse glow effect // DRAMATIC WHITE GLOW for scattered points
if (this.isMouseOverCanvas && distanceToMouse < 50) { if (scatterIntensity > 0) {
const glowFactor = (1 - distanceToMouse / 50) * 0.6; const glowFactor =
finalColor[0] = Math.min(1.0, finalColor[0] + glowFactor * 0.7); (scatterIntensity / this.SCATTER_STRENGTH) * this.GLOW_INTENSITY;
finalColor[1] = Math.min(1.0, finalColor[1] + glowFactor * 0.8);
finalColor[2] = Math.min(1.0, finalColor[2] + glowFactor * 1.0); // Progressive white explosion effect
if (glowFactor > 2.0) {
// Complete white explosion for heavily scattered points
finalColor = [1.0, 1.0, 1.0];
} else if (glowFactor > 1.0) {
// Intense white glow
finalColor[0] = Math.min(1.0, finalColor[0] + glowFactor * 0.8);
finalColor[1] = Math.min(1.0, finalColor[1] + glowFactor * 0.9);
finalColor[2] = Math.min(1.0, finalColor[2] + glowFactor * 1.0);
} else {
// Moderate white glow with blue tint
finalColor[0] = Math.min(1.0, finalColor[0] + glowFactor * 0.6);
finalColor[1] = Math.min(1.0, finalColor[1] + glowFactor * 0.8);
finalColor[2] = Math.min(1.0, finalColor[2] + glowFactor * 1.0);
}
// Add extra white flash for very scattered points
if (scatterIntensity > this.SCATTER_STRENGTH * 0.7) {
finalColor[0] = finalColor[1] = finalColor[2] = 1.0;
}
}
// Enhanced glitch effect with more white flashing
if (isGlitching) {
const glitchWhiteness = this.glitchIntensity * 0.8;
finalColor[0] = Math.min(1.0, finalColor[0] + glitchWhiteness);
finalColor[1] = Math.min(1.0, finalColor[1] + glitchWhiteness);
finalColor[2] = Math.min(1.0, finalColor[2] + glitchWhiteness);
// More frequent pure white flashes during glitch
if (Math.random() < 0.25) {
finalColor[0] = finalColor[1] = finalColor[2] = 1.0;
}
} }
colors[index * 3] = finalColor[0]; colors[index * 3] = finalColor[0];
@@ -569,27 +548,35 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
} }
private updateGlowLayers(): void { private updateGlowLayers(): void {
const mainPositions = this.geometry.attributes['position']
.array as Float32Array;
const mainColors = this.geometry.attributes['color'].array as Float32Array;
this.glowLayers.forEach((layer, layerIndex) => { this.glowLayers.forEach((layer, layerIndex) => {
const layerPositions = layer.geometry.attributes['position'] const layerPositions = layer.geometry.attributes['position']
.array as Float32Array; .array as Float32Array;
const layerColors = layer.geometry.attributes['color'] const layerColors = layer.geometry.attributes['color']
.array as Float32Array; .array as Float32Array;
const mainPositions = this.geometry.attributes['position']
.array as Float32Array;
const mainColors = this.geometry.attributes['color']
.array as Float32Array;
// Copy positions with slight offset for depth
for (let i = 0; i < mainPositions.length; i += 3) { for (let i = 0; i < mainPositions.length; i += 3) {
layerPositions[i] = mainPositions[i]; layerPositions[i] = mainPositions[i];
layerPositions[i + 1] = mainPositions[i + 1]; layerPositions[i + 1] = mainPositions[i + 1];
layerPositions[i + 2] = mainPositions[i + 2] - (layerIndex + 1) * 2; layerPositions[i + 2] = mainPositions[i + 2] - (layerIndex + 1) * 4; // Increased offset
} }
// Enhanced glow colors with stronger intensity for white particles
const glowIntensity = 0.9 / (layerIndex + 1); // Increased from 0.7
for (let i = 0; i < mainColors.length; i += 3) { for (let i = 0; i < mainColors.length; i += 3) {
const glowIntensity = 0.8 / (layerIndex + 1); // Amplify white/bright colors dramatically in glow layers
layerColors[i] = mainColors[i] * glowIntensity; const brightnessFactor =
layerColors[i + 1] = mainColors[i + 1] * glowIntensity; (mainColors[i] + mainColors[i + 1] + mainColors[i + 2]) / 3;
layerColors[i + 2] = mainColors[i + 2] * glowIntensity; const isWhite = brightnessFactor > 0.9;
const glowBoost = isWhite ? 2.5 : brightnessFactor > 0.8 ? 1.8 : 1.0;
layerColors[i] = mainColors[i] * glowIntensity * glowBoost;
layerColors[i + 1] = mainColors[i + 1] * glowIntensity * glowBoost;
layerColors[i + 2] = mainColors[i + 2] * glowIntensity * glowBoost;
} }
layer.geometry.attributes['position'].needsUpdate = true; layer.geometry.attributes['position'].needsUpdate = true;
@@ -610,19 +597,15 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
'mouseenter', 'mouseenter',
() => (this.isMouseOverCanvas = true), () => (this.isMouseOverCanvas = true),
); );
canvas.addEventListener( canvas.addEventListener('mouseleave', () => {
'mouseleave', this.isMouseOverCanvas = false;
() => (this.isMouseOverCanvas = false), // Don't reset scatter immediately - let it decay naturally
); });
// Removed wheel event listener - no more zooming
} }
private animate(): void { private animate(): void {
this.animationId = requestAnimationFrame(() => this.animate()); this.animationId = requestAnimationFrame(() => this.animate());
const time = Date.now();
this.updatePointCloud(); this.updatePointCloud();
this.animateSideElements(time);
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
} }