This commit is contained in:
2025-08-18 16:13:05 -03:00
commit e3e83a3f40
12 changed files with 1054 additions and 0 deletions

BIN
assets/lain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

97
css/content.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()