分享图
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=Chakra+Petch:wght@300;400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
  :root {
    --bg: #050a14;
    --bg2: #0a1628;
    --fg: #d8e2f0;
    --muted: #4a5e80;
    --accent: #00ffc8;
    --accent2: #00b8ff;
    --stress: #ff6b2b;
    --stress2: #ffb347;
    --lattice: #3da0ff;
    --lattice-dim: #1a4a80;
    --shell: rgba(200,170,80,0.12);
    --shell-stroke: rgba(200,170,80,0.35);
    --void-fill: rgba(255,255,255,0.02);
    --void-stroke: rgba(255,255,255,0.06);
    --card: rgba(10,22,40,0.85);
    --border: rgba(0,255,200,0.12);
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background:var(--bg); color:var(--fg); font-family:'Share Tech Mono',monospace; }
  #app { width:100%; height:100%; display:flex; position:relative; }

  /* 主 SVG 容器 */
  #svg-wrap { flex:1; display:flex; align-items:center; justify-content:center; position:relative; }
  #main-svg { width:100%; height:100%; }

  /* 右侧面板 */
  #panel {
    width:320px; min-width:280px; background:var(--card);
    border-left:1px solid var(--border); padding:28px 22px;
    display:flex; flex-direction:column; gap:20px; overflow-y:auto;
    backdrop-filter:blur(12px); z-index:10;
  }
  #panel h2 { font-family:'Chakra Petch',sans-serif; font-weight:700; font-size:18px; color:var(--accent); letter-spacing:1px; }
  #panel h3 { font-family:'Chakra Petch',sans-serif; font-weight:600; font-size:13px; color:var(--muted); letter-spacing:2px; text-transform:uppercase; margin-bottom:6px; }

  .metric-card {
    background:rgba(0,255,200,0.04); border:1px solid var(--border);
    border-radius:8px; padding:14px 16px; display:flex; flex-direction:column; gap:4px;
    transition: border-color .3s;
  }
  .metric-card:hover { border-color:rgba(0,255,200,0.3); }
  .metric-label { font-size:11px; color:var(--muted); letter-spacing:1px; }
  .metric-value { font-family:'Chakra Petch',sans-serif; font-size:28px; font-weight:700; color:var(--fg); line-height:1; }
  .metric-value.accent { color:var(--accent); }
  .metric-value.stress { color:var(--stress); }
  .metric-unit { font-size:12px; color:var(--muted); margin-left:4px; font-weight:400; }

  .slider-group { display:flex; flex-direction:column; gap:6px; }
  .slider-group label { font-size:12px; color:var(--muted); display:flex; justify-content:space-between; }
  .slider-group label span { color:var(--accent); font-weight:600; }
  input[type=range] {
    -webkit-appearance:none; width:100%; height:6px; border-radius:3px;
    background:linear-gradient(90deg,var(--lattice-dim),var(--accent)); outline:none; cursor:pointer;
  }
  input[type=range]::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:var(--accent); border:2px solid var(--bg); box-shadow:0 0 10px var(--accent);
  }

  .toggle-row { display:flex; align-items:center; gap:10px; cursor:pointer; font-size:12px; color:var(--muted); transition:color .2s; }
  .toggle-row:hover { color:var(--fg); }
  .toggle-box {
    width:36px; height:20px; border-radius:10px; background:var(--lattice-dim);
    position:relative; transition:background .3s; flex-shrink:0;
  }
  .toggle-box.on { background:var(--accent); }
  .toggle-box::after {
    content:''; position:absolute; top:2px; left:2px; width:16px; height:16px;
    border-radius:50%; background:#fff; transition:transform .3s;
  }
  .toggle-box.on::after { transform:translateX(16px); }

  .divider { height:1px; background:var(--border); margin:4px 0; }

  .legend-item { display:flex; align-items:center; gap:8px; font-size:11px; color:var(--muted); }
  .legend-swatch { width:24px; height:3px; border-radius:2px; flex-shrink:0; }

  /* 标题覆盖层 */
  #title-overlay {
    position:absolute; top:24px; left:28px; z-index:5; pointer-events:none;
  }
  #title-overlay h1 {
    font-family:'Chakra Petch',sans-serif; font-weight:700; font-size:22px;
    color:var(--accent); letter-spacing:2px; line-height:1.2;
  }
  #title-overlay p {
    font-size:12px; color:var(--muted); margin-top:4px; letter-spacing:1px;
  }

  /* 脉冲动画 */
  @keyframes pulse-glow {
    0%,100% { opacity:0.6; }
    50% { opacity:1; }
  }
  @keyframes stress-flow {
    0% { offset-distance:0%; opacity:0; }
    10% { opacity:1; }
    90% { opacity:1; }
    100% { offset-distance:100%; opacity:0; }
  }
  @keyframes force-pulse {
    0%,100% { transform:translateY(0); opacity:0.7; }
    50% { transform:translateY(3px); opacity:1; }
  }

  /* 减弱动画偏好 */
  @media (prefers-reduced-motion:reduce) {
    *, *::before, *::after { animation-duration:0.01ms!important; transition-duration:0.01ms!important; }
  }
</style>
</head>
<body>
<div id="app">
  <div id="svg-wrap">
    <div id="title-overlay">
      <h1>Bionic Topology-Optimized Lattice</h1>
      <p>IFR · 仿生拓扑优化点阵结构 · 消除冗余质量</p>
    </div>
    <svg id="main-svg" viewBox="-520 -380 1040 760" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <!-- 发光滤镜 -->
        <filter id="glow-sm" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="2" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glow-md" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="4" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glow-lg" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="8" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glow-stress" x="-80%" y="-80%" width="260%" height="260%">
          <feGaussianBlur stdDeviation="5" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <!-- 渐变 -->
        <linearGradient id="beam-grad" x1="0%" y1="0%" x2="0%" y2="100%">
          <stop offset="0%" stop-color="#00ffc8" stop-opacity="0.9"/>
          <stop offset="50%" stop-color="#3da0ff" stop-opacity="0.6"/>
          <stop offset="100%" stop-color="#1a4a80" stop-opacity="0.3"/>
        </linearGradient>
        <radialGradient id="node-grad" cx="50%" cy="50%" r="50%">
          <stop offset="0%" stop-color="#00ffc8" stop-opacity="1"/>
          <stop offset="100%" stop-color="#00ffc8" stop-opacity="0"/>
        </radialGradient>
        <!-- 背景网格图案 -->
        <pattern id="bg-grid" width="30" height="30" patternUnits="userSpaceOnUse">
          <path d="M30,0 L0,0 0,30" fill="none" stroke="rgba(0,255,200,0.03)" stroke-width="0.5"/>
        </pattern>
      </defs>
      <!-- 背景层 -->
      <rect x="-520" y="-380" width="1040" height="760" fill="var(--bg)"/>
      <rect x="-520" y="-380" width="1040" height="760" fill="url(#bg-grid)"/>
      <!-- 所有动态内容将通过 JS 生成 -->
      <g id="ghost-layer"></g>
      <g id="shell-layer"></g>
      <g id="lattice-layer"></g>
      <g id="particle-layer"></g>
      <g id="force-layer"></g>
      <g id="label-layer"></g>
      <g id="micro-layer"></g>
    </svg>
  </div>

  <div id="panel">
    <h2>参数控制台</h2>

    <div>
      <h3>关键指标</h3>
      <div style="display:flex;gap:10px;flex-wrap:wrap;">
        <div class="metric-card" style="flex:1;min-width:120px;">
          <span class="metric-label">点阵相对密度</span>
          <span class="metric-value accent" id="m-density">15<span class="metric-unit">%</span></span>
        </div>
        <div class="metric-card" style="flex:1;min-width:120px;">
          <span class="metric-label">刚度保留率</span>
          <span class="metric-value" id="m-stiffness">95<span class="metric-unit">%</span></span>
        </div>
      </div>
      <div class="metric-card" style="margin-top:10px;">
        <span class="metric-label">质量削减</span>
        <span class="metric-value stress" id="m-weight">-72<span class="metric-unit">%</span></span>
      </div>
    </div>

    <div class="divider"></div>

    <div>
      <h3>交互控制</h3>
      <div class="slider-group">
        <label>密度阈值 <span id="v-threshold">0.15</span></label>
        <input type="range" id="s-threshold" min="0.05" max="0.65" step="0.01" value="0.15">
      </div>
      <div class="slider-group" style="margin-top:12px;">
        <label>应力流速度 <span id="v-speed">1.0x</span></label>
        <input type="range" id="s-speed" min="0.2" max="3.0" step="0.1" value="1.0">
      </div>
    </div>

    <div class="divider"></div>

    <div>
      <h3>显示切换</h3>
      <div style="display:flex;flex-direction:column;gap:10px;">
        <div class="toggle-row" data-toggle="ghost">
          <div class="toggle-box on" id="t-ghost"></div>
          <span>原始实体轮廓</span>
        </div>
        <div class="toggle-row" data-toggle="shell">
          <div class="toggle-box on" id="t-shell"></div>
          <span>芳纶纤维蒙皮</span>
        </div>
        <div class="toggle-row" data-toggle="particles">
          <div class="toggle-box on" id="t-particles"></div>
          <span>应力流粒子</span>
        </div>
        <div class="toggle-row" data-toggle="micro">
          <div class="toggle-box" id="t-micro"></div>
          <span>微观点阵详图</span>
        </div>
      </div>
    </div>

    <div class="divider"></div>

    <div>
      <h3>图例</h3>
      <div style="display:flex;flex-direction:column;gap:6px;">
        <div class="legend-item"><div class="legend-swatch" style="background:#00ffc8;"></div>高应力路径(主承力杆件)</div>
        <div class="legend-item"><div class="legend-swatch" style="background:#3da0ff;"></div>中应力路径(辅助杆件)</div>
        <div class="legend-item"><div class="legend-swatch" style="background:#1a4a80;"></div>低应力路径(弱连接)</div>
        <div class="legend-item"><div class="legend-swatch" style="background:linear-gradient(90deg,#ff6b2b,#ffb347);"></div>应力流粒子</div>
        <div class="legend-item"><div class="legend-swatch" style="background:rgba(200,170,80,0.5);border:1px solid rgba(200,170,80,0.3);"></div>芳纶纤维蒙皮</div>
      </div>
    </div>

    <div class="divider"></div>
    <div style="font-size:10px;color:var(--muted);line-height:1.6;">
      <strong style="color:var(--accent);">IFR 核心思想:</strong>材料仅存在于受力路径上,所有不参与承力的冗余质量被消除。宏观刚度几乎无损,质量大幅削减,使装置总重降至楼板阈值之下。
    </div>
  </div>
</div>

<script>
// ============================================================
// 仿生拓扑优化点阵结构 · IFR 原理动画
// ============================================================

const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('main-svg');

// 图层引用
const layers = {
  ghost: document.getElementById('ghost-layer'),
  shell: document.getElementById('shell-layer'),
  lattice: document.getElementById('lattice-layer'),
  particle: document.getElementById('particle-layer'),
  force: document.getElementById('force-layer'),
  label: document.getElementById('label-layer'),
  micro: document.getElementById('micro-layer'),
};

// ---- 全局状态 ----
const state = {
  densityThreshold: 0.15,
  speed: 1.0,
  showGhost: true,
  showShell: true,
  showParticles: true,
  showMicro: false,
  time: 0,
};

// ---- 等轴测投影 ----
const COS30 = Math.cos(Math.PI / 6);
const SIN30 = Math.sin(Math.PI / 6);
const SCALE = 2.2; // 3D → SVG 缩放
const OFFSET_X = -20;
const OFFSET_Y = 20;

function project(x, y, z) {
  const sx = (x - y) * COS30 * SCALE + OFFSET_X;
  const sy = ((x + y) * SIN30 - z) * SCALE + OFFSET_Y;
  return { x: sx, y: sy };
}

// ---- 密度场(模拟拓扑优化结果)----
// 结构尺寸(3D 单位)
const SW = 8, SD = 5, SH = 12;

function computeDensity(ix, iy, iz) {
  // 归一化到 [0,1]
  const nx = ix / SW, ny = iy / SD, nz = iz / SH;

  let d = 0;

  // 1) 顶面加载区域 — 高密度
  d += Math.max(0, 1 - nz * 6) * 0.7;

  // 2) 底面支撑区域 — 高密度
  d += Math.max(0, 1 - (1 - nz) * 6) * 0.65;

  // 3) 侧表面 — 剪切/弯曲外壳
  const dxS = Math.min(nx, 1 - nx);
  const dyS = Math.min(ny, 1 - ny);
  d += Math.max(0, 1 - dxS * 5) * 0.35;
  d += Math.max(0, 1 - dyS * 5) * 0.25;

  // 4) 对角载荷路径 — 从顶部中心向底部四角延伸
  for (const corner of [[0,0],[1,0],[0,1],[1,1]]) {
    const cx = corner[0], cy = corner[1];
    const pathDist = Math.sqrt(
      Math.pow(nx - cx, 2) * 0.6 +
      Math.pow(ny - cy, 2) * 0.4 +
      Math.pow(nz - 0, 2) * 0.3
    );
    d += Math.max(0, 1 - pathDist * 2.2) * 0.45;
  }

  // 5) 中心垂直立柱
  const cDist = Math.sqrt(Math.pow(nx - 0.5, 2) + Math.pow(ny - 0.5, 2));
  d += Math.max(0, 1 - cDist * 5) * 0.5;

  // 6) 内部削减 — 中部低应力区掏空
  if (nz > 0.15 && nz < 0.85 && dxS > 0.2 && dyS > 0.2 && cDist < 0.25) {
    d *= 0.15;
  }

  return Math.min(1, Math.max(0, d));
}

// ---- 生成点阵结构数据 ----
let latticeNodes = [];
let latticeEdges = [];
let stressPaths = [];

function generateLattice() {
  const NX = SW, NY = SD, NZ = SH;
  const nodes = [];
  const nodeMap = {};

  // 生成节点
  for (let iz = 0; iz <= NZ; iz++) {
    for (let iy = 0; iy <= NY; iy++) {
      for (let ix = 0; ix <= NX; ix++) {
        const density = computeDensity(ix, iy, iz);
        const idx = ix + iy * (NX + 1) + iz * (NX + 1) * (NY + 1);
        const p = project(ix, iy, iz);
        nodes.push({
          ix, iy, iz, density, idx,
          sx: p.x, sy: p.y,
          depth: ix + iy - iz // 用于深度排序
        });
        nodeMap[`${ix},${iy},${iz}`] = nodes.length - 1;
      }
    }
  }

  // 生成边 — 包含轴向和面对角线(模拟八面体点阵连接)
  const edges = [];
  const NX1 = NX + 1, NY1 = NY + 1;

  function addEdge(i1, i2) {
    if (i1 == null || i2 == null) return;
    const d = (nodes[i1].density + nodes[i2].density) / 2;
    edges.push({ a: i1, b: i2, density: d });
  }

  for (let iz = 0; iz <= NZ; iz++) {
    for (let iy = 0; iy <= NY; iy++) {
      for (let ix = 0; ix <= NX; ix++) {
        const ci = nodeMap[`${ix},${iy},${iz}`];
        // 轴向连接
        if (ix < NX) addEdge(ci, nodeMap[`${ix+1},${iy},${iz}`]);
        if (iy < NY) addEdge(ci, nodeMap[`${ix},${iy+1},${iz}`]);
        if (iz < NZ) addEdge(ci, nodeMap[`${ix},${iy},${iz+1}`]);

        // XZ面对角线(八面体微结构)
        if (ix < NX && iz < NZ) {
          addEdge(ci, nodeMap[`${ix+1},${iy},${iz+1}`]);
          addEdge(nodeMap[`${ix+1},${iy},${iz}`], nodeMap[`${ix},${iy},${iz+1}`]);
        }
        // YZ面对角线
        if (iy < NY && iz < NZ) {
          addEdge(ci, nodeMap[`${ix},${iy+1},${iz+1}`]);
          addEdge(nodeMap[`${ix},${iy+1},${iz}`], nodeMap[`${ix},${iy},${iz+1}`]);
        }
        // XY面对角线(选择性添加,避免过密)
        if (ix < NX && iy < NY && (ix + iy + iz) % 2 === 0) {
          addEdge(ci, nodeMap[`${ix+1},${iy+1},${iz}`]);
          addEdge(nodeMap[`${ix+1},${iy},${iz}`], nodeMap[`${ix},${iy+1},${iz}`]);
        }
      }
    }
  }

  latticeNodes = nodes;
  latticeEdges = edges;

  // 生成应力流路径 — 从顶部节点到底部节点的路径
  generateStressPaths();
}

function generateStressPaths() {
  stressPaths = [];
  // 选择顶部高密度节点作为路径起点
  const topNodes = latticeNodes
    .filter(n => n.iz === SH && n.density > 0.3)
    .sort((a, b) => b.density - a.density)
    .slice(0, 8);

  for (const startNode of topNodes) {
    const path = [startNode];
    let current = startNode;
    const visited = new Set([current.idx]);

    // 贪心向下走,优先选择高密度邻居
    for (let step = 0; step < SH + 4; step++) {
      // 找到与当前节点相连且更靠近底部的节点
      const neighbors = latticeEdges
        .filter(e => (e.a === current.idx || e.b === current.idx) && e.density > state.densityThreshold * 0.5)
        .map(e => e.a === current.idx ? latticeNodes[e.b] : latticeNodes[e.a])
        .filter(n => !visited.has(n.idx) && n.iz <= current.iz)
        .sort((a, b) => {
          // 优先向下,其次高密度
          const da = current.iz - a.iz + a.density * 0.5;
          const db = current.iz - b.iz + b.density * 0.5;
          return db - da;
        });

      if (neighbors.length === 0) break;
      const next = neighbors[0];
      path.push(next);
      visited.add(next.idx);
      current = next;
      if (current.iz === 0) break;
    }

    if (path.length > 3) {
      stressPaths.push(path.map(n => ({ x: n.sx, y: n.sy, density: n.density })));
    }
  }
}

// ---- 渲染函数 ----

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;
}

// 清空图层
function clearLayer(layer) {
  while (layer.firstChild) layer.removeChild(layer.firstChild);
}

// 绘制原始实体轮廓(幽灵线框)
function renderGhost() {
  clearLayer(layers.ghost);
  if (!state.showGhost) return;
  const opacity = 0.08;
  const corners3D = [
    [0,0,0],[SW,0,0],[SW,SD,0],[0,SD,0],
    [0,0,SH],[SW,0,SH],[SW,SD,SH],[0,SD,SH]
  ];
  const c = corners3D.map(p => project(p[0], p[1], p[2]));
  const edgePairs = [
    [0,1],[1,2],[2,3],[3,0],
    [4,5],[5,6],[6,7],[7,4],
    [0,4],[1,5],[2,6],[3,7]
  ];
  for (const [a, b] of edgePairs) {
    svgEl('line', {
      x1: c[a].x, y1: c[a].y, x2: c[b].x, y2: c[b].y,
      stroke: '#ffffff', 'stroke-width': 1, 'stroke-dasharray': '6,4',
      opacity: opacity
    }, layers.ghost);
  }
  // 填充半透明体
  const topFace = [c[4], c[5], c[6], c[7]];
  svgEl('polygon', {
    points: topFace.map(p => `${p.x},${p.y}`).join(' '),
    fill: 'rgba(255,255,255,0.015)', stroke: 'none'
  }, layers.ghost);
}

// 绘制芳纶纤维蒙皮
function renderShell() {
  clearLayer(layers.shell);
  if (!state.showShell) return;

  const corners3D = [
    [0,0,0],[SW,0,0],[SW,SD,0],[0,SD,0],
    [0,0,SH],[SW,0,SH],[SW,SD,SH],[0,SD,SH]
  ];
  const c = corners3D.map(p => project(p[0], p[1], p[2]));

  // 可见面:顶面、前面、右面
  // 顶面
  svgEl('polygon', {
    points: [c[4], c[5], c[6], c[7]].map(p => `${p.x},${p.y}`).join(' '),
    fill: 'rgba(200,170,80,0.06)', stroke: 'rgba(200,170,80,0.2)',
    'stroke-width': 0.8
  }, layers.shell);
  // 前面 (y=0)
  svgEl('polygon', {
    points: [c[0], c[1], c[5], c[4]].map(p => `${p.x},${p.y}`).join(' '),
    fill: 'rgba(200,170,80,0.04)', stroke: 'rgba(200,170,80,0.15)',
    'stroke-width': 0.6
  }, layers.shell);
  // 右面 (x=SW)
  svgEl('polygon', {
    points: [c[1], c[2], c[6], c[5]].map(p => `${p.x},${p.y}`).join(' '),
    fill: 'rgba(200,170,80,0.03)', stroke: 'rgba(200,170,80,0.12)',
    'stroke-width': 0.6
  }, layers.shell);
}

// 绘制点阵结构
function renderLattice() {
  clearLayer(layers.lattice);

  const threshold = state.densityThreshold;

  // 过滤并排序边(先画远处的)
  const visibleEdges = latticeEdges
    .filter(e => e.density >= threshold * 0.6)
    .sort((a, b) => {
      const da = (latticeNodes[a.a].depth + latticeNodes[a.b].depth) / 2;
      const db = (latticeNodes[b.a].depth + latticeNodes[b.b].depth) / 2;
      return da - db;
    });

  // 绘制边
  for (const edge of visibleEdges) {
    const na = latticeNodes[edge.a], nb = latticeNodes[edge.b];
    const d = edge.density;
    const relD = Math.min(1, (d - threshold * 0.6) / (1 - threshold * 0.6));

    // 颜色映射:低密度暗蓝 → 中密度蓝 → 高密度青绿
    let color, width, opacity;
    if (relD > 0.6) {
      color = `rgba(0,255,200,${0.5 + relD * 0.5})`;
      width = 1.2 + relD * 1.5;
      opacity = 0.7 + relD * 0.3;
    } else if (relD > 0.25) {
      const t = (relD - 0.25) / 0.35;
      const r = Math.round(61 * t);
      const g = Math.round(74 + 181 * t);
      const b2 = Math.round(128 + 72 * t);
      color = `rgba(${r},${g},${b2},${0.4 + t * 0.3})`;
      width = 0.6 + t * 0.8;
      opacity = 0.4 + t * 0.3;
    } else {
      color = `rgba(26,74,128,${0.2 + relD * 0.6})`;
      width = 0.4 + relD * 0.4;
      opacity = 0.2 + relD * 0.4;
    }

    const line = svgEl('line', {
      x1: na.sx, y1: na.sy, x2: nb.sx, y2: nb.sy,
      stroke: color, 'stroke-width': width, opacity: opacity,
      'stroke-linecap': 'round'
    }, layers.lattice);

    // 高密度杆件加发光
    if (relD > 0.65) {
      line.setAttribute('filter', 'url(#glow-sm)');
    }
  }

  // 绘制高密度节点(只在密度较高的位置显示)
  const visibleNodes = latticeNodes.filter(n => n.density >= threshold);
  for (const node of visibleNodes) {
    if (node.density < threshold * 1.2) continue; // 只显示足够高密度的节点
    const r = 1 + node.density * 2.5;
    const alpha = Math.min(1, node.density * 1.5);
    svgEl('circle', {
      cx: node.sx, cy: node.sy, r: r,
      fill: `rgba(0,255,200,${alpha * 0.7})`,
      opacity: alpha
    }, layers.lattice);
  }
}

// 绘制应力流粒子
let particleElements = [];
let particleStates = [];

function initParticles() {
  clearLayer(layers.particle);
  particleElements = [];
  particleStates = [];

  // 为每条应力路径创建多个粒子
  const particlesPerPath = 4;
  for (let pi = 0; pi < stressPaths.length; pi++) {
    for (let i = 0; i < particlesPerPath; i++) {
      const el = svgEl('circle', {
        r: 2.5, fill: '#ff6b2b', filter: 'url(#glow-stress)', opacity: 0
      }, layers.particle);
      particleElements.push(el);
      particleStates.push({
        pathIndex: pi,
        progress: i / particlesPerPath, // 0-1 沿路径的位置
        speed: 0.003 + Math.random() * 0.002,
      });
    }
  }
}

function updateParticles(dt) {
  if (!state.showParticles) {
    particleElements.forEach(el => el.setAttribute('opacity', 0));
    return;
  }

  for (let i = 0; i < particleStates.length; i++) {
    const ps = particleStates[i];
    const path = stressPaths[ps.pathIndex];
    if (!path || path.length < 2) continue;

    ps.progress += ps.speed * state.speed * dt * 0.06;
    if (ps.progress > 1) ps.progress -= 1;

    // 在路径上插值位置
    const totalSegs = path.length - 1;
    const segF = ps.progress * totalSegs;
    const segI = Math.min(Math.floor(segF), totalSegs - 1);
    const segT = segF - segI;

    const p0 = path[segI], p1 = path[segI + 1];
    const px = p0.x + (p1.x - p0.x) * segT;
    const py = p0.y + (p1.y - p0.y) * segT;

    // 计算当前密度(用于亮度)
    const d = p0.density + (p1.density - p0.density) * segT;

    const el = particleElements[i];
    el.setAttribute('cx', px);
    el.setAttribute('cy', py);

    // 淡入淡出
    let alpha = 1;
    if (ps.progress < 0.1) alpha = ps.progress / 0.1;
    else if (ps.progress > 0.9) alpha = (1 - ps.progress) / 0.1;
    alpha *= Math.min(1, d * 2);

    el.setAttribute('opacity', Math.max(0, alpha));
    el.setAttribute('r', 2 + d * 2);

    // 颜色随密度变化
    if (d > 0.6) {
      el.setAttribute('fill', '#ffb347');
    } else {
      el.setAttribute('fill', '#ff6b2b');
    }
  }
}

// 绘制力箭头
function renderForceArrows() {
  clearLayer(layers.force);

  // 顶部下压载荷箭头
  const arrowPositions = [
    [2, 1, SH + 1.5], [4, 1, SH + 1.5], [6, 1, SH + 1.5],
    [2, 3, SH + 1.5], [4, 3, SH + 1.5], [6, 3, SH + 1.5],
  ];

  for (const pos of arrowPositions) {
    const p = project(pos[0], pos[1], pos[2]);
    const pEnd = project(pos[0], pos[1], pos[2] - 1.2);

    // 箭杆
    const line = svgEl('line', {
      x1: p.x, y1: p.y, x2: pEnd.x, y2: pEnd.y,
      stroke: '#ff6b2b', 'stroke-width': 2, opacity: 0.8,
      'stroke-linecap': 'round'
    }, layers.force);
    line.style.animation = 'force-pulse 2s ease-in-out infinite';

    // 箭头
    const arrowSize = 5;
    const angle = Math.atan2(pEnd.y - p.y, pEnd.x - p.x);
    const ax1 = pEnd.x - arrowSize * Math.cos(angle - 0.4);
    const ay1 = pEnd.y - arrowSize * Math.sin(angle - 0.4);
    const ax2 = pEnd.x - arrowSize * Math.cos(angle + 0.4);
    const ay2 = pEnd.y - arrowSize * Math.sin(angle + 0.4);

    const arrow = svgEl('polygon', {
      points: `${pEnd.x},${pEnd.y} ${ax1},${ay1} ${ax2},${ay2}`,
      fill: '#ff6b2b', opacity: 0.8
    }, layers.force);
    arrow.style.animation = 'force-pulse 2s ease-in-out infinite';
  }

  // 底部支撑三角形
  const supportPositions = [
    [0.5, 0.5, -0.5], [SW - 0.5, 0.5, -0.5],
    [0.5, SD - 0.5, -0.5], [SW - 0.5, SD - 0.5, -0.5],
    [SW / 2, SD / 2, -0.5],
  ];

  for (const pos of supportPositions) {
    const p = project(pos[0], pos[1], pos[2]);
    const size = 6;
    svgEl('polygon', {
      points: `${p.x},${p.y - size} ${p.x - size},${p.y + size * 0.6} ${p.x + size},${p.y + size * 0.6}`,
      fill: 'none', stroke: '#00ffc8', 'stroke-width': 1.2, opacity: 0.6
    }, layers.force);
    // 底线
    svgEl('line', {
      x1: p.x - size - 2, y1: p.y + size * 0.6 + 2,
      x2: p.x + size + 2, y2: p.y + size * 0.6 + 2,
      stroke: '#00ffc8', 'stroke-width': 1, opacity: 0.4
    }, layers.force);
  }

  // 力的标注
  const labelPos = project(SW / 2, SD / 2, SH + 2.2);
  const gLabel = svgEl('text', {
    x: labelPos.x, y: labelPos.y,
    fill: '#ff6b2b', 'font-size': 11, 'text-anchor': 'middle',
    'font-family': 'Share Tech Mono, monospace', opacity: 0.8
  }, layers.force);
  gLabel.textContent = 'F (自重 + 动载荷)';
}

// 绘制标注
function renderLabels() {
  clearLayer(layers.label);

  // 右侧标注:主应力轨迹
  const labelData = [
    { pos: project(SW + 1.5, SD / 2, SH * 0.75), text: '主应力轨迹', color: '#00ffc8' },
    { pos: project(SW + 1.5, SD / 2, SH * 0.35), text: '变密度八面体点阵', color: '#3da0ff' },
    { pos: project(-2, SD / 2, SH * 0.5), text: '掏空区域', color: 'rgba(255,255,255,0.3)' },
  ];

  for (const ld of labelData) {
    svgEl('text', {
      x: ld.pos.x, y: ld.pos.y,
      fill: ld.color, 'font-size': 10, 'text-anchor': 'middle',
      'font-family': 'Share Tech Mono, monospace', opacity: 0.7
    }, layers.label).textContent = ld.text;
  }
}

// 绘制微观详图
function renderMicro() {
  clearLayer(layers.micro);
  if (!state.showMicro) return;

  // 在左下角绘制放大的八面体微观点阵单元
  const ox = -380, oy = 200; // 详图中心

  // 背景框
  svgEl('rect', {
    x: ox - 95, y: oy - 85, width: 190, height: 170,
    rx: 6, fill: 'rgba(5,10,20,0.9)', stroke: 'rgba(0,255,200,0.2)',
    'stroke-width': 1
  }, layers.micro);

  svgEl('text', {
    x: ox, y: oy - 70,
    fill: '#00ffc8', 'font-size': 9, 'text-anchor': 'middle',
    'font-family': 'Share Tech Mono, monospace', opacity: 0.8
  }, layers.micro).textContent = '微观八面体点阵单元';

  // 绘制 2×2×2 的八面体点阵
  const ms = 22; // 微观缩放
  const mNodes = [];

  for (let iz = 0; iz <= 2; iz++) {
    for (let iy = 0; iy <= 2; iy++) {
      for (let ix = 0; ix <= 2; ix++) {
        const px = (ix - 1) * ms;
        const py = ((ix + iy) * 0.5 - 1) * ms * 0.5 - iz * ms * 0.6;
        const pz = -iz * ms * 0.8;
        // 简化等轴测
        const sx = ox + (ix - iy) * ms * 0.6;
        const sy = oy + ((ix + iy) * 0.35 - iz * 0.7) * ms;
        mNodes.push({ ix, iy, iz, sx, sy });
      }
    }
  }

  function mIdx(ix, iy, iz) { return ix + iy * 3 + iz * 9; }

  // 绘制边 — 轴向
  const mEdges = [];
  for (let iz = 0; iz <= 2; iz++) {
    for (let iy = 0; iy <= 2; iy++) {
      for (let ix = 0; ix <= 2; ix++) {
        const ci = mIdx(ix, iy, iz);
        if (ix < 2) mEdges.push([ci, mIdx(ix+1, iy, iz)]);
        if (iy < 2) mEdges.push([ci, mIdx(ix, iy+1, iz)]);
        if (iz < 2) mEdges.push([ci, mIdx(ix, iy, iz+1)]);
        // 面对角线(八面体关键特征)
        if (ix < 2 && iz < 2) {
          mEdges.push([ci, mIdx(ix+1, iy, iz+1)]);
          mEdges.push([mIdx(ix+1, iy, iz), mIdx(ix, iy, iz+1)]);
        }
        if (iy < 2 && iz < 2) {
          mEdges.push([ci, mIdx(ix, iy+1, iz+1)]);
          mEdges.push([mIdx(ix, iy+1, iz), mIdx(ix, iy, iz+1)]);
        }
      }
    }
  }

  for (const [a, b] of mEdges) {
    const na = mNodes[a], nb = mNodes[b];
    svgEl('line', {
      x1: na.sx, y1: na.sy, x2: nb.sx, y2: nb.sy,
      stroke: '#3da0ff', 'stroke-width': 1, opacity: 0.5,
      'stroke-linecap': 'round'
    }, layers.micro);
  }

  // 节点
  for (const n of mNodes) {
    svgEl('circle', {
      cx: n.sx, cy: n.sy, r: 2.5,
      fill: '#00ffc8', opacity: 0.8
    }, layers.micro);
  }

  // 标注杆件直径
  const labelEdge = mEdges[Math.floor(mEdges.length / 3)];
  const leA = mNodes[labelEdge[0]], leB = mNodes[labelEdge[1]];
  svgEl('text', {
    x: (leA.sx + leB.sx) / 2 + 10, y: (leA.sy + leB.sy) / 2 - 5,
    fill: '#ffb347', 'font-size': 8, 'font-family': 'Share Tech Mono, monospace', opacity: 0.7
  }, layers.micro).textContent = 'd ≈ 0.8mm';

  // 相对密度标注
  svgEl('text', {
    x: ox, y: oy + 75,
    fill: 'rgba(255,255,255,0.4)', 'font-size': 8, 'text-anchor': 'middle',
    'font-family': 'Share Tech Mono, monospace'
  }, layers.micro).textContent = `ρ*/ρs = ${state.densityThreshold.toFixed(2)}  |  E*/Es ≈ ${(Math.pow(state.densityThreshold, 2) * 100).toFixed(0)}%`;
}

// ---- 更新指标面板 ----
function updateMetrics() {
  const t = state.densityThreshold;
  document.getElementById('m-density').innerHTML = `${(t * 100).toFixed(0)}<span class="metric-unit">%</span>`;

  // 刚度保留率:基于 Gibson-Ashby 模型 E*/Es ≈ C * (ρ*/ρs)^2
  // 简化:假设 C ≈ 1,n = 2
  const stiffnessRetention = Math.min(99, Math.max(50, (1 - Math.pow(1 - t, 2.5)) * 100));
  document.getElementById('m-stiffness').innerHTML = `${stiffnessRetention.toFixed(0)}<span class="metric-unit">%</span>`;

  // 质量削减 = 1 - 相对密度
  const weightReduction = -(1 - t) * 100;
  document.getElementById('m-weight').innerHTML = `${weightReduction.toFixed(0)}<span class="metric-unit">%</span>`;
}

// ---- 交互控制 ----
function setupControls() {
  // 密度阈值滑块
  const sThreshold = document.getElementById('s-threshold');
  sThreshold.addEventListener('input', () => {
    state.densityThreshold = parseFloat(sThreshold.value);
    document.getElementById('v-threshold').textContent = state.densityThreshold.toFixed(2);
    renderLattice();
    renderMicro();
    updateMetrics();
    initParticles();
  });

  // 速度滑块
  const sSpeed = document.getElementById('s-speed');
  sSpeed.addEventListener('input', () => {
    state.speed = parseFloat(sSpeed.value);
    document.getElementById('v-speed').textContent = state.speed.toFixed(1) + 'x';
  });

  // 切换按钮
  document.querySelectorAll('.toggle-row').forEach(row => {
    row.addEventListener('click', () => {
      const key = row.dataset.toggle;
      const box = row.querySelector('.toggle-box');
      if (key === 'ghost') {
        state.showGhost = !state.showGhost;
        box.classList.toggle('on', state.showGhost);
        renderGhost();
      } else if (key === 'shell') {
        state.showShell = !state.showShell;
        box.classList.toggle('on', state.showShell);
        renderShell();
      } else if (key === 'particles') {
        state.showParticles = !state.showParticles;
        box.classList.toggle('on', state.showParticles);
      } else if (key === 'micro') {
        state.showMicro = !state.showMicro;
        box.classList.toggle('on', state.showMicro);
        renderMicro();
      }
    });
  });
}

// ---- 主动画循环 ----
let lastTime = 0;

function animate(timestamp) {
  const dt = Math.min(timestamp - lastTime, 50); // 限制最大帧间隔
  lastTime = timestamp;

  state.time += dt * 0.001;

  updateParticles(dt);

  requestAnimationFrame(animate);
}

// ---- 初始化 ----
function init() {
  generateLattice();
  renderGhost();
  renderShell();
  renderLattice();
  renderForceArrows();
  renderLabels();
  renderMicro();
  initParticles();
  updateMetrics();
  setupControls();

  requestAnimationFrame(animate);
}

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

实现说明

本动画围绕 TRIZ 的**最终理想解(IFR)**思想构建,核心视觉叙事为:材料仅存在于受力路径上,冗余质量被彻底消除

视觉设计要点:

  1. 密度场驱动的点阵渲染:通过模拟拓扑优化结果的密度场函数,自动生成变密度八面体微观点阵。高密度区域(载荷传递路径)的杆件以明亮的青绿色发光呈现,低密度区域渐隐为暗蓝,无效区域完全透明——直接展现"材料仅分布在主应力轨迹上"的 IFR 本质。

  2. 应力流粒子系统:橙色发光粒子沿拓扑优化后的主应力路径从顶部载荷点流向底部支撑点,动态展示力流如何在微观杆件间均匀分散,赋予静态结构以"活的"力传导感。

  3. 交互式密度阈值滑块:用户可手动调节相对密度阈值(5%→65%),实时观察点阵结构的消长——阈值越低保留材料越少、质量削减越大,但刚度保留率也同步下降。这使直观体验"刚度-重量极值点"成为可能。

  4. 微观点阵详图(默认关闭,点击切换):展示 2×2×2 八面体单元的放大视图,标注杆件尺寸与相对密度/模量的 Gibson-Ashby 关系。

  5. 分层视觉架构:原始实体轮廓(幽灵线框)→ 芳纶纤维蒙皮(半透明金色壳)→ 钛合金点阵骨架 → 应力流粒子,四层叠加清晰表达"删除实心基座、替换为点阵+蒙皮"的方案逻辑。

积分规则:第一轮对话扣减6分,后续每轮扣4分