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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。