分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IFR原理动画 — 三维空间三角桁架底盘</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;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分