Added Logo and Header Text Animate Animation Typewriter and Glitch

This commit is contained in:
AdamBtech
2025-05-20 17:17:47 +02:00
parent 83cc2aec1c
commit 76a19d30b6
10 changed files with 672 additions and 21 deletions

View File

@@ -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;
}

View File

@@ -1 +1,5 @@
<p class="font-terminal-nier text-8xl">AB</p>
<div class="nier-logo-container">
<p class="font-terminal-nier text-8xl nier-logo" data-text="AB">AB</p>
<div class="nier-scan-line"></div>
<div class="nier-blocks"></div>
</div>

View File

@@ -1,8 +1,8 @@
<nav class="flex items-center justify-center">
<section class="grid grid-cols-2 gap-4 mt-4">
<app-button class="flex items-center" label="Neural Profile" (click)="onClick()"/>
<app-button class="flex items-center" label="Execute//Directory" (click)="onClick()"/>
<app-button class="flex items-center" label="Operative History" (click)="onClick()"/>
<app-button class="flex items-center" label="Transmission Link" (click)="onClick()"/>
<section class="grid grid-cols-2 gap-x-4 gap-y-5 my-4">
<app-button class="flex items-center h-10" label="Neural Profile" (click)="onClick()"/>
<app-button class="flex items-center h-10" label="Execute//Directory" (click)="onClick()"/>
<app-button class="flex items-center h-10" label="Operative History" (click)="onClick()"/>
<app-button class="flex items-center h-10" label="Transmission Link" (click)="onClick()"/>
</section>
</nav>

View File

@@ -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;
}
}

View File

@@ -1 +1,13 @@
<p>header-text-animate-section works!</p>
<div class="typewriter-container">
@for (item of displayedTexts(); track item.id) {
<div class="typewriter-item">
<div class="text-scroller-text font-noto-jp text-sm">
{{ item.displayed }}
@if (item.isTyping) {
<span class="cursor">|</span>
}
</div>
</div>
}
</div>

View File

@@ -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<string[]>([
'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<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
// Signals for internal state
displayedTexts = signal<TextItem[]>([]);
nextId = signal<number>(0);
phraseIndex = signal<number>(0);
isTypingInProgress = signal<boolean>(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;
}
}

View File

@@ -1,8 +1,36 @@
<header>
<section class="grid grid-cols-[1fr_5fr_4fr_4fr] gap-1 bg-nier-bg font-terminal text-nier-dark divide-x divide-nier-accent/30 border border-nier-accent h-32">
<app-header-logo />
<app-header-text-animate-section />
<app-header-nav-links />
<app-header-contact-links />
<section
class="grid grid-cols-[1fr_5fr_4fr_4fr] gap-1 bg-nier-bg font-terminal text-nier-dark divide-x divide-nier-accent/30 border border-nier-accent h-32"
>
<app-header-logo />
<app-header-text-animate-section
[phrases]="[
'Converting existential dread to binary...',
'Hacking the mainframe with a rubber duck...',
'Error: Sarcasm module overloaded. Rebooting...',
'Coffee.exe has stopped working. Attempting to reboot human...',
'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...',
'Decrypting corporate secrets... replaced with cat videos...',
'Attempting to find motivation.exe... running scan...',
'Corporate-mandated fun protocol initiated...',
'Rerouting power to sarcasm generators...',
'Cybernetic arm requesting high-five calibration...',
'Deleting unnecessary small talk subroutines...',
'AI analyzing your taste in music... respect decreased by 17%...',
'Searching for affordable apartment in Night City... zero results...',
'Tactical roomba squad deployed to kitchen sector...',
'Virtual assistant has muted your notifications... all of them...',
'Installing social skills patch 2.0... buffering...',
'Your smart fridge: &quot;The expired milk stays. I like the science experiment.&quot;',
'Digital plant monitor reports: &quot;Even cacti need water sometimes.&quot;',
'Calculating fastest route to avoid small talk...',
'Quantum computer processing your excuse... seems implausible...',
]"
[interval]="2500"
/>
<app-header-nav-links />
<app-header-contact-links />
</section>
</header>

View File

@@ -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%;
}

View File

@@ -1,6 +1,6 @@
<button
class="w-48 text-left font-noto-jp text-base uppercase transition-all duration-300 bg-nier-mid text-nier-dark rounded-none button-hover p-1.5"
class="w-48 text-left font-noto-jp text-base uppercase transition-all duration-300 rounded-none p-1.5 button-custom"
(click)="handleClick()"
>
{{ label() }}
<span class="relative z-10">{{ label() }}</span>
</button>

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularPortfolio</title>
<title>Adam Benyekkou Portfolio</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">