分享图
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@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#050910;--card:#0a1018;--border:#162030;
  --text:#b8c8d8;--dim:#4a6080;
  --green:#00e896;--orange:#ff8c2e;--pink:#ff2d6b;
  --gold:#ffc800;--blue:#00aaff;--teal:#00ddc8;
  --chassis-fill:#1a2538;--chassis-stroke:#3a5575;
  --wheel-fill:#2a3848;--wheel-stroke:#5a7898;
}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans SC',sans-serif;overflow:hidden;height:100vh;display:flex;flex-direction:column}
#svgWrap{flex:1;display:flex;justify-content:center;align-items:center;min-height:0}
#mainSvg{width:100%;height:100%;max-height:calc(100vh - 120px)}
#controls{background:var(--card);border-top:1px solid var(--border);padding:12px 28px;display:flex;align-items:center;gap:24px;flex-wrap:wrap}
.cg{display:flex;align-items:center;gap:8px}
.cl{font-size:12px;color:var(--dim);white-space:nowrap;font-weight:500}
.cv{font-family:'Rajdhani',monospace;font-size:14px;font-weight:700;min-width:48px;text-align:right}
input[type=range]{-webkit-appearance:none;width:180px;height:5px;background:var(--border);border-radius:3px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;cursor:pointer}
#steerSlider::-webkit-slider-thumb{background:var(--green);box-shadow:0 0 8px var(--green)}
#loadSlider::-webkit-slider-thumb{background:var(--gold);box-shadow:0 0 8px var(--gold)}
.btn{padding:5px 14px;border:1px solid var(--border);background:transparent;color:var(--text);border-radius:5px;cursor:pointer;font-size:12px;font-family:'Noto Sans SC',sans-serif;transition:all .3s}
.btn:hover{border-color:var(--teal)}
.btn.on{border-color:var(--blue);background:rgba(0,170,255,.12);color:var(--blue);box-shadow:0 0 10px rgba(0,170,255,.2)}
.sep{width:1px;height:24px;background:var(--border)}
</style>
</head>
<body>
<div id="svgWrap">
<svg id="mainSvg" viewBox="0 0 1400 880" preserveAspectRatio="xMidYMid meet">
<defs>
  <!-- 网格图案 -->
  <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
    <path d="M40 0L0 0 0 40" fill="none" stroke="#0e1828" stroke-width=".5"/>
  </pattern>
  <!-- 发光滤镜 -->
  <filter id="glow"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="glowStrong"><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="softGlow"><feGaussianBlur stdDeviation="12" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <!-- 箭头标记 -->
  <marker id="arrowGold" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--gold)" opacity=".8"/></marker>
  <marker id="arrowPink" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--pink)" opacity=".8"/></marker>
  <marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="var(--blue)" opacity=".7"/></marker>
  <!-- 橡胶纹理 -->
  <pattern id="rubberTex" width="6" height="6" patternUnits="userSpaceOnUse">
    <rect width="6" height="6" fill="#cc2255"/>
    <circle cx="3" cy="3" r="1" fill="#aa1144" opacity=".5"/>
  </pattern>
  <!-- 剪裁区域 - 放大镜 -->
  <clipPath id="zoomClip"><circle cx="1180" cy="180" r="130"/></clipPath>
</defs>

<!-- 背景 -->
<rect width="1400" height="880" fill="#050910"/>
<rect width="1400" height="880" fill="url(#grid)" opacity=".6"/>

<!-- 标题 -->
<text x="40" y="38" font-family="Rajdhani,sans-serif" font-size="22" font-weight="700" fill="var(--teal)" letter-spacing="2">ARTICULATED PASSIVE STEERING</text>
<text x="40" y="58" font-family="Noto Sans SC,sans-serif" font-size="13" fill="var(--dim)">4轴8轮平台 · 铰接式随动转向 · IFR原理演示</text>

<!-- 转弯半径可视化 -->
<g id="radiusGroup"></g>

<!-- 后段车架 -->
<g id="rearChassis"></g>

<!-- 铰接点 -->
<g id="hingeGroup"></g>

<!-- 前段车架 -->
<g id="frontChassis"></g>

<!-- 力流箭头 -->
<g id="forceFlow"></g>

<!-- 角度指示器 -->
<g id="angleIndicators"></g>

<!-- 标注 -->
<g id="annotations"></g>

<!-- 放大镜 -->
<g id="zoomInset"></g>

<!-- 参数面板 -->
<g id="paramPanel"></g>
</svg>
</div>

<div id="controls">
  <div class="cg">
    <span class="cl">转向角</span>
    <input type="range" id="steerSlider" min="-30" max="30" value="0" step="0.5">
    <span class="cv" id="steerVal" style="color:var(--green)">0.0°</span>
  </div>
  <div class="sep"></div>
  <div class="cg">
    <span class="cl">载重</span>
    <input type="range" id="loadSlider" min="0" max="100" value="80" step="1">
    <span class="cv" id="loadVal" style="color:var(--gold)">80%</span>
  </div>
  <div class="sep"></div>
  <button class="btn" id="autoBtn">自动演示</button>
  <button class="btn" id="resetBtn">复位</button>
  <div class="sep"></div>
  <div class="cg">
    <span class="cl" style="color:var(--dim);font-size:11px">随动轮偏转上限 ±5° | 橡胶块邵氏硬度 70-80A</span>
  </div>
</div>

<script>
const SVG_NS = 'http://www.w3.org/2000/svg';
const HINGE_X = 680, HINGE_Y = 430;

// 车辆尺寸参数
const C_LENGTH = 330, C_HALF_W = 72;
const W_LEN = 52, W_WID = 20;
const WHEEL_OFFSET = 92;
const AXLE_OFFSETS_FRONT = [290, 150]; // 前段两轴距铰接点
const AXLE_OFFSETS_REAR = [-150, -290]; // 后段两轴距铰接点

// 状态
let steerAngle = 0, loadPct = 80, isAuto = false, autoTime = 0;
let animId = null;

// SVG辅助
function el(tag, attrs, parent) {
  const e = document.createElementNS(SVG_NS, tag);
  if (attrs) Object.entries(attrs).forEach(([k,v]) => e.setAttribute(k, v));
  if (parent) parent.appendChild(e);
  return e;
}

function rotateTransform(angle, cx, cy) {
  return `rotate(${angle},${cx},${cy})`;
}

function computeState() {
  const loadFactor = loadPct / 100;
  const frontAngle = steerAngle * 0.52;
  const hingeDeform = frontAngle * loadFactor * 0.48;
  const rearAngle = frontAngle - hingeDeform;
  const rearDeflect = Math.sign(hingeDeform) * Math.min(5, Math.abs(hingeDeform) * 0.38) * loadFactor;
  return { frontAngle, hingeDeform, rearAngle, rearDeflect, loadFactor };
}

// ============ 构建场景 ============
const svg = document.getElementById('mainSvg');
const rearGroup = document.getElementById('rearChassis');
const frontGroup = document.getElementById('frontChassis');
const hingeGroupEl = document.getElementById('hingeGroup');
const forceGroup = document.getElementById('forceFlow');
const angleGroup = document.getElementById('angleIndicators');
const annoGroup = document.getElementById('annotations');
const zoomGroup = document.getElementById('zoomInset');
const radiusGroup = document.getElementById('radiusGroup');
const paramGroup = document.getElementById('paramPanel');

// --- 后段车架 ---
function buildChassis(group, xStart, xEnd, isRear) {
  const y1 = HINGE_Y - C_HALF_W, y2 = HINGE_Y + C_HALF_W;
  const w = xEnd - xStart;
  
  // 主框架
  el('rect', {
    x: xStart + 8, y: y1 + 6, width: w - 16, height: C_HALF_W * 2 - 12,
    rx: 6, fill: 'var(--chassis-fill)', stroke: 'var(--chassis-stroke)', 'stroke-width': 1.5
  }, group);
  
  // 内部斜撑(激光切割风格)
  const step = 45;
  for (let x = xStart + 30; x < xEnd - 20; x += step) {
    el('line', { x1: x, y1: y1 + 10, x2: x + 25, y2: y2 - 10, stroke: '#1e3048', 'stroke-width': .8 }, group);
  }
  
  // 边框加强
  el('rect', {
    x: xStart + 4, y: y1 + 3, width: w - 8, height: C_HALF_W * 2 - 6,
    rx: 5, fill: 'none', stroke: '#2a4060', 'stroke-width': .8, 'stroke-dasharray': '4,3'
  }, group);
  
  // 车架端部标记
  if (!isRear) {
    // 前段前端 - 牵引指示
    el('path', { d: `M${xEnd-4},${HINGE_Y-12} L${xEnd+8},${HINGE_Y} L${xEnd-4},${HINGE_Y+12}`, fill: 'none', stroke: 'var(--green)', 'stroke-width': 1.5, opacity: .6 }, group);
  }
  
  // 轮轴和车轮
  const offsets = isRear ? AXLE_OFFSETS_REAR : AXLE_OFFSETS_FRONT;
  offsets.forEach((off, idx) => {
    const ax = HINGE_X + off;
    const axleIdx = isRear ? idx + 2 : idx;
    
    // 轮轴线
    el('line', {
      x1: ax, y1: y1 - 8, x2: ax, y2: y2 + 8,
      stroke: '#3a5570', 'stroke-width': 2
    }, group);
    
    // 左右车轮
    [-1, 1].forEach(side => {
      const wy = HINGE_Y + side * WHEEL_OFFSET;
      const wheelG = el('g', {
        class: isRear ? 'rear-wheel' : 'front-wheel',
        'data-cx': ax, 'data-cy': wy, 'data-idx': axleIdx, 'data-side': side
      }, group);
      
      // 轮胎
      el('rect', {
        x: ax - W_LEN / 2, y: wy - W_WID / 2, width: W_LEN, height: W_WID,
        rx: 4, fill: isRear ? '#1e2e40' : '#1e3028',
        stroke: isRear ? 'var(--orange)' : 'var(--green)', 'stroke-width': 1.8
      }, wheelG);
      
      // 轮胎花纹
      for (let i = -18; i <= 18; i += 8) {
        el('line', {
          x1: ax + i, y1: wy - W_WID / 2 + 3, x2: ax + i, y2: wy + W_WID / 2 - 3,
          stroke: isRear ? '#2a4058' : '#2a4838', 'stroke-width': .6
        }, wheelG);
      }
      
      // 销轴点
      el('circle', { cx: ax, cy: wy, r: 3.5, fill: isRear ? 'var(--orange)' : 'var(--green)', opacity: .9 }, wheelG);
      el('circle', { cx: ax, cy: wy, r: 1.5, fill: '#0a1018' }, wheelG);
      
      // 后轮的复位橡胶圈指示
      if (isRear) {
        el('circle', {
          cx: ax, cy: wy, r: 7, fill: 'none',
          stroke: 'var(--pink)', 'stroke-width': 1.2, 'stroke-dasharray': '2,2', opacity: .7
        }, wheelG);
      }
    });
  });
}

buildChassis(rearGroup, HINGE_X - C_LENGTH, HINGE_X, true);
buildChassis(frontGroup, HINGE_X, HINGE_X + C_LENGTH, false);

// --- 铰接点 ---
function buildHinge() {
  // 外圈
  el('circle', { cx: HINGE_X, cy: HINGE_Y, r: 28, fill: '#0c1520', stroke: '#3a5570', 'stroke-width': 2 }, hingeGroupEl);
  
  // 万向节十字
  el('line', { x1: HINGE_X - 18, y1: HINGE_Y, x2: HINGE_X + 18, y2: HINGE_Y, stroke: '#6a8aaa', 'stroke-width': 3 }, hingeGroupEl);
  el('line', { x1: HINGE_X, y1: HINGE_Y - 18, x2: HINGE_X, y2: HINGE_Y + 18, stroke: '#6a8aaa', 'stroke-width': 3 }, hingeGroupEl);
  
  // 中心
  el('circle', { cx: HINGE_X, cy: HINGE_Y, r: 6, fill: '#5a7a9a', stroke: '#8ab0d0', 'stroke-width': 1 }, hingeGroupEl);
  
  // 橡胶块 - 4个象限
  const rubberColors = ['#ff2d6b', '#e82660', '#ff2d6b', '#e82660'];
  const rubberPositions = [
    { x: HINGE_X - 14, y: HINGE_Y - 14 }, { x: HINGE_X + 8, y: HINGE_Y - 14 },
    { x: HINGE_X - 14, y: HINGE_Y + 8 },  { x: HINGE_X + 8, y: HINGE_Y + 8 }
  ];
  rubberPositions.forEach((pos, i) => {
    const rb = el('rect', {
      x: pos.x, y: pos.y, width: 6, height: 6, rx: 1,
      fill: rubberColors[i], class: 'rubber-block', opacity: .8
    }, hingeGroupEl);
  });
  
  // 发光圈
  el('circle', {
    cx: HINGE_X, cy: HINGE_Y, r: 32, fill: 'none',
    stroke: 'var(--pink)', 'stroke-width': 1, opacity: .3, class: 'hinge-glow',
    filter: 'url(#glow)'
  }, hingeGroupEl);
}
buildHinge();

// --- 标注 ---
function buildAnnotations() {
  const labels = [
    { x: HINGE_X + 310, y: HINGE_Y - 105, text: '前轮主动转向', color: 'var(--green)', lineTo: [HINGE_X + 290, HINGE_Y - 70] },
    { x: HINGE_X - 350, y: HINGE_Y - 105, text: '后轮随动偏转', color: 'var(--orange)', lineTo: [HINGE_X - 290, HINGE_Y - 70] },
    { x: HINGE_X, y: HINGE_Y + 75, text: '万向节+橡胶扭力块', color: 'var(--pink)', lineTo: [HINGE_X, HINGE_Y + 35], anchor: 'middle' },
  ];
  
  labels.forEach(l => {
    // 连接线
    el('line', {
      x1: l.anchor === 'middle' ? l.x : (l.lineTo[0] > l.x ? l.x + 90 : l.x - 10),
      y1: l.y - 4, x2: l.lineTo[0], y2: l.lineTo[1],
      stroke: l.color, 'stroke-width': .8, opacity: .5, 'stroke-dasharray': '3,3'
    }, annoGroup);
    // 文字
    el('text', {
      x: l.x, y: l.y,
      'font-family': 'Noto Sans SC,sans-serif', 'font-size': 12, 'font-weight': 500,
      fill: l.color, 'text-anchor': l.anchor || 'start', opacity: .85
    }, annoGroup).textContent = l.text;
  });
  
  // 载重指示
  const loadLabel = el('text', {
    x: HINGE_X, y: HINGE_Y + C_HALF_W + 50,
    'font-family': 'Rajdhani,monospace', 'font-size': 14, 'font-weight': 600,
    fill: 'var(--gold)', 'text-anchor': 'middle', class: 'load-label', opacity: .7
  }, annoGroup);
  loadLabel.textContent = '▼ 载重区 ▼';
  
  // 载重块
  for (let i = 0; i < 5; i++) {
    el('rect', {
      x: HINGE_X - 130 + i * 55, y: HINGE_Y + C_HALF_W + 14,
      width: 48, height: 18, rx: 2,
      fill: '#1a1a10', stroke: 'var(--gold)', 'stroke-width': .6, opacity: .4,
      class: 'load-block'
    }, annoGroup);
  }
}
buildAnnotations();

// --- 放大镜 ---
function buildZoomInset() {
  // 背景圆
  el('circle', { cx: 1180, cy: 180, r: 134, fill: '#080e18', stroke: '#2a3a55', 'stroke-width': 2 }, zoomGroup);
  
  // 放大内容(剪裁)
  const zoomContent = el('g', { 'clip-path': 'url(#zoomClip)' }, zoomGroup);
  
  // 放大比例 3.5x,中心对齐到铰接点
  const scale = 3.5;
  const zoomG = el('g', { class: 'zoom-content' }, zoomContent);
  
  // 在放大镜中重新绘制铰接点细节
  const zCx = 1180, zCy = 180;
  
  // 万向节外框
  el('circle', { cx: zCx, cy: zCy, r: 60, fill: '#0a1520', stroke: '#3a5570', 'stroke-width': 2 }, zoomG);
  
  // 十字轴
  el('line', { x1: zCx - 45, y1: zCy, x2: zCx + 45, y2: zCy, stroke: '#7a9aba', 'stroke-width': 5 }, zoomG);
  el('line', { x1: zCx, y1: zCy - 45, x2: zCx, y2: zCy + 45, stroke: '#7a9aba', 'stroke-width': 5 }, zoomG);
  
  // 中心轴
  el('circle', { cx: zCx, cy: zCy, r: 12, fill: '#5a7a9a', stroke: '#9abcda', 'stroke-width': 2 }, zoomG);
  el('circle', { cx: zCx, cy: zCy, r: 4, fill: '#0a1018' }, zoomG);
  
  // 橡胶块 - 4块(放大版)
  const rubberData = [
    { x: -30, y: -30, w: 18, h: 18, label: '70A' },
    { x: 12, y: -30, w: 18, h: 18, label: '80A' },
    { x: -30, y: 12, w: 18, h: 18, label: '80A' },
    { x: 12, y: 12, w: 18, h: 18, label: '70A' },
  ];
  rubberData.forEach((r, i) => {
    const rb = el('rect', {
      x: zCx + r.x, y: zCy + r.y, width: r.w, height: r.h, rx: 3,
      fill: '#cc2255', stroke: '#ff4477', 'stroke-width': 1,
      class: 'zoom-rubber', 'data-idx': i
    }, zoomG);
    // 硬度标注
    el('text', {
      x: zCx + r.x + r.w / 2, y: zCy + r.y + r.h / 2 + 3,
      'font-family': 'Rajdhani,monospace', 'font-size': 8, 'font-weight': 700,
      fill: '#ffccdd', 'text-anchor': 'middle'
    }, zoomG).textContent = r.label;
  });
  
  // 连接臂(前后车架连接点)
  el('rect', { x: zCx + 50, y: zCy - 10, width: 30, height: 20, rx: 2, fill: '#1a2538', stroke: '#3a5575', 'stroke-width': 1.5 }, zoomG);
  el('rect', { x: zCx - 80, y: zCy - 10, width: 30, height: 20, rx: 2, fill: '#1a2538', stroke: '#3a5575', 'stroke-width': 1.5 }, zoomG);
  
  // 变形指示线
  el('line', {
    x1: zCx - 75, y1: zCy - 18, x2: zCx + 75, y2: zCy - 18,
    stroke: 'var(--pink)', 'stroke-width': .8, 'stroke-dasharray': '4,3', opacity: .5, class: 'deform-line'
  }, zoomG);
  
  // 标签
  el('text', {
    x: 1180, y: 50, 'font-family': 'Rajdhani,sans-serif', 'font-size': 13, 'font-weight': 700,
    fill: 'var(--teal)', 'text-anchor': 'middle', letter-spacing: 1
  }, zoomGroup).textContent = 'HINGE DETAIL';
  el('text', {
    x: 1180, y: 66, 'font-family': 'Noto Sans SC,sans-serif', 'font-size': 11,
    fill: 'var(--dim)', 'text-anchor': 'middle'
  }, zoomGroup).textContent = '铰接点放大视图';
  
  // 虚线连接到主视图铰接点
  el('line', {
    x1: HINGE_X + 35, y1: HINGE_Y - 25, x2: 1060, y2: 140,
    stroke: 'var(--teal)', 'stroke-width': .8, 'stroke-dasharray': '5,4', opacity: .35
  }, zoomGroup);
}
buildZoomInset();

// --- 参数面板 ---
function buildParamPanel() {
  const px = 40, py = 780;
  el('rect', { x: px, y: py, width: 340, height: 85, rx: 6, fill: '#0a1018', stroke: '#162030', 'stroke-width': 1, opacity: .9 }, paramGroup);
  
  const items = [
    { id: 'pSteer', label: '转向角', x: px + 15, y: py + 22, color: 'var(--green)' },
    { id: 'pHinge', label: '铰接变形', x: px + 15, y: py + 42, color: 'var(--pink)' },
    { id: 'pRear', label: '后轮偏转', x: px + 15, y: py + 62, color: 'var(--orange)' },
    { id: 'pRadius', label: '转弯半径', x: px + 180, y: py + 22, color: 'var(--blue)' },
    { id: 'pLoad', label: '载重系数', x: px + 180, y: py + 42, color: 'var(--gold)' },
    { id: 'pStatus', label: '随动状态', x: px + 180, y: py + 62, color: 'var(--teal)' },
  ];
  
  items.forEach(it => {
    el('text', {
      x: it.x, y: it.y, 'font-family': 'Noto Sans SC,sans-serif', 'font-size': 11, fill: 'var(--dim)'
    }, paramGroup).textContent = it.label;
    el('text', {
      x: it.x + 68, y: it.y, 'font-family': 'Rajdhani,monospace', 'font-size': 13, 'font-weight': 700,
      fill: it.color, class: it.id
    }, paramGroup).textContent = '--';
  });
}
buildParamPanel();

// --- IFR说明面板 ---
function buildIFRPanel() {
  const px = 1060, py = 780;
  el('rect', { x: px, y: py, width: 310, height: 85, rx: 6, fill: '#0a1018', stroke: '#162030', 'stroke-width': 1, opacity: .9 }, paramGroup);
  
  el('text', {
    x: px + 12, y: py + 18, 'font-family': 'Rajdhani,sans-serif', 'font-size': 12, 'font-weight': 700,
    fill: 'var(--teal)', letter-spacing: 1
  }, paramGroup).textContent = 'IFR — 最终理想解';
  
  const lines = [
    '资源利用:车辆自重 + 惯性力 → 驱动随动',
    '零新增动力:橡胶块+销轴替代转向电机',
    '自回正:橡胶扭力块消除外力后自动复位',
  ];
  lines.forEach((l, i) => {
    el('text', {
      x: px + 16, y: py + 36 + i * 17, 'font-family': 'Noto Sans SC,sans-serif', 'font-size': 10.5,
      fill: '#8a9ab0'
    }, paramGroup).textContent = '● ' + l;
  });
}
buildIFRPanel();

// --- 力流箭头 ---
let forceFlowElements = [];
function buildForceFlow() {
  forceFlowElements = [];
  // 5个阶段的力流
  const flowPaths = [
    { d: `M${HINGE_X+290},${HINGE_Y} L${HINGE_X+200},${HINGE_Y}`, color: 'var(--green)', label: '1' },
    { d: `M${HINGE_X+180},${HINGE_Y} L${HINGE_X+60},${HINGE_Y}`, color: 'var(--gold)', label: '2' },
    { d: `M${HINGE_X-20},${HINGE_Y} L${HINGE_X-60},${HINGE_Y}`, color: 'var(--pink)', label: '3' },
    { d: `M${HINGE_X-90},${HINGE_Y} L${HINGE_X-180},${HINGE_Y}`, color: 'var(--orange)', label: '4' },
  ];
  
  flowPaths.forEach(fp => {
    const p = el('path', {
      d: fp.d, fill: 'none', stroke: fp.color, 'stroke-width': 2,
      'marker-end': fp.color === 'var(--pink)' ? 'url(#arrowPink)' : 'url(#arrowGold)',
      'stroke-dasharray': '8,6', 'stroke-dashoffset': 0, opacity: 0,
      class: 'force-path'
    }, forceGroup);
    forceFlowElements.push(p);
  });
}
buildForceFlow();

// --- 角度弧线 ---
let angleArcs = {};
function buildAngleIndicators() {
  // 铰接变形角
  angleArcs.hinge = el('path', {
    d: '', fill: 'none', stroke: 'var(--pink)', 'stroke-width': 1.5, opacity: 0,
    filter: 'url(#glow)'
  }, angleGroup);
  
  angleArcs.hingeLabel = el('text', {
    x: 0, y: 0, 'font-family': 'Rajdhani,monospace', 'font-size': 12, 'font-weight': 700,
    fill: 'var(--pink)', 'text-anchor': 'middle', opacity: 0
  }, angleGroup);
  
  // 后轮偏转角(每个后轮一个)
  angleArcs.rearWheels = [];
  document.querySelectorAll('.rear-wheel').forEach((w, i) => {
    const arc = el('path', {
      d: '', fill: 'none', stroke: 'var(--orange)', 'stroke-width': 1.2, opacity: 0
    }, angleGroup);
    const label = el('text', {
      x: 0, y: 0, 'font-family': 'Rajdhani,monospace', 'font-size': 10, 'font-weight': 600,
      fill: 'var(--orange)', 'text-anchor': 'middle', opacity: 0
    }, angleGroup);
    angleArcs.rearWheels.push({ arc, label, cx: +w.dataset.cx, cy: +w.dataset.cy, side: +w.dataset.side });
  });
}
buildAngleIndicators();

// --- 转弯半径弧线 ---
let radiusArcs = {};
function buildRadiusArcs() {
  // 有随动时的转弯半径
  radiusArcs.withPassive = el('path', {
    d: '', fill: 'none', stroke: 'var(--blue)', 'stroke-width': 1.8,
    'stroke-dasharray': '8,5', opacity: 0
  }, radiusGroup);
  
  // 无随动时的转弯半径(更大)
  radiusArcs.withoutPassive = el('path', {
    d: '', fill: 'none', stroke: '#ff4444', 'stroke-width': 1.2,
    'stroke-dasharray': '4,4', opacity: 0
  }, radiusGroup);
  
  // 标签
  radiusArcs.labelWith = el('text', {
    x: 0, y: 0, 'font-family': 'Noto Sans SC,sans-serif', 'font-size': 11,
    fill: 'var(--blue)', opacity: 0
  }, radiusGroup);
  radiusArcs.labelWithout = el('text', {
    x: 0, y: 0, 'font-family': 'Noto Sans SC,sans-serif', 'font-size': 11,
    fill: '#ff4444', opacity: 0
  }, radiusGroup);
}
buildRadiusArcs();

// ============ 动画更新 ============
let time = 0;

function updateAnimation() {
  const st = computeState();
  const absSteer = Math.abs(steerAngle);
  
  // --- 更新车架旋转 ---
  frontGroup.setAttribute('transform', rotateTransform(st.frontAngle, HINGE_X, HINGE_Y));
  rearGroup.setAttribute('transform', rotateTransform(st.rearAngle, HINGE_X, HINGE_Y));
  
  // --- 更新前轮转向角 ---
  document.querySelectorAll('.front-wheel').forEach(w => {
    const cx = w.dataset.cx, cy = w.dataset.cy;
    w.setAttribute('transform', rotateTransform(steerAngle, cx, cy));
  });
  
  // --- 更新后轮偏转 ---
  document.querySelectorAll('.rear-wheel').forEach(w => {
    const cx = w.dataset.cx, cy = w.dataset.cy;
    const side = +w.dataset.side;
    // 后轮偏转方向与转向方向相同,但幅度很小
    const deflect = st.rearDeflect;
    w.setAttribute('transform', rotateTransform(deflect, cx, cy));
  });
  
  // --- 更新铰接点发光 ---
  const glowIntensity = Math.min(1, Math.abs(st.hingeDeform) / 5);
  const hingeGlow = hingeGroupEl.querySelector('.hinge-glow');
  hingeGlow.setAttribute('opacity', .2 + glowIntensity * .6);
  hingeGlow.setAttribute('r', 32 + glowIntensity * 10);
  
  // 橡胶块变形效果
  const rubberBlocks = hingeGroupEl.querySelectorAll('.rubber-block');
  const deformDir = Math.sign(st.hingeDeform);
  const deformMag = Math.abs(st.hingeDeform) / 8;
  rubberBlocks.forEach((rb, i) => {
    const row = Math.floor(i / 2);
    const col = i % 2;
    const compress = (col === 0 ? 1 : -1) * deformDir * deformMag;
    const baseX = [HINGE_X - 14, HINGE_X + 8, HINGE_X - 14, HINGE_X + 8][i];
    rb.setAttribute('x', baseX + compress * 3);
    rb.setAttribute('width', 6 - compress * 2);
    // 受压侧高亮
    rb.setAttribute('opacity', .5 + deformMag * 2);
  });
  
  // --- 更新放大镜中的橡胶块 ---
  const zoomRubbers = zoomGroup.querySelectorAll('.zoom-rubber');
  zoomRubbers.forEach((zr, i) => {
    const row = Math.floor(i / 2);
    const col = i % 2;
    const compress = (col === 0 ? 1 : -1) * deformDir * deformMag;
    const baseX = [-30, 12, -30, 12][i];
    const baseY = [-30, -30, 12, 12][i];
    zr.setAttribute('x', 1180 + baseX + compress * 6);
    zr.setAttribute('width', 18 - compress * 4);
    // 受压侧变亮
    const stress = Math.abs(compress) / 2;
    zr.setAttribute('fill', `rgb(${Math.round(204 + stress * 51)},${Math.round(34 - stress * 20)},${Math.round(85 + stress * 30)})`);
  });
  
  // 放大镜变形线
  const deformLine = zoomGroup.querySelector('.deform-line');
  if (deformLine) {
    deformLine.setAttribute('opacity', .3 + glowIntensity * .5);
  }
  
  // --- 力流箭头 ---
  const flowActive = absSteer > 1;
  forceFlowElements.forEach((fp, i) => {
    if (flowActive) {
      const delay = i * 14;
      const offset = -(time * 1.5 + delay) % 28;
      fp.setAttribute('stroke-dashoffset', offset);
      fp.setAttribute('opacity', .3 + Math.min(absSteer / 15, .6));
    } else {
      fp.setAttribute('opacity', 0);
    }
  });
  
  // --- 角度指示器 ---
  // 铰接变形角弧
  if (absSteer > 0.5) {
    const arcR = 50;
    const startA = -st.frontAngle * Math.PI / 180;
    const endA = -st.rearAngle * Math.PI / 180;
    const midA = (startA + endA) / 2;
    
    const sx = HINGE_X + arcR * Math.cos(startA - Math.PI / 2);
    const sy = HINGE_Y + arcR * Math.sin(startA - Math.PI / 2);
    const ex = HINGE_X + arcR * Math.cos(endA - Math.PI / 2);
    const ey = HINGE_Y + arcR * Math.sin(endA - Math.PI / 2);
    
    const largeArc = Math.abs(st.hingeDeform) > 180 ? 1 : 0;
    const sweep = st.hingeDeform > 0 ? 1 : 0;
    
    angleArcs.hinge.setAttribute('d', `M${sx},${sy} A${arcR},${arcR} 0 ${largeArc},${sweep} ${ex},${ey}`);
    angleArcs.hinge.setAttribute('opacity', .6 + glowIntensity * .4);
    
    const labelR = arcR + 14;
    const lx = HINGE_X + labelR * Math.cos(midA - Math.PI / 2);
    const ly = HINGE_Y + labelR * Math.sin(midA - Math.PI / 2);
    angleArcs.hingeLabel.setAttribute('x', lx);
    angleArcs.hingeLabel.setAttribute('y', ly + 4);
    angleArcs.hingeLabel.setAttribute('opacity', .7 + glowIntensity * .3);
    angleArcs.hingeLabel.textContent = Math.abs(st.hingeDeform).toFixed(1) + '°';
  } else {
    angleArcs.hinge.setAttribute('opacity', 0);
    angleArcs.hingeLabel.setAttribute('opacity', 0);
  }
  
  // 后轮偏转角弧
  angleArcs.rearWheels.forEach(aw => {
    if (absSteer > 1 && Math.abs(st.rearDeflect) > 0.1) {
      const arcR = 28;
      const baseAngle = st.rearAngle;
      const deflectAngle = st.rearDeflect;
      
      const startRad = -baseAngle * Math.PI / 180;
      const endRad = -(baseAngle + deflectAngle) * Math.PI / 180;
      
      // 需要考虑车架旋转后的坐标
      const cos_r = Math.cos(baseAngle * Math.PI / 180);
      const sin_r = Math.sin(baseAngle * Math.PI / 180);
      const localCx = aw.cx - HINGE_X;
      const localCy = aw.cy - HINGE_Y;
      const worldCx = HINGE_X + localCx * cos_r - localCy * sin_r;
      const worldCy = HINGE_Y + localCx * sin_r + localCy * cos_r;
      
      const s1x = worldCx + arcR * Math.cos(startRad - Math.PI / 2);
      const s1y = worldCy + arcR * Math.sin(startRad - Math.PI / 2);
      const e1x = worldCx + arcR * Math.cos(endRad - Math.PI / 2);
      const e1y = worldCy + arcR * Math.sin(endRad - Math.PI / 2);
      
      const sweep = deflectAngle > 0 ? 1 : 0;
      
      aw.arc.setAttribute('d', `M${s1x},${s1y} A${arcR},${arcR} 0 0,${sweep} ${e1x},${e1y}`);
      aw.arc.setAttribute('opacity', .5);
      
      const midRad = (startRad + endRad) / 2;
      aw.label.setAttribute('x', worldCx + (arcR + 12) * Math.cos(midRad - Math.PI / 2));
      aw.label.setAttribute('y', worldCy + (arcR + 12) * Math.sin(midRad - Math.PI / 2) + 3);
      aw.label.setAttribute('opacity', .6);
      aw.label.textContent = Math.abs(st.rearDeflect).toFixed(1) + '°';
    } else {
      aw.arc.setAttribute('opacity', 0);
      aw.label.setAttribute('opacity', 0);
    }
  });
  
  // --- 转弯半径可视化 ---
  if (absSteer > 2) {
    const steerRad = absSteer * Math.PI / 180;
    const wheelBase = 580; // 总轴距像素
    const R_noPassive = wheelBase / Math.sin(steerRad);
    const R_withPassive = R_noPassive * (1 - st.loadFactor * 0.18 * Math.min(absSteer / 15, 1));
    
    const maxR = Math.min(R_noPassive, 700);
    const arcCenterY = HINGE_Y + maxR + 30;
    
    // 无随动(红色虚线)
    const r1 = Math.min(R_noPassive, 700);
    const startAngle1 = Math.asin(Math.min(1, 300 / r1));
    const x1s = HINGE_X - 300;
    const x1e = HINGE_X + 300;
    const y1 = arcCenterY - Math.sqrt(Math.max(0, r1 * r1 - 300 * 300));
    
    radiusArcs.withoutPassive.setAttribute('d',
      `M${x1s},${y1} A${r1},${r1} 0 0,1 ${x1e},${y1}`);
    radiusArcs.withoutPassive.setAttribute('opacity', .35);
    
    // 有随动(蓝色实线)
    const r2 = Math.min(R_withPassive, 700);
    const y2 = arcCenterY - Math.sqrt(Math.max(0, r2 * r2 - 300 * 300));
    
    radiusArcs.withPassive.setAttribute('d',
      `M${x1s},${y2} A${r2},${r2} 0 0,1 ${x1e},${y2}`);
    radiusArcs.withPassive.setAttribute('opacity', .6 + glowIntensity * .3);
    
    // 标签
    radiusArcs.labelWith.setAttribute('x', HINGE_X);
    radiusArcs.labelWith.setAttribute('y', y2 + 18);
    radiusArcs.labelWith.setAttribute('opacity', .7);
    radiusArcs.labelWith.textContent = '随动转弯半径';
    
    radiusArcs.labelWithout.setAttribute('x', HINGE_X);
    radiusArcs.labelWithout.setAttribute('y', y1 + 18);
    radiusArcs.labelWithout.setAttribute('opacity', .5);
    radiusArcs.labelWithout.textContent = '无随动转弯半径';
  } else {
    radiusArcs.withPassive.setAttribute('opacity', 0);
    radiusArcs.withoutPassive.setAttribute('opacity', 0);
    radiusArcs.labelWith.setAttribute('opacity', 0);
    radiusArcs.labelWithout.setAttribute('opacity', 0);
  }
  
  // --- 载重块可视化 ---
  const loadBlocks = annoGroup.querySelectorAll('.load-block');
  const visibleBlocks = Math.round(loadPct / 20);
  loadBlocks.forEach((lb, i) => {
    lb.setAttribute('opacity', i < visibleBlocks ? .5 + (loadPct / 100) * .4 : .1);
    lb.setAttribute('fill', i < visibleBlocks ? '#2a2a10' : '#1a1a10');
  });
  
  // --- 参数面板更新 ---
  const pSteer = paramGroup.querySelector('.pSteer');
  const pHinge = paramGroup.querySelector('.pHinge');
  const pRear = paramGroup.querySelector('.pRear');
  const pRadius = paramGroup.querySelector('.pRadius');
  const pLoad = paramGroup.querySelector('.pLoad');
  const pStatus = paramGroup.querySelector('.pStatus');
  
  pSteer.textContent = steerAngle.toFixed(1) + '°';
  pHinge.textContent = st.hingeDeform.toFixed(1) + '°';
  pRear.textContent = st.rearDeflect.toFixed(2) + '°';
  
  if (absSteer > 2) {
    const steerRad = absSteer * Math.PI / 180;
    const R = 580 / Math.sin(steerRad);
    const R_eff = R * (1 - st.loadFactor * 0.18 * Math.min(absSteer / 15, 1));
    pRadius.textContent = (R_eff / 58).toFixed(1) + 'm';
  } else {
    pRadius.textContent = '—';
  }
  
  pLoad.textContent = (st.loadFactor * 100).toFixed(0) + '%';
  
  if (absSteer < 0.5) {
    pStatus.textContent = '直行';
    pStatus.setAttribute('fill', 'var(--teal)');
  } else if (st.loadFactor > 0.3) {
    pStatus.textContent = '随动有效';
    pStatus.setAttribute('fill', 'var(--green)');
  } else {
    pStatus.textContent = '随动失效';
    pStatus.setAttribute('fill', '#ff4444');
  }
}

// ============ 动画循环 ============
function animate() {
  time++;
  
  if (isAuto) {
    autoTime += 0.012;
    steerAngle = Math.sin(autoTime) * 25;
    document.getElementById('steerSlider').value = steerAngle;
    document.getElementById('steerVal').textContent = steerAngle.toFixed(1) + '°';
  }
  
  updateAnimation();
  animId = requestAnimationFrame(animate);
}

// ============ 交互事件 ============
document.getElementById('steerSlider').addEventListener('input', function() {
  if (isAuto) return;
  steerAngle = +this.value;
  document.getElementById('steerVal').textContent = steerAngle.toFixed(1) + '°';
});

document.getElementById('loadSlider').addEventListener('input', function() {
  loadPct = +this.value;
  document.getElementById('loadVal').textContent = loadPct + '%';
});

document.getElementById('autoBtn').addEventListener('click', function() {
  isAuto = !isAuto;
  this.classList.toggle('on', isAuto);
  if (isAuto) {
    autoTime = Math.asin(steerAngle / 25);
  }
});

document.getElementById('resetBtn').addEventListener('click', function() {
  isAuto = false;
  document.getElementById('autoBtn').classList.remove('on');
  steerAngle = 0;
  loadPct = 80;
  document.getElementById('steerSlider').value = 0;
  document.getElementById('steerVal').textContent = '0.0°';
  document.getElementById('loadSlider').value = 80;
  document.getElementById('loadVal').textContent = '80%';
});

// 键盘控制
document.addEventListener('keydown', function(e) {
  if (e.key === 'ArrowLeft') {
    steerAngle = Math.max(-30, steerAngle - 2);
    document.getElementById('steerSlider').value = steerAngle;
    document.getElementById('steerVal').textContent = steerAngle.toFixed(1) + '°';
  } else if (e.key === 'ArrowRight') {
    steerAngle = Math.min(30, steerAngle + 2);
    document.getElementById('steerSlider').value = steerAngle;
    document.getElementById('steerVal').textContent = steerAngle.toFixed(1) + '°';
  } else if (e.key === ' ') {
    e.preventDefault();
    document.getElementById('autoBtn').click();
  }
});

// 启动
animate();
</script>
</body>
</html>

这段代码实现了一个完整的铰接式随动转向 IFR 原理动画,以下是核心设计要点:

视觉架构:

  • 深色工业蓝图风格背景 + 细网格,营造技术图纸质感
  • 前段车架(绿色系)与后段车架(橙色系)用色彩区分功能角色
  • 铰接点/橡胶块(品红色)作为视觉焦点,带动态发光效果
  • 右上角放大镜视图详细展示万向节+橡胶扭力块的微观变形

IFR 思想体现:

  • 动画直接展示理想工作状态,无前后对比
  • 力流箭头(金色/品红虚线动画)可视化展示"离心力→底盘弯曲→橡胶变形→后轮随动"的力传递链路,突出**利用现有资源(自重+惯性力)**的巧妙性
  • 转弯半径对比弧线:蓝色(随动有效)vs 红色虚线(无随动),直观展示效果
  • 右下角 IFR 面板文字总结:零新增动力、自回正、资源利用

交互设计:

  • 转向角滑块:-30° 至 +30°,实时控制前轮转向
  • 载重滑块:0% 至 100%,当载重低于约30%时,随动效果急剧衰减(状态显示变红"随动失效"),直观展示边界条件
  • 自动演示按钮:正弦波自动转向
  • 键盘方向键控制转向,空格键切换自动模式
  • 实时参数面板显示转向角、铰接变形角、后轮偏转角、等效转弯半径、载重系数、随动状态
动画没生成

我来仔细排查问题,重写一个更稳健、视觉效果更强的完整版本。之前的问题很可能是 SVG 中 CSS 变量在 presentation attributes 里不生效、以及部分 DOM 查询时序错误导致的。这次全部用直接颜色值并确保执行顺序正确。

<!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@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
  background:#050910;color:#b8c8d8;
  font-family:'Noto Sans SC',sans-serif;
  overflow:hidden;height:100vh;
  display:flex;flex-direction:column;
}
#svgWrap{
  flex:1;display:flex;justify-content:center;align-items:center;
  min-height:0;padding:4px;
}
#mainSvg{
  width:100%;height:100%;
  max-height:calc(100vh - 110px);
  border-radius:6px;
}
#controls{
  background:#0a1018;border-top:1px solid #162030;
  padding:10px 24px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;
}
.cg{display:flex;align-items:center;gap:8px}
.cl{font-size:12px;color:#4a6080;white-space:nowrap;font-weight:500}
.cv{font-family:'Rajdhani',monospace;font-size:14px;font-weight:700;min-width:52px;text-align:right}
input[type=range]{
  -webkit-appearance:none;width:170px;height:5px;
  background:#162030;border-radius:3px;outline:none;
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:16px;height:16px;
  border-radius:50%;cursor:pointer;border:none;
}
#steerSlider::-webkit-slider-thumb{background:#00e896;box-shadow:0 0 8px #00e896}
#loadSlider::-webkit-slider-thumb{background:#ffc800;box-shadow:0 0 8px #ffc800}
.btn{
  padding:5px 14px;border:1px solid #1e2e42;background:transparent;
  color:#8a9ab0;border-radius:5px;cursor:pointer;
  font-size:12px;font-family:'Noto Sans SC',sans-serif;
  transition:all .25s;
}
.btn:hover{border-color:#00ddc8;color:#00ddc8}
.btn.on{border-color:#00aaff;background:rgba(0,170,255,.12);color:#00aaff;box-shadow:0 0 10px rgba(0,170,255,.2)}
.sep{width:1px;height:22px;background:#162030}
.hint{font-size:10.5px;color:#3a5070;white-space:nowrap}
</style>
</head>
<body>

<div id="svgWrap">
  <svg id="mainSvg" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
  </svg>
</div>

<div id="controls">
  <div class="cg">
    <span class="cl">转向角</span>
    <input type="range" id="steerSlider" min="-30" max="30" value="0" step="0.5">
    <span class="cv" id="steerVal" style="color:#00e896">0.0°</span>
  </div>
  <div class="sep"></div>
  <div class="cg">
    <span class="cl">载重</span>
    <input type="range" id="loadSlider" min="0" max="100" value="80" step="1">
    <span class="cv" id="loadVal" style="color:#ffc800">80%</span>
  </div>
  <div class="sep"></div>
  <button class="btn" id="autoBtn">自动演示</button>
  <button class="btn" id="resetBtn">复位</button>
  <div class="sep"></div>
  <span class="hint">← → 键控制转向 · 空格键切换自动</span>
</div>

<script>
(function(){
  "use strict";

  /* ====== 常量 ====== */
  var NS = "http://www.w3.org/2000/svg";
  var HX = 680, HY = 410;          // 铰接点坐标
  var CL = 310, CHW = 68;          // 车架半长、半宽
  var WL = 48, WW = 18;            // 轮胎长宽
  var WOFF = 88;                    // 轮距(半)
  var FA = [280, 140];             // 前段两轴距铰接点距离
  var RA = [-140, -280];           // 后段两轴距铰接点距离

  /* ====== 颜色 ====== */
  var C = {
    green:  "#00e896", greenD: "#00a86a",
    orange: "#ff8c2e", orangeD:"#cc6a18",
    pink:   "#ff2d6b", pinkD:  "#cc1a4e",
    gold:   "#ffc800", goldD:  "#cc9e00",
    blue:   "#00aaff", blueD:  "#007acc",
    teal:   "#00ddc8",
    chassis:"#1a2538", chassisS:"#3a5575",
    wheel:  "#2a3848", wheelS: "#5a7898",
    dim:    "#4a6080", text:   "#b8c8d8",
    bg:     "#050910", grid:   "#0e1828",
    card:   "#0a1018", border: "#162030",
    red:    "#ff4444"
  };

  /* ====== 状态 ====== */
  var steerAngle = 0, loadPct = 80, isAuto = false, autoTime = 0, time = 0;

  /* ====== SVG辅助 ====== */
  function $(tag, a, p) {
    var e = document.createElementNS(NS, tag);
    if (a) for (var k in a) e.setAttribute(k, a[k]);
    if (p) p.appendChild(e);
    return e;
  }

  function rot(a, cx, cy) {
    return "rotate(" + a + "," + cx + "," + cy + ")";
  }

  /* ====== 计算状态 ====== */
  function calc() {
    var lf = loadPct / 100;
    var fa = steerAngle * 0.52;
    var hd = fa * lf * 0.48;
    var ra = fa - hd;
    var rd = (hd >= 0 ? 1 : -1) * Math.min(5, Math.abs(hd) * 0.38) * lf;
    return {fa:fa, hd:hd, ra:ra, rd:rd, lf:lf};
  }

  /* ====== 主SVG ====== */
  var svg = document.getElementById("mainSvg");

  // -- defs --
  var defs = $("defs", null, svg);

  // 网格
  var pat = $("pattern", {id:"grid",width:40,height:40,patternUnits:"userSpaceOnUse"}, defs);
  $("path", {d:"M40 0L0 0 0 40",fill:"none",stroke:C.grid,"stroke-width":"0.5"}, pat);

  // 滤镜
  function makeGlow(id, dev) {
    var f = $("filter", {id:id,x:"-50%",y:"-50%",width:"200%",height:"200%"}, defs);
    $("feGaussianBlur", {in:"SourceGraphic",stdDeviation:dev,result:"b"}, f);
    var m = $("feMerge", null, f);
    $("feMergeNode", {in:"b"}, m);
    $("feMergeNode", {in:"SourceGraphic"}, m);
  }
  makeGlow("glow", 4);
  makeGlow("glowS", 8);
  makeGlow("glowL", 14);

  // 箭头标记
  function makeArrow(id, col) {
    var m = $("marker", {id:id,markerWidth:8,markerHeight:6,refX:7,refY:3,orient:"auto"}, defs);
    $("path", {d:"M0,0 L8,3 L0,6 Z",fill:col,opacity:"0.8"}, m);
  }
  makeArrow("arrG", C.gold);
  makeArrow("arrP", C.pink);
  makeArrow("arrB", C.blue);

  // 剪裁 - 放大镜
  $("clipPath", {id:"zoomClip"}, defs)
    .appendChild($("circle", {cx:1180,cy:200,r:125}));

  /* ====== 背景层 ====== */
  $("rect", {width:1400,height:900,fill:C.bg}, svg);
  $("rect", {width:1400,height:900,fill:"url(#grid)",opacity:"0.6"}, svg);

  /* ====== 标题 ====== */
  $("text", {x:40,y:38,"font-family":"Rajdhani,sans-serif","font-size":"22",
    "font-weight":"700",fill:C.teal,"letter-spacing":"2"}, svg)
    .textContent = "ARTICULATED PASSIVE STEERING";
  $("text", {x:40,y:58,"font-family":"Noto Sans SC,sans-serif","font-size":"13",fill:C.dim}, svg)
    .textContent = "4轴8轮平台 · 铰接式随动转向 · IFR最终理想解原理演示";

  /* ====== 转弯半径层 ====== */
  var radiusGrp = $("g", {id:"radGrp"}, svg);
  var radLine1, radLine2, radLbl1, radLbl2, radDiff;
  (function(){
    radLine1 = $("path", {d:"",fill:"none",stroke:C.red,"stroke-width":"1.2","stroke-dasharray":"5,4",opacity:"0"}, radiusGrp);
    radLine2 = $("path", {d:"",fill:"none",stroke:C.blue,"stroke-width":"2","stroke-dasharray":"8,5",opacity:"0"}, radiusGrp);
    radDiff = $("path", {d:"",fill:"rgba(0,170,255,0.06)",stroke:"none",opacity:"0"}, radiusGrp);
    radLbl1 = $("text", {"font-family":"Noto Sans SC,sans-serif","font-size":"11",fill:C.red,opacity:"0"}, radiusGrp);
    radLbl2 = $("text", {"font-family":"Noto Sans SC,sans-serif","font-size":"11",fill:C.blue,opacity:"0"}, radiusGrp);
  })();

  /* ====== 后段车架 ====== */
  var rearGrp = $("g", null, svg);
  var rearWheels = [];

  (function buildRear(){
    var y1 = HY - CHW, y2 = HY + CHW;
    // 框架
    $("rect", {x:HX-CL+6,y:y1+5,width:CL-12,height:CHW*2-10,
      rx:5,fill:C.chassis,stroke:C.chassisS,"stroke-width":"1.5"}, rearGrp);
    // 斜撑
    for(var x=HX-CL+25; x<HX-15; x+=42){
      $("line", {x1:x,y1:y1+8,x2:x+22,y2:y2-8,stroke:"#1e3048","stroke-width":"0.7"}, rearGrp);
    }
    // 边框
    $("rect", {x:HX-CL+3,y:y1+2,width:CL-6,height:CHW*2-4,
      rx:4,fill:"none",stroke:"#2a4060","stroke-width":"0.7","stroke-dasharray":"4,3"}, rearGrp);
    // 端部标记(后端)
    $("path", {d:"M"+(HX-CL+2)+","+(HY-10)+" L"+(HX-CL-8)+","+HY+" L"+(HX-CL+2)+","+(HY+10),
      fill:"none",stroke:C.orange,"stroke-width":"1.5",opacity:"0.5"}, rearGrp);

    // 轮轴和车轮
    RA.forEach(function(off, ai){
      var ax = HX + off;
      $("line", {x1:ax,y1:y1-6,x2:ax,y2:y2+6,stroke:"#3a5570","stroke-width":"2"}, rearGrp);
      [-1,1].forEach(function(side){
        var wy = HY + side * WOFF;
        var wg = $("g", null, rearGrp);
        // 轮胎
        $("rect", {x:ax-WL/2,y:wy-WW/2,width:WL,height:WW,
          rx:4,fill:"#1e2e40",stroke:C.orange,"stroke-width":"1.8"}, wg);
        // 花纹
        for(var i=-18;i<=18;i+=7){
          $("line", {x1:ax+i,y1:wy-WW/2+3,x2:ax+i,y2:wy+WW/2-3,
            stroke:"#2a4058","stroke-width":"0.6"}, wg);
        }
        // 销轴点
        $("circle", {cx:ax,cy:wy,r:3.5,fill:C.orange,opacity:"0.9"}, wg);
        $("circle", {cx:ax,cy:wy,r:1.5,fill:C.bg}, wg);
        // 橡胶圈
        $("circle", {cx:ax,cy:wy,r:7,fill:"none",
          stroke:C.pink,"stroke-width":"1.2","stroke-dasharray":"2,2",opacity:"0.6"}, wg);
        rearWheels.push({g:wg, cx:ax, cy:wy, side:side});
      });
    });
  })();

  /* ====== 铰接点 ====== */
  var hingeGrp = $("g", null, svg);
  var rubberEls = [], hingeGlowEl, hingeDeformInd;

  (function buildHinge(){
    // 外圈
    $("circle", {cx:HX,cy:HY,r:26,fill:"#0c1520",stroke:"#3a5570","stroke-width":"2"}, hingeGrp);
    // 十字轴
    $("line", {x1:HX-16,y1:HY,x2:HX+16,y2:HY,stroke:"#6a8aaa","stroke-width":"3"}, hingeGrp);
    $("line", {x1:HX,y1:HY-16,x2:HX,y2:HY+16,stroke:"#6a8aaa","stroke-width":"3"}, hingeGrp);
    // 中心
    $("circle", {cx:HX,cy:HY,r:5.5,fill:"#5a7a9a",stroke:"#8ab0d0","stroke-width":"1"}, hingeGrp);

    // 橡胶块 4个
    var rpos = [[-12,-12],[6,-12],[-12,6],[6,6]];
    rpos.forEach(function(p){
      var r = $("rect", {x:HX+p[0],y:HY+p[1],width:6,height:6,rx:1,
        fill:C.pink,opacity:"0.7"}, hingeGrp);
      rubberEls.push({el:r, bx:p[0], by:p[1]});
    });

    // 发光圈
    hingeGlowEl = $("circle", {cx:HX,cy:HY,r:30,fill:"none",
      stroke:C.pink,"stroke-width":"1",opacity:"0.2",filter:"url(#glow)"}, hingeGrp);

    // 变形指示弧(动态)
    hingeDeformInd = $("path", {d:"",fill:"none",stroke:C.pink,
      "stroke-width":"1.8",opacity:"0",filter:"url(#glow)"}, hingeGrp);
  })();

  /* ====== 前段车架 ====== */
  var frontGrp = $("g", null, svg);
  var frontWheels = [];

  (function buildFront(){
    var y1 = HY - CHW, y2 = HY + CHW;
    // 框架
    $("rect", {x:HX-6,y:y1+5,width:CL-6,height:CHW*2-10,
      rx:5,fill:C.chassis,stroke:C.chassisS,"stroke-width":"1.5"}, frontGrp);
    // 斜撑
    for(var x=HX+15; x<HX+CL-20; x+=42){
      $("line", {x1:x,y1:y1+8,x2:x+22,y2:y2-8,stroke:"#1e3048","stroke-width":"0.7"}, frontGrp);
    }
    // 边框
    $("rect", {x:HX+3,y:y1+2,width:CL-6,height:CHW*2-4,
      rx:4,fill:"none",stroke:"#2a4060","stroke-width":"0.7","stroke-dasharray":"4,3"}, frontGrp);
    // 端部标记(前端)
    $("path", {d:"M"+(HX+CL-2)+","+(HY-10)+" L"+(HX+CL+8)+","+HY+" L"+(HX+CL-2)+","+(HY+10),
      fill:"none",stroke:C.green,"stroke-width":"1.5",opacity:"0.5"}, frontGrp);

    // 轮轴和车轮
    FA.forEach(function(off, ai){
      var ax = HX + off;
      $("line", {x1:ax,y1:y1-6,x2:ax,y2:y2+6,stroke:"#3a5570","stroke-width":"2"}, frontGrp);
      [-1,1].forEach(function(side){
        var wy = HY + side * WOFF;
        var wg = $("g", null, frontGrp);
        // 轮胎
        $("rect", {x:ax-WL/2,y:wy-WW/2,width:WL,height:WW,
          rx:4,fill:"#1e3028",stroke:C.green,"stroke-width":"1.8"}, wg);
        // 花纹
        for(var i=-18;i<=18;i+=7){
          $("line", {x1:ax+i,y1:wy-WW/2+3,x2:ax+i,y2:wy+WW/2-3,
            stroke:"#2a4838","stroke-width":"0.6"}, wg);
        }
        // 销轴点
        $("circle", {cx:ax,cy:wy,r:3.5,fill:C.green,opacity:"0.9"}, wg);
        $("circle", {cx:ax,cy:wy,r:1.5,fill:C.bg}, wg);
        frontWheels.push({g:wg, cx:ax, cy:wy, side:side});
      });
    });
  })();

  /* ====== 力流箭头层 ====== */
  var forceGrp = $("g", null, svg);
  var forceEls = [];
  (function(){
    var paths = [
      {d:"M"+(HX+270)+","+HY+" L"+(HX+180)+","+HY, stroke:C.green, marker:"", label:"1"},
      {d:"M"+(HX+160)+","+HY+" L"+(HX+50)+","+HY, stroke:C.gold, marker:"url(#arrG)", label:"2"},
      {d:"M"+(HX-10)+","+HY+" L"+(HX-60)+","+HY, stroke:C.pink, marker:"url(#arrP)", label:"3"},
      {d:"M"+(HX-80)+","+HY+" L"+(HX-190)+","+HY, stroke:C.orange, marker:"url(#arrG)", label:"4"},
    ];
    paths.forEach(function(p){
      var el = $("path", {d:p.d,fill:"none",stroke:p.stroke,"stroke-width":"2.2",
        "marker-end":p.marker,"stroke-dasharray":"8,6","stroke-dashoffset":"0",opacity:"0"}, forceGrp);
      forceEls.push(el);
    });
  })();

  /* ====== 角度弧线层 ====== */
  var arcGrp = $("g", null, svg);
  var hingeArcEl, hingeArcLbl;
  var rearArcs = [];

  (function(){
    hingeArcEl = $("path", {d:"",fill:"none",stroke:C.pink,"stroke-width":"1.8",opacity:"0",filter:"url(#glow)"}, arcGrp);
    hingeArcLbl = $("text", {"font-family":"Rajdhani,monospace","font-size":"12","font-weight":"700",
      fill:C.pink,"text-anchor":"middle",opacity:"0"}, arcGrp);

    rearWheels.forEach(function(w){
      var arc = $("path", {d:"",fill:"none",stroke:C.orange,"stroke-width":"1.3",opacity:"0"}, arcGrp);
      var lbl = $("text", {"font-family":"Rajdhani,monospace","font-size":"10","font-weight":"600",
        fill:C.orange,"text-anchor":"middle",opacity:"0"}, arcGrp);
      rearArcs.push({arc:arc, lbl:lbl, w:w});
    });
  })();

  /* ====== 标注层 ====== */
  var annoGrp = $("g", null, svg);
  var loadBlocks = [];

  (function(){
    function label(x,y,text,color,lineTo,anchor){
      if(lineTo){
        $("line", {x1:x+(anchor==="middle"?0:(lineTo[0]>x?90:-5)),y1:y-4,
          x2:lineTo[0],y2:lineTo[1],stroke:color,"stroke-width":"0.8",opacity:"0.5",
          "stroke-dasharray":"3,3"}, annoGrp);
      }
      $("text", {x:x,y:y,"font-family":"Noto Sans SC,sans-serif","font-size":"12",
        "font-weight":"500",fill:color,"text-anchor":anchor||"start",opacity:"0.85"}, annoGrp)
        .textContent = text;
    }
    label(HX+280, HY-108, "前轮主动转向", C.green, [HX+260, HY-75]);
    label(HX-370, HY-108, "后轮随动偏转", C.orange, [HX-260, HY-75]);
    label(HX, HY+80, "万向节+橡胶扭力块", C.pink, [HX, HY+38], "middle");

    // 载重区
    $("text", {x:HX,y:HY+CHW+45,"font-family":"Rajdhani,monospace","font-size":"14",
      "font-weight":"600",fill:C.gold,"text-anchor":"middle",opacity:"0.6"}, annoGrp)
      .textContent = "\u25BC 载重区 \u25BC";

    for(var i=0;i<5;i++){
      var b = $("rect", {x:HX-120+i*52,y:HY+CHW+10,width:46,height:16,
        rx:2,fill:"#1a1a10",stroke:C.gold,"stroke-width":"0.6",opacity:"0.3"}, annoGrp);
      loadBlocks.push(b);
    }

    // 力流标签
    var flowLabels = [
      {x:HX+225,y:HY-14,text:"转向输入",color:C.green},
      {x:HX+100,y:HY-14,text:"离心力",color:C.gold},
      {x:HX-40,y:HY-14,text:"铰接变形",color:C.pink},
      {x:HX-140,y:HY-14,text:"随动偏转",color:C.orange},
    ];
    flowLabels.forEach(function(fl){
      $("text", {x:fl.x,y:fl.y,"font-family":"Noto Sans SC,sans-serif","font-size":"9",
        fill:fl.color,"text-anchor":"middle",opacity:"0"}, annoGrp).textContent = fl.text;
    });
  })();

  var flowLabelEls = annoGrp.querySelectorAll("text[opacity='0']");

  /* ====== 放大镜 ====== */
  var zoomGrp = $("g", null, svg);
  var zoomRubbers = [];

  (function(){
    var zcx=1180, zcy=200;
    // 背景
    $("circle", {cx:zcx,cy:zcy,r:129,fill:"#080e18",stroke:"#2a3a55","stroke-width":"2"}, zoomGrp);
    // 剪裁内容
    var zc = $("g", {"clip-path":"url(#zoomClip)"}, zoomGrp);
    // 万向节外框
    $("circle", {cx:zcx,cy:zcy,r:55,fill:"#0a1520",stroke:"#3a5570","stroke-width":"2"}, zc);
    // 十字轴
    $("line", {x1:zcx-42,y1:zcy,x2:zcx+42,y2:zcy,stroke:"#7a9aba","stroke-width":"5"}, zc);
    $("line", {x1:zcx,y1:zcy-42,x2:zcx,y2:zcy+42,stroke:"#7a9aba","stroke-width":"5"}, zc);
    // 中心轴
    $("circle", {cx:zcx,cy:zcy,r:11,fill:"#5a7a9a",stroke:"#9abcda","stroke-width":"2"}, zc);
    $("circle", {cx:zcx,cy:zcy,r:3.5,fill:C.bg}, zc);

    // 橡胶块4块
    var rd = [{x:-28,y:-28,w:17,h:17,lb:"70A"},{x:11,y:-28,w:17,h:17,lb:"80A"},
              {x:-28,y:11,w:17,h:17,lb:"80A"},{x:11,y:11,w:17,h:17,lb:"70A"}];
    rd.forEach(function(r){
      var rb = $("rect", {x:zcx+r.x,y:zcy+r.y,width:r.w,height:r.h,rx:3,
        fill:"#cc2255",stroke:"#ff4477","stroke-width":"1"}, zc);
      $("text", {x:zcx+r.x+r.w/2,y:zcy+r.y+r.h/2+3,
        "font-family":"Rajdhani,monospace","font-size":"8","font-weight":"700",
        fill:"#ffccdd","text-anchor":"middle"}, zc).textContent = r.lb;
      zoomRubbers.push({el:rb, bx:r.x, by:r.y, bw:r.w});
    });

    // 连接臂
    $("rect", {x:zcx+50,y:zcy-9,width:28,height:18,rx:2,fill:C.chassis,stroke:C.chassisS,"stroke-width":"1.5"}, zc);
    $("rect", {x:zcx-78,y:zcy-9,width:28,height:18,rx:2,fill:C.chassis,stroke:C.chassisS,"stroke-width":"1.5"}, zc);

    // 标签
    $("text", {x:zcx,y:58,"font-family":"Rajdhani,sans-serif","font-size":"13",
      "font-weight":"700",fill:C.teal,"text-anchor":"middle","letter-spacing":"1"}, zoomGrp)
      .textContent = "HINGE DETAIL";
    $("text", {x:zcx,y:74,"font-family":"Noto Sans SC,sans-serif","font-size":"11",
      fill:C.dim,"text-anchor":"middle"}, zoomGrp).textContent = "铰接点放大视图";

    // 连接虚线
    $("line", {x1:HX+32,y1:HY-22,x2:1062,y2:160,stroke:C.teal,
      "stroke-width":"0.8","stroke-dasharray":"5,4",opacity:"0.3"}, zoomGrp);
  })();

  /* ====== 参数面板 ====== */
  var paramEls = {};
  (function(){
    var px=40, py=810;
    $("rect", {x:px,y:py,width:340,height:80,rx:6,fill:C.card,stroke:C.border,"stroke-width":"1",opacity:"0.92"}, svg);
    var items = [
      {id:"pSteer",lb:"转向角",x:px+14,y:py+20,c:C.green},
      {id:"pHinge",lb:"铰接变形",x:px+14,y:py+40,c:C.pink},
      {id:"pRear",lb:"后轮偏转",x:px+14,y:py+60,c:C.orange},
      {id:"pRad",lb:"转弯半径",x:px+178,y:py+20,c:C.blue},
      {id:"pLoad",lb:"载重系数",x:px+178,y:py+40,c:C.gold},
      {id:"pStatus",lb:"随动状态",x:px+178,y:py+60,c:C.teal},
    ];
    items.forEach(function(it){
      $("text", {x:it.x,y:it.y,"font-family":"Noto Sans SC,sans-serif","font-size":"11",fill:C.dim}, svg)
        .textContent = it.lb;
      paramEls[it.id] = $("text", {x:it.x+68,y:it.y,"font-family":"Rajdhani,monospace",
        "font-size":"13","font-weight":"700",fill:it.c}, svg);
      paramEls[it.id].textContent = "--";
    });
  })();

  /* ====== IFR说明面板 ====== */
  (function(){
    var px=1060, py=810;
    $("rect", {x:px,y:py,width:310,height:80,rx:6,fill:C.card,stroke:C.border,"stroke-width":"1",opacity:"0.92"}, svg);
    $("text", {x:px+12,y:py+18,"font-family":"Rajdhani,sans-serif","font-size":"12",
      "font-weight":"700",fill:C.teal,"letter-spacing":"1"}, svg).textContent = "IFR — 最终理想解";
    var lines = [
      "\u25CF 资源利用:车辆自重 + 惯性力 \u2192 驱动随动",
      "\u25CF 零新增动力:橡胶块+销轴替代转向电机",
      "\u25CF 自回正:橡胶扭力块消除外力后自动复位",
    ];
    lines.forEach(function(l,i){
      $("text", {x:px+16,y:py+36+i*16,"font-family":"Noto Sans SC,sans-serif","font-size":"10.5",
        fill:"#7a8a9e"}, svg).textContent = l;
    });
  })();

  /* ====== 后轮轨迹粒子层 ====== */
  var trailGrp = $("g", null, svg);
  var trails = [];
  var MAX_TRAILS = 60;

  /* ====== 更新函数 ====== */
  function update(){
    var st = calc();
    var as = Math.abs(steerAngle);

    // --- 车架旋转 ---
    frontGrp.setAttribute("transform", rot(st.fa, HX, HY));
    rearGrp.setAttribute("transform", rot(st.ra, HX, HY));

    // --- 前轮转向 ---
    frontWheels.forEach(function(w){
      w.g.setAttribute("transform", rot(steerAngle, w.cx, w.cy));
    });

    // --- 后轮偏转 ---
    rearWheels.forEach(function(w){
      w.g.setAttribute("transform", rot(st.rd, w.cx, w.cy));
    });

    // --- 铰接点 ---
    var gi = Math.min(1, Math.abs(st.hd) / 4);
    hingeGlowEl.setAttribute("opacity", 0.15 + gi * 0.6);
    hingeGlowEl.setAttribute("r", 30 + gi * 12);

    // 橡胶块变形
    var dd = Math.sign(st.hd);
    var dm = Math.abs(st.hd) / 7;
    rubberEls.forEach(function(r, i){
      var col = i % 2;
      var compress = (col === 0 ? 1 : -1) * dd * dm;
      r.el.setAttribute("x", HX + r.bx + compress * 2.5);
      r.el.setAttribute("width", Math.max(2, 6 - compress * 1.5));
      r.el.setAttribute("opacity", 0.5 + dm * 2);
    });

    // 放大镜中的橡胶块变形
    zoomRubbers.forEach(function(zr, i){
      var col = i % 2;
      var compress = (col === 0 ? 1 : -1) * dd * dm;
      zr.el.setAttribute("x", 1180 + zr.bx + compress * 5);
      zr.el.setAttribute("width", Math.max(8, zr.bw - compress * 3));
      var stress = Math.min(1, Math.abs(compress) / 2);
      var r = Math.round(204 + stress * 51);
      var g = Math.round(34 - stress * 20);
      var b = Math.round(85 + stress * 30);
      zr.el.setAttribute("fill", "rgb("+r+","+g+","+b+")");
    });

    // --- 力流箭头 ---
    if(as > 1){
      forceEls.forEach(function(fe, i){
        var offset = -(time * 1.8 + i * 14) % 28;
        fe.setAttribute("stroke-dashoffset", offset);
        fe.setAttribute("opacity", 0.25 + Math.min(as / 15, 0.65));
      });
      // 力流标签
      flowLabelEls.forEach(function(fl){
        fl.setAttribute("opacity", 0.4 + Math.min(as / 20, 0.5));
      });
    } else {
      forceEls.forEach(function(fe){ fe.setAttribute("opacity", 0); });
      flowLabelEls.forEach(function(fl){ fl.setAttribute("opacity", 0); });
    }

    // --- 铰接变形角弧 ---
    if(as > 0.8 && Math.abs(st.hd) > 0.2){
      var arcR = 48;
      var sA = -st.fa * Math.PI / 180;
      var eA = -st.ra * Math.PI / 180;
      var mA = (sA + eA) / 2;
      var sx = HX + arcR * Math.cos(sA - Math.PI/2);
      var sy = HY + arcR * Math.sin(sA - Math.PI/2);
      var ex = HX + arcR * Math.cos(eA - Math.PI/2);
      var ey = HY + arcR * Math.sin(eA - Math.PI/2);
      var sweep = st.hd > 0 ? 1 : 0;
      hingeArcEl.setAttribute("d", "M"+sx+","+sy+" A"+arcR+","+arcR+" 0 0,"+sweep+" "+ex+","+ey);
      hingeArcEl.setAttribute("opacity", 0.5 + gi * 0.5);
      var lR = arcR + 16;
      hingeArcLbl.setAttribute("x", HX + lR * Math.cos(mA - Math.PI/2));
      hingeArcLbl.setAttribute("y", HY + lR * Math.sin(mA - Math.PI/2) + 4);
      hingeArcLbl.setAttribute("opacity", 0.6 + gi * 0.4);
      hingeArcLbl.textContent = Math.abs(st.hd).toFixed(1) + "\u00B0";
    } else {
      hingeArcEl.setAttribute("opacity", 0);
      hingeArcLbl.setAttribute("opacity", 0);
    }

    // --- 后轮偏转角弧 ---
    rearArcs.forEach(function(ra){
      if(as > 1.5 && Math.abs(st.rd) > 0.15){
        var w = ra.w;
        var arcR = 26;
        var baseA = st.ra;
        var deflA = st.rd;
        var cosR = Math.cos(baseA * Math.PI / 180);
        var sinR = Math.sin(baseA * Math.PI / 180);
        var lx = w.cx - HX, ly = w.cy - HY;
        var wx = HX + lx * cosR - ly * sinR;
        var wy = HY + lx * sinR + ly * cosR;
        var s1 = -(baseA) * Math.PI / 180;
        var e1 = -(baseA + deflA) * Math.PI / 180;
        var s1x = wx + arcR * Math.cos(s1 - Math.PI/2);
        var s1y = wy + arcR * Math.sin(s1 - Math.PI/2);
        var e1x = wx + arcR * Math.cos(e1 - Math.PI/2);
        var e1y = wy + arcR * Math.sin(e1 - Math.PI/2);
        var sw = deflA > 0 ? 1 : 0;
        ra.arc.setAttribute("d", "M"+s1x+","+s1y+" A"+arcR+","+arcR+" 0 0,"+sw+" "+e1x+","+e1y);
        ra.arc.setAttribute("opacity", 0.5);
        var mid1 = (s1 + e1) / 2;
        ra.lbl.setAttribute("x", wx + (arcR+11) * Math.cos(mid1 - Math.PI/2));
        ra.lbl.setAttribute("y", wy + (arcR+11) * Math.sin(mid1 - Math.PI/2) + 3);
        ra.lbl.setAttribute("opacity", 0.5);
        ra.lbl.textContent = Math.abs(st.rd).toFixed(1) + "\u00B0";
      } else {
        ra.arc.setAttribute("opacity", 0);
        ra.lbl.setAttribute("opacity", 0);
      }
    });

    // --- 转弯半径 ---
    if(as > 2){
      var sRad = as * Math.PI / 180;
      var WB = 580;
      var Rno = WB / Math.sin(sRad);
      var Rwi = Rno * (1 - st.lf * 0.18 * Math.min(as/15, 1));
      var maxR = Math.min(Rno, 650);
      var cY = HY + maxR + 50;

      var r1 = Math.min(Rno, 650);
      var half = 280;
      var y1 = cY - Math.sqrt(Math.max(1, r1*r1 - half*half));
      radLine1.setAttribute("d", "M"+(HX-half)+","+y1+" A"+r1+","+r1+" 0 0,1 "+(HX+half)+","+y1);
      radLine1.setAttribute("opacity", "0.3");

      var r2 = Math.min(Rwi, 650);
      var y2 = cY - Math.sqrt(Math.max(1, r2*r2 - half*half));
      radLine2.setAttribute("d", "M"+(HX-half)+","+y2+" A"+r2+","+r2+" 0 0,1 "+(HX+half)+","+y2);
      radLine2.setAttribute("opacity", "0.5");

      // 差异填充
      if(Math.abs(y1-y2) > 2){
        radDiff.setAttribute("d",
          "M"+(HX-half)+","+y2+" A"+r2+","+r2+" 0 0,1 "+(HX+half)+","+y2+
          " L"+(HX+half)+","+y1+" A"+r1+","+r1+" 0 0,0 "+(HX-half)+","+y1+" Z");
        radDiff.setAttribute("opacity", "1");
      } else {
        radDiff.setAttribute("opacity", "0");
      }

      radLbl1.setAttribute("x", HX);
      radLbl1.setAttribute("y", y1 + 16);
      radLbl1.setAttribute("opacity", "0.5");
      radLbl1.textContent = "\u65E0\u968F\u52A8\u8F6C\u5F2F\u534A\u5F84";
      radLbl2.setAttribute("x", HX);
      radLbl2.setAttribute("y", y2 + 16);
      radLbl2.setAttribute("opacity", "0.7");
      radLbl2.textContent = "\u968F\u52A8\u8F6C\u5F2F\u534A\u5F84";
    } else {
      radLine1.setAttribute("opacity", "0");
      radLine2.setAttribute("opacity", "0");
      radDiff.setAttribute("opacity", "0");
      radLbl1.setAttribute("opacity", "0");
      radLbl2.setAttribute("opacity", "0");
    }

    // --- 载重块 ---
    var vis = Math.round(loadPct / 20);
    loadBlocks.forEach(function(b, i){
      b.setAttribute("opacity", i < vis ? (0.35 + loadPct/200) : 0.08);
      b.setAttribute("fill", i < vis ? "#2a2a10" : "#1a1a10");
    });

    // --- 后轮轨迹粒子 ---
    if(as > 3 && Math.abs(st.rd) > 0.3){
      rearWheels.forEach(function(w){
        var cosR = Math.cos(st.ra * Math.PI / 180);
        var sinR = Math.sin(st.ra * Math.PI / 180);
        var lx = w.cx - HX, ly = w.cy - HY;
        var wx = HX + lx * cosR - ly * sinR;
        var wy = HY + lx * sinR + ly * cosR;
        if(trails.length < MAX_TRAILS){
          var dot = $("circle", {cx:wx,cy:wy,r:2.5,fill:C.orange,opacity:"0.5"}, trailGrp);
          trails.push({el:dot, life:1});
        }
      });
    }
    // 更新轨迹粒子
    for(var i = trails.length - 1; i >= 0; i--){
      trails[i].life -= 0.015;
      if(trails[i].life <= 0){
        trailGrp.removeChild(trails[i].el);
        trails.splice(i, 1);
      } else {
        trails[i].el.setAttribute("opacity", trails[i].life * 0.4);
        trails[i].el.setAttribute("r", trails[i].life * 2.5);
      }
    }

    // --- 参数面板 ---
    paramEls.pSteer.textContent = steerAngle.toFixed(1) + "\u00B0";
    paramEls.pHinge.textContent = st.hd.toFixed(1) + "\u00B0";
    paramEls.pRear.textContent = st.rd.toFixed(2) + "\u00B0";
    if(as > 2){
      var sR2 = as * Math.PI / 180;
      var RR = 580 / Math.sin(sR2);
      var RRe = RR * (1 - st.lf * 0.18 * Math.min(as/15, 1));
      paramEls.pRad.textContent = (RRe / 58).toFixed(1) + "m";
    } else {
      paramEls.pRad.textContent = "\u2014";
    }
    paramEls.pLoad.textContent = (st.lf * 100).toFixed(0) + "%";
    if(as < 0.5){
      paramEls.pStatus.textContent = "\u76F4\u884C";
      paramEls.pStatus.setAttribute("fill", C.teal);
    } else if(st.lf > 0.3){
      paramEls.pStatus.textContent = "\u968F\u52A8\u6709\u6548";
      paramEls.pStatus.setAttribute("fill", C.green);
    } else {
      paramEls.pStatus.textContent = "\u968F\u52A8\u5931\u6548";
      paramEls.pStatus.setAttribute("fill", C.red);
    }
  }

  /* ====== 动画循环 ====== */
  function loop(){
    time++;
    if(isAuto){
      autoTime += 0.012;
      steerAngle = Math.sin(autoTime) * 25;
      document.getElementById("steerSlider").value = steerAngle;
      document.getElementById("steerVal").textContent = steerAngle.toFixed(1) + "\u00B0";
    }
    update();
    requestAnimationFrame(loop);
  }

  /* ====== 事件绑定 ====== */
  document.getElementById("steerSlider").addEventListener("input", function(){
    if(isAuto) return;
    steerAngle = +this.value;
    document.getElementById("steerVal").textContent = steerAngle.toFixed(1) + "\u00B0";
  });
  document.getElementById("loadSlider").addEventListener("input", function(){
    loadPct = +this.value;
    document.getElementById("loadVal").textContent = loadPct + "%";
  });
  document.getElementById("autoBtn").addEventListener("click", function(){
    isAuto = !isAuto;
    this.classList.toggle("on", isAuto);
    if(isAuto) autoTime = Math.asin(steerAngle / 25);
  });
  document.getElementById("resetBtn").addEventListener("click", function(){
    isAuto = false;
    document.getElementById("autoBtn").classList.remove("on");
    steerAngle = 0; loadPct = 80;
    document.getElementById("steerSlider").value = 0;
    document.getElementById("steerVal").textContent = "0.0\u00B0";
    document.getElementById("loadSlider").value = 80;
    document.getElementById("loadVal").textContent = "80%";
    // 清除轨迹
    trails.forEach(function(t){ trailGrp.removeChild(t.el); });
    trails = [];
  });
  document.addEventListener("keydown", function(e){
    if(e.key === "ArrowLeft"){
      e.preventDefault();
      steerAngle = Math.max(-30, steerAngle - 2);
      if(!isAuto){
        document.getElementById("steerSlider").value = steerAngle;
        document.getElementById("steerVal").textContent = steerAngle.toFixed(1) + "\u00B0";
      }
    } else if(e.key === "ArrowRight"){
      e.preventDefault();
      steerAngle = Math.min(30, steerAngle + 2);
      if(!isAuto){
        document.getElementById("steerSlider").value = steerAngle;
        document.getElementById("steerVal").textContent = steerAngle.toFixed(1) + "\u00B0";
      }
    } else if(e.key === " "){
      e.preventDefault();
      document.getElementById("autoBtn").click();
    }
  });

  /* ====== 启动 ====== */
  loop();

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

实现说明:

这次重写彻底解决了之前的渲染问题,核心修复点:

  1. SVG 属性使用直接颜色值:之前在 SVG presentation attributes(如 fillstroke)中使用了 CSS 变量 var(--xxx),这在大多数浏览器的 SVG 属性中不生效。现在全部替换为直接十六进制颜色值。

  2. IIFE 封装:所有代码包裹在立即执行函数中,避免全局变量污染和时序问题,确保 DOM 查询在元素创建后才执行。

  3. DOM 引用前置保存:所有需要动态更新的 SVG 元素在创建时就被保存到变量中(如 radLine1hingeGlowElrearWheels 等),不再通过运行时 querySelector 查找,避免查询失败。

动画核心特性:

  • 前段/后段车架独立旋转:前段绕铰接点旋转 frontAngle,后段旋转 rearAngle = frontAngle - hingeDeform,铰接变形角 = 两者之差
  • 4阶段力流箭头:绿色(转向输入)→金色(离心力)→粉红(铰接变形)→橙色(随动偏转),虚线流动动画
  • 铰接点橡胶块实时变形:4块橡胶根据弯曲方向压缩/拉伸,放大镜视图中同步显示并标注硬度值(70A/80A)
  • 转弯半径对比弧:红色虚线(无随动) vs 蓝色实线(随动有效),中间半透明填充显示半径差
  • 后轮轨迹粒子:转向时后轮位置留下渐隐粒子,直观展示随动偏转效果
  • 载重边界条件可视化:载重低于30%时状态变红显示"随动失效",体现方案适用边界
积分规则:第一轮对话扣减6分,后续每轮扣4分