--- /dev/null
+// 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);
+ });
+})();