0xfeiker

0xfeiker

frontend engineer / web3 新手

如何实现代码雨的特效

看到一个 web3 的项目网站,背景是一串由代码组成的落雨特效,看起来挺酷的,所以尝试在本文去解析下。

仔细看特效内容,可以看到特效由两部分组成

  1. 特效开始时,由字符组成的文本从上到下匀速生成,且从透明渐变为不透明,直到铺满整个窗口
  2. 铺慢窗口后,每一行的不同列字符透明度不一致,呈现出一种不同速度下落的特效

我们用 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);

image

接下来需要处理的是

canvas 图板,从上到下,慢慢开始模糊,越靠顶部的文本,透明度越高。

这可以在每一轮绘制开始的时候,给整个画板先 fill 一个带透明度的背景色 color,这样处理会导致

  1. 在当前 X 轮绘制之前,color 是其前景色,根据浏览器的颜色混合计算,文本会呈现一个透明的效果
  2. 当前 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;
  }
}

`
image

这样整个主体效果就有了,然后判断下最下面的文本是否溢出 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;
    }
  }
}

image

暂时看起来比较丑,我们将每一列是否从头开始变成一个随机事件

// 从头开始

if (currentHeights[i] * fontSize > dimensions.height && Math.random() > 0.9) {
  currentHeights[i] = 0;
}

这样代码雨的效果就出来了

image

再加上一个小优化,比如对特殊字符高亮,隐藏最开始从上到下的代码呈现等。

完整代码如下

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。