分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>折纸超材料蜂窝结构 · Miura-ori 原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
  --bg: #070b14;
  --bg2: #0d1320;
  --card: #111827;
  --border: #1e293b;
  --fg: #e2e8f0;
  --muted: #7a8ba5;
  --cyan: #22d3ee;
  --cyan-dim: rgba(34,211,238,0.10);
  --cyan-mid: rgba(34,211,238,0.28);
  --orange: #f97316;
  --orange-dim: rgba(249,115,22,0.12);
  --emerald: #10b981;
  --emerald-dim: rgba(16,185,129,0.15);
  --rose: #f43f5e;
}

* { margin:0; padding:0; box-sizing:border-box; }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: 'Fira Code', monospace;
  min-height: 100vh;
  overflow-x: hidden;
}

/* 背景网格 */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(rgba(34,211,238,0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(34,211,238,0.03) 1px, transparent 1px);
  background-size: 60px 60px;
  pointer-events: none;
  z-index: 0;
}

.page-wrap {
  position: relative;
  z-index: 1;
  max-width: 1440px;
  margin: 0 auto;
  padding: 24px 32px 40px;
}

/* 顶部标题 */
.header {
  display: flex;
  align-items: baseline;
  gap: 18px;
  margin-bottom: 24px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
}
.header h1 {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 28px;
  letter-spacing: -0.5px;
  color: var(--fg);
}
.header .tag {
  font-size: 11px;
  font-weight: 500;
  color: var(--cyan);
  background: var(--cyan-dim);
  padding: 3px 10px;
  border-radius: 4px;
  border: 1px solid rgba(34,211,238,0.18);
  letter-spacing: 0.5px;
}
.header .sub {
  font-size: 12px;
  color: var(--muted);
  margin-left: auto;
}

/* 主布局 */
.main-grid {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 24px;
  align-items: start;
}

/* SVG 容器 */
.svg-area {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  position: relative;
  overflow: hidden;
}
.svg-area::after {
  content: '';
  position: absolute;
  top: -60%;
  left: -20%;
  width: 140%;
  height: 100%;
  background: radial-gradient(ellipse, rgba(34,211,238,0.04) 0%, transparent 70%);
  pointer-events: none;
}

.svg-main {
  width: 100%;
  height: 420px;
  display: block;
}

.svg-section {
  width: 100%;
  height: 130px;
  display: block;
  margin-top: 12px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.section-label {
  font-size: 10px;
  color: var(--muted);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  margin-bottom: 6px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.section-label::before {
  content: '';
  width: 6px; height: 6px;
  background: var(--cyan);
  border-radius: 1px;
  transform: rotate(45deg);
}

/* 右侧面板 */
.panel {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 16px;
}
.card-title {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 13px;
  color: var(--muted);
  letter-spacing: 1px;
  text-transform: uppercase;
  margin-bottom: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.card-title .dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* 参数行 */
.param-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 7px 0;
  border-bottom: 1px solid rgba(30,41,59,0.5);
}
.param-row:last-child { border-bottom: none; }
.param-label {
  font-size: 11px;
  color: var(--muted);
}
.param-value {
  font-size: 13px;
  font-weight: 500;
  color: var(--fg);
}
.param-value.accent-cyan { color: var(--cyan); }
.param-value.accent-orange { color: var(--orange); }
.param-value.accent-emerald { color: var(--emerald); }

/* 时序步骤 */
.step {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 0;
  position: relative;
}
.step-indicator {
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 600;
  color: var(--muted);
  flex-shrink: 0;
  transition: all 0.4s;
}
.step.active .step-indicator {
  border-color: var(--cyan);
  color: var(--cyan);
  background: var(--cyan-dim);
  box-shadow: 0 0 12px rgba(34,211,238,0.25);
}
.step.done .step-indicator {
  border-color: var(--emerald);
  color: var(--emerald);
  background: var(--emerald-dim);
}
.step-text {
  font-size: 11px;
  color: var(--muted);
  line-height: 1.5;
  transition: color 0.3s;
}
.step.active .step-text { color: var(--fg); }
.step.done .step-text { color: var(--emerald); }

/* 失效条件 */
.warn-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  font-size: 11px;
  color: var(--muted);
}
.warn-icon {
  width: 16px; height: 16px;
  border-radius: 3px;
  background: var(--orange-dim);
  color: var(--orange);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  flex-shrink: 0;
}

/* 底部控制栏 */
.controls {
  margin-top: 20px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 18px 24px;
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
}

.slider-group {
  flex: 1;
  min-width: 220px;
}
.slider-label {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
  margin-bottom: 8px;
}
.slider-label .val { color: var(--cyan); font-weight: 500; }

input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  outline: none;
  cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px;
  background: var(--cyan);
  border-radius: 50%;
  box-shadow: 0 0 10px rgba(34,211,238,0.4);
  transition: transform 0.15s;
}
input[type=range]::-webkit-slider-thumb:hover {
  transform: scale(1.2);
}

.btn-group {
  display: flex;
  gap: 10px;
}
.btn {
  font-family: 'Fira Code', monospace;
  font-size: 12px;
  font-weight: 500;
  padding: 8px 18px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: all 0.2s;
  white-space: nowrap;
}
.btn:hover {
  border-color: var(--cyan);
  color: var(--cyan);
  background: var(--cyan-dim);
}
.btn.primary {
  background: rgba(34,211,238,0.12);
  border-color: rgba(34,211,238,0.3);
  color: var(--cyan);
}
.btn.primary:hover {
  background: rgba(34,211,238,0.22);
}
.btn.danger {
  color: var(--rose);
  border-color: rgba(244,63,94,0.25);
}
.btn.danger:hover {
  background: rgba(244,63,94,0.1);
  border-color: var(--rose);
}

/* 动画状态指示器 */
.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  padding: 4px 12px;
  border-radius: 20px;
  background: var(--cyan-dim);
  border: 1px solid rgba(34,211,238,0.2);
  color: var(--cyan);
  transition: all 0.4s;
}
.status-badge.locked {
  background: var(--emerald-dim);
  border-color: rgba(16,185,129,0.3);
  color: var(--emerald);
}
.status-badge.collapsed {
  background: var(--orange-dim);
  border-color: rgba(249,115,22,0.25);
  color: var(--orange);
}
.status-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: pulse-dot 1.5s ease infinite;
}
@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* 注释标签 */
.annotation {
  font-family: 'Fira Code', monospace;
  font-size: 9px;
  fill: var(--muted);
  letter-spacing: 0.5px;
}

/* 响应式 */
@media (max-width: 900px) {
  .main-grid {
    grid-template-columns: 1fr;
  }
  .page-wrap {
    padding: 16px;
  }
  .svg-main { height: 320px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
</style>
</head>
<body>
<div class="page-wrap">
  <!-- 标题栏 -->
  <header class="header">
    <h1>折纸超材料蜂窝结构</h1>
    <span class="tag">Miura-ori Metamaterial</span>
    <span class="sub">IFR 原理演示 · 柔性铰链折叠</span>
  </header>

  <!-- 主内容 -->
  <div class="main-grid">
    <!-- 左侧 SVG 动画区 -->
    <div class="svg-area">
      <svg id="mainSvg" class="svg-main" viewBox="-220 -60 520 360" preserveAspectRatio="xMidYMid meet"></svg>
      <div class="section-label">侧面截面 · 折叠轮廓</div>
      <svg id="sectionSvg" class="svg-section" viewBox="0 -30 420 100" preserveAspectRatio="xMidYMid meet"></svg>
    </div>

    <!-- 右侧信息面板 -->
    <div class="panel">
      <!-- 核心参数 -->
      <div class="card">
        <div class="card-title"><span class="dot" style="background:var(--cyan)"></span>核心参数</div>
        <div class="param-row">
          <span class="param-label">极限压缩比</span>
          <span class="param-value accent-cyan">15 : 1</span>
        </div>
        <div class="param-row">
          <span class="param-label">疲劳弯折寿命</span>
          <span class="param-value">&gt; 100,000 次</span>
        </div>
        <div class="param-row">
          <span class="param-label">当前展开度</span>
          <span class="param-value accent-cyan" id="paramExpand">0%</span>
        </div>
        <div class="param-row">
          <span class="param-label">实时厚度比</span>
          <span class="param-value accent-orange" id="paramThickness">1 : 1</span>
        </div>
        <div class="param-row">
          <span class="param-label">折叠角</span>
          <span class="param-value" id="paramAngle">0°</span>
        </div>
        <div class="param-row">
          <span class="param-label">结构状态</span>
          <span class="status-badge collapsed" id="statusBadge"><span class="status-dot"></span><span id="statusText">已折叠</span></span>
        </div>
      </div>

      <!-- 动作时序 -->
      <div class="card">
        <div class="card-title"><span class="dot" style="background:var(--orange)"></span>动作时序</div>
        <div class="step" id="step0">
          <div class="step-indicator">1</div>
          <div class="step-text">储能释放<br><span style="font-size:9px;opacity:0.6">卡扣解开,弹性势能初释放</span></div>
        </div>
        <div class="step" id="step1">
          <div class="step-indicator">2</div>
          <div class="step-text">弹性驱动展开<br><span style="font-size:9px;opacity:0.6">柔性膜弹力驱动初步展开</span></div>
        </div>
        <div class="step" id="step2">
          <div class="step-indicator">3</div>
          <div class="step-text">单手拉出<br><span style="font-size:9px;opacity:0.6">拉动末端细杆,蜂窝阵列展开</span></div>
        </div>
        <div class="step" id="step3">
          <div class="step-indicator">4</div>
          <div class="step-text">自锁刚性支撑<br><span style="font-size:9px;opacity:0.6">结构自锁形成硬质面板</span></div>
        </div>
        <div class="step" id="step4">
          <div class="step-indicator">5</div>
          <div class="step-text">按压塌缩<br><span style="font-size:9px;opacity:0.6">按压中心点触发同步塌缩</span></div>
        </div>
      </div>

      <!-- 适用边界 -->
      <div class="card">
        <div class="card-title"><span class="dot" style="background:var(--rose)"></span>失效边界</div>
        <div class="warn-item">
          <span class="warn-icon">!</span>
          <span>无法承受集中点载荷(尖锐穿刺)</span>
        </div>
        <div class="warn-item">
          <span class="warn-icon">!</span>
          <span>铰链沙尘沾染可致折叠卡死</span>
        </div>
        <div class="warn-item">
          <span class="warn-icon">~</span>
          <span>高频折叠后柔性膜蠕变松弛</span>
        </div>
      </div>
    </div>
  </div>

  <!-- 底部控制栏 -->
  <div class="controls">
    <div class="slider-group">
      <div class="slider-label">
        <span>折叠状态</span>
        <span class="val" id="sliderVal">0%</span>
      </div>
      <input type="range" id="foldSlider" min="0" max="100" value="0" step="1">
    </div>
    <div class="btn-group">
      <button class="btn primary" id="btnDeploy">展开部署</button>
      <button class="btn danger" id="btnCollapse">触发塌缩</button>
      <button class="btn" id="btnReset">重置</button>
    </div>
  </div>
</div>

<script>
// ============ 常量与配置 ============
const SVG_NS = 'http://www.w3.org/2000/svg';
const ROWS = 7;
const COLS = 9;
const CELL_W = 32;
const CELL_H = 26;
const PEAK_H = 20;
const COS30 = Math.cos(Math.PI / 6);
const SIN30 = 0.5;

// ============ 全局状态 ============
const state = {
  t: 0,           // 折叠参数 0=完全折叠 1=完全展开
  targetT: 0,
  animating: false,
  phase: -1,      // 当前时序阶段 -1=无 0~4对应五个步骤
  locked: false,
  seqResolve: null
};

// ============ SVG 辅助 ============
function svgEl(tag, attrs) {
  const el = document.createElementNS(SVG_NS, tag);
  if (attrs) Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
  return el;
}

// ============ 几何计算 ============
// 获取顶点的世界坐标和投影坐标
function getVertex(i, j, t) {
  // t=0 折叠: x方向极度压缩,y方向适度压缩,z=0
  // t=1 展开: 标准间距,z方向交替峰值
  const xScale = 0.08 + 0.92 * t;
  const yScale = 0.55 + 0.45 * t;
  const x = i * CELL_W * xScale;
  const y = j * CELL_H * yScale;
  const sign = ((i + j) % 2 === 0) ? 1 : -1;
  const z = t * PEAK_H * sign;
  // 等轴测投影
  const px = (x - y) * COS30;
  const py = -z + (x + y) * SIN30;
  return { x, y, z, px, py, sign };
}

// 获取截面数据(沿中间行的侧面轮廓)
function getSectionProfile(t) {
  const midJ = Math.floor(ROWS / 2);
  const pts = [];
  for (let i = 0; i <= COLS; i++) {
    const v = getVertex(i, midJ, t);
    pts.push({ x: v.x, z: v.z });
  }
  return pts;
}

// ============ 主 SVG 初始化 ============
const mainSvg = document.getElementById('mainSvg');
const sectionSvg = document.getElementById('sectionSvg');

// 定义滤镜
const defs = svgEl('defs');

// 辉光滤镜
const glowFilter = svgEl('filter', { id: 'glow', x: '-50%', y: '-50%', width: '200%', height: '200%' });
glowFilter.appendChild(svgEl('feGaussianBlur', { stdDeviation: '3', result: 'blur' }));
const glowMerge = svgEl('feMerge');
glowMerge.appendChild(svgEl('feMergeNode', { in: 'blur' }));
glowMerge.appendChild(svgEl('feMergeNode', { in: 'SourceGraphic' }));
glowFilter.appendChild(glowMerge);
defs.appendChild(glowFilter);

// 强辉光
const glowStrong = svgEl('filter', { id: 'glowStrong', x: '-80%', y: '-80%', width: '260%', height: '260%' });
glowStrong.appendChild(svgEl('feGaussianBlur', { stdDeviation: '6', result: 'blur' }));
const gsMerge = svgEl('feMerge');
gsMerge.appendChild(svgEl('feMergeNode', { in: 'blur' }));
gsMerge.appendChild(svgEl('feMergeNode', { in: 'SourceGraphic' }));
glowStrong.appendChild(gsMerge);
defs.appendChild(glowStrong);

// 阴影滤镜
const shadowFilter = svgEl('filter', { id: 'shadow', x: '-20%', y: '-20%', width: '140%', height: '140%' });
shadowFilter.appendChild(svgEl('feGaussianBlur', { stdDeviation: '4', result: 'blur' }));
const shadowFlood = svgEl('feFlood', { 'flood-color': 'rgba(0,0,0,0.35)', result: 'color' });
const shadowComp = svgEl('feComposite', { in: 'color', in2: 'blur', operator: 'in', result: 'shadow' });
const shadowMerge = svgEl('feMerge');
shadowMerge.appendChild(svgEl('feMergeNode', { in: 'shadow' }));
shadowMerge.appendChild(svgEl('feMergeNode', { in: 'SourceGraphic' }));
shadowFilter.appendChild(shadowFlood);
shadowFilter.appendChild(shadowComp);
shadowFilter.appendChild(shadowMerge);
defs.appendChild(shadowFilter);

mainSvg.appendChild(defs);

// 创建图层组
const shadowGroup = svgEl('g', { id: 'shadowLayer' });
const facetGroup = svgEl('g', { id: 'facetLayer' });
const rodGroup = svgEl('g', { id: 'rodLayer' });
const highlightGroup = svgEl('g', { id: 'highlightLayer' });
const annotationGroup = svgEl('g', { id: 'annotationLayer' });

mainSvg.appendChild(shadowGroup);
mainSvg.appendChild(facetGroup);
mainSvg.appendChild(rodGroup);
mainSvg.appendChild(highlightGroup);
mainSvg.appendChild(annotationGroup);

// 预创建面片元素 (ROWS x COLS 个四边形)
const facetEls = [];
for (let j = 0; j < ROWS; j++) {
  for (let i = 0; i < COLS; i++) {
    const poly = svgEl('polygon', {
      'stroke-linejoin': 'round'
    });
    facetEls.push({ el: poly, i, j });
    facetGroup.appendChild(poly);
  }
}

// 预创建碳纤维杆(水平边和垂直边)
const rodEls = [];
// 水平边: (ROWS+1) 行 x COLS 列
for (let j = 0; j <= ROWS; j++) {
  for (let i = 0; i < COLS; i++) {
    const line = svgEl('line', {});
    rodEls.push({ el: line, type: 'h', i, j });
    rodGroup.appendChild(line);
  }
}
// 垂直边: ROWS 行 x (COLS+1) 列
for (let j = 0; j < ROWS; j++) {
  for (let i = 0; i <= COLS; i++) {
    const line = svgEl('line', {});
    rodEls.push({ el: line, type: 'v', i, j });
    rodGroup.appendChild(line);
  }
}

// 高亮元素:中心触发点、拉力箭头、自锁指示
const centerHighlight = svgEl('circle', {
  r: '8', fill: 'none', stroke: '#f97316', 'stroke-width': '2',
  opacity: '0', filter: 'url(#glowStrong)', id: 'centerDot'
});
highlightGroup.appendChild(centerHighlight);

const centerPulse = svgEl('circle', {
  r: '14', fill: 'none', stroke: '#f97316', 'stroke-width': '1',
  opacity: '0', id: 'centerPulse'
});
highlightGroup.appendChild(centerPulse);

// 拉力箭头(展开时显示)
const arrowGroup = svgEl('g', { id: 'arrows', opacity: '0' });
// 左侧箭头
arrowGroup.appendChild(svgEl('line', { x1: '-190', y1: '60', x2: '-155', y2: '60', stroke: '#22d3ee', 'stroke-width': '2', 'marker-end': 'url(#arrowMarker)' }));
arrowGroup.appendChild(svgEl('line', { x1: '190', y1: '60', x2: '155', y2: '60', stroke: '#22d3ee', 'stroke-width': '2' }));
// 箭头标记
const arrowDefs = svgEl('defs');
const arrowMarker = svgEl('marker', { id: 'arrowMarker', viewBox: '0 0 10 10', refX: '5', refY: '5', markerWidth: '6', markerHeight: '6', orient: 'auto-start-reverse' });
arrowMarker.appendChild(svgEl('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: '#22d3ee' }));
arrowDefs.appendChild(arrowMarker);
mainSvg.insertBefore(arrowDefs, mainSvg.firstChild);
// 箭头文字
const arrowTextL = svgEl('text', { x: '-195', y: '55', fill: '#22d3ee', 'font-size': '9', 'font-family': 'Fira Code, monospace', 'text-anchor': 'end' });
arrowTextL.textContent = '拉力';
arrowGroup.appendChild(arrowTextL);
const arrowTextR = svgEl('text', { x: '195', y: '55', fill: '#22d3ee', 'font-size': '9', 'font-family': 'Fira Code, monospace', 'text-anchor': 'start' });
arrowTextR.textContent = '拉力';
arrowGroup.appendChild(arrowTextR);
highlightGroup.appendChild(arrowGroup);

// 自锁图标
const lockIcon = svgEl('g', { id: 'lockIcon', opacity: '0', transform: 'translate(0, -50)' });
lockIcon.appendChild(svgEl('rect', { x: '-8', y: '-2', width: '16', height: '12', rx: '2', fill: 'none', stroke: '#10b981', 'stroke-width': '1.5' }));
lockIcon.appendChild(svgEl('path', { d: 'M -4 -2 L -4 -6 A 4 4 0 0 1 4 -6 L 4 -2', fill: 'none', stroke: '#10b981', 'stroke-width': '1.5' }));
highlightGroup.appendChild(lockIcon);

// 厚度标注线
const thicknessLine = svgEl('line', { stroke: '#f97316', 'stroke-width': '1', 'stroke-dasharray': '3,3', opacity: '0' });
const thicknessText = svgEl('text', { fill: '#f97316', 'font-size': '9', 'font-family': 'Fira Code, monospace', 'text-anchor': 'start', opacity: '0' });
annotationGroup.appendChild(thicknessLine);
annotationGroup.appendChild(thicknessText);

// ============ 截面 SVG 初始化 ============
const secDefs = svgEl('defs');
const secGrad = svgEl('linearGradient', { id: 'secGrad', x1: '0', y1: '0', x2: '0', y2: '1' });
secGrad.appendChild(svgEl('stop', { offset: '0%', 'stop-color': 'rgba(34,211,238,0.3)' }));
secGrad.appendChild(svgEl('stop', { offset: '100%', 'stop-color': 'rgba(34,211,238,0.02)' }));
secDefs.appendChild(secGrad);
sectionSvg.appendChild(secDefs);

const secArea = svgEl('path', { fill: 'url(#secGrad)', stroke: 'none' });
const secLine = svgEl('path', { fill: 'none', stroke: '#22d3ee', 'stroke-width': '2' });
const secBaseline = svgEl('line', { x1: '0', y1: '0', x2: '420', y2: '0', stroke: '#1e293b', 'stroke-width': '1', 'stroke-dasharray': '4,4' });
const secLabel = svgEl('text', { x: '410', y: '-8', fill: '#7a8ba5', 'font-size': '9', 'font-family': 'Fira Code, monospace', 'text-anchor': 'end' });
secLabel.textContent = '折叠基准面';
sectionSvg.appendChild(secBaseline);
sectionSvg.appendChild(secLabel);
sectionSvg.appendChild(secArea);
sectionSvg.appendChild(secLine);

// 截面厚度标注
const secThickLine = svgEl('line', { stroke: '#f97316', 'stroke-width': '1', 'stroke-dasharray': '2,2', opacity: '0' });
const secThickText = svgEl('text', { fill: '#f97316', 'font-size': '9', 'font-family': 'Fira Code, monospace', opacity: '0' });
sectionSvg.appendChild(secThickLine);
sectionSvg.appendChild(secThickText);

// ============ 渲染函数 ============
function render(t) {
  t = Math.max(0, Math.min(1, t));

  // 计算所有顶点
  const verts = [];
  for (let j = 0; j <= ROWS; j++) {
    verts[j] = [];
    for (let i = 0; i <= COLS; i++) {
      verts[j][i] = getVertex(i, j, t);
    }
  }

  // 计算中心点位置
  const ci = Math.floor(COLS / 2);
  const cj = Math.floor(ROWS / 2);
  const centerV = verts[cj][ci];

  // 渲染面片
  for (const { el, i, j } of facetEls) {
    const v0 = verts[j][i];
    const v1 = verts[j][i + 1];
    const v2 = verts[j + 1][i + 1];
    const v3 = verts[j + 1][i];

    el.setAttribute('points',
      `${v0.px},${v0.py} ${v1.px},${v1.py} ${v2.px},${v2.py} ${v3.px},${v3.py}`
    );

    // 颜色:根据面的朝向(上/下)和展开度
    const avgSign = (v0.sign + v1.sign + v2.sign + v3.sign) / 4;
    const isUp = avgSign > 0;
    const baseAlpha = 0.06 + 0.20 * t;
    const alpha = isUp ? baseAlpha + 0.08 : baseAlpha;

    if (state.locked && t > 0.95) {
      // 自锁刚性状态:更饱和
      const lockedAlpha = isUp ? 0.38 : 0.22;
      el.setAttribute('fill', isUp ? `rgba(34,211,238,${lockedAlpha})` : `rgba(16,185,129,${lockedAlpha})`);
      el.setAttribute('stroke', isUp ? 'rgba(34,211,238,0.6)' : 'rgba(16,185,129,0.45)');
      el.setAttribute('stroke-width', '1.2');
    } else {
      el.setAttribute('fill', isUp ? `rgba(34,211,238,${alpha})` : `rgba(34,211,238,${alpha * 0.5})`);
      el.setAttribute('stroke', `rgba(34,211,238,${0.12 + 0.25 * t})`);
      el.setAttribute('stroke-width', '0.6');
    }
  }

  // 渲染碳纤维杆
  for (const { el, type, i, j } of rodEls) {
    let v0, v1;
    if (type === 'h') {
      v0 = verts[j][i];
      v1 = verts[j][i + 1];
    } else {
      v0 = verts[j][i];
      v1 = verts[j + 1][i];
    }
    el.setAttribute('x1', v0.px);
    el.setAttribute('y1', v0.py);
    el.setAttribute('x2', v1.px);
    el.setAttribute('y2', v1.py);

    // 碳纤维杆样式:折叠线处更亮
    const isFoldLine = (type === 'v' && (i + j) % 2 === 0) || (type === 'h' && (i + j) % 2 === 0);
    const rodAlpha = isFoldLine ? (0.25 + 0.55 * t) : (0.08 + 0.18 * t);
    const rodWidth = isFoldLine ? (0.8 + 1.2 * t) : (0.4 + 0.3 * t);

    if (state.locked && t > 0.95 && isFoldLine) {
      el.setAttribute('stroke', `rgba(16,185,129,${rodAlpha + 0.15})`);
      el.setAttribute('stroke-width', String(rodWidth + 0.5));
    } else {
      el.setAttribute('stroke', `rgba(34,211,238,${rodAlpha})`);
      el.setAttribute('stroke-width', String(rodWidth));
    }
    el.setAttribute('stroke-linecap', 'round');
  }

  // 中心高亮点(塌缩触发点)
  centerHighlight.setAttribute('cx', centerV.px);
  centerHighlight.setAttribute('cy', centerV.py);
  centerPulse.setAttribute('cx', centerV.px);
  centerPulse.setAttribute('cy', centerV.py);

  // 拉力箭头位置
  const leftV = verts[Math.floor(ROWS / 2)][0];
  const rightV = verts[Math.floor(ROWS / 2)][COLS];
  arrowGroup.querySelector('line:nth-child(1)').setAttribute('y1', leftV.py - 10);
  arrowGroup.querySelector('line:nth-child(1)').setAttribute('y2', leftV.py - 10);
  arrowGroup.querySelector('line:nth-child(2)').setAttribute('y1', rightV.py - 10);
  arrowGroup.querySelector('line:nth-child(2)').setAttribute('y2', rightV.py - 10);
  arrowTextL.setAttribute('y', leftV.py - 16);
  arrowTextR.setAttribute('y', rightV.py - 16);

  // 自锁图标位置
  lockIcon.setAttribute('transform', `translate(${centerV.px}, ${centerV.py - 45})`);

  // 厚度标注
  if (t > 0.05) {
    const topV = getVertex(ci, 0, t);
    const botV = getVertex(ci, ROWS, t);
    const x = botV.px + 30;
    const y1 = topV.py;
    const y2 = botV.py;
    thicknessLine.setAttribute('x1', x); thicknessLine.setAttribute('y1', y1);
    thicknessLine.setAttribute('x2', x); thicknessLine.setAttribute('y2', y2);
    thicknessLine.setAttribute('opacity', '0.6');
    thicknessText.setAttribute('x', x + 5);
    thicknessText.setAttribute('y', (y1 + y2) / 2 + 3);
    const thicknessRatio = Math.max(1, Math.round(15 * t));
    thicknessText.textContent = `${thicknessRatio}:1`;
    thicknessText.setAttribute('opacity', '0.7');
  } else {
    thicknessLine.setAttribute('opacity', '0');
    thicknessText.setAttribute('opacity', '0');
  }

  // ---- 截面渲染 ----
  const profile = getSectionProfile(t);
  const xOff = 10;
  const yOff = 0;
  const scaleX = 420 / (COLS * CELL_W + 20);

  let linePath = '';
  let areaPath = '';
  profile.forEach((p, idx) => {
    const sx = xOff + p.x * scaleX;
    const sy = yOff - p.z * 2.5; // 放大z方向以便观察
    if (idx === 0) {
      linePath += `M ${sx} ${sy}`;
      areaPath += `M ${sx} 0 L ${sx} ${sy}`;
    } else {
      linePath += ` L ${sx} ${sy}`;
      areaPath += ` L ${sx} ${sy}`;
    }
  });
  const lastP = profile[profile.length - 1];
  const lastSx = xOff + lastP.x * scaleX;
  areaPath += ` L ${lastSx} 0 Z`;

  secLine.setAttribute('d', linePath);
  secArea.setAttribute('d', areaPath);

  // 截面厚度标注
  if (t > 0.1) {
    const maxZ = PEAK_H * t * 2.5;
    const markerX = lastSx + 15;
    secThickLine.setAttribute('x1', markerX);
    secThickLine.setAttribute('y1', -maxZ);
    secThickLine.setAttribute('x2', markerX);
    secThickLine.setAttribute('y2', maxZ);
    secThickLine.setAttribute('opacity', '0.6');
    secThickText.setAttribute('x', markerX + 4);
    secThickText.setAttribute('y', 3);
    secThickText.textContent = `h=${(PEAK_H * t * 2).toFixed(1)}`;
    secThickText.setAttribute('opacity', '0.7');
  } else {
    secThickLine.setAttribute('opacity', '0');
    secThickText.setAttribute('opacity', '0');
  }

  // ---- UI 参数更新 ----
  updateParams(t);
}

// ============ 参数面板更新 ============
function updateParams(t) {
  document.getElementById('paramExpand').textContent = Math.round(t * 100) + '%';
  const thicknessRatio = Math.max(1, Math.round(15 * t));
  document.getElementById('paramThickness').textContent = `1 : ${thicknessRatio}`;
  document.getElementById('paramAngle').textContent = Math.round(t * 72) + '°';

  const badge = document.getElementById('statusBadge');
  const statusText = document.getElementById('statusText');

  badge.className = 'status-badge';
  if (state.locked && t > 0.95) {
    badge.classList.add('locked');
    statusText.textContent = '自锁刚性';
  } else if (t < 0.05) {
    badge.classList.add('collapsed');
    statusText.textContent = '已折叠';
  } else {
    statusText.textContent = '展开中';
  }
}

// ============ 时序步骤高亮 ============
function setPhase(phaseIdx) {
  state.phase = phaseIdx;
  for (let i = 0; i < 5; i++) {
    const el = document.getElementById('step' + i);
    el.className = 'step';
    if (i < phaseIdx) el.classList.add('done');
    if (i === phaseIdx) el.classList.add('active');
  }
}

// ============ 高亮效果控制 ============
function showCenterHighlight(show) {
  centerHighlight.setAttribute('opacity', show ? '0.8' : '0');
  centerPulse.setAttribute('opacity', show ? '0.4' : '0');
}

function showArrows(show) {
  arrowGroup.setAttribute('opacity', show ? '0.7' : '0');
}

function showLockIcon(show) {
  lockIcon.setAttribute('opacity', show ? '0.9' : '0');
}

// 中心脉冲动画
let pulseAnim = null;
function startPulse() {
  let frame = 0;
  function tick() {
    frame++;
    const s = 1 + 0.4 * Math.sin(frame * 0.08);
    const o = 0.3 + 0.3 * Math.sin(frame * 0.08);
    centerPulse.setAttribute('r', String(14 * s));
    centerPulse.setAttribute('opacity', String(o));
    pulseAnim = requestAnimationFrame(tick);
  }
  tick();
}
function stopPulse() {
  if (pulseAnim) cancelAnimationFrame(pulseAnim);
  pulseAnim = null;
  centerPulse.setAttribute('opacity', '0');
}

// ============ 动画引擎 ============
let rafId = null;
const ANIM_SPEED = 0.012;

function animateToTarget() {
  if (state.animating) return;
  state.animating = true;

  function tick() {
    const diff = state.targetT - state.t;
    if (Math.abs(diff) < 0.002) {
      state.t = state.targetT;
      render(state.t);
      state.animating = false;
      if (state.seqResolve) {
        const resolve = state.seqResolve;
        state.seqResolve = null;
        resolve();
      }
      return;
    }
    // 使用缓动
    const speed = Math.max(0.004, Math.abs(diff) * 0.08);
    state.t += Math.sign(diff) * Math.min(speed, Math.abs(diff));
    state.t = Math.max(0, Math.min(1, state.t));
    render(state.t);
    document.getElementById('foldSlider').value = Math.round(state.t * 100);
    document.getElementById('sliderVal').textContent = Math.round(state.t * 100) + '%';
    rafId = requestAnimationFrame(tick);
  }
  tick();
}

function setTarget(t) {
  state.targetT = Math.max(0, Math.min(1, t));
  if (!state.animating) animateToTarget();
}

// ============ 部署序列 ============
async function playDeploySequence() {
  state.locked = false;

  // 步骤1:储能释放 - 快速弹出一点
  setPhase(0);
  showCenterHighlight(true);
  startPulse();
  await delay(400);
  setTarget(0.12);
  await waitForAnim();
  await delay(200);

  // 步骤2:弹性驱动展开
  setPhase(1);
  showCenterHighlight(false);
  stopPulse();
  showArrows(true);
  setTarget(0.45);
  await waitForAnim();
  await delay(200);

  // 步骤3:单手拉出
  setPhase(2);
  setTarget(0.85);
  await waitForAnim();
  await delay(150);

  // 步骤4:完全展开 + 自锁
  setPhase(3);
  setTarget(1.0);
  await waitForAnim();
  await delay(100);

  // 自锁效果
  state.locked = true;
  showArrows(false);
  showLockIcon(true);
  render(state.t); // 刷新为锁定样式

  // 闪烁自锁图标
  for (let i = 0; i < 3; i++) {
    lockIcon.setAttribute('opacity', '0.3');
    await delay(120);
    lockIcon.setAttribute('opacity', '1');
    await delay(120);
  }
  await delay(600);
  showLockIcon(false);
}

// ============ 塌缩序列 ============
async function playCollapseSequence() {
  state.locked = false;

  // 步骤5:按压中心触发塌缩
  setPhase(4);
  showCenterHighlight(true);
  startPulse();
  await delay(600);
  showCenterHighlight(false);
  stopPulse();
  showArrows(false);
  showLockIcon(false);

  // 从中心向外的塌缩波 - 通过快速压缩实现
  setTarget(0);
  await waitForAnim();

  // 重置时序
  setPhase(-1);
}

// ============ 辅助 ============
function delay(ms) {
  return new Promise(r => setTimeout(r, ms));
}

function waitForAnim() {
  return new Promise(r => {
    if (!state.animating) { r(); return; }
    state.seqResolve = r;
  });
}

// ============ 交互绑定 ============
const slider = document.getElementById('foldSlider');

slider.addEventListener('input', (e) => {
  const val = parseInt(e.target.value);
  state.t = val / 100;
  state.targetT = state.t;
  state.locked = false;
  showArrows(false);
  showLockIcon(false);
  showCenterHighlight(false);
  stopPulse();
  setPhase(-1);
  render(state.t);
  document.getElementById('sliderVal').textContent = val + '%';
});

document.getElementById('btnDeploy').addEventListener('click', () => {
  if (state.animating) return;
  playDeploySequence();
});

document.getElementById('btnCollapse').addEventListener('click', () => {
  if (state.animating) return;
  playCollapseSequence();
});

document.getElementById('btnReset').addEventListener('click', () => {
  if (state.animating) {
    cancelAnimationFrame(rafId);
    state.animating = false;
  }
  state.t = 0;
  state.targetT = 0;
  state.locked = false;
  state.phase = -1;
  showArrows(false);
  showLockIcon(false);
  showCenterHighlight(false);
  stopPulse();
  setPhase(-1);
  slider.value = 0;
  document.getElementById('sliderVal').textContent = '0%';
  render(0);
});

// ============ 初始化 ============
function init() {
  render(0);

  // 入场动画:结构微微抖动后静止
  setTimeout(() => {
    let frame = 0;
    function introTick() {
      frame++;
      if (frame > 40) {
        render(0);
        return;
      }
      const wobble = Math.sin(frame * 0.3) * 0.005 * (40 - frame) / 40;
      render(wobble);
      requestAnimationFrame(introTick);
    }
    introTick();
  }, 500);
}

window.addEventListener('DOMContentLoaded', init);

// 自动展开演示(首次加载 3 秒后)
let autoPlayed = false;
setTimeout(() => {
  if (!autoPlayed && state.t < 0.01) {
    autoPlayed = true;
    playDeploySequence();
  }
}, 3000);
</script>
</body>
</html>

实现说明

核心设计思路

本动画以 TRIZ 最终理想解 (IFR) 为指导思想,直接展示折纸超材料消除空间矛盾后的理想运作状态,而非对比传统方案。

几何与动画

  • 三浦折叠 (Miura-ori) 建模:使用 ROWS×COLS 网格顶点,通过参数 t(0=折叠,1=展开)驱动等轴测投影。顶点高度按棋盘格交替正负,模拟山折/谷折的锯齿形结构。X 方向压缩比达 0.08:1(折叠时仅保留 8% 间距),直观呈现 15:1 的极致空间压缩。
  • 双视图联动:上方主视图为等轴测 3D 透视图,下方为侧面截面轮廓,同步响应折叠参数变化,让用户同时理解三维结构和折叠几何原理。

交互设计

  • 滑块:手动连续控制折叠状态,实时体验从极薄平面到刚性蜂窝的完整过程
  • "展开部署"按钮:按动作时序自动播放五步协同过程(储能释放→弹性驱动→手动拉出→自锁刚性),每步在右侧面板高亮对应步骤
  • "触发塌缩"按钮:模拟按压中心点触发的同步塌缩,中心点以脉冲动画标示
  • 首次加载 3 秒后自动演示展开序列

视觉引导

  • 碳纤维骨架:折叠线处杆件更粗更亮,非折叠线处较细较淡,区分结构主次
  • 柔性膜填充:面片半透明着色,朝上面更亮、朝下面更暗,营造立体层次
  • 自锁状态:展开锁定后整体变为翠绿色调、边缘加粗,配合锁形图标闪烁,传达"刚性支撑"的触感
  • 动态标注:厚度比、折叠角、展开度等参数随动画实时更新;截面视图中标注当前折叠高度
积分规则:第一轮对话扣减6分,后续每轮扣4分