|
15 | 15 | <script> |
16 | 16 | // Initialize theme safely before body renders |
17 | 17 | const savedTheme = localStorage.getItem("theme"); |
18 | | - const validThemes = ["dark", "light", "sepia"]; |
| 18 | + const validThemes = ["dark", "light", "sepia", "matrix"]; |
19 | 19 | if (savedTheme && validThemes.includes(savedTheme)) { |
20 | 20 | document.documentElement.setAttribute("data-theme", savedTheme); |
21 | 21 | } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { |
|
94 | 94 | <select id="theme-select" aria-label="Select color theme"> |
95 | 95 | <option value="light">Light</option> |
96 | 96 | <option value="dark">Dark</option> |
97 | | - <option value="sepia">Sepia</option> |
98 | | - </select> |
| 97 | + <option value="sepia">Sepia</option> <option value="matrix">Matrix</option> </select> |
99 | 98 | </li> |
100 | 99 | </ul> |
101 | 100 | </header> |
|
157 | 156 | const newTheme = e.target.value; |
158 | 157 | document.documentElement.setAttribute('data-theme', newTheme); |
159 | 158 | localStorage.setItem('theme', newTheme); |
| 159 | + if (newTheme === 'matrix') { |
| 160 | + startMatrixRain(); |
| 161 | + } else { |
| 162 | + stopMatrixRain(); |
| 163 | + } |
160 | 164 | }); |
161 | 165 |
|
162 | 166 | // Mobile Menu Toggle |
|
205 | 209 | } |
206 | 210 | }); |
207 | 211 |
|
| 212 | + // Matrix Rain Animation |
| 213 | + let matrixAnimId = null; |
| 214 | + let matrixCanvas = null; |
| 215 | +
|
| 216 | + function startMatrixRain() { |
| 217 | + if (matrixCanvas) return; // already running |
| 218 | +
|
| 219 | + matrixCanvas = document.createElement('canvas'); |
| 220 | + matrixCanvas.id = 'matrix-canvas'; |
| 221 | + document.body.appendChild(matrixCanvas); |
| 222 | +
|
| 223 | + const ctx = matrixCanvas.getContext('2d'); |
| 224 | + let w, h, columns, drops; |
| 225 | +
|
| 226 | + // Half-width katakana + digits for that authentic Matrix look |
| 227 | + const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; |
| 228 | +
|
| 229 | + function resize() { |
| 230 | + w = matrixCanvas.width = window.innerWidth; |
| 231 | + h = matrixCanvas.height = window.innerHeight; |
| 232 | + const fontSize = 16; |
| 233 | + columns = Math.floor(w / fontSize); |
| 234 | + // Preserve existing drop positions where possible |
| 235 | + const oldDrops = drops; |
| 236 | + drops = new Array(columns); |
| 237 | + for (let i = 0; i < columns; i++) { |
| 238 | + drops[i] = (oldDrops && oldDrops[i] != null) ? oldDrops[i] : Math.random() * -100; |
| 239 | + } |
| 240 | + } |
| 241 | +
|
| 242 | + resize(); |
| 243 | + window.addEventListener('resize', resize); |
| 244 | +
|
| 245 | + const fontSize = 16; |
| 246 | + const frameInterval = 1000 / 12; // Cap at 12 FPS |
| 247 | + let lastFrameTime = 0; |
| 248 | +
|
| 249 | + function draw(now) { |
| 250 | + matrixAnimId = requestAnimationFrame(draw); |
| 251 | +
|
| 252 | + if (now - lastFrameTime < frameInterval) return; |
| 253 | + lastFrameTime = now; |
| 254 | +
|
| 255 | + // Semi-transparent black to create fade trail |
| 256 | + ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; |
| 257 | + ctx.fillRect(0, 0, w, h); |
| 258 | +
|
| 259 | + ctx.fillStyle = '#00ff41'; |
| 260 | + ctx.font = fontSize + 'px monospace'; |
| 261 | +
|
| 262 | + for (let i = 0; i < columns; i++) { |
| 263 | + const char = chars[Math.floor(Math.random() * chars.length)]; |
| 264 | + const x = i * fontSize; |
| 265 | + const y = drops[i] * fontSize; |
| 266 | +
|
| 267 | + // Bright leading character |
| 268 | + if (y > 0 && y < h) { |
| 269 | + ctx.fillStyle = '#aaffaa'; |
| 270 | + ctx.fillText(char, x, y); |
| 271 | + ctx.fillStyle = '#00ff41'; |
| 272 | + } |
| 273 | +
|
| 274 | + // Main trail character (one step behind) |
| 275 | + if (y - fontSize > 0) { |
| 276 | + const trailChar = chars[Math.floor(Math.random() * chars.length)]; |
| 277 | + ctx.fillText(trailChar, x, y - fontSize); |
| 278 | + } |
| 279 | +
|
| 280 | + drops[i]++; |
| 281 | +
|
| 282 | + // Reset drop to top with randomness for natural feel |
| 283 | + if (drops[i] * fontSize > h && Math.random() > 0.975) { |
| 284 | + drops[i] = 0; |
| 285 | + } |
| 286 | + } |
| 287 | +
|
| 288 | + matrixAnimId = requestAnimationFrame(draw); |
| 289 | + } |
| 290 | +
|
| 291 | + matrixAnimId = requestAnimationFrame(draw); |
| 292 | +
|
| 293 | + // Store resize handler reference for cleanup |
| 294 | + matrixCanvas._resizeHandler = resize; |
| 295 | + } |
| 296 | +
|
| 297 | + function stopMatrixRain() { |
| 298 | + if (matrixAnimId) { |
| 299 | + cancelAnimationFrame(matrixAnimId); |
| 300 | + matrixAnimId = null; |
| 301 | + } |
| 302 | + if (matrixCanvas) { |
| 303 | + window.removeEventListener('resize', matrixCanvas._resizeHandler); |
| 304 | + matrixCanvas.remove(); |
| 305 | + matrixCanvas = null; |
| 306 | + } |
| 307 | + } |
| 308 | +
|
| 309 | + // Start rain on load if matrix theme is active |
| 310 | + if (initialTheme === 'matrix') { |
| 311 | + startMatrixRain(); |
| 312 | + } |
| 313 | +
|
208 | 314 | // Copy Code Button |
209 | 315 | const preBlocks = document.querySelectorAll('pre'); |
210 | 316 | preBlocks.forEach((pre) => { |
|
0 commit comments