前言 #
原先我的主網站「YoZ Music 鼓手柚子
」和生活向部落格「YoZ Blog
」的背景是用 Blowfish 主題中的 background 佈局(Layout),訪問這兩個網站就可以看到,背景是我的舞台照上層覆蓋了半透明的單色濾鏡。
將資訊類的文章獨立移到這裡後,畢竟文章主軸跟我「個人」沒什麼關係,便將背景改回純色,但看上去就是有點單調。想到之前看到的 React Bits - Animated UI Components For React
這個網站,裡面有很多酷炫的 React 背景元件但 Hugo 不能直接使用,我便透過 Perplexity 用 JavaScript 做出了「由中心向外波動」並且能跟「游標位置互動」的3D網格動畫,並且在畫面上段空白處加入了「代碼瀑布
」動畫
Demo 展示 #
新增 CSS 設定 #
編輯/assets/css路徑下的custom.css,添加:
/* 網格加代碼瀑布風格動畫背景 */
/* /layouts/partials/grid-n-digi-rain-bg.html */
#grid-n-digi-rain-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
perspective: 1000px;
}
創建 JavaScript #
在/static/js路徑下創建grid-n-digi-rain-bg.js並編輯,如路徑不存在就手動建立。
const canvas = document.getElementById('grid-n-digi-rain-bg');
const ctx = canvas.getContext('2d');
let width, height;
let mouseX = 0;
let mouseY = 0;
let time = 0;
// 代碼瀑布變數
const matrixChars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン가나다라마바사아자차카타파하거너더러머버서어저처커터퍼허고노도로모보소오조초코토포호구누두루무부수우주추쿠투푸후0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const fontSize = 16;
let columns;
let drops = [];
let speeds = [];
let trails = [];
let matrixTrails = [];
// 主題檢測
function isDarkMode() {
return document.documentElement.classList.contains('dark') ||
document.body.classList.contains('dark');
}
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
columns = Math.floor(width / fontSize / 3);
drops = [];
speeds = [];
trails = [];
matrixTrails = [];
for (let i = 0; i < columns; i++) {
drops[i] = Math.random() * -50 - 10;
speeds[i] = 0.15 + Math.random() * 0.15;
trails[i] = 10 + Math.floor(Math.random() * 15);
matrixTrails[i] = [];
for (let t = 0; t < 30; t++) {
matrixTrails[i][t] = matrixChars[Math.floor(Math.random() * matrixChars.length)];
}
}
}
resize();
window.addEventListener('resize', resize);
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
// 監聽主題切換
const observer = new MutationObserver(() => {
// 主題改變時重繪
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class']
});
// 繪製代碼瀑布
function drawMatrix() {
const darkMode = isDarkMode();
const bgOpacity = darkMode ? 0.05 : 0.08;
const bgColor = darkMode ? '10, 10, 10' : '252, 252, 249';
ctx.fillStyle = `rgba(${bgColor}, ${bgOpacity})`;
ctx.fillRect(0, 0, width, height * 0.4);
ctx.font = fontSize + 'px monospace';
for (let i = 0; i < drops.length; i++) {
const x = (i / columns) * width;
const y = drops[i] * fontSize;
const trailLength = trails[i];
for (let t = 0; t < trailLength; t++) {
const trailY = (drops[i] - t) * fontSize;
if (trailY > 0 && trailY < height * 0.4) {
// 定期更新字符(小概率)
if (Math.random() > 0.95) {
matrixTrails[i][t % 30] = matrixChars[Math.floor(Math.random() * matrixChars.length)];
}
const char = matrixTrails[i][t % 30];
let alpha, color;
if (t === 0) {
// 瀑布頭部
alpha = 0.65;
color = darkMode ?
`rgba(95, 211, 160, ${alpha})` :
`rgba(45, 166, 178, ${alpha})`;
} else {
// 瀑布拖尾 - 漸層變暗
alpha = Math.max(0, 1 - (t / trailLength));
if (darkMode) {
const green = Math.floor(200 - (t / trailLength) * 100);
color = `rgba(74, ${green}, 109, ${alpha * 0.5})`;
} else {
const brightness = Math.floor(128 - (t / trailLength) * 95);
color = `rgba(33, ${brightness}, 141, ${alpha * 0.4})`;
}
}
ctx.fillStyle = color;
ctx.fillText(char, x, trailY);
}
}
// 更新位置
drops[i] += speeds[i];
// 重置
if (drops[i] * fontSize > height * 0.4 && Math.random() > 0.975) {
drops[i] = -trails[i];
speeds[i] = 0.15 + Math.random() * 0.15;
trails[i] = 10 + Math.floor(Math.random() * 15);
// 重置時更新整個字符數組
matrixTrails[i] = [];
for (let k = 0; k < 30; k++) {
matrixTrails[i][k] = matrixChars[Math.floor(Math.random() * matrixChars.length)];
}
}
}
}
// 繪製 3D 網格
function drawGrid() {
const darkMode = isDarkMode();
const gridSize = 40;
const rows = 20;
const cols = Math.ceil(width / gridSize) + 10;
const centerX = width / 2;
const centerY = height / 2;
// 根據主題調整顏色
const colors = darkMode ? [
'rgba(74, 155, 109, 0.2)',
'rgba(95, 211, 160, 0.2)',
'rgba(60, 179, 113, 0.25)',
'rgba(64, 224, 208, 0.2)',
'rgba(72, 160, 200, 0.25)'
] : [
'rgba(33, 128, 141, 0.15)',
'rgba(26, 104, 115, 0.15)',
'rgba(29, 116, 128, 0.2)',
'rgba(45, 166, 178, 0.15)',
'rgba(41, 150, 161, 0.2)'
];
// 繪製橫向網格線
for (let i = 0; i < rows; i++) {
ctx.beginPath();
for (let j = 0; j < cols; j++) {
const x = (j - cols / 2) * gridSize;
const z = (i - rows / 2) * gridSize;
const distance = Math.sqrt(x * x + z * z);
const wave = Math.sin(distance * 0.01 - time * 1.5) * 25 +
Math.sin(z * 0.02 + time * 0.6) * 15 +
Math.cos(x * 0.015 - time * 0.4) * 10;
const mouseDistX = (mouseX - centerX) * 0.1;
const mouseDistY = (mouseY - centerY) * 0.1;
const perspective = 600;
const scale = perspective / (perspective + z + wave);
const projX = centerX + x * scale + mouseDistX * scale;
const projY = centerY + (wave - z * 0.5) * scale + mouseDistY * scale;
if (j === 0) {
ctx.moveTo(projX, projY);
} else {
ctx.lineTo(projX, projY);
}
}
const gradient = ctx.createLinearGradient(0, 0, width, height);
const colorIndex = Math.floor((time * 10 + i * 5) % 5);
gradient.addColorStop(0, colors[colorIndex]);
gradient.addColorStop(1, colors[(colorIndex + 1) % 5]);
ctx.strokeStyle = gradient;
ctx.lineWidth = 1.5;
ctx.stroke();
}
// 繪製縱向網格線
for (let j = 0; j < cols; j++) {
ctx.beginPath();
for (let i = 0; i < rows; i++) {
const x = (j - cols / 2) * gridSize;
const z = (i - rows / 2) * gridSize;
const distance = Math.sqrt(x * x + z * z);
const wave = Math.sin(distance * 0.01 - time * 1.5) * 25 +
Math.sin(z * 0.02 + time * 0.6) * 15 +
Math.cos(x * 0.015 - time * 0.4) * 10;
const mouseDistX = (mouseX - centerX) * 0.1;
const mouseDistY = (mouseY - centerY) * 0.1;
const perspective = 600;
const scale = perspective / (perspective + z + wave);
const projX = centerX + x * scale + mouseDistX * scale;
const projY = centerY + (wave - z * 0.5) * scale + mouseDistY * scale;
if (i === 0) {
ctx.moveTo(projX, projY);
} else {
ctx.lineTo(projX, projY);
}
}
const gradient = ctx.createLinearGradient(0, 0, width, height);
const colorIndex = Math.floor((time * 10 + j * 5) % 5);
gradient.addColorStop(0, colors[colorIndex]);
gradient.addColorStop(1, colors[(colorIndex + 1) % 5]);
ctx.strokeStyle = gradient;
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function animate() {
time += 0.016;
const darkMode = isDarkMode();
const bgColor = darkMode ? 'rgba(10, 10, 10, 1)' : 'rgba(252, 252, 249, 1)';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
drawGrid();
drawMatrix();
requestAnimationFrame(animate);
}
animate();
創建 Partial 模板 #
在/layouts/partials路徑下創建grid-n-digi-rain-bg.html模板並編輯:
<canvas id="grid-n-digi-rain-bg"></canvas> <!-- 創建畫布元素 -->
<script src="/js/grid-n-digi-rain-bg.js"></script> <!-- 載入 JavaScript -->
套用 Partial 模板 #
找到網站主模板檔案並編輯,通常是/layouts/_default/baseof.html。像我使用 Blowfish 主題,就將/themes/blowfish/layouts/_default下的baseof.html複製到/layouts/_default並編輯。
套用到所有頁面 #
如果要套用此動畫背景到所有頁面,在baseof.html中的<body>標籤後加入:
<!doctype html>
<html lang="zh-TW">
<head>
<!-- 原始 head 內容 ... -->
</head>
<body>
<!-- 加入這一行 -->
{{ partial "grid-n-digi-rain-bg.html" . }}
<!-- 原始 body 內容 -->
</body>
</html>
只套用到主頁 #
<body>
<!-- 只在首頁顯示此背景 -->
{{ if .IsHome }}
{{ partial "grid-n-digi-rain-bg.html" . }}
{{ end }}
<!-- 你原本的內容 -->
</body>
套用到所有頁面,非主頁時時降低透明度 #
在<body>的class屬性中新增{{ if not .IsHome }}reading-mode{{ end }}
<body
class="原本的標籤 {{ if not .IsHome }}reading-mode{{ end }}">
{{ partial "grid-n-digi-rain-bg.html" . }}
<!-- 你原本的內容 -->
</body>
編輯custom.css:
/* 網格加代碼瀑布風格動畫背景 */
/* 文章閱讀模式*/
.reading-mode #grid-n-digi-rain-bg {
opacity: 0.35; /* 降低透明度,自訂參數 */
}
只套用到特定頁面模板 #
{{ define "main" }}
<!-- 只在這個頁面顯示動畫背景 -->
{{ partial "grid-background.html" . }}
<div class="content">
<!-- 原始內容 -->
</div>
{{ end }}