先看效果

访问这个博客任何一个页面(暗色模式),背景里有一层 “01” 字符在缓缓下坠。和传统 Matrix 雨不同,它的列距不规则、字符随机倾斜、每列速度略有差异,整体有一种”有机的节奏感”而不是机械复制。

这篇讲清楚:这个效果是怎么从想法到实现的。

整体架构

只有三个部分:

HTML  → 一个 <canvas> 画布
CSS   → 绝对定位铺满全屏,z-index 扔到最底层
JS    → Canvas 2D 绘制循环

没有依赖,不装包,不调库。

HTML

<canvas id="matrix-canvas"></canvas>

一行。要点在 Layout.astro 里的位置——它必须在 <body> 最开头,在所有内容之前,这样 CSS 才能把它垫到底层去。

<body>
  <canvas id="matrix-canvas"></canvas>
  <!-- 其他内容都堆在这里 -->

CSS

#matrix-canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -999;
  pointer-events: none; /* 允许点击穿透 */
}

z-index: -999 保证它永远在最底下。pointer-events: none 让鼠标可以正常点击下层的页面内容。

背景透明,所以它会和页面的暗色背景叠加在一起,不抢内容视线。

JavaScript:完整代码

(function () {
  if (!document.querySelector('#matrix-canvas')) return;

  const canvas = document.getElementById('matrix-canvas');
  const ctx = canvas.getContext('2d');

  const FONT_SIZE = 11;       // 字符大小
  let colX = [];              // 每列的 x 坐标(不规则间距)
  let drops = [];             // 每列的状态:y坐标、字符、倾斜角度
  let animId = null;

  // ── 初始化所有列 ──
  function initDrops() {
    colX = [];
    drops = [];
    let x = 0;
    // 不规则列距:每个 gap = 1.2~3 倍字符宽度
    while (x < canvas.width + FONT_SIZE * 4) {
      colX.push(x);
      drops.push({
        y: Math.random() * canvas.height,
        char: Math.random() > 0.5 ? '1' : '0',
        angle: (Math.random() - 0.5) * 0.6  // ±0.6 弧度 ≈ ±34°
      });
      x += FONT_SIZE * (1.2 + Math.random() * 1.8);
    }
  }

  // ── 响应窗口大小变化 ──
  function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    initDrops();
  }

  // ── 绘制循环 ──
  let frame = 0;
  function draw() {
    frame++;
    // 隔帧渲染:降低 CPU 负担,同时让下落看起来更自然
    if (frame % 2 !== 0) {
      animId = requestAnimationFrame(draw);
      return;
    }

    // 覆盖式淡出,产生拖尾效果
    ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.font = `${FONT_SIZE}px monospace`;

    for (let i = 0; i < colX.length; i++) {
      const x = colX[i];
      const d = drops[i];

      // ── 旋转字符 ──
      ctx.save();
      ctx.translate(x, d.y);
      ctx.rotate(d.angle);
      ctx.fillStyle = '#7dff9a';  // 亮绿色头部
      ctx.fillText(d.char, 0, 0);
      ctx.restore();

      // ── 下落速度:列间有微小差异 ──
      d.y += FONT_SIZE * (0.7 + (i % 3) * 0.15);

      // ── 落出屏幕后随机重置到顶部 ──
      if (d.y > canvas.height + FONT_SIZE) {
        d.y = -FONT_SIZE * (1 + Math.random() * 3);
        d.char = Math.random() > 0.5 ? '1' : '0';
        d.angle = (Math.random() - 0.5) * 0.6;
      }
    }

    animId = requestAnimationFrame(draw);
  }

  // ── 启动 ──
  resize();
  draw();

  // ── 监听暗色模式切换 ──
  window.matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', (e) => {
      if (e.matches) { resize(); draw(); }
      else { ctx.clearRect(0, 0, canvas.width, canvas.height); }
    });
})();

设计决策拆解

1. 为什么不用传统竖条文字?

经典 Matrix 雨是竖向排列的片假名字符,从上往下刷,同步度极高。好处是整齐,坏处是太机械——每一列和旁边 100% 一样,像贴满了同一版瓷砖。

我的改动:

  • 列距随机(1.2~3 倍字符宽),每列起跑位置不同
  • 字符本身随机旋转(±34°),打破正交感
  • 每列速度略微不同(0.7x ~ 1.15x)

这三个加起来,视觉上就有了”错落有致的节奏”,像一群有序但不完全同步的信号。

2. 拖尾效果怎么来的?

ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);

每帧用 半透明黑色 覆盖画布而不是清空。这样字符在移动时,旧位置不会立刻消失,而是以 12 帧左右的速度淡出。视觉上就是流畅的拖尾。

如果用 clearRect 清空,字符会像静态水珠往下掉,没有那种”流动”感。

3. 为什么用 “01” 而不是片假名?

纯粹个人审美。博客是极客技术向,“01” 更直接地呼应二进制/程序员的视觉语言,比片假名更切题。而且 “01” 横排时两个字符挨在一起,旋转后更像”碎片”而非”字符”,整体更抽象。

4. Canvas vs CSS 动画

有人会用 CSS @keyframes + opacity 轮转实现类似效果。Canvas 的优势是:

  • 像素级控制,想画什么字符就画什么
  • JS 逻辑可以随时改(随机间距、随机速度、随机旋转)
  • 不占用 DOM,不触发 CSS 重排

Astro 静态页面里,Canvas 的额外开销几乎为零(只有一个 <canvas> 节点 + 一段 IIFE)。

5. 性能考虑

  • requestAnimationFrame 做渲染循环,浏览器自动匹配屏幕刷新率
  • 隔帧渲染(frame % 2),实际约 30fps,足够流畅且 CPU 友好
  • 窗口 resize 时重新初始化,这是唯一可能的性能峰值

暗色模式联动

matchMedia 监听系统主题切换:

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    if (e.matches) { resize(); draw(); }
    else { ctx.clearRect(0, 0, canvas.width, canvas.height); }
  });

切到亮色模式时直接清空画布,亮色页面上不需要这个暗色效果。

总结

核心就三件事:

  1. 不规则列距 — 打破机械感
  2. 随机旋转 — 打破正交感
  3. 半透明覆盖 — 拖尾 + 节奏感

如果把这三个特性拿掉,变成整齐等距、竖直向下、同速的”01”条,那就只是一个普通跑马灯了。差别就在于这些”不规则”的细节里。