用一个 HTML 文件实现黑客帝国经典代码雨效果,但有以下创意要求:
- 下落的不是随机字符,而是中国古诗词(每列是一首完整的诗,从上往下逐字飘落)
- 背景纯黑,文字渐变色(从亮绿到暗绿到消失)
- 当鼠标划过某一列时,该列暂停并高亮显示完整诗句,旁边浮现诗人名和朝代
- 至少包含20首不同的古诗
- 整体流畅度要高,不能卡顿
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>古诗代码雨 · Matrix of Chinese Poems</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
font-family: 'KaiTi', '楷体', 'STKaiti', 'Songti SC', serif;
}
#canvas {
display: block;
cursor: crosshair;
}
/* 信息面板 - 显示完整诗句 */
.info-panel {
position: fixed;
top: 50%;
right: 60px;
transform: translate(20px, -50%);
background: rgba(0, 15, 0, 0.92);
border: 1px solid #0f0;
padding: 28px 36px;
color: #0f0;
box-shadow: 0 0 30px rgba(0, 255, 0, 0.4), inset 0 0 20px rgba(0, 255, 0, 0.08);
pointer-events: none;
opacity: 0;
transition: opacity 0.35s ease, transform 0.35s ease;
min-width: 340px;
max-width: 460px;
z-index: 10;
}
.info-panel.show {
opacity: 1;
transform: translate(0, -50%);
}
.info-panel::before,
.info-panel::after {
content: '';
position: absolute;
width: 18px;
height: 18px;
border-color: #0f0;
border-style: solid;
}
.info-panel::before {
top: -1px;
left: -1px;
border-width: 2px 0 0 2px;
}
.info-panel::after {
bottom: -1px;
right: -1px;
border-width: 0 2px 2px 0;
}
.poem-title {
font-size: 26px;
color: #c8ffc8;
text-align: center;
margin-bottom: 8px;
text-shadow: 0 0 12px #0f0;
letter-spacing: 4px;
}
.poem-author {
font-size: 14px;
color: #7f7;
text-align: center;
margin-bottom: 24px;
letter-spacing: 2px;
font-family: 'Courier New', monospace;
}
.poem-content {
font-size: 22px;
line-height: 2.2;
color: #00ff66;
text-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
letter-spacing: 2px;
text-align: center;
}
/* 顶部系统提示 */
.system-overlay {
position: fixed;
top: 18px;
left: 20px;
color: rgba(0, 200, 0, 0.45);
font-family: 'Courier New', monospace;
font-size: 12px;
pointer-events: none;
letter-spacing: 1px;
line-height: 1.6;
}
.system-overlay .cursor {
display: inline-block;
width: 8px;
height: 14px;
background: rgba(0, 200, 0, 0.6);
vertical-align: -2px;
margin-left: 2px;
animation: blink 1s steps(2) infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
/* 底部提示 */
.hint {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
color: rgba(0, 200, 0, 0.55);
font-family: 'Courier New', monospace;
font-size: 12px;
letter-spacing: 3px;
pointer-events: none;
text-transform: uppercase;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="system-overlay">
> MATRIX_OF_ANCIENT_POEMS.exe<br />
> POEMS LOADED: <span id="poemCount">25</span><br />
> STATUS: <span style="color:#0f0">ONLINE</span
><span class="cursor"></span>
</div>
<div class="info-panel" id="infoPanel">
<div class="poem-title" id="poemTitle"></div>
<div class="poem-author" id="poemAuthor"></div>
<div class="poem-content" id="poemContent"></div>
</div>
<div class="hint">MOVE MOUSE OVER A COLUMN · HOVER TO REVEAL POEM</div>
<script>
;(function () {
'use strict'
// ============= 古诗数据库 (25首) =============
const poems = [
{
title: '静夜思',
author: '李白',
dynasty: '唐',
content: '床前明月光,疑是地上霜。举头望明月,低头思故乡。'
},
{
title: '春晓',
author: '孟浩然',
dynasty: '唐',
content: '春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。'
},
{
title: '登鹳雀楼',
author: '王之涣',
dynasty: '唐',
content: '白日依山尽,黄河入海流。欲穷千里目,更上一层楼。'
},
{
title: '望庐山瀑布',
author: '李白',
dynasty: '唐',
content:
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'
},
{
title: '早发白帝城',
author: '李白',
dynasty: '唐',
content:
'朝辞白帝彩云间,千里江陵一日还。两岸猿声啼不住,轻舟已过万重山。'
},
{
title: '江雪',
author: '柳宗元',
dynasty: '唐',
content: '千山鸟飞绝,万径人踪灭。孤舟蓑笠翁,独钓寒江雪。'
},
{
title: '渔歌子',
author: '张志和',
dynasty: '唐',
content:
'西塞山前白鹭飞,桃花流水鳜鱼肥。青箬笠,绿蓑衣,斜风细雨不须归。'
},
{
title: '山居秋暝',
author: '王维',
dynasty: '唐',
content: '空山新雨后,天气晚来秋。明月松间照,清泉石上流。'
},
{
title: '九月九日忆山东兄弟',
author: '王维',
dynasty: '唐',
content:
'独在异乡为异客,每逢佳节倍思亲。遥知兄弟登高处,遍插茱萸少一人。'
},
{
title: '黄鹤楼送孟浩然之广陵',
author: '李白',
dynasty: '唐',
content:
'故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流。'
},
{
title: '凉州词',
author: '王之涣',
dynasty: '唐',
content:
'黄河远上白云间,一片孤城万仞山。羌笛何须怨杨柳,春风不度玉门关。'
},
{
title: '绝句',
author: '杜甫',
dynasty: '唐',
content:
'两个黄鹂鸣翠柳,一行白鹭上青天。窗含西岭千秋雪,门泊东吴万里船。'
},
{
title: '春夜喜雨',
author: '杜甫',
dynasty: '唐',
content: '好雨知时节,当春乃发生。随风潜入夜,润物细无声。'
},
{
title: '江畔独步寻花',
author: '杜甫',
dynasty: '唐',
content:
'黄四娘家花满蹊,千朵万朵压枝低。留连戏蝶时时舞,自在娇莺恰恰啼。'
},
{
title: '绝句二首',
author: '杜甫',
dynasty: '唐',
content: '迟日江山丽,春风花草香。泥融飞燕子,沙暖睡鸳鸯。'
},
{
title: '送元二使安西',
author: '王维',
dynasty: '唐',
content:
'渭城朝雨浥轻尘,客舍青青柳色新。劝君更尽一杯酒,西出阳关无故人。'
},
{
title: '滁州西涧',
author: '韦应物',
dynasty: '唐',
content:
'独怜幽草涧边生,上有黄鹂深树鸣。春潮带雨晚来急,野渡无人舟自横。'
},
{
title: '枫桥夜泊',
author: '张继',
dynasty: '唐',
content:
'月落乌啼霜满天,江枫渔火对愁眠。姑苏城外寒山寺,夜半钟声到客船。'
},
{
title: '赠花卿',
author: '杜甫',
dynasty: '唐',
content:
'锦城丝管日纷纷,半入江风半入云。此曲只应天上有,人间能得几回闻。'
},
{
title: '江南逢李龟年',
author: '杜甫',
dynasty: '唐',
content:
'岐王宅里寻常见,崔九堂前几度闻。正是江南好风景,落花时节又逢君。'
},
{
title: '赋得古原草送别',
author: '白居易',
dynasty: '唐',
content: '离离原上草,一岁一枯荣。野火烧不尽,春风吹又生。'
},
{
title: '钱塘湖春行',
author: '白居易',
dynasty: '唐',
content:
'孤山寺北贾亭西,水面初平云脚低。几处早莺争暖树,谁家新燕啄春泥。'
},
{
title: '村居',
author: '高鼎',
dynasty: '清',
content:
'草长莺飞二月天,拂堤杨柳醉春烟。儿童散学归来早,忙趁东风放纸鸢。'
},
{
title: '悯农',
author: '李绅',
dynasty: '唐',
content: '锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。'
},
{
title: '咏鹅',
author: '骆宾王',
dynasty: '唐',
content: '鹅鹅鹅,曲项向天歌。白毛浮绿水,红掌拨清波。'
}
]
// ============= Canvas 初始化 =============
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d', { alpha: false })
// 字号 / 列宽根据屏幕宽度自适应
let fontSize = 24
let lineHeight = 30
let columnWidth = 32
function resize() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
if (canvas.width < 800) {
fontSize = 16
lineHeight = 22
columnWidth = 24
} else if (canvas.width < 1280) {
fontSize = 20
lineHeight = 26
columnWidth = 28
} else {
fontSize = 24
lineHeight = 30
columnWidth = 32
}
initColumns()
// 缩放后立即清屏,避免残影
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
// ============= 列类 =============
class Column {
constructor(x) {
this.x = x
this.paused = false
this.setPoem(poems[Math.floor(Math.random() * poems.length)])
// headY 是诗中第一个字(head)的 y 坐标,往下依次排列
this.headY =
-lineHeight * this.chars.length -
Math.random() * canvas.height * 0.6
this.speed = 0.6 + Math.random() * 0.7
// 渲染缓存:高频使用的属性提前计算
this.font = fontSize + 'px "KaiTi","楷体","STKaiti",serif'
}
setPoem(poem) {
this.poem = poem
// 移除标点,飘落的字更纯粹
this.chars = poem.content.replace(/[,。!?、;:]/g, '').split('')
}
update() {
if (this.paused) return
this.headY += this.speed
// 整列已出屏,切换新诗
if (this.headY > canvas.height + lineHeight) {
// 尽量选不同的诗,避免连续两列同诗紧贴
let next
do {
next = poems[Math.floor(Math.random() * poems.length)]
} while (next === this.poem && poems.length > 1)
this.setPoem(next)
this.headY =
-lineHeight * this.chars.length -
Math.random() * canvas.height * 0.4
}
}
draw(ctx) {
if (this.paused) {
this.drawHighlight(ctx)
return
}
ctx.font = this.font
const trail = Math.min(this.chars.length, 18)
for (let i = 0; i < this.chars.length; i++) {
const y = this.headY + i * lineHeight
if (y > canvas.height || y < -lineHeight) continue
// 渐变亮度:head(i=0)最亮,向下线性衰减
const b = i === 0 ? 1 : Math.max(0, 1 - i / trail)
if (b <= 0) continue
if (i === 0) {
// head 略偏白绿 + 微光
ctx.shadowColor = 'rgba(120, 255, 120, 0.9)'
ctx.shadowBlur = 8
ctx.fillStyle = '#c8ffc8'
} else {
ctx.shadowBlur = 0
const g = Math.floor(b * 255)
const r = Math.floor(b * 70)
ctx.fillStyle =
'rgb(' + r + ',' + g + ',' + Math.floor(b * 30) + ')'
}
ctx.fillText(this.chars[i], this.x, y)
}
ctx.shadowBlur = 0
}
drawHighlight(ctx) {
ctx.font = this.font
// 高亮背景框
const bgX = this.x - 12
const bgY = this.headY - 8
const bgW = columnWidth + 8
const bgH = this.chars.length * lineHeight + 16
ctx.fillStyle = 'rgba(0, 50, 0, 0.45)'
ctx.fillRect(bgX, bgY, bgW, bgH)
ctx.strokeStyle = 'rgba(0, 255, 0, 0.55)'
ctx.lineWidth = 1
ctx.strokeRect(bgX, bgY, bgW, bgH)
// 全部字以亮绿色显示 + 光晕
ctx.shadowColor = 'rgba(120, 255, 120, 0.9)'
ctx.shadowBlur = 10
ctx.fillStyle = '#ddffdd'
for (let i = 0; i < this.chars.length; i++) {
const y = this.headY + i * lineHeight
ctx.fillText(this.chars[i], this.x, y)
}
ctx.shadowBlur = 0
}
}
// ============= 列初始化 =============
let columns = []
function initColumns() {
columns = []
const n = Math.ceil(canvas.width / columnWidth)
for (let i = 0; i < n; i++) {
columns.push(new Column(i * columnWidth))
}
}
// ============= 鼠标 / 触摸交互 =============
let mouseX = -1
let hoveredColumn = null
const infoPanel = document.getElementById('infoPanel')
const poemTitle = document.getElementById('poemTitle')
const poemAuthor = document.getElementById('poemAuthor')
const poemContent = document.getElementById('poemContent')
function updateHover() {
if (mouseX < 0) {
if (hoveredColumn) {
hoveredColumn.paused = false
hoveredColumn = null
}
infoPanel.classList.remove('show')
return
}
const idx = Math.floor(mouseX / columnWidth)
const target = idx >= 0 && idx < columns.length ? columns[idx] : null
if (target !== hoveredColumn) {
if (hoveredColumn) hoveredColumn.paused = false
hoveredColumn = target
if (hoveredColumn) {
hoveredColumn.paused = true
poemTitle.textContent = hoveredColumn.poem.title
poemAuthor.textContent =
'【' +
hoveredColumn.poem.dynasty +
'】 ' +
hoveredColumn.poem.author
poemContent.textContent = hoveredColumn.poem.content
infoPanel.classList.add('show')
} else {
infoPanel.classList.remove('show')
}
}
}
window.addEventListener('mousemove', e => {
mouseX = e.clientX
updateHover()
})
window.addEventListener('mouseleave', () => {
mouseX = -1
updateHover()
})
// 触摸支持(移动端)
window.addEventListener(
'touchstart',
e => {
if (e.touches.length) {
mouseX = e.touches[0].clientX
updateHover()
}
},
{ passive: true }
)
window.addEventListener(
'touchmove',
e => {
if (e.touches.length) {
mouseX = e.touches[0].clientX
updateHover()
}
},
{ passive: true }
)
window.addEventListener('touchend', () => {
mouseX = -1
updateHover()
})
window.addEventListener('resize', resize)
// ============= 主循环 =============
function animate() {
// 拖影:半透明黑覆盖,保留上一帧残影形成字符下落尾迹
ctx.fillStyle = 'rgba(0, 0, 0, 0.12)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.textBaseline = 'top'
ctx.textAlign = 'left'
for (let i = 0, len = columns.length; i < len; i++) {
const c = columns[i]
c.update()
c.draw(ctx)
}
requestAnimationFrame(animate)
}
// 初始化 + 启动
document.getElementById('poemCount').textContent = poems.length
resize()
animate()
})()
</script>
</body>
</html>