initial
This commit is contained in:
BIN
assets/lain.png
Normal file
BIN
assets/lain.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
97
css/content.css
Normal file
97
css/content.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* content.css - Layout, glassmorphism, and content styles */
|
||||
.glass {
|
||||
background: rgba(20, 20, 20, 0.125);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.125);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.10);
|
||||
backdrop-filter: blur(32px) saturate(180%) brightness(1.1);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(180%) brightness(1.1);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25), inset 0 1px 0 rgba(255,255,255,0.15);
|
||||
padding: 32px 24px;
|
||||
transition: background 0.3s, box-shadow 0.3s;
|
||||
min-width: 26rem;
|
||||
max-width: 26rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.email-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
width: max-content;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Stylish tooltip for email copy */
|
||||
.email-tooltip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -32px;
|
||||
transform: translateX(-50%) scale(1);
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
}
|
58
css/matrix.css
Normal file
58
css/matrix.css
Normal file
@@ -0,0 +1,58 @@
|
||||
/* matrix.css - Matrix animation styles */
|
||||
.matrix-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
background-image: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(20,20,40,0.7) 100%);
|
||||
filter: brightness(0.55);
|
||||
}
|
||||
|
||||
.matrix-dot {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(255, 255, 255, 0.4), 0 0 12px rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(100, 149, 237, 0) 0%,
|
||||
rgba(138, 43, 226, 0.8) 50%,
|
||||
rgba(100, 149, 237, 0) 100%);
|
||||
transform-origin: left center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
|
||||
.connection-line.active {
|
||||
opacity: 1;
|
||||
animation: connectionPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes connectionPulse {
|
||||
0% {
|
||||
filter: brightness(1);
|
||||
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.5);
|
||||
box-shadow: 0 0 8px rgba(138, 43, 226, 0.6), 0 0 16px rgba(100, 149, 237, 0.3);
|
||||
}
|
||||
100% {
|
||||
filter: brightness(1);
|
||||
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
}
|
153
css/sections/hero.css
Normal file
153
css/sections/hero.css
Normal file
@@ -0,0 +1,153 @@
|
||||
.hero-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 32px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
.hero-main-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 48px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 32px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
padding: 12px 20px;
|
||||
background: rgba(10, 18, 40, 0.85);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
box-shadow: 0 2px 16px 0 rgba(31, 38, 135, 0.12);
|
||||
transition: background 0.18s, transform 0.18s;
|
||||
}
|
||||
/* Modern underline effect under text */
|
||||
.social-link span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
.social-link span::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #fff 0%, #667eea 100%);
|
||||
border-radius: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
opacity: 0.7;
|
||||
transition: height 0.18s, opacity 0.18s;
|
||||
}
|
||||
.social-link:hover {
|
||||
background: rgba(20, 30, 60, 0.95);
|
||||
transform: translateY(-1px) scale(1.03);
|
||||
}
|
||||
.social-link:hover span::after {
|
||||
height: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-details {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.hero-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 32px;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
margin: 0 auto;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-card {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
68
css/sections/homelab.css
Normal file
68
css/sections/homelab.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.homelab-section {
|
||||
margin-top: 18px;
|
||||
padding: 32px 24px;
|
||||
background: rgba(20, 20, 30, 0.10);
|
||||
backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18), inset 0 1px 0 rgba(255,255,255,0.10);
|
||||
min-width: 26rem;
|
||||
max-width: 26rem;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.homelab-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 0.03em;
|
||||
background: linear-gradient(270deg, #764ba2, #2b2e6c, #764ba2, #2b2e6c);
|
||||
background-size: 400% 400%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: homelab-gradient-move 12s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes homelab-gradient-move {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
.homelab-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.homelab-btn {
|
||||
padding: 12px 28px;
|
||||
background: rgba(10, 18, 40, 0.85);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.02em;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 24px 0 rgba(31, 38, 135, 0.18), 0 1.5px 0 rgba(255,255,255,0.08);
|
||||
transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.18s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.homelab-btn:hover {
|
||||
background: rgba(20, 30, 60, 0.95);
|
||||
transform: translateY(-2px) scale(1.04);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.28);
|
||||
}
|
||||
.homelab-btn:active {
|
||||
transform: translateY(0px) scale(1.0);
|
||||
}
|
78
css/sections/profile.css
Normal file
78
css/sections/profile.css
Normal file
@@ -0,0 +1,78 @@
|
||||
/* Profile Info Section Styles */
|
||||
.profile-info {
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.profile-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.95rem;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.profile-info-item i {
|
||||
font-size: 1.3rem;
|
||||
color: #4fc3f7;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.profile-info-label {
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.profile-info-value {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.profile-info-value span {
|
||||
display: block;
|
||||
}
|
||||
/* profile.css - Styles for avatar and avatar-container */
|
||||
|
||||
.profile-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.avatar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 24px;
|
||||
}
|
74
css/sections/projects.css
Normal file
74
css/sections/projects.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.projects-section {
|
||||
flex: 0 0 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 32px 24px;
|
||||
background: rgba(20, 20, 30, 0.10);
|
||||
backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18), inset 0 1px 0 rgba(255,255,255,0.10);
|
||||
min-width: 280px;
|
||||
max-width: 340px;
|
||||
margin-left: 20px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.projects-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
color: #fff;
|
||||
letter-spacing: 0.03em;
|
||||
background: linear-gradient(135deg, #667eea 0%, #fff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.project {
|
||||
padding: 18px 16px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px 0 rgba(31, 38, 135, 0.10);
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
}
|
||||
.project:nth-child(odd) {
|
||||
background: linear-gradient(135deg, rgba(20, 30, 60, 0.75) 0%, rgba(40, 50, 120, 0.75) 100%);
|
||||
}
|
||||
.project:nth-child(even) {
|
||||
background: linear-gradient(135deg, rgba(10, 10, 10, 0.82) 0%, rgba(30, 30, 30, 0.82) 100%);
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
letter-spacing: 0.01em;
|
||||
background: linear-gradient(90deg, #fff 0%, #667eea 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
font-size: 0.98rem;
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.projects-section {
|
||||
flex: unset;
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
margin-top: 24px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
17
css/style.css
Normal file
17
css/style.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/* style.css - Glassmorphism and layout styles */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; */
|
||||
font-family: "Lexend", sans-serif;
|
||||
background: #000000;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
108
index.html
Normal file
108
index.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ivanch - Portfolio</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/button.css">
|
||||
<link rel="stylesheet" href="css/matrix.css">
|
||||
<link rel="stylesheet" href="css/content.css">
|
||||
|
||||
<link rel="stylesheet" href="css/sections/profile.css">
|
||||
|
||||
<link rel="stylesheet" href="css/sections/hero.css">
|
||||
<link rel="stylesheet" href="css/sections/projects.css">
|
||||
<link rel="stylesheet" href="css/sections/homelab.css">
|
||||
<!-- Remix Icon CDN for social icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Matrix-like background animation -->
|
||||
<div class="matrix-canvas" id="matrixCanvas"></div>
|
||||
|
||||
<!-- Main hero section -->
|
||||
<main class="main-container">
|
||||
<div class="hero-card glass">
|
||||
<div class="profile-container">
|
||||
<div class="avatar">
|
||||
<img src="/assets/lain.png" alt="Avatar" />
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-info-item">
|
||||
<i class="ri-graduation-cap-fill"></i>
|
||||
<span class="profile-info-value">
|
||||
<span>UTFPR</span>
|
||||
<span>Informations Systems</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="profile-info-item">
|
||||
<i class="ri-building-2-fill"></i>
|
||||
<span class="profile-info-value">
|
||||
<span>EposNow</span>
|
||||
<span>Software Engineer</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="profile-info-item">
|
||||
<i class="ri-book-2-fill"></i>
|
||||
<span class="profile-info-value">5+ years</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
<div class="hero-main-column">
|
||||
<div class="content">
|
||||
<h2 class="name">ivanch</h2>
|
||||
<p class="description">Developer and Self Hoster</p>
|
||||
<div class="social-links">
|
||||
<a href="https://www.linkedin.com/in/joseivanch/" class="social-link" target="_blank">
|
||||
<i class="ri-linkedin-fill"></i> <span>LinkedIn</span>
|
||||
</a>
|
||||
<a href="#" class="social-link" id="email-link">
|
||||
<i class="ri-mail-fill"></i> <span>Email</span>
|
||||
</a>
|
||||
<span class="email-tooltip" id="email-tooltip">Copied!</span>
|
||||
<a href="https://github.com/ivanch" class="social-link" target="_blank">
|
||||
<i class="ri-github-fill"></i> <span>GitHub</span>
|
||||
</a>
|
||||
<a href="https://git.ivanch.me/ivanch" class="social-link" target="_blank">
|
||||
<i class="ri-git-repository-fill"></i> <span>Gitea</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="homelab-section">
|
||||
<h3 class="homelab-title">Homelab</h3>
|
||||
<div class="homelab-buttons">
|
||||
<a href="#" class="homelab-btn">Diagram</a>
|
||||
<a href="#" class="homelab-btn">Main Applications</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-section">
|
||||
<h3 class="projects-title">Projects</h3>
|
||||
<div class="project">
|
||||
<div class="project-title">OpenCand</div>
|
||||
<div class="project-desc">Brazilian Candidate Viewer.</div>
|
||||
</div>
|
||||
<div class="project">
|
||||
<div class="project-title">Vision Start</div>
|
||||
<div class="project-desc">Chrome Startpage.</div>
|
||||
</div>
|
||||
<div class="project">
|
||||
<div class="project-title">Kasbot</div>
|
||||
<div class="project-desc">Discord DJ bot.</div>
|
||||
</div>
|
||||
<div class="project">
|
||||
<div class="project-title">Candlebright</div>
|
||||
<div class="project-desc">C++ project game.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="js/matrix.js"></script>
|
||||
<script src="js/social.js"></script>
|
||||
</body>
|
||||
</html>
|
314
js/matrix.js
Normal file
314
js/matrix.js
Normal file
@@ -0,0 +1,314 @@
|
||||
class MatrixBackground {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('matrixCanvas');
|
||||
this.dots = [];
|
||||
this.connections = [];
|
||||
this.config = {
|
||||
dotCount: 50,
|
||||
dotSpeed: 0.2, // pixels per frame
|
||||
connectionDuration: { min: 2000, max: 6000 }, // milliseconds
|
||||
connectionChance: 0.0002, // chance per frame per dot pair
|
||||
maxConnections: 8, // maximum simultaneous connections
|
||||
dotSize: 3,
|
||||
colors: {
|
||||
dotDefault: 'rgba(255, 255, 255, 0.8)',
|
||||
dotDefaultGlow: 'rgba(255, 255, 255, 0.4)',
|
||||
dotConnected: 'rgba(100, 149, 237, 0.8)',
|
||||
dotConnectedGlow: 'rgba(100, 149, 237, 0.6)',
|
||||
connection: {
|
||||
start: 'rgba(100, 149, 237, 0)',
|
||||
middle: 'rgba(138, 43, 226, 0.8)',
|
||||
end: 'rgba(100, 149, 237, 0)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createDots();
|
||||
this.animate();
|
||||
this.handleResize();
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => this.handleResize());
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
// Recreate dots with new screen dimensions
|
||||
// Clear previous DOM elements to avoid orphan/sticky dots
|
||||
this.clearCanvas();
|
||||
this.createDots();
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
// Remove all dot and connection elements from the canvas
|
||||
try {
|
||||
while (this.canvas.firstChild) {
|
||||
this.canvas.removeChild(this.canvas.firstChild);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// Reset arrays
|
||||
this.dots = [];
|
||||
this.connections = [];
|
||||
}
|
||||
|
||||
createDots() {
|
||||
this.dots = [];
|
||||
for (let i = 0; i < this.config.dotCount; i++) {
|
||||
this.createDot();
|
||||
}
|
||||
}
|
||||
|
||||
createDot() {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'matrix-dot';
|
||||
|
||||
// Random starting position within screen bounds
|
||||
const startX = Math.random() * (window.innerWidth - 20) + 10;
|
||||
const startY = Math.random() * (window.innerHeight - 20) + 10;
|
||||
|
||||
// Random movement direction (flying in all directions)
|
||||
let velocityX = 0, velocityY = 0;
|
||||
let attempts = 0;
|
||||
while ((Math.abs(velocityX) < 0.15 || Math.abs(velocityY) < 0.15) && attempts < 10) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); // Ensure minimum speed
|
||||
velocityX = Math.cos(angle) * speed;
|
||||
velocityY = Math.sin(angle) * speed;
|
||||
attempts++;
|
||||
}
|
||||
// Final fallback if still too low
|
||||
if (Math.abs(velocityX) < 0.15) velocityX = velocityX < 0 ? -0.15 : 0.15;
|
||||
if (Math.abs(velocityY) < 0.15) velocityY = velocityY < 0 ? -0.15 : 0.15;
|
||||
const dotData = {
|
||||
element: dot,
|
||||
x: startX,
|
||||
y: startY,
|
||||
vx: velocityX,
|
||||
vy: velocityY,
|
||||
opacity: Math.random() * 0.3 + 0.7,
|
||||
size: this.config.dotSize + Math.random() * 2,
|
||||
isConnected: false,
|
||||
connectionCount: 0,
|
||||
stuckFrames: 0 // Track how long dot is stuck
|
||||
};
|
||||
|
||||
// Set initial styles (white by default)
|
||||
dot.style.left = startX + 'px';
|
||||
dot.style.top = startY + 'px';
|
||||
dot.style.width = dotData.size + 'px';
|
||||
dot.style.height = dotData.size + 'px';
|
||||
dot.style.opacity = dotData.opacity;
|
||||
dot.style.background = this.config.colors.dotDefault;
|
||||
dot.style.boxShadow = `0 0 6px ${this.config.colors.dotDefaultGlow}, 0 0 12px rgba(255, 255, 255, 0.2)`;
|
||||
|
||||
this.canvas.appendChild(dot);
|
||||
this.dots.push(dotData);
|
||||
}
|
||||
|
||||
updateDots() {
|
||||
this.dots.forEach((dot, index) => {
|
||||
// Update position
|
||||
dot.x += dot.vx;
|
||||
dot.y += dot.vy;
|
||||
|
||||
// Track if dot is stuck (velocity very low for several frames)
|
||||
if (Math.abs(dot.vx) < 0.03 && Math.abs(dot.vy) < 0.03) {
|
||||
dot.stuckFrames = (dot.stuckFrames || 0) + 1;
|
||||
} else {
|
||||
dot.stuckFrames = 0;
|
||||
}
|
||||
// If stuck for more than 30 frames, give a strong nudge
|
||||
if (dot.stuckFrames > 30) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4);
|
||||
dot.vx = Math.cos(angle) * speed;
|
||||
dot.vy = Math.sin(angle) * speed;
|
||||
dot.stuckFrames = 0;
|
||||
} else {
|
||||
// Prevent dots from getting stuck - add small random nudge if velocity is too low
|
||||
if (Math.abs(dot.vx) < 0.05) {
|
||||
dot.vx += (Math.random() - 0.5) * 0.1;
|
||||
}
|
||||
if (Math.abs(dot.vy) < 0.05) {
|
||||
dot.vy += (Math.random() - 0.5) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce off screen edges (consider dot size so centers stay inside)
|
||||
const maxX = window.innerWidth - dot.size;
|
||||
const maxY = window.innerHeight - dot.size;
|
||||
if (dot.x <= 0 || dot.x >= maxX) {
|
||||
dot.vx = -dot.vx;
|
||||
dot.x = Math.max(0, Math.min(maxX, dot.x));
|
||||
}
|
||||
if (dot.y <= 0 || dot.y >= maxY) {
|
||||
dot.vy = -dot.vy;
|
||||
dot.y = Math.max(0, Math.min(maxY, dot.y));
|
||||
}
|
||||
|
||||
// Update DOM element position (top-left)
|
||||
dot.element.style.left = dot.x + 'px';
|
||||
dot.element.style.top = dot.y + 'px';
|
||||
|
||||
// Update dot color based on connection status
|
||||
if (dot.connectionCount > 0 && !dot.isConnected) {
|
||||
dot.isConnected = true;
|
||||
dot.element.style.background = this.config.colors.dotConnected;
|
||||
dot.element.style.boxShadow = `0 0 6px ${this.config.colors.dotConnectedGlow}, 0 0 12px rgba(100, 149, 237, 0.3)`;
|
||||
} else if (dot.connectionCount === 0 && dot.isConnected) {
|
||||
dot.isConnected = false;
|
||||
dot.element.style.background = this.config.colors.dotDefault;
|
||||
dot.element.style.boxShadow = `0 0 6px ${this.config.colors.dotDefaultGlow}, 0 0 12px rgba(255, 255, 255, 0.2)`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createConnection(dot1, dot2) {
|
||||
if (this.connections.length >= this.config.maxConnections) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = document.createElement('div');
|
||||
connection.className = 'connection-line';
|
||||
|
||||
// Calculate center positions of dots
|
||||
const dot1Rect = dot1.element.getBoundingClientRect();
|
||||
const dot2Rect = dot2.element.getBoundingClientRect();
|
||||
const canvasRect = this.canvas.getBoundingClientRect();
|
||||
const dot1CenterX = dot1Rect.left - canvasRect.left + dot1Rect.width / 2;
|
||||
const dot1CenterY = dot1Rect.top - canvasRect.top + dot1Rect.height / 2;
|
||||
const dot2CenterX = dot2Rect.left - canvasRect.left + dot2Rect.width / 2;
|
||||
const dot2CenterY = dot2Rect.top - canvasRect.top + dot2Rect.height / 2;
|
||||
|
||||
const dx = dot2CenterX - dot1CenterX;
|
||||
const dy = dot2CenterY - dot1CenterY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
|
||||
// Position and style the connection line from center to center
|
||||
const lineHalf = 1;
|
||||
connection.style.left = Math.round(dot1CenterX) + 'px';
|
||||
connection.style.top = Math.round(dot1CenterY - lineHalf) + 'px';
|
||||
connection.style.width = Math.round(distance) + 'px';
|
||||
connection.style.transformOrigin = '0 50%';
|
||||
connection.style.transform = `rotate(${angle}deg)`;
|
||||
connection.style.background = `linear-gradient(90deg,
|
||||
${this.config.colors.connection.start} 0%,
|
||||
${this.config.colors.connection.middle} 50%,
|
||||
${this.config.colors.connection.end} 100%)`;
|
||||
|
||||
this.canvas.appendChild(connection);
|
||||
|
||||
// Fade in
|
||||
setTimeout(() => {
|
||||
connection.classList.add('active');
|
||||
}, 50);
|
||||
|
||||
const connectionData = {
|
||||
element: connection,
|
||||
dot1: dot1,
|
||||
dot2: dot2,
|
||||
startTime: Date.now(),
|
||||
duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + this.config.connectionDuration.min
|
||||
};
|
||||
|
||||
// Mark dots as connected
|
||||
dot1.connectionCount++;
|
||||
dot2.connectionCount++;
|
||||
|
||||
this.connections.push(connectionData);
|
||||
}
|
||||
|
||||
updateConnections() {
|
||||
this.connections.forEach((connection, index) => {
|
||||
const elapsed = Date.now() - connection.startTime;
|
||||
|
||||
// Update connection position as dots move (using center positions)
|
||||
const dot1Rect = connection.dot1.element.getBoundingClientRect();
|
||||
const dot2Rect = connection.dot2.element.getBoundingClientRect();
|
||||
const canvasRect = this.canvas.getBoundingClientRect();
|
||||
const dot1CenterX = dot1Rect.left - canvasRect.left + dot1Rect.width / 2;
|
||||
const dot1CenterY = dot1Rect.top - canvasRect.top + dot1Rect.height / 2;
|
||||
const dot2CenterX = dot2Rect.left - canvasRect.left + dot2Rect.width / 2;
|
||||
const dot2CenterY = dot2Rect.top - canvasRect.top + dot2Rect.height / 2;
|
||||
|
||||
const dx = dot2CenterX - dot1CenterX;
|
||||
const dy = dot2CenterY - dot1CenterY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
|
||||
const lineHalf = 1;
|
||||
connection.element.style.left = Math.round(dot1CenterX) + 'px';
|
||||
connection.element.style.top = Math.round(dot1CenterY - lineHalf) + 'px';
|
||||
connection.element.style.width = Math.round(distance) + 'px';
|
||||
connection.element.style.transformOrigin = '0 50%';
|
||||
connection.element.style.transform = `rotate(${angle}deg)`;
|
||||
|
||||
// Remove expired connections
|
||||
if (elapsed > connection.duration) {
|
||||
connection.element.classList.remove('active');
|
||||
|
||||
// Mark dots as no longer connected
|
||||
connection.dot1.connectionCount--;
|
||||
connection.dot2.connectionCount--;
|
||||
|
||||
setTimeout(() => {
|
||||
connection.element.remove();
|
||||
}, 500); // Wait for fade out
|
||||
this.connections.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tryCreateConnections() {
|
||||
// Randomly try to create connections between nearby dots
|
||||
for (let i = 0; i < this.dots.length; i++) {
|
||||
for (let j = i + 1; j < this.dots.length; j++) {
|
||||
if (Math.random() < this.config.connectionChance) {
|
||||
const dot1 = this.dots[i];
|
||||
const dot2 = this.dots[j];
|
||||
|
||||
// Calculate distance between dot centers
|
||||
const dot1CenterX = dot1.x + dot1.size / 2;
|
||||
const dot1CenterY = dot1.y + dot1.size / 2;
|
||||
const dot2CenterX = dot2.x + dot2.size / 2;
|
||||
const dot2CenterY = dot2.y + dot2.size / 2;
|
||||
|
||||
const dx = dot2CenterX - dot1CenterX;
|
||||
const dy = dot2CenterY - dot1CenterY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 150 && distance > 30) {
|
||||
// Check if these dots are already connected
|
||||
const alreadyConnected = this.connections.some(conn =>
|
||||
(conn.dot1 === dot1 && conn.dot2 === dot2) ||
|
||||
(conn.dot1 === dot2 && conn.dot2 === dot1)
|
||||
);
|
||||
|
||||
if (!alreadyConnected) {
|
||||
this.createConnection(dot1, dot2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.updateDots();
|
||||
this.updateConnections();
|
||||
this.tryCreateConnections();
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the matrix background when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new MatrixBackground();
|
||||
});
|
60
js/social.js
Normal file
60
js/social.js
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
// Initialize the matrix background when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new MatrixBackground();
|
||||
|
||||
// Email copy-to-clipboard and tooltip logic
|
||||
const emailLink = document.getElementById('email-link');
|
||||
const emailTooltip = document.getElementById('email-tooltip');
|
||||
const obfuscatedEmail = 'Bg4aC0IJDAAeCBgbCU8AGA0PKQkBAAACQgIGAw=='; // generated through utils/xor-enc.py
|
||||
const xorKey = 'lain';
|
||||
|
||||
function decodeEmail(obf, key) {
|
||||
// Double base64 decode
|
||||
let b64 = atob(obf);
|
||||
// XOR decode
|
||||
let email = '';
|
||||
for (let i = 0; i < b64.length; i++) {
|
||||
email += String.fromCharCode(b64.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
||||
}
|
||||
return email;
|
||||
}
|
||||
const emailAddress = decodeEmail(obfuscatedEmail, xorKey);
|
||||
|
||||
if (emailLink && emailTooltip) {
|
||||
emailLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Copy email to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(emailAddress);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = emailAddress;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// Position tooltip above email link
|
||||
const emailRect = emailLink.getBoundingClientRect();
|
||||
const socialLinksRect = emailLink.parentElement.getBoundingClientRect();
|
||||
|
||||
const left = emailRect.left - socialLinksRect.left + (emailRect.width / 2);
|
||||
const top = emailRect.top - socialLinksRect.top - 10;
|
||||
|
||||
emailTooltip.style.left = left + 'px';
|
||||
emailTooltip.style.top = top + 'px';
|
||||
emailTooltip.style.transform = 'translateX(-50%) translateY(-75%)';
|
||||
|
||||
// Show tooltip
|
||||
emailTooltip.style.opacity = '1';
|
||||
|
||||
// Hide after 1.5s
|
||||
setTimeout(() => {
|
||||
emailTooltip.style.opacity = '0';
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
});
|
27
utils/xor-enc.py
Normal file
27
utils/xor-enc.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import base64
|
||||
import sys
|
||||
|
||||
KEY = b"lain"
|
||||
|
||||
def xor_bytes(data: bytes, key: bytes) -> bytes:
|
||||
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
||||
|
||||
def encrypt(text: str) -> str:
|
||||
data = text.encode("utf-8")
|
||||
xored = xor_bytes(data, KEY)
|
||||
return base64.b64encode(xored).decode("ascii")
|
||||
|
||||
def decrypt(b64text: str) -> str:
|
||||
xored = base64.b64decode(b64text)
|
||||
return xor_bytes(xored, KEY).decode("utf-8")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
# Use command-line arguments as the text
|
||||
text = " ".join(sys.argv[1:])
|
||||
else:
|
||||
text = input("Text to encrypt: ")
|
||||
print(encrypt(text))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user