commit e3e83a3f406ade1d0fbcaf84606976d372538909 Author: José Henrique Date: Mon Aug 18 16:13:05 2025 -0300 initial diff --git a/assets/lain.png b/assets/lain.png new file mode 100644 index 0000000..489087e Binary files /dev/null and b/assets/lain.png differ diff --git a/css/content.css b/css/content.css new file mode 100644 index 0000000..2ae66f5 --- /dev/null +++ b/css/content.css @@ -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; +} diff --git a/css/matrix.css b/css/matrix.css new file mode 100644 index 0000000..d745438 --- /dev/null +++ b/css/matrix.css @@ -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); + } +} diff --git a/css/sections/hero.css b/css/sections/hero.css new file mode 100644 index 0000000..525f5f4 --- /dev/null +++ b/css/sections/hero.css @@ -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; + } +} \ No newline at end of file diff --git a/css/sections/homelab.css b/css/sections/homelab.css new file mode 100644 index 0000000..df39ce3 --- /dev/null +++ b/css/sections/homelab.css @@ -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); +} diff --git a/css/sections/profile.css b/css/sections/profile.css new file mode 100644 index 0000000..76eeb4c --- /dev/null +++ b/css/sections/profile.css @@ -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; +} diff --git a/css/sections/projects.css b/css/sections/projects.css new file mode 100644 index 0000000..0e83c79 --- /dev/null +++ b/css/sections/projects.css @@ -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%; + } +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..61f7b7b --- /dev/null +++ b/css/style.css @@ -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; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb20924 --- /dev/null +++ b/index.html @@ -0,0 +1,108 @@ + + + + + + ivanch - Portfolio + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ Avatar +
+
+
+ + + UTFPR + Informations Systems + +
+
+ + + EposNow + Software Engineer + +
+
+ + 5+ years +
+
+
+
+
+
+

ivanch

+

Developer and Self Hoster

+ +
+
+

Homelab

+ +
+
+
+

Projects

+
+
OpenCand
+
Brazilian Candidate Viewer.
+
+
+
Vision Start
+
Chrome Startpage.
+
+
+
Kasbot
+
Discord DJ bot.
+
+
+
Candlebright
+
C++ project game.
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/js/matrix.js b/js/matrix.js new file mode 100644 index 0000000..a47ff01 --- /dev/null +++ b/js/matrix.js @@ -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(); +}); diff --git a/js/social.js b/js/social.js new file mode 100644 index 0000000..3936baf --- /dev/null +++ b/js/social.js @@ -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); + }); + } +}); \ No newline at end of file diff --git a/utils/xor-enc.py b/utils/xor-enc.py new file mode 100644 index 0000000..2e82113 --- /dev/null +++ b/utils/xor-enc.py @@ -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()