<!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 原理动画,核心设计思路如下:
IFR 理想解聚焦:动画直接展示最终理想解的运行状态——非圆轮廓的几何形状本身即转向函数,势能直接转化为正弦转向,无中间传动环节。左下角「核心创新」标注框明确点明这一点。
视觉引导机制:
- 绳索差动可视化:左绳(青色)与右绳(品红)的粗细和透明度随张力实时变化——拉紧侧高亮加粗,放松侧变暗变细,直观呈现「一拉一松」的差动原理
- 非圆轮廓:绕线轮的波浪形轮廓持续旋转,两个标记点(峰/谷)随轮廓径向脉动,半径指示线动态伸缩
- 能量流向:虚线箭头从重物经轮到转向臂,标示能量传递路径
三大联动视图:
- 左:机构原理图 — 非圆轮旋转、重物下落、滑轮转向、转向臂摆动、回位弹簧伸缩、前轮偏转,全部实时联动
- 右上:俯视轨迹 — 车辆沿 S 型曲线实时行驶,4 个障碍物标注于旁
- 右下:转向角曲线 — 正弦曲线图实时描绘,当前相位点高亮追踪
交互控制:偏心距 e(影响转向幅度)、周期数 n(影响 S 弯数量)、运行速度、弹簧刚度(模拟刚度失配带来的转向滞后)均可实时调节,让用户亲手体验参数对理想解行为的影响。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
