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);
});
})();
|