看到一个 web3 的项目网站,背景是一串由代码组成的落雨特效,看起来挺酷的,所以尝试在本文去解析下。
仔细看特效内容,可以看到特效由两部分组成
- 特效开始时,由字符组成的文本从上到下匀速生成,且从透明渐变为不透明,直到铺满整个窗口
- 铺慢窗口后,每一行的不同列字符透明度不一致,呈现出一种不同速度下落的特效
我们用 canvas 先尝试实现从上到下匀速生成文本矩阵,整体思路是将整个 canvas 根据每个字符的大小切换为 col * row 的矩阵,同时通过定时器一列一列生成代码字符
代码思路比较简单
- 在每一个绘制周期内绘制一行的内容,同时计算下一行字符在 canvas 坐标的位置
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const allChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+~`|}{[]:;?><,./-=\\';
const chars = Array.from(allChars);
// 每个字符的字体大小
const fontSize = 16;
// 绘制的二维信息
const dimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// 每一行的字符数量
const rows = Math.floor(dimensions.width / fontSize);
const currentHeights = Array.from({ length: rows }).fill(0);
const textColor = '#0F0';
const bgColor = 'rgba(0, 0, 0, 0.08)';
// 每次绘制一行内容
function draw() {
context.fillStyle = textColor;
context.font = `${fontSize}px monospace`;
for (let i = 0; i < rows; i++) {
const x = i * fontSize;
const y = currentHeights[i] * fontSize;
context.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
// 下一行的y坐标
currentHeights[i] = currentHeights[i] + 1;
}
}
setInterval(draw, 100);
接下来需要处理的是
canvas 图板,从上到下,慢慢开始模糊,越靠顶部的文本,透明度越高。
这可以在每一轮绘制开始的时候,给整个画板先 fill 一个带透明度的背景色 color,这样处理会导致
- 在当前 X 轮绘制之前,color 是其前景色,根据浏览器的颜色混合计算,文本会呈现一个透明的效果
- 当前 X 轮绘制时,color 是起背景色,对当前行的文本无任何影响
核心代码如下
const textColor = '#0F0';
const bgColor = 'rgba(0, 0, 0, 0.08)';
// 每次绘制一行内容
function draw() {
// 给整个画板上一层0.08透明度的黑色背景
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// 开始绘制当前行的文本
context.fillStyle = textColor;
context.font = `${fontSize}px monospace`;
for (let i = 0; i < rows; i++) {
const x = i * fontSize;
const y = currentHeights[i] * fontSize;
context.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
// 下一行的y坐标
currentHeights[i] = currentHeights[i] + 1;
}
}
`
这样整个主体效果就有了,然后判断下最下面的文本是否溢出 canvas 边界,如果溢出则从顶部重新开始绘制,
// 每次绘制一行内容
function draw() {
// 给整个画板上一层0.08透明度的黑色背景
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// 开始绘制当前行的文本
context.fillStyle = textColor;
context.font = `${fontSize}px monospace`;
for (let i = 0; i < rows; i++) {
const x = i * fontSize;
const y = currentHeights[i] * fontSize;
context.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
// 下一行的y坐标
currentHeights[i] = currentHeights[i] + 1;
// 从头开始
if (currentHeights[i] * fontSize > dimensions.height) {
currentHeights[i] = 0;
}
}
}
暂时看起来比较丑,我们将每一列是否从头开始变成一个随机事件
// 从头开始
if (currentHeights[i] * fontSize > dimensions.height && Math.random() > 0.9) {
currentHeights[i] = 0;
}
这样代码雨的效果就出来了
再加上一个小优化,比如对特殊字符高亮,隐藏最开始从上到下的代码呈现等。
完整代码如下
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const allChars =
'∅abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+~`|}{[]:;?><,./-=\\';
const chars = Array.from(allChars);
// 每个字符的字体大小
const fontSize = 16;
// 绘制的二维信息
const dimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// 每一行的字符数量
const rows = Math.floor(dimensions.width / fontSize);
const currentHeights = Array.from({ length: rows }).fill(0);
const textColor = '#0F0';
const highlightColor = '#FF55BB';
const bgColor = 'rgba(0, 0, 0, 0.08)';
// 每次绘制一行内容
function draw() {
// 给整个画板上一层0.08透明度的黑色背景
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// 开始绘制当前行的文本
context.fillStyle = textColor;
context.font = `${fontSize}px monospace`;
for (let i = 0; i < rows; i++) {
const x = i * fontSize;
const y = currentHeights[i] * fontSize;
const char = chars[Math.floor(Math.random() * chars.length)];
context.fillStyle = char === '∅' ? highlightColor : textColor;
context.fillText(char, x, y);
// 下一行的y坐标
currentHeights[i] = currentHeights[i] + 1;
// 从头开始
if (
currentHeights[i] * fontSize > dimensions.height &&
Math.random() > 0.95
) {
currentHeights[i] = 0;
}
}
}
setInterval(draw, 100);
#blog