<!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>
实现说明:
这次重写彻底解决了之前的渲染问题,核心修复点:
SVG 属性使用直接颜色值:之前在 SVG presentation attributes(如
fill、stroke)中使用了 CSS 变量var(--xxx),这在大多数浏览器的 SVG 属性中不生效。现在全部替换为直接十六进制颜色值。IIFE 封装:所有代码包裹在立即执行函数中,避免全局变量污染和时序问题,确保 DOM 查询在元素创建后才执行。
DOM 引用前置保存:所有需要动态更新的 SVG 元素在创建时就被保存到变量中(如
radLine1、hingeGlowEl、rearWheels等),不再通过运行时querySelector查找,避免查询失败。
动画核心特性:
- 前段/后段车架独立旋转:前段绕铰接点旋转
frontAngle,后段旋转rearAngle = frontAngle - hingeDeform,铰接变形角 = 两者之差 - 4阶段力流箭头:绿色(转向输入)→金色(离心力)→粉红(铰接变形)→橙色(随动偏转),虚线流动动画
- 铰接点橡胶块实时变形:4块橡胶根据弯曲方向压缩/拉伸,放大镜视图中同步显示并标注硬度值(70A/80A)
- 转弯半径对比弧:红色虚线(无随动) vs 蓝色实线(随动有效),中间半透明填充显示半径差
- 后轮轨迹粒子:转向时后轮位置留下渐隐粒子,直观展示随动偏转效果
- 载重边界条件可视化:载重低于30%时状态变红显示"随动失效",体现方案适用边界
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
