Canvas 粒子特效:带你写一个黑客帝国同款的代码雨(附源码)😆

大家好,来了来了😁。

如果你问我,电影史上有哪个镜头,让无数少年瞬间燃起了对计算机世界的无限向往?

我会毫不犹豫地回答: 《黑客帝国》(The Matrix)开场的那一幕数字雨。

无数绿色的字符,像瀑布一样从漆黑的屏幕上方倾泻而下,神秘、冷峻、充满了赛博朋克的美感。那时候我就在想: 总有一天,我也要在自己的屏幕上敲出这个效果!🤣

今天,我们就用最原生的 HTML5 Canvas,来实现这个童年梦想。

别担心你的 Canvas 基础,跟着我,只需 50 行核心代码,我们就能让你的浏览器变身母体入口!😁


如何制造下雨的错觉?🤔

很多新手看到这个效果,第一反应是:哇,是不是要创建成千上万个字符对象,然后每个对象都有自己的坐标、速度,每一帧都要更新它们?

千万别这么想! 如果这样做,你的浏览器风扇马上就会起飞。

实现代码雨的核心,在于一个极其巧妙的偷懒思维 。我们不需要追踪每一个掉落的字符,我们只需要追踪每一列雨滴的头部位置

核心思路如下:

  1. 网格化屏幕 :我们把整个屏幕想象成一个由很多列组成的网格。假设字体大小是 16px,屏幕宽度是 1920px,那么我们就有 1920 / 16 ≈ 120 列。
  2. 记录Y坐标 :我们只需要一个数组 drops[],这个数组的长度就是列数。drops[i] 存储的是i 列目前雨滴下落到了哪个 Y 坐标
  3. 循环绘制 :每一帧动画,我们遍历这个数组。在第 i 列,我们在 (i * fontSize, drops[i]) 的位置画一个随机字符,然后让 drops[i] 增加一点点(往下落)。
  4. 随机重置:当某一列的 Y 坐标超出了屏幕高度,我们就让它随机"回到"屏幕最上方,重新开始下落。

懂了这个思路,代码就呼之欲出了!


见证奇迹的时刻

Step 1: 搭建舞台(HTML & CSS)

一切从简,我们只需要一个全屏的 Canvas。

HTML 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Matrix Code Rain</title>
    <style>
        /* 让 body 全屏变黑,去掉滚动条 */
        body {
            margin: 0;
            padding: 0;
            background-color: #000;
            overflow: hidden;
        }
        /* canvas 块级显示 */
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="matrix"></canvas>

    <script>
        // 我们的 JS 代码将写在这里
    </script>
</body>
</html>
Step 2: 初始化 Canvas 环境(JS)

我们需要获取 Canvas 上下文,并把它的宽高设置成浏览器窗口的宽高。

JavaScript 复制代码
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');

// 让 Canvas 撑满全屏
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 监听窗口大小变化,保持全屏(可选,为了体验更好)
window.addEventListener('resize', () => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
});
Step 3: 定义核心变量

这里有几个关键参数:字体大小、字符集、以及我们上面提到的核心数组 drops

Tips:为了原汁原味,字符集我们选用片假名,这才是 Matrix 的灵魂!

JavaScript 复制代码
// 字体大小,决定了列宽和字符的高度
const fontSize = 16;
// 计算屏幕能容纳多少列
const columns = canvas.width / fontSize; 

// 核心数组:drops[i] 代表第 i 列雨滴当前的 Y 坐标
// 初始化时,让所有列的 Y 坐标都为 1(稍微露个头)
const drops = [];
for (let i = 0; i < columns; i++) {
    drops[i] = 1;
}

// 字符集:片假名 + 数字 + 字母,看起来更像乱码
const chars = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
Step 4: 灵魂画师 ------ draw 函数

这是整个特效最核心的部分。请瞪大眼睛看好,那个迷人的拖尾残影是怎么实现的

如果你用传统的 ctx.clearRect(0, 0, width, height) 来清空画布,那么你得到只会是一排排往下跳动的字符,非常生硬,没有拖尾。

我们每一帧都不清空画布,而是画一个半透明的黑色矩形盖在上面。这样,上一帧画的绿色字符不会立刻消失,而是变暗了一点点。随着时间推移,它会越来越暗,直到完全消失。这就是残影的由来!

JavaScript 复制代码
function draw() {
    // 每一帧都用一个半透明的黑色覆盖之前的画布
    // 透明度 0.05 意味着需要覆盖很多次才能完全盖住,拖尾就长
    // 透明度 0.1 拖尾就短一点
    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; 
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // --- 设置绘制新字符的样式 ---
    ctx.fillStyle = '#0F0'; // 经典的黑客绿
    ctx.font = fontSize + 'px monospace'; // 等宽字体很关键

    // --- 遍历每一列,绘制新字符 ---
    for (let i = 0; i < drops.length; i++) {
        // 1. 随机取一个字符
        const text = chars.charAt(Math.floor(Math.random() * chars.length));

        // 2. 计算绘制坐标
        // x 坐标是固定的列位置
        const x = i * fontSize;
        // y 坐标是当前列记录的下落位置 * 字体大小
        const y = drops[i] * fontSize;

        // 3. 绘制字符
        ctx.fillText(text, x, y);

        // 4. 更新 Y 坐标,准备下一帧的绘制
        
        // 如果雨滴超出了屏幕底部,或者随机触发了一个重置条件
        // (Math.random() > 0.975 让重置具有随机性,雨滴看起来参差不齐)
        if (y > canvas.height && Math.random() > 0.975) {
            // 重置回屏幕顶部
            drops[i] = 0;
        }

        // 这里的增量决定了下落速度
        drops[i]++; 
    }
}

// --- 让动画动起来 ---
// 这里用 setInterval 而不用 requestAnimationFrame
// 是为了有一种复古的、卡顿的电子感,每秒大概 30 帧
setInterval(draw, 33); 

大功告成! 保存 HTML 文件,用浏览器打开,快看看你的屏幕是不是已经被代码雨淹没了!


完整源码 (Copy & Paste)

为了方便大家直接体验,这里奉上整合版的完整代码,复制粘贴保存为 matrix.html 即可运行。

HTML 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Matrix Code Rain Demo</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #000;
            overflow: hidden;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="matrix"></canvas>

    <script>
        const canvas = document.getElementById('matrix');
        const ctx = canvas.getContext('2d');

        // 设置 Canvas 全屏
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        // 监听屏幕大小改变
        window.addEventListener('resize', () => {
             canvas.width = window.innerWidth;
             canvas.height = window.innerHeight;
             initDrops(); // 重新初始化雨滴列数
        });

        // 核心配置
        const fontSize = 16;
        // 经典的片假名字符集
        const chars = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        
        let drops = [];
        let columns = 0;

        // 初始化雨滴位置数组
        function initDrops() {
            columns = canvas.width / fontSize;
            drops = [];
            for (let i = 0; i < columns; i++) {
                // 初始化为 1,避免开局全屏空白
                drops[i] = 1;
            }
        }

        // 绘图主函数
        function draw() {
            // 关键:用半透明黑色覆盖,制造拖尾效果
            // 调整 0.05 可以改变拖尾的长度
            ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // 设置字体样式
            ctx.fillStyle = '#0F0'; // 荧光绿
            ctx.font = fontSize + 'px arial'; // 使用系统自带支持片假名的字体即可

            // 遍历每一列
            for (let i = 0; i < drops.length; i++) {
                // 随机字符
                const text = chars.charAt(Math.floor(Math.random() * chars.length));
                
                const x = i * fontSize;
                const y = drops[i] * fontSize;
                
                ctx.fillText(text, x, y);

                // 边界判断与随机重置
                // Math.random() > 0.975 增加了随机性,让雨滴不是同时回到顶部
                if (y > canvas.height && Math.random() > 0.975) {
                    drops[i] = 0;
                }

                // y坐标递增
                drops[i]++;
            }
        }

        // 启动!
        initDrops();
        // 33ms 大约是 30fps,这种复古特效不需要 60fps
        setInterval(draw, 33);

    </script>
</body>
</html>

看,实现一个看起来狂拽酷炫的特效,原理其实就这么简单。

Canvas 并没有那么可怕,很多时候,限制我们创造力的不是技术本身,而是一些巧妙的思路(比如那个半透明蒙版)。

快去试试吧!

相关推荐
文心快码BaiduComate2 小时前
我用文心快码Spec 模式搓了个“pre作弊器”,妈妈再也不用担心我开会忘词了(附源码)
前端·后端·程序员
JH灰色2 小时前
【大模型】-LangChain--stream流式同步异步
服务器·前端·langchain
lxh01132 小时前
二叉树中的最大路径和
前端·算法·js
CC码码3 小时前
前端字符串排序搜索可以更加细化了
前端·javascript·面试
喵爱吃鱼3 小时前
kuma-ui中Flex vs FlexMin的关键区别
前端
codingMan3 小时前
[Android Compose] 拒绝闪烁!打造丝滑的聊天页面列表(仿微信效果)
前端
你别追我跑不动3 小时前
基于代码扫描的 Icon 优化实践
前端·性能优化
磊磊磊磊磊3 小时前
用AI做了个排版工具,分享一下如何高效省钱地用AI!
前端·后端·react.js
喵爱吃鱼3 小时前
flex 0 flex 1 flex none flex auto 应该在什么场景下使用
前端