分享图
动画工坊
引擎就绪
<!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 href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
  --bg: #04080f;
  --bio: #00f0d4;
  --neural: #3388ff;
  --intent: #f5c842;
  --execute: #00ff88;
  --voice: #ff5577;
  --text: #b8c8da;
  --dim: #14203a;
  --panel: #0a1424;
}
*{margin:0;padding:0;box-sizing:border-box;}
body{
  background:var(--bg);
  color:var(--text);
  font-family:'Rajdhani',sans-serif;
  min-height:100vh;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  overflow:hidden;
  position:relative;
}
body::before{
  content:'';
  position:fixed;inset:0;
  background:
    radial-gradient(ellipse 60% 50% at 25% 40%, rgba(0,240,212,0.04) 0%, transparent 70%),
    radial-gradient(ellipse 50% 50% at 72% 35%, rgba(51,136,255,0.04) 0%, transparent 70%),
    radial-gradient(ellipse 40% 40% at 50% 80%, rgba(245,200,66,0.03) 0%, transparent 60%);
  pointer-events:none;
  z-index:0;
}
.container{
  width:96vw;max-width:1500px;
  position:relative;z-index:1;
}
.svg-wrap{width:100%;position:relative;}
.svg-wrap svg{width:100%;height:auto;display:block;}

/* 控制面板 */
.controls{
  position:fixed;bottom:20px;left:50%;transform:translateX(-50%);
  display:flex;align-items:center;gap:18px;
  background:rgba(10,20,36,0.92);
  backdrop-filter:blur(14px);
  border:1px solid var(--dim);
  border-radius:14px;
  padding:10px 28px;
  z-index:20;
  font-family:'IBM Plex Mono',monospace;
}
.controls label{font-size:12px;color:var(--bio);white-space:nowrap;letter-spacing:0.5px;}
.controls input[type=range]{
  -webkit-appearance:none;width:160px;height:3px;
  background:var(--dim);border-radius:2px;outline:none;
}
.controls input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;border-radius:50%;
  background:var(--bio);cursor:pointer;box-shadow:0 0 10px var(--bio);
}
.threshold-val{font-size:14px;font-weight:500;color:var(--intent);min-width:38px;text-align:right;}
.ctrl-btn{
  background:transparent;border:1px solid var(--dim);
  color:var(--text);font-family:'Rajdhani',sans-serif;
  font-size:13px;padding:5px 16px;border-radius:8px;
  cursor:pointer;transition:all .3s;letter-spacing:0.5px;
}
.ctrl-btn:hover{border-color:var(--bio);color:var(--bio);}
.ctrl-btn.active{border-color:var(--execute);color:var(--execute);}

/* 阶段指示器 */
.phase-bar{
  position:fixed;top:16px;left:50%;transform:translateX(-50%);
  display:flex;gap:6px;z-index:20;
}
.phase-dot{
  width:8px;height:8px;border-radius:50%;
  background:var(--dim);transition:all .4s;
}
.phase-dot.active{background:var(--bio);box-shadow:0 0 8px var(--bio);}
.phase-dot.done{background:rgba(0,240,212,0.3);}

/* SVG 内动画 */
@keyframes pulseGlow{
  0%,100%{opacity:0.3;r:6;}
  50%{opacity:0.9;r:10;}
}
@keyframes ringPulse{
  0%{r:10;opacity:0.7;stroke-width:2;}
  100%{r:35;opacity:0;stroke-width:0.5;}
}
@keyframes engineSpin{
  to{transform:rotate(360deg);}
}
@keyframes engineSpinR{
  to{transform:rotate(-360deg);}
}
@keyframes coreBreath{
  0%,100%{r:22;opacity:0.6;}
  50%{r:28;opacity:1;}
}
@keyframes dashFlow{
  to{stroke-dashoffset:-24;}
}
@keyframes fadeUp{
  from{opacity:0;transform:translateY(8px);}
  to{opacity:1;transform:translateY(0);}
}
@keyframes execFlash{
  0%{r:30;opacity:0.9;}
  60%{r:80;opacity:0.4;}
  100%{r:120;opacity:0;}
}
@keyframes voiceWave{
  0%,100%{transform:scaleY(0.3);}
  50%{transform:scaleY(1);}
}
@keyframes timelineGrow{
  from{width:0;}
}
@keyframes particleTrail{
  0%{opacity:0.9;r:3;}
  100%{opacity:0;r:1;}
}

@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:0.01ms!important;
    transition-duration:0.01ms!important;
  }
}
</style>
</head>
<body>

<!-- 阶段指示器 -->
<div class="phase-bar" id="phaseBar">
  <div class="phase-dot" data-idx="0"></div>
  <div class="phase-dot" data-idx="1"></div>
  <div class="phase-dot" data-idx="2"></div>
  <div class="phase-dot" data-idx="3"></div>
  <div class="phase-dot" data-idx="4"></div>
</div>

<div class="container">
<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 1400 800" xmlns="http://www.w3.org/2000/svg">
<defs>
  <!-- 发光滤镜 -->
  <filter id="glowBio" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="4" result="b"/>
    <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="glowNeural" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="6" result="b"/>
    <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="glowIntent" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="5" result="b"/>
    <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="glowExec" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="8" result="b"/>
    <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="12" result="b"/>
    <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>

  <!-- 渐变 -->
  <radialGradient id="engineAura" cx="50%" cy="50%" r="50%">
    <stop offset="0%" stop-color="#3388ff" stop-opacity="0.15"/>
    <stop offset="100%" stop-color="#3388ff" stop-opacity="0"/>
  </radialGradient>
  <radialGradient id="execAura" cx="50%" cy="50%" r="50%">
    <stop offset="0%" stop-color="#00ff88" stop-opacity="0.2"/>
    <stop offset="100%" stop-color="#00ff88" stop-opacity="0"/>
  </radialGradient>
  <linearGradient id="timelineTradGrad" x1="0" y1="0" x2="1" y2="0">
    <stop offset="0%" stop-color="#ff5577" stop-opacity="0.8"/>
    <stop offset="100%" stop-color="#ff5577" stop-opacity="0.3"/>
  </linearGradient>
  <linearGradient id="timelineIFRGrad" x1="0" y1="0" x2="1" y2="0">
    <stop offset="0%" stop-color="#00f0d4" stop-opacity="0.8"/>
    <stop offset="100%" stop-color="#00ff88" stop-opacity="0.3"/>
  </linearGradient>

  <!-- 信号路径(不可见,用于粒子运动) -->
  <path id="sigPath1" d="M 260 278 Q 420 220 580 270" fill="none" stroke="none"/>
  <path id="sigPath2" d="M 310 400 Q 440 340 580 290" fill="none" stroke="none"/>
</defs>

<!-- ========== 背景层 ========== -->
<g id="bgLayer">
  <!-- 网格 -->
  <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
    <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#0e1a30" stroke-width="0.5"/>
  </pattern>
  <rect width="1400" height="800" fill="url(#grid)" opacity="0.5"/>

  <!-- 扫描线装饰 -->
  <line x1="0" y1="400" x2="1400" y2="400" stroke="#0e1a30" stroke-width="0.5" stroke-dasharray="4 8"/>
</g>

<!-- ========== 人形图层 ========== -->
<g id="humanLayer" opacity="0">
  <!-- 人形轮廓(侧面,面向右) -->
  <g transform="translate(60, 80)">
    <!-- 头部 -->
    <ellipse cx="155" cy="95" rx="52" ry="62" fill="none" stroke="#1e3456" stroke-width="1.5"/>
    <!-- 面部特征线 -->
    <path d="M 195 75 Q 210 85 207 100" fill="none" stroke="#1e3456" stroke-width="1" opacity="0.6"/>
    <path d="M 200 105 L 195 115" fill="none" stroke="#1e3456" stroke-width="1" opacity="0.5"/>
    <!-- 颈部 -->
    <path d="M 140 155 L 138 180 M 168 155 L 170 180" fill="none" stroke="#1e3456" stroke-width="1.5"/>
    <!-- 肩膀和躯干 -->
    <path d="M 138 180 Q 80 190 60 230 L 55 370 Q 55 385 70 385 L 130 385 Q 140 385 140 375 L 142 280 L 145 375 Q 146 385 155 385 L 215 385 Q 228 385 225 370 L 210 230 Q 200 195 170 180"
      fill="none" stroke="#1e3456" stroke-width="1.5"/>
    <!-- 右臂(伸向设备) -->
    <path d="M 210 230 Q 240 250 270 280 Q 290 300 300 310"
      fill="none" stroke="#1e3456" stroke-width="1.5" stroke-linecap="round"/>
    <!-- 左臂 -->
    <path d="M 60 230 Q 40 260 35 310 Q 33 340 40 360"
      fill="none" stroke="#1e3456" stroke-width="1.5" stroke-linecap="round"/>

    <!-- 设备(手持控制器/耳机) -->
    <rect x="285" y="295" width="55" height="35" rx="6" fill="none" stroke="#1e3456" stroke-width="1.5"/>
    <rect x="290" y="300" width="18" height="25" rx="3" fill="none" stroke="#1e3456" stroke-width="0.8" opacity="0.5"/>

    <!-- 内部神经/肌肉信号路径 -->
    <path d="M 170 130 Q 175 140 172 155" fill="none" stroke="rgba(0,240,212,0.15)" stroke-width="1" stroke-dasharray="2 3"/>
    <path d="M 172 155 Q 185 190 210 225" fill="none" stroke="rgba(0,240,212,0.15)" stroke-width="1" stroke-dasharray="2 3"/>
    <path d="M 210 225 Q 240 250 270 275" fill="none" stroke="rgba(0,240,212,0.15)" stroke-width="1" stroke-dasharray="2 3"/>
  </g>

  <!-- 传感器节点 - 喉部 -->
  <g id="sensorThroat" transform="translate(230, 238)">
    <circle r="6" fill="var(--bio)" opacity="0.8" filter="url(#glowBio)">
      <animate attributeName="r" values="5;8;5" dur="1.5s" repeatCount="indefinite"/>
      <animate attributeName="opacity" values="0.6;1;0.6" dur="1.5s" repeatCount="indefinite"/>
    </circle>
    <circle r="10" fill="none" stroke="var(--bio)" stroke-width="1" opacity="0">
      <animate attributeName="r" values="8;28;8" dur="2s" repeatCount="indefinite"/>
      <animate attributeName="opacity" values="0.5;0;0.5" dur="2s" repeatCount="indefinite"/>
    </circle>
  </g>

  <!-- 传感器节点 - 手部 -->
  <g id="sensorHand" transform="translate(358, 360)">
    <circle r="5" fill="var(--bio)" opacity="0.7" filter="url(#glowBio)">
      <animate attributeName="r" values="4;7;4" dur="1.8s" repeatCount="indefinite"/>
      <animate attributeName="opacity" values="0.5;0.9;0.5" dur="1.8s" repeatCount="indefinite"/>
    </circle>
    <circle r="9" fill="none" stroke="var(--bio)" stroke-width="1" opacity="0">
      <animate attributeName="r" values="7;24;7" dur="2.2s" repeatCount="indefinite"/>
      <animate attributeName="opacity" values="0.4;0;0.4" dur="2.2s" repeatCount="indefinite"/>
    </circle>
  </g>

  <!-- 传感器标签 -->
  <text x="195" y="225" fill="var(--bio)" font-family="IBM Plex Mono" font-size="9" opacity="0.7">sEMG 1000Hz</text>
  <text x="330" y="348" fill="var(--bio)" font-family="IBM Plex Mono" font-size="9" opacity="0.7">μ-Radar</text>
</g>

<!-- ========== 信号流动层 ========== -->
<g id="signalLayer" opacity="0">
  <!-- 可见信号路径线 -->
  <path d="M 260 278 Q 420 220 580 270" fill="none" stroke="var(--bio)" stroke-width="1.5"
    stroke-dasharray="8 4" opacity="0.5" filter="url(#glowBio)">
    <animate attributeName="stroke-dashoffset" from="0" to="-24" dur="0.8s" repeatCount="indefinite"/>
  </path>
  <path d="M 310 400 Q 440 340 580 290" fill="none" stroke="var(--bio)" stroke-width="1.2"
    stroke-dasharray="6 5" opacity="0.4" filter="url(#glowBio)">
    <animate attributeName="stroke-dashoffset" from="0" to="-22" dur="1s" repeatCount="indefinite"/>
  </path>

  <!-- 信号波形装饰 - 喉部 -->
  <g transform="translate(248, 260)" opacity="0.6">
    <path d="M 0 0 Q 5 -8 10 0 Q 15 8 20 0 Q 25 -6 30 0 Q 35 5 40 0" fill="none" stroke="var(--bio)" stroke-width="1">
      <animate attributeName="d"
        values="M 0 0 Q 5 -8 10 0 Q 15 8 20 0 Q 25 -6 30 0 Q 35 5 40 0;
                M 0 0 Q 5 6 10 0 Q 15 -7 20 0 Q 25 8 30 0 Q 35 -5 40 0;
                M 0 0 Q 5 -8 10 0 Q 15 8 20 0 Q 25 -6 30 0 Q 35 5 40 0"
        dur="0.6s" repeatCount="indefinite"/>
    </path>
  </g>
</g>

<!-- ========== 粒子层 ========== -->
<g id="particleLayer"></g>

<!-- ========== 意图引擎层 ========== -->
<g id="engineLayer" opacity="0">
  <!-- 引擎光晕 -->
  <circle cx="700" cy="280" r="120" fill="url(#engineAura)"/>

  <!-- 外环 - 旋转 -->
  <g transform="translate(700,280)">
    <circle r="90" fill="none" stroke="#1a3060" stroke-width="1" stroke-dasharray="3 6">
      <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="20s" repeatCount="indefinite"/>
    </circle>
    <!-- 六边形外框 -->
    <polygon points="0,-78 67.5,-39 67.5,39 0,78 -67.5,39 -67.5,-39"
      fill="none" stroke="var(--neural)" stroke-width="1.2" opacity="0.5">
      <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="15s" repeatCount="indefinite"/>
    </polygon>
    <!-- 内六边形 -->
    <polygon points="0,-55 47.6,-27.5 47.6,27.5 0,55 -47.6,27.5 -47.6,-27.5"
      fill="none" stroke="var(--neural)" stroke-width="0.8" opacity="0.3">
      <animateTransform attributeName="transform" type="rotate" from="0" to="-360" dur="10s" repeatCount="indefinite"/>
    </polygon>
    <!-- 核心脉冲 -->
    <circle r="22" fill="rgba(51,136,255,0.15)" stroke="var(--neural)" stroke-width="1.5" opacity="0.7">
      <animate attributeName="r" values="20;26;20" dur="2s" repeatCount="indefinite"/>
      <animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
    </circle>
    <!-- 核心点 -->
    <circle r="6" fill="var(--neural)" opacity="0.9" filter="url(#glowNeural)">
      <animate attributeName="r" values="5;8;5" dur="1.5s" repeatCount="indefinite"/>
    </circle>
    <!-- 数据节点 -->
    <g opacity="0.6">
      <circle cx="0" cy="-65" r="3" fill="var(--bio)">
        <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="8s" repeatCount="indefinite"/>
      </circle>
      <circle cx="56" cy="33" r="2.5" fill="var(--intent)">
        <animateTransform attributeName="transform" type="rotate" from="0" to="-360" dur="6s" repeatCount="indefinite"/>
      </circle>
      <circle cx="-56" cy="33" r="2" fill="var(--execute)">
        <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="12s" repeatCount="indefinite"/>
      </circle>
    </g>
  </g>

  <!-- 引擎标签 -->
  <text x="700" y="395" text-anchor="middle" fill="var(--neural)" font-family="Rajdhani" font-size="16" font-weight="700" letter-spacing="3">意图预判引擎</text>
  <text x="700" y="412" text-anchor="middle" fill="var(--neural)" font-family="IBM Plex Mono" font-size="9" opacity="0.5">INTENT PRE-JUDGMENT ENGINE</text>
</g>

<!-- ========== 候选动作层 ========== -->
<g id="candidateLayer" opacity="0">
  <!-- 候选卡片1 - 高置信度 -->
  <g id="cand1" transform="translate(830, 180)" opacity="0">
    <rect x="0" y="0" width="130" height="50" rx="6" fill="rgba(10,20,40,0.85)" stroke="var(--intent)" stroke-width="1"/>
    <text x="12" y="20" fill="var(--intent)" font-family="Rajdhani" font-size="14" font-weight="600">开 灯</text>
    <text x="12" y="36" fill="var(--text)" font-family="IBM Plex Mono" font-size="10" opacity="0.6">confidence</text>
    <rect x="82" y="24" width="38" height="6" rx="3" fill="var(--dim)"/>
    <rect x="82" y="24" width="0" height="6" rx="3" fill="var(--intent)" id="cand1Bar"/>
    <text x="122" y="20" fill="var(--intent)" font-family="IBM Plex Mono" font-size="11" text-anchor="end" id="cand1Val">0%</text>
  </g>
  <!-- 候选卡片2 -->
  <g id="cand2" transform="translate(850, 245)" opacity="0">
    <rect x="0" y="0" width="130" height="50" rx="6" fill="rgba(10,20,40,0.85)" stroke="var(--neural)" stroke-width="0.8"/>
    <text x="12" y="20" fill="var(--neural)" font-family="Rajdhani" font-size="14" font-weight="600">导航回家</text>
    <text x="12" y="36" fill="var(--text)" font-family="IBM Plex Mono" font-size="10" opacity="0.6">confidence</text>
    <rect x="82" y="24" width="38" height="6" rx="3" fill="var(--dim)"/>
    <rect x="82" y="24" width="0" height="6" rx="3" fill="var(--neural)" id="cand2Bar"/>
    <text x="122" y="20" fill="var(--neural)" font-family="IBM Plex Mono" font-size="11" text-anchor="end" id="cand2Val">0%</text>
  </g>
  <!-- 候选卡片3 -->
  <g id="cand3" transform="translate(835, 310)" opacity="0">
    <rect x="0" y="0" width="130" height="50" rx="6" fill="rgba(10,20,40,0.85)" stroke="var(--dim)" stroke-width="0.8"/>
    <text x="12" y="20" fill="var(--text)" font-family="Rajdhani" font-size="14" font-weight="500" opacity="0.6">播放音乐</text>
    <text x="12" y="36" fill="var(--text)" font-family="IBM Plex Mono" font-size="10" opacity="0.4">confidence</text>
    <rect x="82" y="24" width="38" height="6" rx="3" fill="var(--dim)"/>
    <rect x="82" y="24" width="0" height="6" rx="3" fill="var(--text)" opacity="0.4" id="cand3Bar"/>
    <text x="122" y="20" fill="var(--text)" font-family="IBM Plex Mono" font-size="11" text-anchor="end" opacity="0.5" id="cand3Val">0%</text>
  </g>
  <!-- 候选卡片4 -->
  <g id="cand4" transform="translate(820, 375)" opacity="0">
    <rect x="0" y="0" width="130" height="50" rx="6" fill="rgba(10,20,40,0.85)" stroke="var(--dim)" stroke-width="0.6"/>
    <text x="12" y="20" fill="var(--text)" font-family="Rajdhani" font-size="14" font-weight="500" opacity="0.4">调节温度</text>
    <text x="12" y="36" fill="var(--text)" font-family="IBM Plex Mono" font-size="10" opacity="0.3">confidence</text>
    <rect x="82" y="24" width="38" height="6" rx="3" fill="var(--dim)"/>
    <rect x="82" y="24" width="0" height="6" rx="3" fill="var(--text)" opacity="0.3" id="cand4Bar"/>
    <text x="122" y="20" fill="var(--text)" font-family="IBM Plex Mono" font-size="11" text-anchor="end" opacity="0.35" id="cand4Val">0%</text>
  </g>

  <!-- 阈值线 -->
  <g id="thresholdLine" opacity="0">
    <line x1="815" y1="195" x2="965" y2="195" stroke="var(--voice)" stroke-width="1" stroke-dasharray="4 3" opacity="0.5"/>
    <text x="968" y="199" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9" opacity="0.6" id="thresholdLabel">阈值 85%</text>
  </g>
</g>

<!-- ========== 语音确认层 ========== -->
<g id="voiceLayer" opacity="0">
  <!-- 语音波形 -->
  <g transform="translate(280, 310)">
    <rect x="0" y="-15" width="3" height="30" rx="1.5" fill="var(--voice)" opacity="0.7" transform="scaleY(0.3)">
      <animate attributeName="transform" values="scaleY(0.3);scaleY(1);scaleY(0.3)" dur="0.3s" repeatCount="indefinite"/>
    </rect>
    <rect x="6" y="-15" width="3" height="30" rx="1.5" fill="var(--voice)" opacity="0.7" transform="scaleY(0.5)">
      <animate attributeName="transform" values="scaleY(0.5);scaleY(0.8);scaleY(0.5)" dur="0.35s" repeatCount="indefinite"/>
    </rect>
    <rect x="12" y="-15" width="3" height="30" rx="1.5" fill="var(--voice)" opacity="0.7" transform="scaleY(0.4)">
      <animate attributeName="transform" values="scaleY(0.4);scaleY(1);scaleY(0.4)" dur="0.25s" repeatCount="indefinite"/>
    </rect>
    <rect x="18" y="-15" width="3" height="30" rx="1.5" fill="var(--voice)" opacity="0.7" transform="scaleY(0.6)">
      <animate attributeName="transform" values="scaleY(0.6);scaleY(0.9);scaleY(0.6)" dur="0.4s" repeatCount="indefinite"/>
    </rect>
    <rect x="24" y="-15" width="3" height="30" rx="1.5" fill="var(--voice)" opacity="0.7" transform="scaleY(0.3)">
      <animate attributeName="transform" values="scaleY(0.3);scaleY(0.7);scaleY(0.3)" dur="0.32s" repeatCount="indefinite"/>
    </rect>
  </g>
  <text x="310" y="348" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="10" opacity="0.6">语音确认</text>
</g>

<!-- ========== 执行层 ========== -->
<g id="executionLayer" opacity="0">
  <!-- 执行闪光 -->
  <circle cx="1100" cy="280" r="30" fill="none" stroke="var(--execute)" stroke-width="2" opacity="0" id="execFlash1">
    <animate attributeName="r" values="30;90" dur="1s" fill="freeze"/>
    <animate attributeName="opacity" values="0.9;0" dur="1s" fill="freeze"/>
  </circle>
  <circle cx="1100" cy="280" r="20" fill="var(--execute)" opacity="0" id="execFlash2" filter="url(#glowExec)">
    <animate attributeName="r" values="15;50" dur="0.8s" fill="freeze"/>
    <animate attributeName="opacity" values="0.8;0" dur="0.8s" fill="freeze"/>
  </circle>

  <!-- 执行目标 - 智能家居图标 -->
  <g transform="translate(1070, 240)" opacity="0" id="execTarget">
    <!-- 灯泡图标 -->
    <path d="M 30 0 Q 50 0 50 25 Q 50 38 38 42 L 38 52 L 22 52 L 22 42 Q 10 38 10 25 Q 10 0 30 0 Z"
      fill="none" stroke="var(--execute)" stroke-width="2"/>
    <line x1="24" y1="55" x2="36" y2="55" stroke="var(--execute)" stroke-width="1.5"/>
    <line x1="26" y1="59" x2="34" y2="59" stroke="var(--execute)" stroke-width="1.5"/>
    <!-- 灯光射线 -->
    <line x1="30" y1="-12" x2="30" y2="-6" stroke="var(--intent)" stroke-width="1.5" opacity="0.7"/>
    <line x1="50" y1="-2" x2="56" y2="-8" stroke="var(--intent)" stroke-width="1.5" opacity="0.7"/>
    <line x1="10" y1="-2" x2="4" y2="-8" stroke="var(--intent)" stroke-width="1.5" opacity="0.7"/>
    <line x1="58" y1="18" x2="64" y2="18" stroke="var(--intent)" stroke-width="1.5" opacity="0.5"/>
    <line x1="2" y1="18" x2="-4" y2="18" stroke="var(--intent)" stroke-width="1.5" opacity="0.5"/>
  </g>
  <text x="1100" y="330" text-anchor="middle" fill="var(--execute)" font-family="Rajdhani" font-size="15" font-weight="700" opacity="0" id="execLabel">即时执行</text>
  <text x="1100" y="348" text-anchor="middle" fill="var(--execute)" font-family="IBM Plex Mono" font-size="10" opacity="0" id="execSub">&lt; 50ms</text>
</g>

<!-- ========== 连接线:引擎到执行 ========== -->
<g id="engineToExec" opacity="0">
  <path d="M 790 280 Q 920 260 1040 280" fill="none" stroke="var(--execute)" stroke-width="2"
    stroke-dasharray="10 5" opacity="0.5" filter="url(#glowExec)">
    <animate attributeName="stroke-dashoffset" from="0" to="-30" dur="0.6s" repeatCount="indefinite"/>
  </path>
  <!-- 箭头 -->
  <polygon points="1045,280 1035,274 1035,286" fill="var(--execute)" opacity="0.7"/>
</g>

<!-- ========== 时间线对比层 ========== -->
<g id="timelineLayer" opacity="0" transform="translate(0, 0)">
  <!-- 分割线 -->
  <line x1="80" y1="490" x2="1320" y2="490" stroke="var(--dim)" stroke-width="0.5"/>

  <!-- 标题 -->
  <text x="80" y="520" fill="var(--text)" font-family="Rajdhani" font-size="14" font-weight="700" letter-spacing="2" opacity="0.7">时序对比 / TEMPORAL COMPARISON</text>

  <!-- 传统交互时间线 -->
  <g id="tradTimeline">
    <text x="80" y="558" fill="var(--voice)" font-family="IBM Plex Mono" font-size="11" opacity="0.7">传统交互</text>
    <!-- 时间轴 -->
    <line x1="220" y1="554" x2="1300" y2="554" stroke="var(--dim)" stroke-width="1"/>
    <!-- 阶段块 -->
    <g id="tradBlocks" opacity="0">
      <rect x="220" y="542" width="180" height="24" rx="3" fill="rgba(255,85,119,0.15)" stroke="var(--voice)" stroke-width="0.8"/>
      <text x="310" y="558" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9">沉默等待</text>

      <rect x="410" y="542" width="280" height="24" rx="3" fill="rgba(255,85,119,0.25)" stroke="var(--voice)" stroke-width="0.8"/>
      <text x="550" y="558" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9">语音输入 + 识别</text>

      <rect x="700" y="542" width="200" height="24" rx="3" fill="rgba(255,85,119,0.2)" stroke="var(--voice)" stroke-width="0.8"/>
      <text x="800" y="558" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9">语义解析</text>

      <rect x="910" y="542" width="120" height="24" rx="3" fill="rgba(255,85,119,0.15)" stroke="var(--voice)" stroke-width="0.8"/>
      <text x="970" y="558" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9">执行</text>

      <rect x="1040" y="542" width="100" height="24" rx="3" fill="rgba(255,85,119,0.1)" stroke="var(--voice)" stroke-width="0.8"/>
      <text x="1090" y="558" text-anchor="middle" fill="var(--voice)" font-family="IBM Plex Mono" font-size="9">反馈</text>
    </g>
    <text x="1200" y="558" fill="var(--voice)" font-family="IBM Plex Mono" font-size="11" opacity="0" id="tradTime">~4.2s</text>
  </g>

  <!-- IFR 意图预判时间线 -->
  <g id="ifrTimeline">
    <text x="80" y="620" fill="var(--bio)" font-family="IBM Plex Mono" font-size="11" opacity="0.7">IFR 预判</text>
    <!-- 时间轴 -->
    <line x1="220" y1="616" x2="1300" y2="616" stroke="var(--dim)" stroke-width="1"/>
    <!-- 阶段块 - 更短,且有重叠 -->
    <g id="ifrBlocks" opacity="0">
      <rect x="220" y="604" width="120" height="24" rx="3" fill="rgba(0,240,212,0.2)" stroke="var(--bio)" stroke-width="0.8"/>
      <text x="280" y="620" text-anchor="middle" fill="var(--bio)" font-family="IBM Plex Mono" font-size="9">前导信号</text>

      <rect x="310" y="604" width="180" height="24" rx="3" fill="rgba(51,136,255,0.2)" stroke="var(--neural)" stroke-width="0.8"/>
      <text x="400" y="620" text-anchor="middle" fill="var(--neural)" font-family="IBM Plex Mono" font-size="9">意图预计算</text>

      <rect x="470" y="604" width="80" height="24" rx="3" fill="rgba(245,200,66,0.2)" stroke="var(--intent)" stroke-width="0.8"/>
      <text x="510" y="620" text-anchor="middle" fill="var(--intent)" font-family="IBM Plex Mono" font-size="9">确认</text>

      <rect x="530" y="604" width="60" height="24" rx="3" fill="rgba(0,255,136,0.25)" stroke="var(--execute)" stroke-width="0.8"/>
      <text x="560" y="620" text-anchor="middle" fill="var(--execute)" font-family="IBM Plex Mono" font-size="9">执行</text>
    </g>
    <text x="640" y="620" fill="var(--execute)" font-family="IBM Plex Mono" font-size="11" opacity="0" id="ifrTime">~0.45s</text>
  </g>

  <!-- 时间节省指示 -->
  <g id="timeSaved" opacity="0">
    <!-- 大括号标注节省的时间 -->
    <line x1="600" y1="596" x2="600" y2="640" stroke="var(--execute)" stroke-width="1" stroke-dasharray="3 2" opacity="0.4"/>
    <text x="620" y="636" fill="var(--execute)" font-family="Rajdhani" font-size="18" font-weight="700" opacity="0.9">节省 ~89%</text>
    <text x="620" y="654" fill="var(--execute)" font-family="IBM Plex Mono" font-size="9" opacity="0.5">时间压缩比</text>
  </g>

  <!-- 原理标注 -->
  <g id="principleNote" opacity="0">
    <text x="80" y="700" fill="var(--intent)" font-family="Rajdhani" font-size="13" font-weight="500" opacity="0.6">IFR 核心原理:</text>
    <text x="210" y="700" fill="var(--text)" font-family="IBM Plex Mono" font-size="11" opacity="0.45">利用发声前已存在的前导生理信号,将计算时间线前移至"意图形成期",消除了传统交互中的等待与识别延迟。</text>
    <text x="80" y="724" fill="var(--intent)" font-family="Rajdhani" font-size="13" font-weight="500" opacity="0.6">资源巧用:</text>
    <text x="170" y="724" fill="var(--text)" font-family="IBM Plex Mono" font-size="11" opacity="0.45">肌电信号是人体交互的"已有资源",传感器仅被动采集,无需用户额外操作,系统复杂度增幅极小。</text>
  </g>
</g>

<!-- ========== 阶段文字标注 ========== -->
<g id="phaseText" opacity="0">
  <text x="700" y="460" text-anchor="middle" fill="var(--text)" font-family="Rajdhani" font-size="20" font-weight="700" letter-spacing="2" id="phaseTitle" opacity="0"></text>
  <text x="700" y="480" text-anchor="middle" fill="var(--text)" font-family="IBM Plex Mono" font-size="11" opacity="0.5" id="phaseDesc"></text>
</g>

</svg>
</div>
</div>

<!-- 控制面板 -->
<div class="controls">
  <label>置信度阈值</label>
  <input type="range" id="thresholdSlider" min="50" max="99" value="85" step="1"/>
  <span class="threshold-val" id="thresholdDisplay">85%</span>
  <div style="width:1px;height:20px;background:var(--dim);margin:0 4px;"></div>
  <button class="ctrl-btn" id="restartBtn">重新播放</button>
</div>

<script>
(function(){
  const SVG_NS = 'http://www.w3.org/2000/svg';
  const svg = document.getElementById('mainSvg');

  // 获取所有图层
  const layers = {
    human: document.getElementById('humanLayer'),
    signal: document.getElementById('signalLayer'),
    particle: document.getElementById('particleLayer'),
    engine: document.getElementById('engineLayer'),
    candidate: document.getElementById('candidateLayer'),
    voice: document.getElementById('voiceLayer'),
    execution: document.getElementById('executionLayer'),
    engineToExec: document.getElementById('engineToExec'),
    timeline: document.getElementById('timelineLayer'),
    phaseText: document.getElementById('phaseText'),
  };

  // 候选动作元素
  const candEls = {
    1: { g: document.getElementById('cand1'), bar: document.getElementById('cand1Bar'), val: document.getElementById('cand1Val'), target: 92 },
    2: { g: document.getElementById('cand2'), bar: document.getElementById('cand2Bar'), val: document.getElementById('cand2Val'), target: 87 },
    3: { g: document.getElementById('cand3'), bar: document.getElementById('cand3Bar'), val: document.getElementById('cand3Val'), target: 68 },
    4: { g: document.getElementById('cand4'), bar: document.getElementById('cand4Bar'), val: document.getElementById('cand4Val'), target: 52 },
  };

  // 阈值控制
  let confidenceThreshold = 85;
  const slider = document.getElementById('thresholdSlider');
  const thresholdDisplay = document.getElementById('thresholdDisplay');
  const thresholdLabel = document.getElementById('thresholdLabel');

  slider.addEventListener('input', function(){
    confidenceThreshold = parseInt(this.value);
    thresholdDisplay.textContent = confidenceThreshold + '%';
    thresholdLabel.textContent = '阈值 ' + confidenceThreshold + '%';
    updateCandidateHighlights();
  });

  function updateCandidateHighlights(){
    Object.keys(candEls).forEach(k => {
      const c = candEls[k];
      const above = c.target >= confidenceThreshold;
      const rect = c.g.querySelector('rect');
      if(above){
        rect.setAttribute('stroke', k === '1' ? '#f5c842' : '#3388ff');
        rect.setAttribute('stroke-width', '1.2');
        c.g.style.opacity = '1';
      } else {
        rect.setAttribute('stroke', '#14203a');
        rect.setAttribute('stroke-width', '0.6');
        c.g.style.opacity = '0.4';
      }
    });
  }

  // 粒子系统
  const particles = [];
  const sigPath1 = document.getElementById('sigPath1');
  const sigPath2 = document.getElementById('sigPath2');

  function spawnParticle(pathEl, color, size, duration){
    const totalLen = pathEl.getTotalLength();
    const circle = document.createElementNS(SVG_NS, 'circle');
    circle.setAttribute('r', size || 3);
    circle.setAttribute('fill', color || '#00f0d4');
    circle.setAttribute('opacity', '0');
    circle.setAttribute('filter', 'url(#glowBio)');
    layers.particle.appendChild(circle);

    const p = {
      el: circle,
      pathLen: totalLen,
      pathEl: pathEl,
      startTime: performance.now(),
      duration: duration || 1200,
      alive: true
    };
    particles.push(p);
    return p;
  }

  function updateParticles(now){
    for(let i = particles.length - 1; i >= 0; i--){
      const p = particles[i];
      const elapsed = now - p.startTime;
      const t = Math.min(elapsed / p.duration, 1);
      const pt = p.pathEl.getPointAtLength(t * p.pathLen);
      p.el.setAttribute('cx', pt.x);
      p.el.setAttribute('cy', pt.y);
      // 淡入淡出
      const opacity = t < 0.1 ? t * 10 : t > 0.85 ? (1 - t) / 0.15 : 1;
      p.el.setAttribute('opacity', Math.max(0, opacity * 0.9));
      if(t >= 1){
        p.el.remove();
        particles.splice(i, 1);
      }
    }
  }

  // 持续产生粒子的定时器
  let particleInterval = null;
  function startParticleFlow(){
    if(particleInterval) return;
    particleInterval = setInterval(() => {
      spawnParticle(sigPath1, '#00f0d4', 3, 1000 + Math.random() * 400);
      spawnParticle(sigPath2, '#00f0d4', 2.5, 1200 + Math.random() * 500);
    }, 200);
  }
  function stopParticleFlow(){
    if(particleInterval){
      clearInterval(particleInterval);
      particleInterval = null;
    }
  }

  // 辅助函数:设置元素透明度(带过渡)
  function setOpacity(el, val, duration){
    if(!el) return;
    duration = duration || 600;
    el.style.transition = 'opacity ' + duration + 'ms ease';
    el.style.opacity = val;
  }

  // 辅助函数:动画数值
  function animateValue(from, to, duration, callback, done){
    const start = performance.now();
    function tick(now){
      const t = Math.min((now - start) / duration, 1);
      const ease = t < 0.5 ? 2*t*t : -1+(4-2*t)*t; // easeInOutQuad
      callback(from + (to - from) * ease);
      if(t < 1) requestAnimationFrame(tick);
      else if(done) done();
    }
    requestAnimationFrame(tick);
  }

  // 阶段指示器
  const phaseDots = document.querySelectorAll('.phase-dot');
  function setPhaseDot(idx, state){
    if(!phaseDots[idx]) return;
    phaseDots[idx].classList.remove('active','done');
    if(state === 'active') phaseDots[idx].classList.add('active');
    else if(state === 'done') phaseDots[idx].classList.add('done');
  }

  // ===== 动画阶段 =====
  const PHASE_DURATIONS = [2000, 3000, 3500, 2000, 2500, 2000];
  // 0:激活 1:前导信号 2:意图预计算 3:语音确认 4:即时执行 5:展示/保持

  let animFrame = null;
  let phaseTimeout = null;
  let running = false;

  function resetAll(){
    // 停止所有动画
    if(animFrame) cancelAnimationFrame(animFrame);
    if(phaseTimeout) clearTimeout(phaseTimeout);
    stopParticleFlow();

    // 清除粒子
    particles.forEach(p => p.el.remove());
    particles.length = 0;

    // 重置图层透明度
    Object.values(layers).forEach(l => {
      if(l) l.style.opacity = '0';
    });

    // 重置候选卡片
    Object.values(candEls).forEach(c => {
      c.g.style.opacity = '0';
      c.bar.setAttribute('width', '0');
      c.val.textContent = '0%';
    });

    // 重置时间线子元素
    const tradBlocks = document.getElementById('tradBlocks');
    const ifrBlocks = document.getElementById('ifrBlocks');
    if(tradBlocks) tradBlocks.style.opacity = '0';
    if(ifrBlocks) ifrBlocks.style.opacity = '0';
    const tradTime = document.getElementById('tradTime');
    const ifrTime = document.getElementById('ifrTime');
    if(tradTime) tradTime.style.opacity = '0';
    if(ifrTime) ifrTime.style.opacity = '0';

    // 重置时间节省
    const timeSaved = document.getElementById('timeSaved');
    if(timeSaved) timeSaved.style.opacity = '0';
    const principleNote = document.getElementById('principleNote');
    if(principleNote) principleNote.style.opacity = '0';

    // 重置执行层
    const execTarget = document.getElementById('execTarget');
    const execLabel = document.getElementById('execLabel');
    const execSub = document.getElementById('execSub');
    if(execTarget) execTarget.style.opacity = '0';
    if(execLabel) execLabel.style.opacity = '0';
    if(execSub) execSub.style.opacity = '0';

    // 重置阈值线
    const thresholdLine = document.getElementById('thresholdLine');
    if(thresholdLine) thresholdLine.style.opacity = '0';

    // 重置阶段指示器
    phaseDots.forEach(d => d.classList.remove('active','done'));

    // 重置阶段文字
    const phaseTitle = document.getElementById('phaseTitle');
    const phaseDesc = document.getElementById('phaseDesc');
    if(phaseTitle) phaseTitle.style.opacity = '0';
    if(phaseDesc) phaseDesc.textContent = '';
  }

  function startAnimation(){
    resetAll();
    running = true;
    phase0_activate();
  }

  // 阶段0:激活 - 人形图和传感器出现
  function phase0_activate(){
    setPhaseDot(0, 'active');

    setOpacity(layers.human, 1, 800);

    setTimeout(() => {
      // 传感器脉冲增强
      setOpacity(layers.engine, 0.4, 1000);
    }, 600);

    setTimeout(() => {
      setOpacity(layers.phaseText, 1, 400);
      const pt = document.getElementById('phaseTitle');
      const pd = document.getElementById('phaseDesc');
      pt.style.opacity = '1';
      pt.textContent = '微生理信号采集阵列';
      pd.textContent = 'sEMG传感器 · 微雷达 · 1000Hz采样率';
      pt.setAttribute('fill', '#00f0d4');
    }, 800);

    phaseTimeout = setTimeout(() => {
      setPhaseDot(0, 'done');
      phase1_presignal();
    }, PHASE_DURATIONS[0]);
  }

  // 阶段1:前导信号 - 传感器捕获信号,粒子流向引擎
  function phase1_presignal(){
    setPhaseDot(1, 'active');

    setOpacity(layers.signal, 1, 600);
    startParticleFlow();

    const pt = document.getElementById('phaseTitle');
    const pd = document.getElementById('phaseDesc');
    pt.textContent = '前导信号捕获';
    pd.textContent = '喉部/面部微肌肉收缩 → 电信号产生(发声前200-500ms)';
    pt.setAttribute('fill', '#00f0d4');

    // 引擎开始接收
    setTimeout(() => {
      setOpacity(layers.engine, 1, 800);
    }, 500);

    phaseTimeout = setTimeout(() => {
      setPhaseDot(1, 'done');
      phase2_precompute();
    }, PHASE_DURATIONS[1]);
  }

  // 阶段2:意图预计算 - 引擎处理,候选动作出现,置信度上升
  function phase2_precompute(){
    setPhaseDot(2, 'active');

    const pt = document.getElementById('phaseTitle');
    const pd = document.getElementById('phaseDesc');
    pt.textContent = '意图预计算';
    pd.textContent = '结合界面上下文,生成候选动作集 · 置信度持续攀升';
    pt.setAttribute('fill', '#3388ff');

    // 显示候选卡片和阈值线
    setOpacity(layers.candidate, 1, 600);
    setTimeout(() => {
      const tl = document.getElementById('thresholdLine');
      if(tl) setOpacity(tl, 1, 400);
    }, 300);

    // 依次显示候选卡片并动画置信度
    const delay = 300;
    Object.keys(candEls).forEach((k, i) => {
      const c = candEls[k];
      setTimeout(() => {
        setOpacity(c.g, 1, 400);
        // 动画置信度值
        const targetConf = c.target;
        animateValue(0, targetConf, 1200, (v) => {
          c.val.textContent = Math.round(v) + '%';
          c.bar.setAttribute('width', (v / 100 * 38) + '');
        });
      }, i * delay);
    });

    // 延迟后高亮超阈值项
    setTimeout(() => {
      updateCandidateHighlights();
    }, 1800);

    phaseTimeout = setTimeout(() => {
      setPhaseDot(2, 'done');
      phase3_confirm();
    }, PHASE_DURATIONS[2]);
  }

  // 阶段3:语音确认 - 用户开口,系统瞬间匹配
  function phase3_confirm(){
    setPhaseDot(3, 'active');

    const pt = document.getElementById('phaseTitle');
    const pd = document.getElementById('phaseDesc');
    pt.textContent = '语音微确认';
    pd.textContent = '用户开口说第一个词 → 系统已在高置信度区间完成匹配';
    pt.setAttribute('fill', '#ff5577');

    setOpacity(layers.voice, 1, 400);

    // 高亮匹配的候选
    setTimeout(() => {
      const c1 = candEls[1];
      const rect = c1.g.querySelector('rect');
      rect.setAttribute('stroke', '#00ff88');
      rect.setAttribute('stroke-width', '2');
      // 闪光效果
      c1.g.style.filter = 'url(#glowExec)';
      setTimeout(() => { c1.g.style.filter = ''; }, 600);
    }, 500);

    phaseTimeout = setTimeout(() => {
      setPhaseDot(3, 'done');
      phase4_execute();
    }, PHASE_DURATIONS[3]);
  }

  // 阶段4:即时执行
  function phase4_execute(){
    setPhaseDot(4, 'active');

    const pt = document.getElementById('phaseTitle');
    const pd = document.getElementById('phaseDesc');
    pt.textContent = '即时执行';
    pd.textContent = '指令瞬间下发 → 设备响应 → 用户感知"零延迟"';
    pt.setAttribute('fill', '#00ff88');

    // 停止信号粒子
    stopParticleFlow();

    // 显示执行连线和目标
    setOpacity(layers.engineToExec, 1, 300);

    setTimeout(() => {
      setOpacity(layers.execution, 1, 200);

      // 重建闪光动画(克隆替换以重启动画)
      const flash1 = document.getElementById('execFlash1');
      const flash2 = document.getElementById('execFlash2');
      if(flash1){
        const clone1 = flash1.cloneNode(true);
        clone1.removeAttribute('opacity');
        flash1.parentNode.replaceChild(clone1, flash1);
      }
      if(flash2){
        const clone2 = flash2.cloneNode(true);
        clone2.removeAttribute('opacity');
        flash1.parentNode.replaceChild(clone2, flash2);
      }

      // 显示目标图标
      const execTarget = document.getElementById('execTarget');
      const execLabel = document.getElementById('execLabel');
      const execSub = document.getElementById('execSub');
      setTimeout(() => {
        if(execTarget){ setOpacity(execTarget, 1, 400); }
        if(execLabel){ setOpacity(execLabel, 1, 400); }
        if(execSub){ setOpacity(execSub, 1, 400); }
      }, 300);
    }, 400);

    // 隐藏语音层
    setTimeout(() => { setOpacity(layers.voice, 0, 300); }, 800);

    phaseTimeout = setTimeout(() => {
      setPhaseDot(4, 'done');
      phase5_timeline();
    }, PHASE_DURATIONS[4]);
  }

  // 阶段5:时间线对比展示
  function phase5_timeline(){
    setOpacity(layers.timeline, 1, 800);

    // 显示传统时间线
    setTimeout(() => {
      const tradBlocks = document.getElementById('tradBlocks');
      if(tradBlocks) setOpacity(tradBlocks, 1, 600);
      setTimeout(() => {
        const tradTime = document.getElementById('tradTime');
        if(tradTime) setOpacity(tradTime, 1, 400);
      }, 400);
    }, 500);

    // 显示IFR时间线
    setTimeout(() => {
      const ifrBlocks = document.getElementById('ifrBlocks');
      if(ifrBlocks) setOpacity(ifrBlocks, 1, 600);
      setTimeout(() => {
        const ifrTime = document.getElementById('ifrTime');
        if(ifrTime) setOpacity(ifrTime, 1, 400);
      }, 400);
    }, 1500);

    // 显示时间节省
    setTimeout(() => {
      const timeSaved = document.getElementById('timeSaved');
      if(timeSaved) setOpacity(timeSaved, 1, 600);
    }, 2500);

    // 显示原理标注
    setTimeout(() => {
      const principleNote = document.getElementById('principleNote');
      if(principleNote) setOpacity(principleNote, 1, 800);
    }, 3000);

    // 保持后重新循环
    phaseTimeout = setTimeout(() => {
      running = false;
      startAnimation();
    }, PHASE_DURATIONS[5]);
  }

  // 动画帧循环(用于粒子更新)
  function mainLoop(now){
    updateParticles(now);
    if(running || particles.length > 0){
      animFrame = requestAnimationFrame(mainLoop);
    }
  }

  // 重新播放按钮
  document.getElementById('restartBtn').addEventListener('click', function(){
    startAnimation();
    animFrame = requestAnimationFrame(mainLoop);
  });

  // 页面加载后自动开始
  window.addEventListener('DOMContentLoaded', function(){
    // 短暂延迟确保渲染完成
    setTimeout(() => {
      startAnimation();
      animFrame = requestAnimationFrame(mainLoop);
    }, 300);
  });

  // iframe重载时也自动开始
  window.addEventListener('load', function(){
    if(!running){
      startAnimation();
      animFrame = requestAnimationFrame(mainLoop);
    }
  });

  // 页面可见性变化时处理
  document.addEventListener('visibilitychange', function(){
    if(!document.hidden && !running){
      startAnimation();
      animFrame = requestAnimationFrame(mainLoop);
    }
  });

})();
</script>
</body>
</html>

这是一个完整的、自动播放的 SVG 原理动画,以下是实现要点说明:

设计思路

  • 美学方向:深空暗色背景 + 生物信号青绿 + 神经处理蓝 + 意图金色 + 执行绿色的配色体系,营造"生物-数字融合"的技术美学感
  • 字体选择:Rajdhani(显示/标题)+ IBM Plex Mono(数据/标注),形成科技感与数据感的层次对比
  • IFR 聚焦:直接展示"发声前预判→即时执行"的理想状态,不制作前后对比拖沓

五阶段动画流程(自动循环播放)

  1. 激活 — 人形轮廓与传感器节点渐现,sEMG 与微雷达标注亮起
  2. 前导信号 — 粒子沿贝塞尔曲线从传感器流向引擎,波形动画模拟 1000Hz 采样信号
  3. 意图预计算 — 引擎核心旋转加速,四张候选动作卡片依次出现,置信度条动态攀升,阈值线标识超限项
  4. 语音微确认 — 声波动画出现,最高置信度项瞬间匹配高亮(绿色闪光)
  5. 即时执行 — 执行闪光爆发,灯泡图标亮起,底部时间线对比展示传统 ~4.2s vs IFR ~0.45s(节省 89%)

交互功能

  • 置信度阈值滑块:拖动可实时改变 50%-99% 阈值,低于阈值的候选卡片自动降低视觉权重,帮助理解阈值对预判准确率的影响
  • 重新播放按钮:随时重启动画循环
积分规则:第一轮对话扣减8分,后续每轮扣6分