<!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;400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{
--bg:#050a15;--fg:#c8d6e5;--muted:#3d5168;--accent:#00e5ff;
--tension:#00e5ff;--compression:#ff6b35;--danger:#ff2d55;
--success:#00ff88;--beam:#8faabe;--card:rgba(8,18,36,0.92);
--border:rgba(0,229,255,0.12);
}
*{margin:0;padding:0;box-sizing:border-box}
body{
background:var(--bg);color:var(--fg);font-family:'Rajdhani',sans-serif;
min-height:100vh;overflow:hidden;display:flex;flex-direction:column;
background:radial-gradient(ellipse at 40% 40%,#0c1a30 0%,#050a15 70%);
}
header{
padding:16px 28px 8px;display:flex;align-items:center;gap:16px;
border-bottom:1px solid var(--border);flex-shrink:0;
background:linear-gradient(180deg,rgba(0,229,255,0.03) 0%,transparent 100%);
}
header .logo{
width:36px;height:36px;border:2px solid var(--accent);border-radius:6px;
display:flex;align-items:center;justify-content:center;
font-family:'Share Tech Mono',monospace;font-size:14px;color:var(--accent);
letter-spacing:-1px;flex-shrink:0;
}
header h1{font-size:20px;font-weight:600;letter-spacing:1px;color:#e8f0f8}
header .subtitle{font-size:13px;color:var(--muted);font-weight:400;margin-left:auto;font-family:'Share Tech Mono',monospace}
.main-wrap{flex:1;display:flex;position:relative;overflow:hidden}
.svg-wrap{flex:1;display:flex;align-items:center;justify-content:center;position:relative}
.svg-wrap svg{width:100%;height:100%;max-height:calc(100vh - 60px)}
.panel{
width:280px;flex-shrink:0;padding:16px;overflow-y:auto;
border-left:1px solid var(--border);background:var(--card);
display:flex;flex-direction:column;gap:14px;
scrollbar-width:thin;scrollbar-color:var(--muted) transparent;
}
.section-title{
font-size:11px;text-transform:uppercase;letter-spacing:2px;
color:var(--muted);font-family:'Share Tech Mono',monospace;
margin-bottom:4px;
}
.mode-btns{display:flex;flex-direction:column;gap:6px}
.mode-btn{
padding:10px 14px;border:1px solid var(--border);border-radius:6px;
background:transparent;color:var(--fg);font-family:'Rajdhani',sans-serif;
font-size:14px;font-weight:500;cursor:pointer;transition:all .25s;
display:flex;align-items:center;gap:10px;text-align:left;
}
.mode-btn i{width:18px;text-align:center;font-size:13px;color:var(--muted);transition:color .25s}
.mode-btn:hover{border-color:rgba(0,229,255,0.3);background:rgba(0,229,255,0.04)}
.mode-btn.active{
border-color:var(--accent);background:rgba(0,229,255,0.08);
color:#fff;box-shadow:0 0 20px rgba(0,229,255,0.08);
}
.mode-btn.active i{color:var(--accent)}
.slider-group{display:flex;flex-direction:column;gap:6px}
.slider-label{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
.slider-label .val{color:var(--accent);font-family:'Share Tech Mono',monospace}
input[type=range]{
-webkit-appearance:none;width:100%;height:4px;border-radius:2px;
background:var(--border);outline:none;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:16px;height:16px;border-radius:50%;
background:var(--accent);cursor:pointer;box-shadow:0 0 8px rgba(0,229,255,0.4);
}
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.info-card{
padding:10px;border-radius:6px;border:1px solid var(--border);
background:rgba(0,229,255,0.02);
}
.info-card .label{font-size:10px;color:var(--muted);font-family:'Share Tech Mono',monospace;text-transform:uppercase;letter-spacing:1px}
.info-card .value{font-size:20px;font-weight:700;color:#e8f0f8;margin-top:2px;font-family:'Share Tech Mono',monospace}
.info-card .value.cyan{color:var(--accent)}
.info-card .value.green{color:var(--success)}
.info-card .value.orange{color:var(--compression)}
.info-card .value.red{color:var(--danger)}
.legend{display:flex;flex-direction:column;gap:6px}
.legend-item{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}
.legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.status-bar{
padding:8px 12px;border-radius:6px;font-size:12px;
font-family:'Share Tech Mono',monospace;text-align:center;
border:1px solid;transition:all .4s;
}
.status-bar.normal{border-color:rgba(0,255,136,0.3);background:rgba(0,255,136,0.06);color:var(--success)}
.status-bar.warning{border-color:rgba(255,107,53,0.3);background:rgba(255,107,53,0.06);color:var(--compression)}
.status-bar.danger{border-color:rgba(255,45,85,0.3);background:rgba(255,45,85,0.06);color:var(--danger)}
.ifr-note{
padding:12px;border-radius:6px;font-size:12px;line-height:1.6;
border:1px solid rgba(0,229,255,0.15);background:rgba(0,229,255,0.03);
color:rgba(200,214,229,0.8);
}
.ifr-note strong{color:var(--accent);font-weight:600}
@keyframes pulse-glow{
0%,100%{opacity:0.6}50%{opacity:1}
}
@keyframes fade-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.panel>*{animation:fade-in .5s ease both}
.panel>*:nth-child(2){animation-delay:.05s}
.panel>*:nth-child(3){animation-delay:.1s}
.panel>*:nth-child(4){animation-delay:.15s}
.panel>*:nth-child(5){animation-delay:.2s}
.panel>*:nth-child(6){animation-delay:.25s}
.panel>*:nth-child(7){animation-delay:.3s}
.panel>*:nth-child(8){animation-delay:.35s}
@media(max-width:900px){
.main-wrap{flex-direction:column}
.panel{width:100%;max-height:40vh;border-left:none;border-top:1px solid var(--border);flex-direction:row;flex-wrap:wrap}
.panel>*{min-width:200px;flex:1}
}
</style>
</head>
<body>
<header>
<div class="logo">IFR</div>
<h1>三维空间三角桁架底盘 — 最终理想解原理动画</h1>
<span class="subtitle">TRIZ / Ideal Final Result</span>
</header>
<div class="main-wrap">
<div class="svg-wrap">
<svg id="mainSvg" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid meet">
<defs>
<filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowCyanStrong" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="8" result="b"/><feGaussianBlur stdDeviation="3" in="SourceGraphic" result="b2"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="b2"/></feMerge>
</filter>
<filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowRed" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowOrange" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowWhite" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<pattern id="gridPattern" width="50" height="50" patternUnits="userSpaceOnUse">
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(0,229,255,0.04)" stroke-width="0.5"/>
</pattern>
<linearGradient id="beamGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6d8aa0"/><stop offset="50%" stop-color="#a0bcd0"/><stop offset="100%" stop-color="#6d8aa0"/>
</linearGradient>
<marker id="arrowCyan" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(0,229,255,0.6)"/>
</marker>
<marker id="arrowRed" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(255,45,85,0.7)"/>
</marker>
<marker id="arrowGreen" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(0,255,136,0.7)"/>
</marker>
</defs>
<rect width="1200" height="800" fill="url(#gridPattern)"/>
<g id="scene"></g>
</svg>
</div>
<aside class="panel">
<div>
<div class="section-title">工况模式</div>
<div class="mode-btns">
<button class="mode-btn active" data-mode="empty"><i class="fas fa-feather"></i>空载启动</button>
<button class="mode-btn" data-mode="full"><i class="fas fa-weight-hanging"></i>满载行驶 (50kg)</button>
<button class="mode-btn" data-mode="corner"><i class="fas fa-route"></i>过弯扭剪</button>
<button class="mode-btn" data-mode="fail"><i class="fas fa-exclamation-triangle"></i>失效边界</button>
</div>
</div>
<div>
<div class="section-title">载荷控制</div>
<div class="slider-group">
<div class="slider-label"><span>施加载荷</span><span class="val" id="loadVal">0 kg</span></div>
<input type="range" id="loadSlider" min="0" max="50" value="0" step="1">
</div>
</div>
<div>
<div class="section-title">实时参数</div>
<div class="info-grid">
<div class="info-card"><div class="label">载荷</div><div class="value cyan" id="infoLoad">0 kg</div></div>
<div class="info-card"><div class="label">最大变形</div><div class="value green" id="infoDeform">0.00 mm</div></div>
<div class="info-card"><div class="label">杆件拉力</div><div class="value cyan" id="infoTension">200 N</div></div>
<div class="info-card"><div class="label">梁弯矩</div><div class="value orange" id="infoBending">0 N·m</div></div>
</div>
</div>
<div id="statusBar" class="status-bar normal">系统正常 — 预紧力维持刚度</div>
<div>
<div class="section-title">图例</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#8faabe"></div>铝型材主纵梁 (40×20×1.5mm)</div>
<div class="legend-item"><div class="legend-dot" style="background:#00e5ff"></div>碳纤维斜拉杆 (受拉 · φ8mm)</div>
<div class="legend-item"><div class="legend-dot" style="background:#ff6b35"></div>局部压应力区</div>
<div class="legend-item"><div class="legend-dot" style="background:#fff"></div>球铰节点 (3D打印铝合金)</div>
<div class="legend-item"><div class="legend-dot" style="background:#ff2d55"></div>外载荷 / 危险区域</div>
<div class="legend-item"><div class="legend-dot" style="background:#00ff88"></div>支座反力</div>
</div>
</div>
<div class="ifr-note" id="ifrNote">
<strong>IFR 理想解:</strong>三角桁架几何将弯矩转化为轴向拉压力——载荷越重,碳纤维杆预紧力激活越充分,刚度反而越高。载荷自身成为结构刚度的资源。
</div>
</aside>
</div>
<script>
// ===== 配置 =====
const SVG_NS = 'http://www.w3.org/2000/svg';
const CX = 560, CY = 410; // SVG中心偏移
const SCALE = 1.05;
// ===== 3D节点定义 (mm单位,放大显示) =====
const NODES_BASE = {
TFL:[0,0,160], TFR:[500,0,160], TBL:[0,260,160], TBR:[500,260,160],
BFL:[0,0,0], BFR:[500,0,0], BBL:[0,260,0], BBR:[500,260,0]
};
// ===== 杆件定义 =====
const BEAMS = [
{id:'bTF', from:'TFL',to:'TFR',label:'前横梁'},
{id:'bTB', from:'TBL',to:'TBR',label:'后横梁'},
{id:'bL', from:'TFL',to:'TBL',label:'左纵梁'},
{id:'bR', from:'TFR',to:'TBR',label:'右纵梁'},
];
const RODS = [
{id:'rF1',from:'TFL',to:'BFR',face:'front'},
{id:'rF2',from:'TFR',to:'BFL',face:'front'},
{id:'rB1',from:'TBL',to:'BBR',face:'back'},
{id:'rB2',from:'TBR',to:'BBL',face:'back'},
{id:'rL1',from:'TFL',to:'BBL',face:'left'},
{id:'rL2',from:'TBL',to:'BFL',face:'left'},
{id:'rR1',from:'TFR',to:'BBR',face:'right'},
{id:'rR2',from:'TBR',to:'BFR',face:'right'},
];
const EDGES = [
{id:'eF',from:'BFL',to:'BFR'},
{id:'eB',from:'BBL',to:'BBR'},
{id:'eL',from:'BFL',to:'BBL'},
{id:'eR',from:'BFR',to:'BBR'},
];
// ===== 状态 =====
let currentMode = 'empty';
let loadKg = 0;
let rotY = -0.55; // 视角绕Y轴旋转
let rotX = -0.42; // 视角绕X轴旋转
let isDragging = false, dragStartX=0, dragStartY=0, dragStartRotY=0, dragStartRotX=0;
let time = 0;
let nodesDeformed = {};
let particles = [];
let introProgress = 0; // 0~1 入场动画进度
// ===== 等轴测投影 =====
function project(x, y, z) {
// 居中
x -= 250; y -= 130; z -= 80;
// 绕Y轴旋转
const cy = Math.cos(rotY), sy = Math.sin(rotY);
let rx = x*cy + y*sy;
let ry = -x*sy + y*cy;
let rz = z;
// 绕X轴旋转
const cx = Math.cos(rotX), sx = Math.sin(rotX);
let fy = ry*cx - rz*sx;
let fz = ry*sx + rz*cx;
return { x: rx*SCALE + CX, y: -fy*SCALE + CY, depth: fz };
}
function projNode(id) {
const p = nodesDeformed[id] || NODES_BASE[id];
return project(p[0], p[1], p[2]);
}
// ===== SVG辅助 =====
function svgEl(tag, attrs, parent) {
const el = document.createElementNS(SVG_NS, tag);
for (const [k,v] of Object.entries(attrs||{})) el.setAttribute(k, v);
if (parent) parent.appendChild(el);
return el;
}
const scene = document.getElementById('scene');
let svgGroups = {};
function initSVG() {
// 按深度排序创建分组
svgGroups.triFill = svgEl('g',{id:'triFill'}, scene);
svgGroups.edges = svgEl('g',{id:'edges'}, scene);
svgGroups.rods = svgEl('g',{id:'rods'}, scene);
svgGroups.beams = svgEl('g',{id:'beams'}, scene);
svgGroups.particles = svgEl('g',{id:'particles'}, scene);
svgGroups.nodes = svgEl('g',{id:'nodes'}, scene);
svgGroups.arrows = svgEl('g',{id:'arrows'}, scene);
svgGroups.labels = svgEl('g',{id:'labels'}, scene);
// 创建底边
EDGES.forEach(e => {
e.el = svgEl('line',{stroke:'rgba(100,130,160,0.15)','stroke-width':1.5,'stroke-dasharray':'4,4'}, svgGroups.edges);
});
// 创建CF杆
RODS.forEach(r => {
r.elBg = svgEl('line',{stroke:'rgba(0,229,255,0.06)','stroke-width':3,'stroke-linecap':'round'}, svgGroups.rods);
r.el = svgEl('line',{stroke:'rgba(42,48,64,0.7)','stroke-width':2,'stroke-linecap':'round'}, svgGroups.rods);
r.elGlow = svgEl('line',{stroke:'rgba(0,229,255,0)','stroke-width':3,'stroke-linecap':'round',filter:'url(#glowCyan)'}, svgGroups.rods);
});
// 创建铝梁
BEAMS.forEach(b => {
b.elBg = svgEl('line',{stroke:'rgba(143,170,190,0.1)','stroke-width':8,'stroke-linecap':'round'}, svgGroups.beams);
b.el = svgEl('line',{stroke:'url(#beamGrad)','stroke-width':5,'stroke-linecap':'round'}, svgGroups.beams);
});
// 创建节点
Object.keys(NODES_BASE).forEach(id => {
const isTop = id.startsWith('T');
const g = svgEl('g',{}, svgGroups.nodes);
const glow = svgEl('circle',{r:isTop?10:8,fill:isTop?'rgba(0,229,255,0.08)':'rgba(0,255,136,0.06)',filter:'url(#glowWhite)'}, g);
const circle = svgEl('circle',{r:isTop?5:4,fill:isTop?'#e0eaf2':'#a0bcd0',stroke:isTop?'rgba(0,229,255,0.5)':'rgba(0,255,136,0.3)','stroke-width':1.5}, g);
nodesDeformed[id] = [...NODES_BASE[id]];
NODES_BASE[id]._el = {g, glow, circle};
});
// 初始化粒子
RODS.forEach(r => {
for (let i = 0; i < 4; i++) {
const p = svgEl('circle',{r:2,fill:'rgba(0,229,255,0)',filter:'url(#glowCyan)'}, svgGroups.particles);
particles.push({rod:r, progress:Math.random(), speed:0.003, el:p, active:false});
}
});
}
// ===== 变形计算 =====
function computeDeformation() {
// 重置
Object.keys(NODES_BASE).forEach(id => {
nodesDeformed[id] = [...NODES_BASE[id]];
});
const loadFactor = loadKg / 50;
const maxDeform = 18; // 夸张的变形量(像素级)
if (currentMode === 'empty') {
// 微小预变形
['TFL','TFR','TBL','TBR'].forEach(id => {
nodesDeformed[id][2] -= 1.5;
});
} else if (currentMode === 'full') {
// 对称下沉
['TFL','TFR','TBL','TBR'].forEach(id => {
nodesDeformed[id][2] -= maxDeform * loadFactor;
});
// 底部节点微升(反力)
['BFL','BFR','BBL','BBR'].forEach(id => {
nodesDeformed[id][2] += 1.5 * loadFactor;
});
} else if (currentMode === 'corner') {
// 左侧下沉,右侧微升(扭转)
nodesDeformed['TFL'][2] -= maxDeform * loadFactor * 1.1;
nodesDeformed['TBL'][2] -= maxDeform * loadFactor * 0.9;
nodesDeformed['TFR'][2] -= maxDeform * loadFactor * 0.3;
nodesDeformed['TBR'][2] -= maxDeform * loadFactor * 0.2;
// 底部反力不对称
nodesDeformed['BFL'][2] += 2 * loadFactor;
nodesDeformed['BBL'][2] += 1.5 * loadFactor;
} else if (currentMode === 'fail') {
// 跨中无节点支撑——左纵梁跨中严重下挠
// TFL和TBL之间没有中间节点,纵梁中点下挠
// 我们通过让TFL和TBL之间的梁弯曲来表现
// 简化:TFL和TBL下沉更多
nodesDeformed['TFL'][2] -= maxDeform * 2.5;
nodesDeformed['TBL'][2] -= maxDeform * 2.5;
nodesDeformed['TFR'][2] -= maxDeform * 0.5;
nodesDeformed['TBR'][2] -= maxDeform * 0.5;
}
}
// ===== 渲染 =====
function render() {
computeDeformation();
// 投影所有节点
const proj = {};
Object.keys(NODES_BASE).forEach(id => { proj[id] = projNode(id); });
// 计算杆件深度排序
const rodOrder = [...RODS].sort((a,b) => {
const da = (proj[a.from].depth + proj[a.to].depth)/2;
const db = (proj[b.from].depth + proj[b.to].depth)/2;
return da - db; // 远的先画
});
// 重新排列SVG元素顺序
rodOrder.forEach(r => {
svgGroups.rods.appendChild(r.elBg);
svgGroups.rods.appendChild(r.el);
svgGroups.rods.appendChild(r.elGlow);
});
// 更新底边
EDGES.forEach(e => {
const p1 = proj[e.from], p2 = proj[e.to];
e.el.setAttribute('x1',p1.x); e.el.setAttribute('y1',p1.y);
e.el.setAttribute('x2',p2.x); e.el.setAttribute('y2',p2.y);
});
// 更新CF杆
const loadFactor = loadKg / 50;
RODS.forEach(r => {
const p1 = proj[r.from], p2 = proj[r.to];
[r.elBg, r.el, r.elGlow].forEach(el => {
el.setAttribute('x1',p1.x); el.setAttribute('y1',p1.y);
el.setAttribute('x2',p2.x); el.setAttribute('y2',p2.y);
});
// 根据模式设置杆件发光
let glowIntensity = 0;
let rodOpacity = 0.7;
if (currentMode === 'empty') {
glowIntensity = 0.15; // 预紧力微光
rodOpacity = 0.6;
} else if (currentMode === 'full') {
glowIntensity = 0.3 + 0.5 * loadFactor;
rodOpacity = 0.4;
} else if (currentMode === 'corner') {
// 判断杆件在哪一侧
const isLeftRod = (r.from.includes('L') || r.to.includes('L')) && r.face !== 'front' && r.face !== 'back';
const isCrossRod = r.face === 'front' || r.face === 'back';
if (isLeftRod) glowIntensity = 0.6 + 0.4 * loadFactor;
else if (isCrossRod) glowIntensity = 0.4 + 0.4 * loadFactor;
else glowIntensity = 0.2 + 0.2 * loadFactor;
rodOpacity = 0.35;
} else if (currentMode === 'fail') {
glowIntensity = r.face === 'left' ? 0.1 : 0.2;
rodOpacity = 0.4;
}
r.elGlow.setAttribute('stroke', `rgba(0,229,255,${glowIntensity})`);
r.el.setAttribute('stroke', `rgba(42,48,64,${rodOpacity})`);
r.elBg.setAttribute('stroke', `rgba(0,229,255,${glowIntensity*0.2})`);
});
// 更新铝梁
BEAMS.forEach(b => {
const p1 = proj[b.from], p2 = proj[b.to];
[b.elBg, b.el].forEach(el => {
el.setAttribute('x1',p1.x); el.setAttribute('y1',p1.y);
el.setAttribute('x2',p2.x); el.setAttribute('y2',p2.y);
});
// 失效模式下左纵梁变红
if (currentMode === 'fail' && (b.id === 'bL')) {
b.el.setAttribute('stroke', '#ff2d55');
b.el.setAttribute('stroke-width', 5);
b.elBg.setAttribute('stroke', 'rgba(255,45,85,0.15)');
b.elBg.setAttribute('stroke-width', 12);
} else {
b.el.setAttribute('stroke', 'url(#beamGrad)');
b.el.setAttribute('stroke-width', 5);
b.elBg.setAttribute('stroke', 'rgba(143,170,190,0.1)');
b.elBg.setAttribute('stroke-width', 8);
}
});
// 更新节点
Object.keys(NODES_BASE).forEach(id => {
const p = proj[id];
const isTop = id.startsWith('T');
const el = NODES_BASE[id]._el;
el.g.setAttribute('transform', `translate(${p.x},${p.y})`);
// 节点发光强度
let glowR = isTop ? 10 : 8;
let glowColor = isTop ? 'rgba(0,229,255,0.08)' : 'rgba(0,255,136,0.06)';
let nodeColor = isTop ? '#e0eaf2' : '#a0bcd0';
let nodeStroke = isTop ? 'rgba(0,229,255,0.5)' : 'rgba(0,255,136,0.3)';
if (currentMode === 'fail' && id.includes('L') && isTop) {
glowColor = 'rgba(255,45,85,0.15)';
nodeColor = '#ff6b80';
nodeStroke = 'rgba(255,45,85,0.6)';
glowR = 14;
}
// 脉动效果
const pulse = 1 + 0.08 * Math.sin(time * 2 + p.depth * 0.01);
el.glow.setAttribute('r', glowR * pulse);
el.glow.setAttribute('fill', glowColor);
el.circle.setAttribute('fill', nodeColor);
el.circle.setAttribute('stroke', nodeStroke);
});
// 清除旧的箭头和标签
svgGroups.arrows.innerHTML = '';
svgGroups.labels.innerHTML = '';
svgGroups.triFill.innerHTML = '';
// 绘制三角形填充(展示三角结构)
if (currentMode !== 'fail') {
drawTriangleFills(proj);
}
// 绘制载荷箭头
drawLoadArrows(proj);
// 绘制支座反力
drawReactionArrows(proj);
// 绘制注释
drawAnnotations(proj);
// 更新粒子
updateParticles(proj);
// 失效模式下的屈曲可视化
if (currentMode === 'fail') {
drawBuckling(proj);
}
}
// ===== 三角形填充 =====
function drawTriangleFills(proj) {
const loadFactor = loadKg / 50;
const alpha = currentMode === 'empty' ? 0.02 : 0.03 + 0.04 * loadFactor;
// 每个面两个三角形
const faces = [
// 前面
[proj.TFL, proj.TFR, proj.BFR],
[proj.TFL, proj.BFL, proj.BFR],
// 后面
[proj.TBL, proj.TBR, proj.BBR],
[proj.TBL, proj.BBL, proj.BBR],
// 左面
[proj.TFL, proj.TBL, proj.BBL],
[proj.TFL, proj.BFL, proj.BBL],
// 右面
[proj.TFR, proj.TBR, proj.BBR],
[proj.TFR, proj.BFR, proj.BBR],
];
faces.forEach(tri => {
const path = svgEl('path',{
d: `M${tri[0].x},${tri[0].y} L${tri[1].x},${tri[1].y} L${tri[2].x},${tri[2].y} Z`,
fill: `rgba(0,229,255,${alpha})`,
stroke: 'none'
}, svgGroups.triFill);
});
}
// ===== 载荷箭头 =====
function drawLoadArrows(proj) {
if (loadKg <= 0 && currentMode === 'empty') return;
const loadFactor = loadKg / 50;
const arrowLen = 30 + 40 * loadFactor;
if (currentMode === 'full' || currentMode === 'corner') {
// 顶部节点向下箭头
const topNodes = ['TFL','TFR','TBL','TBR'];
topNodes.forEach(id => {
const p = proj[id];
let len = arrowLen;
let color = 'rgba(255,45,85,0.7)';
let w = 2;
if (currentMode === 'corner') {
if (id.includes('L')) { len = arrowLen * 1.3; w = 2.5; }
else { len = arrowLen * 0.5; w = 1.5; color = 'rgba(255,45,85,0.4)'; }
}
svgEl('line',{
x1:p.x, y1:p.y - len - 8, x2:p.x, y2:p.y - 8,
stroke:color, 'stroke-width':w, 'marker-end':'url(#arrowRed)'
}, svgGroups.arrows);
// 载荷值标签
if (id === 'TFL' || (currentMode === 'corner' && id === 'TBL')) {
const kg = currentMode === 'corner' && id.includes('L') ? Math.round(loadKg*0.65) : Math.round(loadKg/4);
svgEl('text',{
x:p.x, y:p.y - len - 14,
fill:'rgba(255,45,85,0.8)', 'font-size':'11',
'font-family':'Share Tech Mono, monospace',
'text-anchor':'middle'
}, svgGroups.arrows).textContent = `${kg}kg`;
}
});
// 总载荷标注
const cx = (proj.TFL.x + proj.TBR.x) / 2;
const cy = Math.min(proj.TFL.y, proj.TFR.y, proj.TBL.y, proj.TBR.y) - arrowLen - 35;
svgEl('text',{
x:cx, y:cy,
fill:'rgba(255,45,85,0.9)', 'font-size':'14', 'font-weight':'600',
'font-family':'Rajdhani, sans-serif', 'text-anchor':'middle'
}, svgGroups.arrows).textContent = `总载荷 ${loadKg}kg ↓`;
}
if (currentMode === 'fail') {
// 失效模式:跨中集中力
const midX = (proj.TFL.x + proj.TBL.x) / 2;
const midY = (proj.TFL.y + proj.TBL.y) / 2;
const len = 70;
svgEl('line',{
x1:midX, y1:midY - len - 8, x2:midX, y2:midY - 8,
stroke:'rgba(255,45,85,0.9)', 'stroke-width':3, 'marker-end':'url(#arrowRed)'
}, svgGroups.arrows);
svgEl('text',{
x:midX, y:midY - len - 14,
fill:'#ff2d55', 'font-size':'13', 'font-weight':'600',
'font-family':'Rajdhani, sans-serif', 'text-anchor':'middle'
}, svgGroups.arrows).textContent = '跨中集中载荷 (无节点支撑)';
}
}
// ===== 支座反力 =====
function drawReactionArrows(proj) {
if (currentMode === 'empty') return;
const loadFactor = loadKg / 50;
const arrowLen = 20 + 25 * loadFactor;
const bottomNodes = ['BFL','BFR','BBL','BBR'];
bottomNodes.forEach(id => {
const p = proj[id];
let len = arrowLen;
let color = 'rgba(0,255,136,0.6)';
if (currentMode === 'corner') {
if (id.includes('L')) { len = arrowLen * 1.2; color = 'rgba(0,255,136,0.8)'; }
else { len = arrowLen * 0.5; color = 'rgba(0,255,136,0.3)'; }
}
if (currentMode === 'fail' && id.includes('L')) {
color = 'rgba(255,107,53,0.6)';
}
svgEl('line',{
x1:p.x, y1:p.y + 10, x2:p.x, y2:p.y + 10 + len,
stroke:color, 'stroke-width':1.5, 'marker-end':'url(#arrowGreen)'
}, svgGroups.arrows);
// 车轮符号
svgEl('circle',{
cx:p.x, cy:p.y + 10 + len + 8, r:5,
fill:'none', stroke:color, 'stroke-width':1.5
}, svgGroups.arrows);
svgEl('line',{
x1:p.x-5, y1:p.y+10+len+8, x2:p.x+5, y2:p.y+10+len+8,
stroke:color, 'stroke-width':1
}, svgGroups.arrows);
});
}
// ===== 注释 =====
function drawAnnotations(proj) {
const loadFactor = loadKg / 50;
if (currentMode === 'full' && loadKg > 10) {
// 弯矩→轴力 标注
const cx = (proj.TFL.x + proj.TBR.x) / 2;
const cy = (proj.TFL.y + proj.BBR.y) / 2;
// 三角结构消除弯矩
svgEl('text',{
x:cx - 80, y:cy - 10,
fill:'rgba(0,229,255,0.7)', 'font-size':'13', 'font-weight':'600',
'font-family':'Rajdhani, sans-serif'
}, svgGroups.labels).textContent = '弯矩 → 轴力';
svgEl('text',{
x:cx - 80, y:cy + 8,
fill:'rgba(200,214,229,0.4)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = '三角结构消除节点弯矩';
// 变形量标注
const deflMm = (loadKg / 50 * 0.85).toFixed(2);
svgEl('text',{
x:proj.TFL.x + 20, y:proj.TFL.y - 3,
fill:'rgba(0,255,136,0.6)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = `δ=${deflMm}mm`;
}
if (currentMode === 'corner' && loadKg > 10) {
const cx = (proj.TFL.x + proj.TBR.x) / 2;
const cy = (proj.TFL.y + proj.BBR.y) / 2;
svgEl('text',{
x:cx - 80, y:cy - 10,
fill:'rgba(0,229,255,0.7)', 'font-size':'13', 'font-weight':'600',
'font-family':'Rajdhani, sans-serif'
}, svgGroups.labels).textContent = '空间交叉斜拉杆';
svgEl('text',{
x:cx - 80, y:cy + 8,
fill:'rgba(200,214,229,0.4)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = '对角线抵抗扭剪变形';
// 侧向力箭头
const sideArrowY = (proj.TFL.y + proj.TBL.y) / 2;
svgEl('line',{
x1:proj.TFL.x - 60, y1:sideArrowY,
x2:proj.TFL.x - 15, y2:sideArrowY,
stroke:'rgba(255,107,53,0.7)', 'stroke-width':2,
'marker-end':'url(#arrowCyan)'
}, svgGroups.arrows);
svgEl('text',{
x:proj.TFL.x - 65, y:sideArrowY - 6,
fill:'rgba(255,107,53,0.7)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace', 'text-anchor':'end'
}, svgGroups.labels).textContent = '离心力→';
}
if (currentMode === 'fail') {
const midX = (proj.TFL.x + proj.TBL.x) / 2;
const midY = (proj.TFL.y + proj.TBL.y) / 2 + 20;
svgEl('text',{
x:midX - 60, y:midY,
fill:'#ff2d55', 'font-size':'14', 'font-weight':'700',
'font-family':'Rajdhani, sans-serif'
}, svgGroups.labels).textContent = '局部屈曲失稳!';
svgEl('text',{
x:midX - 60, y:midY + 16,
fill:'rgba(255,45,85,0.6)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = '薄壁管跨中无节点支撑';
svgEl('text',{
x:midX - 60, y:midY + 30,
fill:'rgba(255,45,85,0.5)', 'font-size':'10',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = '壁厚1.5mm不足以抵抗集中力';
}
// 组件标签(始终显示)
if (currentMode !== 'fail') {
const blMid = [(proj.TFL.x+proj.TBL.x)/2, (proj.TFL.y+proj.TBL.y)/2];
svgEl('text',{
x:blMid[0]-50, y:blMid[1]-8,
fill:'rgba(143,170,190,0.5)', 'font-size':'9',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = '40×20×1.5mm 铝纵梁';
// CF杆标签
const r = RODS[0];
const rp1 = proj[r.from], rp2 = proj[r.to];
const rmx = (rp1.x+rp2.x)/2 + 15, rmy = (rp1.y+rp2.y)/2 - 5;
svgEl('text',{
x:rmx, y:rmy,
fill:'rgba(0,229,255,0.4)', 'font-size':'9',
'font-family':'Share Tech Mono, monospace'
}, svgGroups.labels).textContent = 'φ8 碳纤维斜拉杆';
}
}
// ===== 屈曲可视化 =====
function drawBuckling(proj) {
// 在左纵梁上画屈曲波形
const p1 = proj.TFL, p2 = proj.TBL;
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy);
const nx = -dy/len, ny = dx/len; // 法线方向
const buckAmp = 8; // 屈曲振幅
const segments = 20;
let d = `M${p1.x},${p1.y}`;
for (let i = 1; i <= segments; i++) {
const t = i / segments;
const bx = p1.x + dx*t;
const by = p1.y + dy*t;
// 屈曲形状:正弦波,中点最大
const buck = Math.sin(t * Math.PI) * buckAmp * Math.sin(time * 8 + t * 10) * 0.3 + Math.sin(t * Math.PI) * buckAmp * 0.7;
d += ` L${bx + nx*buck},${by + ny*buck}`;
}
svgEl('path',{
d: d,
fill:'none', stroke:'rgba(255,45,85,0.5)', 'stroke-width':2,
'stroke-dasharray':'3,3'
}, svgGroups.arrows);
// 屈曲区域高亮
const midX = (p1.x+p2.x)/2 + nx*buckAmp*0.5;
const midY = (p1.y+p2.y)/2 + ny*buckAmp*0.5;
svgEl('circle',{
cx:midX, cy:midY, r:20+5*Math.sin(time*6),
fill:'rgba(255,45,85,0.06)', stroke:'rgba(255,45,85,0.2)',
'stroke-width':1, 'stroke-dasharray':'2,2'
}, svgGroups.arrows);
}
// ===== 粒子更新 =====
function updateParticles(proj) {
const loadFactor = loadKg / 50;
particles.forEach(p => {
const r = p.rod;
const p1 = proj[r.from], p2 = proj[r.to];
// 确定粒子是否激活
let active = false;
let speed = 0.002;
let size = 1.5;
let alpha = 0;
if (currentMode === 'empty') {
active = true; speed = 0.002; size = 1.2; alpha = 0.25;
} else if (currentMode === 'full') {
active = true; speed = 0.004 + 0.006 * loadFactor; size = 1.5 + loadFactor; alpha = 0.4 + 0.5 * loadFactor;
} else if (currentMode === 'corner') {
active = true;
const isLeft = r.from.includes('L') || r.to.includes('L');
const isCross = r.face === 'front' || r.face === 'back';
if (isLeft) { speed = 0.008; size = 2.5; alpha = 0.9; }
else if (isCross) { speed = 0.006; size = 2; alpha = 0.7; }
else { speed = 0.003; size = 1.5; alpha = 0.3; }
} else if (currentMode === 'fail') {
if (r.face === 'left') { active = false; }
else { active = true; speed = 0.003; size = 1.2; alpha = 0.3; }
}
p.active = active;
if (!active) {
p.el.setAttribute('fill', 'rgba(0,229,255,0)');
return;
}
// 更新进度
p.progress += speed;
if (p.progress > 1) p.progress -= 1;
// 计算位置
const t = p.progress;
const x = p1.x + (p2.x - p1.x) * t;
const y = p1.y + (p2.y - p1.y) * t;
// 淡入淡出
const fade = Math.sin(t * Math.PI);
const finalAlpha = alpha * fade;
p.el.setAttribute('cx', x);
p.el.setAttribute('cy', y);
p.el.setAttribute('r', size * fade);
p.el.setAttribute('fill', `rgba(0,229,255,${finalAlpha})`);
});
}
// ===== 信息面板更新 =====
function updateInfoPanel() {
const loadFactor = loadKg / 50;
document.getElementById('loadVal').textContent = `${loadKg} kg`;
document.getElementById('infoLoad').textContent = `${loadKg} kg`;
let deform = 0, tension = 200, bending = 0;
let statusClass = 'normal', statusText = '系统正常 — 预紧力维持刚度';
let ifrNote = '<strong>IFR 理想解:</strong>三角桁架几何将弯矩转化为轴向拉压力——载荷越重,碳纤维杆预紧力激活越充分,刚度反而越高。载荷自身成为结构刚度的资源。';
if (currentMode === 'empty') {
deform = 0.05;
tension = 200;
bending = 0.5;
} else if (currentMode === 'full') {
deform = 0.85 * loadFactor;
tension = 200 + 245 * loadFactor;
bending = 12.5 * loadFactor;
if (loadKg > 30) statusText = '满载运行 — CF杆充分受拉,刚度达标';
else statusText = '载荷施加中 — 预紧力逐步转化为工作拉力';
} else if (currentMode === 'corner') {
deform = 1.2 * loadFactor;
tension = 200 + 320 * loadFactor;
bending = 18 * loadFactor;
statusClass = 'warning';
statusText = '过弯工况 — 交叉斜拉杆抵抗扭剪';
ifrNote = '<strong>IFR 资源利用:</strong>空间交叉的碳纤维斜拉杆在对角线方向形成抗扭路径——同样的杆件,仅靠空间排布的几何资源,便同时获得抗弯与抗扭能力。';
} else if (currentMode === 'fail') {
deform = 8.5;
tension = 150;
bending = 65;
statusClass = 'danger';
statusText = '失效边界 — 跨中无节点支撑,薄壁管局部屈曲!';
ifrNote = '<strong>失效边界:</strong>三角桁架的理想解依赖"载荷作用于节点"这一前提。集中力作用于无节点支撑的跨中时,1.5mm壁厚无法抵抗局部凹陷——理想解有其适用边界。';
}
document.getElementById('infoDeform').textContent = `${deform.toFixed(2)} mm`;
document.getElementById('infoTension').textContent = `${Math.round(tension)} N`;
document.getElementById('infoBending').textContent = `${bending.toFixed(1)} N·m`;
const sb = document.getElementById('statusBar');
sb.className = `status-bar ${statusClass}`;
sb.textContent = statusText;
document.getElementById('ifrNote').innerHTML = ifrNote;
// 信息卡片颜色
const deformEl = document.getElementById('infoDeform');
deformEl.className = `value ${deform > 3 ? 'red' : deform > 1 ? 'orange' : 'green'}`;
const tensionEl = document.getElementById('infoTension');
tensionEl.className = `value ${tension > 400 ? 'cyan' : 'green'}`;
const bendingEl = document.getElementById('infoBending');
bendingEl.className = `value ${bending > 30 ? 'red' : bending > 10 ? 'orange' : 'orange'}`;
}
// ===== 交互 =====
function setupControls() {
// 模式按钮
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
// 自动设置载荷
if (currentMode === 'empty') { loadKg = 0; document.getElementById('loadSlider').value = 0; }
else if (currentMode === 'full') { loadKg = 50; document.getElementById('loadSlider').value = 50; }
else if (currentMode === 'corner') { loadKg = 50; document.getElementById('loadSlider').value = 50; }
else if (currentMode === 'fail') { loadKg = 50; document.getElementById('loadSlider').value = 50; }
});
});
// 载荷滑块
document.getElementById('loadSlider').addEventListener('input', (e) => {
loadKg = parseInt(e.target.value);
// 如果手动调载荷,切换到满载模式
if (loadKg > 0 && currentMode === 'empty') {
currentMode = 'full';
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
document.querySelector('[data-mode="full"]').classList.add('active');
}
});
// 拖拽旋转
const svgWrap = document.querySelector('.svg-wrap');
svgWrap.addEventListener('mousedown', (e) => {
isDragging = true;
dragStartX = e.clientX; dragStartY = e.clientY;
dragStartRotY = rotY; dragStartRotX = rotX;
svgWrap.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
rotY = dragStartRotY + dx * 0.005;
rotX = Math.max(-1.2, Math.min(-0.1, dragStartRotX + dy * 0.005));
});
window.addEventListener('mouseup', () => {
isDragging = false;
svgWrap.style.cursor = 'grab';
});
svgWrap.style.cursor = 'grab';
// 触控支持
svgWrap.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
isDragging = true;
dragStartX = e.touches[0].clientX; dragStartY = e.touches[0].clientY;
dragStartRotY = rotY; dragStartRotX = rotX;
}
}, {passive:true});
window.addEventListener('touchmove', (e) => {
if (!isDragging || e.touches.length !== 1) return;
const dx = e.touches[0].clientX - dragStartX;
const dy = e.touches[0].clientY - dragStartY;
rotY = dragStartRotY + dx * 0.005;
rotX = Math.max(-1.2, Math.min(-0.1, dragStartRotX + dy * 0.005));
}, {passive:true});
window.addEventListener('touchend', () => { isDragging = false; });
}
// ===== 主动画循环 =====
function animate() {
time += 0.016;
render();
updateInfoPanel();
// 缓慢自动旋转(无拖拽时)
if (!isDragging && currentMode !== 'fail') {
rotY += 0.0008;
}
requestAnimationFrame(animate);
}
// ===== 初始化 =====
function init() {
initSVG();
setupControls();
animate();
}
init();
</script>
</body>
</html>
这个动画完整实现了三维空间三角桁架底盘的 IFR(最终理想解)原理演示,以下是关键设计说明:
视觉设计
- 深空工程美学风格,暗色背景配合蓝图网格,使用 Rajdhani + Share Tech Mono 字体组合
- 铝型材主纵梁以银蓝金属渐变呈现,碳纤维斜拉杆在受拉时发出青色辉光
- 球铰节点带脉动光晕,底部车轮支座以绿色标识
IFR 原理展示
- 满载模式:直接展示理想状态——50kg 载荷通过三角桁架几何转化为轴向拉力,CF 杆发光粒子流动可视化力的传递路径,标注"弯矩→轴力"转换
- 过弯模式:空间交叉斜拉杆的不对称发光展示对角线抗扭机理,离心力箭头标识侧向载荷
- 失效边界:跨中集中载荷导致薄壁管屈曲失稳的动态波形可视化,红色警告标识适用边界
交互设计
- 四种工况模式切换(空载/满载/过弯/失效)
- 载荷滑块可手动调节 0-50kg
- 拖拽旋转 3D 视角(鼠标/触控)
- 右侧面板实时显示变形量、杆件拉力、梁弯矩等参数
- IFR 理解笔记随模式动态切换,阐释资源利用逻辑
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
