开源祭祖网页index

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>缅怀先祖 · 祠堂牌位</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:opsz,wght@17..88,200..900&display=swap" rel="stylesheet">
  <style>
    /* ========== CSS @property 自定义属性 (2024-2026) ========== */
    @property --flame-flicker {
      syntax: '<number>';
      initial-value: 1;
      inherits: false;
    }
    @property --smoke-drift {
      syntax: '<length>';
      initial-value: 0px;
      inherits: false;
    }
    @property --glow-rotate {
      syntax: '<angle>';
      initial-value: 0deg;
      inherits: false;
    }
    @property --mist-flow {
      syntax: '<number>';
      initial-value: 0.5;
      inherits: false;
    }

    /* ========== CSS @layer 分层管理 ========== */
    @layer reset, base, layout, plaques, candles, incense, anim, responsive;

    /* ========== reset ========== */
    @layer reset {
      *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
      html { color-scheme: dark; text-size-adjust: 100%; scroll-behavior: smooth; }
      body {
        min-height: 100vh; min-height: 100dvh;
        font-family: 'Noto Serif SC', 'KaiTi', '楷体', 'SimSun', '宋体', serif;
        background: #03030d;
        color: #e8e0d0;
        cursor: crosshair;
        -webkit-tap-highlight-color: transparent;
        overflow-x: hidden;
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      canvas { display: block; position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; }
    }

    /* ========== base ========== */
    @layer base {
      :root {
        --gold: #c9a96e;
        --gold-light: #e0c78a;
        --gold-bright: #f0d890;
        --wood-dark: #1a1208;
        --wood-mid: #2a1c0e;
        --wood-light: #3d2814;
        --text-gold: #d4b870;
        --shadow-glow: 0 0 30px rgba(200,160,60,0.45);
        --space-xs: 0.25rem;
        --space-sm: 0.5rem;
        --space-md: 1rem;
        --space-lg: 1.5rem;
        --space-xl: 2rem;
        --space-2xl: 3rem;
      }
      /* :has() 父选择器 ------ 任意牌位被悬停时,整体微亮 */
      body:has(.plaque-card:hover) {
        background: #050512;
      }
    }

    /* ========== layout ========== */
    @layer layout {
      .shrine-wrapper {
        container: shrine / inline-size;
        position: relative;
        z-index: 10;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: var(--space-xl) var(--space-md) var(--space-2xl);
        width: 100%;
        max-width: 1200px;
        min-height: 100vh;
        min-height: 100dvh;
        justify-content: center;
        gap: var(--space-xl);
      }
      .shrine-title {
        text-align: center;
        margin-bottom: var(--space-sm);
        & h1 {
          font-size: clamp(1.8rem, 4vw, 2.8rem);
          font-weight: 300;
          letter-spacing: 0.3em;
          color: var(--gold-light);
          text-shadow: 0 0 25px rgba(210,170,80,0.6), 0 0 60px rgba(180,140,50,0.3);
          font-variation-settings: 'wght' 300;
          animation: titleWeight 8s ease-in-out infinite alternate;
        }
        & p {
          font-size: 0.9rem;
          color: #a0896c;
          letter-spacing: 0.25em;
          margin-top: 0.3em;
        }
      }
      @keyframes titleWeight {
        0% { font-variation-settings: 'wght' 200; }
        100% { font-variation-settings: 'wght' 500; }
      }
      /* 牌位网格 */
      .plaques-grid {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: flex-end;
        gap: var(--space-lg) var(--space-md);
        padding: var(--space-md) 0;
      }
    }

    /* ========== plaques 牌位卡片 ========== */
    @layer plaques {
      .plaque-card {
        container: plaque / inline-size;
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        cursor: pointer;
        view-transition-name: var(--plaque-vt, plaque-default);
        transition: filter 0.3s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
        &:hover {
          filter: brightness(1.15);
          transform: translateY(-6px);
          z-index: 5;
        }
        &:active {
          transform: scale(0.96);
          transition: transform 0.1s;
        }
        /* 牌位主体 */
        & .plaque-body {
          position: relative;
          width: clamp(70px, 10cqw, 110px);
          height: clamp(140px, 22cqw, 220px);
          background: linear-gradient(
            180deg,
            var(--wood-mid) 0%,
            var(--wood-dark) 15%,
            #1f140a 30%,
            var(--wood-dark) 50%,
            #1a1008 70%,
            var(--wood-mid) 85%,
            var(--wood-light) 100%
          );
          border: 2.5px solid #8b6914;
          border-radius: 18px 18px 5px 5px / 25px 25px 6px 6px;
          box-shadow:
            0 0 0 1px rgba(160,120,50,0.5),
            0 0 0 4px rgba(25,15,8,0.8),
            0 0 0 5px rgba(120,80,30,0.3),
            0 0 18px rgba(200,150,60,0.3),
            0 6px 24px rgba(0,0,0,0.55),
            inset 0 1px 0 rgba(200,160,80,0.1),
            inset 0 -1px 0 rgba(0,0,0,0.3);
          display: flex;
          align-items: center;
          justify-content: center;
          overflow: hidden;
          /* 木纹纹理 */
          &::before {
            content: '';
            position: absolute;
            inset: 0;
            background: repeating-linear-gradient(
              90deg,
              transparent,
              transparent 2px,
              rgba(0,0,0,0.06) 2px,
              rgba(0,0,0,0.06) 3px,
              transparent 3px,
              transparent 5px
            );
            pointer-events: none;
            z-index: 1;
            opacity: 0.5;
          }
          /* 顶部金色云纹装饰 */
          &::after {
            content: '';
            position: absolute;
            top: 8px;
            left: 50%;
            transform: translateX(-50%);
            width: 40%;
            height: 2px;
            background: linear-gradient(90deg, transparent, var(--gold-light), transparent);
            border-radius: 50%;
            opacity: 0.7;
            z-index: 2;
          }
        }
        /* 牌位文字 */
        & .plaque-text {
          position: relative;
          z-index: 3;
          writing-mode: vertical-rl;
          text-orientation: upright;
          color: var(--text-gold);
          font-size: clamp(0.7rem, 1.5cqw, 1rem);
          letter-spacing: 0.15em;
          line-height: 2;
          text-shadow:
            0 0 10px rgba(210,170,80,0.5),
            0 0 25px rgba(180,140,50,0.25);
          font-weight: 400;
          max-height: 80%;
          text-align: center;
          padding: 0.5em 0;
        }
        /* 底座 */
        & .plaque-base {
          width: clamp(90px, 13cqw, 140px);
          height: clamp(14px, 2.5cqw, 22px);
          background: linear-gradient(180deg, var(--wood-light), var(--wood-dark));
          border: 2px solid #6b4f1a;
          border-radius: 3px 3px 8px 8px;
          box-shadow: 0 4px 12px rgba(0,0,0,0.5), 0 0 8px rgba(180,140,60,0.2);
          margin-top: -2px;
          position: relative;
          &::after {
            content: '';
            position: absolute;
            top: 2px;
            left: 10%;
            right: 10%;
            height: 1px;
            background: rgba(180,140,70,0.3);
          }
        }
        /* 小烛火 */
        & .mini-flame {
          position: absolute;
          bottom: -32px;
          left: 50%;
          transform: translateX(-50%);
          width: 14px;
          height: 22px;
          z-index: 4;
          pointer-events: none;
          & .mf-outer {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%) scaleY(var(--flame-flicker));
            width: 12px;
            height: 18px;
            background: radial-gradient(ellipse at center 65%, rgba(255,140,25,0.75), transparent);
            border-radius: 50% 50% 50% 50% / 55% 55% 25% 25%;
            filter: blur(2px);
            animation: flamePulse 0.18s ease-in-out infinite alternate;
          }
          & .mf-inner {
            position: absolute;
            bottom: 2px;
            left: 50%;
            transform: translateX(-50%) scaleY(calc(var(--flame-flicker) * 1.15));
            width: 5px;
            height: 10px;
            background: radial-gradient(ellipse at center 55%, #fffde8, #ffbb33);
            border-radius: 50% 50% 40% 40% / 50% 50% 25% 25%;
            filter: blur(0.4px);
            animation: flamePulse 0.14s ease-in-out infinite alternate;
            animation-delay: 0.05s;
          }
        }
        @keyframes flamePulse {
          0% { --flame-flicker: 0.85; }
          100% { --flame-flicker: 1.25; }
        }
        /* 主牌位更华丽 */
        &.plaque-main {
          & .plaque-body {
            width: clamp(85px, 12cqw, 130px);
            height: clamp(160px, 26cqw, 260px);
            border-width: 3px;
            border-color: #b8942e;
            box-shadow:
              0 0 0 1px rgba(200,160,50,0.6),
              0 0 0 5px rgba(25,15,8,0.85),
              0 0 0 6px rgba(160,100,30,0.4),
              0 0 30px rgba(220,170,60,0.45),
              0 0 60px rgba(200,140,40,0.2),
              0 8px 30px rgba(0,0,0,0.6),
              inset 0 1px 0 rgba(220,180,90,0.15);
          }
          & .plaque-text {
            font-size: clamp(0.8rem, 1.8cqw, 1.15rem);
            color: var(--gold-bright);
            letter-spacing: 0.2em;
          }
          & .plaque-base {
            border-color: #b8942e;
            box-shadow: 0 4px 16px rgba(0,0,0,0.5), 0 0 12px rgba(200,150,50,0.3);
          }
        }
      }
    }

    /* ========== candles 供桌蜡烛 ========== */
    @layer candles {
      .offering-candles {
        display: flex;
        gap: var(--space-xl);
        align-items: flex-end;
        margin-top: var(--space-sm);
      }
      .candle-pair {
        display: flex;
        flex-direction: column;
        align-items: center;
        animation: candleFloat 5s ease-in-out infinite;
        &:nth-child(2) { animation-delay: -2.5s; }
        & .candle-stick {
          width: 16px;
          height: 55px;
          background: linear-gradient(180deg, #fdf8f0, #ecd9b0, #d4b888);
          border-radius: 3px 3px 2px 2px;
          box-shadow: 0 0 14px rgba(240,180,100,0.35), 0 3px 12px rgba(0,0,0,0.4);
          position: relative;
        }
        & .candle-flame {
          position: absolute;
          top: -24px;
          left: 50%;
          transform: translateX(-50%);
          width: 16px;
          height: 26px;
          & .cf-o {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%) scaleY(var(--flame-flicker));
            width: 14px;
            height: 22px;
            background: radial-gradient(ellipse at center 65%, rgba(255,150,30,0.8), transparent);
            border-radius: 50% 50% 50% 50% / 55% 55% 25% 25%;
            filter: blur(3px);
            animation: flamePulse 0.16s ease-in-out infinite alternate;
          }
          & .cf-i {
            position: absolute;
            bottom: 3px;
            left: 50%;
            transform: translateX(-50%) scaleY(calc(var(--flame-flicker) * 1.2));
            width: 6px;
            height: 11px;
            background: radial-gradient(ellipse at center 55%, #ffffee, #ffaa20);
            border-radius: 50% 50% 38% 38% / 48% 48% 22% 22%;
            filter: blur(0.4px);
            animation: flamePulse 0.13s ease-in-out infinite alternate;
            animation-delay: 0.06s;
          }
        }
      }
      @keyframes candleFloat {
        0%,100% { transform: translateY(0); }
        50% { transform: translateY(-7px); }
      }
    }

    /* ========== incense 香炉 ========== */
    @layer incense {
      .incense-burner {
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        margin-top: var(--space-md);
        & .burner-body {
          width: 60px;
          height: 40px;
          background: radial-gradient(ellipse at 50% 40%, #6b5a3a, #3d2b14);
          border-radius: 30px 30px 20px 20px;
          border: 2px solid #8b6914;
          box-shadow: 0 0 20px rgba(200,150,50,0.3), 0 4px 16px rgba(0,0,0,0.5);
          position: relative;
          &::before {
            content: '';
            position: absolute;
            top: -3px;
            left: 10%;
            right: 10%;
            height: 6px;
            background: #5c401a;
            border-radius: 3px;
          }
        }
        /* 三足 */
        & .burner-legs {
          display: flex;
          gap: 16px;
          margin-top: -2px;
          & span {
            width: 6px;
            height: 10px;
            background: #3d2b14;
            border-radius: 0 0 3px 3px;
            border: 1px solid #5c401a;
          }
        }
        /* 香烟 */
        & .smoke-streams {
          position: absolute;
          top: -50px;
          left: 50%;
          transform: translateX(-50%);
          display: flex;
          gap: 8px;
          pointer-events: none;
          & .smoke-wisp {
            width: 3px;
            height: 30px;
            background: rgba(160,140,110,0.5);
            border-radius: 50%;
            filter: blur(5px);
            animation: smokeRise 3s ease-out infinite;
            &:nth-child(1) { animation-delay: 0s; }
            &:nth-child(2) { animation-delay: 0.8s; }
            &:nth-child(3) { animation-delay: 1.6s; }
            &:nth-child(4) { animation-delay: 0.4s; }
            &:nth-child(5) { animation-delay: 1.2s; }
          }
        }
      }
      @keyframes smokeRise {
        0% {
          opacity: 0.5;
          transform: translateY(0) scaleX(1) scaleY(1);
        }
        40% {
          opacity: 0.35;
          transform: translateY(-25px) scaleX(1.8) scaleY(2.5);
        }
        100% {
          opacity: 0;
          transform: translateY(-60px) scaleX(3) scaleY(4);
        }
      }
    }

    /* ========== anim 滚动驱动 & 背景 ========== */
    @layer anim {
      .scroll-mist-layer {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 32vh;
        background: linear-gradient(to top, rgba(120,100,70,0.18), transparent);
        pointer-events: none;
        z-index: 1;
        opacity: var(--mist-flow);
        animation: mistScroll linear;
        animation-timeline: scroll(root);
        animation-range: 0% 100%;
      }
      @keyframes mistScroll {
        0% { --mist-flow: 0.4; transform: translateY(0); }
        100% { --mist-flow: 0.8; transform: translateY(-18px); }
      }
      /* 供桌装饰线 */
      .altar-line {
        width: 70%;
        max-width: 900px;
        height: 1px;
        background: linear-gradient(90deg, transparent, rgba(180,140,60,0.35), transparent);
        margin: var(--space-sm) auto;
        opacity: 0.6;
      }
    }

    /* ========== responsive ========== */
    @layer responsive {
      @container shrine (max-width: 700px) {
        .plaques-grid {
          gap: var(--space-sm);
        }
        .plaque-card .plaque-body {
          width: clamp(55px, 14cqw, 80px);
          height: clamp(110px, 28cqw, 160px);
        }
        .plaque-card .plaque-text {
          font-size: 0.65rem;
          letter-spacing: 0.08em;
        }
        .plaque-card .plaque-base {
          width: clamp(70px, 16cqw, 100px);
          height: 10px;
        }
        .plaque-card.plaque-main .plaque-body {
          width: clamp(65px, 16cqw, 100px);
          height: clamp(130px, 32cqw, 190px);
        }
        .offering-candles { gap: var(--space-md); }
        .candle-pair .candle-stick { height: 40px; width: 12px; }
      }
      @container shrine (max-width: 420px) {
        .plaques-grid {
          gap: 0.4rem;
          justify-content: center;
        }
        .plaque-card .plaque-body {
          width: 50px;
          height: 100px;
          border-radius: 12px 12px 3px 3px / 16px 16px 4px 4px;
          border-width: 1.5px;
        }
        .plaque-card .plaque-text {
          font-size: 0.55rem;
          letter-spacing: 0.04em;
          line-height: 1.6;
        }
        .plaque-card .plaque-base {
          width: 60px;
          height: 8px;
          border-width: 1px;
        }
        .plaque-card.plaque-main .plaque-body {
          width: 58px;
          height: 120px;
        }
        .plaque-card .mini-flame {
          bottom: -22px;
          width: 10px;
          height: 16px;
        }
        .incense-burner .burner-body {
          width: 40px;
          height: 28px;
        }
        .incense-burner .smoke-streams { top: -35px; gap: 4px; }
      }
    }

    /* ========== Dialog 祭文弹窗 ========== */
    #prayer-dialog {
      background: rgba(18,13,7,0.96);
      backdrop-filter: blur(20px);
      border: 2px solid #c9a96e;
      border-radius: 14px;
      color: #f0e5d0;
      padding: 2rem;
      text-align: center;
      max-width: 440px;
      width: 88vw;
      box-shadow: 0 0 60px rgba(200,160,60,0.5), 0 20px 50px rgba(0,0,0,0.7);
      animation: dialogIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
    }
    #prayer-dialog::backdrop {
      background: rgba(0,0,0,0.65);
      backdrop-filter: blur(5px);
    }
    @keyframes dialogIn {
      from { opacity: 0; transform: scale(0.85) translateY(30px); }
      to { opacity: 1; transform: scale(1) translateY(0); }
    }
    #prayer-dialog h2 {
      font-size: 1.4rem;
      color: #e0c78a;
      margin-bottom: 0.5rem;
      font-weight: 400;
    }
    #prayer-dialog .verse-content {
      font-style: italic;
      line-height: 2;
      color: #c0a878;
      margin: 1rem 0;
      font-size: 0.95rem;
    }
    #prayer-dialog .close-btn {
      background: #5c3a11;
      border: 1px solid #8b6914;
      color: #f0e5d0;
      padding: 0.5rem 1.8rem;
      border-radius: 24px;
      font-family: inherit;
      font-size: 0.95rem;
      cursor: pointer;
      transition: all 0.25s;
      &:hover {
        background: #7a4f1a;
        box-shadow: 0 0 18px rgba(200,150,50,0.4);
      }
    }

    /* ========== Popover 提示 ========== */
    #hint-popover {
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(10,8,5,0.8);
      backdrop-filter: blur(10px);
      border: 1px solid #c9a96e;
      border-radius: 22px;
      padding: 0.5rem 1.4rem;
      font-size: 0.85rem;
      color: #e0c78a;
      letter-spacing: 0.06em;
      opacity: 0;
      transition: opacity 0.35s;
      pointer-events: none;
      &:popover-open {
        opacity: 1;
      }
    }
  </style>
</head>
<body>

  <!-- 背景粒子画布 (Web Worker + OffscreenCanvas) -->
  <canvas id="particleCanvas"></canvas>

  <!-- 滚动驱动的烟雾层 -->
  <div class="scroll-mist-layer"></div>

  <!-- 主祠堂容器 -->
  <main class="shrine-wrapper" id="shrineWrapper">

    <!-- 标题 -->
    <div class="shrine-title">
      <h1>缅 怀 先 祖</h1>
      <p>慎终追远 · 民德归厚 · 祖德流芳</p>
    </div>

    <!-- 牌位网格 -->
    <div class="plaques-grid" id="plaquesGrid">
      <!-- 牌位数据由JS动态生成,此处为占位 -->
    </div>

    <!-- 供桌装饰线 -->
    <div class="altar-line"></div>

    <!-- 供桌蜡烛一对 -->
    <div class="offering-candles">
      <div class="candle-pair">
        <div class="candle-stick">
          <div class="candle-flame"><div class="cf-o"></div><div class="cf-i"></div></div>
        </div>
      </div>
      <div class="candle-pair">
        <div class="candle-stick">
          <div class="candle-flame"><div class="cf-o"></div><div class="cf-i"></div></div>
        </div>
      </div>
    </div>

    <!-- 香炉 -->
    <div class="incense-burner">
      <div class="smoke-streams">
        <span class="smoke-wisp"></span>
        <span class="smoke-wisp"></span>
        <span class="smoke-wisp"></span>
        <span class="smoke-wisp"></span>
        <span class="smoke-wisp"></span>
      </div>
      <div class="burner-body"></div>
      <div class="burner-legs">
        <span></span><span></span><span></span>
      </div>
    </div>

    <div class="altar-line"></div>
  </main>

  <!-- 祭文对话框 -->
  <dialog id="prayer-dialog">
    <h2 id="dialogTitle">先祖在上</h2>
    <div class="verse-content" id="dialogVerse"></div>
    <button class="close-btn" id="dialogClose">🕯️ 诚心叩拜</button>
  </dialog>

  <!-- Popover 提示 -->
  <div id="hint-popover" popover>🕯️ 点击牌位诵祭文 · 处处寄哀思</div>

  <script>
    (() => {
      // ==================== 牌位数据 ====================
      const plaqueData = [
        {
          id: 'main',
          name: '张氏历代先祖之位',
          title: '张氏始祖',
          verse: '开基立业,德泽绵长。\n积善之家,必有余庆。\n愿先祖在天之灵,\n护佑张氏子孙,世代昌隆。',
          isMain: true
        },
        {
          id: 'anc1',
          name: '曾祖考张公\n讳德厚之位',
          title: '曾祖考张公',
          verse: '仁德传家,福荫后世。\n敦厚持重,乡里称贤。\n一瓣心香,遥寄追思。',
          isMain: false
        },
        {
          id: 'anc2',
          name: '曾祖妣李太君之位',
          title: '曾祖妣李太君',
          verse: '慈惠贤良,母仪足式。\n相夫教子,淑德流芳。\n追思慈容,永怀恩泽。',
          isMain: false
        },
        {
          id: 'anc3',
          name: '祖考张公\n讳仁安之位',
          title: '祖考张公',
          verse: '勤俭持家,恩泽子孙。\n敦亲睦族,德被乡邻。\n祖德巍巍,山高水长。',
          isMain: false
        },
        {
          id: 'anc4',
          name: '祖妣王太君之位',
          title: '祖妣王太君',
          verse: '温恭淑慎,垂范后昆。\n勤俭慈爱,家风永传。\n音容虽远,慈辉永存。',
          isMain: false
        },
        {
          id: 'anc5',
          name: '先考张公\n讳文远之位',
          title: '先考张公',
          verse: '严慈并济,教诲难忘。\n养育之恩,山高海深。\n子欲养而亲不待,\n此恨绵绵无绝期。',
          isMain: false
        },
        {
          id: 'anc6',
          name: '先妣陈太君之位',
          title: '先妣陈太君',
          verse: '慈爱无疆,思念永存。\n手中线,身上衣,\n临行密密缝,意恐迟迟归。\n母爱如天,永世不忘。',
          isMain: false
        }
      ];

      // ==================== 动态生成牌位 ====================
      const plaquesGrid = document.getElementById('plaquesGrid');
      plaqueData.forEach((data, index) => {
        const card = document.createElement('div');
        card.className = `plaque-card${data.isMain ? ' plaque-main' : ''}`;
        card.style.setProperty('--plaque-vt', `plaque-${data.id}`);
        card.setAttribute('data-index', index);
        card.setAttribute('data-title', data.title);
        card.setAttribute('data-verse', data.verse);
        card.innerHTML = `
          <div class="plaque-body">
            <div class="plaque-text">${data.name.replace(/\n/g, '<br>')}</div>
          </div>
          <div class="plaque-base"></div>
          <div class="mini-flame">
            <div class="mf-outer"></div>
            <div class="mf-inner"></div>
          </div>
        `;
        card.addEventListener('click', () => openPlaqueDialog(index));
        plaquesGrid.appendChild(card);
      });

      // ==================== Dialog 逻辑 ====================
      const dialog = document.getElementById('prayer-dialog');
      const dialogTitle = document.getElementById('dialogTitle');
      const dialogVerse = document.getElementById('dialogVerse');
      const dialogClose = document.getElementById('dialogClose');

      function openPlaqueDialog(index) {
        const data = plaqueData[index];
        dialogTitle.textContent = '📿 ' + data.title;
        dialogVerse.textContent = data.verse;
        if (document.startViewTransition) {
          document.startViewTransition(() => {
            dialog.showModal();
          });
        } else {
          dialog.showModal();
        }
      }

      dialogClose.addEventListener('click', () => {
        if (document.startViewTransition) {
          document.startViewTransition(() => {
            dialog.close();
          });
        } else {
          dialog.close();
        }
      });

      dialog.addEventListener('click', (e) => {
        if (e.target === dialog) dialog.close();
      });

      // ==================== Web Audio API 钟声 ====================
      function playChime() {
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (!AudioCtx) return;
        const ctx = new AudioCtx();
        const now = ctx.currentTime;
        // 低频钟声
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(180, now);
        osc.frequency.linearRampToValueAtTime(90, now + 2);
        gain.gain.setValueAtTime(0.35, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 2.5);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start(now);
        osc.stop(now + 2.5);
        // 泛音
        const osc2 = ctx.createOscillator();
        const gain2 = ctx.createGain();
        osc2.type = 'triangle';
        osc2.frequency.setValueAtTime(360, now + 0.1);
        osc2.frequency.linearRampToValueAtTime(180, now + 2.2);
        gain2.gain.setValueAtTime(0.2, now + 0.1);
        gain2.gain.exponentialRampToValueAtTime(0.001, now + 2.8);
        osc2.connect(gain2);
        gain2.connect(ctx.destination);
        osc2.start(now + 0.1);
        osc2.stop(now + 2.8);
      }

      // 点击蜡烛播放钟声
      document.querySelectorAll('.candle-stick').forEach(c => {
        c.style.cursor = 'pointer';
        c.addEventListener('click', (e) => {
          e.stopPropagation();
          playChime();
        });
      });

      // 点击香炉也播放
      document.querySelector('.burner-body').style.cursor = 'pointer';
      document.querySelector('.burner-body').addEventListener('click', (e) => {
        e.stopPropagation();
        playChime();
      });

      // ==================== Web Worker + OffscreenCanvas 粒子 ====================
      const particleCanvas = document.getElementById('particleCanvas');
      let offscreen;
      try {
        offscreen = particleCanvas.transferControlToOffscreen();
      } catch (e) {
        offscreen = null;
      }

      if (offscreen) {
        const workerCode = `
          let canvas, ctx, particles = [];
          const COUNT = 130;
          self.onmessage = function(e) {
            if (e.data.canvas) {
              canvas = e.data.canvas;
              ctx = canvas.getContext('2d');
              init(canvas.width, canvas.height);
              requestAnimationFrame(loop);
            }
            if (e.data.resize) {
              canvas.width = e.data.resize.w;
              canvas.height = e.data.resize.h;
              init(canvas.width, canvas.height);
            }
          };
          function init(w, h) {
            particles = [];
            for (let i = 0; i < COUNT; i++) {
              particles.push({
                x: Math.random() * w,
                y: Math.random() * h,
                r: Math.random() * 2.2 + 0.4,
                vy: Math.random() * 0.5 + 0.15,
                vx: (Math.random() - 0.5) * 0.35,
                alpha: Math.random() * 0.7 + 0.15,
                wobble: Math.random() * Math.PI * 2,
                wobbleSpeed: Math.random() * 0.02 + 0.005,
              });
            }
          }
          function loop() {
            if (!canvas) return;
            const w = canvas.width, h = canvas.height;
            ctx.clearRect(0, 0, w, h);
            for (let p of particles) {
              p.y -= p.vy;
              p.x += p.vx + Math.sin(p.wobble) * 0.4;
              p.wobble += p.wobbleSpeed;
              if (p.y < -15) { p.y = h + 10; p.x = Math.random() * w; }
              if (p.x < -15) p.x = w + 10;
              if (p.x > w + 15) p.x = -10;
              const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3);
              grad.addColorStop(0, \`rgba(240,210,140,\${p.alpha})\`);
              grad.addColorStop(1, 'rgba(0,0,0,0)');
              ctx.fillStyle = grad;
              ctx.beginPath();
              ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI*2);
              ctx.fill();
            }
            requestAnimationFrame(loop);
          }
        `;
        const blob = new Blob([workerCode], { type: 'text/javascript' });
        const workerUrl = URL.createObjectURL(blob);
        const worker = new Worker(workerUrl);
        URL.revokeObjectURL(workerUrl);
        function resizeOffscreen() {
          const dpr = Math.min(window.devicePixelRatio || 1, 2);
          const w = window.innerWidth;
          const h = window.innerHeight;
          particleCanvas.width = w * dpr;
          particleCanvas.height = h * dpr;
          particleCanvas.style.width = w + 'px';
          particleCanvas.style.height = h + 'px';
          worker.postMessage({ resize: { w: w * dpr, h: h * dpr } });
        }
        window.addEventListener('resize', resizeOffscreen);
        resizeOffscreen();
        worker.postMessage({ canvas: offscreen }, [offscreen]);
      } else {
        // 降级:主线程 Canvas 2D
        const ctx = particleCanvas.getContext('2d');
        let particles = [];
        const COUNT = 100;
        function init2D() {
          const dpr = Math.min(window.devicePixelRatio || 1, 2);
          const w = window.innerWidth;
          const h = window.innerHeight;
          particleCanvas.width = w * dpr;
          particleCanvas.height = h * dpr;
          particleCanvas.style.width = w + 'px';
          particleCanvas.style.height = h + 'px';
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.scale(dpr, dpr);
          particles = [];
          for (let i = 0; i < COUNT; i++) {
            particles.push({
              x: Math.random() * w, y: Math.random() * h,
              r: Math.random() * 2 + 0.4, vy: Math.random() * 0.5 + 0.15,
              vx: (Math.random() - 0.5) * 0.35, alpha: Math.random() * 0.7 + 0.15,
              wobble: Math.random() * Math.PI * 2, wobbleSpeed: Math.random() * 0.02 + 0.005,
            });
          }
        }
        function loop2D() {
          const w = window.innerWidth, h = window.innerHeight;
          ctx.clearRect(0, 0, w, h);
          for (let p of particles) {
            p.y -= p.vy; p.x += p.vx + Math.sin(p.wobble) * 0.4; p.wobble += p.wobbleSpeed;
            if (p.y < -15) { p.y = h + 10; p.x = Math.random() * w; }
            if (p.x < -15) p.x = w + 10; if (p.x > w + 15) p.x = -10;
            const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3);
            grad.addColorStop(0, `rgba(240,210,140,${p.alpha})`);
            grad.addColorStop(1, 'rgba(0,0,0,0)');
            ctx.fillStyle = grad;
            ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2); ctx.fill();
          }
          requestAnimationFrame(loop2D);
        }
        init2D();
        window.addEventListener('resize', init2D);
        loop2D();
      }

      // ==================== Popover 提示 ====================
      const hint = document.getElementById('hint-popover');
      if (hint.showPopover) {
        setTimeout(() => {
          hint.showPopover();
          setTimeout(() => hint.hidePopover(), 5000);
        }, 1200);
      } else {
        hint.style.opacity = '1';
        setTimeout(() => { hint.style.opacity = '0'; }, 5000);
      }

      // 全局点击空白处播放细微钟声
      document.addEventListener('click', (e) => {
        if (
          e.target.closest('.plaque-card') ||
          e.target.closest('.candle-stick') ||
          e.target.closest('.burner-body') ||
          e.target.closest('dialog') ||
          e.target.closest('#hint-popover')
        ) return;
        // 轻声回响
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (!AudioCtx) return;
        const ctx = new AudioCtx();
        const now = ctx.currentTime;
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(260, now);
        osc.frequency.linearRampToValueAtTime(130, now + 1);
        gain.gain.setValueAtTime(0.12, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 1.5);
        osc.connect(gain); gain.connect(ctx.destination);
        osc.start(now); osc.stop(now + 1.5);
      });

      console.log('🏛️ 祠堂牌位 · 2026技术栈:CSS @property · CSS Nesting · :has() · View Transitions · Popover · Dialog · Container Queries · Scroll-driven Animations · Web Worker + OffscreenCanvas · Web Audio API · 可变字体');
      console.log('🕯️ 七位先祖牌位已立,点击牌位诵读祭文,点击蜡烛聆听钟声。');
    })();
  </script>
</body>
</html>
相关推荐
程序员夏末3 小时前
【开源经历 | 第一篇】参与开源需要掌握的Git和Github指令
git·开源
@不误正业3 小时前
第13章-开源鸿蒙是否适合做端侧AI操作系统
人工智能·开源·harmonyos
傻瓜搬砖人3 小时前
SpringMVC的请求
java·前端·javascript·spring
爱上好庆祝3 小时前
学习js的第六天(js基础的结束)
开发语言·前端·javascript·学习·ecmascript
IT_陈寒4 小时前
JavaScript的异步地狱,我差点没爬出来
前端·人工智能·后端
光影少年4 小时前
Webpack打包性能优化方面的经验
前端·webpack·性能优化
冬奇Lab4 小时前
一天一个开源项目(第91篇):RuFlo - Github趋势榜第一,让 AI 像蜂群一样协同作战的多智能体编排引擎
开源·agent·ai编程
Das14 小时前
通过命令行下载kaggle数据
前端·chrome
剑神一笑4 小时前
CSS Animation Timeline 可视化动画编辑器:从关键帧到流畅动画
前端·css·编辑器