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