I saw a web3 project website where the background features a rain effect made up of code, which looks pretty cool, so I tried to analyze it in this article.
Upon closer inspection of the effect, it can be seen that it consists of two parts:
- At the beginning of the effect, text made up of characters is generated uniformly from top to bottom, transitioning from transparent to opaque until it fills the entire window.
- After filling the window, the opacity of characters in different columns of each line is inconsistent, creating an effect of falling at different speeds.
We will first try to implement a text matrix generated uniformly from top to bottom using a canvas. The overall idea is to switch the entire canvas to a col * row matrix based on the size of each character, while generating code characters column by column using a timer.
The code logic is quite simple:
- In each drawing cycle, draw the content of one line while calculating the position of the next line's characters in canvas coordinates.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const allChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+~`|}{[]:;?><,./-=\\';
const chars = Array.from(allChars);
// Font size for each character
const fontSize = 16;
// 2D information for drawing
const dimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// Number of characters per line
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)';
// Draw one line of content each time
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 coordinate for the next line
currentHeights[i] = currentHeights[i] + 1;
}
}
setInterval(draw, 100);
Next, we need to handle:
The canvas board slowly becomes blurry from top to bottom, with the text closer to the top being more opaque.
This can be achieved by filling the entire canvas with a background color with transparency at the start of each drawing cycle. This processing will lead to:
- Before the current X round of drawing, the color is its foreground color, and based on the browser's color blending calculations, the text will appear transparent.
- During the current X round of drawing, the color is the background color, which has no effect on the text of the current line.
The core code is as follows:
const textColor = '#0F0';
const bgColor = 'rgba(0, 0, 0, 0.08)';
// Draw one line of content each time
function draw() {
// Fill the entire canvas with a layer of black background with 0.08 opacity
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// Start drawing the current line of text
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 coordinate for the next line
currentHeights[i] = currentHeights[i] + 1;
}
}
`
Now the entire main effect is in place, and we need to check if the text at the bottom overflows the canvas boundary. If it does, we start drawing from the top again.
// Draw one line of content each time
function draw() {
// Fill the entire canvas with a layer of black background with 0.08 opacity
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// Start drawing the current line of text
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 coordinate for the next line
currentHeights[i] = currentHeights[i] + 1;
// Start from the top
if (currentHeights[i] * fontSize > dimensions.height) {
currentHeights[i] = 0;
}
}
}
It looks a bit ugly for now, so we will make whether each column starts from the top a random event.
// Start from the top
if (currentHeights[i] * fontSize > dimensions.height && Math.random() > 0.9) {
currentHeights[i] = 0;
}
This way, the code rain effect is achieved.
Additionally, we can add a small optimization, such as highlighting special characters and hiding the initial code presentation from top to bottom.
The complete code is as follows:
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const allChars =
'∅abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+~`|}{[]:;?><,./-=\\';
const chars = Array.from(allChars);
// Font size for each character
const fontSize = 16;
// 2D information for drawing
const dimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// Number of characters per line
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)';
// Draw one line of content each time
function draw() {
// Fill the entire canvas with a layer of black background with 0.08 opacity
context.fillStyle = bgColor;
context.fillRect(0, 0, dimensions.width, dimensions.height);
// Start drawing the current line of text
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 coordinate for the next line
currentHeights[i] = currentHeights[i] + 1;
// Start from the top
if (
currentHeights[i] * fontSize > dimensions.height &&
Math.random() > 0.95
) {
currentHeights[i] = 0;
}
}
}
setInterval(draw, 100);
#blog