mirror of
https://github.com/adam-benyekkou/my_portfolio.git
synced 2026-01-15 20:20:09 +00:00
Added scramble animation on mount of section title component
This commit is contained in:
@@ -1,19 +1,27 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<p
|
<!-- First shadow layer (light) -->
|
||||||
class="absolute text-6xl font-noto-jp text-white opacity-50"
|
<p
|
||||||
style="top: 1.5rem; left: 1.5rem; transform: translate(1px, 1px)"
|
#shadow1
|
||||||
>
|
class="absolute text-6xl font-noto-jp text-white opacity-50"
|
||||||
{{title()}}
|
style="top: 1.5rem; left: 1.5rem; transform: translate(1px, 1px)"
|
||||||
</p>
|
>
|
||||||
<!-- Second shadow layer (dark) -->
|
{{title()}}
|
||||||
<p
|
</p>
|
||||||
class="absolute text-6xl font-noto-jp text-black opacity-15"
|
|
||||||
style="top: 1.5rem; left: 1.5rem; transform: translate(5px, 10px)"
|
<!-- Second shadow layer (dark) -->
|
||||||
>
|
<p
|
||||||
{{title()}}
|
#shadow2
|
||||||
</p>
|
class="absolute text-6xl font-noto-jp text-black opacity-15"
|
||||||
<!-- Main text on top -->
|
style="top: 1.5rem; left: 1.5rem; transform: translate(5px, 10px)"
|
||||||
<p class="relative text-6xl font-noto-jp text-nier-dark z-10">
|
>
|
||||||
{{title()}}
|
{{title()}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Main text on top -->
|
||||||
|
<p
|
||||||
|
#mainText
|
||||||
|
class="relative text-6xl font-noto-jp text-nier-dark z-10"
|
||||||
|
>
|
||||||
|
{{title()}}
|
||||||
|
</p>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Component, input } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-section-title',
|
selector: 'app-section-title',
|
||||||
@@ -6,6 +13,101 @@ import { Component, input } from '@angular/core';
|
|||||||
templateUrl: './section-title.component.html',
|
templateUrl: './section-title.component.html',
|
||||||
styleUrl: './section-title.component.css',
|
styleUrl: './section-title.component.css',
|
||||||
})
|
})
|
||||||
export class SectionTitleComponent {
|
export class SectionTitleComponent implements OnInit, OnDestroy {
|
||||||
|
@ViewChild('mainText', { static: true })
|
||||||
|
mainTextRef!: ElementRef<HTMLElement>;
|
||||||
|
@ViewChild('shadow1', { static: true }) shadow1Ref!: ElementRef<HTMLElement>;
|
||||||
|
@ViewChild('shadow2', { static: true }) shadow2Ref!: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
title = input<string>();
|
title = input<string>();
|
||||||
|
|
||||||
|
private animationFrame?: number;
|
||||||
|
private timeoutIds: number[] = [];
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Start animation after component initializes
|
||||||
|
setTimeout(() => this.startScrambleAnimation(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame);
|
||||||
|
}
|
||||||
|
this.timeoutIds.forEach((id) => clearTimeout(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private startScrambleAnimation() {
|
||||||
|
const titleText = this.title() || '';
|
||||||
|
if (!titleText) return;
|
||||||
|
|
||||||
|
const duration = 2000; // Total animation duration
|
||||||
|
const letterDelay = 80; // Delay between each letter reveal
|
||||||
|
|
||||||
|
this.simultaneousScrambleAndReveal(titleText, duration, letterDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private simultaneousScrambleAndReveal(
|
||||||
|
targetText: string,
|
||||||
|
totalDuration: number,
|
||||||
|
letterDelay: number,
|
||||||
|
) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const scrambleChars = '01';
|
||||||
|
const revealedLetters = new Array(targetText.length).fill(false);
|
||||||
|
const letterBinaryStates = new Array(targetText.length)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => (Math.random() > 0.5 ? '0' : '1'));
|
||||||
|
|
||||||
|
// Schedule letter reveals
|
||||||
|
for (let i = 0; i < targetText.length; i++) {
|
||||||
|
if (targetText[i] !== ' ') {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
revealedLetters[i] = true;
|
||||||
|
}, i * letterDelay);
|
||||||
|
this.timeoutIds.push(timeoutId);
|
||||||
|
} else {
|
||||||
|
revealedLetters[i] = true; // Spaces are always "revealed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (elapsed < totalDuration) {
|
||||||
|
let displayText = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < targetText.length; i++) {
|
||||||
|
if (targetText[i] === ' ') {
|
||||||
|
displayText += ' ';
|
||||||
|
} else if (revealedLetters[i]) {
|
||||||
|
displayText += targetText[i];
|
||||||
|
} else {
|
||||||
|
// Each position gets its own random binary that changes
|
||||||
|
letterBinaryStates[i] = Math.random() > 0.5 ? '0' : '1';
|
||||||
|
displayText += letterBinaryStates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTextContent(displayText);
|
||||||
|
this.animationFrame = requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
// Final state - show complete text
|
||||||
|
this.updateTextContent(targetText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTextContent(text: string) {
|
||||||
|
if (this.mainTextRef?.nativeElement) {
|
||||||
|
this.mainTextRef.nativeElement.textContent = text;
|
||||||
|
}
|
||||||
|
if (this.shadow1Ref?.nativeElement) {
|
||||||
|
this.shadow1Ref.nativeElement.textContent = text;
|
||||||
|
}
|
||||||
|
if (this.shadow2Ref?.nativeElement) {
|
||||||
|
this.shadow2Ref.nativeElement.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user