html5

诗词版黑客帝国代码雨

# 1. 提示词

用一个 HTML 文件实现黑客帝国经典代码雨效果,但有以下创意要求:
- 下落的不是随机字符,而是中国古诗词(每列是一首完整的诗,从上往下逐字飘落)
- 背景纯黑,文字渐变色(从亮绿到暗绿到消失)
- 当鼠标划过某一列时,该列暂停并高亮显示完整诗句,旁边浮现诗人名和朝代
- 至少包含20首不同的古诗
- 整体流畅度要高,不能卡顿

# 2. 源码

<!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>
上次更新: