独立渲染引擎就绪引擎就绪
<!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分
等待动画代码生成...
