看到一個 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