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

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