Skip to content

Commit f544a78

Browse files
committed
feat: add 'matrix' theme with animated Matrix rain effect and corresponding styles
1 parent e2757a1 commit f544a78

2 files changed

Lines changed: 178 additions & 4 deletions

File tree

src/_includes/base.njk

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<script>
1616
// Initialize theme safely before body renders
1717
const savedTheme = localStorage.getItem("theme");
18-
const validThemes = ["dark", "light", "sepia"];
18+
const validThemes = ["dark", "light", "sepia", "matrix"];
1919
if (savedTheme && validThemes.includes(savedTheme)) {
2020
document.documentElement.setAttribute("data-theme", savedTheme);
2121
} else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
@@ -94,8 +94,7 @@
9494
<select id="theme-select" aria-label="Select color theme">
9595
<option value="light">Light</option>
9696
<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>
9998
</li>
10099
</ul>
101100
</header>
@@ -157,6 +156,11 @@
157156
const newTheme = e.target.value;
158157
document.documentElement.setAttribute('data-theme', newTheme);
159158
localStorage.setItem('theme', newTheme);
159+
if (newTheme === 'matrix') {
160+
startMatrixRain();
161+
} else {
162+
stopMatrixRain();
163+
}
160164
});
161165
162166
// Mobile Menu Toggle
@@ -205,6 +209,108 @@
205209
}
206210
});
207211
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+
208314
// Copy Code Button
209315
const preBlocks = document.querySelectorAll('pre');
210316
preBlocks.forEach((pre) => {

src/style.css

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
}
4848

4949
@media (prefers-color-scheme: light) {
50-
:root:not([data-theme="dark"]):not([data-theme="sepia"]) {
50+
:root:not([data-theme="dark"]):not([data-theme="sepia"]):not([data-theme="matrix"]) {
5151
color-scheme: light;
5252
--bg: #f0f4f8;
5353
--text-color: #1f2937;
@@ -84,6 +84,32 @@
8484
--pagefind-ui-border: var(--border);
8585
}
8686

87+
:root[data-theme="matrix"] {
88+
color-scheme: dark;
89+
--bg: #000000;
90+
--text-color: #00ff41;
91+
--text-color-light: #00cc33;
92+
--link-color: #7fff00;
93+
--link-color-visited: #32cd32;
94+
--accent: #00ff41;
95+
--gray-light: #0a0a0a;
96+
--gray-medium: #003b00;
97+
--gray-dark: #000;
98+
--surface: #0a0f0a;
99+
--border: #003b00;
100+
101+
/* Pagefind Theming */
102+
--pagefind-ui-scale: 0.8;
103+
--pagefind-ui-primary: var(--accent);
104+
--pagefind-ui-text: var(--text-color);
105+
--pagefind-ui-background: var(--surface);
106+
--pagefind-ui-border: var(--border);
107+
--pagefind-ui-tag: var(--gray-medium);
108+
--pagefind-ui-border-width: 1px;
109+
--pagefind-ui-border-radius: 4px;
110+
--pagefind-ui-font: 'JetBrains Mono', monospace;
111+
}
112+
87113
* {
88114
box-sizing: border-box;
89115
}
@@ -101,6 +127,48 @@ body {
101127
background-size: 50px 50px;
102128
}
103129

130+
/* Matrix theme overrides */
131+
:root[data-theme="matrix"] body {
132+
background-image: none;
133+
background-color: transparent;
134+
}
135+
136+
:root[data-theme="matrix"] .app-layout {
137+
position: relative;
138+
z-index: 1;
139+
}
140+
141+
:root[data-theme="matrix"] .sidebar {
142+
background-color: rgba(0, 10, 0, 0.85);
143+
}
144+
145+
:root[data-theme="matrix"] .container {
146+
background-color: rgba(0, 0, 0, 0.8);
147+
}
148+
149+
:root[data-theme="matrix"] .chapter-link:hover,
150+
:root[data-theme="matrix"] .chapter-link.active {
151+
background-color: rgba(0, 60, 0, 0.4);
152+
}
153+
154+
:root[data-theme="matrix"] pre[class*="language-"] {
155+
background-color: rgba(0, 10, 0, 0.9);
156+
}
157+
158+
:root[data-theme="matrix"] footer {
159+
background-color: transparent;
160+
}
161+
162+
#matrix-canvas {
163+
position: fixed;
164+
top: 0;
165+
left: 0;
166+
width: 100vw;
167+
height: 100vh;
168+
z-index: 0;
169+
pointer-events: none;
170+
}
171+
104172
/* Custom Scrollbar */
105173
::-webkit-scrollbar {
106174
width: 12px;

0 commit comments

Comments
 (0)