Zhang Xin's Tech Blog

Back

文章也可以是一块可以操作的实验台。在下面的预览中,HTML 负责结构、CSS 负责样式,JavaScript 让波形随时间运动。它是正在运行的网页,可以直接操作。

波的叠加:动起来的 HTML#

拖动「相位差」,观察两列波的叠加:0° 时相互增强,180° 时相互抵消。还可以调整速度、暂停动画、重新运行,或全屏查看。三个波形使用相同的振幅标尺。

波的叠加 · 交互实验HTML · 交互预览
独立打开 ↗ 下载 HTML

查看完整 HTML 源码
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>波的叠加 · HTML 动效示例</title>
  <style>
    :root{color-scheme:light dark;--ink:#263b48;--muted:#75848d;--accent:#527e94;--border:#dfe5e9;--paper:#fcfcfd;--plot:#f8fafb;--button:#fff;--grid:#dfe5e9;--axis:#d7e0e5;--wave-a:#8ba8b9;--wave-b:#c29981;--on-accent:#fff}
    @media(prefers-color-scheme:dark){:root{--ink:#e4eaf0;--muted:#94a3b0;--accent:#b4edfc;--border:#30343b;--paper:#101318;--plot:#151a21;--button:#1c232d;--grid:#2a343e;--axis:#465362;--wave-a:#7a9bb1;--wave-b:#d2a389;--on-accent:#152830}}
    *{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif}
    main{max-width:1000px;margin:auto;padding:24px}header{display:flex;align-items:center;justify-content:space-between;gap:14px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted)}h1{font-size:25px;line-height:1.3;font-weight:600;margin:7px 0}header p{font-size:12px;color:var(--muted);margin:0}.badge{font-size:10px;color:var(--accent);white-space:nowrap;border:1px solid var(--border);padding:5px 9px;border-radius:20px}
    .plot{margin:20px 0 12px;border:1px solid var(--border);border-radius:12px;overflow:hidden;background:var(--plot);position:relative}canvas{display:block;width:100%;height:245px}.legend{display:flex;gap:18px;flex-wrap:wrap;font-size:11px;color:var(--muted);margin-bottom:18px}.legend span:before{content:"";display:inline-block;width:14px;height:3px;background:var(--color);margin-right:6px;vertical-align:middle;border-radius:2px}
    .controls{display:grid;grid-template-columns:1fr 1fr;gap:18px}label{font-size:12px;display:block}label>span{display:flex;justify-content:space-between;gap:8px}output{font-variant-numeric:tabular-nums;color:var(--accent)}input{width:100%;accent-color:var(--accent);margin:10px 0 0;cursor:pointer}.footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:18px}button{font:inherit;font-size:12px;border:1px solid var(--border);border-radius:7px;padding:7px 13px;background:var(--button);color:var(--ink);cursor:pointer}button.primary{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}button:hover{filter:brightness(.95)}button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.hint{font-size:10px;color:var(--muted)}
    @media(max-width:500px){main{padding:18px}h1{font-size:22px}.badge{display:none}canvas{height:220px}.controls{gap:14px}.footer{align-items:flex-start}.hint{max-width:110px;text-align:right}}
  </style>
</head>
<body>
  <main>
    <header><div><div class="eyebrow">PLAYGROUND / 001</div><h1>当两列波相遇</h1><p>拖动相位,观察波形如何相互增强或抵消。</p></div><span class="badge">HTML + CSS + JavaScript</span></header>
    <div class="plot"><canvas id="wave" role="img" aria-label="两列正弦波及其叠加波的实时动画"></canvas></div>
    <div class="legend"><span style="--color:var(--wave-a)">波 A</span><span style="--color:var(--wave-b)">波 B</span><span style="--color:var(--accent)">叠加结果 A + B</span></div>
    <div class="controls">
      <label for="phase"><span>相位差 <output id="phase-value" for="phase">90°</output></span><input id="phase" type="range" min="0" max="360" value="90" step="1"></label>
      <label for="speed"><span>播放速度 <output id="speed-value" for="speed">1.0×</output></span><input id="speed" type="range" min="0.2" max="2" value="1" step="0.1"></label>
    </div>
    <div class="footer"><div><button class="primary" id="pause" type="button" aria-pressed="false">暂停动画</button> <button id="reset" type="button">重置</button></div><span class="hint">同相增强 · 反相抵消</span></div>
  </main>
  <script>
    const canvas = document.querySelector('#wave');
    const ctx = canvas.getContext('2d');
    const phase = document.querySelector('#phase');
    const speed = document.querySelector('#speed');
    const pause = document.querySelector('#pause');
    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
    let running = !reducedMotion.matches;
    let time = 0, last = 0, width = 0, height = 0, animation = 0;
    function updateLabels() {
      document.querySelector('#phase-value').textContent = phase.value + '°';
      document.querySelector('#speed-value').textContent = Number(speed.value).toFixed(1) + '×';
      pause.textContent = running ? '暂停动画' : '播放动画';
      pause.setAttribute('aria-pressed', String(!running));
    }
    function draw() {
      const style = getComputedStyle(canvas);
      const color = name => style.getPropertyValue(name).trim();
      ctx.clearRect(0, 0, width, height);
      const mid = height / 2, amplitude = height * .16;
      ctx.fillStyle = color('--grid');
      for (let x = 16; x < width; x += 24) for (let y = 14; y < height; y += 24) {
        ctx.beginPath(); ctx.arc(x, y, .8, 0, Math.PI * 2); ctx.fill();
      }
      ctx.strokeStyle = color('--axis'); ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(0, mid); ctx.lineTo(width, mid); ctx.stroke();
      const offset = Number(phase.value) * Math.PI / 180;
      const waves = [
        {color:color('--wave-a'), line:1.5, sample:x => Math.sin(x)},
        {color:color('--wave-b'), line:1.5, sample:x => Math.sin(x + offset)},
        {color:color('--accent'), line:3, sample:x => Math.sin(x) + Math.sin(x + offset)}
      ];
      for (const wave of waves) {
        ctx.beginPath(); ctx.strokeStyle = wave.color; ctx.lineWidth = wave.line;
        for (let x = 0; x <= width; x += 2) {
          const angle = x / Math.max(1, width) * Math.PI * 5 - time;
          const y = mid - wave.sample(angle) * amplitude;
          if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.stroke();
      }
    }
    function resize() {
      const rect = canvas.getBoundingClientRect();
      width = rect.width; height = rect.height;
      const ratio = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0); draw();
    }
    function tick(now) {
      animation = 0;
      if (!running || document.hidden) return;
      if (last) time += Math.min((now - last) / 1000, .05) * Number(speed.value) * 1.8;
      last = now; draw(); animation = requestAnimationFrame(tick);
    }
    function schedule() {
      cancelAnimationFrame(animation); animation = 0; last = 0;
      if (running && !document.hidden) animation = requestAnimationFrame(tick);
    }
    pause.addEventListener('click', () => { running = !running; updateLabels(); schedule(); });
    phase.addEventListener('input', () => { updateLabels(); draw(); });
    speed.addEventListener('input', updateLabels);
    document.querySelector('#reset').addEventListener('click', () => {
      phase.value = '90'; speed.value = '1'; time = 0;
      running = !reducedMotion.matches; updateLabels(); draw(); schedule();
    });
    document.addEventListener('visibilitychange', schedule);
    colorScheme.addEventListener('change', draw);
    reducedMotion.addEventListener('change', () => { running = !reducedMotion.matches; updateLabels(); schedule(); });
    new ResizeObserver(resize).observe(canvas);
    updateLabels(); resize(); schedule();
  </script>
</body>
</html>

如何在自己的文章中插入 HTML#

把包含 HTML、CSS 和 JavaScript 的完整页面保存为 public/html/my-demo.html。将文章后缀设为 .mdx,在正文中导入预览组件并指定文件路径:

import HtmlPreview from '@/components/materials/HtmlPreview.astro'

<HtmlPreview
  src='/html/my-demo.html'
  title='我的交互可视化'
  height={580}
/>
mdx

同一篇文章可以插入多个预览。height 设置预览高度;窄屏会自动适应文章宽度,HTML 页面内部也应使用响应式布局。

上面的「下载 HTML」可以获得完整示例。修改其中的样式、绘图逻辑和控件,再保存到 public/html/,即可作为新演示嵌入文章。完整代码也可以通过「查看完整 HTML 源码」展开阅读。修改文章或 HTML 文件后,按网站原有流程在本地构建并发布。

适合展示什么#

  • CSS 动画、交互图表、粒子运动、数学曲线及算法过程。
  • Canvas 或 SVG 可视化,以及通过滑块、按钮控制的实验。
  • 已有的独立 HTML 页面;将样式和脚本放在页面内即可像本例一样运行。

预览在独立框架中运行,样式不会影响文章。脚本可用于动画和交互,框架与博客的登录信息、本地设置相隔离。需要读取文件的演示可以提供文件选择按钮;依赖 fetch 或 ES 模块的外部服务需要允许跨域访问。若希望整页浏览,可使用「独立打开」。

HTML在线预览测试
https://blog.zxmath.top/blog/html-preview-demo
Author Zhang Xin
Published at 2026年10月17日