mcpField
H
html-presentations

html-presentations

github.com/ericmjl文档处理4165 次安装5 天前更新
代码仓库

关于 html-presentations

html-presentations 是用于制作单个 HTML 文件幻灯片的技能,仅使用原生 HTML、CSS 和 JavaScript。它涵盖主题布局、键盘导航、URL 幻灯片位置保存,以及 SVG 图表和动画。

主要功能

  1. 01单文件 HTML 幻灯片
  2. 02三种配色主题
  3. 03键盘切换幻灯片
  4. 04通过 URL 哈希保存页码
  5. 05主题化 SVG 图表与动画

适用场景

  1. 01无需构建步骤制作浏览器演示文稿
  2. 02分享可直接打开指定幻灯片的链接
  3. 03用 SVG 动画图表讲解流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ericmjl/skills --skill html-presentations

安装后在 Claude Code 中输入/html-presentations确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
canvas-chat-overview.html
example-agent-skills-overview.html
references/
svg-animations.md
svg-diagrams.md
theme-catppuccin.css
theme-nord.css
theme-terminal.css

详细介绍

HTML 演示文稿

创建独立的 HTML 幻灯片演示文稿。无需框架、CDN 或构建步骤——只需一个 .html 文件,即可在任何浏览器中打开。

用法

当用户需要演示文稿、幻灯片组或幻灯片,并希望使用原生 HTML、CSS 和 JavaScript 制作单个 HTML 文件时,请使用此技能。不适用于 reveal.js 或基于框架的演示文稿。生成幻灯片之前,请先询问目标、受众和交付方式(步骤 0);根据答案调整长度、重点和内容密度。

要求

无。输出为单个 .html 文件,可在任何现代浏览器(Chrome、Firefox、Safari、Edge)中运行。

功能

  1. 单个文件——所有 CSS、JS、SVG 图表和内容都放在一个 .html 文件中
  2. 主题化——提供三种配色预设:terminal.css(默认)、catppuccin、nord
  3. 键盘导航——按 Space/Right/k 前进,按 Shift+Space/Left/j 后退;按 Escape 可打开所有幻灯片的可视网格,跳转到任意幻灯片
  4. URL 哈希持久化——当前幻灯片的索引会存储在 URL 哈希中(例如 #5)。刷新页面后仍会停留在同一张幻灯片;分享带有哈希的链接即可直接打开对应幻灯片
  5. SVG 图表——流程图、架构图、流水线、对比图,全部通过 CSS 自定义属性应用主题
  6. SVG 动画——路径绘制、依次淡入、脉冲/发光、流动动画;幻灯片激活时触发

工作原理

示例:功能展示

查看以下完整演示文稿,了解此技能能够生成的内容:

  1. assets/example-agent-skills-overview.html — 精致的概览,包含聚焦式布局、双列网格、丰富的 SVG 图表、胶囊徽标、进度导航和概览网格。可将其作为结构和视觉效果的质量标杆。

  2. assets/canvas-chat-overview.html — 产品概览,包含交互式循环幻灯片演示:

    • 迷你画布——可拖动节点,以及实时更新的幻灯片 SVG 连线
    • 聊天与回复——模拟输入、节点显示和连线绘制;自动循环
    • 高亮与分支——模拟文本选择、摘录节点和来源高亮;循环播放
    • 多选——模拟节点选择、回复、“Thinking…”和合并节点
    • 自动布局——模拟光标移至工具栏的“Layout”按钮,点击时出现日芒/射线效果,节点从杂乱状态动画变为整齐布局;重置后循环播放
    • 导航——模拟光标和滚动提示;语义缩放放大(显示完整节点内容)和缩小(显示摘要视图);循环播放
    • URL 哈希持久化——刷新后仍停留在当前幻灯片;使用 setTimeout(0) 分发 slidechange,以便演示脚本在加载时运行

当用户需要自动循环演示(光标、点击效果、缩放或逐步 UI 模拟),而不是静态图表时,请使用 Canvas Chat 概览。

步骤 0:询问用户(开始制作前)

在向用户确认以下事项之前,不要生成幻灯片。 以对话的方式提问;每条消息问一两个问题即可。根据用户的回答,决定演示文稿的长度、侧重点,以及文字与图表的占比。

(a) 演示目标

如果用户还没有明确说明目标,提供选项供其选择或组合:

  • 推介 / 说服 — 说服受众接受某个想法、产品或决策
  • 解释 / 教学 — 传达某个概念、流程或系统,让受众理解
  • 更新 / 状态 — 分享进展、结果或发现(例如项目更新、实验结果)
  • 参考资料 / 讲义 — 供受众留存并日后浏览;内容密集,但便于快速查阅
  • 概览 / 路线图 — 提供高层次的结构或计划;细节少,着眼全局
  • 其他 — 请用户自行描述

(b) 目标受众

询问演示文稿面向哪些人(例如高管、工程师、客户、学生、公众)。这会影响术语的使用程度、内容深度,以及需要解释哪些内容、哪些内容可以默认受众已知。

(c) 呈现方式

  • 有演讲者 — 幻灯片辅助演讲;每张幻灯片文字更少,视觉元素更多,由演讲者补充说明。优先使用简短要点和图表。
  • 自助浏览式(无演讲者) — 受众自行阅读;幻灯片必须能够独立传达信息。需要更多说明性文字、更清晰的标签,也可以增加幻灯片数量或提高内容密度。

(d) 其他有用的问题

  • 大致时长 — “几分钟?” / “10–15 分钟?” / “深入讲解?”——据此调整幻灯片数量和细节程度。
  • 必备内容与省略内容 — “有哪些内容必须包含,或应该省略?”
  • 语气 — 正式还是随意、严肃还是轻松,若会影响措辞或设计,请予以确认。

记录用户的回答,并在选择主题、编写大纲和决定每张幻灯片的内容量时加以参考。

步骤 1:选择主题

阅读一份主题参考文件,并将其中的 CSS 粘贴到演示文稿的 <style> 块中。默认使用 catppuccin,除非用户另有要求。

主题文件风格
terminal.cssreferences/theme-terminal.css黑底绿色磷光、等宽字体、扫描线
catppuccinreferences/theme-catppuccin.css暖色深色底上的柔和点缀色(Mocha 变体)
nordreferences/theme-nord.css北极蓝灰色调、低饱和配色

所有主题都定义了相同的 CSS 自定义属性(--bg、--fg、--accent 等),因此基础布局 CSS 和所有图表都可以与任意主题搭配使用。

步骤 2:生成 HTML 文件

使用此骨架。三个部分——主题 CSS、基础布局 CSS 和 导航 JS——结构始终相同;只有幻灯片内容会变化。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PRESENTATION_TITLE</title>
  <style>
    /* ===== Theme (paste from reference file) ===== */
    :root { /* ... theme variables ... */ }

    /* ===== Base layout ===== */
    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { height: 100%; overflow: hidden; }
    body {
      background: var(--bg);
      color: var(--fg);
      font-family: var(--body-font);
    }
    .deck { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
    .slide {
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;
      padding: 6vh 8vw;
      opacity: 0;
      transition: opacity 0.5s ease;
      pointer-events: none;
      overflow: hidden;
    }
    .slide.active {
      opacity: 1;
      pointer-events: auto;
    }

    /* ===== Typography ===== */
    h1 { font-size: 3.5vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 2vh; }
    h2 { font-size: 2.8vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 2vh; }
    h3 { font-size: 2.2vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 1.5vh; }
    p, li { font-size: 1.6vw; line-height: 1.7; margin-bottom: 1vh; }
    ul, ol { padding-left: 2vw; }
    code {
      font-family: var(--mono-font);
      background: var(--surface);
      padding: 0.15em 0.4em;
      border-radius: 4px;
      font-size: 0.9em;
    }
    pre {
      background: var(--surface);
      padding: 2vh 2vw;
      border-radius: 8px;
      overflow-x: auto;
    }
    pre code { background: none; padding: 0; font-size: 1.3vw; }

    /* ===== Bottom nav bar (arrows + progress dots) ===== */
    .nav-bar {
      position: fixed;
      bottom: 2.5vh;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 12px;
      align-items: center;
      z-index: 100;
    }
    .progress {
      display: flex;
      gap: 6px;
      align-items: center;
    }
    .progress-dot {
      width: 8px;
      height: 8px;
      border-radius: 4px;
      background: var(--muted);
      transition: width 0.35s ease, background 0.35s ease;
      cursor: pointer;
    }
    .progress-dot.active {
      width: 32px;
      background: var(--accent);
    }
    .nav-arrow {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      width: 36px;
      height: 32px;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--muted);
      cursor: pointer;
      transition: color 0.2s, border-color 0.2s;
      user-select: none;
      flex-shrink: 0;
    }
    .nav-arrow:hover {
      color: var(--accent);
      border-color: var(--accent);
    }
    .nav-arrow .arrow {
      font-size: 1vw;
      line-height: 1;
      font-family: var(--body-font);
    }
    .nav-arrow .key {
      font-size: 0.55vw;
      font-family: var(--mono-font);
      opacity: 0.7;
    }

    /* ===== Overview grid (Escape) ===== */
    .overview-overlay {
      position: fixed;
      inset: 0;
      background: var(--bg);
      z-index: 200;
      display: none;
      align-items: center;
      justify-content: center;
      padding: 3vh 3vw;
      overflow: auto;
    }
    .overview-overlay.visible {
      display: flex;
      flex-direction: column;
    }
    .overview-overlay .overview-hint {
      font-size: 1.2vw;
      color: var(--muted);
      margin-bottom: 2vh;
    }
    .overview-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
      gap: 1rem;
      max-width: 90vw;
    }
    .overview-card {
      background: var(--surface);
      border: 2px solid var(--border);
      border-radius: 8px;
      padding: 1rem;
      cursor: pointer;
      text-align: center;
      transition: border-color 0.2s, transform 0.2s;
    }
    .overview-card:hover {
      border-color: var(--accent);
      transform: scale(1.02);
    }
    .overview-card.active {
      border-color: var(--accent);
      background: var(--surface2);
    }
    .overview-card .num {
      font-size: 1.5vw;
      font-weight: 700;
      color: var(--accent);
      display: block;
    }
    .overview-card .title {
      font-size: 0.9vw;
      margin-top: 0.5rem;
      color: var(--fg);
      line-height: 1.3;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    /* ===== Supplementary slides ===== */
    .supp-label {
      display: inline-block; font-size: 0.85vw; text-transform: uppercase;
      letter-spacing: 0.15em; color: var(--muted); border: 1px solid var(--border);
      border-radius: 4px; padding: 0.2em 0.7em; margin-bottom: 2vh;
    }
    .supp-link {
      color: var(--accent); text-decoration: none;
      border-bottom: 1px dashed var(--accent); cursor: pointer;
    }
    .supp-link:hover { border-bottom-style: solid; }
    .progress-sep {
      width: 1px; height: 12px; background: var(--border); margin: 0 4px; flex-shrink: 0;
    }
    .progress-dot.supplementary { opacity: 0.5; }
    .progress-dot.supplementary.active { opacity: 1; }
    .overview-card.supplementary { opacity: 0.7; }

    /* ===== Diagram base classes ===== */
    .diagram-node      { fill: var(--diagram-fill); stroke: var(--diagram-stroke); stroke-width: 2; }
    .diagram-highlight  { fill: var(--diagram-highlight); stroke: var(--diagram-highlight); stroke-width: 2; }
    .diagram-diamond    { fill: var(--surface2); stroke: var(--diagram-stroke); stroke-width: 2; }
    .diagram-label      { fill: var(--diagram-text); font-family: var(--body-font); font-size: 14px; text-anchor: middle; dominant-baseline: central; }
    .diagram-label-inv  { fill: var(--bg); font-family: var(--body-font); font-size: 14px; text-anchor: middle; dominant-baseline: central; }
    .diagram-label-sm   { fill: var(--diagram-text); font-family: var(--body-font); font-size: 11px; text-anchor: middle; dominant-baseline: central; opacity: 0.7; }
    .diagram-edge       { stroke: var(--diagram-arrow); stroke-width: 2; fill: none; }
    .diagram-edge-accent { stroke: var(--diagram-stroke); stroke-width: 2; fill: none; }
    .slide svg          { max-width: 100%; height: auto; }

    /* ===== Animation base classes ===== */
    .slide .anim-fade {
      opacity: 0;
      transform: translateY(20px);
    }
    .slide.active .anim-fade {
      opacity: 1;
      transform: translateY(0);
      transition: opacity 0.6s ease var(--delay, 0s),
                  transform 0.6s ease var(--delay, 0s);
    }
    .slide .anim-draw {
      stroke-dasharray: var(--len, 1000);
      stroke-dashoffset: var(--len, 1000);
    }
    .slide.active .anim-draw {
      stroke-dashoffset: 0;
      transition: stroke-dashoffset 1.2s ease-in-out var(--delay, 0s);
    }
    @keyframes pulse-glow {
      0%, 100% { filter: drop-shadow(0 0 3px var(--diagram-stroke)); }
      50%      { filter: drop-shadow(0 0 10px var(--diagram-stroke)); }
    }
    .slide.active .anim-pulse {
      animation: pulse-glow 2s ease-in-out infinite;
      animation-delay: var(--delay, 0s);
    }

    /* Additional theme-specific rules (paste from theme file if any) */
  </style>
</head>
<body>
  <div class="deck">

    <section class="slide" id="title">
      <h1>Presentation Title</h1>
      <p>Author &mdash; Date</p>
    </section>

    <section class="slide" id="slide-2">
      <h2>Slide Heading</h2>
      <ul>
        <li>Point one</li>
        <li>Point two</li>
      </ul>
    </section>

    

  </div>

  <div class="nav-bar">
    <div class="nav-arrow nav-prev"><span class="arrow">&#8249;</span><span class="key">j</span></div>
    <div class="progress"></div>
    <div class="nav-arrow nav-next"><span class="arrow">&#8250;</span><span class="key">k</span></div>
  </div>

  <div class="overview-overlay" id="overview" aria-label="Slide overview">
    <p class="overview-hint">Click a slide to jump · Esc to close</p>
    <div class="overview-grid"></div>
  </div>

  <script>
    (function () {
      var slides = document.querySelectorAll('.slide');
      var current = 0;
      var total = slides.length;
      var progress = document.querySelector('.progress');
      var prevBtn = document.querySelector('.nav-prev');
      var nextBtn = document.querySelector('.nav-next');
      var overview = document.getElementById('overview');
      var overviewGrid = overview.querySelector('.overview-grid');

      var suppInserted = false;
      for (var i = 0; i < total; i++) {
        if (!suppInserted && slides[i].hasAttribute('data-supplementary')) {
          var sep = document.createElement('div');
          sep.className = 'progress-sep';
          progress.appendChild(sep);
          suppInserted = true;
        }
        var dot = document.createElement('div');
        dot.className = 'progress-dot';
        if (slides[i].hasAttribute('data-supplementary')) dot.classList.add('supplementary');
        dot.dataset.index = i;
        dot.addEventListener('click', function () {
          show(parseInt(this.dataset.index));
        });
        progress.appendChild(dot);
      }
      var dots = progress.querySelectorAll('.progress-dot');

      for (var i = 0; i < total; i++) {
        var card = document.createElement('button');
        card.type = 'button';
        card.className = 'overview-card';
        if (slides[i].hasAttribute('data-supplementary')) card.classList.add('supplementary');
        card.dataset.index = i;
        var titleEl = slides[i].querySelector('h1, h2, h3');
        var title = titleEl ? titleEl.textContent.trim() : 'Slide ' + (i + 1);
        card.innerHTML = '<span class="num">' + (i + 1) + '</span><span class="title">' + title.replace(/</g, '&lt;') + '</span>';
        card.addEventListener('click', function () {
          show(parseInt(this.dataset.index));
          overview.classList.remove('visible');
        });
        overviewGrid.appendChild(card);
      }
      var overviewCards = overviewGrid.querySelectorAll('.overview-card');

      var slideIdMap = {};
      for (var i = 0; i < total; i++) {
        if (slides[i].id) slideIdMap[slides[i].id] = i;
      }

      function getSlideIndexFromHash() {
        var hash = window.location.hash;
        if (!hash || hash.length < 2) return 0;
        var n = parseInt(hash.slice(1), 10);
        if (isNaN(n) || n < 0 || n >= total) return 0;
        return n;
      }

      function updateHashForSlide(idx) {
        var url = window.location.pathname + window.location.search + '#' + idx;
        if (window.history.replaceState) {
          window.history.replaceState(null, '', url);
        } else {
          window.location.hash = idx;
        }
      }

      function show(idx) {
        if (idx < 0 || idx >= total) return;
        slides[current].classList.remove('active');
        dots[current].classList.remove('active');
        if (overviewCards[current]) overviewCards[current].classList.remove('active');
        current = idx;
        slides[current].classList.add('active');
        dots[current].classList.add('active');
        if (overviewCards[current]) overviewCards[current].classList.add('active');
        updateHashForSlide(current);
      }

      document.addEventListener('click', function (e) {
        var link = e.target.closest('[data-goto]');
        if (link) {
          e.preventDefault();
          var target = link.getAttribute('data-goto');
          if (slideIdMap[target] !== undefined) show(slideIdMap[target]);
        }
      });

      prevBtn.addEventListener('click', function () { show(current - 1); });
      nextBtn.addEventListener('click', function () { show(current + 1); });

      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          overview.classList.toggle('visible');
          if (overview.classList.contains('visible')) {
            for (var i = 0; i < overviewCards.length; i++) {
              overviewCards[i].classList.toggle('active', i === current);
            }
          }
          return;
        }
        if (overview.classList.contains('visible')) return;
        switch (e.key) {
          case ' ':
            e.preventDefault();
            show(current + (e.shiftKey ? -1 : 1));
            break;
          case 'ArrowRight': case 'k':
            show(current + 1);
            break;
          case 'ArrowLeft': case 'j':
            show(current - 1);
            break;
        }
      });

      window.addEventListener('hashchange', function () {
        var idx = getSlideIndexFromHash();
        if (idx !== current) show(idx);
      });

      show(getSlideIndexFromHash());
    })();
  </script>
</body>
</html>
第 3 步:添加幻灯片内容

标题幻灯片——居中、大号标题、副标题:

<section class="slide" id="title">
  <h1>Title Text</h1>
  <p>Subtitle or author</p>
</section>

内容幻灯片——标题 + 正文:

<section class="slide" id="topic-name">
  <h2>Heading</h2>
  <p>Body text or a list:</p>
  <ul>
    <li>Item</li>
  </ul>
</section>

双栏幻灯片——使用内联 CSS 网格:

<section class="slide" id="comparison">
  <h2>Side by Side</h2>
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:4vw; flex:1;">
    <div>Left column content</div>
    <div>Right column content</div>
  </div>
</section>

图表幻灯片——使用图表类的内联 SVG:

<section class="slide" id="architecture">
  <h2>System Architecture</h2>
  <svg viewBox="0 0 800 400">
    
  </svg>
</section>

聚焦展示 / 功能幻灯片——双栏网格,左侧为文字,右侧为大型 SVG。适用于以醒目的视觉效果展示工具、功能或概念:

<section class="slide" id="spotlight-feature" style="justify-content:center;">
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:4vw; align-items:center;">
    <div>
      <p class="dim anim-fade" style="--delay:0.1s; font-size:1vw; text-transform:uppercase; letter-spacing:0.2em; margin-bottom:1vh;">Label</p>
      <h2 class="anim-fade" style="--delay:0.2s; font-size:3.2vw;"><code style="background:var(--surface); padding:0.15em 0.4em; border-radius:6px;">Feature Name</code></h2>
      <p class="anim-fade" style="--delay:0.4s; font-size:2.4vw; color:var(--accent); margin-top:2vh; line-height:1.3;">Short tagline.<br>One line per idea.</p>
      <div class="anim-fade" style="--delay:0.7s; display:flex; gap:0.8vw; flex-wrap:wrap; margin-top:3vh;">
        <span style="background:var(--accent); color:white; padding:0.4em 1em; border-radius:999px; font-size:1.1vw; font-weight:600;">Primary pill</span>
        <span style="background:var(--surface); color:var(--fg); padding:0.4em 1em; border-radius:999px; font-size:1.1vw;">Secondary pill</span>
        <span style="background:var(--surface); color:var(--fg); padding:0.4em 1em; border-radius:999px; font-size:1.1vw;">Another pill</span>
      </div>
    </div>
    <div class="anim-fade" style="--delay:0.5s;">
      <svg viewBox="0 0 400 340">
        
      </svg>
    </div>
  </div>
</section>

聚焦展示的设计原则:

  • 填满两栏。 SVG 的 viewBox 应足够宽裕(例如 0 0 400 340),让图表填满幻灯片右半部分。过小且留白过多的 SVG 会显得不完整。
  • 不使用项目符号。 将项目符号列表替换为简短的标语(使用 <br> 换行),并在下方放置关键词胶囊。
  • 突出显示一个胶囊。 主关键词使用 background:var(--accent); color:white; font-weight:600;其余关键词保持中性色(var(--surface) / var(--fg))。
  • 丰富图表内容。 右栏的 SVG 应讲述一个故事——使用多个节点、箭头、标签、阶段或微型线框图。图表是幻灯片的视觉焦点。

补充幻灯片——放在“谢谢”幻灯片 之后 的详情、常见问题或参考幻灯片。通过 data-goto 从主幻灯片链接到这些幻灯片:

<section class="slide" id="supp-topic" data-supplementary>
  <span class="supp-label">Supplementary</span>
  <h2>Detailed Topic</h2>
  <p>Deep-dive content that supports a main slide.</p>
</section>

补充幻灯片的约定:

  • 放在“谢谢”之后。 演示文稿的顺序为:主幻灯片 → 致谢 → 谢谢 → 补充内容。观众看到的是简洁的结尾;补充材料可供问答或自主学习时使用。
  • 使用 data-supplementary 标记。 将此属性添加到 <section>。JS 会用它在进度条中插入视觉分隔线,并将补充内容对应的圆点淡化显示。
  • 添加 <span class="supp-label">。 在幻灯片顶部显示“补充内容”或“补充内容 · 常见问题”徽标。
  • 从主幻灯片链接。 在任意主幻灯片中使用 <a class="supp-link" data-goto="supp-topic">详情 →</a>。JS 会拦截点击事件,并按 ID 跳转到目标幻灯片。
  • 从“谢谢”幻灯片链接。 在“谢谢”幻灯片上列出所有补充幻灯片的链接,让观众了解可用内容。
  • ID 约定。 补充幻灯片的 ID 以 supp- 为前缀(例如 supp-detail、supp-harness)。

必需的 CSS(已包含在上方的基础布局中):

.supp-label {
  display: inline-block; font-size: 0.85vw; text-transform: uppercase;
  letter-spacing: 0.15em; color: var(--muted); border: 1px solid var(--border);
  border-radius: 4px; padding: 0.2em 0.7em; margin-bottom: 2vh;
}
.supp-link {
  color: var(--accent); text-decoration: none;
  border-bottom: 1px dashed var(--accent); cursor: pointer;
}
.supp-link:hover { border-bottom-style: solid; }
.progress-sep {
  width: 1px; height: 12px; background: var(--border); margin: 0 4px; flex-shrink: 0;
}
.progress-dot.supplementary { opacity: 0.5; }
.progress-dot.supplementary.active { opacity: 1; }

必需的 JS 补充内容(已包含在上方的基础脚本中):

  • 构建 slideIdMap,将每张幻灯片的 id 映射到其索引。
  • 点击 data-goto 链接时:show(slideIdMap[target])。
  • 构建进度圆点时,在第一张补充幻灯片之前插入 .progress-sep <div>。
  • 为带有 data-supplementary 的幻灯片对应的圆点和概览卡片添加 .supplementary 类。
  • URL 哈希持久化: 将当前幻灯片索引存入 URL 哈希(例如 #5)。页面加载时,调用 show(getSlideIndexFromHash()) 而不是 show(0),这样刷新页面后仍会停留在同一张幻灯片中。在 show(idx) 中调用 updateHashForSlide(current)(可用时使用 history.replaceState),让 URL 始终反映当前幻灯片。添加 hashchange 监听器,以便用户编辑 URL 或使用浏览器的前进 / 后退功能时同步演示文稿。

每个 <section> 都必须有唯一的 id。

演示文稿结构

结构良好的演示文稿应按以下顺序排列:

  1. 标题——名称、作者、日期
  2. 主幻灯片——核心内容(定义、缘由、方法、示例等)
  3. 致谢——鸣谢协作者、工具和来源
  4. 谢谢——结尾幻灯片,包含链接和补充主题列表
  5. 补充内容——详情幻灯片、常见问题、参考资料(从主幻灯片和“谢谢”幻灯片链接)

进度条会用一条细竖线在视觉上分隔主内容与补充内容。

步骤 4:添加 SVG 图表

阅读 references/svg-diagrams.md,了解完整示例:

  • 流程图 — 决策树、包含方框、菱形和箭头的流程
  • 架构图 — 分层结构、互连的服务
  • 管线图 — 流经各处理阶段的数据
  • 对比图 — 并排对比功能或选项

所有图表 SVG 元素都使用基础布局中定义的 .diagram-* CSS 类,因此会自动匹配当前主题。

箭头标记 — 每个使用箭头的 SVG 都需要此 <defs> 块:

<defs>
  <marker id="arrow" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto-start-reverse">
    <polygon points="0 0, 10 3.5, 0 7" fill="var(--diagram-arrow)" />
  </marker>
  <marker id="arrow-accent" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto-start-reverse">
    <polygon points="0 0, 10 3.5, 0 7" fill="var(--diagram-stroke)" />
  </marker>
  <filter id="shadow" x="-5%" y="-5%" width="110%" height="120%">
    <feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.15" />
  </filter>
</defs>

在 .diagram-edge 线上使用 marker-end="url(#arrow)",在 .diagram-edge-accent 线上使用 marker-end="url(#arrow-accent)"。

步骤 5:酌情添加 SVG 动画

阅读 references/svg-animations.md,了解完整示例和每种动画适用场景的指导。

三个内置动画类(在基础 CSS 中定义):

类效果触发时机
.anim-fade淡入视图幻灯片变为活动状态
.anim-drawSVG 路径自行绘制(stroke-dashoffset)幻灯片变为活动状态
.anim-pulseSVG 元素发出脉冲光晕幻灯片变为活动状态

使用内联 --delay 设置错开时间:

<p class="anim-fade" style="--delay:0.2s">Appears first</p>
<p class="anim-fade" style="--delay:0.5s">Appears second</p>

绘制 SVG 路径时,将 --len 设为路径的总长度:

<path class="diagram-edge anim-draw" style="--len:350"
      d="M 50,100 C 150,50 250,150 350,100" />

动画的适用场景(摘要 — 完整指导请参阅参考文件):

  • 路径绘制:展示连接的建立过程、网络拓扑、图表的构建过程
  • 依次淡入:逐步讲解概念、讲述一个故事的列表
  • 脉冲/光晕:突出流程中的当前步骤、吸引注意力到关键节点
  • 不要为纯装饰元素、每个项目符号,或观众需要立即阅读的内容添加动画
设计指南
  1. 每张幻灯片只讲一个要点 — 避免内容过于拥挤。幻灯片多一些也没关系。
  2. 使用大号文字 — 如果幻灯片内容较少,就放大文字。使用 font-size: 2.5vw 或更大。
  3. 优先使用图表,而非项目符号列表 — 讲解流程、架构或流转时,使用 SVG 图表,而不是项目符号列表。
  4. 保持间距一致 — 使用 vh/vw 单位设置内边距和外边距,让间距随视口缩放。
  5. 建立颜色层次 — 标题使用 --heading,正文使用 --fg,次要文字使用 --muted,强调内容使用 --accent。
  6. 代码块 — 使用 <pre><code>,并应用主题的 --code-bg/--code-fg。代码应简短(每张幻灯片少于 15 行)。
  7. 在幻灯片中使用 Python — 添加 Python 代码时,应确保代码可通过 uv run script.py 运行,从而独立运行,无需外部软件环境。在脚本顶部使用 PEP 723 内联脚本元数据(# /// script、requires-python、dependencies、# ///),并在幻灯片或演讲者备注中说明观众使用 uv run script.py 运行代码。
  8. 不使用外部素材 — 所有内容都应内联。需要图像时,使用内联 SVG 插图,而非栅格图像。
  9. 充实幻灯片 — 避免内容挤在一个角落、其余部分空空如也的布局。使用双栏网格(grid-template-columns: 1fr 1fr),让文字和图表并排平衡,并使用 align-items:center 使两栏内容垂直居中。
  10. 功能介绍用标语代替项目符号 — 突出介绍某项功能或概念时,用简短标语(分成 2–3 行短句,并以 <br> 换行)取代项目符号列表,并在下方添加关键词标签。这种方式更有冲击力,也更易于浏览。
  11. 让 SVG 成为视觉焦点 — 幻灯片包含图表时,图表至少应占幻灯片宽度的一半。使用宽裕的 viewBox 尺寸,并通过多个节点、标签、阶段、线框线条和回环箭头等细节填充空间。小图表孤零零地漂浮在大片留白中,会显得未完成。

Eric 的演示文稿原则(适用于所有演示文稿:HTML、PPTX、咨询风格;于 2026-09-01 委托制作 BCG 风格的咨询演示文稿技能时提出)

  1. 每张幻灯片只传达一个关键信息,并用行动式标题表达(结论 + 意义),绝不使用主题标签。
  2. 使用人人都能理解的通俗语言。避免高深的咨询术语(协同效应、杠杆、整体性)。
  3. 幻灯片应简洁清晰:留出充足的页边距和空白;如果内容太挤,就拆分幻灯片。
  4. 除非几何结构本身能够承载论点,否则绝不要把要点排进方框/网格/支柱结构(四宫格、2x2、六大支柱)。如果布局不能让信息更快传达,就使用段落或列表。
  5. 只保留一种强调色,用于突出决定性的对比;每张展示证据的幻灯片都要注明来源。
  6. 品牌演示文稿应使用品牌的官方颜色,并通过品牌自有网站的实时 CSS 核实(以 HTML 格式获取;获取路径见 webfetch-empty-use-agent-browser),绝不使用第三方聚合网站列出的颜色。BCG 调色板于 2026-09-01 根据 bcg.com 中 id=global-colors 的内联样式块核实:#144622、#0C2B15、#197A56、#21BF61、#96F878。

评论

支持 Markdown