Added optimization / patched memory leaks for several animation and Threejs component

This commit is contained in:
AdamBtech
2025-05-27 12:03:10 +02:00
parent 7718edca2b
commit 06bb9df500
25 changed files with 1326 additions and 2091 deletions

View File

@@ -1,3 +1,24 @@
/* ===================== OPTIMIZED NIER BUTTON ===================== */
/* Core keyframes - simplified and optimized */
@keyframes glitchEffect {
0%, 100% { opacity: 0; transform: translateX(0); }
10% { opacity: 0.2; transform: translateX(-2px); clip-path: inset(10% 0 80% 0); }
20% { opacity: 0.2; transform: translateX(2px); clip-path: inset(30% 0 50% 0); }
30% { opacity: 0.1; transform: translateX(-1px); clip-path: inset(50% 0 20% 0); }
}
@keyframes scanLine {
from { opacity: 0.5; left: -100%; }
to { opacity: 0; left: 100%; }
}
@keyframes flash {
from { opacity: 0.2; }
to { opacity: 0; }
}
/* Button base styles */
.button-custom {
cursor: pointer;
background-color: var(--color-nier-mid);
@@ -7,43 +28,25 @@
border-left: none;
border-right: none;
overflow: hidden;
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
transition: all 0.2s ease;
}
/* Fill effect */
.button-custom::before {
content: "";
position: absolute;
left: 0;
top: 3px; /* Space from top border */
bottom: 3px; /* Space from bottom border */
inset: 3px 0;
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;
}
/* Normal hover/focus state - keep your original behavior */
.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%;
}
/* Add NieR-style glitch effects on hover */
/* Glitch text overlay */
.button-custom::after {
content: attr(data-label); /* Use the button's text from data-label attribute */
content: attr(data-label);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
@@ -53,11 +56,7 @@
pointer-events: none;
}
.button-custom:hover::after {
animation: nier-button-glitch 0.6s ease forwards;
}
/* Add subtle scan line on hover */
/* Scan line element */
.button-custom .scan-line {
position: absolute;
top: 0;
@@ -70,80 +69,35 @@
pointer-events: none;
}
/* ===================== HOVER STATES ===================== */
.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%;
}
.button-custom:hover::after {
animation: glitchEffect 0.6s ease forwards;
}
.button-custom:hover .scan-line {
animation: nier-button-scan 0.3s ease forwards;
animation: scanLine 0.3s ease forwards;
}
/* The glitch effect animation */
@keyframes nier-button-glitch {
0%, 100% {
opacity: 0;
transform: translateX(0);
clip-path: inset(0 0 0 0);
}
10% {
opacity: 0.2;
transform: translateX(-2px);
clip-path: inset(10% 0 80% 0);
}
12% {
opacity: 0;
transform: translateX(0);
}
20% {
opacity: 0.2;
transform: translateX(2px);
clip-path: inset(30% 0 50% 0);
}
22% {
opacity: 0;
transform: translateX(0);
}
30% {
opacity: 0.1;
transform: translateX(-1px);
clip-path: inset(50% 0 20% 0);
}
32% {
opacity: 0;
transform: translateX(0);
}
}
/* ===================== ACTIVE STATES ===================== */
/* The scan line animation */
@keyframes nier-button-scan {
0% {
opacity: 0.5;
left: -100%;
}
100% {
opacity: 0;
left: 100%;
}
}
/* Add a brief effect on button click */
.button-custom:active {
transform: scale(0.98);
}
.button-custom:active::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.1);
opacity: 0;
animation: nier-button-flash 0.2s ease;
}
@keyframes nier-button-flash {
0% {
opacity: 0.2;
}
100% {
opacity: 0;
}
animation: flash 0.2s ease;
}

View File

@@ -1 +0,0 @@
<p>container-decorator works!</p>

View File

@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-container-decorator',
imports: [],
templateUrl: './container-decorator.component.html',
styleUrl: './container-decorator.component.css'
})
export class ContainerDecoratorComponent {
}

View File

@@ -1,3 +1,25 @@
/* ===================== OPTIMIZED SVG BUTTONS & LINKS ===================== */
/* Core keyframes - reused across components */
@keyframes glitchEffect {
0%, 100% { opacity: 0; transform: translateX(0); clip-path: inset(0); }
10% { opacity: 0.2; transform: translateX(-2px); clip-path: inset(10% 0 80% 0); }
20% { opacity: 0.2; transform: translateX(2px); clip-path: inset(30% 0 50% 0); }
30% { opacity: 0.1; transform: translateX(-1px); clip-path: inset(50% 0 20% 0); }
}
@keyframes scanLine {
from { opacity: 0.5; left: -100%; }
to { opacity: 0; left: 100%; }
}
@keyframes flash {
from { opacity: 0.2; }
to { opacity: 0; }
}
/* ===================== SVG BUTTON CONTAINER ===================== */
.svg-button-container {
position: relative;
cursor: pointer;
@@ -5,14 +27,11 @@
transition: transform 0.2s ease;
-webkit-tap-highlight-color: transparent;
user-select: none;
/* Add these properties to match button-custom */
border: 1px solid transparent;
border-left: none;
border-right: none;
}
/* SVG icon styling */
.svg-icon {
fill: var(--color-nier-dark, #5a5a50);
position: relative;
@@ -20,45 +39,22 @@
transition: fill 0.3s ease;
}
/* Background fill effect - matching button-custom */
/* Fill effect */
.svg-button-container::before {
content: "";
position: absolute;
left: 0;
top: 3px; /* Space from top border */
bottom: 3px; /* Space from bottom border */
inset: 3px 0;
width: 0;
height: auto; /* This makes it respect top and bottom values */
background-color: var(--color-nier-dark, rgba(30, 30, 30, 0.8));
transition: width 0.2s ease;
z-index: 0;
}
/* Hover state */
.svg-button-container:hover,
.svg-button-container:focus {
border-color: var(--color-nier-dark);
}
.svg-button-container:hover::before,
.svg-button-container:focus::before {
width: 100%;
}
/* Change SVG fill on hover */
.svg-button-container:hover .svg-icon,
.svg-button-container:active .svg-icon {
fill: var(--color-nier-text-light, #dcd8c0);
}
/* Glitch effect on hover */
/* Glitch text overlay */
.svg-button-container::after {
content: attr(data-label);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
@@ -71,15 +67,10 @@
font-family: "Noto Sans JP", monospace;
}
.svg-button-container:hover::after {
animation: nier-button-glitch 0.6s ease forwards;
}
/* Scan line effect */
/* Scan line */
.svg-button-container .scan-line {
position: absolute;
top: 0;
left: -100%;
inset: 0 0 auto -100%;
width: 100%;
height: 1px;
background-color: var(--color-nier-text-light, rgba(230, 230, 230, 0.9));
@@ -88,100 +79,42 @@
pointer-events: none;
}
/* Hover states */
.svg-button-container:hover,
.svg-button-container:focus {
border-color: var(--color-nier-dark);
}
.svg-button-container:hover::before,
.svg-button-container:focus::before {
width: 100%;
}
.svg-button-container:hover .svg-icon,
.svg-button-container:active .svg-icon {
fill: var(--color-nier-text-light, #dcd8c0);
}
.svg-button-container:hover::after {
animation: glitchEffect 0.6s ease forwards;
}
.svg-button-container:hover .scan-line {
animation: nier-button-scan 0.3s ease forwards;
animation: scanLine 0.3s ease forwards;
}
/* The glitch effect animation */
@keyframes nier-button-glitch {
0%, 100% {
opacity: 0;
transform: translateX(0);
clip-path: inset(0 0 0 0);
}
10% {
opacity: 0.2;
transform: translateX(-2px);
clip-path: inset(10% 0 80% 0);
}
12% {
opacity: 0;
transform: translateX(0);
}
20% {
opacity: 0.2;
transform: translateX(2px);
clip-path: inset(30% 0 50% 0);
}
22% {
opacity: 0;
transform: translateX(0);
}
30% {
opacity: 0.1;
transform: translateX(-1px);
clip-path: inset(50% 0 20% 0);
}
32% {
opacity: 0;
transform: translateX(0);
}
}
/* The scan line animation */
@keyframes nier-button-scan {
0% {
opacity: 0.5;
left: -100%;
}
100% {
opacity: 0;
left: 100%;
}
}
/* Button click effect */
/* Active state */
.svg-button-container:active {
transform: scale(0.98);
}
.svg-button-container:active::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.1);
opacity: 0;
animation: nier-button-flash 0.2s ease;
animation: flash 0.2s ease;
}
@keyframes nier-button-flash {
0% {
opacity: 0.2;
}
100% {
opacity: 0;
}
}
/* ===================== TEXT LINKS ===================== */
/* Touch device optimization */
@media (hover: none) {
.svg-button-container:active::before {
width: 100%;
}
.svg-button-container:active .scan-line {
animation: nier-button-scan 0.3s ease forwards;
}
.svg-button-container:active .svg-icon {
fill: var(--color-nier-dark, #dcd8c0);
}
}
/* Styling for text links with NieR aesthetic */
a[data-label] {
position: relative;
cursor: pointer;
@@ -194,7 +127,6 @@ a[data-label] {
border-bottom: 1px solid transparent;
}
/* Underline effect */
a[data-label]::before {
content: "";
position: absolute;
@@ -207,27 +139,9 @@ a[data-label]::before {
z-index: 0;
}
/* Text color change on hover */
a[data-label]:hover {
color: var(--color-nier-dark, #dcd8c0);
border-bottom-color: var(--color-nier-dark);
}
/* Underline animation on hover */
a[data-label]:hover::before {
width: 100%;
}
/* Glitch effect on hover (similar to buttons) */
a[data-label]:hover span:first-child {
animation: nier-text-glitch 0.6s ease;
}
/* Scan line for links */
a[data-label] .scan-line {
position: absolute;
top: 0;
left: -100%;
inset: 0 0 auto -100%;
width: 100%;
height: 1px;
background-color: var(--color-nier-dark, rgba(230, 230, 230, 0.9));
@@ -236,65 +150,62 @@ a[data-label] .scan-line {
pointer-events: none;
}
/* Link hover states */
a[data-label]:hover {
color: var(--color-nier-dark, #dcd8c0);
border-bottom-color: var(--color-nier-dark);
}
a[data-label]:hover::before {
width: 100%;
}
a[data-label]:hover span:first-child {
animation: glitchEffect 0.6s ease;
}
a[data-label]:hover .scan-line {
animation: nier-button-scan 0.3s ease forwards;
animation: scanLine 0.3s ease forwards;
}
/* Text glitch animation for links */
@keyframes nier-text-glitch {
0%, 100% {
transform: translateX(0);
clip-path: inset(0 0 0 0);
}
10% {
transform: translateX(-2px);
clip-path: inset(0 0 40% 0);
}
15% {
transform: translateX(0);
}
20% {
transform: translateX(1px);
clip-path: inset(40% 0 0 0);
}
25% {
transform: translateX(0);
}
30% {
transform: translateX(-1px);
clip-path: inset(20% 0 20% 0);
}
35% {
transform: translateX(0);
}
}
/* Active/click state for links */
a[data-label]:active {
transform: scale(0.98);
}
/* Touch device optimization */
/* ===================== ICON STYLES ===================== */
.icon-email {
width: 150px;
height: 150px;
background-color: var(--color-nier-text-dark);
-webkit-mask: url('/email_qr_black.svg') no-repeat center / contain;
mask: url('/email_qr_black.svg') no-repeat center / contain;
}
/* ===================== TOUCH DEVICES ===================== */
@media (hover: none) {
.svg-button-container:active::before {
width: 100%;
}
.svg-button-container:active .scan-line {
animation: scanLine 0.3s ease forwards;
}
.svg-button-container:active .svg-icon {
fill: var(--color-nier-dark, #dcd8c0);
}
a[data-label]:active::before {
width: 100%;
}
a[data-label]:active .scan-line {
animation: nier-button-scan 0.3s ease forwards;
animation: scanLine 0.3s ease forwards;
}
a[data-label]:active {
color: var(--color-nier-dark, #dcd8c0);
}
}
.icon-email {
width: 150px;
height: 150px;
background-color: var(--color-nier-text-dark);
-webkit-mask: url('/email_qr_black.svg') no-repeat center;
mask: url('/email_qr_black.svg') no-repeat center;
-webkit-mask-size: contain;
mask-size: contain;
}

View File

@@ -1,62 +1,52 @@
/* header-switch-theme-button.component.scss */
/* ===================== OPTIMIZED THEME SWITCH BUTTON ===================== */
/* Base styling for SVG elements */
/* Base SVG styling */
.svg-icon {
transition: fill 0.3s ease, color 0.3s ease, transform 0.3s ease;
transition: all 0.3s ease;
}
/* Moon cutout styling - hidden by default in light mode */
.moon-cutout {
transition: fill 0.3s ease, transform 0.3s ease, opacity 0.3s ease;
fill: #1a1a18; /* Default darker color */
opacity: 0; /* Hidden in light mode */
}
/* LIGHT MODE (default) - show sun */
/* Base circle acts as the sun */
.svg-icon circle[cx="12"][cy="12"][r="5"] {
fill: currentColor;
}
/* DARK MODE - show moon */
/* Show the moon cutout in dark mode */
/* Moon cutout - hidden by default */
.moon-cutout {
fill: #1a1a18;
opacity: 0;
transition: all 0.3s ease;
}
/* ===================== THEME STATES ===================== */
/* Dark mode - show moon */
:host-context(.dark) .moon-cutout,
body[data-theme="dark"] .moon-cutout {
fill: var(--color-nier-dark-bg, #292925);
opacity: 1; /* Visible in dark mode */
}
/* HOVER STATE - always show moon with rotation */
/* Rotation animation on hover */
:host-context(.svg-button-container:hover) .svg-icon,
.svg-button-container:hover .svg-icon {
transform: rotate(45deg);
color: var(--color-nier-text-light, #dcd8c0);
fill: var(--color-nier-text-light, #dcd8c0);
}
/* Always show moon cutout on hover */
:host-context(.svg-button-container:hover) .moon-cutout,
.svg-button-container:hover .moon-cutout {
opacity: 1;
fill: #3a3a34; /* Dark color for cutout on hover */
}
/* ACTIVE STATE - same as hover */
/* ===================== INTERACTIVE STATES ===================== */
/* Hover and active states */
:host-context(.svg-button-container:hover) .svg-icon,
:host-context(.svg-button-container:active) .svg-icon,
.svg-button-container:hover .svg-icon,
.svg-button-container:active .svg-icon {
transform: rotate(45deg);
color: var(--color-nier-text-light, #dcd8c0);
fill: var(--color-nier-text-light, #dcd8c0);
}
:host-context(.svg-button-container:hover) .moon-cutout,
:host-context(.svg-button-container:active) .moon-cutout,
.svg-button-container:hover .moon-cutout,
.svg-button-container:active .moon-cutout {
opacity: 1;
fill: #3a3a34;
}
/* Touch device support */
/* ===================== TOUCH DEVICES ===================== */
@media (hover: none) {
:host-context(.svg-button-container:active) .svg-icon,
.svg-button-container:active .svg-icon {

View File

@@ -1,4 +1,35 @@
/* This simpler approach uses your existing global variables directly */
/* ===================== OPTIMIZED NIER LOGO ===================== */
/* Core keyframes - simplified and optimized */
@keyframes fadeInReveal {
0% { opacity: 0; clip-path: inset(0 100% 0 0); }
100% { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.9; }
}
@keyframes glitch {
0%, 100% { opacity: 0; transform: translateX(0); }
10.5% { opacity: 0.5; transform: translateX(3px); }
30% { opacity: 0.4; transform: translateX(-3px); }
80.5% { opacity: 0.6; transform: translateX(5px); }
}
@keyframes scanLine {
from { transform: translateY(-100%); }
to { transform: translateY(100%); }
}
@keyframes floatingBlocks {
0% { opacity: 0; transform: translate(0, 0); }
10% { opacity: 0.8; }
100% { opacity: 0; transform: translate(40px, 100px); }
}
/* ===================== LOGO CONTAINER ===================== */
.nier-logo-container {
position: relative;
@@ -8,107 +39,15 @@
transition: all 0.3s ease;
}
.nier-logo {
position: relative;
color: var(--color-nier-text-dark); /* Use your global variable */
text-shadow: 0 0 2px var(--color-nier-text-dark);
animation: nier-fade-in 2.5s ease-out forwards, nier-subtle-pulse 4s 2.5s infinite;
transition: color 0.3s ease, text-shadow 0.3s ease;
}
.nier-logo::before {
content: attr(data-text);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
text-shadow: 0 0 3px var(--color-nier-text-dark);
opacity: 0;
animation: nier-glitch 6s 3s infinite;
transition: text-shadow 0.3s ease;
}
.nier-logo::after {
content: attr(data-text);
position: absolute;
left: -2px;
top: 0;
width: 100%;
height: 100%;
text-shadow: -1px 0 1px var(--color-nier-text-dark);
opacity: 0;
animation: nier-glitch-2 5s 3s infinite;
transition: text-shadow 0.3s ease;
}
.nier-scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent 0%,
color-mix(in srgb, var(--color-nier-text-dark) 5%, transparent) 50%,
transparent 100%
);
animation: nier-scan 3s linear infinite;
z-index: 2;
pointer-events: none;
transition: background 0.3s ease;
}
.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;
width: 10px;
height: 6px;
background: color-mix(in srgb, var(--color-nier-text-dark) 10%, transparent);
animation: nier-blocks 10s linear infinite;
transition: background 0.3s ease;
}
.nier-blocks::before {
top: 20%;
left: 10%;
animation-delay: 1s;
}
.nier-blocks::after {
bottom: 40%;
right: 10%;
width: 15px;
height: 4px;
animation-delay: 3s;
}
/* Border and interface details */
.nier-logo-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
inset: 0;
border: 1px solid var(--color-nier-border);
pointer-events: none;
transition: border-color 0.3s ease;
}
/* Interface dots in the corner */
.nier-logo-container::after {
content: "";
position: absolute;
@@ -124,57 +63,88 @@
-6px 6px 0 var(--color-nier-text-dark),
-12px 6px 0 var(--color-nier-text-dark);
pointer-events: none;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
transition: all 0.3s ease;
}
/* Animations unchanged */
@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); }
/* ===================== LOGO TEXT ===================== */
.nier-logo {
position: relative;
color: var(--color-nier-text-dark);
text-shadow: 0 0 2px var(--color-nier-text-dark);
animation: fadeInReveal 2.5s ease-out forwards, pulse 4s 2.5s infinite;
transition: all 0.3s ease;
}
@keyframes nier-subtle-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.9; }
.nier-logo::before,
.nier-logo::after {
content: attr(data-text);
position: absolute;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
animation: glitch 6s 3s infinite;
transition: text-shadow 0.3s ease;
}
@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); }
.nier-logo::before {
left: 0;
text-shadow: 0 0 3px var(--color-nier-text-dark);
}
@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); }
.nier-logo::after {
left: -2px;
text-shadow: -1px 0 1px var(--color-nier-text-dark);
animation-delay: 3s;
animation-duration: 5s;
}
@keyframes nier-scan {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
/* ===================== EFFECTS ===================== */
.nier-scan-line {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
color-mix(in srgb, var(--color-nier-text-dark) 5%, transparent) 50%,
transparent 100%
);
animation: scanLine 3s linear infinite;
z-index: 2;
pointer-events: none;
transition: background 0.3s ease;
}
@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); }
.nier-blocks {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.nier-blocks::before,
.nier-blocks::after {
content: "";
position: absolute;
background: color-mix(in srgb, var(--color-nier-text-dark) 10%, transparent);
animation: floatingBlocks 10s linear infinite;
transition: background 0.3s ease;
}
.nier-blocks::before {
top: 20%;
left: 10%;
width: 10px;
height: 6px;
animation-delay: 1s;
}
.nier-blocks::after {
bottom: 40%;
right: 10%;
width: 15px;
height: 4px;
animation-delay: 3s;
}

View File

@@ -8,6 +8,8 @@ import {
signal,
computed,
ChangeDetectionStrategy,
DestroyRef,
inject,
} from '@angular/core';
import {
trigger,
@@ -16,14 +18,15 @@ import {
transition,
animate,
} from '@angular/animations';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { timer, Subject } from 'rxjs';
import { type TextItem } from '../../../shared/models/header.model';
// 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 },
// Simplified constants
const TYPING_PATTERNS = {
GLITCH: { delay: 150, probability: 0.03 },
RAPID: { delay: 10, probability: 0.3 },
SLOW: { delay: 60, probability: 0.1 },
} as const;
@Component({
@@ -35,24 +38,11 @@ const TYPING_DELAYS = {
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('textChange', [
state(
'visible',
style({
opacity: 1,
transform: 'translateY(0)',
}),
),
state(
'hidden',
style({
opacity: 0,
transform: 'translateY(20px)',
}),
),
state('visible', style({ opacity: 1, transform: 'translateY(0)' })),
state('hidden', style({ opacity: 0, transform: 'translateY(20px)' })),
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)' }),
@@ -65,7 +55,9 @@ const TYPING_DELAYS = {
],
})
export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
// Input signals with better defaults
private readonly destroyRef = inject(DestroyRef);
// Input signals
phrases = input<readonly string[]>([
'Uploading guinea pig consciousness to the cloud...',
'Error: Sarcasm module overloaded. Rebooting...',
@@ -83,32 +75,40 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
typingSpeed = input<number>(25);
maxDisplayedTexts = input<number>(4);
// 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);
// State signals - simplified
private readonly displayedTexts = signal<TextItem[]>([]);
private nextId = 0;
private phraseIndex = 0;
private isTyping = false;
// Computed signals for better performance
// Computed signals
readonly currentTexts = computed(() => this.displayedTexts());
readonly hasTexts = computed(() => this.displayedTexts().length > 0);
// Timeout management
private readonly activeTimeouts = new Set<number>();
private nextTextTimeout?: number;
// Single RAF handle for all animations
private animationFrame: number | null = null;
private nextTextTimer: number | null = null;
// Performance optimization: reuse objects
private readonly typingState = {
textId: -1,
charIndex: 0,
lastUpdate: 0,
nextDelay: 0,
};
constructor() {
// Initialize first text when phrases are available
// Initialize when phrases are available
effect(() => {
const phrasesList = this.phrases();
if (phrasesList.length > 0 && this.displayedTexts().length === 0) {
this.scheduleNextText(0); // Start immediately
this.scheduleNextText(0);
}
});
}
ngOnInit(): void {
// Component initialization handled in constructor effect
// Handled in constructor
}
ngOnDestroy(): void {
@@ -116,94 +116,108 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
}
private cleanup(): void {
// Clear all timeouts
this.activeTimeouts.forEach((id) => window.clearTimeout(id));
this.activeTimeouts.clear();
if (this.nextTextTimeout) {
window.clearTimeout(this.nextTextTimeout);
this.nextTextTimeout = undefined;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
if (this.nextTextTimer) {
clearTimeout(this.nextTextTimer);
this.nextTextTimer = null;
}
this.isTyping = false;
}
private scheduleNextText(delay: number = this.interval()): void {
if (this.nextTextTimeout) {
window.clearTimeout(this.nextTextTimeout);
if (this.nextTextTimer) {
clearTimeout(this.nextTextTimer);
}
this.nextTextTimeout = window.setTimeout(() => {
this.nextTextTimer = setTimeout(() => {
this.startNextText();
this.nextTextTimer = null;
}, delay);
}
private startNextText(): void {
if (this.isTypingInProgress()) return;
if (this.isTyping) return;
const phrasesList = this.phrases();
if (phrasesList.length === 0) return;
const currentIndex = this.phraseIndex();
const nextText = phrasesList[currentIndex];
const nextText = phrasesList[this.phraseIndex];
this.phraseIndex = (this.phraseIndex + 1) % phrasesList.length;
// Create immutable text item
// Create new text item
const newItem: TextItem = {
id: this.nextId(),
id: this.nextId++,
text: nextText,
displayed: '',
isTyping: true,
isComplete: false,
};
// Update state atomically
this.phraseIndex.update((idx) => (idx + 1) % phrasesList.length);
this.nextId.update((id) => id + 1);
this.isTypingInProgress.set(true);
// Update displayed texts with proper cleanup
// Update displayed texts efficiently
this.displayedTexts.update((texts) => {
const maxTexts = this.maxDisplayedTexts();
if (texts.length >= maxTexts) {
return [...texts.slice(texts.length - maxTexts + 1), newItem];
}
return [...texts, newItem];
const newTexts =
texts.length >= maxTexts
? [...texts.slice(-maxTexts + 1), newItem]
: [...texts, newItem];
return newTexts;
});
// Start typing animation
this.animateText(newItem.id);
// Start typing with RAF
this.isTyping = true;
this.typingState.textId = newItem.id;
this.typingState.charIndex = 0;
this.typingState.lastUpdate = performance.now();
this.typingState.nextDelay = this.calculateTypingDelay();
this.animateWithRAF();
}
private animateText(textId: number): void {
private animateWithRAF(): void {
if (!this.isTyping) return;
const now = performance.now();
const elapsed = now - this.typingState.lastUpdate;
if (elapsed >= this.typingState.nextDelay) {
this.updateTypingCharacter();
this.typingState.lastUpdate = now;
this.typingState.nextDelay = this.calculateTypingDelay();
}
if (this.isTyping) {
this.animationFrame = requestAnimationFrame(() => this.animateWithRAF());
}
}
private updateTypingCharacter(): void {
const texts = this.displayedTexts();
const textIndex = texts.findIndex((t) => t.id === textId);
const textIndex = texts.findIndex((t) => t.id === this.typingState.textId);
if (textIndex === -1) return;
const textItem = texts[textIndex];
const { text, displayed } = textItem;
const nextCharIndex = displayed.length;
const { text } = textItem;
if (nextCharIndex < text.length) {
// Update displayed text
const newDisplayed = text.substring(0, nextCharIndex + 1);
if (this.typingState.charIndex < text.length) {
// Update character
const newDisplayed = text.substring(0, this.typingState.charIndex + 1);
// Batch update for better performance
this.displayedTexts.update((currentTexts) => {
const updatedTexts = [...currentTexts];
updatedTexts[textIndex] = {
...textItem,
displayed: newDisplayed,
};
return updatedTexts;
const updated = [...currentTexts];
updated[textIndex] = { ...textItem, displayed: newDisplayed };
return updated;
});
// Schedule next character with dynamic timing
const delay = this.calculateTypingDelay();
const timeoutId = window.setTimeout(() => {
this.animateText(textId);
}, delay);
this.activeTimeouts.add(timeoutId);
this.typingState.charIndex++;
} else {
// Text complete
// Complete text
this.completeText(textIndex, textItem);
}
}
@@ -212,59 +226,45 @@ export class HeaderTextAnimateSectionComponent implements OnInit, OnDestroy {
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)
);
// Simplified delay calculation
if (random < TYPING_PATTERNS.GLITCH.probability) {
return TYPING_PATTERNS.GLITCH.delay + Math.random() * 50;
}
if (random < TYPING_DELAYS.RAPID.probability) {
return (
TYPING_DELAYS.RAPID.min +
Math.random() * (TYPING_DELAYS.RAPID.max - TYPING_DELAYS.RAPID.min)
);
if (random < TYPING_PATTERNS.RAPID.probability) {
return TYPING_PATTERNS.RAPID.delay + Math.random() * 10;
}
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)
);
if (random < TYPING_PATTERNS.SLOW.probability) {
return TYPING_PATTERNS.SLOW.delay + Math.random() * 20;
}
// 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
// Normal typing with minimal variation
return baseSpeed + (Math.random() * 10 - 5);
}
private completeText(textIndex: number, textItem: TextItem): void {
// Mark text as complete
// Stop animation
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
// Mark complete
this.displayedTexts.update((texts) => {
const updatedTexts = [...texts];
updatedTexts[textIndex] = {
const updated = [...texts];
updated[textIndex] = {
...textItem,
isTyping: false,
isComplete: true,
};
return updatedTexts;
return updated;
});
// Reset typing state and schedule next text
this.isTypingInProgress.set(false);
this.isTyping = false;
this.scheduleNextText();
}
// Optimized tracking function
// Optimized tracking
trackByTextId = (index: number, item: TextItem): number => item.id;
}

View File

@@ -1,24 +1,28 @@
/* ===================== OPTIMIZED COMPONENT STYLES ===================== */
/* Host element */
:host {
display: block;
width: 100%;
height: 100%;
}
/* Custom spinner border width for Tailwind */
/* Custom utility classes */
.border-3 {
border-width: 3px;
}
/* Backdrop blur fallback for older browsers */
.backdrop-blur-sm {
backdrop-filter: blur(4px);
}
/* Custom shadow glow effect */
.shadow-cyan-500\/30 {
box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
}
/* Responsive container for smaller screens */
/* ===================== RESPONSIVE DESIGN ===================== */
/* Tablet and below */
@media (max-width: 840px) {
.w-\[800px\] {
width: 100%;
@@ -31,11 +35,10 @@
}
}
/* Mobile */
@media (max-width: 768px) {
.absolute.top-5.right-5 {
top: 0.625rem;
right: 0.625rem;
left: 0.625rem;
inset: 0.625rem 0.625rem auto 0.625rem;
}
.absolute.top-5.right-5 button {

View File

@@ -58,14 +58,18 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
// 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
private readonly MAX_SCATTER_DISTANCE = 600;
private readonly SCATTER_STRENGTH = 45;
private readonly GLOW_INTENSITY = 4.0;
// Optimization: Reuse arrays
private tempPositions = new Float32Array(this.POINTS_COUNT * 3);
private tempColors = new Float32Array(this.POINTS_COUNT * 3);
// MEMORY LEAK FIX: Add pattern animation cleanup
private patternAnimationId: number = 0;
private isDestroyed = false;
constructor() {
effect(() => {
if (this.material) {
@@ -74,36 +78,86 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
});
}
ngOnInit(): void {
async ngOnInit(): Promise<void> {
this.initThreeJS();
this.createPointCloud();
this.setupVideo();
await this.setupVideo();
this.setupEventListeners();
this.setupResizeObserver();
this.animate();
this.onWindowResize();
if (this.videoSrc) {
this.loadVideoFromSrc(this.videoSrc);
await this.loadVideoFromSrc(this.videoSrc);
}
}
ngOnDestroy(): void {
// MEMORY LEAK FIX: Comprehensive cleanup
this.cleanup();
}
// MEMORY LEAK FIX: Proper cleanup method
private cleanup(): void {
this.isDestroyed = true;
// Cancel all animations
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = 0;
}
if (this.patternAnimationId) {
cancelAnimationFrame(this.patternAnimationId);
this.patternAnimationId = 0;
}
// Disconnect resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
// Stop and cleanup video
if (this.video) {
this.video.pause();
if (this.video.srcObject) {
const stream = this.video.srcObject as MediaStream;
stream.getTracks().forEach((track) => track.stop());
}
this.video.src = '';
this.video.srcObject = null;
this.video.load();
}
// Cleanup Three.js resources
this.geometry?.dispose();
this.material?.dispose();
if (this.geometry) {
this.geometry.dispose();
}
if (this.material) {
this.material.dispose();
}
this.glowLayers.forEach((layer) => {
if (layer.geometry) {
layer.geometry.dispose();
}
if (layer.points.material) {
(layer.points.material as THREE.Material).dispose();
}
if (this.scene) {
this.scene.remove(layer.points);
}
});
this.renderer?.dispose();
this.glowLayers = [];
if (this.points && this.scene) {
this.scene.remove(this.points);
}
if (this.renderer) {
this.renderer.dispose();
this.renderer.forceContextLoss();
}
}
private initThreeJS(): void {
@@ -243,7 +297,7 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
}
}
private setupVideo(): void {
private async setupVideo(): Promise<void> {
this.video = document.createElement('video');
this.video.width = this.GRID_WIDTH;
this.video.height = this.GRID_HEIGHT;
@@ -256,17 +310,24 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
this.canvas2D.height = this.GRID_HEIGHT;
this.ctx2D = this.canvas2D.getContext('2d')!;
this.createAnimatedPattern();
await this.createAnimatedPattern();
}
private createAnimatedPattern(): void {
private async createAnimatedPattern(): Promise<void> {
const canvas = document.createElement('canvas');
canvas.width = this.GRID_WIDTH;
canvas.height = this.GRID_HEIGHT;
const ctx = canvas.getContext('2d')!;
let time = 0;
// MEMORY LEAK FIX: Store animation ID and check for component destruction
const animate = () => {
// MEMORY LEAK FIX: Stop animation if component is destroyed
if (this.isDestroyed) {
return;
}
const imageData = ctx.createImageData(this.GRID_WIDTH, this.GRID_HEIGHT);
const data = imageData.data;
@@ -294,13 +355,22 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
ctx.putImageData(imageData, 0, 0);
time++;
requestAnimationFrame(animate);
// MEMORY LEAK FIX: Store the animation ID for proper cleanup
if (!this.isDestroyed) {
this.patternAnimationId = requestAnimationFrame(animate);
}
};
animate();
try {
const stream = canvas.captureStream(30);
this.video.srcObject = stream;
this.video.play();
await this.video.play();
} catch (error) {
console.warn('Pattern video setup failed:', error);
}
}
private updatePointCloud(): void {
@@ -336,6 +406,17 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
const isGlitching = this.glitchIntensity > 0;
// MEMORY LEAK FIX: Add periodic cleanup of scatter arrays
if (this.glitchTime % 300 === 0) {
// Every 5 seconds at 60fps
for (let i = 0; i < this.POINTS_COUNT; i++) {
if (this.scatterDecayMap[i] < 0.01) {
this.scatterDecayMap[i] = 0;
this.scatterIntensityMap[i] = 0;
}
}
}
// Update scatter decay for all points
for (let i = 0; i < this.POINTS_COUNT; i++) {
if (this.scatterDecayMap[i] > 0) {
@@ -604,19 +685,32 @@ export class HoloVideoContainerComponent implements OnInit, OnDestroy {
}
private animate(): void {
// MEMORY LEAK FIX: Check if component is destroyed
if (this.isDestroyed) return;
this.animationId = requestAnimationFrame(() => this.animate());
this.updatePointCloud();
this.renderer.render(this.scene, this.camera);
}
private async loadVideoFromSrc(src: string): Promise<void> {
try {
// MEMORY LEAK FIX: Properly stop existing stream before loading new one
if (this.video.srcObject) {
const stream = this.video.srcObject as MediaStream;
stream.getTracks().forEach((track) => track.stop());
this.video.srcObject = null;
}
this.video.src = src;
this.video.loop = true; // Ensure loop is set after changing source
this.video.muted = true; // Also ensure muted is maintained
this.video.autoplay = true; // Ensure autoplay is maintained
this.video.load();
await this.video.play();
} catch (error) {
console.warn('Video load failed:', error);
}
}
get isLoadingValue(): boolean {

View File

@@ -1 +0,0 @@
<p>infocard works!</p>

View File

@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-infocard',
imports: [],
templateUrl: './infocard.component.html',
styleUrl: './infocard.component.css'
})
export class InfocardComponent {
}

View File

@@ -1 +0,0 @@
<p>long-button works!</p>

View File

@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-long-button',
imports: [],
templateUrl: './long-button.component.html',
styleUrl: './long-button.component.css'
})
export class LongButtonComponent {
}

View File

@@ -1,385 +1,164 @@
/* ===================== NEURAL PROFILE SECTION ANIMATIONS ===================== */
/* ===================== NEURAL PROFILE ANIMATIONS ===================== */
/* Section Container Animation */
/* Core keyframes - reused across components */
@keyframes slideInLeft {
from {
opacity: 0;
transform: translate3d(-30px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translate3d(30px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* ===================== NEURAL SECTION ELEMENTS ===================== */
/* Section container - no animation */
.neural-section {
opacity: 0;
transform: translateY(30px);
animation: neuralSectionMaterialize 1.2s ease-out 0.1s forwards;
}
@keyframes neuralSectionMaterialize {
0% {
opacity: 0;
transform: translateY(30px);
filter: blur(2px);
}
60% {
opacity: 0.8;
transform: translateY(-5px);
filter: blur(0.5px);
}
100% {
opacity: 1;
transform: translateY(0);
filter: blur(0px);
}
}
/* Header Section Animation */
/* Header - no animation */
.neural-header {
opacity: 0;
transform: translateY(-20px);
animation: neuralHeaderSlideDown 1s ease-out 0.3s forwards;
}
@keyframes neuralHeaderSlideDown {
0% {
opacity: 0;
transform: translateY(-20px);
filter: blur(1px);
}
70% {
opacity: 0.9;
transform: translateY(3px);
filter: blur(0.3px);
}
100% {
opacity: 1;
transform: translateY(0);
filter: blur(0px);
}
}
/* Divider Line Animation */
/* Divider - static */
.neural-divider {
opacity: 0;
transform: scaleX(0);
transform-origin: left;
animation: neuralDividerExpand 0.8s ease-out 0.8s forwards;
}
@keyframes neuralDividerExpand {
0% {
opacity: 0;
transform: scaleX(0);
}
50% {
opacity: 0.3;
transform: scaleX(0.7);
}
100% {
opacity: 0.5;
transform: scaleX(1);
}
}
/* Main Content Grid Animation */
/* Main content grid */
.neural-content-grid {
opacity: 0;
transform: translateY(40px);
animation: neuralContentReveal 1.2s ease-out 0.5s forwards;
animation: slideInUp 0.8s ease-out 0.3s forwards;
}
@keyframes neuralContentReveal {
0% {
opacity: 0;
transform: translateY(40px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Left Panel - Neural Profile Tree Animation */
/* Left panel */
.neural-left-panel {
opacity: 0;
transform: translateX(-40px) scale(0.95);
animation: neuralLeftPanelSlideIn 1.2s ease-out 0.7s forwards;
animation: slideInLeft 0.8s ease-out 0.5s forwards;
}
@keyframes neuralLeftPanelSlideIn {
0% {
opacity: 0;
transform: translateX(-40px) scale(0.95);
border-color: transparent;
background-color: rgba(41, 41, 37, 0);
}
40% {
opacity: 0.6;
transform: translateX(-10px) scale(0.98);
border-color: var(--color-nier-border);
}
80% {
opacity: 0.9;
transform: translateX(2px) scale(1.01);
background-color: var(--color-nier-bg);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
border-color: var(--color-nier-border);
background-color: var(--color-nier-bg);
}
}
/* Neural Profile Tree Content */
/* Tree content */
.neural-tree-container {
opacity: 0;
transform: translateY(20px);
animation: neuralTreeContentFadeUp 0.8s ease-out 1.2s forwards;
animation: slideInUp 0.6s ease-out 0.8s forwards;
}
@keyframes neuralTreeContentFadeUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Right Panel Container Animation */
/* Right panel */
.neural-right-panel {
opacity: 0;
transform: translateX(40px);
animation: neuralRightPanelSlideIn 1s ease-out 0.9s forwards;
animation: slideInRight 0.8s ease-out 0.6s forwards;
}
@keyframes neuralRightPanelSlideIn {
0% {
opacity: 0;
transform: translateX(40px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Video Display Panel Animation */
/* Video panel */
.neural-video-panel {
opacity: 0;
transform: scale(0.95) translateY(20px);
animation: neuralVideoPanelMaterialize 1.2s ease-out 1.1s forwards;
animation: scaleIn 0.8s ease-out 0.7s forwards;
}
@keyframes neuralVideoPanelMaterialize {
0% {
opacity: 0;
transform: scale(0.95) translateY(20px);
border-color: transparent;
background-color: rgba(41, 41, 37, 0);
}
30% {
opacity: 0.4;
transform: scale(0.98) translateY(10px);
border-color: var(--color-nier-accent);
}
70% {
opacity: 0.8;
transform: scale(1.01) translateY(-2px);
background-color: var(--color-nier-dark);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
border-color: var(--color-nier-accent);
background-color: var(--color-nier-dark);
}
}
/* Video Container Inner Animation */
/* Video inner container */
.neural-video-inner {
opacity: 0;
transform: scale(0.9);
animation: neuralVideoInnerExpand 0.8s ease-out 1.5s forwards;
animation: scaleIn 0.6s ease-out 1.0s forwards;
}
@keyframes neuralVideoInnerExpand {
0% {
opacity: 0;
transform: scale(0.9);
border-color: transparent;
background-color: rgba(0, 0, 0, 0);
}
60% {
opacity: 0.7;
transform: scale(1.02);
border-color: rgba(255, 201, 102, 0.3);
}
100% {
opacity: 1;
transform: scale(1);
border-color: rgba(255, 201, 102, 0.5);
background-color: rgba(41, 41, 37, 0.1);
}
}
/* Holo Video Component Animation */
/* Holo video */
.neural-holo-video {
opacity: 0;
animation: neuralHoloVideoFadeIn 1s ease-out 1.8s forwards;
animation: fadeIn 0.8s ease-out 1.2s forwards;
}
@keyframes neuralHoloVideoFadeIn {
0% {
opacity: 0;
filter: blur(2px) brightness(0.5);
}
50% {
opacity: 0.7;
filter: blur(1px) brightness(0.8);
}
100% {
opacity: 1;
filter: blur(0px) brightness(1);
}
}
/* Status Panel Animation */
/* Status panel */
.neural-status-panel {
opacity: 0;
transform: translateY(30px) scale(0.95);
animation: neuralStatusPanelRise 1s ease-out 1.3s forwards;
animation: slideInUp 0.8s ease-out 0.9s forwards;
}
@keyframes neuralStatusPanelRise {
0% {
opacity: 0;
transform: translateY(30px) scale(0.95);
border-color: transparent;
background-color: rgba(41, 41, 37, 0);
}
40% {
opacity: 0.6;
transform: translateY(10px) scale(0.98);
border-color: var(--color-nier-border);
}
80% {
opacity: 0.9;
transform: translateY(-2px) scale(1.01);
background-color: var(--color-nier-mid);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
border-color: var(--color-nier-border);
background-color: var(--color-nier-mid);
}
}
/* Status Grid Content Animation */
/* Status grid */
.neural-status-grid {
opacity: 0;
transform: translateY(15px);
animation: neuralStatusGridFadeUp 0.6s ease-out 1.7s forwards;
animation: slideInUp 0.5s ease-out 1.2s forwards;
}
@keyframes neuralStatusGridFadeUp {
0% {
opacity: 0;
transform: translateY(15px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Status Items Staggered Animation */
/* Status items */
.neural-status-item {
opacity: 0;
transform: translateX(-15px);
animation: neuralStatusItemSlideIn 0.5s ease-out forwards;
animation: slideInLeft 0.4s ease-out forwards;
}
.neural-status-item:nth-child(1) { animation-delay: 1.9s; }
.neural-status-item:nth-child(2) { animation-delay: 2.1s; }
.neural-status-item:nth-child(1) { animation-delay: 1.4s; }
.neural-status-item:nth-child(2) { animation-delay: 1.5s; }
@keyframes neuralStatusItemSlideIn {
0% {
opacity: 0;
transform: translateX(-15px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Status Labels Animation */
/* Status labels */
.neural-status-label {
opacity: 0;
transform: translateY(-5px);
animation: neuralStatusLabelFadeDown 0.4s ease-out forwards;
animation: fadeIn 0.3s ease-out forwards;
}
.neural-status-item:nth-child(1) .neural-status-label { animation-delay: 2.0s; }
.neural-status-item:nth-child(2) .neural-status-label { animation-delay: 2.2s; }
.neural-status-item:nth-child(1) .neural-status-label { animation-delay: 1.5s; }
.neural-status-item:nth-child(2) .neural-status-label { animation-delay: 1.6s; }
@keyframes neuralStatusLabelFadeDown {
0% {
opacity: 0;
transform: translateY(-5px);
}
100% {
opacity: 0.6;
transform: translateY(0);
}
}
/* Status Values Animation */
/* Status values */
.neural-status-value {
opacity: 0;
transform: scale(0.9);
animation: neuralStatusValuePop 0.4s ease-out forwards;
animation: scaleIn 0.3s ease-out forwards;
}
.neural-status-item:nth-child(1) .neural-status-value { animation-delay: 2.1s; }
.neural-status-item:nth-child(2) .neural-status-value { animation-delay: 2.3s; }
.neural-status-item:nth-child(1) .neural-status-value { animation-delay: 1.6s; }
.neural-status-item:nth-child(2) .neural-status-value { animation-delay: 1.7s; }
@keyframes neuralStatusValuePop {
0% {
opacity: 0;
transform: scale(0.9);
}
50% {
opacity: 0.8;
transform: scale(1.05);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Pulse Animation for Status Indicator */
/* Pulse dot */
.neural-pulse-dot {
opacity: 0;
animation: neuralPulseDotAppear 0.6s ease-out forwards;
animation: scaleIn 0.4s ease-out forwards;
}
.neural-status-item:nth-child(1) .neural-pulse-dot { animation-delay: 2.2s; }
.neural-status-item:nth-child(1) .neural-pulse-dot { animation-delay: 1.7s; }
@keyframes neuralPulseDotAppear {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 0.8;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* ===================== BACKGROUND ELEMENTS ===================== */
/* Background Checkered Pattern Enhancement */
/* Checkered background - static */
.neural-checkered-bg {
position: relative;
}
@@ -387,11 +166,8 @@
.neural-checkered-bg::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
inset: 0;
opacity: 1;
background-image:
linear-gradient(45deg, rgba(255, 201, 102, 0.01) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255, 201, 102, 0.01) 25%, transparent 25%),
@@ -399,153 +175,74 @@
linear-gradient(-45deg, transparent 75%, rgba(255, 201, 102, 0.01) 75%);
background-size: 30px 30px;
background-position: 0 0, 0 15px, 15px -15px, -15px 0px;
animation: neuralBgPatternFadeIn 3s ease-out 0.2s forwards;
pointer-events: none;
z-index: 0;
}
@keyframes neuralBgPatternFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Scanning Line Effect for Neural Section */
/* Scan line - hidden */
.neural-scan-line {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent,
rgba(255, 201, 102, 0.6),
transparent
);
animation: neuralScanLine 4s ease-in-out 1s forwards;
z-index: 1;
}
@keyframes neuralScanLine {
0% {
left: -100%;
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
left: 100%;
opacity: 0;
}
display: none;
}
/* ===================== RESPONSIVE DESIGN ===================== */
@media (max-width: 1024px) {
/* Faster animations on tablets */
.neural-section {
animation-duration: 1s;
}
.neural-content-grid,
.neural-left-panel,
.neural-video-panel {
animation-duration: 1s;
}
.neural-video-panel,
.neural-right-panel {
animation-duration: 0.8s;
animation-delay: 0.7s;
animation-duration: 0.6s;
}
}
@media (max-width: 768px) {
/* Much faster animations on mobile */
.neural-section {
animation-duration: 0.8s;
animation-delay: 0.05s;
}
.neural-content-grid { animation-delay: 0.2s; }
.neural-left-panel { animation-delay: 0.3s; }
.neural-right-panel { animation-delay: 0.4s; }
.neural-video-panel { animation-delay: 0.5s; }
.neural-status-panel { animation-delay: 0.6s; }
.neural-header {
animation-duration: 0.6s;
animation-delay: 0.2s;
}
.neural-status-item:nth-child(1) { animation-delay: 0.8s; }
.neural-status-item:nth-child(2) { animation-delay: 0.9s; }
.neural-divider {
animation-duration: 0.5s;
animation-delay: 0.4s;
}
.neural-status-item:nth-child(1) .neural-status-label { animation-delay: 0.9s; }
.neural-status-item:nth-child(2) .neural-status-label { animation-delay: 1.0s; }
.neural-content-grid {
animation-duration: 0.8s;
animation-delay: 0.3s;
}
.neural-status-item:nth-child(1) .neural-status-value { animation-delay: 1.0s; }
.neural-status-item:nth-child(2) .neural-status-value { animation-delay: 1.1s; }
.neural-left-panel {
animation-duration: 0.8s;
animation-delay: 0.4s;
}
.neural-status-item:nth-child(1) .neural-pulse-dot { animation-delay: 1.1s; }
.neural-right-panel {
animation-duration: 0.6s;
animation-delay: 0.5s;
}
.neural-video-panel {
animation-duration: 0.8s;
animation-delay: 0.6s;
}
.neural-status-panel {
animation-duration: 0.6s;
animation-delay: 0.7s;
}
.neural-status-item:nth-child(1) { animation-delay: 0.9s; }
.neural-status-item:nth-child(2) { animation-delay: 1.0s; }
.neural-status-item:nth-child(1) .neural-status-label { animation-delay: 1.0s; }
.neural-status-item:nth-child(2) .neural-status-label { animation-delay: 1.1s; }
.neural-status-item:nth-child(1) .neural-status-value { animation-delay: 1.1s; }
.neural-status-item:nth-child(2) .neural-status-value { animation-delay: 1.2s; }
.neural-status-item:nth-child(1) .neural-pulse-dot { animation-delay: 1.2s; }
}
/* ===================== ACCESSIBILITY ===================== */
/* Respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
.neural-section,
.neural-header,
.neural-divider,
/* Faster animations on mobile */
.neural-content-grid,
.neural-left-panel,
.neural-tree-container,
.neural-right-panel,
.neural-video-panel,
.neural-status-panel {
animation-duration: 0.5s;
}
.neural-tree-container,
.neural-video-inner,
.neural-holo-video,
.neural-status-panel,
.neural-status-grid,
.neural-status-grid {
animation-duration: 0.4s;
}
.neural-status-item,
.neural-status-label,
.neural-status-value,
.neural-pulse-dot {
animation-duration: 0.3s;
}
}
/* ===================== ACCESSIBILITY ===================== */
@media (prefers-reduced-motion: reduce) {
*[class*="neural-"] {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.neural-checkered-bg::before,
.neural-scan-line {
display: none !important;
}
}

View File

@@ -1,4 +1,20 @@
/* NieR-style hover effects for tree nodes */
/* ===================== OPTIMIZED TREE & CHIP SYSTEM ===================== */
/* Core keyframes */
@keyframes textGlitch {
0%, 100% { transform: translateX(0); clip-path: inset(0); }
10% { transform: translateX(-2px); clip-path: inset(0 0 40% 0); }
20% { transform: translateX(1px); clip-path: inset(40% 0 0 0); }
30% { transform: translateX(-1px); clip-path: inset(20% 0 20% 0); }
}
@keyframes scanLine {
from { opacity: 0.5; left: -100%; }
to { opacity: 0; left: 100%; }
}
/* ===================== TREE NODES ===================== */
.tree-node-item {
position: relative;
overflow: hidden;
@@ -6,7 +22,6 @@
border-bottom: 1px solid transparent;
}
/* Underline effect */
.tree-node-item::before {
content: "";
position: absolute;
@@ -16,35 +31,11 @@
height: 1px;
background-color: var(--color-nier-text-dark);
transition: width 0.3s ease;
z-index: 0;
}
/* Text color change on hover */
.tree-node-item:hover {
color: var(--color-nier-text-dark);
border-bottom-color: var(--color-nier-text-dark);
}
/* Underline animation on hover */
.tree-node-item:hover::before {
width: 100%;
}
/* Glitch effect on hover - target the text span directly */
.tree-node-item:hover span.text-nier-dark {
animation: nier-text-glitch 0.6s ease;
}
/* Alternative - target any span with text */
.tree-node-item:hover span:last-of-type {
animation: nier-text-glitch 0.6s ease;
}
/* Scan line for tree nodes */
.tree-node-item .scan-line {
position: absolute;
top: 0;
left: -100%;
inset: 0 0 auto -100%;
width: 100%;
height: 1px;
background-color: var(--color-nier-text-dark);
@@ -53,16 +44,30 @@
pointer-events: none;
}
.tree-node-item:hover .scan-line {
animation: nier-button-scan 0.3s ease forwards;
/* Tree node hover effects */
.tree-node-item:hover {
color: var(--color-nier-text-dark);
border-bottom-color: var(--color-nier-text-dark);
}
.tree-node-item:hover::before {
width: 100%;
}
.tree-node-item:hover span:last-of-type {
animation: textGlitch 0.6s ease;
}
.tree-node-item:hover .scan-line {
animation: scanLine 0.3s ease forwards;
}
/* Active/click state for tree nodes */
.tree-node-item:active {
transform: scale(0.98);
}
/* NieR-style Chip Container System - NO INTERACTIONS */
/* ===================== CHIP SYSTEM ===================== */
.chip-container {
position: relative;
}
@@ -70,33 +75,19 @@
.chip-parent {
position: relative;
padding: 16px;
border-radius: 0;
border: 1px solid rgba(0, 0, 0, 0.15); /* Very thin border */
box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.15);
overflow: hidden;
transition: transform 0.2s ease; /* Allow parent scaling */
transition: transform 0.2s ease;
}
/* Parent Chip Colors - Complete NieR Earth Palette */
.tools-parent {
background: linear-gradient(135deg, #a67c63 0%, #8b6b52 100%);
}
/* Parent chip gradients */
.tools-parent { background: linear-gradient(135deg, #a67c63, #8b6b52); }
.frontend-parent { background: linear-gradient(135deg, #b8916f, #a67c63); }
.backend-parent { background: linear-gradient(135deg, #c9b896, #b8916f); }
.database-parent { background: linear-gradient(135deg, #e6d7b8, #d4c4a0); }
.frontend-parent {
background: linear-gradient(135deg, #b8916f 0%, #a67c63 100%);
}
.backend-parent {
background: linear-gradient(135deg, #c9b896 0%, #b8916f 100%);
}
.database-parent {
background: linear-gradient(135deg, #e6d7b8 0%, #d4c4a0 100%);
}
/* Locked Parent Category */
.locked-parent {
background: linear-gradient(135deg, #606060 0%, #4a4a4a 100%);
background: linear-gradient(135deg, #606060, #4a4a4a);
border: none;
cursor: not-allowed;
height: 100%;
@@ -111,14 +102,12 @@
min-height: 0;
}
/* Chip Cells Column Layout */
.chip-cells-column {
display: flex;
flex-direction: column;
gap: 4px;
}
/* Locked Content Styling */
.locked-content {
display: flex;
align-items: center;
@@ -136,58 +125,46 @@
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
}
/* Individual Chip Cells - Keep original grayish overlay */
/* Chip cells */
.chip-cell {
position: relative;
width: 100%;
height: 16px;
border-radius: 0;
border: 1px solid rgba(0, 0, 0, 0.2); /* Keep original border */
background: rgba(0, 0, 0, 0.1); /* Keep original grayish overlay */
border: 1px solid rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.1);
cursor: pointer;
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); /* Keep original shadow */
transition: transform 0.2s ease; /* Allow scale animation */
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
}
.chip-cell::before {
content: '';
position: absolute;
top: 1px;
left: 1px;
right: 1px;
inset: 1px 1px auto 1px;
height: 4px;
background: rgba(255, 255, 255, 0.1); /* Keep original highlight */
border-radius: 0;
background: rgba(255, 255, 255, 0.1);
}
/* Scale animation when tree node is hovered OR selected */
/* Active states */
.chip-cell.cell-hovered,
.chip-cell.cell-selected {
transform: scale(1.05) !important;
background: rgba(0, 0, 0, 0.1) !important; /* Keep original grayish color */
border: 1px solid rgba(0, 0, 0, 0.2) !important; /* Keep original border */
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2) !important; /* Keep original shadow */
}
/* Parent categories scale when children are hovered OR selected */
.chip-container.chip-hovered .chip-parent,
.chip-container.chip-selected .chip-parent {
transform: scale(1.02) !important;
}
/* NO direct hover effects on cells */
/* Prevent direct chip interactions */
.chip-cell:hover,
.chip-cell:focus,
.chip-cell:active {
background: rgba(0, 0, 0, 0.1) !important; /* Keep original grayish */
border: 1px solid rgba(0, 0, 0, 0.2) !important; /* Keep original border */
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2) !important; /* Keep original shadow */
background: rgba(0, 0, 0, 0.1) !important;
border: 1px solid rgba(0, 0, 0, 0.2) !important;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2) !important;
transform: none !important;
filter: none !important;
opacity: 1 !important;
animation: none !important;
transition: none !important;
}
.chip-cell:hover::before,
@@ -195,60 +172,12 @@
.chip-cell:active::before,
.chip-cell.cell-hovered::before,
.chip-cell.cell-selected::before {
background: rgba(255, 255, 255, 0.1) !important; /* Keep original highlight */
background: rgba(255, 255, 255, 0.1) !important;
height: 4px !important;
box-shadow: none !important;
}
/* NO CONTAINER EFFECTS */
.chip-normal,
.chip-selected,
.chip-hovered {
/* No effects */
}
/* ===================== RESPONSIVE DESIGN ===================== */
/* Text glitch effect for tree nodes only */
@keyframes nier-text-glitch {
0%, 100% {
transform: translateX(0);
clip-path: inset(0 0 0 0);
}
10% {
transform: translateX(-2px);
clip-path: inset(0 0 40% 0);
}
15% {
transform: translateX(0);
}
20% {
transform: translateX(1px);
clip-path: inset(40% 0 0 0);
}
25% {
transform: translateX(0);
}
30% {
transform: translateX(-1px);
clip-path: inset(20% 0 20% 0);
}
35% {
transform: translateX(0);
}
}
/* Scan line animation for tree nodes only */
@keyframes nier-button-scan {
0% {
opacity: 0.5;
left: -100%;
}
100% {
opacity: 0;
left: 100%;
}
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.chip-cell {
height: 12px;
@@ -263,14 +192,14 @@
}
}
/* Touch device optimization for tree nodes only */
/* Touch devices */
@media (hover: none) {
.tree-node-item:active::before {
width: 100%;
}
.tree-node-item:active .scan-line {
animation: nier-button-scan 0.3s ease forwards;
animation: scanLine 0.3s ease forwards;
}
.tree-node-item:active {

View File

@@ -0,0 +1,248 @@
/* ===================== CONTACT PAGE ANIMATIONS ===================== */
/* Core keyframes - reused across components */
@keyframes slideInLeft {
from {
opacity: 0;
transform: translate3d(-30px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scanLine {
from { transform: translate3d(-100%, 0, 0); opacity: 0; }
50% { opacity: 1; }
to { transform: translate3d(100%, 0, 0); opacity: 0; }
}
/* ===================== CONTACT ELEMENTS ===================== */
/* Content container */
.contact-content-container {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.4s forwards;
}
/* Availability status */
.availability-status {
opacity: 0;
animation: slideInLeft 0.6s ease-out 0.6s forwards;
}
/* Status dot */
.status-dot {
opacity: 0;
animation: scaleIn 0.4s ease-out 0.8s forwards;
}
/* Contact grid */
.contact-grid {
opacity: 0;
animation: slideInUp 0.8s ease-out 0.8s forwards;
}
/* Contact items */
.contact-item {
opacity: 0;
animation: scaleIn 0.8s ease-out forwards;
position: relative;
}
.contact-item:nth-child(1) { animation-delay: 1.0s; }
.contact-item:nth-child(2) { animation-delay: 1.1s; }
.contact-item:nth-child(3) { animation-delay: 1.2s; }
/* Scan line effect */
.contact-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--color-nier-accent), transparent);
animation: scanLine 1.5s ease-out forwards;
z-index: 20;
}
.contact-item:nth-child(1)::before { animation-delay: 1.2s; }
.contact-item:nth-child(2)::before { animation-delay: 1.3s; }
.contact-item:nth-child(3)::before { animation-delay: 1.4s; }
/* Contact icons */
.contact-icon {
opacity: 0;
animation: scaleIn 0.5s ease-out forwards;
}
.contact-item:nth-child(1) .contact-icon { animation-delay: 1.3s; }
.contact-item:nth-child(2) .contact-icon { animation-delay: 1.4s; }
.contact-item:nth-child(3) .contact-icon { animation-delay: 1.5s; }
/* Contact labels and values */
.contact-label,
.contact-value {
opacity: 0;
animation: slideInLeft 0.4s ease-out forwards;
}
.contact-item:nth-child(1) .contact-label { animation-delay: 1.4s; }
.contact-item:nth-child(2) .contact-label { animation-delay: 1.5s; }
.contact-item:nth-child(3) .contact-label { animation-delay: 1.6s; }
.contact-item:nth-child(1) .contact-value { animation-delay: 1.5s; }
.contact-item:nth-child(2) .contact-value { animation-delay: 1.6s; }
.contact-item:nth-child(3) .contact-value { animation-delay: 1.7s; }
/* Info panels container */
.info-panels-container {
opacity: 0;
animation: slideInUp 0.8s ease-out 1.4s forwards;
}
/* Info panels */
.info-panel {
opacity: 0;
animation: scaleIn 0.6s ease-out forwards;
}
.info-panel:nth-child(1) { animation-delay: 1.6s; }
.info-panel:nth-child(2) { animation-delay: 1.7s; }
/* Info panel titles and content */
.info-panel-title {
opacity: 0;
animation: slideInLeft 0.4s ease-out forwards;
}
.info-panel-content {
opacity: 0;
animation: fadeIn 0.4s ease-out forwards;
}
.info-panel:nth-child(1) .info-panel-title { animation-delay: 1.8s; }
.info-panel:nth-child(2) .info-panel-title { animation-delay: 1.9s; }
.info-panel:nth-child(1) .info-panel-content { animation-delay: 1.9s; }
.info-panel:nth-child(2) .info-panel-content { animation-delay: 2.0s; }
/* ===================== HOVER EFFECTS ===================== */
.contact-item:hover {
transform: translateY(-3px);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.2),
0 0 20px rgba(90, 90, 80, 0.1);
transition: all 0.3s ease;
}
.contact-item:hover .contact-icon {
transform: scale(1.05);
box-shadow: 0 0 15px rgba(90, 90, 80, 0.3);
transition: all 0.3s ease;
}
/* ===================== BACKGROUND PATTERN ===================== */
.contact-content-container::before {
content: '';
position: absolute;
inset: 0;
opacity: 1;
background-image:
linear-gradient(45deg, rgba(90, 90, 80, 0.02) 25%, transparent 25%),
linear-gradient(-45deg, rgba(90, 90, 80, 0.02) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(90, 90, 80, 0.02) 75%),
linear-gradient(-45deg, transparent 75%, rgba(90, 90, 80, 0.02) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
pointer-events: none;
z-index: -1;
}
/* ===================== RESPONSIVE DESIGN ===================== */
@media (max-width: 768px) {
.contact-content-container { animation-delay: 0.3s; }
.availability-status { animation-delay: 0.4s; }
.contact-grid { animation-delay: 0.6s; }
.contact-item:nth-child(1) { animation-delay: 0.8s; }
.contact-item:nth-child(2) { animation-delay: 0.9s; }
\contact-item:nth-child(3) { animation-delay: 1.0s; }
.info-panels-container { animation-delay: 1.2s; }
.info-panel:nth-child(1) { animation-delay: 1.3s; }
.info-panel:nth-child(2) { animation-delay: 1.4s; }
/* Faster animations on mobile */
.contact-content-container,
.availability-status,
.contact-grid,
.contact-item,
.info-panels-container,
.info-panel {
animation-duration: 0.6s;
}
.status-dot,
.contact-icon,
.contact-label,
.contact-value,
.info-panel-title,
.info-panel-content {
animation-duration: 0.3s;
}
}
/* ===================== ACCESSIBILITY ===================== */
@media (prefers-reduced-motion: reduce) {
*[class*="contact-"],
*[class*="info-"],
.availability-status,
.status-dot {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
.contact-item::before,
.contact-content-container::before {
display: none !important;
}
.contact-item:hover {
transform: none;
}
}

View File

@@ -0,0 +1,99 @@
<!-- Contact content in separate container, isolated from section title -->
<div class="flex justify-center w-full contact-content-container">
<div class="max-w-5xl w-full p-6">
<!-- Availability Status -->
<div class="inline-flex items-center gap-2 bg-nier-dark text-nier-light px-5 py-3 border border-nier-border mb-12 font-terminal text-xs uppercase tracking-wide availability-status">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse status-dot"></div>
AVAILABLE FOR OPPORTUNITIES
</div>
<!-- Contact Grid -->
<div class="grid gap-6 w-full contact-grid">
<!-- Primary Communication Channel -->
<a
href="mailto:adam.benyekkou@example.com"
class="bg-nier-bg border border-nier-border p-8 flex items-center gap-6 text-nier-dark no-underline transition-all duration-200 hover:bg-nier-highlight hover:text-nier-light hover:border-nier-accent group contact-item relative"
>
<div class="w-14 h-14 bg-nier-mid border border-nier-border flex items-center justify-center text-xl text-nier-dark flex-shrink-0 transition-all duration-200 group-hover:bg-nier-light group-hover:text-nier-dark contact-icon">
<i class="fas fa-envelope"></i>
</div>
<div class="flex-1">
<div class="font-terminal font-medium text-base text-nier-dark uppercase tracking-wide mb-2 group-hover:text-nier-light contact-label">
PRIMARY COMMUNICATION CHANNEL
</div>
<div class="font-noto-jp text-base text-nier-mid leading-relaxed group-hover:text-nier-light contact-value">
adam.benyekkou&#64;example.com<br>
Secure encrypted transmission protocol enabled
</div>
</div>
</a>
<!-- Professional Network Access -->
<a
href="https://linkedin.com/in/adambnk"
target="_blank"
class="bg-nier-bg border border-nier-border p-8 flex items-center gap-6 text-nier-dark no-underline transition-all duration-200 hover:bg-nier-highlight hover:text-nier-light hover:border-nier-accent group contact-item relative"
>
<div class="w-14 h-14 bg-nier-mid border border-nier-border flex items-center justify-center text-xl text-nier-dark flex-shrink-0 transition-all duration-200 group-hover:bg-nier-light group-hover:text-nier-dark contact-icon">
<i class="fab fa-linkedin-in"></i>
</div>
<div class="flex-1">
<div class="font-terminal font-medium text-base text-nier-dark uppercase tracking-wide mb-2 group-hover:text-nier-light contact-label">
PROFESSIONAL NETWORK ACCESS
</div>
<div class="font-noto-jp text-base text-nier-mid leading-relaxed group-hover:text-nier-light contact-value">
LinkedIn professional interface<br>
Connect for career opportunities and networking
</div>
</div>
</a>
<!-- Source Code Repository -->
<a
href="https://github.com/adam-benyekkou"
target="_blank"
class="bg-nier-bg border border-nier-border p-8 flex items-center gap-6 text-nier-dark no-underline transition-all duration-200 hover:bg-nier-highlight hover:text-nier-light hover:border-nier-accent group contact-item relative"
>
<div class="w-14 h-14 bg-nier-mid border border-nier-border flex items-center justify-center text-xl text-nier-dark flex-shrink-0 transition-all duration-200 group-hover:bg-nier-light group-hover:text-nier-dark contact-icon">
<i class="fab fa-github"></i>
</div>
<div class="flex-1">
<div class="font-terminal font-medium text-base text-nier-dark uppercase tracking-wide mb-2 group-hover:text-nier-light contact-label">
SOURCE CODE REPOSITORY
</div>
<div class="font-noto-jp text-base text-nier-mid leading-relaxed group-hover:text-nier-light contact-value">
GitHub development archives<br>
Access project source files and contribution history
</div>
</div>
</a>
</div>
<!-- Info Panels -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12 w-full info-panels-container">
<!-- Response Protocol -->
<div class="bg-nier-mid border border-nier-border p-6 info-panel">
<div class="font-terminal text-base font-medium text-nier-dark mb-3 uppercase tracking-wide info-panel-title">
RESPONSE PROTOCOL
</div>
<div class="font-noto-jp text-base text-nier-dark leading-relaxed info-panel-content">
Standard response time: 24-48 hour cycle<br>
Emergency contact: Email protocol recommended<br>
Priority inquiries: LinkedIn messaging for urgent requests
</div>
</div>
<!-- Collaboration Status -->
<div class="bg-nier-mid border border-nier-border p-6 info-panel">
<div class="font-terminal text-base font-medium text-nier-dark mb-3 uppercase tracking-wide info-panel-title">
COLLABORATION STATUS
</div>
<div class="font-noto-jp text-base text-nier-dark leading-relaxed info-panel-content">
Currently seeking: Full-time opportunities, internships, freelance projects<br>
Specialization: Web development, Angular, modern tech stack<br>
Location: Remote-ready, French hideout base of operations
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-contact-content',
imports: [],
templateUrl: './contact-content.component.html',
styleUrl: './contact-content.component.css'
})
export class ContactContentComponent {
}

View File

@@ -1,3 +1,9 @@
<section class="border-b-1 h-screen flex items-top justify-top checkered-bg p-6 relative">
<section class="min-h-screen bg-nier-bg checkered-background">
<!-- Section title with animation - separate container -->
<div class="pt-8 pl-8 pb-8 bg-nier-bg checkered-background section-title-container">
<app-section-title title="TRANSMISSION LINKS" />
</div>
<!-- Contact content - completely separate container -->
<app-contact-content />
</section>

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core';
import { SectionTitleComponent } from '../../components/section-title/section-title.component';
import { ContactContentComponent } from './contact-content/contact-content.component';
@Component({
selector: 'app-contact',
imports: [SectionTitleComponent],
imports: [SectionTitleComponent, ContactContentComponent],
templateUrl: './contact.component.html',
styleUrl: './contact.component.css',
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
/* ===================== OPTIMIZED GLOBAL THEME ===================== */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&family=Major+Mono+Display&family=Noto+Sans+JP:wght@300;400;500&family=VT323&display=swap');
@layer theme, tailwind-base, tailwind-utilities;
@@ -14,146 +16,108 @@
--font-terminal-nier: 'Major Mono Display', monospace;
--font-noto-jp: 'Noto Sans JP', sans-serif;
/* Authentic NieR Automata UI colors from screenshot */
--color-nier-bg: #dcd8c0; /* Main parchment background */
--color-nier-dark: #5a5a50; /* Dark UI elements */
--color-nier-mid: #adaba0; /* Medium gray UI elements */
--color-nier-text-dark: #292925; /* Dark text */
--color-nier-text-light: #dcd8c0; /* Light text */
--color-nier-border: #a39e8c; /* Border color */
--color-nier-grid: #bfbcad; /* Grid lines */
--color-nier-accent: #292925; /* Dark accent */
--color-nier-highlight: #5a5a50; /* Highlight for selection */
/* Checkered background colors */
--color-nier-checkered-bg: #d1cdb7; /* Slightly darker than main bg */
--color-nier-checkered-grid: #ccc8b1; /* Grid lines for checkered pattern */
/* NieR Automata UI colors */
--color-nier-bg: #dcd8c0;
--color-nier-dark: #5a5a50;
--color-nier-mid: #adaba0;
--color-nier-text-dark: #292925;
--color-nier-text-light: #dcd8c0;
--color-nier-border: #a39e8c;
--color-nier-grid: #bfbcad;
--color-nier-accent: #292925;
--color-nier-highlight: #5a5a50;
--color-nier-checkered-bg: #d1cdb7;
--color-nier-checkered-grid: #ccc8b1;
}
}
@media (prefers-color-scheme: dark) {
/* Dark mode variables */
@media (prefers-color-scheme: dark) {
:root {
/* Inverted colors for dark mode */
--color-nier-bg: #292925; /* Inverted from text-dark */
--color-nier-dark: #adaba0; /* Inverted from mid */
--color-nier-mid: #5a5a50; /* Inverted from dark */
--color-nier-text-dark: #dcd8c0; /* Inverted from bg */
--color-nier-text-light: #292925; /* Inverted from text-dark */
--color-nier-border: #5a5a50; /* Darkened border */
--color-nier-grid: #3f3e39; /* Darkened grid */
--color-nier-accent: #dcd8c0; /* Inverted from accent */
--color-nier-highlight: #adaba0; /* Inverted from highlight */
--color-nier-bg: #292925;
--color-nier-dark: #adaba0;
--color-nier-mid: #5a5a50;
--color-nier-text-dark: #dcd8c0;
--color-nier-text-light: #292925;
--color-nier-border: #5a5a50;
--color-nier-grid: #3f3e39;
--color-nier-accent: #dcd8c0;
--color-nier-highlight: #adaba0;
--color-nier-checkered-bg: #121210;
--color-nier-checkered-grid: #323232;
}
}
/* Inverted checkered background colors */
--color-nier-checkered-bg: #121210; /* Dark background for checkered pattern */
--color-nier-checkered-grid: #323232; /* Grid lines for dark checkered pattern */
/* Manual theme classes */
.dark {
--color-nier-bg: #292925;
--color-nier-dark: #adaba0;
--color-nier-mid: #5a5a50;
--color-nier-text-dark: #dcd8c0;
--color-nier-text-light: #292925;
--color-nier-border: #5a5a50;
--color-nier-grid: #3f3e39;
--color-nier-accent: #dcd8c0;
--color-nier-highlight: #adaba0;
--color-nier-checkered-bg: #121210;
--color-nier-checkered-grid: #323232;
}
.light {
--color-nier-bg: #dcd8c0;
--color-nier-dark: #5a5a50;
--color-nier-mid: #adaba0;
--color-nier-text-dark: #292925;
--color-nier-text-light: #dcd8c0;
--color-nier-border: #a39e8c;
--color-nier-grid: #bfbcad;
--color-nier-accent: #292925;
--color-nier-highlight: #5a5a50;
--color-nier-checkered-bg: #d1cdb7;
--color-nier-checkered-grid: #ccc8b1;
}
}
/* Manual dark mode class for toggle functionality */
.dark {
/* Same inverted colors as media query */
--color-nier-bg: #292925; /* Inverted from text-dark */
--color-nier-dark: #adaba0; /* Inverted from mid */
--color-nier-mid: #5a5a50; /* Inverted from dark */
--color-nier-text-dark: #dcd8c0; /* Inverted from bg */
--color-nier-text-light: #292925; /* Inverted from text-dark */
--color-nier-border: #5a5a50; /* Darkened border */
--color-nier-grid: #3f3e39; /* Darkened grid */
--color-nier-accent: #dcd8c0; /* Inverted from accent */
--color-nier-highlight: #adaba0; /* Inverted from highlight */
/* Inverted checkered background colors */
--color-nier-checkered-bg: #121210; /* Dark background for checkered pattern */
--color-nier-checkered-grid: #323232; /* Grid lines for dark checkered pattern */
}
/* Light mode class to override system preference */
.light {
/* Original colors */
--color-nier-bg: #dcd8c0; /* Main parchment background */
--color-nier-dark: #5a5a50; /* Dark UI elements */
--color-nier-mid: #adaba0; /* Medium gray UI elements */
--color-nier-text-dark: #292925; /* Dark text */
--color-nier-text-light: #dcd8c0; /* Light text */
--color-nier-border: #a39e8c; /* Border color */
--color-nier-grid: #bfbcad; /* Grid lines */
--color-nier-accent: #292925; /* Dark accent */
--color-nier-highlight: #5a5a50; /* Highlight for selection */
/* Checkered background colors */
--color-nier-checkered-bg: #d1cdb7; /* Slightly darker than main bg */
--color-nier-checkered-grid: #ccc8b1; /* Grid lines for checkered pattern */
}
@layer tailwind-utilities {
/* Font utilities */
.font-terminal {
font-family: var(--font-terminal);
}
.font-terminal-retro {
font-family: var(--font-terminal-retro);
}
.font-terminal-nier {
font-family: var(--font-terminal-nier);
}
.font-terminal { font-family: var(--font-terminal); }
.font-terminal-retro { font-family: var(--font-terminal-retro); }
.font-terminal-nier { font-family: var(--font-terminal-nier); }
.font-noto-jp {
font-family: var(--font-noto-jp);
letter-spacing: -0.02em; /* Slight adjustment to mimic NieR style */
letter-spacing: -0.02em;
}
/* Background utilities */
.bg-nier-bg {
background-color: var(--color-nier-bg);
}
.bg-nier-dark {
background-color: var(--color-nier-dark);
}
.bg-nier-mid {
background-color: var(--color-nier-mid);
}
.bg-nier-bg { background-color: var(--color-nier-bg); }
.bg-nier-dark { background-color: var(--color-nier-dark); }
.bg-nier-mid { background-color: var(--color-nier-mid); }
/* Text utilities */
.text-nier-dark {
color: var(--color-nier-text-dark);
}
.text-nier-light {
color: var(--color-nier-text-light);
}
.text-nier-dark { color: var(--color-nier-text-dark); }
.text-nier-light { color: var(--color-nier-text-light); }
/* Border utilities */
.border-nier-border {
border-color: var(--color-nier-border);
}
.border-nier-border { border-color: var(--color-nier-border); }
.border-nier-accent { border-color: var(--color-nier-accent); }
.border-nier-accent {
border-color: var(--color-nier-accent);
}
/* Checkered background using theme variables */
/* Pattern utilities */
.checkered-background {
background-color: var(--color-nier-checkered-bg);
background-size: 0.3rem 0.3rem;
background-image: linear-gradient(to right, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, rgba(0, 0, 0, 0) 1px);
background-image:
linear-gradient(to right, var(--color-nier-checkered-grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--color-nier-checkered-grid) 1px, transparent 1px);
color: var(--color-nier-text-dark);
transition: background-color 0.3s ease, background-image 0.3s ease, color 0.3s ease;
transition: all 0.3s ease;
}
/* Additional utility for grid-based layouts */
.nier-grid {
display: grid;
background-color: var(--color-nier-bg);
background-size: 2rem 2rem;
background-image: linear-gradient(to right, var(--color-nier-grid) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(to bottom, var(--color-nier-grid) 1px, rgba(0, 0, 0, 0) 1px);
background-image:
linear-gradient(to right, var(--color-nier-grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--color-nier-grid) 1px, transparent 1px);
border: 1px solid var(--color-nier-border);
transition: all 0.3s ease;
}