summaryrefslogtreecommitdiffstats
path: root/themes/danix-xyz-hacker/assets/js/matrix-rain.js
blob: 53c55d8412ca089d9303da97d233f9b37fd15095 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// 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);
  });
})();