]> danix's work - danix.xyz-2.git/commitdiff
Complete matrix rain background effect implementation
authorDanilo M. <redacted>
Wed, 15 Apr 2026 22:24:58 +0000 (00:24 +0200)
committerDanilo M. <redacted>
Wed, 15 Apr 2026 22:24:58 +0000 (00:24 +0200)
- Add canvas-based matrix rain animation with ASCII + katakana characters
- Implement per-column animation with varied drop speeds (2-4 frame throttle)
- Theme-aware colors: purple and green accents with live switching
- Homepage: 28% opacity (dark) / 35% opacity (light) for prominent hero effect
- Inner pages: 13% opacity (dark) / 18% opacity (light) for subtle side gutters
- Respect prefers-reduced-motion system setting
- Add opaque background to content grids to block rain under text
- Add .content-grid class to differentiate single pages from list pages
- Add solid background to article list item cards
- Update article list item with bg-bg class for readability
- Z-index stack: canvas (z-1), content grid (z-9), main content (z-10)

Files modified:
- matrix-rain.js: new IIFE animation script with MutationObserver for theme switching
- baseof.html: add canvas element and script tag with guard
- main.css: add canvas positioning, opacity rules, content grid background
- _default/single.html: add max-w-7xl and .content-grid class
- articles/single.html: add max-w-7xl and .content-grid class
- is/list.html: add max-w-7xl and .content-grid class
- article-list-item.html: add bg-bg class for solid background

Co-Authored-By: Claude Haiku 4.5 <redacted>
themes/danix-xyz-hacker/assets/css/main.css
themes/danix-xyz-hacker/assets/css/main.min.css
themes/danix-xyz-hacker/assets/js/matrix-rain.js [new file with mode: 0644]
themes/danix-xyz-hacker/layouts/_default/baseof.html
themes/danix-xyz-hacker/layouts/_default/single.html
themes/danix-xyz-hacker/layouts/articles/single.html
themes/danix-xyz-hacker/layouts/is/list.html
themes/danix-xyz-hacker/layouts/partials/article-list-item.html

index 2b8cbdeb2a00b32161c70a326790c3b976fe9ec8..53b2410be27d76c97de0a153c8d950dec874af74 100644 (file)
@@ -286,3 +286,46 @@ html.theme-light .prose-invert blockquote {
     transition-duration: 0.01ms !important;
   }
 }
+
+/* Matrix rain canvas background */
+#matrix-rain {
+  position: fixed;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 1;
+}
+
+/* Dark theme: 13% opacity (inner pages) */
+html.theme-dark #matrix-rain {
+  opacity: 0.13;
+}
+
+/* Light theme: 18% opacity (inner pages) */
+html.theme-light #matrix-rain {
+  opacity: 0.18;
+}
+
+/* Homepage: more prominent background */
+html.theme-dark body[data-page-kind="home"] #matrix-rain {
+  opacity: 0.28;
+}
+
+html.theme-light body[data-page-kind="home"] #matrix-rain {
+  opacity: 0.35;
+}
+
+/* Reduced motion: hide canvas entirely */
+@media (prefers-reduced-motion: reduce) {
+  #matrix-rain {
+    display: none;
+  }
+}
+
+/* Content grid background — blocks rain under text, visible in gutters (single pages only) */
+.grid.md\:grid-cols-3.gap-8.max-w-7xl.content-grid {
+  position: relative;
+  z-index: 10;
+  background-color: var(--bg);
+}
index 74161bc451b810e64293fc66afe85c9a4f701933..9eabce5d8b87cba27bc12df21641d9cdae6d9530 100644 (file)
@@ -2062,6 +2062,55 @@ html.theme-light .prose-invert blockquote {
   }
 }
 
+/* Matrix rain canvas background */
+
+#matrix-rain {
+  position: fixed;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 1;
+}
+
+/* Dark theme: 13% opacity (inner pages) */
+
+html.theme-dark #matrix-rain {
+  opacity: 0.13;
+}
+
+/* Light theme: 18% opacity (inner pages) */
+
+html.theme-light #matrix-rain {
+  opacity: 0.18;
+}
+
+/* Homepage: more prominent background */
+
+html.theme-dark body[data-page-kind="home"] #matrix-rain {
+  opacity: 0.28;
+}
+
+html.theme-light body[data-page-kind="home"] #matrix-rain {
+  opacity: 0.35;
+}
+
+/* Reduced motion: hide canvas entirely */
+
+@media (prefers-reduced-motion: reduce) {
+  #matrix-rain {
+    display: none;
+  }
+}
+
+/* Content grid background — blocks rain under text, visible in gutters (single pages only) */
+
+.grid.md\:grid-cols-3.gap-8.max-w-7xl.content-grid {
+  position: relative;
+  z-index: 10;
+  background-color: var(--bg);
+}
+
 .hover\:bg-surface:hover {
   background-color: var(--surface);
 }
diff --git a/themes/danix-xyz-hacker/assets/js/matrix-rain.js b/themes/danix-xyz-hacker/assets/js/matrix-rain.js
new file mode 100644 (file)
index 0000000..53c55d8
--- /dev/null
@@ -0,0 +1,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);
+  });
+})();
index 7e8338a8e2031b76ba6de1b671d12d079ad42f20..efeba38b2e961fdebeb399afe04d6e463533bca6 100644 (file)
@@ -27,7 +27,7 @@
   {{ $chroma := resources.Get "css/chroma-custom.css" | minify }}
   <link rel="stylesheet" href="{{ $chroma.RelPermalink }}">
 </head>
-<body class="bg-bg text-text antialiased">
+<body class="bg-bg text-text antialiased" data-page-kind="{{ if .IsHome }}home{{ else }}other{{ end }}">
   <!-- Reading progress bar (only on single pages/articles) -->
   {{ if eq .Kind "page" }}
   <div
@@ -48,6 +48,9 @@
     z-index: -1;
   "></div>
 
+  <!-- Matrix rain canvas background -->
+  <canvas id="matrix-rain" aria-hidden="true"></canvas>
+
   <!-- Theme toggle & language toggle (before Alpine loads to prevent flash) -->
   <script>
     (function() {
   {{ $progressScript := resources.Get "js/reading-progress.js" | minify }}
   <script src="{{ $progressScript.RelPermalink }}"></script>
   {{ end }}
+
+  <!-- Matrix rain background effect -->
+  {{ with resources.Get "js/matrix-rain.js" }}
+  {{ $s := . | minify }}
+  <script src="{{ $s.RelPermalink }}"></script>
+  {{ end }}
 </body>
 </html>
index 16d519f1001d8d261c8f260d41dcad4fa55a9c39..57079681086e129962961ee1fd70620165b47306 100644 (file)
@@ -1,6 +1,6 @@
 {{ define "main" }}
 <article class="mx-auto px-4 py-12">
-  <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
+  <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
     <!-- Article section -->
     <div class="md:col-span-2">
       <!-- Article header -->
index 93abdb646b1fa45889af14615f767821e8aedad2..fe2ff6e0723c782db6dbaf9c6adf37fa1aef9986 100644 (file)
@@ -2,7 +2,7 @@
 {{ $articleType := .Params.type | default "life" }}
 {{ $template := printf "article-types/%s.html" $articleType }}
 <article class="mx-auto px-4 py-12">
-  <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
+  <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
     <!-- Article section -->
     <div class="md:col-span-2">
       <!-- Article header -->
index 7535a37723e871f85ef892438c8af1edec0d8187..1184b1877a3e3767d50fd2c0724462298a03f690 100644 (file)
@@ -1,6 +1,6 @@
 {{ define "main" }}
 <div class="mx-auto px-4 py-12">
-  <div class="grid md:grid-cols-3 gap-8">
+  <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
     <!-- Article section -->
     <div class="md:col-span-2">
       <!-- Article header -->
index 70d530c3aa1f72a87cc3a50e39d515a9c505b2a7..29f8d2b175f565aa78a37d5ea3e46d6d83451a3d 100644 (file)
@@ -13,7 +13,7 @@
   {{ end }}
 {{ end }}
 
-<article class="border border-border/30 rounded-lg overflow-hidden hover:border-accent/50 transition-all duration-200 group">
+<article class="border border-border/30 rounded-lg overflow-hidden hover:border-accent/50 transition-all duration-200 group bg-bg">
   <!-- Thumbnail -->
   {{ if $imageURL }}
   <a href="{{ .RelPermalink }}" class="block overflow-hidden bg-surface/50 relative" tabindex="-1">