new page :3
This commit is contained in:
166
js/matrix.js
166
js/matrix.js
@@ -4,6 +4,11 @@ class MatrixBackground {
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.dots = [];
|
||||
this.connections = [];
|
||||
this.mouse = { x: -9999, y: -9999, active: false };
|
||||
this.rafId = null;
|
||||
this.isRunning = true;
|
||||
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
this.config = {
|
||||
dotCount: this.getMobileDotCount(),
|
||||
dotSpeed: 0.2,
|
||||
@@ -11,15 +16,18 @@ class MatrixBackground {
|
||||
connectionChance: 0.0002,
|
||||
maxConnections: this.getMobileMaxConnections(),
|
||||
dotSize: 3,
|
||||
mouseRadius: 110,
|
||||
mouseForce: 0.6,
|
||||
// Palette synced to the UI accent tokens
|
||||
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)',
|
||||
dotDefault: 'rgba(230, 240, 255, 0.78)',
|
||||
dotDefaultGlow: 'rgba(180, 220, 255, 0.40)',
|
||||
dotConnected: 'rgba(120, 190, 245, 0.85)',
|
||||
dotConnectedGlow: 'rgba(79, 195, 247, 0.55)',
|
||||
connection: {
|
||||
start: 'rgba(100, 149, 237, 0.2)',
|
||||
middle: 'rgba(138, 43, 226, 0.6)',
|
||||
end: 'rgba(100, 149, 237, 0.2)'
|
||||
start: 'rgba(102, 126, 234, 0.22)',
|
||||
middle: 'rgba(118, 75, 162, 0.62)',
|
||||
end: 'rgba(79, 195, 247, 0.22)'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -28,14 +36,13 @@ class MatrixBackground {
|
||||
}
|
||||
|
||||
getMobileDotCount() {
|
||||
// Reduce dot count on mobile devices for better performance
|
||||
if (window.innerWidth <= 480) return 25;
|
||||
if (window.innerWidth <= 768) return 35;
|
||||
if (window.innerWidth <= 480) return 35;
|
||||
if (window.innerWidth <= 768) return 45;
|
||||
if (window.innerWidth >= 2100) return 100;
|
||||
return 50;
|
||||
}
|
||||
|
||||
getMobileMaxConnections() {
|
||||
// Reduce max connections on mobile devices for better performance
|
||||
if (window.innerWidth <= 480) return 4;
|
||||
if (window.innerWidth <= 768) return 6;
|
||||
return 8;
|
||||
@@ -43,20 +50,61 @@ class MatrixBackground {
|
||||
|
||||
init() {
|
||||
this.handleResize();
|
||||
this.bindEvents();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
// Prevent resizing for mobile performance reasons
|
||||
// window.addEventListener('resize', () => this.handleResize());
|
||||
bindEvents() {
|
||||
// Pointer reactivity — push dots away from cursor for a "matrix repel" feel.
|
||||
// Touch devices are included; the dot pushes happen only while touched.
|
||||
window.addEventListener('pointermove', (e) => {
|
||||
this.mouse.x = e.clientX;
|
||||
this.mouse.y = e.clientY;
|
||||
this.mouse.active = true;
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('pointerout', () => {
|
||||
this.mouse.active = false;
|
||||
this.mouse.x = -9999;
|
||||
this.mouse.y = -9999;
|
||||
});
|
||||
|
||||
// Pause the rAF loop when the tab is hidden to save CPU / battery.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.start();
|
||||
}
|
||||
});
|
||||
|
||||
// Resize is debounced via rAF; disabled on mobile for perf parity with original.
|
||||
if (window.innerWidth > 768) {
|
||||
let resizeRaf = null;
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(() => this.handleResize());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.rafId || !this.isRunning) return;
|
||||
this.animate();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
|
||||
// Update mobile-specific configurations
|
||||
this.config.dotCount = this.getMobileDotCount();
|
||||
this.config.maxConnections = this.getMobileMaxConnections();
|
||||
|
||||
this.createDots();
|
||||
}
|
||||
|
||||
@@ -71,51 +119,54 @@ class MatrixBackground {
|
||||
const startX = Math.random() * (this.canvas.width - 20) + 10;
|
||||
const startY = Math.random() * (this.canvas.height - 20) + 10;
|
||||
|
||||
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);
|
||||
velocityX = Math.cos(angle) * speed;
|
||||
velocityY = Math.sin(angle) * speed;
|
||||
attempts++;
|
||||
}
|
||||
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 angle = Math.random() * Math.PI * 2;
|
||||
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4);
|
||||
const baseVelocityX = Math.cos(angle) * speed;
|
||||
const baseVelocityY = Math.sin(angle) * speed;
|
||||
|
||||
this.dots.push({
|
||||
x: startX,
|
||||
y: startY,
|
||||
vx: velocityX,
|
||||
vy: velocityY,
|
||||
vx: baseVelocityX,
|
||||
vy: baseVelocityY,
|
||||
baseVx: baseVelocityX,
|
||||
baseVy: baseVelocityY,
|
||||
wanderPhase: Math.random() * Math.PI * 2,
|
||||
wanderSpeed: 0.01 + Math.random() * 0.01,
|
||||
opacity: Math.random() * 0.3 + 0.7,
|
||||
size: this.config.dotSize + Math.random() * 2,
|
||||
connectionCount: 0,
|
||||
stuckFrames: 0
|
||||
connectionCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
updateDots() {
|
||||
const radius = this.config.mouseRadius;
|
||||
const radiusSq = radius * radius;
|
||||
const force = this.config.mouseForce;
|
||||
|
||||
this.dots.forEach(dot => {
|
||||
// Mouse repulsion
|
||||
if (this.mouse.active) {
|
||||
const dx = dot.x - this.mouse.x;
|
||||
const dy = dot.y - this.mouse.y;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
if (distSq < radiusSq && distSq > 0.5) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
const strength = (1 - dist / radius) * force;
|
||||
dot.vx += (dx / dist) * strength;
|
||||
dot.vy += (dy / dist) * strength;
|
||||
}
|
||||
}
|
||||
|
||||
dot.x += dot.vx;
|
||||
dot.y += dot.vy;
|
||||
|
||||
if (Math.abs(dot.vx) < 0.03 && Math.abs(dot.vy) < 0.03) {
|
||||
dot.stuckFrames++;
|
||||
} else {
|
||||
dot.stuckFrames = 0;
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
// Ease back to a gentle autonomous drift after pointer interaction.
|
||||
dot.wanderPhase += dot.wanderSpeed;
|
||||
const wanderX = Math.cos(dot.wanderPhase) * 0.03;
|
||||
const wanderY = Math.sin(dot.wanderPhase * 0.8) * 0.03;
|
||||
dot.vx += (dot.baseVx + wanderX - dot.vx) * 0.02;
|
||||
dot.vy += (dot.baseVy + wanderY - dot.vy) * 0.02;
|
||||
|
||||
const maxX = this.canvas.width - dot.size;
|
||||
const maxY = this.canvas.height - dot.size;
|
||||
@@ -175,7 +226,7 @@ class MatrixBackground {
|
||||
this.connections.forEach(conn => {
|
||||
const { dot1, dot2, startTime, duration } = conn;
|
||||
const elapsed = Date.now() - startTime;
|
||||
const opacity = Math.min(1, elapsed / 500); // Fade in
|
||||
const opacity = Math.min(1, elapsed / 500);
|
||||
|
||||
const gradient = this.ctx.createLinearGradient(dot1.x, dot1.y, dot2.x, dot2.y);
|
||||
gradient.addColorStop(0, this.config.colors.connection.start);
|
||||
@@ -185,15 +236,14 @@ class MatrixBackground {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(dot1.x, dot1.y);
|
||||
this.ctx.lineTo(dot2.x, dot2.y);
|
||||
|
||||
|
||||
this.ctx.strokeStyle = gradient;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.globalAlpha = opacity;
|
||||
|
||||
// Pulsing effect
|
||||
const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2; // 2-second pulse cycle
|
||||
const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2;
|
||||
this.ctx.shadowBlur = 4 + pulse * 6;
|
||||
this.ctx.shadowColor = 'rgba(138, 43, 226, 0.4)';
|
||||
this.ctx.shadowColor = 'rgba(118, 75, 162, 0.5)';
|
||||
|
||||
this.ctx.stroke();
|
||||
});
|
||||
@@ -229,17 +279,19 @@ class MatrixBackground {
|
||||
animate() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.updateDots();
|
||||
this.updateConnections();
|
||||
this.tryCreateConnections();
|
||||
if (!this.prefersReducedMotion) {
|
||||
this.updateDots();
|
||||
this.updateConnections();
|
||||
this.tryCreateConnections();
|
||||
}
|
||||
|
||||
this.drawConnections();
|
||||
this.drawDots();
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
this.rafId = requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new MatrixBackground();
|
||||
});
|
||||
});
|
||||
|
||||
89
js/social.js
89
js/social.js
@@ -1,7 +1,26 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new MatrixBackground();
|
||||
// ----------------------------------------------------------------
|
||||
// Avatar glitch layers — clone the avatar image twice (R + B
|
||||
// channel shift clones) so the CSS hover animation has elements
|
||||
// to slice/displace. Skipped when the user prefers reduced motion.
|
||||
// ----------------------------------------------------------------
|
||||
const avatar = document.querySelector('.avatar');
|
||||
const avatarImg = avatar && avatar.querySelector('img');
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (avatar && avatarImg && !prefersReducedMotion) {
|
||||
['r', 'b'].forEach((channel) => {
|
||||
const clone = avatarImg.cloneNode(false);
|
||||
clone.classList.add('glitch-layer', channel);
|
||||
clone.removeAttribute('alt');
|
||||
clone.setAttribute('aria-hidden', 'true');
|
||||
avatar.appendChild(clone);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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
|
||||
@@ -20,39 +39,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
let tooltipTimeout;
|
||||
|
||||
async function copyEmail() {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(emailAddress);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Fall through to the legacy copy path when clipboard permissions fail.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = emailAddress;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
let copied = false;
|
||||
try {
|
||||
copied = document.execCommand('copy');
|
||||
} catch (error) {
|
||||
copied = false;
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
return copied;
|
||||
}
|
||||
|
||||
emailLink.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const copied = await copyEmail();
|
||||
if (!copied) return;
|
||||
|
||||
// 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';
|
||||
|
||||
|
||||
// Show tooltip only after the copy operation succeeds.
|
||||
emailTooltip.classList.add('is-visible');
|
||||
|
||||
// Hide after 1.5s
|
||||
setTimeout(() => {
|
||||
emailTooltip.style.opacity = '0';
|
||||
clearTimeout(tooltipTimeout);
|
||||
tooltipTimeout = setTimeout(() => {
|
||||
emailTooltip.classList.remove('is-visible');
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user