分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管内导波超声检测机器人 · IFR理想解原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=Azeret+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
  --bg:#060a14;--panel:#0a1020;--border:rgba(0,229,255,0.12);
  --text:#b0bcd0;--dim:#3e4e66;--accent:#00e5ff;--accent2:#00ff9d;
  --piezo:#ff8f00;--cilia:#ffb74d;--crack:#ff1744;--metal:#222e42;
  --metal2:#2c3c55;--wall:#1c2840;--interior:#0b1220;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Azeret Mono',monospace;
  min-height:100vh;display:flex;justify-content:center;padding:16px 12px;
  background-image:radial-gradient(ellipse at 50% 0%,rgba(0,229,255,0.03) 0%,transparent 60%)}
.wrap{max-width:1160px;width:100%}
header{text-align:center;margin-bottom:18px}
header h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(18px,3vw,26px);
  color:#dfe6f0;letter-spacing:.5px;margin-bottom:4px}
header .sub{font-size:11px;color:var(--dim);letter-spacing:2.5px;text-transform:uppercase}
.views{display:flex;gap:14px;margin-bottom:16px}
.vp{background:var(--panel);border:1px solid var(--border);border-radius:10px;
  overflow:hidden;position:relative}
.vp .vl{position:absolute;top:8px;left:12px;font-size:10px;color:var(--accent);
  letter-spacing:1.5px;text-transform:uppercase;z-index:2;opacity:.7}
.sv{flex:2.2}.cv{flex:1}
.vp svg{display:block;width:100%;height:auto}
.ann{position:absolute;bottom:8px;right:10px;font-size:10px;color:var(--accent2);
  opacity:0;transition:opacity .5s;z-index:2;text-align:right;max-width:200px;
  line-height:1.6;letter-spacing:.3px}
.ann.vis{opacity:.85}
.tl{display:flex;align-items:center;justify-content:center;gap:2px;margin-bottom:14px;
  padding:10px 14px;background:var(--panel);border:1px solid var(--border);border-radius:8px;flex-wrap:wrap}
.pi{display:flex;align-items:center;gap:5px;padding:5px 10px;border-radius:5px;
  font-size:11px;color:var(--dim);transition:all .35s;white-space:nowrap}
.pi.act{color:var(--accent);background:rgba(0,229,255,.07)}
.pi .dt{width:7px;height:7px;border-radius:50%;background:var(--dim);transition:all .35s}
.pi.act .dt{background:var(--accent);box-shadow:0 0 8px var(--accent)}
.pa{color:var(--dim);font-size:12px;opacity:.35}
.ctrls{display:flex;align-items:center;justify-content:center;gap:16px;margin-bottom:12px;flex-wrap:wrap}
.cg{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--dim)}
.cg label{white-space:nowrap}
input[type=range]{-webkit-appearance:none;appearance:none;width:100px;height:3px;
  background:var(--metal);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;
  border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 6px rgba(0,229,255,.4)}
.btn{background:var(--metal);border:1px solid var(--border);color:var(--text);
  padding:5px 12px;border-radius:5px;font-family:'Azeret Mono',monospace;
  font-size:11px;cursor:pointer;transition:all .2s}
.btn:hover{background:var(--metal2);border-color:var(--accent)}
.params{display:flex;justify-content:center;gap:18px;font-size:10px;color:var(--dim);
  letter-spacing:.4px;flex-wrap:wrap}
.params span{opacity:.6}
.ifr-tag{display:inline-block;background:rgba(0,255,157,.08);color:var(--accent2);
  padding:2px 8px;border-radius:3px;font-size:10px;letter-spacing:1px;margin-top:8px}
@media(max-width:760px){.views{flex-direction:column}.sv,.cv{flex:1}}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>管内导波超声检测机器人</h1>
    <p class="sub">IFR 最终理想解 · 移动与检测机理解耦</p>
    <div class="ifr-tag">利用管壁自身作为导波介质 — 零额外扫描机构</div>
  </header>
  <div class="views">
    <div class="vp sv">
      <div class="vl">管道纵切面</div>
      <svg id="sSvg" viewBox="0 0 820 270"></svg>
      <div class="ann" id="sAnn"></div>
    </div>
    <div class="vp cv">
      <div class="vl">管道横截面</div>
      <svg id="cSvg" viewBox="0 0 340 340"></svg>
      <div class="ann" id="cAnn"></div>
    </div>
  </div>
  <div class="tl" id="tl"></div>
  <div class="ctrls">
    <div class="cg"><label>速度</label>
      <input type="range" id="spd" min="0.3" max="2.5" step="0.1" value="1">
      <span id="spdL">1.0x</span>
    </div>
    <button class="btn" id="repBtn">⟳ 重播</button>
    <button class="btn" id="paBtn">⏸ 暂停</button>
  </div>
  <div class="params">
    <span>导波频率 250 kHz</span><span>·</span>
    <span>压电谐振 45 kHz</span><span>·</span>
    <span>静摩擦系数 μ = 0.6</span>
  </div>
</div>

<script>
/* =============== 配置 =============== */
const PH = [
  { name:'压电推进', dur:3200, sAnn:'压电纤毛 45kHz 高频微振\n定向摩擦驱动 — 无腿滑行', cAnn:'' },
  { name:'制动定位', dur:500,  sAnn:'振动停止\n精确定位检测节点', cAnn:'' },
  { name:'激发导波', dur:2200, sAnn:'环形换能器激发\n250kHz 周向超声导波', cAnn:'导波从换能器出发\n沿管壁双向传播' },
  { name:'回波检测', dur:2600, sAnn:'导波遇裂纹反射\n同一换能器接收', cAnn:'瞬间 100% 周向覆盖\n裂纹反射信号被捕获' },
  { name:'继续推进', dur:400,  sAnn:'检测完成\n推进至下一节点', cAnn:'' },
];

/* =============== 状态 =============== */
let phase=0, pTime=0, speed=1, paused=false, lastT=null;
let robX=170, robStartX=170, robEndX=600;
let globalT=0; // 全局时间,用于各种动画

/* =============== 构建侧视图 SVG =============== */
const sSvg = document.getElementById('sSvg');
const cSvg = document.getElementById('cSvg');

function buildSide(){
  sSvg.innerHTML = `
  <defs>
    <linearGradient id="wG" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#141e30"/><stop offset="35%" stop-color="#243448"/>
      <stop offset="65%" stop-color="#243448"/><stop offset="100%" stop-color="#141e30"/>
    </linearGradient>
    <filter id="cG"><feGaussianBlur stdDeviation="5" result="b"/>
      <feFlood flood-color="#00e5ff" flood-opacity=".55" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <filter id="aG"><feGaussianBlur stdDeviation="3" result="b"/>
      <feFlood flood-color="#ff8f00" flood-opacity=".5" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <filter id="rG"><feGaussianBlur stdDeviation="5" result="b"/>
      <feFlood flood-color="#ff1744" flood-opacity=".6" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <filter id="gG"><feGaussianBlur stdDeviation="8" result="b"/>
      <feFlood flood-color="#00ff9d" flood-opacity=".4" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  </defs>
  <!-- 背景 -->
  <rect width="820" height="270" fill="#080e1a"/>
  <!-- 网格 -->
  <g opacity=".04" stroke="#00e5ff" stroke-width=".5">
    ${Array.from({length:21},(_,i)=>`<line x1="${i*40+10}" y1="0" x2="${i*40+10}" y2="270"/>`).join('')}
    ${Array.from({length:7},(_,i)=>`<line x1="0" y1="${i*40+15}" x2="820" y2="${i*40+15}"/>`).join('')}
  </g>
  <!-- 管道内部 -->
  <rect x="25" y="78" width="770" height="114" fill="#0b1220" rx="1"/>
  <!-- 管壁-上 -->
  <rect x="25" y="38" width="770" height="40" fill="url(#wG)" rx="1"/>
  <!-- 管壁-下 -->
  <rect x="25" y="192" width="770" height="40" fill="url(#wG)" rx="1"/>
  <!-- 内壁高光线 -->
  <line x1="25" y1="78" x2="795" y2="78" stroke="#3a506a" stroke-width=".6" opacity=".4"/>
  <line x1="25" y1="192" x2="795" y2="192" stroke="#3a506a" stroke-width=".6" opacity=".4"/>
  <!-- 裂纹 -->
  <g id="crkS">
    <path d="M620,38 L617,50 L622,58 L618,66 L623,74 L620,84" stroke="#ff1744" stroke-width="1.8" fill="none" opacity=".6"/>
    <path d="M620,38 L617,50 L622,58 L618,66 L623,74 L620,84" stroke="#ff1744" stroke-width="6" fill="none" opacity=".08" filter="url(#rG)"/>
  </g>
  <!-- 刻度 -->
  <g opacity=".15" fill="#4a6080" font-size="7" text-anchor="middle">
    <text x="200" y="248">200</text><text x="400" y="248">400</text><text x="600" y="248">600</text>
    <line x1="200" y1="235" x2="200" y2="240" stroke="#4a6080" stroke-width=".5"/>
    <line x1="400" y1="235" x2="400" y2="240" stroke="#4a6080" stroke-width=".5"/>
    <line x1="600" y1="235" x2="600" y2="240" stroke="#4a6080" stroke-width=".5"/>
  </g>
  <!-- 机器人组 -->
  <g id="robG">
    <!-- 缆线 -->
    <path id="cable" d="" stroke="#5a6e88" stroke-width="2.5" fill="none" stroke-linecap="round"/>
    <!-- 机体 -->
    <rect id="robB" x="0" y="120" width="48" height="28" rx="7" fill="#16202e" stroke="#3a5068" stroke-width="1"/>
    <!-- 机体细节 -->
    <rect x="6" y="126" width="12" height="4" rx="1" fill="#1e2e42" opacity=".8"/>
    <rect x="6" y="138" width="12" height="4" rx="1" fill="#1e2e42" opacity=".8"/>
    <!-- 压电足-上 -->
    <g id="ftT" transform="translate(0,0)">
      <rect x="4" y="0" width="24" height="9" rx="2" fill="#1e2c40" stroke="#3a5068" stroke-width=".5"/>
      <g id="clT" opacity=".8">
        <line x1="7"  y1="0" x2="7"  y2="-5" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="11" y1="0" x2="11" y2="-5" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="15" y1="0" x2="15" y2="-5" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="19" y1="0" x2="19" y2="-5" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="23" y1="0" x2="23" y2="-5" stroke="#ffb74d" stroke-width=".9"/>
      </g>
    </g>
    <!-- 压电足-下 -->
    <g id="ftB" transform="translate(0,0)">
      <rect x="4" y="0" width="24" height="9" rx="2" fill="#1e2c40" stroke="#3a5068" stroke-width=".5"/>
      <g id="clB" opacity=".8">
        <line x1="7"  y1="9" x2="7"  y2="14" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="11" y1="9" x2="11" y2="14" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="15" y1="9" x2="15" y2="14" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="19" y1="9" x2="19" y2="14" stroke="#ffb74d" stroke-width=".9"/>
        <line x1="23" y1="9" x2="23" y2="14" stroke="#ffb74d" stroke-width=".9"/>
      </g>
    </g>
    <!-- 换能器-上 -->
    <rect id="trT" x="0" y="0" width="10" height="12" rx="1.5" fill="#00e5ff" opacity=".25"/>
    <rect id="trGT" x="0" y="0" width="10" height="12" rx="2" fill="#00e5ff" opacity="0" filter="url(#cG)"/>
    <!-- 换能器-下 -->
    <rect id="trB" x="0" y="0" width="10" height="12" rx="1.5" fill="#00e5ff" opacity=".25"/>
    <rect id="trGB" x="0" y="0" width="10" height="12" rx="2" fill="#00e5ff" opacity="0" filter="url(#cG)"/>
  </g>
  <!-- 侧视图导波效果 -->
  <g id="sWave" opacity="0">
    <rect x="0" y="38" width="6" height="194" rx="3" fill="#00e5ff" opacity=".35"/>
    <rect x="0" y="38" width="6" height="194" rx="3" fill="#00e5ff" opacity=".12" filter="url(#cG)"/>
  </g>
  <!-- 检出标记 -->
  <g id="detMark" opacity="0">
    <circle cx="620" cy="55" r="10" fill="none" stroke="#ff1744" stroke-width="2"/>
    <line x1="614" y1="49" x2="626" y2="61" stroke="#ff1744" stroke-width="2"/>
    <line x1="626" y1="49" x2="614" y2="61" stroke="#ff1744" stroke-width="2"/>
  </g>
  <!-- 100%覆盖标签 -->
  <g id="covLabel" opacity="0">
    <rect x="570" y="96" width="100" height="22" rx="4" fill="rgba(0,255,157,.12)" stroke="#00ff9d" stroke-width=".8"/>
    <text x="620" y="111" text-anchor="middle" fill="#00ff9d" font-size="11" font-family="Syne,sans-serif" font-weight="700">100% 覆盖</text>
  </g>
  <!-- 标注:管壁 = 导波介质 -->
  <g id="resLabel" opacity="0">
    <text x="56" y="60" fill="#00ff9d" font-size="9" opacity=".7" font-family="'Azeret Mono',monospace">管壁 = 导波介质</text>
    <line x1="50" y1="64" x2="50" y2="76" stroke="#00ff9d" stroke-width=".5" opacity=".5" stroke-dasharray="2 2"/>
  </g>
  `;
}

function buildCross(){
  const cx=170,cy=170,or=128,ir=108;
  cSvg.innerHTML = `
  <defs>
    <linearGradient id="rG2" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#1a2638"/><stop offset="100%" stop-color="#243448"/>
    </linearGradient>
    <filter id="cG2"><feGaussianBlur stdDeviation="4" result="b"/>
      <feFlood flood-color="#00e5ff" flood-opacity=".5" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <filter id="rG2f"><feGaussianBlur stdDeviation="4" result="b"/>
      <feFlood flood-color="#ff1744" flood-opacity=".6" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <filter id="gG2"><feGaussianBlur stdDeviation="6" result="b"/>
      <feFlood flood-color="#00ff9d" flood-opacity=".35" result="c"/>
      <feComposite in="c" in2="b" operator="in" result="g"/>
      <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  </defs>
  <rect width="340" height="340" fill="#080e1a"/>
  <!-- 网格 -->
  <g opacity=".03" stroke="#00e5ff" stroke-width=".5">
    ${Array.from({length:9},(_,i)=>`<line x1="${i*40+10}" y1="0" x2="${i*40+10}" y2="340"/>`).join('')}
    ${Array.from({length:9},(_,i)=>`<line x1="0" y1="${i*40+10}" x2="340" y2="${i*40+10}"/>`).join('')}
  </g>
  <!-- 管壁环 -->
  <circle cx="${cx}" cy="${cy}" r="${or}" fill="none" stroke="url(#rG2)" stroke-width="20"/>
  <!-- 内壁线 -->
  <circle cx="${cx}" cy="${cy}" r="${ir}" fill="none" stroke="#3a506a" stroke-width=".5" opacity=".3"/>
  <!-- 内部空间 -->
  <circle cx="${cx}" cy="${cy}" r="${ir}" fill="#0b1220"/>
  <!-- 裂纹 -->
  <g id="crkC">
    <line id="crkCL" x1="${cx+ir*Math.cos(.95)}" y1="${cy-ir*Math.sin(.95)}" x2="${cx+(or+4)*Math.cos(.95)}" y2="${cy-(or+4)*Math.sin(.95)}" stroke="#ff1744" stroke-width="2.5" opacity=".7" stroke-linecap="round"/>
  </g>
  <!-- 换能器弧 -->
  <path id="transArc" d="" fill="#00e5ff" opacity=".3"/>
  <path id="transArcG" d="" fill="#00e5ff" opacity="0" filter="url(#cG2)"/>
  <!-- 压电足 (3组,120°间隔) -->
  <g id="feetC" opacity=".5">
    ${[90,210,330].map(a=>{
      const r=ir+10, x1=cx+r*Math.cos(a*Math.PI/180), y1=cy-r*Math.sin(a*Math.PI/180);
      return `<circle cx="${x1}" cy="${y1}" r="5" fill="#1e2c40" stroke="#3a5068" stroke-width=".5"/>`;
    }).join('')}
  </g>
  <!-- 缆线截面 -->
  <circle cx="${cx}" cy="${cy}" r="6" fill="#1a2638" stroke="#3a506a" stroke-width=".8"/>
  <circle cx="${cx}" cy="${cy}" r="2.5" fill="#5a6e88"/>
  <!-- 导波脉冲 - 顺时针 -->
  <circle id="wCW" cx="${cx}" cy="${cy}" r="${ir+10}" fill="none" stroke="#00e5ff" stroke-width="4" opacity="0" stroke-linecap="round"/>
  <!-- 导波脉冲 - 逆时针 -->
  <circle id="wCCW" cx="${cx}" cy="${cy}" r="${ir+10}" fill="none" stroke="#00e5ff" stroke-width="4" opacity="0" stroke-linecap="round"/>
  <!-- 反射波 -->
  <circle id="wRef" cx="${cx}" cy="${cy}" r="${ir+10}" fill="none" stroke="#ff1744" stroke-width="3" opacity="0" stroke-linecap="round"/>
  <!-- 检出闪烁 -->
  <circle id="detFlash" cx="${cx}" cy="${cy}" r="${ir+10}" fill="none" stroke="#00ff9d" stroke-width="2" opacity="0" filter="url(#gG2)"/>
  <!-- 角度标注 -->
  <g opacity=".12" fill="none" stroke="#4a6080" stroke-width=".3">
    ${[0,45,90,135,180,225,270,315].map(a=>{
      const x1=cx+(ir-5)*Math.cos(a*Math.PI/180), y1=cy-(ir-5)*Math.sin(a*Math.PI/180);
      const x2=cx+(or+8)*Math.cos(a*Math.PI/180), y2=cy-(or+8)*Math.sin(a*Math.PI/180);
      return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"/>`;
    }).join('')}
  </g>
  `;
}

buildSide();
buildCross();

/* =============== 获取元素引用 =============== */
const $=id=>document.getElementById(id);
const robG=$('robG'), cable=$('cable'), robB=$('robB');
const ftT=$('ftT'), ftB=$('ftB'), clT=$('clT'), clB=$('clB');
const trT=$('trT'), trB=$('trB'), trGT=$('trGT'), trGB=$('trGB');
const sWave=$('sWave'), detMark=$('detMark'), covLabel=$('covLabel'), resLabel=$('resLabel');
const crkS=$('crkS');
const wCW=$('wCW'), wCCW=$('wCCW'), wRef=$('wRef'), detFlash=$('detFlash');
const transArc=$('transArc'), transArcG=$('transArcG'), crkC=$('crkC');
const sAnn=$('sAnn'), cAnn=$('cAnn');

/* =============== 时间线UI =============== */
const tlEl=$('tl');
PH.forEach((p,i)=>{
  if(i>0){ const ar=document.createElement('span'); ar.className='pa'; ar.textContent='→'; tlEl.appendChild(ar); }
  const d=document.createElement('div'); d.className='pi'+(i===0?' act':''); d.id='ph'+i;
  d.innerHTML=`<span class="dt"></span>${p.name}`; tlEl.appendChild(d);
});

/* =============== 工具函数 =============== */
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}
function easeInOut(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
function arcD(cx,cy,r,a1,a2){
  const r1=a1*Math.PI/180, r2=a2*Math.PI/180;
  const x1=cx+r*Math.cos(r1),y1=cy-r*Math.sin(r1);
  const x2=cx+r*Math.cos(r2),y2=cy-r*Math.sin(r2);
  let sw=a2-a1; if(sw<0)sw+=360;
  const la=sw>180?1:0, sf=1;
  return `M${x1} ${y1}A${r} ${r} 0 ${la} ${sf} ${x2} ${y2}`;
}

/* =============== 侧视图渲染 =============== */
function renderSide(ph, prog){
  const pipeTop=78, pipeBot=192, midY=135;
  
  // 机器人位置
  let rx;
  if(ph===0) rx=lerp(robStartX,robEndX,easeInOut(prog));
  else rx=robEndX;
  
  robG.setAttribute('transform',`translate(${rx-24},0)`);
  
  // 缆线
  const waveAmp=4, waveFreq=0.04;
  let cp=`M 25 ${midY}`;
  for(let x=30;x<=rx-28;x+=4){
    const wy=midY+Math.sin(x*waveFreq+globalT*2)*waveAmp*(1-clamp((x-rx+80)/80,0,1));
    cp+=` L ${x} ${wy}`;
  }
  cable.setAttribute('d',cp);
  
  // 机体
  robB.setAttribute('x',rx-24);
  robB.setAttribute('y',midY-14);
  
  // 压电足
  ftT.setAttribute('transform',`translate(${rx-14},${pipeTop})`);
  ftB.setAttribute('transform',`translate(${rx-14},${pipeBot-9})`);
  
  // 纤毛动画
  const vibrating=(ph===0);
  const ciliaLen=5;
  const ciliaOff=vibrating?Math.sin(globalT*45)*3:0; // 高频振动
  [clT,clB].forEach(cl=>{
    const lines=cl.querySelectorAll('line');
    lines.forEach((ln,i)=>{
      const base=cl===clT?-ciliaLen:ciliaLen;
      const dir=cl===clT?-1:1;
      const off=vibrating?ciliaOff*Math.sin(i*1.3+globalT*30):0;
      const tilt=vibrating?Math.sin(globalT*45+i)*1.5:0; // 倾斜
      ln.setAttribute('y2',parseFloat(ln.getAttribute('y1'))+(base+off*dir));
      ln.setAttribute('x2',parseFloat(ln.getAttribute('x1'))+tilt);
      ln.setAttribute('stroke',vibrating?'#ffb74d':'#5a6a80');
      ln.setAttribute('opacity',vibrating?'.9':'.3');
    });
  });
  
  // 压电足发光
  [ftT,ftB].forEach(f=>{
    const rects=f.querySelectorAll('rect');
    rects[0].setAttribute('stroke',vibrating?'#ffb74d':'#3a5068');
    rects[0].setAttribute('stroke-width',vibrating?'1':'.5');
  });
  
  // 换能器位置
  const tx=rx+20;
  trT.setAttribute('x',tx); trT.setAttribute('y',pipeTop);
  trB.setAttribute('x',tx); trB.setAttribute('y',pipeBot-12);
  trGT.setAttribute('x',tx-2); trGT.setAttribute('y',pipeTop-2);
  trGT.setAttribute('width','14'); trGT.setAttribute('height','16');
  trGB.setAttribute('x',tx-2); trGB.setAttribute('y',pipeBot-14);
  trGB.setAttribute('width','14'); trGB.setAttribute('height','16');
  
  // 换能器发光
  const emitting=(ph===2||ph===3);
  const emGlow=emitting?.7:.0;
  const emOp=emitting?.6:.25;
  trGT.setAttribute('opacity',emGlow);
  trGB.setAttribute('opacity',emGlow);
  trT.setAttribute('opacity',emOp);
  trB.setAttribute('opacity',emOp);
  
  // 导波效果(侧视图中显示为竖直亮带)
  if(ph===2||ph===3){
    sWave.setAttribute('opacity',ph===2?clamp(prog*3,0,.8):clamp(1-prog,.0,.8));
    const wChild=sWave.children;
    for(let c of wChild) c.setAttribute('x',tx-3);
  } else {
    sWave.setAttribute('opacity','0');
  }
  
  // 裂纹高亮
  const crackActive=(ph===3 && prog>.3);
  crkS.querySelectorAll('path').forEach(p=>{
    p.setAttribute('opacity',crackActive?'1':'.6');
    if(crackActive) p.setAttribute('filter','url(#rG)');
    else p.removeAttribute('filter');
  });
  
  // 检出标记
  detMark.setAttribute('opacity',(ph===3&&prog>.5)?clamp((prog-.5)*4,0,1):'0');
  
  // 100%覆盖标签
  covLabel.setAttribute('opacity',(ph===3&&prog>.6)?clamp((prog-.6)*3,0,1):'0');
  
  // 资源利用标注(推进和检测切换时显示)
  resLabel.setAttribute('opacity',(ph===2)?clamp(prog*2,0,.8):'0');
}

/* =============== 横截面渲染 =============== */
const CCX=170, CCY=170, CR=118; // 波传播半径
const TRANS_ANGLE=270; // 换能器角度(底部,6点钟)
const CRACK_ANGLE=55;  // 裂纹角度(~2点钟)
const CIRC=2*Math.PI*CR;

function renderCross(ph, prog){
  // 换能器弧
  const tArcD=arcD(CCX,CCY,CR-8,TRANS_ANGLE-15,TRANS_ANGLE+15);
  transArc.setAttribute('d',tArcD);
  transArcG.setAttribute('d',tArcD);
  
  const emitting=(ph===2||ph===3);
  transArc.setAttribute('opacity',emitting?.5:.25);
  transArcG.setAttribute('opacity',emitting?.5:0);
  
  // 导波脉冲
  const pulseLen=30; // 脉冲弧长(度)
  const totalDegCW=360; // 顺时针传播总角度
  const crackDistCW=(CRACK_ANGLE-TRANS_ANGLE+360)%360; // 到裂纹的角度距离
  
  if(ph===2){
    // 发射阶段:两个脉冲从换能器出发,双向传播
    const waveProg=prog; // 0→1
    const cwAngle=waveProg*180; // 顺时针已传播角度
    const ccwAngle=waveProg*180; // 逆时针已传播角度
    
    // 顺时针脉冲
    const cwStart=TRANS_ANGLE-cwAngle;
    const cwEnd=TRANS_ANGLE-cwAngle+pulseLen;
    setWaveArc(wCW, cwStart, cwEnd, .8);
    
    // 逆时针脉冲
    const ccwStart=TRANS_ANGLE+ccwAngle-pulseLen;
    const ccwEnd=TRANS_ANGLE+ccwAngle;
    setWaveArc(wCCW, ccwStart, ccwEnd, .8);
    
    wRef.setAttribute('opacity','0');
    detFlash.setAttribute('opacity','0');
  } else if(ph===3){
    // 检测阶段:脉冲继续传播,顺时针到达裂纹后反射
    const waveProg=.5+prog*.5; // 从50%继续到100%
    const cwAngle=waveProg*360;
    const ccwAngle=waveProg*360;
    
    // 顺时针脉冲(继续前进)
    if(cwAngle<crackDistCW+pulseLen+40){
      const cwStart=TRANS_ANGLE-cwAngle;
      const cwEnd=TRANS_ANGLE-cwAngle+pulseLen;
      setWaveArc(wCW, cwStart, cwEnd, clamp(1-prog,.2,.8));
    } else {
      wCW.setAttribute('opacity','0');
    }
    
    // 逆时针脉冲
    const ccwStart=TRANS_ANGLE+ccwAngle-pulseLen;
    const ccwEnd=TRANS_ANGLE+ccwAngle;
    setWaveArc(wCCW, ccwStart, ccwEnd, clamp(1-prog,.1,.6));
    
    // 反射波(裂纹处产生)
    if(prog>.15){
      const refProg=(prog-.15)/.85;
      const refAngle=refProg*crackDistCW; // 反射波从裂纹回到换能器
      const refStart=CRACK_ANGLE+refAngle-pulseLen*.7;
      const refEnd=CRACK_ANGLE+refAngle;
      setWaveArc(wRef, refStart, refEnd, clamp(refProg*1.5,0,.9));
    } else {
      wRef.setAttribute('opacity','0');
    }
    
    // 裂纹闪烁
    const crkLine=crkC.querySelector('line');
    if(prog>.1&&prog<.6){
      crkLine.setAttribute('opacity','1');
      crkLine.setAttribute('stroke-width','3');
    } else {
      crkLine.setAttribute('opacity','.7');
      crkLine.setAttribute('stroke-width','2.5');
    }
    
    // 检出闪烁
    if(prog>.75){
      detFlash.setAttribute('opacity',clamp((prog-.75)*4,0,.6));
    } else {
      detFlash.setAttribute('opacity','0');
    }
  } else {
    wCW.setAttribute('opacity','0');
    wCCW.setAttribute('opacity','0');
    wRef.setAttribute('opacity','0');
    detFlash.setAttribute('opacity','0');
  }
  
  // 压电足可见性
  const feetC=$('feetC');
  feetC.querySelectorAll('circle').forEach(c=>{
    c.setAttribute('fill',ph===0?'#2a3850':'#1e2c40');
    c.setAttribute('stroke',ph===0?'#ffb74d':'#3a5068');
  });
}

function setWaveArc(el, startDeg, endDeg, opacity){
  // 将角度转为SVG弧
  const d=arcD(CCX,CCY,CR,startDeg,endDeg);
  // stroke-dasharray 方法不太适合,直接用弧路径
  // 创建弧形路径
  el.setAttribute('d',d);
  el.setAttribute('fill','none');
  el.setAttribute('stroke','#00e5ff');
  el.setAttribute('stroke-width','4');
  el.setAttribute('opacity',opacity);
  // 重新设置为circle+dash方式更简单
  // 实际上直接用path arc更好控制
}

// 重新实现:用circle + dashoffset
function setWaveDash(el, startAngleDeg, spanDeg, opacity, color){
  const circ=CIRC;
  const dashLen=spanDeg/360*circ;
  const gapLen=circ-dashLen;
  const offset=startAngleDeg/360*circ;
  el.setAttribute('stroke-dasharray',`${dashLen} ${gapLen}`);
  el.setAttribute('stroke-dashoffset',`${-offset}`);
  el.setAttribute('opacity',opacity);
  if(color) el.setAttribute('stroke',color);
}

/* =============== 重新构建横截面波动画方式 =============== */
// 用 circle + stroke-dasharray + stroke-dashoffset 更流畅
// 但需要从12点钟方向开始。SVG circle的dash从3点钟位置开始,顺时针。
// 换能器在底部(270°从3点钟顺时针=6点钟),裂纹在约55°(约2点钟)

// 角度约定:从3点钟方向(0°)顺时针
// 换能器位置:270° (6点钟)
// 裂纹位置:360-55=305°... 不对
// 让我重新定义:
// 在SVG中,circle的stroke-dasharray从右侧(3点钟)开始,顺时针方向绘制
// 我的换能器在底部=SVG的90°位置(从3点钟顺时针90°=6点钟)
// 裂纹在大约2点钟=SVG的300°位置(从3点钟顺时针300°)
// 不对...从3点钟顺时针:0°=3点,90°=6点,180°=9点,270°=12点,300°=1点,330°=2点
// 所以:换能器在90°位置,裂纹在330°位置

// 顺时针从换能器到裂纹:90°→180°→270°→330° = 240°
// 逆时针从换能器到裂纹:90°→0°→330° = 120°

const T_SVG=90;  // 换能器在SVG角度(3点钟为0°,顺时针)
const C_SVG=330; // 裂纹在SVG角度
const distCW=(C_SVG-T_SVG+360)%360; // 顺时针距离=240°
const distCCW=(T_SVG-C_SVG+360)%360; // 逆时针距离=120°

function renderCrossV2(ph, prog){
  const emitting=(ph===2||ph===3);
  
  // 换能器弧(底部)
  const tArcD=arcD(CCX,CCY,CR-8,T_SVG-12+90,T_SVG+12+90);
  // 不对,arcD用的是数学角度(0°=右,逆时针)
  // 我需要统一角度约定...让我直接用SVG角度
  
  // 简化:直接用坐标计算换能器弧的位置
  const tR=CR-8;
  const tAngles=[-15,-10,-5,0,5,10,15]; // 相对于换能器中心的偏移
  const tCx=CCX+tR*Math.cos((T_SVG)*Math.PI/180);
  const tCy=CCY+tR*Math.sin((T_SVG)*Math.PI/180);
  
  // 用小矩形表示换能器
  transArc.setAttribute('d','');
  transArcG.setAttribute('d','');
  
  // 改用直接定位的元素
  // 换能器直接用circle上的dash表示
  const transDash=30/360*CIRC; // 换能器弧长
  const transOff=T_SVG/360*CIRC;
  transArc.setAttribute('stroke-dasharray',`${transDash} ${CIRC-transDash}`);
  transArc.setAttribute('stroke-dashoffset',`${-transOff}`);
  transArc.setAttribute('r',CR-8);
  transArc.setAttribute('fill','none');
  transArc.setAttribute('stroke','#00e5ff');
  transArc.setAttribute('stroke-width','8');
  transArc.setAttribute('stroke-linecap','round');
  transArc.setAttribute('opacity',emitting?.5:.2);
  
  transArcG.setAttribute('stroke-dasharray',`${transDash} ${CIRC-transDash}`);
  transArcG.setAttribute('stroke-dashoffset',`${-transOff}`);
  transArcG.setAttribute('r',CR-8);
  transArcG.setAttribute('fill','none');
  transArcG.setAttribute('stroke','#00e5ff');
  transArcG.setAttribute('stroke-width','12');
  transArcG.setAttribute('stroke-linecap','round');
  transArcG.setAttribute('opacity',emitting?.5:0);
  
  const pulseSpan=25; // 脉冲跨角度
  
  if(ph===2){
    const p=prog;
    // 顺时针脉冲
    const cwPos=T_SVG+p*180; // 当前前端角度
    setWaveDash(wCW, cwPos-pulseSpan, pulseSpan, .85, '#00e5ff');
    // 逆时针脉冲
    const ccwPos=T_SVG-p*180;
    setWaveDash(wCCW, ccwPos, pulseSpan, .85, '#00e5ff');
    // 隐藏反射波
    wRef.setAttribute('opacity','0');
    detFlash.setAttribute('opacity','0');
  } else if(ph===3){
    const p=prog;
    // 顺时针继续到裂纹
    const cwTotal=distCW+40; // 稍微过一点
    const cwAngle=T_SVG+(.5+p*.5)*cwTotal;
    if(cwAngle<T_SVG+distCW+30){
      setWaveDash(wCW, cwAngle-pulseSpan, pulseSpan, clamp(1-p,.15,.7), '#00e5ff');
    } else {
      wCW.setAttribute('opacity','0');
    }
    // 逆时针继续
    const ccwTotal=distCCW+40;
    const ccwAngle=T_SVG-(.5+p*.5)*ccwTotal;
    setWaveDash(wCCW, ccwAngle, pulseSpan, clamp(1-p,.1,.5), '#00e5ff');
    
    // 反射波
    if(p>.12){
      const rp=(p-.12)/.88;
      const refPos=C_SVG-rp*distCCW; // 从裂纹逆时针回到换能器
      setWaveDash(wRef, refPos, pulseSpan*.7, clamp(rp*1.8,0,.9), '#ff6e40');
    } else {
      wRef.setAttribute('opacity','0');
    }
    
    // 裂纹闪烁
    const crkLine=crkC.querySelector('line');
    const flash=p>.08&&p<.5?Math.sin(p*30)*.3+.7:.7;
    crkLine.setAttribute('opacity',flash);
    
    // 检出闪烁
    detFlash.setAttribute('opacity',p>.7?clamp((p-.7)*4,0,.5):'0');
    
  } else {
    wCW.setAttribute('opacity','0');
    wCCW.setAttribute('opacity','0');
    wRef.setAttribute('opacity','0');
    detFlash.setAttribute('opacity','0');
  }
  
  // 压电足颜色
  $('feetC').querySelectorAll('circle').forEach(c=>{
    c.setAttribute('stroke',ph===0?'#ffb74d':'#3a5068');
  });
}

/* =============== 注释更新 =============== */
function updateAnns(ph){
  sAnn.textContent=PH[ph].sAnn;
  sAnn.classList.toggle('vis',!!PH[ph].sAnn);
  cAnn.textContent=PH[ph].cAnn;
  cAnn.classList.toggle('vis',!!PH[ph].cAnn);
}

/* =============== 时间线更新 =============== */
function updateTL(ph){
  PH.forEach((_,i)=>{
    const el=$('ph'+i);
    el.classList.toggle('act',i===ph);
  });
}

/* =============== 主动画循环 =============== */
function tick(ts){
  if(!lastT) lastT=ts;
  const dt=ts-lastT;
  lastT=ts;
  
  if(!paused){
    globalT+=dt/1000*speed;
    pTime+=dt*speed;
    
    const dur=PH[phase].dur;
    if(pTime>=dur){
      pTime-=dur;
      phase=(phase+1)%5;
      if(phase===0){
        // 新循环
        robStartX=170;
        robEndX=600;
      }
      updateTL(phase);
      updateAnns(phase);
    }
    
    const prog=clamp(pTime/PH[phase].dur,0,1);
    renderSide(phase,prog);
    renderCrossV2(phase,prog);
  }
  
  requestAnimationFrame(tick);
}

/* =============== 交互 =============== */
const spdSlider=$('spd'), spdL=$('spdL');
spdSlider.addEventListener('input',()=>{
  speed=parseFloat(spdSlider.value);
  spdL.textContent=speed.toFixed(1)+'x';
});

$('repBtn').addEventListener('click',()=>{
  phase=0;pTime=0;globalT=0;lastT=null;paused=false;
  robStartX=170;robEndX=600;
  $('paBtn').textContent='⏸ 暂停';
  updateTL(0);updateAnns(0);
});

$('paBtn').addEventListener('click',()=>{
  paused=!paused;
  $('paBtn').textContent=paused?'▶ 播放':'⏸ 暂停';
  if(!paused) lastT=null;
});

/* =============== 启动 =============== */
updateTL(0);
updateAnns(0);
requestAnimationFrame(tick);
</script>
</body>
</html>

这个动画实现了以下关键设计:

IFR 理想解可视化

  • 资源利用:标注"管壁 = 导波介质",直观展示方案如何利用管道自身作为波传播介质,零额外扫描机构
  • 解耦展示:压电推进阶段(琥珀色纤毛振动)与导波检测阶段(青色波传播)用颜色和时序明确分离,突出"移动与检测机理解耦"的核心创新
  • 100% 覆盖:检测完成时显示"100% 覆盖"标签,强调瞬间全周向扫描的理想结果

双视图协同

  • 纵切面:展示机器人推进、纤毛高频振动、换能器激发、裂纹检出完整时序
  • 横截面:展示导波从环形换能器双向传播、遇裂纹反射、回波被捕获的核心物理过程

五阶段自动循环
压电推进 → 制动定位 → 激发导波 → 回波检测 → 继续推进,每阶段配有中文注释说明机理

交互控制

  • 速度滑块(0.3x ~ 2.5x)调节动画节奏
  • 重播/暂停按钮控制播放状态
  • 页面加载即自动播放,iframe 重载也会自动重启
积分规则:第一轮对话扣减8分,后续每轮扣6分