From 76a19d30b68380cc346ceff911b10ca536384c4b Mon Sep 17 00:00:00 2001 From: AdamBtech <60339324+AdamBtech@users.noreply.github.com> Date: Tue, 20 May 2025 17:17:47 +0200 Subject: [PATCH] Added Logo and Header Text Animate Animation Typewriter and Glitch --- .../header-logo/header-logo.component.css | 264 ++++++++++++++++++ .../header-logo/header-logo.component.html | 6 +- .../header-nav-links.component.html | 10 +- .../header-text-animate-section.component.css | 79 ++++++ ...header-text-animate-section.component.html | 14 +- .../header-text-animate-section.component.ts | 239 +++++++++++++++- .../components/header/header.component.html | 38 ++- src/app/shared/ui/button/button.component.css | 37 ++- .../shared/ui/button/button.component.html | 4 +- src/index.html | 2 +- 10 files changed, 672 insertions(+), 21 deletions(-) diff --git a/src/app/features/header-display/header-logo/header-logo.component.css b/src/app/features/header-display/header-logo/header-logo.component.css index e69de29..fd8d3ea 100644 --- a/src/app/features/header-display/header-logo/header-logo.component.css +++ b/src/app/features/header-display/header-logo/header-logo.component.css @@ -0,0 +1,264 @@ +.nier-logo-container { + position: relative; + display: inline-block; + padding: 0.25rem; + overflow: hidden; +} + +.nier-logo { + position: relative; + color: var(--nier-dark, #2C2A21); /* Using nier-dark color with fallback */ + text-shadow: 0 0 2px rgba(44, 42, 33, 0.8); + animation: nier-fade-in 2.5s ease-out forwards, nier-subtle-pulse 4s 2.5s infinite; +} + +.nier-logo::before { + content: attr(data-text); + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + text-shadow: 0 0 3px var(--nier-dark, #2C2A21); + opacity: 0; + animation: nier-glitch 6s 3s infinite; +} + +.nier-logo::after { + content: attr(data-text); + position: absolute; + left: -2px; + top: 0; + width: 100%; + height: 100%; + text-shadow: -1px 0 1px rgba(44, 42, 33, 0.6); + opacity: 0; + animation: nier-glitch-2 5s 3s infinite; +} + +.nier-scan-line { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + to bottom, + transparent 0%, + rgba(44, 42, 33, 0.05) 50%, + transparent 100% + ); + animation: nier-scan 3s linear infinite; + z-index: 2; + pointer-events: none; +} + +.nier-blocks { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; +} + +.nier-blocks::before, +.nier-blocks::after { + content: ""; + position: absolute; + background: rgba(44, 42, 33, 0.1); + width: 10px; + height: 6px; + animation: nier-blocks 10s linear infinite; +} + +.nier-blocks::before { + top: 20%; + left: 10%; + animation-delay: 1s; +} + +.nier-blocks::after { + bottom: 40%; + right: 10%; + width: 15px; + height: 4px; + animation-delay: 3s; +} + +/* Animations */ +@keyframes nier-fade-in { + 0% { + opacity: 0; + clip-path: inset(0 100% 0 0); + } + 20% { + opacity: 0.3; + clip-path: inset(0 80% 0 0); + } + 40% { + opacity: 0.5; + clip-path: inset(0 60% 0 0); + } + 60% { + opacity: 0.7; + clip-path: inset(0 40% 0 0); + } + 80% { + opacity: 0.9; + clip-path: inset(0 20% 0 0); + } + 95% { + opacity: 1; + clip-path: inset(0 5% 0 0); + } + 100% { + opacity: 1; + clip-path: inset(0 0 0 0); + } +} + +@keyframes nier-subtle-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.9; + } +} + +@keyframes nier-glitch { + 0%, 100% { + opacity: 0; + transform: translateX(0); + } + 10.5% { + opacity: 0.5; + transform: translateX(3px); + } + 11% { + opacity: 0; + transform: translateX(0); + } + 29.5% { + opacity: 0; + transform: translateX(0); + } + 30% { + opacity: 0.4; + transform: translateX(-3px); + } + 30.5% { + opacity: 0; + transform: translateX(0); + } + 80% { + opacity: 0; + transform: translateX(0); + } + 80.5% { + opacity: 0.6; + transform: translateX(5px); + } + 81% { + opacity: 0; + transform: translateX(0); + } +} + +@keyframes nier-glitch-2 { + 0%, 100% { + opacity: 0; + transform: translateX(0); + } + 10.5% { + opacity: 0; + transform: translateX(0); + } + 11% { + opacity: 0.4; + transform: translateX(-2px); + } + 11.5% { + opacity: 0; + transform: translateX(0); + } + 50% { + opacity: 0; + transform: translateX(0); + } + 50.5% { + opacity: 0.4; + transform: translateX(2px); + } + 51% { + opacity: 0; + transform: translateX(0); + } +} + +@keyframes nier-scan { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(100%); + } +} + +@keyframes nier-blocks { + 0% { + opacity: 0; + transform: translateY(0) translateX(0); + } + 10% { + opacity: 0.8; + } + 30% { + opacity: 0.6; + transform: translateY(20px) translateX(10px); + } + 50% { + opacity: 0.4; + transform: translateY(40px) translateX(20px); + } + 70% { + opacity: 0.2; + transform: translateY(60px) translateX(30px); + } + 100% { + opacity: 0; + transform: translateY(100px) translateX(40px); + } +} + +/* Add this for the small border and interface details */ +.nier-logo-container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 1px solid rgba(44, 42, 33, 0.3); + pointer-events: none; +} + +/* Optional: Add small interface dots in the corner */ +.nier-logo-container::after { + content: ""; + position: absolute; + top: 5px; + right: 5px; + width: 3px; + height: 3px; + background-color: rgba(44, 42, 33, 0.6); + box-shadow: + -6px 0 0 rgba(44, 42, 33, 0.6), + -12px 0 0 rgba(44, 42, 33, 0.6), + 0 6px 0 rgba(44, 42, 33, 0.6), + -6px 6px 0 rgba(44, 42, 33, 0.6), + -12px 6px 0 rgba(44, 42, 33, 0.6); + pointer-events: none; +} diff --git a/src/app/features/header-display/header-logo/header-logo.component.html b/src/app/features/header-display/header-logo/header-logo.component.html index 9064105..8661da8 100644 --- a/src/app/features/header-display/header-logo/header-logo.component.html +++ b/src/app/features/header-display/header-logo/header-logo.component.html @@ -1 +1,5 @@ -

AB

+
+ +
+
+
diff --git a/src/app/features/header-display/header-nav-links/header-nav-links.component.html b/src/app/features/header-display/header-nav-links/header-nav-links.component.html index c814295..ee879a7 100644 --- a/src/app/features/header-display/header-nav-links/header-nav-links.component.html +++ b/src/app/features/header-display/header-nav-links/header-nav-links.component.html @@ -1,8 +1,8 @@ diff --git a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.css b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.css index e69de29..040bae9 100644 --- a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.css +++ b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.css @@ -0,0 +1,79 @@ +.typewriter-container { + display: flex; + flex-direction: column; + gap: 10px; + font-family: 'Noto Sans JP', monospace; +} + +.typewriter-item { + opacity: 0.9; + transition: opacity 0.3s ease; + position: relative; +} + +.text-scroller-text { + display: inline-block; + letter-spacing: 0.08em; +} + +.cursor { + display: inline-block; + animation: blink-and-glitch 1.2s infinite; + position: relative; +} + +@keyframes blink-and-glitch { + 0%, 100% { + opacity: 1; + transform: translateY(0); + } + 40% { + opacity: 1; + } + 50% { + opacity: 0; + } + 75% { + opacity: 1; + transform: translateY(0); + } + 76% { + opacity: 1; + transform: translateY(-3px); + } + 78% { + opacity: 0.5; + transform: translateY(2px); + } + 80% { + opacity: 1; + transform: translateY(0); + } +} + +/* Occasional text flicker effect */ +.typewriter-item:nth-child(2n+1) .text-scroller-text { + animation: text-flicker 4s infinite; + animation-delay: calc(var(--i, 0) * 1s); +} + +@keyframes text-flicker { + 0%, 100% { + opacity: 0.9; + } + 92% { + opacity: 0.9; + } + 92.5% { + opacity: 0.2; + } + 92.8% { + opacity: 0.9; + } + 93.5% { + opacity: 0.2; + } + 94% { + opacity: 0.9; + } +} diff --git a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.html b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.html index 68badb7..6b7ef05 100644 --- a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.html +++ b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.html @@ -1 +1,13 @@ -

header-text-animate-section works!

+ +
+ @for (item of displayedTexts(); track item.id) { +
+
+ {{ item.displayed }} + @if (item.isTyping) { + | + } +
+
+ } +
diff --git a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.ts b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.ts index 604fafe..15a25ba 100644 --- a/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.ts +++ b/src/app/features/header-display/header-text-animate-section/header-text-animate-section.component.ts @@ -1,11 +1,244 @@ -import { Component } from '@angular/core'; +// header-text-animate-section.component.ts +import { + Component, + effect, + input, + OnDestroy, + OnInit, + signal, +} from '@angular/core'; +import { + trigger, + state, + style, + transition, + animate, +} from '@angular/animations'; + +interface TextItem { + id: number; + text: string; + displayed: string; + isTyping: boolean; + isComplete: boolean; +} @Component({ selector: 'app-header-text-animate-section', + standalone: true, imports: [], templateUrl: './header-text-animate-section.component.html', - styleUrl: './header-text-animate-section.component.css' + styleUrl: './header-text-animate-section.component.css', + animations: [ + trigger('textChange', [ + state( + 'visible', + style({ + opacity: 1, + transform: 'translateY(0)', + }), + ), + state( + 'hidden', + style({ + opacity: 0, + transform: 'translateY(20px)', + }), + ), + transition('visible => hidden', [animate('0.5s ease-out')]), + transition('hidden => visible', [animate('0.5s ease-in')]), + ]), + ], }) -export class HeaderTextAnimateSectionComponent { +export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy { + phrases = input([ + 'Uploading guinea pig consciousness to the cloud...', + 'Error: Sarcasm module overloaded. Rebooting...', + 'Downloading personalities... 404: Personality not found.', + 'Coffee.exe has stopped working. Attempting to reboot human...', + 'Converting existential dread to binary...', + 'Hacking the mainframe with a rubber duck...', + "Neural implant reports: You're still not cool enough...", + '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...', + ]); + interval = input(3000); // Default interval of 3 seconds + typingSpeed = input(30); // Time in ms between each character (faster than before) + maxDisplayedTexts = input(4); // Maximum number of texts to display + + // Signals for internal state + displayedTexts = signal([]); + nextId = signal(0); + phraseIndex = signal(0); + isTypingInProgress = signal(false); + + private typewriterTimeouts: number[] = []; + private nextTextTimeout: any; + + constructor() { + // Use effect to react to changes in phrases + effect(() => { + const phrasesList = this.phrases(); + if (phrasesList.length > 0 && this.displayedTexts().length === 0) { + this.startNextText(); + } + }); + } + + ngOnInit(): void { + if (this.phrases().length) { + this.startTextRotation(); + } + } + + ngOnDestroy(): void { + this.stopTextRotation(); + this.clearTypewriterTimeouts(); + } + + private startTextRotation(): void { + // Start with the first text + this.startNextText(); + } + + private stopTextRotation(): void { + if (this.nextTextTimeout) { + clearTimeout(this.nextTextTimeout); + } + } + + private clearTypewriterTimeouts(): void { + this.typewriterTimeouts.forEach((id) => window.clearTimeout(id)); + this.typewriterTimeouts = []; + } + + 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]; + + // Update the index for the next time + this.phraseIndex.update((idx) => (idx + 1) % phrasesList.length); + + // Create a new text item + const newItem: TextItem = { + id: this.nextId(), + text: nextText, + displayed: '', + isTyping: true, + isComplete: false, + }; + + // Update the nextId for the next time + this.nextId.update((id) => id + 1); + + // Add the new text to the displayed texts list + 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]; + } + + // 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); + } + + private typeText(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; + + if (currentLength < fullText.length) { + // Add one character + const newDisplayed = fullText.substring(0, currentLength + 1); + + // Update the text + this.displayedTexts.update((texts) => { + const updatedTexts = [...texts]; + updatedTexts[textIndex] = { + ...textItem, + displayed: newDisplayed, + }; + 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; + } + + const timeoutId = window.setTimeout(() => { + this.typeText(textId); + }, nextDelay); + + this.typewriterTimeouts.push(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()); + } + } + + // Helper method to track items + trackByTextId(index: number, item: TextItem): number { + return item.id; + } } diff --git a/src/app/layout/components/header/header.component.html b/src/app/layout/components/header/header.component.html index b74ce07..c8cb426 100644 --- a/src/app/layout/components/header/header.component.html +++ b/src/app/layout/components/header/header.component.html @@ -1,8 +1,36 @@
-
- - - - +
+ + + +
diff --git a/src/app/shared/ui/button/button.component.css b/src/app/shared/ui/button/button.component.css index 091111c..04007b4 100644 --- a/src/app/shared/ui/button/button.component.css +++ b/src/app/shared/ui/button/button.component.css @@ -1,4 +1,35 @@ -.button-hover:hover { - background-color: var(--color-nier-dark); - color: var(--color-nier-text-light); +.button-custom { + cursor: pointer; + background-color: var(--color-nier-mid); + color: var(--color-nier-dark); + position: relative; + border: 1px solid transparent; + border-left: none; + border-right: none; + overflow: hidden; +} + +.button-custom::before { + content: ""; + position: absolute; + left: 0; + top: 3px; /* Space from top border */ + bottom: 3px; /* Space from bottom border */ + width: 0; + height: auto; /* This makes it respect top and bottom values */ + background-color: var(--color-nier-dark); + transition: width 0.2s ease; + z-index: 0; +} + +.button-custom:hover, +.button-custom:focus { + color: var(--color-nier-text-light); + background-color: transparent; + border-color: var(--color-nier-dark); +} + +.button-custom:hover::before, +.button-custom:focus::before { + width: 100%; } diff --git a/src/app/shared/ui/button/button.component.html b/src/app/shared/ui/button/button.component.html index 7de1e8f..acd553a 100644 --- a/src/app/shared/ui/button/button.component.html +++ b/src/app/shared/ui/button/button.component.html @@ -1,6 +1,6 @@ diff --git a/src/index.html b/src/index.html index 68e2475..7f96f54 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - AngularPortfolio + Adam Benyekkou Portfolio