分享图
A
动画渲染工坊
就绪
<!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=JetBrains+Mono:wght@300;400;600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#060a12;color:#c0d0e0;font-family:'Rajdhani',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
.header{width:100%;padding:18px 32px 10px;display:flex;align-items:baseline;gap:18px;border-bottom:1px solid rgba(0,180,255,.12);background:linear-gradient(180deg,rgba(6,10,18,.95),transparent)}
.header h1{font-size:22px;font-weight:700;letter-spacing:1px;color:#e8f0ff}
.header .tag{font-size:11px;font-weight:500;padding:2px 10px;border-radius:3px;background:rgba(0,200,255,.12);color:#00c8ff;letter-spacing:.5px;border:1px solid rgba(0,200,255,.2)}
.svg-wrap{width:100%;max-width:1460px;flex:1;display:flex;justify-content:center;align-items:center;padding:8px 12px}
.svg-wrap svg{width:100%;height:auto;max-height:82vh}
.controls{width:100%;max-width:1460px;padding:12px 28px 18px;display:flex;flex-wrap:wrap;gap:16px 32px;align-items:center;background:linear-gradient(0deg,rgba(6,10,18,.98),transparent);border-top:1px solid rgba(0,180,255,.08)}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{font-size:13px;font-weight:500;color:#7a8fa6;white-space:nowrap}
.ctrl-group input[type=range]{width:120px;accent-color:#00b8ff;height:4px;cursor:pointer}
.ctrl-group .val{font-family:'JetBrains Mono',monospace;font-size:12px;color:#00d4ff;min-width:42px;text-align:right}
.btn{padding:5px 18px;border:1px solid rgba(0,180,255,.3);border-radius:4px;background:rgba(0,180,255,.08);color:#00d4ff;font-family:'Rajdhani',sans-serif;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s}
.btn:hover{background:rgba(0,180,255,.18);border-color:rgba(0,180,255,.5)}
.btn.active{background:rgba(0,180,255,.22);border-color:#00b8ff}
.sep{width:1px;height:24px;background:rgba(0,180,255,.12)}
@keyframes pulseGlow{0%,100%{opacity:.6}50%{opacity:1}}
</style>
</head>
<body>

<div class="header">
  <h1>非圆轮廓绕线轮转向耦合机构</h1>
  <span class="tag">IFR 最终理想解</span>
  <span class="tag">零中间传动</span>
</div>

<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 1440 860" xmlns="http://www.w3.org/2000/svg">
<defs>
  <!-- 滤镜 -->
  <filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="4" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/>
  </filter>
  <filter id="glowAmber" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="6" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/>
  </filter>
  <filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="3" result="b"/><feComposite in="SourceGraphic" in2="b" operator="over"/>
  </filter>
  <filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
    <feGaussianBlur stdDeviation="8" result="b"/><feFlood flood-color="#000" flood-opacity=".35"/><feComposite in2="b" operator="in"/><feComposite in="SourceGraphic"/>
  </filter>
  <!-- 渐变 -->
  <linearGradient id="wheelGrad" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%" stop-color="#0af"/><stop offset="100%" stop-color="#06c"/>
  </linearGradient>
  <linearGradient id="weightGrad" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" stop-color="#ff9800"/><stop offset="100%" stop-color="#e65100"/>
  </linearGradient>
  <radialGradient id="bgGrad" cx="35%" cy="40%">
    <stop offset="0%" stop-color="#0d1525"/><stop offset="100%" stop-color="#060a12"/>
  </radialGradient>
  <!-- 网格 -->
  <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
    <path d="M40 0L0 0 0 40" fill="none" stroke="rgba(0,160,255,.04)" stroke-width=".5"/>
  </pattern>
</defs>

<!-- 背景 -->
<rect width="1440" height="860" fill="url(#bgGrad)"/>
<rect width="1440" height="860" fill="url(#grid)"/>

<!-- 分隔线 -->
<line x1="830" y1="40" x2="830" y2="820" stroke="rgba(0,160,255,.08)" stroke-width="1" stroke-dasharray="4,6"/>

<!-- ========== 左侧:机构图 ========== -->
<g id="mechanismGroup">
  <!-- 标题 -->
  <text x="410" y="42" fill="#5a7a9a" font-size="13" font-weight="500" text-anchor="middle" letter-spacing="2">机 构 原 理 图</text>

  <!-- 非圆绕线轮 -->
  <g id="wheelGroup" transform="translate(380,230)">
    <path id="wheelProfile" d="" fill="rgba(0,120,200,.08)" stroke="url(#wheelGrad)" stroke-width="2.5" filter="url(#glowCyan)"/>
    <!-- 辐条 -->
    <g id="wheelSpokes"></g>
    <!-- 中心轴 -->
    <circle r="8" fill="#0a1020" stroke="#0af" stroke-width="1.5"/>
    <circle r="3" fill="#0af"/>
    <!-- 有效半径指示线 -->
    <line id="leftRadiusLine" x1="0" y1="0" x2="0" y2="0" stroke="#00d4ff" stroke-width="1.2" stroke-dasharray="3,3" opacity=".7"/>
    <line id="rightRadiusLine" x1="0" y1="0" x2="0" y2="0" stroke="#ff2d7b" stroke-width="1.2" stroke-dasharray="3,3" opacity=".7"/>
    <!-- 峰/谷标记点 -->
    <circle id="leftMark" r="4.5" fill="#00d4ff" opacity=".9"/>
    <circle id="rightMark" r="4.5" fill="#ff2d7b" opacity=".9"/>
  </g>

  <!-- 左滑轮 -->
  <g id="leftPulleyGroup" transform="translate(120,230)">
    <circle r="14" fill="#0a1020" stroke="#2a4a6a" stroke-width="1.5"/>
    <circle r="3" fill="#3a5a7a"/>
    <line id="leftPulleySpoke" x1="-10" y1="0" x2="10" y2="0" stroke="#3a5a7a" stroke-width=".8"/>
  </g>
  <!-- 右滑轮 -->
  <g id="rightPulleyGroup" transform="translate(640,230)">
    <circle r="14" fill="#0a1020" stroke="#2a4a6a" stroke-width="1.5"/>
    <circle r="3" fill="#3a5a7a"/>
    <line id="rightPulleySpoke" x1="-10" y1="0" x2="10" y2="0" stroke="#3a5a7a" stroke-width=".8"/>
  </g>

  <!-- 主绳 -->
  <line id="mainRope" x1="380" y1="0" x2="380" y2="0" stroke="#ff9800" stroke-width="2" opacity=".85"/>

  <!-- 左牵引绳 -->
  <polyline id="leftRope" points="" fill="none" stroke="#00d4ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  <!-- 右牵引绳 -->
  <polyline id="rightRope" points="" fill="none" stroke="#ff2d7b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>

  <!-- 重物 -->
  <g id="weightGroup" filter="url(#glowAmber)">
    <rect id="weightRect" x="-22" y="0" width="44" height="52" rx="4" fill="url(#weightGrad)" stroke="#ffb74d" stroke-width="1"/>
    <text id="weightLabel" x="0" y="30" fill="#fff" font-size="13" font-weight="700" text-anchor="middle" font-family="'JetBrains Mono',monospace">W</text>
  </g>

  <!-- 转向臂 -->
  <g id="steeringArmGroup" transform="translate(380,490)">
    <!-- 回位弹簧 -->
    <path id="springPath" d="" fill="none" stroke="#76ff03" stroke-width="1.5" opacity=".7"/>
    <!-- 臂 -->
    <rect id="armBar" x="-110" y="-6" width="220" height="12" rx="3" fill="#1a2a3a" stroke="#4a6a8a" stroke-width="1"/>
    <!-- 左连接点 -->
    <circle cx="-100" cy="0" r="5" fill="#00d4ff" opacity=".6"/>
    <!-- 右连接点 -->
    <circle cx="100" cy="0" r="5" fill="#ff2d7b" opacity=".6"/>
    <!-- 枢轴 -->
    <circle r="8" fill="#0a1020" stroke="#4a6a8a" stroke-width="1.5"/>
    <circle r="3" fill="#5a7a9a"/>
  </g>

  <!-- 前轮 -->
  <g id="frontWheelGroup" transform="translate(380,610)">
    <circle r="30" fill="#0a1020" stroke="#4a6a8a" stroke-width="2"/>
    <circle r="5" fill="#3a5a7a"/>
    <line id="wheelDirLine" x1="0" y1="-30" x2="0" y2="-42" stroke="#e0e0e0" stroke-width="2" stroke-linecap="round"/>
    <!-- 轮胎纹 -->
    <g id="tireTreads"></g>
  </g>

  <!-- 标注文字 -->
  <g id="labels" fill="#5a7a9a" font-size="11" font-weight="500">
    <text x="380" y="140" text-anchor="middle" fill="#00b8ff" font-size="12" font-weight="700">非圆绕线轮</text>
    <text x="120" y="205" text-anchor="middle">左滑轮</text>
    <text x="640" y="205" text-anchor="middle">右滑轮</text>
    <text id="weightText" x="420" y="0" fill="#ff9800" font-size="11">重物</text>
    <text x="380" y="535" text-anchor="middle">转向臂</text>
    <text id="springLabel" x="420" y="475" fill="#76ff03" font-size="10">回位弹簧</text>
    <text x="380" y="660" text-anchor="middle">前轮</text>
    <!-- 绳索标注 -->
    <text id="leftRopeLabel" x="220" y="310" fill="#00d4ff" font-size="10" opacity=".8">左牵引绳</text>
    <text id="rightRopeLabel" x="530" y="310" fill="#ff2d7b" font-size="10" opacity=".8">右牵引绳</text>
    <text id="mainRopeLabel" x="400" y="340" fill="#ff9800" font-size="10" opacity=".8">主绳</text>
  </g>

  <!-- 核心创新标注 -->
  <g id="innovationCallout">
    <rect x="30" y="690" width="310" height="70" rx="6" fill="rgba(0,180,255,.06)" stroke="rgba(0,180,255,.2)" stroke-width="1"/>
    <text x="46" y="714" fill="#00d4ff" font-size="13" font-weight="700">核心创新</text>
    <text x="46" y="733" fill="#7a9ab0" font-size="11">几何轮廓即转向函数 — 势能直接生成正弦转向</text>
    <text x="46" y="750" fill="#5a7a9a" font-size="10">消除中间传动环节,零摩擦损耗</text>
  </g>

  <!-- 能量流标注 -->
  <g id="energyFlow" fill="none" stroke-width="1.2">
    <path d="M380,640 L380,660 Q380,670 370,670 L300,670 Q290,670 290,680 L290,700" stroke="#ff9800" stroke-dasharray="4,3" opacity=".4"/>
    <path d="M290,700 L290,720 Q290,730 300,730 L460,730 Q470,730 470,720 L470,510" stroke="#00d4ff" stroke-dasharray="4,3" opacity=".4"/>
  </g>
  <text x="270" y="695" fill="#ff9800" font-size="9" opacity=".5">势能↓</text>
  <text x="475" y="620" fill="#00d4ff" font-size="9" opacity=".5">转向→</text>

  <!-- 参数显示 -->
  <g id="paramDisplay" font-family="'JetBrains Mono',monospace" font-size="11">
    <text x="630" y="700" fill="#5a7a9a">偏心距 e = <tspan id="eVal" fill="#00d4ff">15</tspan> mm</text>
    <text x="630" y="720" fill="#5a7a9a">周期数 n = <tspan id="nVal" fill="#00d4ff">2</tspan></text>
    <text x="630" y="740" fill="#5a7a9a">转角 φ = <tspan id="phiVal" fill="#00d4ff">0.0</tspan>°</text>
    <text x="630" y="760" fill="#5a7a9a">转向角 δ = <tspan id="deltaVal" fill="#76ff03">0.0</tspan>°</text>
  </g>
</g>

<!-- ========== 右侧上:轨迹俯视图 ========== -->
<g id="trajectoryGroup" transform="translate(1130,250)">
  <text x="0" y="-195" fill="#5a7a9a" font-size="13" font-weight="500" text-anchor="middle" letter-spacing="2">俯 视 轨 迹</text>
  <!-- 地面网格 -->
  <g opacity=".15" stroke="#0af" stroke-width=".4">
    <line x1="-250" y1="-180" x2="-250" y2="180"/><line x1="-150" y1="-180" x2="-150" y2="180"/>
    <line x1="-50" y1="-180" x2="-50" y2="180"/><line x1="50" y1="-180" x2="50" y2="180"/>
    <line x1="150" y1="-180" x2="150" y2="180"/><line x1="250" y1="-180" x2="250" y2="180"/>
    <line x1="-250" y1="-150" x2="250" y2="-150"/><line x1="-250" y1="-100" x2="250" y2="-100"/>
    <line x1="-250" y1="-50" x2="250" y2="-50"/><line x1="-250" y1="0" x2="250" y2="0"/>
    <line x1="-250" y1="50" x2="250" y2="50"/><line x1="-250" y1="100" x2="250" y2="100"/>
    <line x1="-250" y1="150" x2="250" y2="150"/>
  </g>
  <!-- 障碍物 -->
  <g id="obstacles"></g>
  <!-- 轨迹线 -->
  <path id="trajPath" d="" fill="none" stroke="#00ff88" stroke-width="2.5" stroke-linecap="round" filter="url(#glowGreen)" opacity=".9"/>
  <!-- 车辆图标 -->
  <g id="carIcon" transform="translate(0,0)">
    <rect x="-8" y="-14" width="16" height="28" rx="3" fill="rgba(0,255,136,.15)" stroke="#00ff88" stroke-width="1.2"/>
    <line x1="0" y1="-14" x2="0" y2="-20" stroke="#00ff88" stroke-width="1.5" stroke-linecap="round"/>
    <circle cx="-5" cy="-10" r="2" fill="#00ff88" opacity=".5"/>
    <circle cx="5" cy="-10" r="2" fill="#00ff88" opacity=".5"/>
  </g>
  <!-- 方向标注 -->
  <text x="-235" y="170" fill="#3a5a7a" font-size="9">前←</text>
  <text x="220" y="170" fill="#3a5a7a" font-size="9">→后</text>
</g>

<!-- ========== 右侧下:转向角曲线图 ========== -->
<g id="graphGroup" transform="translate(1130,590)">
  <text x="0" y="-115" fill="#5a7a9a" font-size="13" font-weight="500" text-anchor="middle" letter-spacing="2">转 向 角 曲 线</text>
  <!-- 坐标轴 -->
  <line x1="-230" y1="80" x2="230" y2="80" stroke="#2a4a6a" stroke-width="1"/>
  <line x1="-230" y1="-80" x2="-230" y2="80" stroke="#2a4a6a" stroke-width="1"/>
  <!-- 轴标签 -->
  <text x="230" y="96" fill="#3a5a7a" font-size="9" text-anchor="end">φ (转角)</text>
  <text x="-222" y="-72" fill="#3a5a7a" font-size="9">δ (转向角)</text>
  <!-- 零线 -->
  <line x1="-230" y1="0" x2="230" y2="0" stroke="#2a4a6a" stroke-width=".5" stroke-dasharray="3,4"/>
  <!-- 正弦曲线 -->
  <path id="sinCurve" d="" fill="none" stroke="#00ff88" stroke-width="1.8" opacity=".7"/>
  <!-- 当前点 -->
  <circle id="sinDot" cx="0" cy="0" r="5" fill="#00ff88" filter="url(#glowGreen)"/>
  <!-- 竖线指示 -->
  <line id="sinVLine" x1="0" y1="-80" x2="0" y2="80" stroke="#00ff88" stroke-width=".5" opacity=".3" stroke-dasharray="2,3"/>
  <!-- Y轴刻度 -->
  <text x="-240" y="4" fill="#3a5a7a" font-size="8" text-anchor="end">0</text>
  <text id="yMaxLabel" x="-240" y="-72" fill="#3a5a7a" font-size="8" text-anchor="end">+δmax</text>
  <text id="yMinLabel" x="-240" y="84" fill="#3a5a7a" font-size="8" text-anchor="end">-δmax</text>
</g>

<!-- 状态指示 -->
<g id="stateIndicator" transform="translate(630,790)">
  <rect x="-100" y="-14" width="200" height="28" rx="4" fill="rgba(0,180,255,.06)" stroke="rgba(0,180,255,.15)" stroke-width="1"/>
  <text id="stateText" x="0" y="4" fill="#00d4ff" font-size="12" font-weight="600" text-anchor="middle" font-family="'JetBrains Mono',monospace">● 运行中</text>
</g>

</svg>
</div>

<!-- 控制面板 -->
<div class="controls">
  <div class="ctrl-group">
    <label>偏心距 e</label>
    <input type="range" id="eSlider" min="5" max="30" value="15" step="1">
    <span class="val" id="eDisplay">15 mm</span>
  </div>
  <div class="ctrl-group">
    <label>周期数 n</label>
    <input type="range" id="nSlider" min="1" max="4" value="2" step="1">
    <span class="val" id="nDisplay">2</span>
  </div>
  <div class="ctrl-group">
    <label>速度</label>
    <input type="range" id="speedSlider" min="0.1" max="3" value="1" step="0.1">
    <span class="val" id="speedDisplay">1.0x</span>
  </div>
  <div class="sep"></div>
  <button class="btn active" id="playBtn">暂停</button>
  <button class="btn" id="resetBtn">重置</button>
  <div class="sep"></div>
  <div class="ctrl-group">
    <label>弹簧刚度</label>
    <input type="range" id="springSlider" min="0.2" max="2" value="1" step="0.1">
    <span class="val" id="springDisplay">1.0</span>
  </div>
</div>

<script>
// ===== 配置 =====
const C = {
  wheel: { cx: 380, cy: 230, R: 65 },
  leftPulley:  { cx: 120, cy: 230, r: 14 },
  rightPulley: { cx: 640, cy: 230, r: 14 },
  arm: { cx: 380, cy: 490, halfLen: 100 },
  frontWheel: { cx: 380, cy: 610, r: 30 },
  weightYStart: 355,
  weightYEnd: 640,
  weightH: 52,
  trajCenter: { x: 1130, y: 250 },
  graphCenter: { x: 1130, y: 590 }
};

// ===== 状态 =====
const S = {
  time: 0,
  playing: true,
  speed: 1,
  e: 15,        // 偏心距
  n: 2,         // 周期数
  springK: 1,   // 弹簧刚度
  wheelAngle: 0,
  steerDeg: 0,
  leftTension: 0.5,
  rightTension: 0.5,
  trajPoints: [],
  maxTrajPoints: 600,
  weightProgress: 0
};

// ===== DOM引用 =====
const $ = id => document.getElementById(id);

// ===== 工具函数 =====
function wheelR(theta) {
  return C.wheel.R + S.e * Math.sin(S.n * theta);
}

function wheelProfilePathStr(rotation) {
  const steps = 200;
  let d = '';
  for (let i = 0; i <= steps; i++) {
    const theta = (i / steps) * Math.PI * 2;
    const r = wheelR(theta);
    const x = r * Math.cos(theta + rotation);
    const y = r * Math.sin(theta + rotation);
    d += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
  }
  return d + 'Z';
}

// 计算绕线轮上某参数角对应的世界坐标
function wheelPoint(theta, rotation) {
  const r = wheelR(theta);
  return {
    x: C.wheel.cx + r * Math.cos(theta + rotation),
    y: C.wheel.cy + r * Math.sin(theta + rotation),
    r: r
  };
}

// 生成弹簧路径
function springPathStr(x1, y1, x2, y2, coils, amp) {
  const dx = x2 - x1, dy = y2 - y1;
  const len = Math.sqrt(dx*dx + dy*dy);
  if (len < 1) return `M${x1},${y1}L${x2},${y2}`;
  const ux = dx/len, uy = dy/len;
  const px = -uy, py = ux;
  const lead = Math.min(12, len * 0.12);
  let d = `M${x1},${y1}`;
  const sx = x1 + ux*lead, sy = y1 + uy*lead;
  d += `L${sx.toFixed(1)},${sy.toFixed(1)}`;
  const segLen = (len - 2*lead) / (coils * 2);
  for (let i = 0; i < coils * 2; i++) {
    const t = lead + (i + 1) * segLen;
    const bx = x1 + ux * t + px * amp * (i % 2 === 0 ? 1 : -1);
    const by = y1 + uy * t + py * amp * (i % 2 === 0 ? 1 : -1);
    d += `L${bx.toFixed(1)},${by.toFixed(1)}`;
  }
  const ex = x2 - ux*lead, ey = y2 - uy*lead;
  d += `L${ex.toFixed(1)},${ey.toFixed(1)}`;
  d += `L${x2},${y2}`;
  return d;
}

// ===== 初始化 =====
function initObstacles() {
  const g = $('obstacles');
  g.innerHTML = '';
  const positions = [-120, -40, 40, 120];
  const sides = [-1, 1, -1, 1];
  positions.forEach((yBase, i) => {
    const xOff = sides[i] * 60;
    const cx = xOff, cy = yBase;
    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', cx);
    circle.setAttribute('cy', cy);
    circle.setAttribute('r', '12');
    circle.setAttribute('fill', 'rgba(255,100,50,.15)');
    circle.setAttribute('stroke', '#ff6432');
    circle.setAttribute('stroke-width', '1.2');
    circle.setAttribute('stroke-dasharray', '3,2');
    g.appendChild(circle);
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    text.setAttribute('x', cx);
    text.setAttribute('y', cy + 22);
    text.setAttribute('fill', '#ff6432');
    text.setAttribute('font-size', '8');
    text.setAttribute('text-anchor', 'middle');
    text.setAttribute('opacity', '0.6');
    text.textContent = '障碍' + (i+1);
    g.appendChild(text);
  });
}

function initWheelSpokes() {
  const g = $('wheelSpokes');
  g.innerHTML = '';
  for (let i = 0; i < 6; i++) {
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', '0');
    line.setAttribute('y1', '0');
    line.setAttribute('stroke', '#0af');
    line.setAttribute('stroke-width', '0.6');
    line.setAttribute('opacity', '0.3');
    line.classList.add('spoke');
    g.appendChild(line);
  }
}

function initTireTreads() {
  const g = $('tireTreads');
  g.innerHTML = '';
  for (let i = 0; i < 8; i++) {
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('stroke', '#4a6a8a');
    line.setAttribute('stroke-width', '1');
    line.setAttribute('opacity', '0.4');
    line.classList.add('tread');
    g.appendChild(line);
  }
}

// ===== 更新函数 =====
function update(dt) {
  if (!S.playing) return;

  const effectiveDt = dt * S.speed;
  S.time += effectiveDt;

  // 轮子匀速旋转(对应重物匀速下落)
  const omega = 0.5; // 基础角速度 rad/s
  S.wheelAngle += omega * effectiveDt;

  // 重物下落进度(每转一圈重物下落一段距离,循环)
  const cycleAngle = Math.PI * 2 * S.n; // n个完整周期对应的转角
  S.weightProgress = ((S.wheelAngle % cycleAngle) / cycleAngle);

  // 转向角 = A * sin(n * φ),A与偏心距成正比
  const maxSteerRad = (S.e / C.wheel.R) * 0.45; // 最大转向角(弧度)
  const springDamp = 1 / (1 + (S.springK - 1) * 0.15); // 弹簧刚度影响
  S.steerRad = maxSteerRad * Math.sin(S.n * S.wheelAngle) * springDamp;
  S.steerDeg = S.steerRad * 180 / Math.PI;

  // 左右绳索张力(用于视觉)
  S.leftTension = 0.5 + 0.5 * Math.sin(S.n * S.wheelAngle);
  S.rightTension = 0.5 - 0.5 * Math.sin(S.n * S.wheelAngle);

  // 更新轨迹点
  updateTrajectory();
}

function updateTrajectory() {
  // 俯视轨迹:车辆前进方向为Y负方向,转向影响X偏移
  const vForward = 1.2; // 前进速度
  const dt = 0.02 * S.speed;
  const heading = S.steerRad * 2.5; // 放大转向效果

  if (S.trajPoints.length === 0) {
    S.trajPoints.push({ x: 0, y: 160, heading: 0 });
  }

  const last = S.trajPoints[S.trajPoints.length - 1];
  const nx = last.x + Math.sin(heading) * vForward;
  const ny = last.y - Math.cos(heading) * vForward;

  // 限制范围
  if (ny > -185 && ny < 185 && nx > -260 && nx < 260) {
    S.trajPoints.push({ x: nx, y: ny, heading: heading });
  }

  if (S.trajPoints.length > S.maxTrajPoints) {
    S.trajPoints.shift();
  }
}

function render() {
  const phi = S.wheelAngle;

  // === 非圆绕线轮 ===
  $('wheelProfile').setAttribute('d', wheelProfilePathStr(phi));

  // 辐条
  const spokes = $('wheelSpokes').querySelectorAll('.spoke');
  spokes.forEach((spoke, i) => {
    const theta = (i / 6) * Math.PI * 2;
    const r = wheelR(theta);
    const ex = r * Math.cos(theta + phi);
    const ey = r * Math.sin(theta + phi);
    spoke.setAttribute('x2', ex.toFixed(1));
    spoke.setAttribute('y2', ey.toFixed(1));
  });

  // 左右标记点(峰值/谷值位置)
  // 左绳对应参数角 π/(2n)(峰值),右绳对应 3π/(2n)(谷值)
  const leftTheta = Math.PI / (2 * S.n);
  const rightTheta = 3 * Math.PI / (2 * S.n);
  const lp = wheelPoint(leftTheta, phi);
  const rp = wheelPoint(rightTheta, phi);

  $('leftMark').setAttribute('cx', (lp.x - C.wheel.cx).toFixed(1));
  $('leftMark').setAttribute('cy', (lp.y - C.wheel.cy).toFixed(1));
  $('rightMark').setAttribute('cx', (rp.x - C.wheel.cx).toFixed(1));
  $('rightMark').setAttribute('cy', (rp.y - C.wheel.cy).toFixed(1));

  // 有效半径指示线
  $('leftRadiusLine').setAttribute('x2', (lp.x - C.wheel.cx).toFixed(1));
  $('leftRadiusLine').setAttribute('y2', (lp.y - C.wheel.cy).toFixed(1));
  $('rightRadiusLine').setAttribute('x2', (rp.x - C.wheel.cx).toFixed(1));
  $('rightRadiusLine').setAttribute('y2', (rp.y - C.wheel.cy).toFixed(1));

  // 标记点大小随半径变化
  const leftR = wheelR(leftTheta);
  const rightR = wheelR(rightTheta);
  const baseR = C.wheel.R;
  $('leftMark').setAttribute('r', (3 + 2 * (leftR - baseR + S.e) / (2 * S.e)).toFixed(1));
  $('rightMark').setAttribute('r', (3 + 2 * (rightR - baseR + S.e) / (2 * S.e)).toFixed(1));

  // === 左牵引绳 ===
  const armAngle = S.steerRad;
  const armLeftX = C.arm.cx - C.arm.halfLen * Math.cos(armAngle);
  const armLeftY = C.arm.cy - C.arm.halfLen * Math.sin(armAngle);
  const armRightX = C.arm.cx + C.arm.halfLen * Math.cos(armAngle);
  const armRightY = C.arm.cy + C.arm.halfLen * Math.sin(armAngle);

  // 左绳:轮上标记点 → 左滑轮顶部 → 转向臂左端
  const lPulleyTop = { x: C.leftPulley.cx, y: C.leftPulley.cy - C.leftPulley.r };
  const lPulleyBot = { x: C.leftPulley.cx, y: C.leftPulley.cy + C.leftPulley.r };
  const leftRopePoints = [
    `${lp.x},${lp.y}`,
    `${lPulleyTop.x},${lPulleyTop.y}`,
    `${lPulleyBot.x},${lPulleyBot.y}`,
    `${armLeftX},${armLeftY}`
  ];
  $('leftRope').setAttribute('points', leftRopePoints.join(' '));
  $('leftRope').setAttribute('opacity', (0.3 + 0.7 * S.leftTension).toFixed(2));
  $('leftRope').setAttribute('stroke-width', (1.2 + 1.3 * S.leftTension).toFixed(1));

  // 右绳
  const rPulleyTop = { x: C.rightPulley.cx, y: C.rightPulley.cy - C.rightPulley.r };
  const rPulleyBot = { x: C.rightPulley.cx, y: C.rightPulley.cy + C.rightPulley.r };
  const rightRopePoints = [
    `${rp.x},${rp.y}`,
    `${rPulleyTop.x},${rPulleyTop.y}`,
    `${rPulleyBot.x},${rPulleyBot.y}`,
    `${armRightX},${armRightY}`
  ];
  $('rightRope').setAttribute('points', rightRopePoints.join(' '));
  $('rightRope').setAttribute('opacity', (0.3 + 0.7 * S.rightTension).toFixed(2));
  $('rightRope').setAttribute('stroke-width', (1.2 + 1.3 * S.rightTension).toFixed(1));

  // 滑轮旋转
  const lPulleyAngle = phi * 30;
  const rPulleyAngle = -phi * 30;
  $('leftPulleySpoke').setAttribute('transform', `rotate(${lPulleyAngle})`);
  $('rightPulleySpoke').setAttribute('transform', `rotate(${rPulleyAngle})`);

  // === 主绳 + 重物 ===
  const weightY = C.weightYStart + S.weightProgress * (C.weightYEnd - C.weightYStart);
  const wheelBottom = C.wheel.cy + C.wheel.R + S.e + 5;
  $('mainRope').setAttribute('x1', C.wheel.cx);
  $('mainRope').setAttribute('y1', wheelBottom);
  $('mainRope').setAttribute('x2', C.wheel.cx);
  $('mainRope').setAttribute('y2', weightY);

  $('weightGroup').setAttribute('transform', `translate(${C.wheel.cx},${weightY})`);
  $('weightText').setAttribute('x', C.wheel.cx + 30);
  $('weightText').setAttribute('y', weightY + 30);
  $('mainRopeLabel').setAttribute('x', C.wheel.cx + 12);
  $('mainRopeLabel').setAttribute('y', (wheelBottom + weightY) / 2);

  // === 转向臂 ===
  $('steeringArmGroup').setAttribute('transform',
    `translate(${C.arm.cx},${C.arm.cy}) rotate(${S.steerDeg})`);

  // 弹簧:从枢轴向上到固定点
  const springTop = { x: 0, y: -50 };
  const springBot = { x: 0, y: 0 };
  // 弹簧随转向偏移
  const springOffset = S.steerRad * 15;
  $('springPath').setAttribute('d',
    springPathStr(springTop.x, springTop.y, springBot.x + springOffset, springBot.y, 6, 8));

  // === 前轮 ===
  $('frontWheelGroup').setAttribute('transform',
    `translate(${C.frontWheel.cx},${C.frontWheel.cy}) rotate(${S.steerDeg})`);
  $('wheelDirLine').setAttribute('transform', `rotate(0)`);

  // 轮胎纹
  const treads = $('tireTreads').querySelectorAll('.tread');
  treads.forEach((t, i) => {
    const a = (i / 8) * 360;
    const r1 = C.frontWheel.r - 4;
    const r2 = C.frontWheel.r + 1;
    const rad = a * Math.PI / 180;
    t.setAttribute('x1', (r1 * Math.cos(rad)).toFixed(1));
    t.setAttribute('y1', (r1 * Math.sin(rad)).toFixed(1));
    t.setAttribute('x2', (r2 * Math.cos(rad)).toFixed(1));
    t.setAttribute('y2', (r2 * Math.sin(rad)).toFixed(1));
  });

  // === 俯视轨迹 ===
  if (S.trajPoints.length > 1) {
    let d = `M${S.trajPoints[0].x.toFixed(1)},${S.trajPoints[0].y.toFixed(1)}`;
    for (let i = 1; i < S.trajPoints.length; i++) {
      d += `L${S.trajPoints[i].x.toFixed(1)},${S.trajPoints[i].y.toFixed(1)}`;
    }
    $('trajPath').setAttribute('d', d);

    const last = S.trajPoints[S.trajPoints.length - 1];
    const heading = last.heading * 180 / Math.PI;
    $('carIcon').setAttribute('transform', `translate(${last.x.toFixed(1)},${last.y.toFixed(1)}) rotate(${heading.toFixed(1)})`);
  }

  // === 转向角曲线图 ===
  renderGraph();

  // === 参数显示 ===
  $('eVal').textContent = S.e;
  $('nVal').textContent = S.n;
  $('phiVal').textContent = ((S.wheelAngle * 180 / Math.PI) % 360).toFixed(1);
  $('deltaVal').textContent = S.steerDeg.toFixed(1);

  // 绳索标注位置
  const lrx = (lp.x + lPulleyTop.x) / 2;
  const lry = (lp.y + lPulleyTop.y) / 2 - 12;
  $('leftRopeLabel').setAttribute('x', lrx);
  $('leftRopeLabel').setAttribute('y', lry);
  $('leftRopeLabel').setAttribute('opacity', (0.4 + 0.6 * S.leftTension).toFixed(2));

  const rrx = (rp.x + rPulleyTop.x) / 2;
  const rry = (rp.y + rPulleyTop.y) / 2 - 12;
  $('rightRopeLabel').setAttribute('x', rrx);
  $('rightRopeLabel').setAttribute('y', rry);
  $('rightRopeLabel').setAttribute('opacity', (0.4 + 0.6 * S.rightTension).toFixed(2));
}

function renderGraph() {
  const gw = 230, gh = 80;
  const maxSteerDeg = (S.e / C.wheel.R) * 0.45 * 180 / Math.PI;

  // 正弦曲线
  let d = '';
  for (let i = 0; i <= 200; i++) {
    const t = i / 200;
    const x = -gw + t * 2 * gw;
    const angle = t * 2 * Math.PI * S.n;
    const y = -gh * Math.sin(angle) / (1 + (S.springK - 1) * 0.15);
    d += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + (y * 0.9).toFixed(1);
  }
  $('sinCurve').setAttribute('d', d);

  // 当前点
  const currentT = ((S.wheelAngle % (2 * Math.PI * S.n)) / (2 * Math.PI * S.n));
  const dotX = -gw + currentT * 2 * gw;
  const dotAngle = currentT * 2 * Math.PI * S.n;
  const dotY = -gh * Math.sin(dotAngle) * 0.9 / (1 + (S.springK - 1) * 0.15);
  $('sinDot').setAttribute('cx', dotX.toFixed(1));
  $('sinDot').setAttribute('cy', dotY.toFixed(1));
  $('sinVLine').setAttribute('x1', dotX.toFixed(1));
  $('sinVLine').setAttribute('x2', dotX.toFixed(1));

  $('yMaxLabel').textContent = `+${maxSteerDeg.toFixed(1)}°`;
  $('yMinLabel').textContent = `-${maxSteerDeg.toFixed(1)}°`;
}

// ===== 主循环 =====
let lastTime = 0;
function animate(timestamp) {
  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;
  update(dt);
  render();
  requestAnimationFrame(animate);
}

// ===== 交互控制 =====
$('eSlider').addEventListener('input', e => {
  S.e = parseInt(e.target.value);
  $('eDisplay').textContent = S.e + ' mm';
});

$('nSlider').addEventListener('input', e => {
  S.n = parseInt(e.target.value);
  $('nDisplay').textContent = S.n;
  S.trajPoints = [];
  initObstacles();
});

$('speedSlider').addEventListener('input', e => {
  S.speed = parseFloat(e.target.value);
  $('speedDisplay').textContent = S.speed.toFixed(1) + 'x';
});

$('springSlider').addEventListener('input', e => {
  S.springK = parseFloat(e.target.value);
  $('springDisplay').textContent = S.springK.toFixed(1);
});

$('playBtn').addEventListener('click', () => {
  S.playing = !S.playing;
  $('playBtn').textContent = S.playing ? '暂停' : '播放';
  $('playBtn').classList.toggle('active', S.playing);
  $('stateText').textContent = S.playing ? '● 运行中' : '○ 已暂停';
  $('stateText').setAttribute('fill', S.playing ? '#00d4ff' : '#5a7a9a');
});

$('resetBtn').addEventListener('click', () => {
  S.time = 0;
  S.wheelAngle = 0;
  S.trajPoints = [];
  S.weightProgress = 0;
});

// ===== 启动 =====
initObstacles();
initWheelSpokes();
initTireTreads();
requestAnimationFrame(animate);
</script>
</body>
</html>

实现说明:

这是一个完整的非圆轮廓绕线轮转向耦合机构的高保真 SVG 原理动画,核心设计思路如下:

  1. IFR 理想解聚焦:动画直接展示最终理想解的运行状态——非圆轮廓的几何形状本身即转向函数,势能直接转化为正弦转向,无中间传动环节。左下角「核心创新」标注框明确点明这一点。

  2. 视觉引导机制

    • 绳索差动可视化:左绳(青色)与右绳(品红)的粗细和透明度随张力实时变化——拉紧侧高亮加粗,放松侧变暗变细,直观呈现「一拉一松」的差动原理
    • 非圆轮廓:绕线轮的波浪形轮廓持续旋转,两个标记点(峰/谷)随轮廓径向脉动,半径指示线动态伸缩
    • 能量流向:虚线箭头从重物经轮到转向臂,标示能量传递路径
  3. 三大联动视图

    • 左:机构原理图 — 非圆轮旋转、重物下落、滑轮转向、转向臂摆动、回位弹簧伸缩、前轮偏转,全部实时联动
    • 右上:俯视轨迹 — 车辆沿 S 型曲线实时行驶,4 个障碍物标注于旁
    • 右下:转向角曲线 — 正弦曲线图实时描绘,当前相位点高亮追踪
  4. 交互控制:偏心距 e(影响转向幅度)、周期数 n(影响 S 弯数量)、运行速度、弹簧刚度(模拟刚度失配带来的转向滞后)均可实时调节,让用户亲手体验参数对理想解行为的影响。

积分规则:第一轮对话扣减6分,后续每轮扣4分