mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
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:
@@ -1,5 +1,7 @@
|
||||
<footer>
|
||||
<section class="checkered-background border-t-5 border-nier-accent h-15 flex items-center bg-nier-light text-nier-light">
|
||||
<p class="text-1xl font-noto-jp">Made by Adam Benyekkou 2025</p>
|
||||
</section>
|
||||
<footer class="bg-nier-dark border-t border-nier-accent">
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<p class="text-center text-nier-light font-noto-jp text-sm">
|
||||
© 2025 Adam Benyekkou. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
<div class="typewriter-container">
|
||||
@for (item of displayedTexts(); track item.id) {
|
||||
<div class="typewriter-item">
|
||||
@for (item of currentTexts(); track item.id) {
|
||||
<div class="typewriter-item" [@slideIn]>
|
||||
<div class="text-scroller-text font-noto-jp text-sm">
|
||||
{{ item.displayed }}
|
||||
@if (item.isTyping) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
signal,
|
||||
computed,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
@@ -16,19 +18,28 @@ import {
|
||||
} from '@angular/animations';
|
||||
|
||||
interface TextItem {
|
||||
id: number;
|
||||
text: string;
|
||||
displayed: string;
|
||||
isTyping: boolean;
|
||||
isComplete: boolean;
|
||||
readonly id: number;
|
||||
readonly text: string;
|
||||
readonly displayed: string;
|
||||
readonly isTyping: 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({
|
||||
selector: 'app-header-text-animate-section',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './header-text-animate-section.component.html',
|
||||
styleUrl: './header-text-animate-section.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('textChange', [
|
||||
state(
|
||||
@@ -45,13 +56,24 @@ interface TextItem {
|
||||
transform: 'translateY(20px)',
|
||||
}),
|
||||
),
|
||||
transition('visible => hidden', [animate('0.5s ease-out')]),
|
||||
transition('hidden => visible', [animate('0.5s ease-in')]),
|
||||
transition('visible => hidden', animate('300ms ease-out')),
|
||||
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 {
|
||||
phrases = input<string[]>([
|
||||
// Input signals with better defaults
|
||||
phrases = input<readonly string[]>([
|
||||
'Uploading guinea pig consciousness to the cloud...',
|
||||
'Error: Sarcasm module overloaded. Rebooting...',
|
||||
'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...',
|
||||
'ERROR: Reality.dll has crashed. Would you like to submit a bug report?',
|
||||
'Synthetic sushi generation complete. Tastes like chicken.exe...',
|
||||
]);
|
||||
] as const);
|
||||
|
||||
interval = input<number>(3000); // Default interval of 3 seconds
|
||||
typingSpeed = input<number>(30); // Time in ms between each character (faster than before)
|
||||
maxDisplayedTexts = input<number>(4); // Maximum number of texts to display
|
||||
interval = input<number>(2500);
|
||||
typingSpeed = input<number>(25);
|
||||
maxDisplayedTexts = input<number>(4);
|
||||
|
||||
// Signals for internal state
|
||||
displayedTexts = signal<TextItem[]>([]);
|
||||
nextId = signal<number>(0);
|
||||
phraseIndex = signal<number>(0);
|
||||
isTypingInProgress = signal<boolean>(false);
|
||||
// State signals
|
||||
private readonly displayedTexts = signal<readonly TextItem[]>([]);
|
||||
private readonly nextId = signal<number>(0);
|
||||
private readonly phraseIndex = signal<number>(0);
|
||||
private readonly isTypingInProgress = signal<boolean>(false);
|
||||
|
||||
private typewriterTimeouts: number[] = [];
|
||||
private nextTextTimeout: any;
|
||||
// Computed signals for better performance
|
||||
readonly currentTexts = computed(() => this.displayedTexts());
|
||||
readonly hasTexts = computed(() => this.displayedTexts().length > 0);
|
||||
|
||||
// Timeout management
|
||||
private readonly activeTimeouts = new Set<number>();
|
||||
private nextTextTimeout?: number;
|
||||
|
||||
constructor() {
|
||||
// Use effect to react to changes in phrases
|
||||
// Initialize first text when phrases are available
|
||||
effect(() => {
|
||||
const phrasesList = this.phrases();
|
||||
if (phrasesList.length > 0 && this.displayedTexts().length === 0) {
|
||||
this.startNextText();
|
||||
this.scheduleNextText(0); // Start immediately
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.phrases().length) {
|
||||
this.startTextRotation();
|
||||
}
|
||||
// Component initialization handled in constructor effect
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopTextRotation();
|
||||
this.clearTypewriterTimeouts();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private startTextRotation(): void {
|
||||
// Start with the first text
|
||||
this.startNextText();
|
||||
}
|
||||
private cleanup(): void {
|
||||
// Clear all timeouts
|
||||
this.activeTimeouts.forEach((id) => window.clearTimeout(id));
|
||||
this.activeTimeouts.clear();
|
||||
|
||||
private stopTextRotation(): void {
|
||||
if (this.nextTextTimeout) {
|
||||
clearTimeout(this.nextTextTimeout);
|
||||
window.clearTimeout(this.nextTextTimeout);
|
||||
this.nextTextTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private clearTypewriterTimeouts(): void {
|
||||
this.typewriterTimeouts.forEach((id) => window.clearTimeout(id));
|
||||
this.typewriterTimeouts = [];
|
||||
private scheduleNextText(delay: number = this.interval()): void {
|
||||
if (this.nextTextTimeout) {
|
||||
window.clearTimeout(this.nextTextTimeout);
|
||||
}
|
||||
|
||||
this.nextTextTimeout = window.setTimeout(() => {
|
||||
this.startNextText();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private startNextText(): void {
|
||||
// If we're already typing, don't start a new one
|
||||
if (this.isTypingInProgress()) return;
|
||||
|
||||
const phrasesList = this.phrases();
|
||||
if (phrasesList.length === 0) return;
|
||||
|
||||
// Get the next phrase to display
|
||||
const index = this.phraseIndex();
|
||||
const nextText = phrasesList[index];
|
||||
const currentIndex = this.phraseIndex();
|
||||
const nextText = phrasesList[currentIndex];
|
||||
|
||||
// Update the index for the next time
|
||||
this.phraseIndex.update((idx) => (idx + 1) % phrasesList.length);
|
||||
|
||||
// Create a new text item
|
||||
// Create immutable text item
|
||||
const newItem: TextItem = {
|
||||
id: this.nextId(),
|
||||
text: nextText,
|
||||
@@ -137,44 +161,40 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
|
||||
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.isTypingInProgress.set(true);
|
||||
|
||||
// Add the new text to the displayed texts list
|
||||
// Update displayed texts with proper cleanup
|
||||
this.displayedTexts.update((texts) => {
|
||||
// If we already have the maximum number of texts, remove the oldest one
|
||||
if (texts.length >= this.maxDisplayedTexts()) {
|
||||
return [...texts.slice(1), newItem];
|
||||
const maxTexts = this.maxDisplayedTexts();
|
||||
if (texts.length >= maxTexts) {
|
||||
return [...texts.slice(texts.length - maxTexts + 1), newItem];
|
||||
}
|
||||
|
||||
// Otherwise, just add the new text
|
||||
return [...texts, newItem];
|
||||
});
|
||||
|
||||
// Set typing in progress
|
||||
this.isTypingInProgress.set(true);
|
||||
|
||||
// Start the typewriter effect
|
||||
this.typeText(newItem.id);
|
||||
// Start typing animation
|
||||
this.animateText(newItem.id);
|
||||
}
|
||||
|
||||
private typeText(textId: number): void {
|
||||
private animateText(textId: number): void {
|
||||
const texts = this.displayedTexts();
|
||||
const textIndex = texts.findIndex((t) => t.id === textId);
|
||||
|
||||
if (textIndex === -1) return;
|
||||
|
||||
const textItem = texts[textIndex];
|
||||
const fullText = textItem.text;
|
||||
const currentLength = textItem.displayed.length;
|
||||
const { text, displayed } = textItem;
|
||||
const nextCharIndex = displayed.length;
|
||||
|
||||
if (currentLength < fullText.length) {
|
||||
// Add one character
|
||||
const newDisplayed = fullText.substring(0, currentLength + 1);
|
||||
if (nextCharIndex < text.length) {
|
||||
// Update displayed text
|
||||
const newDisplayed = text.substring(0, nextCharIndex + 1);
|
||||
|
||||
// Update the text
|
||||
this.displayedTexts.update((texts) => {
|
||||
const updatedTexts = [...texts];
|
||||
this.displayedTexts.update((currentTexts) => {
|
||||
const updatedTexts = [...currentTexts];
|
||||
updatedTexts[textIndex] = {
|
||||
...textItem,
|
||||
displayed: newDisplayed,
|
||||
@@ -182,63 +202,76 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
|
||||
return updatedTexts;
|
||||
});
|
||||
|
||||
// Hectic typing effect with much more randomization
|
||||
// Occasionally pause, sometimes type very fast, sometimes slower
|
||||
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;
|
||||
}
|
||||
|
||||
// Schedule next character with dynamic timing
|
||||
const delay = this.calculateTypingDelay();
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
this.typeText(textId);
|
||||
}, nextDelay);
|
||||
this.animateText(textId);
|
||||
}, delay);
|
||||
|
||||
this.typewriterTimeouts.push(timeoutId);
|
||||
this.activeTimeouts.add(timeoutId);
|
||||
} else {
|
||||
// Text is complete
|
||||
this.displayedTexts.update((texts) => {
|
||||
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());
|
||||
// Text complete
|
||||
this.completeText(textIndex, textItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to track items
|
||||
trackByTextId(index: number, item: TextItem): number {
|
||||
return item.id;
|
||||
private calculateTypingDelay(): number {
|
||||
const random = Math.random();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
<section class="">
|
||||
<!-- 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>
|
||||
<section class="h-screen relative">
|
||||
<app-holo-video-container
|
||||
containerClasses="absolute inset-0 w-full h-full"
|
||||
videoSrc="cyber_hands.mp4" />
|
||||
|
||||
<!-- Display Text Section -->
|
||||
<article class="flex-1 flex items-center justify-center checkered-bg-dark pt-4 sm:pt-8 md:pt-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>
|
||||
<!-- Changed from justify-center to justify-start with top padding -->
|
||||
<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">
|
||||
|
||||
<div class="mb-8 sm:mb-12 md:mb-16 lg:mb-20 xl:mb-24 2xl:mb-32">
|
||||
<h1 class="font-terminal-nier scramble-text-glow text-cyan-200
|
||||
text-6xl sm:text-7xl md:text-8xl lg:text-9xl xl:text-[10rem] 2xl:text-[12rem]
|
||||
text-center
|
||||
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>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { HoloVideoContainerComponent } from '../../shared/ui/holo-video-containe
|
||||
})
|
||||
export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
displayText: string = '';
|
||||
nameText: string = '';
|
||||
|
||||
private messageQueue: string[] = [
|
||||
'Web Developer',
|
||||
@@ -17,8 +18,11 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
];
|
||||
|
||||
private currentMessage: string = '';
|
||||
private currentName: string = 'ADAM\nBENYEKKOU';
|
||||
private isGlitching: boolean = false;
|
||||
private isNameGlitching: boolean = false;
|
||||
private frameRequest: number | null = null;
|
||||
private nameFrameRequest: number | null = null;
|
||||
private processTimeout: any = null;
|
||||
private isInitialMount: boolean = true;
|
||||
|
||||
@@ -34,12 +38,57 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
if (this.frameRequest) {
|
||||
cancelAnimationFrame(this.frameRequest);
|
||||
}
|
||||
if (this.nameFrameRequest) {
|
||||
cancelAnimationFrame(this.nameFrameRequest);
|
||||
}
|
||||
if (this.processTimeout) {
|
||||
clearTimeout(this.processTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
let currentIndex = 0;
|
||||
|
||||
@@ -52,7 +101,7 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
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);
|
||||
for (let i = 0; i < scrambleLength; i++) {
|
||||
output += Math.random() > 0.5 ? '0' : '1';
|
||||
@@ -62,12 +111,12 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (currentIndex < firstMessage.length) {
|
||||
currentIndex++;
|
||||
setTimeout(animateNextLetter, 35 + Math.random() * 25); // Much faster: 35-60ms
|
||||
setTimeout(animateNextLetter, 35 + Math.random() * 25);
|
||||
} 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.messageQueue.shift();
|
||||
this.isInitialMount = false;
|
||||
|
||||
// Start the regular cycle after a short pause
|
||||
@@ -95,7 +144,7 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.processTimeout = setTimeout(() => {
|
||||
this.processQueue();
|
||||
}, 6000); // Reduced from 10000 to 6000 (faster rotation)
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
private startScrambleAnimation(nextMessage: string): void {
|
||||
@@ -146,7 +195,6 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
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'))
|
||||
@@ -157,10 +205,8 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
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';
|
||||
}
|
||||
@@ -173,11 +219,9 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
|
||||
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';
|
||||
@@ -192,7 +236,64 @@ export class HeroDisplayComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
[interval]="2500"
|
||||
/>
|
||||
</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">
|
||||
<app-header-nav-links />
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-2 py-3">
|
||||
<div class="flex flex-wrap justify-center gap-2 py-3 border-b border-nier-accent/30">
|
||||
<app-header-contact-links />
|
||||
</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>
|
||||
|
||||
<!-- Tablet Layout (768px - 1023px) -->
|
||||
@@ -136,4 +136,3 @@
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -10,20 +10,6 @@
|
||||
</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>
|
||||
|
||||
@@ -22,7 +22,7 @@ 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
|
||||
@Input() containerClasses: string = 'w-full h-96';
|
||||
|
||||
isLoading = signal(false);
|
||||
|
||||
@@ -38,19 +38,16 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
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_WIDTH = 320;
|
||||
readonly GRID_HEIGHT = 180;
|
||||
readonly POINTS_COUNT = this.GRID_WIDTH * this.GRID_HEIGHT;
|
||||
|
||||
// Mouse and effects
|
||||
// Enhanced mouse and effects
|
||||
private mouseX = 0;
|
||||
private mouseY = 0;
|
||||
private isMouseOverCanvas = false;
|
||||
@@ -58,10 +55,21 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
private glitchIntensity = 0;
|
||||
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() {
|
||||
effect(() => {
|
||||
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 {
|
||||
this.initThreeJS();
|
||||
this.createPointCloud();
|
||||
this.createSideDecorations();
|
||||
this.setupVideo();
|
||||
this.setupEventListeners();
|
||||
this.setupResizeObserver();
|
||||
this.animate();
|
||||
this.onWindowResize(); // Initial resize
|
||||
this.onWindowResize();
|
||||
|
||||
if (this.videoSrc) {
|
||||
this.loadVideoFromSrc(this.videoSrc);
|
||||
@@ -88,6 +95,14 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
if (this.resizeObserver) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -96,13 +111,16 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
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);
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
antialias: true,
|
||||
powerPreference: 'high-performance',
|
||||
});
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
}
|
||||
|
||||
private setupResizeObserver(): void {
|
||||
@@ -117,40 +135,25 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
const gridHeight = this.GRID_HEIGHT * 12;
|
||||
const gridWidth = this.GRID_WIDTH * 12;
|
||||
|
||||
// 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')
|
||||
@@ -172,8 +175,8 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
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
|
||||
const posX = (x - this.GRID_WIDTH / 2) * 12;
|
||||
const posY = -(y - this.GRID_HEIGHT / 2) * 12;
|
||||
|
||||
positions[index * 3] = posX;
|
||||
positions[index * 3 + 1] = posY;
|
||||
@@ -183,7 +186,12 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
originalPositions[index * 3 + 1] = posY;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
@@ -199,7 +207,7 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.material = new THREE.PointsMaterial({
|
||||
size: 8.0, // Increased from 5.0 to 8.0
|
||||
size: 6.0,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 1.0,
|
||||
@@ -215,13 +223,14 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createGlowLayers(): void {
|
||||
// Enhanced glow layers for stronger effect
|
||||
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
|
||||
size: 6.0 * (1 + layer * 0.6), // Larger glow sizes
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.3 / layer,
|
||||
opacity: 0.4 / layer, // Increased opacity
|
||||
alphaTest: 0.1,
|
||||
depthWrite: false,
|
||||
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 {
|
||||
this.video = document.createElement('video');
|
||||
this.video.width = this.GRID_WIDTH;
|
||||
@@ -406,17 +276,18 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
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 wave = Math.sin(distance * 0.08 + time * 0.04) * 0.5 + 0.5;
|
||||
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(
|
||||
0,
|
||||
Math.min(255, (wave + noise * 0.3) * 255),
|
||||
Math.min(255, (wave + noise * 0.4) * 255),
|
||||
);
|
||||
|
||||
data[index] = intensity;
|
||||
data[index + 1] = intensity * 0.8;
|
||||
data[index + 2] = intensity * 0.6;
|
||||
data[index + 1] = intensity * 0.85;
|
||||
data[index + 2] = intensity * 0.7;
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
@@ -435,10 +306,11 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
private updatePointCloud(): void {
|
||||
if (!this.video.videoWidth || !this.video.videoHeight) return;
|
||||
|
||||
// Enhanced glitch system
|
||||
this.glitchTime++;
|
||||
if (Math.random() < 0.003) {
|
||||
this.glitchIntensity = Math.random() * 0.7 + 0.3;
|
||||
setTimeout(() => (this.glitchIntensity = 0), Math.random() * 1000 + 500);
|
||||
if (Math.random() < 0.008) {
|
||||
this.glitchIntensity = Math.random() * 1.2 + 0.6;
|
||||
setTimeout(() => (this.glitchIntensity = 0), Math.random() * 500 + 150);
|
||||
}
|
||||
|
||||
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 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 =
|
||||
-((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;
|
||||
for (let y = 0; y < this.GRID_HEIGHT; y++) {
|
||||
@@ -491,40 +376,99 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
Math.pow(originalY - mouseWorldY, 2),
|
||||
);
|
||||
|
||||
// ENHANCED: Much stronger scatter effect with persistent intensity
|
||||
let scatterX = 0,
|
||||
scatterY = 0,
|
||||
scatterZ = 0;
|
||||
if (this.isMouseOverCanvas && distanceToMouse < 50) {
|
||||
const scatterFactor = 1 - distanceToMouse / 50;
|
||||
const scatterIntensity = scatterFactor * 20;
|
||||
let currentScatterIntensity = this.scatterIntensityMap[index];
|
||||
|
||||
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 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;
|
||||
// Explosive scatter with dramatic randomness
|
||||
const randomFactor = 0.6 + Math.random() * 0.8; // Increased randomness
|
||||
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,
|
||||
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;
|
||||
glitchY = 0,
|
||||
glitchZ = 0;
|
||||
if (isGlitching) {
|
||||
glitchX = (Math.random() - 0.5) * this.glitchIntensity * 35;
|
||||
glitchY = (Math.random() - 0.5) * this.glitchIntensity * 35;
|
||||
glitchZ = (Math.random() - 0.5) * this.glitchIntensity * 30;
|
||||
}
|
||||
|
||||
positions[index * 3] = originalX + scatterX + glitchX;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
@@ -534,33 +478,68 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
this.updateGlowLayers();
|
||||
}
|
||||
|
||||
private applyHolographicColors(
|
||||
private applyEnhancedHolographicColors(
|
||||
colors: Float32Array,
|
||||
index: number,
|
||||
brightness: number,
|
||||
distanceToMouse: number,
|
||||
scatterIntensity: number,
|
||||
isGlitching: boolean,
|
||||
): 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];
|
||||
else if (brightness > 180) finalColor = [0.9, 0.9, 0.9];
|
||||
// Base color mapping
|
||||
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 > 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];
|
||||
else if (brightness > 100) finalColor = [0.55, 0.55, 0.55];
|
||||
else if (brightness > 60) finalColor = [0.4, 0.4, 0.4];
|
||||
else finalColor = [0.25, 0.25, 0.25];
|
||||
|
||||
// Add subtle color variations
|
||||
const variation = (index % 7) / 20;
|
||||
finalColor[0] += variation * 0.1;
|
||||
finalColor[1] += variation * 0.05;
|
||||
finalColor[2] += variation * 0.15;
|
||||
// Subtle color variations
|
||||
const variation = (index % 5) / 25;
|
||||
finalColor[0] += variation * 0.08;
|
||||
finalColor[1] += variation * 0.04;
|
||||
finalColor[2] += variation * 0.12;
|
||||
|
||||
// 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);
|
||||
// DRAMATIC WHITE GLOW for scattered points
|
||||
if (scatterIntensity > 0) {
|
||||
const glowFactor =
|
||||
(scatterIntensity / this.SCATTER_STRENGTH) * this.GLOW_INTENSITY;
|
||||
|
||||
// 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];
|
||||
@@ -569,27 +548,35 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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) => {
|
||||
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;
|
||||
|
||||
// Copy positions with slight offset for depth
|
||||
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;
|
||||
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) {
|
||||
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;
|
||||
// Amplify white/bright colors dramatically in glow layers
|
||||
const brightnessFactor =
|
||||
(mainColors[i] + mainColors[i + 1] + mainColors[i + 2]) / 3;
|
||||
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;
|
||||
@@ -610,19 +597,15 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
|
||||
'mouseenter',
|
||||
() => (this.isMouseOverCanvas = true),
|
||||
);
|
||||
canvas.addEventListener(
|
||||
'mouseleave',
|
||||
() => (this.isMouseOverCanvas = false),
|
||||
);
|
||||
|
||||
// Removed wheel event listener - no more zooming
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
this.isMouseOverCanvas = false;
|
||||
// Don't reset scatter immediately - let it decay naturally
|
||||
});
|
||||
}
|
||||
|
||||
private animate(): void {
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
const time = Date.now();
|
||||
this.updatePointCloud();
|
||||
this.animateSideElements(time);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user