// Matrix rain background effect (function() { // Bail out if user prefers reduced motion if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return; } // Canvas and context let canvas = document.getElementById('matrix-rain'); if (!canvas) return; const ctx = canvas.getContext('2d'); // State let columns = []; let frameCount = 0; let colors = { accent: '#a855f7', accent2: '#00ff88', bg: '#060b10', head: '#ffffff' }; // Character set: 30% ASCII, 70% katakana const ASCII = Array.from({ length: 94 }, (_, i) => String.fromCharCode(33 + i)); const KATA = Array.from({ length: 96 }, (_, i) => String.fromCodePoint(0x30a0 + i)); const CHARS = shuffle([...ASCII, ...KATA, ...KATA, ...KATA]); // Utility: shuffle array function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } // Utility: convert hex or rgb color to rgba string function hexToRgba(color, alpha) { const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (rgbMatch) { return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`; } const hex = color.replace('#', ''); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } // Sample CSS variables based on current theme function sampleColors() { const style = getComputedStyle(document.documentElement); const isDark = document.documentElement.classList.contains('theme-dark'); colors.accent = style.getPropertyValue('--accent').trim(); colors.accent2 = style.getPropertyValue('--accent2').trim(); colors.bg = style.getPropertyValue('--bg').trim(); // Head char: bright white in dark mode, deep purple-black in light mode colors.head = isDark ? '#ffffff' : '#1a0533'; } // Resize canvas to window dimensions function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx.font = '14px "JetBrains Mono", monospace'; ctx.textBaseline = 'top'; initColumns(); } // Initialize columns for the current canvas width function initColumns() { columns = []; const columnWidth = 14; const columnCount = Math.floor(canvas.width / columnWidth); for (let i = 0; i < columnCount; i++) { columns.push({ x: i * columnWidth, y: -Math.floor(Math.random() * 40), // stagger start above viewport speed: 2 + Math.floor(Math.random() * 3), // 2-4 frames between drops color: Math.random() < 0.6 ? 'accent2' : 'accent', // 60% green, 40% purple charIndex: Math.floor(Math.random() * CHARS.length), length: 8 + Math.floor(Math.random() * 13), // trail length 8-20 }); } } // Set up MutationObserver for theme switching function setupThemeObserver() { const observer = new MutationObserver(function(mutations) { for (const m of mutations) { if (m.attributeName === 'class') { sampleColors(); break; } } }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'], }); } // Main animation loop function drawFrame() { frameCount++; // Fade layer: semi-transparent background fill ctx.fillStyle = hexToRgba(colors.bg, 0.085); ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw each column for (const col of columns) { // Skip if not time to drop yet (per-column throttle) if (frameCount % col.speed !== 0) continue; // Draw explicit trail in column color ctx.fillStyle = colors[col.color]; for (let i = 1; i <= col.length; i++) { const trailY = (col.y - i) * 14; if (trailY < 0) continue; const trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length; ctx.fillText(CHARS[trailCharIndex], col.x, trailY); } // Draw head character (bright) ctx.fillStyle = colors.head; const headCharIndex = col.charIndex % CHARS.length; ctx.fillText(CHARS[headCharIndex], col.x, col.y * 14); // Advance column col.y++; col.charIndex = (col.charIndex + 1) % CHARS.length; // Reset when scrolled off screen if (col.y * 14 > canvas.height + col.length * 14) { col.y = -Math.floor(Math.random() * 20); col.charIndex = Math.floor(Math.random() * CHARS.length); col.color = Math.random() < 0.6 ? 'accent2' : 'accent'; } } requestAnimationFrame(drawFrame); } // Initialize sampleColors(); resizeCanvas(); setupThemeObserver(); // Debounced resize handler let resizeTimer; window.addEventListener('resize', function() { clearTimeout(resizeTimer); resizeTimer = setTimeout(resizeCanvas, 150); }); // Start animation when fonts are ready document.fonts.ready.then(function() { requestAnimationFrame(drawFrame); }); })();