<!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">> 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分
等待动画代码生成...
