|
|
|
@@ -1,14 +1,15 @@
|
|
|
|
|
class MatrixBackground {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.canvas = document.getElementById('matrixCanvas');
|
|
|
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
|
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
|
|
|
|
|
dotSpeed: 0.2,
|
|
|
|
|
connectionDuration: { min: 2000, max: 6000 },
|
|
|
|
|
connectionChance: 0.0002,
|
|
|
|
|
maxConnections: 8,
|
|
|
|
|
dotSize: 3,
|
|
|
|
|
colors: {
|
|
|
|
|
dotDefault: 'rgba(255, 255, 255, 0.8)',
|
|
|
|
@@ -22,40 +23,22 @@ class MatrixBackground {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
this.createDots();
|
|
|
|
|
this.animate();
|
|
|
|
|
this.handleResize();
|
|
|
|
|
|
|
|
|
|
// Handle window resize
|
|
|
|
|
this.animate();
|
|
|
|
|
window.addEventListener('resize', () => this.handleResize());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleResize() {
|
|
|
|
|
// Recreate dots with new screen dimensions
|
|
|
|
|
// Clear previous DOM elements to avoid orphan/sticky dots
|
|
|
|
|
this.clearCanvas();
|
|
|
|
|
this.canvas.width = window.innerWidth;
|
|
|
|
|
this.canvas.height = window.innerHeight;
|
|
|
|
|
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++) {
|
|
|
|
@@ -64,65 +47,44 @@ class MatrixBackground {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
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); // Ensure minimum speed
|
|
|
|
|
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4);
|
|
|
|
|
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,
|
|
|
|
|
|
|
|
|
|
this.dots.push({
|
|
|
|
|
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);
|
|
|
|
|
stuckFrames: 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateDots() {
|
|
|
|
|
this.dots.forEach((dot, index) => {
|
|
|
|
|
// Update position
|
|
|
|
|
this.dots.forEach(dot => {
|
|
|
|
|
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;
|
|
|
|
|
dot.stuckFrames++;
|
|
|
|
|
} 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);
|
|
|
|
@@ -130,18 +92,12 @@ class MatrixBackground {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
const maxX = this.canvas.width - dot.size;
|
|
|
|
|
const maxY = this.canvas.height - dot.size;
|
|
|
|
|
if (dot.x <= 0 || dot.x >= maxX) {
|
|
|
|
|
dot.vx = -dot.vx;
|
|
|
|
|
dot.x = Math.max(0, Math.min(maxX, dot.x));
|
|
|
|
@@ -150,146 +106,96 @@ class MatrixBackground {
|
|
|
|
|
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)`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawDots() {
|
|
|
|
|
this.dots.forEach(dot => {
|
|
|
|
|
const isConnected = dot.connectionCount > 0;
|
|
|
|
|
this.ctx.beginPath();
|
|
|
|
|
this.ctx.arc(dot.x, dot.y, dot.size / 2, 0, Math.PI * 2);
|
|
|
|
|
this.ctx.fillStyle = isConnected ? this.config.colors.dotConnected : this.config.colors.dotDefault;
|
|
|
|
|
this.ctx.globalAlpha = dot.opacity;
|
|
|
|
|
this.ctx.shadowBlur = 6;
|
|
|
|
|
this.ctx.shadowColor = isConnected ? this.config.colors.dotConnectedGlow : this.config.colors.dotDefaultGlow;
|
|
|
|
|
this.ctx.fill();
|
|
|
|
|
});
|
|
|
|
|
this.ctx.globalAlpha = 1;
|
|
|
|
|
this.ctx.shadowBlur = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createConnection(dot1, dot2) {
|
|
|
|
|
if (this.connections.length >= this.config.maxConnections) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
dot1.connectionCount++;
|
|
|
|
|
dot2.connectionCount++;
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
this.connections.push({
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateConnections() {
|
|
|
|
|
this.connections = this.connections.filter(conn => {
|
|
|
|
|
const elapsed = Date.now() - conn.startTime;
|
|
|
|
|
if (elapsed > conn.duration) {
|
|
|
|
|
conn.dot1.connectionCount--;
|
|
|
|
|
conn.dot2.connectionCount--;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawConnections() {
|
|
|
|
|
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 gradient = this.ctx.createLinearGradient(dot1.x, dot1.y, dot2.x, dot2.y);
|
|
|
|
|
gradient.addColorStop(0, this.config.colors.connection.start);
|
|
|
|
|
gradient.addColorStop(0.5, this.config.colors.connection.middle);
|
|
|
|
|
gradient.addColorStop(1, this.config.colors.connection.end);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
this.ctx.shadowBlur = 4 + pulse * 6;
|
|
|
|
|
this.ctx.shadowColor = 'rgba(138, 43, 226, 0.4)';
|
|
|
|
|
|
|
|
|
|
this.ctx.stroke();
|
|
|
|
|
});
|
|
|
|
|
this.ctx.globalAlpha = 1;
|
|
|
|
|
this.ctx.shadowBlur = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 dx = dot2.x - dot1.x;
|
|
|
|
|
const dy = dot2.y - dot1.y;
|
|
|
|
|
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 =>
|
|
|
|
|
const alreadyConnected = this.connections.some(conn =>
|
|
|
|
|
(conn.dot1 === dot1 && conn.dot2 === dot2) ||
|
|
|
|
|
(conn.dot1 === dot2 && conn.dot2 === dot1)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!alreadyConnected) {
|
|
|
|
|
this.createConnection(dot1, dot2);
|
|
|
|
|
}
|
|
|
|
@@ -300,15 +206,19 @@ class MatrixBackground {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animate() {
|
|
|
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
|
|
|
|
|
|
this.updateDots();
|
|
|
|
|
this.updateConnections();
|
|
|
|
|
this.tryCreateConnections();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.drawConnections();
|
|
|
|
|
this.drawDots();
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize the matrix background when the DOM is loaded
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
new MatrixBackground();
|
|
|
|
|
});
|
|
|
|
|
});
|