独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>预紧扭簧铰链与拉索自规整折伞机构 - 原理可视化</title>
<style>
:root {
--bg-dark: #090d16;
--panel-bg: rgba(15, 23, 42, 0.75);
--panel-border: rgba(56, 189, 248, 0.2);
--cyan-accent: #38bdf8;
--orange-accent: #f59e0b;
--green-accent: #10b981;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--font-mono: "SF Mono", "Consolas", "Roboto Mono", monospace;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: var(--font-sans);
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部微型信息条 - 紧凑精炼,不遮挡主体 */
.top-bar {
position: absolute;
top: 12px;
left: 16px;
right: 16px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 20;
pointer-events: none;
}
.brand-tag {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 20px;
backdrop-filter: blur(8px);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
animation: pulse-dot 2s infinite ease-in-out;
}
@keyframes pulse-dot {
0%, 100% { opacity: 0.5; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.2); }
}
.brand-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
color: #e2e8f0;
}
.brand-sub {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
margin-left: 4px;
}
/* 状态遥测浮动条 - 极小字号放置在右上角 */
.hud-metrics {
display: flex;
gap: 10px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 8px;
backdrop-filter: blur(8px);
}
.metric-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.metric-label {
font-size: 9px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 12px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--cyan-accent);
}
/* 中央全屏核心画布 */
.stage-container {
flex: 1;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: radial-gradient(circle at 50% 45%, #131d31 0%, #090d16 80%);
}
/* 背景工程栅格 */
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(56, 189, 248, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
pointer-events: none;
}
/* 主 SVG 动画视图 */
svg#mechanism-stage {
width: 100%;
height: 100%;
max-width: 1200px;
max-height: 820px;
z-index: 10;
}
/* 铰链微观特写画中画 (PIP) - 右下角紧凑浮窗 */
.detail-pip {
position: absolute;
right: 18px;
bottom: 84px;
width: 200px;
height: 160px;
background: rgba(13, 20, 36, 0.85);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: 10px;
padding: 8px;
backdrop-filter: blur(10px);
z-index: 20;
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
}
.pip-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.pip-title {
font-size: 10px;
font-weight: 600;
color: var(--orange-accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pip-tag {
font-size: 9px;
font-family: var(--font-mono);
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
padding: 1px 4px;
border-radius: 3px;
}
.pip-svg {
flex: 1;
width: 100%;
height: 100%;
}
/* 底部核心精简控制栏 */
.bottom-control-bar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
width: min(680px, 92%);
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 28px;
padding: 8px 18px;
display: flex;
align-items: center;
gap: 14px;
backdrop-filter: blur(12px);
z-index: 30;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.ctrl-btn {
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.3);
color: var(--cyan-accent);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.ctrl-btn:hover {
background: var(--cyan-accent);
color: #04101e;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
}
.ctrl-slider-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.slider-container {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: rgba(255,255,255,0.15);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.state-badge {
font-size: 11px;
font-weight: 600;
color: #38bdf8;
min-width: 80px;
text-align: right;
font-family: var(--font-mono);
}
/* 图例指示器 - 左下角精炼放置 */
.legend-panel {
position: absolute;
left: 18px;
bottom: 84px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 8px 12px;
backdrop-filter: blur(8px);
z-index: 20;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: #cbd5e1;
}
.legend-color {
width: 12px;
height: 3px;
border-radius: 2px;
}
.legend-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
</style>
</head>
<body>
<!-- 顶部轻量 HUD -->
<header class="top-bar">
<div class="brand-tag">
<div class="status-dot"></div>
<div class="brand-title">自规整折伞机构 <span class="brand-sub">IFR 理想解动态原理</span></div>
</div>
<div class="hud-metrics">
<div class="metric-item">
<span class="metric-label">拉索工作张力</span>
<span class="metric-value" id="cable-tension-val">300.0 N</span>
</div>
<div class="metric-item">
<span class="metric-label">扭簧预紧储能</span>
<span class="metric-value" id="spring-torque-val">0.02 N·m</span>
</div>
<div class="metric-item">
<span class="metric-label">骨段贴合度</span>
<span class="metric-value" id="alignment-val">100 %</span>
</div>
</div>
</header>
<!-- 左下角图例说明 -->
<div class="legend-panel">
<div class="legend-item">
<div class="legend-color" style="background: #38bdf8; box-shadow: 0 0 6px #38bdf8;"></div>
<span>中央张紧拉索 (1.2mm 编织缆)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #f59e0b; box-shadow: 0 0 6px #f59e0b;"></div>
<span>内折预紧扭簧铰链 (限位165°)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #94a3b8;"></div>
<span>玻纤扁骨 (180mm + 160mm)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #10b981;"></div>
<span>尾段防划软质导向球头</span>
</div>
</div>
<!-- 右下角局部放大铰链特写 (画中画) -->
<div class="detail-pip">
<div class="pip-header">
<span class="pip-title">铰链内部扭簧剖面</span>
<span class="pip-tag" id="hinge-pip-mode">锁定受拉</span>
</div>
<svg class="pip-svg" viewBox="0 0 160 120" id="pip-hinge-svg">
<!-- 铰链放大图动态元素 -->
<g id="pip-dynamic-hinge" transform="translate(80, 60)">
<!-- 铰链座 -->
<circle cx="0" cy="0" r="28" fill="#1e293b" stroke="#475569" stroke-width="1.5"/>
<!-- 限位咬合台阶 -->
<path d="M -20 -10 L 0 -10 L 15 15" stroke="rgba(245,158,11,0.5)" stroke-dasharray="2 2" fill="none"/>
<!-- 骨1插芯 -->
<rect x="-42" y="-5" width="42" height="10" rx="3" fill="#334155" stroke="#64748b"/>
<!-- 骨2旋转臂 -->
<g id="pip-rot-arm">
<rect x="0" y="-5" width="42" height="10" rx="3" fill="#475569" stroke="#94a3b8"/>
<!-- 旋转轴心 -->
<circle cx="0" cy="0" r="4" fill="#f59e0b"/>
<!-- 扭簧螺旋线条 -->
<path id="pip-spring-coil" d="M -7 -7 A 10 10 0 1 1 7 7" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
</g>
<!-- 拉索穿行轨迹 -->
<path id="pip-cable-line" d="M -45 -12 L 0 -12 L 45 -12" fill="none" stroke="#38bdf8" stroke-width="2" stroke-dasharray="4 2"/>
<circle cx="0" cy="-12" r="3" fill="#38bdf8"/>
<!-- 扭矩力矢量箭头 -->
<path id="pip-torque-arrow" d="M -16 16 A 22 22 0 0 0 16 16" fill="none" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow)"/>
</g>
<defs>
<marker id="arrow" viewBox="0 0 6 6" refX="4" refY="3" markerWidth="4" markerHeight="4" orient="auto-start-reverse">
<polygon points="0 0, 6 3, 0 6" fill="#f59e0b"/>
</marker>
</defs>
</svg>
</div>
<!-- 中央主舞台 -->
<main class="stage-container">
<div class="grid-bg"></div>
<svg id="mechanism-stage" viewBox="0 0 1000 680" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 渐变定义 -->
<linearGradient id="canopyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#0369a1" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="boneGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#cbd5e1"/>
<stop offset="100%" stop-color="#64748b"/>
</linearGradient>
<linearGradient id="shaftGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#334155"/>
<stop offset="50%" stop-color="#94a3b8"/>
<stop offset="100%" stop-color="#1e293b"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-orange" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 机构几何层(由 JS 动态生成与驱动) -->
<g id="scene-root">
<!-- 伞面布面投影 -->
<path id="canopy-surface-left" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<path id="canopy-surface-right" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<!-- 收伞聚拢导向约束外轮廓(淡线提示规整圆柱包络) -->
<line id="envelope-left" x1="440" y1="120" x2="440" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<line id="envelope-right" x1="560" y1="120" x2="560" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<!-- 中棒主轴与伞巢 -->
<g id="shaft-assembly">
<!-- 伞顶导向轮与顶巢 -->
<circle cx="500" cy="115" r="14" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="500" cy="115" r="5" fill="#38bdf8" filter="url(#glow-cyan)"/>
<!-- 中棒主体钢管 -->
<rect x="494" y="115" width="12" height="470" rx="3" fill="url(#shaftGrad)"/>
<!-- 刻度线 -->
<line x1="497" y1="180" x2="497" y2="480" stroke="rgba(255,255,255,0.15)" stroke-dasharray="2 6"/>
<!-- 伞柄手把 -->
<rect x="490" y="585" width="20" height="50" rx="6" fill="#0f172a" stroke="#334155" stroke-width="2"/>
<circle cx="500" cy="625" r="4" fill="#38bdf8"/>
</g>
<!-- 滑块套筒 (Runner) -->
<g id="runner-assembly" transform="translate(500, 260)">
<rect x="-14" y="-12" width="28" height="24" rx="4" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="0" cy="0" r="3" fill="#38bdf8"/>
<!-- 滑块侧边拉索锁固点 -->
<circle cx="-9" cy="0" r="2" fill="#f59e0b"/>
<circle cx="9" cy="0" r="2" fill="#f59e0b"/>
</g>
<!-- 左侧完整三节骨架机构组 -->
<g id="left-arm">
<line id="l-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<path id="l-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<line id="l-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<line id="l-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<line id="l-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链节点 -->
<g id="l-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<g id="l-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<!-- 末端软质防刮球头 -->
<circle id="l-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 右侧完整三节骨架机构组 (详细高亮对称展示) -->
<g id="right-arm">
<!-- 连杆支撑杆 -->
<line id="r-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<!-- 中央张紧编织拉索 -->
<path id="r-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<!-- 主骨第1段 (180mm) -->
<line id="r-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<!-- 主骨第2段 (160mm) -->
<line id="r-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<!-- 尾骨段 -->
<line id="r-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链1 (扭簧预紧节点 A) -->
<g id="r-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon1" d="M -6 -6 A 8 8 0 1 1 6 6" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 铰链2 (扭簧预紧节点 B) -->
<g id="r-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon2" d="M -5 -5 A 7 7 0 1 1 5 5" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 软质导向球头 -->
<circle id="r-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 动态应力矢量与折叠流动导向波纹 -->
<g id="dynamics-layer" pointer-events="none"></g>
</g>
</svg>
</main>
<!-- 底部微交互控制栏 -->
<footer class="bottom-control-bar">
<button class="ctrl-btn" id="play-btn" title="播放 / 暂停 (空格)">
<svg id="play-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
</svg>
</button>
<div class="ctrl-slider-group">
<div class="slider-labels">
<span>收拢贴合 (拉索松弛·扭簧驱动)</span>
<span id="slider-pct">开伞状态 (100%)</span>
<span>完全撑开 (拉索锁定·刚性限位)</span>
</div>
<div class="slider-container">
<input type="range" id="progress-slider" min="0" max="1" step="0.001" value="1"/>
</div>
</div>
<div class="state-badge" id="mech-state-badge">刚性锁定</div>
</footer>
<script>
/**
* 连杆与扭簧拉索动力学模拟核心
* 实现开伞刚性锁定与收伞自动顺序贴合的 IFR 机制
*/
// 几何拓扑常量配置 (单位: SVG 坐标像素,精确对齐物理比例)
const CFG = {
cx: 500, // 伞中棒 X 坐标
topY: 120, // 伞顶固定铰接座 Y
handleY: 585, // 伞柄位置
// 骨节长度
l1: 175, // 主骨第1段 (180mm 标定)
l2: 155, // 主骨第2段 (160mm 标定)
l3: 110, // 尾骨段
lStrut: 130, // 支撑撑杆长度
strutAttachRatio: 0.38, // 撑杆在主骨1上的铰接比例
// 滑块行程
runnerY_open: 245, // 开伞时滑块位置 (高位,拉紧)
runnerY_close: 520 // 收伞时滑块位置 (下移,卸载)
};
// 动画状态机
let state = {
progress: 1.0, // 0 = 彻底收拢贴合, 1 = 彻底撑开锁定
direction: -1, // 默认加载后自动开始收伞演示
isPlaying: true,
speed: 0.0042, // 平滑播放步进速度
lastTimestamp: 0
};
// DOM 元素引用
const els = {
slider: document.getElementById('progress-slider'),
playBtn: document.getElementById('play-btn'),
playIcon: document.getElementById('play-icon'),
badge: document.getElementById('mech-state-badge'),
sliderPct: document.getElementById('slider-pct'),
cableTensionVal: document.getElementById('cable-tension-val'),
springTorqueVal: document.getElementById('spring-torque-val'),
alignmentVal: document.getElementById('alignment-val'),
// PIP 特写
pipHingeMode: document.getElementById('hinge-pip-mode'),
pipRotArm: document.getElementById('pip-rot-arm'),
pipSpringCoil: document.getElementById('pip-spring-coil'),
pipCableLine: document.getElementById('pip-cable-line'),
// 机构图形元素
runner: document.getElementById('runner-assembly'),
rStrut: document.getElementById('r-strut'),
rCable: document.getElementById('r-cable'),
rBone1: document.getElementById('r-bone1'),
rBone2: document.getElementById('r-bone2'),
rBone3: document.getElementById('r-bone3'),
rJointH1: document.getElementById('r-joint-h1'),
rJointH2: document.getElementById('r-joint-h2'),
rTip: document.getElementById('r-tip'),
rTorqueIcon1: document.getElementById('r-torque-icon1'),
rTorqueIcon2: document.getElementById('r-torque-icon2'),
// 左侧镜像
lStrut: document.getElementById('l-strut'),
lCable: document.getElementById('l-cable'),
lBone1: document.getElementById('l-bone1'),
lBone2: document.getElementById('l-bone2'),
lBone3: document.getElementById('l-bone3'),
lJointH1: document.getElementById('l-joint-h1'),
lJointH2: document.getElementById('l-joint-h2'),
lTip: document.getElementById('l-tip'),
// 伞面与轮廓
canopyLeft: document.getElementById('canopy-surface-left'),
canopyRight: document.getElementById('canopy-surface-right'),
envL: document.getElementById('envelope-left'),
envR: document.getElementById('envelope-right'),
};
// 缓动函数
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
/**
* 根据当前开合进度 t 计算全套骨节与拉索的空间几何姿态
* @param {number} t [0 = 收伞, 1 = 开伞]
* @param {number} side 1 = 右侧, -1 = 左侧
*/
function calculateKinematics(t, side = 1) {
// 1. 滑块位置插值
const runnerY = CFG.runnerY_close + (CFG.runnerY_open - CFG.runnerY_close) * t;
// 2. 主骨1仰角:收伞贴近中棒(约86度向下),开伞展开到约-18度(上扬形成伞穹)
const bone1AngleDeg = 86 - t * 104; // 86° -> -18°
const bone1Rad = (bone1AngleDeg * Math.PI) / 180;
// 主骨1基点 (伞巢固定点)
const p0 = { x: CFG.cx + side * 8, y: CFG.topY };
// 主骨1末端 (铰链1位置)
const p1 = {
x: p0.x + side * CFG.l1 * Math.cos(bone1Rad),
y: p0.y + CFG.l1 * Math.sin(bone1Rad)
};
// 支撑连杆铰接在主骨1上的点
const pStrutAttach = {
x: p0.x + (p1.x - p0.x) * CFG.strutAttachRatio,
y: p0.y + (p1.y - p0.y) * CFG.strutAttachRatio
};
// 滑块支撑座铰接点
const pRunnerJoint = {
x: CFG.cx + side * 10,
y: runnerY
};
// 3. 铰链1 (主骨2) 折角计算 - 创新核心:
// 当 t > 0.8 时拉索完全拉直锁定 (相对夹角 0°)
// 当 t < 0.8 时,扭簧释放,折角按时序急剧向内反折(达到 165° 限位)
let fold1 = 0;
if (t < 0.85) {
const foldProgress1 = (0.85 - t) / 0.85; // 0 -> 1
fold1 = Math.pow(foldProgress1, 1.2) * 165; // 最大内折 165 度
}
const bone2Rad = bone1Rad - side * (fold1 * Math.PI / 180) * 0.96;
const p2 = {
x: p1.x + side * CFG.l2 * Math.cos(bone2Rad),
y: p1.y + CFG.l2 * Math.sin(bone2Rad)
};
// 4. 铰链2 (尾骨段) 折角计算 - 尾骨优先极速内折规整
let fold2 = 0;
if (t < 0.95) {
const foldProgress2 = Math.min(1, (0.95 - t) / 0.7);
fold2 = Math.pow(foldProgress2, 0.9) * 155; // 尾骨反折贴入
}
const bone3Rad = bone2Rad + side * (fold2 * Math.PI / 180) * 0.88;
const p3 = {
x: p2.x + side * CFG.l3 * Math.cos(bone3Rad),
y: p2.y + CFG.l3 * Math.sin(bone3Rad)
};
// 5. 中央拉索穿行路径:从滑块 -> 伞顶导轮 -> 铰链1背侧 -> 铰链2背侧 -> 尾骨固定锚点
// 在张紧状态下拉索紧贴铰链背侧槽,在卸载状态下拉索呈现自然柔性微弧
const cableSlack = (1 - t) * 12;
const cablePath = `M ${pRunnerJoint.x} ${pRunnerJoint.y}
Q ${CFG.cx + side * 16} ${(CFG.topY + runnerY) * 0.48}, ${CFG.cx + side * 14} ${CFG.topY - 4}
Q ${(p0.x + p1.x)*0.5 + side * cableSlack*0.5} ${(p0.y + p1.y)*0.5 - cableSlack*0.3}, ${p1.x} ${p1.y - 3}
Q ${(p1.x + p2.x)*0.5} ${(p1.y + p2.y)*0.5}, ${p2.x} ${p2.y - 2}
L ${p3.x} ${p3.y}`;
return {
p0, p1, p2, p3, pStrutAttach, pRunnerJoint, runnerY, fold1, fold2, cablePath
};
}
/**
* 将计算好的物理坐标渲染到 SVG 视图与 HUD
*/
function renderFrame(t) {
const right = calculateKinematics(t, 1);
const left = calculateKinematics(t, -1);
// 1. 更新滑块套筒位置
els.runner.setAttribute('transform', `translate(${CFG.cx}, ${right.runnerY})`);
// 2. 更新右侧骨架连杆
els.rStrut.setAttribute('x1', right.pRunnerJoint.x);
els.rStrut.setAttribute('y1', right.pRunnerJoint.y);
els.rStrut.setAttribute('x2', right.pStrutAttach.x);
els.rStrut.setAttribute('y2', right.pStrutAttach.y);
els.rBone1.setAttribute('x1', right.p0.x);
els.rBone1.setAttribute('y1', right.p0.y);
els.rBone1.setAttribute('x2', right.p1.x);
els.rBone1.setAttribute('y2', right.p1.y);
els.rBone2.setAttribute('x1', right.p1.x);
els.rBone2.setAttribute('y1', right.p1.y);
els.rBone2.setAttribute('x2', right.p2.x);
els.rBone2.setAttribute('y2', right.p2.y);
els.rBone3.setAttribute('x1', right.p2.x);
els.rBone3.setAttribute('y1', right.p2.y);
els.rBone3.setAttribute('x2', right.p3.x);
els.rBone3.setAttribute('y2', right.p3.y);
els.rJointH1.setAttribute('transform', `translate(${right.p1.x}, ${right.p1.y})`);
els.rJointH2.setAttribute('transform', `translate(${right.p2.x}, ${right.p2.y})`);
els.rTip.setAttribute('cx', right.p3.x);
els.rTip.setAttribute('cy', right.p3.y);
els.rCable.setAttribute('d', right.cablePath);
// 3. 更新左侧骨架连杆 (镜像)
els.lStrut.setAttribute('x1', left.pRunnerJoint.x);
els.lStrut.setAttribute('y1', left.pRunnerJoint.y);
els.lStrut.setAttribute('x2', left.pStrutAttach.x);
els.lStrut.setAttribute('y2', left.pStrutAttach.y);
els.lBone1.setAttribute('x1', left.p0.x);
els.lBone1.setAttribute('y1', left.p0.y);
els.lBone1.setAttribute('x2', left.p1.x);
els.lBone1.setAttribute('y2', left.p1.y);
els.lBone2.setAttribute('x1', left.p1.x);
els.lBone2.setAttribute('y1', left.p1.y);
els.lBone2.setAttribute('x2', left.p2.x);
els.lBone2.setAttribute('y2', left.p2.y);
els.lBone3.setAttribute('x1', left.p2.x);
els.lBone3.setAttribute('y1', left.p2.y);
els.lBone3.setAttribute('x2', left.p3.x);
els.lBone3.setAttribute('y2', left.p3.y);
els.lJointH1.setAttribute('transform', `translate(${left.p1.x}, ${left.p1.y})`);
els.lJointH2.setAttribute('transform', `translate(${left.p2.x}, ${left.p2.y})`);
els.lTip.setAttribute('cx', left.p3.x);
els.lTip.setAttribute('cy', left.p3.y);
els.lCable.setAttribute('d', left.cablePath);
// 4. 更新伞面布面曲线 (由点阵拟合真实织物张力形态)
const rightCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(right.p0.x + right.p1.x)*0.5} ${(right.p0.y + right.p1.y)*0.5 - 6*t}, ${right.p1.x} ${right.p1.y}
Q ${(right.p1.x + right.p2.x)*0.5} ${(right.p1.y + right.p2.y)*0.5 - 4*t}, ${right.p2.x} ${right.p2.y}
Q ${(right.p2.x + right.p3.x)*0.5} ${(right.p2.y + right.p3.y)*0.5 - 2*t}, ${right.p3.x} ${right.p3.y}
L ${right.p3.x - 4} ${right.p3.y + 4}
Z`;
els.canopyRight.setAttribute('d', rightCanopyD);
const leftCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(left.p0.x + left.p1.x)*0.5} ${(left.p0.y + left.p1.y)*0.5 - 6*t}, ${left.p1.x} ${left.p1.y}
Q ${(left.p1.x + left.p2.x)*0.5} ${(left.p1.y + left.p2.y)*0.5 - 4*t}, ${left.p2.x} ${left.p2.y}
Q ${(left.p2.x + left.p3.x)*0.5} ${(left.p2.y + left.p3.y)*0.5 - 2*t}, ${left.p3.x} ${left.p3.y}
L ${left.p3.x + 4} ${left.p3.y + 4}
Z`;
els.canopyLeft.setAttribute('d', leftCanopyD);
// 5. 动态更新包络线透明度 (收伞时显现规整柱状约束)
const envOpacity = Math.max(0, (0.3 - t) * 2.5);
els.envL.style.opacity = envOpacity;
els.envR.style.opacity = envOpacity;
// 6. 更新画中画 (PIP) 铰链特写旋转角
const pipAngle = right.fold1 * 0.9;
els.pipRotArm.setAttribute('transform', `rotate(${-pipAngle})`);
// 扭簧线圈形变
const coilScale = 1 + (1 - t) * 0.25;
els.pipSpringCoil.setAttribute('transform', `scale(${coilScale})`);
// 拉索在特写中的松弛/张紧
if (t > 0.7) {
els.pipHingeMode.textContent = '锁定受拉 300N';
els.pipHingeMode.style.color = '#38bdf8';
els.pipCableLine.setAttribute('stroke', '#38bdf8');
els.pipCableLine.setAttribute('stroke-width', '2.5');
} else {
els.pipHingeMode.textContent = '扭簧释能内折';
els.pipHingeMode.style.color = '#f59e0b';
els.pipCableLine.setAttribute('stroke', 'rgba(56,189,248,0.3)');
els.pipCableLine.setAttribute('stroke-width', '1.2');
}
// 7. 更新 HUD 遥测参数读数与状态徽标
const cableTension = (Math.max(0, (t - 0.2) / 0.8) * 300).toFixed(1);
const springTorque = ((1 - t) * 0.02).toFixed(3);
const alignQuality = (100 - Math.abs(t - 0.5) * 12).toFixed(0);
els.cableTensionVal.textContent = `${cableTension} N`;
els.springTorqueVal.textContent = `${(0.02 * (1 - t * 0.5)).toFixed(3)} N·m`;
if (t < 0.15) {
els.badge.textContent = '自动贴杆聚拢';
els.badge.style.color = '#10b981';
els.alignmentVal.textContent = '100 % (极佳)';
els.rCable.setAttribute('stroke-opacity', '0.25');
els.lCable.setAttribute('stroke-opacity', '0.25');
} else if (t < 0.8) {
els.badge.textContent = '扭簧顺序内折中';
els.badge.style.color = '#f59e0b';
els.alignmentVal.textContent = '自寻优对齐...';
els.rCable.setAttribute('stroke-opacity', '0.6');
els.lCable.setAttribute('stroke-opacity', '0.6');
} else {
els.badge.textContent = '刚性限位锁定';
els.badge.style.color = '#38bdf8';
els.alignmentVal.textContent = '刚性支撑';
els.rCable.setAttribute('stroke-opacity', '1');
els.lCable.setAttribute('stroke-opacity', '1');
}
// 更新滑块状态
els.slider.value = t;
els.sliderPct.textContent = `状态 (${Math.round(t * 100)}%)`;
}
/**
* 主动画循环 - 自动往复展示
*/
function tick(timestamp) {
if (!state.lastTimestamp) state.lastTimestamp = timestamp;
const dt = timestamp - state.lastTimestamp;
state.lastTimestamp = timestamp;
if (state.isPlaying) {
state.progress += state.direction * state.speed;
// 边界平滑停留换向
if (state.progress <= 0) {
state.progress = 0;
state.direction = 0; // 停顿片刻
setTimeout(() => {
if (state.isPlaying && state.progress <= 0.05) state.direction = 1;
}, 1100);
} else if (state.progress >= 1) {
state.progress = 1;
state.direction = 0;
setTimeout(() => {
if (state.isPlaying && state.progress >= 0.95) state.direction = -1;
}, 1400);
}
renderFrame(state.progress);
}
requestAnimationFrame(tick);
}
// 交互事件绑定
function initControls() {
// 拖动条实时手动探究
els.slider.addEventListener('input', (e) => {
state.isPlaying = false;
updatePlayBtnIcon();
state.progress = parseFloat(e.target.value);
renderFrame(state.progress);
});
// 播放/暂停控制
els.playBtn.addEventListener('click', () => {
state.isPlaying = !state.isPlaying;
if (state.isPlaying && state.direction === 0) {
state.direction = state.progress > 0.5 ? -1 : 1;
}
updatePlayBtnIcon();
});
// 键盘快捷键 (空格播放/暂停)
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
els.playBtn.click();
}
});
}
function updatePlayBtnIcon() {
if (state.isPlaying) {
els.playIcon.innerHTML = `
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
`;
} else {
els.playIcon.innerHTML = `
<polygon points="6,4 20,12 6,20"/>
`;
}
}
// 页面加载完成后自启动运行
window.addEventListener('DOMContentLoaded', () => {
initControls();
renderFrame(1.0);
// 触发自动开机动画
requestAnimationFrame(tick);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>预紧扭簧铰链与拉索自规整折伞机构 - 原理可视化</title>
<style>
:root {
--bg-dark: #090d16;
--panel-bg: rgba(15, 23, 42, 0.75);
--panel-border: rgba(56, 189, 248, 0.2);
--cyan-accent: #38bdf8;
--orange-accent: #f59e0b;
--green-accent: #10b981;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--font-mono: "SF Mono", "Consolas", "Roboto Mono", monospace;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: var(--font-sans);
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部微型信息条 - 紧凑精炼,不遮挡主体 */
.top-bar {
position: absolute;
top: 12px;
left: 16px;
right: 16px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 20;
pointer-events: none;
}
.brand-tag {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 20px;
backdrop-filter: blur(8px);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
animation: pulse-dot 2s infinite ease-in-out;
}
@keyframes pulse-dot {
0%, 100% { opacity: 0.5; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.2); }
}
.brand-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
color: #e2e8f0;
}
.brand-sub {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
margin-left: 4px;
}
/* 状态遥测浮动条 - 极小字号放置在右上角 */
.hud-metrics {
display: flex;
gap: 10px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 8px;
backdrop-filter: blur(8px);
}
.metric-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.metric-label {
font-size: 9px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 12px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--cyan-accent);
}
/* 中央全屏核心画布 */
.stage-container {
flex: 1;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: radial-gradient(circle at 50% 45%, #131d31 0%, #090d16 80%);
}
/* 背景工程栅格 */
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(56, 189, 248, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
pointer-events: none;
}
/* 主 SVG 动画视图 */
svg#mechanism-stage {
width: 100%;
height: 100%;
max-width: 1200px;
max-height: 820px;
z-index: 10;
}
/* 铰链微观特写画中画 (PIP) - 右下角紧凑浮窗 */
.detail-pip {
position: absolute;
right: 18px;
bottom: 84px;
width: 200px;
height: 160px;
background: rgba(13, 20, 36, 0.85);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: 10px;
padding: 8px;
backdrop-filter: blur(10px);
z-index: 20;
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
}
.pip-header {
display: space-between;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.pip-title {
font-size: 10px;
font-weight: 600;
color: var(--orange-accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pip-tag {
font-size: 9px;
font-family: var(--font-mono);
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
padding: 1px 4px;
border-radius: 3px;
}
.pip-svg {
flex: 1;
width: 100%;
height: 100%;
}
/* 底部核心精简控制栏 */
.bottom-control-bar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
width: min(680px, 92%);
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 28px;
padding: 8px 18px;
display: flex;
align-items: center;
gap: 14px;
backdrop-filter: blur(12px);
z-index: 30;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.ctrl-btn {
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.3);
color: var(--cyan-accent);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.ctrl-btn:hover {
background: var(--cyan-accent);
color: #04101e;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
}
.ctrl-slider-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.slider-container {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: rgba(255,255,255,0.15);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.state-badge {
font-size: 11px;
font-weight: 600;
color: #38bdf8;
min-width: 80px;
text-align: right;
font-family: var(--font-mono);
}
/* 图例指示器 - 左下角精炼放置 */
.legend-panel {
position: absolute;
left: 18px;
bottom: 84px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 8px 12px;
backdrop-filter: blur(8px);
z-index: 20;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: #cbd5e1;
}
.legend-color {
width: 12px;
height: 3px;
border-radius: 2px;
}
.legend-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
</style>
</head>
<body>
<!-- 顶部轻量 HUD -->
<header class="top-bar">
<div class="brand-tag">
<div class="status-dot"></div>
<div class="brand-title">自规整折伞机构 <span class="brand-sub">IFR 理想解动态原理</span></div>
</div>
<div class="hud-metrics">
<div class="metric-item">
<span class="metric-label">拉索工作张力</span>
<span class="metric-value" id="cable-tension-val">300.0 N</span>
</div>
<div class="metric-item">
<span class="metric-label">扭簧预紧储能</span>
<span class="metric-value" id="spring-torque-val">0.02 N·m</span>
</div>
<div class="metric-item">
<span class="metric-label">骨段贴合度</span>
<span class="metric-value" id="alignment-val">100 %</span>
</div>
</div>
</header>
<!-- 左下角图例说明 -->
<div class="legend-panel">
<div class="legend-item">
<div class="legend-color" style="background: #38bdf8; box-shadow: 0 0 6px #38bdf8;"></div>
<span>中央张紧拉索 (1.2mm 编织缆)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #f59e0b; box-shadow: 0 0 6px #f59e0b;"></div>
<span>内折预紧扭簧铰链 (限位165°)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #94a3b8;"></div>
<span>玻纤扁骨 (180mm + 160mm)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #10b981;"></div>
<span>尾段防划软质导向球头</span>
</div>
</div>
<!-- 右下角局部放大铰链特写 (画中画) -->
<div class="detail-pip">
<div class="pip-header">
<span class="pip-title">铰链内部扭簧剖面</span>
<span class="pip-tag" id="hinge-pip-mode">锁定受拉</span>
</div>
<svg class="pip-svg" viewBox="0 0 160 120" id="pip-hinge-svg">
<!-- 铰链放大图动态元素 -->
<g id="pip-dynamic-hinge" transform="translate(80, 60)">
<!-- 铰链座 -->
<circle cx="0" cy="0" r="28" fill="#1e293b" stroke="#475569" stroke-width="1.5"/>
<!-- 限位咬合台阶 -->
<path d="M -20 -10 L 0 -10 L 15 15" stroke="rgba(245,158,11,0.5)" stroke-dasharray="2 2" fill="none"/>
<!-- 骨1插芯 -->
<rect x="-42" y="-5" width="42" height="10" rx="3" fill="#334155" stroke="#64748b"/>
<!-- 骨2旋转臂 -->
<g id="pip-rot-arm">
<rect x="0" y="-5" width="42" height="10" rx="3" fill="#475569" stroke="#94a3b8"/>
<!-- 旋转轴心 -->
<circle cx="0" cy="0" r="4" fill="#f59e0b"/>
<!-- 扭簧螺旋线条 -->
<path id="pip-spring-coil" d="M -7 -7 A 10 10 0 1 1 7 7" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
</g>
<!-- 拉索穿行轨迹 -->
<path id="pip-cable-line" d="M -45 -12 L 0 -12 L 45 -12" fill="none" stroke="#38bdf8" stroke-width="2" stroke-dasharray="4 2"/>
<circle cx="0" cy="-12" r="3" fill="#38bdf8"/>
<!-- 扭矩力矢量箭头 -->
<path id="pip-torque-arrow" d="M -16 16 A 22 22 0 0 0 16 16" fill="none" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow)"/>
</g>
<defs>
<marker id="arrow" viewBox="0 0 6 6" refX="4" refY="3" markerWidth="4" markerHeight="4" orient="auto-start-reverse">
<polygon points="0 0, 6 3, 0 6" fill="#f59e0b"/>
</marker>
</defs>
</svg>
</div>
<!-- 中央主舞台 -->
<main class="stage-container">
<div class="grid-bg"></div>
<svg id="mechanism-stage" viewBox="0 0 1000 680" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 渐变定义 -->
<linearGradient id="canopyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#0369a1" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="boneGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#cbd5e1"/>
<stop offset="100%" stop-color="#64748b"/>
</linearGradient>
<linearGradient id="shaftGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#334155"/>
<stop offset="50%" stop-color="#94a3b8"/>
<stop offset="100%" stop-color="#1e293b"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-orange" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 机构几何层(由 JS 动态生成与驱动) -->
<g id="scene-root">
<!-- 伞面布面投影 -->
<path id="canopy-surface-left" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<path id="canopy-surface-right" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<!-- 收伞聚拢导向约束外轮廓(淡线提示规整圆柱包络) -->
<line id="envelope-left" x1="440" y1="120" x2="440" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<line id="envelope-right" x1="560" y1="120" x2="560" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<!-- 中棒主轴与伞巢 -->
<g id="shaft-assembly">
<!-- 伞顶导向轮与顶巢 -->
<circle cx="500" cy="115" r="14" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="500" cy="115" r="5" fill="#38bdf8" filter="url(#glow-cyan)"/>
<!-- 中棒主体钢管 -->
<rect x="494" y="115" width="12" height="470" rx="3" fill="url(#shaftGrad)"/>
<!-- 刻度线 -->
<line x1="497" y1="180" x2="497" y2="480" stroke="rgba(255,255,255,0.15)" stroke-dasharray="2 6"/>
<!-- 伞柄手把 -->
<rect x="490" y="585" width="20" height="50" rx="6" fill="#0f172a" stroke="#334155" stroke-width="2"/>
<circle cx="500" cy="625" r="4" fill="#38bdf8"/>
</g>
<!-- 滑块套筒 (Runner) -->
<g id="runner-assembly" transform="translate(500, 260)">
<rect x="-14" y="-12" width="28" height="24" rx="4" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="0" cy="0" r="3" fill="#38bdf8"/>
<!-- 滑块侧边拉索锁固点 -->
<circle cx="-9" cy="0" r="2" fill="#f59e0b"/>
<circle cx="9" cy="0" r="2" fill="#f59e0b"/>
</g>
<!-- 左侧完整三节骨架机构组 -->
<g id="left-arm">
<line id="l-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<path id="l-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<line id="l-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<line id="l-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<line id="l-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链节点 -->
<g id="l-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<g id="l-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<!-- 末端软质防刮球头 -->
<circle id="l-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 右侧完整三节骨架机构组 (详细高亮对称展示) -->
<g id="right-arm">
<!-- 连杆支撑杆 -->
<line id="r-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<!-- 中央张紧编织拉索 -->
<path id="r-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<!-- 主骨第1段 (180mm) -->
<line id="r-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<!-- 主骨第2段 (160mm) -->
<line id="r-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<!-- 尾骨段 -->
<line id="r-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链1 (扭簧预紧节点 A) -->
<g id="r-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon1" d="M -6 -6 A 8 8 0 1 1 6 6" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 铰链2 (扭簧预紧节点 B) -->
<g id="r-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon2" d="M -5 -5 A 7 7 0 1 1 5 5" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 软质导向球头 -->
<circle id="r-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 动态应力矢量与折叠流动导向波纹 -->
<g id="dynamics-layer" pointer-events="none"></g>
</g>
</svg>
</main>
<!-- 底部微交互控制栏 -->
<footer class="bottom-control-bar">
<button class="ctrl-btn" id="play-btn" title="播放 / 暂停 (空格)">
<svg id="play-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
</svg>
</button>
<div class="ctrl-slider-group">
<div class="slider-labels">
<span>收拢贴合 (拉索松弛·扭簧驱动)</span>
<span id="slider-pct">开伞状态 (100%)</span>
<span>完全撑开 (拉索锁定·刚性限位)</span>
</div>
<div class="slider-container">
<input type="range" id="progress-slider" min="0" max="1" step="0.001" value="1"/>
</div>
</div>
<div class="state-badge" id="mech-state-badge">刚性锁定</div>
</footer>
<script>
/**
* 连杆与扭簧拉索动力学模拟核心
* 实现开伞刚性锁定与收伞自动顺序贴合的 IFR 机制
*/
// 几何拓扑常量配置 (单位: SVG 坐标像素,精确对齐物理比例)
const CFG = {
cx: 500, // 伞中棒 X 坐标
topY: 120, // 伞顶固定铰接座 Y
handleY: 585, // 伞柄位置
// 骨节长度
l1: 175, // 主骨第1段 (180mm 标定)
l2: 155, // 主骨第2段 (160mm 标定)
l3: 110, // 尾骨段
lStrut: 130, // 支撑撑杆长度
strutAttachRatio: 0.38, // 撑杆在主骨1上的铰接比例
// 滑块行程
runnerY_open: 245, // 开伞时滑块位置 (高位,拉紧)
runnerY_close: 520 // 收伞时滑块位置 (下移,卸载)
};
// 动画状态机
let state = {
progress: 1.0, // 0 = 彻底收拢贴合, 1 = 彻底撑开锁定
direction: -1, // 默认加载后自动开始收伞演示
isPlaying: true,
speed: 0.0042, // 平滑播放步进速度
lastTimestamp: 0
};
// DOM 元素引用
const els = {
slider: document.getElementById('progress-slider'),
playBtn: document.getElementById('play-btn'),
playIcon: document.getElementById('play-icon'),
badge: document.getElementById('mech-state-badge'),
sliderPct: document.getElementById('slider-pct'),
cableTensionVal: document.getElementById('cable-tension-val'),
springTorqueVal: document.getElementById('spring-torque-val'),
alignmentVal: document.getElementById('alignment-val'),
// PIP 特写
pipHingeMode: document.getElementById('hinge-pip-mode'),
pipRotArm: document.getElementById('pip-rot-arm'),
pipSpringCoil: document.getElementById('pip-spring-coil'),
pipCableLine: document.getElementById('pip-cable-line'),
// 机构图形元素
runner: document.getElementById('runner-assembly'),
rStrut: document.getElementById('r-strut'),
rCable: document.getElementById('r-cable'),
rBone1: document.getElementById('r-bone1'),
rBone2: document.getElementById('r-bone2'),
rBone3: document.getElementById('r-bone3'),
rJointH1: document.getElementById('r-joint-h1'),
rJointH2: document.getElementById('r-joint-h2'),
rTip: document.getElementById('r-tip'),
rTorqueIcon1: document.getElementById('r-torque-icon1'),
rTorqueIcon2: document.getElementById('r-torque-icon2'),
// 左侧镜像
lStrut: document.getElementById('l-strut'),
lCable: document.getElementById('l-cable'),
lBone1: document.getElementById('l-bone1'),
lBone2: document.getElementById('l-bone2'),
lBone3: document.getElementById('l-bone3'),
lJointH1: document.getElementById('l-joint-h1'),
lJointH2: document.getElementById('l-joint-h2'),
lTip: document.getElementById('l-tip'),
// 伞面与轮廓
canopyLeft: document.getElementById('canopy-surface-left'),
canopyRight: document.getElementById('canopy-surface-right'),
envL: document.getElementById('envelope-left'),
envR: document.getElementById('envelope-right'),
};
/**
* 根据当前开合进度 t 计算全套骨节与拉索的空间几何姿态
* @param {number} t [0 = 收伞, 1 = 开伞]
* @param {number} side 1 = 右侧, -1 = 左侧
*/
function calculateKinematics(t, side = 1) {
// 1. 滑块位置插值
const runnerY = CFG.runnerY_close + (CFG.runnerY_open - CFG.runnerY_close) * t;
// 2. 主骨1仰角:收伞贴近中棒(约86度向下),开伞展开到约-18度(上扬形成伞穹)
const bone1AngleDeg = 86 - t * 104; // 86° -> -18°
const bone1Rad = (bone1AngleDeg * Math.PI) / 180;
// 主骨1基点 (伞巢固定点)
const p0 = { x: CFG.cx + side * 8, y: CFG.topY };
// 主骨1末端 (铰链1位置)
const p1 = {
x: p0.x + side * CFG.l1 * Math.cos(bone1Rad),
y: p0.y + CFG.l1 * Math.sin(bone1Rad)
};
// 支撑连杆铰接在主骨1上的点
const pStrutAttach = {
x: p0.x + (p1.x - p0.x) * CFG.strutAttachRatio,
y: p0.y + (p1.y - p0.y) * CFG.strutAttachRatio
};
// 滑块支撑座铰接点
const pRunnerJoint = {
x: CFG.cx + side * 10,
y: runnerY
};
// 3. 铰链1 (主骨2) 折角计算:
// 当 t < 0.85 时,扭簧释放,骨段内折
let fold1 = 0;
if (t < 0.85) {
const foldProgress1 = (0.85 - t) / 0.85; // 0 -> 1
fold1 = Math.pow(foldProgress1, 1.2) * 165; // 最大内折 165 度
}
// 几何角始终保持内折
const bone2Rad = bone1Rad - (fold1 * Math.PI / 180) * 0.96;
const p2 = {
x: p1.x + side * CFG.l2 * Math.cos(bone2Rad),
y: p1.y + CFG.l2 * Math.sin(bone2Rad)
};
// 4. 铰链2 (尾骨段) 折角计算:尾骨内折贴入
let fold2 = 0;
if (t < 0.95) {
const foldProgress2 = Math.min(1, (0.95 - t) / 0.7);
fold2 = Math.pow(foldProgress2, 0.9) * 155; // 尾骨反折贴入
}
const bone3Rad = bone2Rad + (fold2 * Math.PI / 180) * 0.88;
const p3 = {
x: p2.x + side * CFG.l3 * Math.cos(bone3Rad),
y: p2.y + CFG.l3 * Math.sin(bone3Rad)
};
// 5. 中央拉索穿行路径:从滑块 -> 伞顶导轮 -> 铰链1背侧 -> 铰链2背侧 -> 尾骨固定锚点
const cableSlack = (1 - t) * 12;
const cablePath = `M ${pRunnerJoint.x} ${pRunnerJoint.y}
Q ${CFG.cx + side * 16} ${(CFG.topY + runnerY) * 0.48}, ${CFG.cx + side * 14} ${CFG.topY - 4}
Q ${(p0.x + p1.x)*0.5 + side * cableSlack*0.5} ${(p0.y + p1.y)*0.5 - cableSlack*0.3}, ${p1.x} ${p1.y - 3}
Q ${(p1.x + p2.x)*0.5} ${(p1.y + p2.y)*0.5}, ${p2.x} ${p2.y - 2}
L ${p3.x} ${p3.y}`;
return {
p0, p1, p2, p3, pStrutAttach, pRunnerJoint, runnerY, fold1, fold2, cablePath
};
}
/**
* 将计算好的物理坐标渲染到 SVG 视图与 HUD
*/
function renderFrame(t) {
const right = calculateKinematics(t, 1);
const left = calculateKinematics(t, -1);
// 1. 更新滑块套筒位置
els.runner.setAttribute('transform', `translate(${CFG.cx}, ${right.runnerY})`);
// 2. 更新右侧骨架连杆
els.rStrut.setAttribute('x1', right.pRunnerJoint.x);
els.rStrut.setAttribute('y1', right.pRunnerJoint.y);
els.rStrut.setAttribute('x2', right.pStrutAttach.x);
els.rStrut.setAttribute('y2', right.pStrutAttach.y);
els.rBone1.setAttribute('x1', right.p0.x);
els.rBone1.setAttribute('y1', right.p0.y);
els.rBone1.setAttribute('x2', right.p1.x);
els.rBone1.setAttribute('y2', right.p1.y);
els.rBone2.setAttribute('x1', right.p1.x);
els.rBone2.setAttribute('y1', right.p1.y);
els.rBone2.setAttribute('x2', right.p2.x);
els.rBone2.setAttribute('y2', right.p2.y);
els.rBone3.setAttribute('x1', right.p2.x);
els.rBone3.setAttribute('y1', right.p2.y);
els.rBone3.setAttribute('x2', right.p3.x);
els.rBone3.setAttribute('y2', right.p3.y);
els.rJointH1.setAttribute('transform', `translate(${right.p1.x}, ${right.p1.y})`);
els.rJointH2.setAttribute('transform', `translate(${right.p2.x}, ${right.p2.y})`);
els.rTip.setAttribute('cx', right.p3.x);
els.rTip.setAttribute('cy', right.p3.y);
els.rCable.setAttribute('d', right.cablePath);
// 3. 更新左侧骨架连杆 (与右侧精确镜像对称)
els.lStrut.setAttribute('x1', left.pRunnerJoint.x);
els.lStrut.setAttribute('y1', left.pRunnerJoint.y);
els.lStrut.setAttribute('x2', left.pStrutAttach.x);
els.lStrut.setAttribute('y2', left.pStrutAttach.y);
els.lBone1.setAttribute('x1', left.p0.x);
els.lBone1.setAttribute('y1', left.p0.y);
els.lBone1.setAttribute('x2', left.p1.x);
els.lBone1.setAttribute('y2', left.p1.y);
els.lBone2.setAttribute('x1', left.p1.x);
els.lBone2.setAttribute('y1', left.p1.y);
els.lBone2.setAttribute('x2', left.p2.x);
els.lBone2.setAttribute('y2', left.p2.y);
els.lBone3.setAttribute('x1', left.p2.x);
els.lBone3.setAttribute('y1', left.p2.y);
els.lBone3.setAttribute('x2', left.p3.x);
els.lBone3.setAttribute('y2', left.p3.y);
els.lJointH1.setAttribute('transform', `translate(${left.p1.x}, ${left.p1.y})`);
els.lJointH2.setAttribute('transform', `translate(${left.p2.x}, ${left.p2.y})`);
els.lTip.setAttribute('cx', left.p3.x);
els.lTip.setAttribute('cy', left.p3.y);
els.lCable.setAttribute('d', left.cablePath);
// 4. 更新伞面布面曲线
const rightCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(right.p0.x + right.p1.x)*0.5} ${(right.p0.y + right.p1.y)*0.5 - 6*t}, ${right.p1.x} ${right.p1.y}
Q ${(right.p1.x + right.p2.x)*0.5} ${(right.p1.y + right.p2.y)*0.5 - 4*t}, ${right.p2.x} ${right.p2.y}
Q ${(right.p2.x + right.p3.x)*0.5} ${(right.p2.y + right.p3.y)*0.5 - 2*t}, ${right.p3.x} ${right.p3.y}
L ${right.p3.x - 4} ${right.p3.y + 4}
Z`;
els.canopyRight.setAttribute('d', rightCanopyD);
const leftCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(left.p0.x + left.p1.x)*0.5} ${(left.p0.y + left.p1.y)*0.5 - 6*t}, ${left.p1.x} ${left.p1.y}
Q ${(left.p1.x + left.p2.x)*0.5} ${(left.p1.y + left.p2.y)*0.5 - 4*t}, ${left.p2.x} ${left.p2.y}
Q ${(left.p2.x + left.p3.x)*0.5} ${(left.p2.y + left.p3.y)*0.5 - 2*t}, ${left.p3.x} ${left.p3.y}
L ${left.p3.x + 4} ${left.p3.y + 4}
Z`;
els.canopyLeft.setAttribute('d', leftCanopyD);
// 5. 动态更新包络线透明度 (收伞时显现规整柱状约束)
const envOpacity = Math.max(0, (0.3 - t) * 2.5);
els.envL.style.opacity = envOpacity;
els.envR.style.opacity = envOpacity;
// 6. 更新画中画 (PIP) 铰链特写旋转角
const pipAngle = right.fold1 * 0.9;
els.pipRotArm.setAttribute('transform', `rotate(${-pipAngle})`);
// 扭簧线圈形变
const coilScale = 1 + (1 - t) * 0.25;
els.pipSpringCoil.setAttribute('transform', `scale(${coilScale})`);
// 拉索在特写中的松弛/张紧
if (t > 0.7) {
els.pipHingeMode.textContent = '锁定受拉 300N';
els.pipHingeMode.style.color = '#38bdf8';
els.pipCableLine.setAttribute('stroke', '#38bdf8');
els.pipCableLine.setAttribute('stroke-width', '2.5');
} else {
els.pipHingeMode.textContent = '扭簧释能内折';
els.pipHingeMode.style.color = '#f59e0b';
els.pipCableLine.setAttribute('stroke', 'rgba(56,189,248,0.3)');
els.pipCableLine.setAttribute('stroke-width', '1.2');
}
// 7. 更新 HUD 遥测参数读数与状态徽标
const cableTension = (Math.max(0, (t - 0.2) / 0.8) * 300).toFixed(1);
const springTorque = ((1 - t) * 0.02).toFixed(3);
const alignQuality = (100 - Math.abs(t - 0.5) * 12).toFixed(0);
els.cableTensionVal.textContent = `${cableTension} N`;
els.springTorqueVal.textContent = `${(0.02 * (1 - t * 0.5)).toFixed(3)} N·m`;
if (t < 0.15) {
els.badge.textContent = '自动贴杆聚拢';
els.badge.style.color = '#10b981';
els.alignmentVal.textContent = '100 % (极佳)';
els.rCable.setAttribute('stroke-opacity', '0.25');
els.lCable.setAttribute('stroke-opacity', '0.25');
} else if (t < 0.8) {
els.badge.textContent = '扭簧顺序内折中';
els.badge.style.color = '#f59e0b';
els.alignmentVal.textContent = '自寻优对齐...';
els.rCable.setAttribute('stroke-opacity', '0.6');
els.lCable.setAttribute('stroke-opacity', '0.6');
} else {
els.badge.textContent = '刚性限位锁定';
els.badge.style.color = '#38bdf8';
els.alignmentVal.textContent = '刚性支撑';
els.rCable.setAttribute('stroke-opacity', '1');
els.lCable.setAttribute('stroke-opacity', '1');
}
// 更新滑块状态
els.slider.value = t;
els.sliderPct.textContent = `状态 (${Math.round(t * 100)}%)`;
}
/**
* 主动画循环 - 自动往复展示
*/
function tick(timestamp) {
if (!state.lastTimestamp) state.lastTimestamp = timestamp;
const dt = timestamp - state.lastTimestamp;
state.lastTimestamp = timestamp;
if (state.isPlaying) {
state.progress += state.direction * state.speed;
// 边界平滑停留换向
if (state.progress <= 0) {
state.progress = 0;
state.direction = 0; // 停顿片刻
setTimeout(() => {
if (state.isPlaying && state.progress <= 0.05) state.direction = 1;
}, 1100);
} else if (state.progress >= 1) {
state.progress = 1;
state.direction = 0;
setTimeout(() => {
if (state.isPlaying && state.progress >= 0.95) state.direction = -1;
}, 1400);
}
renderFrame(state.progress);
}
requestAnimationFrame(tick);
}
// 交互事件绑定
function initControls() {
// 拖动条实时手动探究
els.slider.addEventListener('input', (e) => {
state.isPlaying = false;
updatePlayBtnIcon();
state.progress = parseFloat(e.target.value);
renderFrame(state.progress);
});
// 播放/暂停控制
els.playBtn.addEventListener('click', () => {
state.isPlaying = !state.isPlaying;
if (state.isPlaying && state.direction === 0) {
state.direction = state.progress > 0.5 ? -1 : 1;
}
updatePlayBtnIcon();
});
// 键盘快捷键 (空格播放/暂停)
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
els.playBtn.click();
}
});
}
function updatePlayBtnIcon() {
if (state.isPlaying) {
els.playIcon.innerHTML = `
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
`;
} else {
els.playIcon.innerHTML = `
<polygon points="6,4 20,12 6,20"/>
`;
}
}
// 页面加载完成后自启动运行
window.addEventListener('DOMContentLoaded', () => {
initControls();
renderFrame(1.0);
// 触发自动开机动画
requestAnimationFrame(tick);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>预紧扭簧铰链与拉索自规整折伞机构 - 原理可视化</title>
<style>
:root {
--bg-dark: #090d16;
--panel-bg: rgba(15, 23, 42, 0.75);
--panel-border: rgba(56, 189, 248, 0.2);
--cyan-accent: #38bdf8;
--orange-accent: #f59e0b;
--green-accent: #10b981;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--font-mono: "SF Mono", "Consolas", "Roboto Mono", monospace;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: var(--font-sans);
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部微型信息条 - 紧凑精炼,不遮挡主体 */
.top-bar {
position: absolute;
top: 12px;
left: 16px;
right: 16px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 20;
pointer-events: none;
}
.brand-tag {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 20px;
backdrop-filter: blur(8px);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
animation: pulse-dot 2s infinite ease-in-out;
}
@keyframes pulse-dot {
0%, 100% { opacity: 0.5; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.2); }
}
.brand-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
color: #e2e8f0;
}
.brand-sub {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
margin-left: 4px;
}
/* 状态遥测浮动条 - 极小字号放置在右上角 */
.hud-metrics {
display: flex;
gap: 10px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
padding: 5px 12px;
border-radius: 8px;
backdrop-filter: blur(8px);
}
.metric-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.metric-label {
font-size: 9px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 12px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--cyan-accent);
}
/* 中央全屏核心画布 */
.stage-container {
flex: 1;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: radial-gradient(circle at 50% 45%, #131d31 0%, #090d16 80%);
}
/* 背景工程栅格 */
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(56, 189, 248, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
pointer-events: none;
}
/* 主 SVG 动画视图 */
svg#mechanism-stage {
width: 100%;
height: 100%;
max-width: 1200px;
max-height: 820px;
z-index: 10;
}
/* 铰链微观特写画中画 (PIP) - 右下角紧凑浮窗 */
.detail-pip {
position: absolute;
right: 18px;
bottom: 84px;
width: 200px;
height: 160px;
background: rgba(13, 20, 36, 0.85);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: 10px;
padding: 8px;
backdrop-filter: blur(10px);
z-index: 20;
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
}
.pip-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.pip-title {
font-size: 10px;
font-weight: 600;
color: var(--orange-accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pip-tag {
font-size: 9px;
font-family: var(--font-mono);
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
padding: 1px 4px;
border-radius: 3px;
}
.pip-svg {
flex: 1;
width: 100%;
height: 100%;
}
/* 底部核心精简控制栏 */
.bottom-control-bar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
width: min(680px, 92%);
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 28px;
padding: 8px 18px;
display: flex;
align-items: center;
gap: 14px;
backdrop-filter: blur(12px);
z-index: 30;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.ctrl-btn {
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.3);
color: var(--cyan-accent);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.ctrl-btn:hover {
background: var(--cyan-accent);
color: #04101e;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
}
.ctrl-slider-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.slider-container {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: rgba(255,255,255,0.15);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--cyan-accent);
box-shadow: 0 0 8px var(--cyan-accent);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.state-badge {
font-size: 11px;
font-weight: 600;
color: #38bdf8;
min-width: 80px;
text-align: right;
font-family: var(--font-mono);
}
/* 图例指示器 - 左下角精炼放置 */
.legend-panel {
position: absolute;
left: 18px;
bottom: 84px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 8px 12px;
backdrop-filter: blur(8px);
z-index: 20;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: #cbd5e1;
}
.legend-color {
width: 12px;
height: 3px;
border-radius: 2px;
}
.legend-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
</style>
</head>
<body>
<!-- 顶部轻量 HUD -->
<header class="top-bar">
<div class="brand-tag">
<div class="status-dot"></div>
<div class="brand-title">自规整折伞机构 <span class="brand-sub">IFR 理想解动态原理</span></div>
</div>
<div class="hud-metrics">
<div class="metric-item">
<span class="metric-label">拉索工作张力</span>
<span class="metric-value" id="cable-tension-val">300.0 N</span>
</div>
<div class="metric-item">
<span class="metric-label">扭簧预紧储能</span>
<span class="metric-value" id="spring-torque-val">0.02 N·m</span>
</div>
<div class="metric-item">
<span class="metric-label">骨段贴合度</span>
<span class="metric-value" id="alignment-val">100 %</span>
</div>
</div>
</header>
<!-- 左下角图例说明 -->
<div class="legend-panel">
<div class="legend-item">
<div class="legend-color" style="background: #38bdf8; box-shadow: 0 0 6px #38bdf8;"></div>
<span>中央张紧拉索 (1.2mm 编织缆)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #f59e0b; box-shadow: 0 0 6px #f59e0b;"></div>
<span>内折预紧扭簧铰链 (限位165°)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #94a3b8;"></div>
<span>玻纤扁骨 (180mm + 160mm)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #10b981;"></div>
<span>尾段防划软质导向球头</span>
</div>
</div>
<!-- 右下角局部放大铰链特写 (画中画) -->
<div class="detail-pip">
<div class="pip-header">
<span class="pip-title">铰链内部扭簧剖面</span>
<span class="pip-tag" id="hinge-pip-mode">锁定受拉</span>
</div>
<svg class="pip-svg" viewBox="0 0 160 120" id="pip-hinge-svg">
<!-- 铰链放大图动态元素 -->
<g id="pip-dynamic-hinge" transform="translate(80, 60)">
<!-- 铰链座 -->
<circle cx="0" cy="0" r="28" fill="#1e293b" stroke="#475569" stroke-width="1.5"/>
<!-- 限位咬合台阶 -->
<path d="M -20 -10 L 0 -10 L 15 15" stroke="rgba(245,158,11,0.5)" stroke-dasharray="2 2" fill="none"/>
<!-- 骨1插芯 -->
<rect x="-42" y="-5" width="42" height="10" rx="3" fill="#334155" stroke="#64748b"/>
<!-- 骨2旋转臂 -->
<g id="pip-rot-arm">
<rect x="0" y="-5" width="42" height="10" rx="3" fill="#475569" stroke="#94a3b8"/>
<!-- 旋转轴心 -->
<circle cx="0" cy="0" r="4" fill="#f59e0b"/>
<!-- 扭簧螺旋线条 -->
<path id="pip-spring-coil" d="M -7 -7 A 10 10 0 1 1 7 7" fill="none" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/>
</g>
<!-- 拉索穿行轨迹 -->
<path id="pip-cable-line" d="M -45 -12 L 0 -12 L 45 -12" fill="none" stroke="#38bdf8" stroke-width="2" stroke-dasharray="4 2"/>
<circle cx="0" cy="-12" r="3" fill="#38bdf8"/>
<!-- 扭矩力矢量箭头 -->
<path id="pip-torque-arrow" d="M -16 16 A 22 22 0 0 0 16 16" fill="none" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow)"/>
</g>
<defs>
<marker id="arrow" viewBox="0 0 6 6" refX="4" refY="3" markerWidth="4" markerHeight="4" orient="auto-start-reverse">
<polygon points="0 0, 6 3, 0 6" fill="#f59e0b"/>
</marker>
</defs>
</svg>
</div>
<!-- 中央主舞台 -->
<main class="stage-container">
<div class="grid-bg"></div>
<svg id="mechanism-stage" viewBox="0 0 1000 680" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 渐变定义 -->
<linearGradient id="canopyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#0369a1" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="boneGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#cbd5e1"/>
<stop offset="100%" stop-color="#64748b"/>
</linearGradient>
<linearGradient id="shaftGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#334155"/>
<stop offset="50%" stop-color="#94a3b8"/>
<stop offset="100%" stop-color="#1e293b"/>
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-orange" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 机构几何层(由 JS 动态生成与驱动) -->
<g id="scene-root">
<!-- 伞面布面投影 -->
<path id="canopy-surface-left" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<path id="canopy-surface-right" fill="url(#canopyGrad)" stroke="rgba(56, 189, 248, 0.4)" stroke-width="1.2"/>
<!-- 收伞聚拢导向约束外轮廓(淡线提示规整圆柱包络) -->
<line id="envelope-left" x1="440" y1="120" x2="440" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<line id="envelope-right" x1="560" y1="120" x2="560" y2="580" stroke="rgba(56,189,248,0.15)" stroke-dasharray="3 3" opacity="0"/>
<!-- 中棒主轴与伞巢 -->
<g id="shaft-assembly">
<!-- 伞顶导向轮与顶巢 -->
<circle cx="500" cy="115" r="14" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="500" cy="115" r="5" fill="#38bdf8" filter="url(#glow-cyan)"/>
<!-- 中棒主体钢管 -->
<rect x="494" y="115" width="12" height="470" rx="3" fill="url(#shaftGrad)"/>
<!-- 刻度线 -->
<line x1="497" y1="180" x2="497" y2="480" stroke="rgba(255,255,255,0.15)" stroke-dasharray="2 6"/>
<!-- 伞柄手把 -->
<rect x="490" y="585" width="20" height="50" rx="6" fill="#0f172a" stroke="#334155" stroke-width="2"/>
<circle cx="500" cy="625" r="4" fill="#38bdf8"/>
</g>
<!-- 滑块套筒 (Runner) -->
<g id="runner-assembly" transform="translate(500, 260)">
<rect x="-14" y="-12" width="28" height="24" rx="4" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>
<circle cx="0" cy="0" r="3" fill="#38bdf8"/>
<!-- 滑块侧边拉索锁固点 -->
<circle cx="-9" cy="0" r="2" fill="#f59e0b"/>
<circle cx="9" cy="0" r="2" fill="#f59e0b"/>
</g>
<!-- 左侧完整三节骨架机构组 -->
<g id="left-arm">
<line id="l-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<path id="l-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<line id="l-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<line id="l-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<line id="l-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链节点 -->
<g id="l-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<g id="l-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
</g>
<!-- 末端软质防刮球头 -->
<circle id="l-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 右侧完整三节骨架机构组 (详细高亮对称展示) -->
<g id="right-arm">
<!-- 连杆支撑杆 -->
<line id="r-strut" stroke="#64748b" stroke-width="3" stroke-linecap="round"/>
<!-- 中央张紧编织拉索 -->
<path id="r-cable" fill="none" stroke="#38bdf8" stroke-width="2.2" filter="url(#glow-cyan)"/>
<!-- 主骨第1段 (180mm) -->
<line id="r-bone1" stroke="url(#boneGrad)" stroke-width="4.5" stroke-linecap="round"/>
<!-- 主骨第2段 (160mm) -->
<line id="r-bone2" stroke="url(#boneGrad)" stroke-width="3.5" stroke-linecap="round"/>
<!-- 尾骨段 -->
<line id="r-bone3" stroke="url(#boneGrad)" stroke-width="2.5" stroke-linecap="round"/>
<!-- 铰链1 (扭簧预紧节点 A) -->
<g id="r-joint-h1">
<circle cx="0" cy="0" r="5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="2" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon1" d="M -6 -6 A 8 8 0 1 1 6 6" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 铰链2 (扭簧预紧节点 B) -->
<g id="r-joint-h2">
<circle cx="0" cy="0" r="4.5" fill="#0f172a" stroke="#f59e0b" stroke-width="2"/>
<circle cx="0" cy="0" r="1.8" fill="#f59e0b" filter="url(#glow-orange)"/>
<path id="r-torque-icon2" d="M -5 -5 A 7 7 0 1 1 5 5" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="2 1"/>
</g>
<!-- 软质导向球头 -->
<circle id="r-tip" cx="0" cy="0" r="4.5" fill="#10b981" stroke="#064e3b" stroke-width="1.2"/>
</g>
<!-- 动态应力矢量与折叠流动导向波纹 -->
<g id="dynamics-layer" pointer-events="none"></g>
</g>
</svg>
</main>
<!-- 底部微交互控制栏 -->
<footer class="bottom-control-bar">
<button class="ctrl-btn" id="play-btn" title="播放 / 暂停 (空格)">
<svg id="play-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
</svg>
</button>
<div class="ctrl-slider-group">
<div class="slider-labels">
<span>收拢贴合 (拉索松弛·扭簧驱动)</span>
<span id="slider-pct">开伞状态 (100%)</span>
<span>完全撑开 (拉索锁定·刚性限位)</span>
</div>
<div class="slider-container">
<input type="range" id="progress-slider" min="0" max="1" step="0.001" value="1"/>
</div>
</div>
<div class="state-badge" id="mech-state-badge">刚性锁定</div>
</footer>
<script>
/**
* 连杆与扭簧拉索动力学模拟核心
* 实现开伞刚性锁定与收伞自动顺序贴合的 IFR 机制
*/
// 几何拓扑常量配置 (单位: SVG 坐标像素,精确对齐物理比例)
const CFG = {
cx: 500, // 伞中棒 X 坐标
topY: 120, // 伞顶固定铰接座 Y
handleY: 585, // 伞柄位置
// 骨节长度
l1: 175, // 主骨第1段 (180mm 标定)
l2: 155, // 主骨第2段 (160mm 标定)
l3: 110, // 尾骨段
lStrut: 130, // 支撑撑杆长度
strutAttachRatio: 0.38, // 撑杆在主骨1上的铰接比例
// 滑块行程
runnerY_open: 285, // 开伞时滑块位置 (撑开至倒V伞穹顶形态)
runnerY_close: 520 // 收伞时滑块位置 (下移,卸载)
};
// 动画状态机
let state = {
progress: 1.0, // 0 = 彻底收拢贴合, 1 = 彻底撑开锁定
direction: -1, // 默认加载后自动开始收伞演示
isPlaying: true,
speed: 0.0042, // 平滑播放步进速度
lastTimestamp: 0
};
// DOM 元素引用
const els = {
slider: document.getElementById('progress-slider'),
playBtn: document.getElementById('play-btn'),
playIcon: document.getElementById('play-icon'),
badge: document.getElementById('mech-state-badge'),
sliderPct: document.getElementById('slider-pct'),
cableTensionVal: document.getElementById('cable-tension-val'),
springTorqueVal: document.getElementById('spring-torque-val'),
alignmentVal: document.getElementById('alignment-val'),
// PIP 特写
pipHingeMode: document.getElementById('hinge-pip-mode'),
pipRotArm: document.getElementById('pip-rot-arm'),
pipSpringCoil: document.getElementById('pip-spring-coil'),
pipCableLine: document.getElementById('pip-cable-line'),
// 机构图形元素
runner: document.getElementById('runner-assembly'),
rStrut: document.getElementById('r-strut'),
rCable: document.getElementById('r-cable'),
rBone1: document.getElementById('r-bone1'),
rBone2: document.getElementById('r-bone2'),
rBone3: document.getElementById('r-bone3'),
rJointH1: document.getElementById('r-joint-h1'),
rJointH2: document.getElementById('r-joint-h2'),
rTip: document.getElementById('r-tip'),
rTorqueIcon1: document.getElementById('r-torque-icon1'),
rTorqueIcon2: document.getElementById('r-torque-icon2'),
// 左侧镜像
lStrut: document.getElementById('l-strut'),
lCable: document.getElementById('l-cable'),
lBone1: document.getElementById('l-bone1'),
lBone2: document.getElementById('l-bone2'),
lBone3: document.getElementById('l-bone3'),
lJointH1: document.getElementById('l-joint-h1'),
lJointH2: document.getElementById('l-joint-h2'),
lTip: document.getElementById('l-tip'),
// 伞面与轮廓
canopyLeft: document.getElementById('canopy-surface-left'),
canopyRight: document.getElementById('canopy-surface-right'),
envL: document.getElementById('envelope-left'),
envR: document.getElementById('envelope-right'),
};
/**
* 根据当前开合进度 t 计算全套骨节与拉索的空间几何姿态
* @param {number} t [0 = 收伞, 1 = 开伞]
* @param {number} side 1 = 右侧, -1 = 左侧
*/
function calculateKinematics(t, side = 1) {
// 1. 滑块位置插值
const runnerY = CFG.runnerY_close + (CFG.runnerY_open - CFG.runnerY_close) * t;
// 2. 主骨1仰角:收伞贴近中棒(约86度向下),开伞展开到约18度(呈倒V伞顶向下倾斜自然支撑形态,杜绝向上仰翻)
const bone1AngleDeg = 86 - t * 68; // 86° -> 18° (保持倾斜向下)
const bone1Rad = (bone1AngleDeg * Math.PI) / 180;
// 主骨1基点 (伞巢固定点)
const p0 = { x: CFG.cx + side * 8, y: CFG.topY };
// 主骨1末端 (铰链1位置)
const p1 = {
x: p0.x + side * CFG.l1 * Math.cos(bone1Rad),
y: p0.y + CFG.l1 * Math.sin(bone1Rad)
};
// 支撑连杆铰接在主骨1上的点
const pStrutAttach = {
x: p0.x + (p1.x - p0.x) * CFG.strutAttachRatio,
y: p0.y + (p1.y - p0.y) * CFG.strutAttachRatio
};
// 滑块支撑座铰接点
const pRunnerJoint = {
x: CFG.cx + side * 10,
y: runnerY
};
// 3. 铰链1 (主骨2) 折角计算:
// 当 t < 0.85 时,扭簧释放,骨段内折;开伞 t=1 时与主骨1微弧衔接(约22°保持整体倒V形态)
let fold1 = 0;
if (t < 0.85) {
const foldProgress1 = (0.85 - t) / 0.85; // 0 -> 1
fold1 = Math.pow(foldProgress1, 1.2) * 165; // 最大内折 165 度
}
const bone2AngleDeg = (bone1AngleDeg + 4 * t) - fold1 * 0.96;
const bone2Rad = (bone2AngleDeg * Math.PI) / 180;
const p2 = {
x: p1.x + side * CFG.l2 * Math.cos(bone2Rad),
y: p1.y + CFG.l2 * Math.sin(bone2Rad)
};
// 4. 铰链2 (尾骨段) 折角计算:开伞时呈自然下收微弧(约26°),收伞时内折贴入
let fold2 = 0;
if (t < 0.95) {
const foldProgress2 = Math.min(1, (0.95 - t) / 0.7);
fold2 = Math.pow(foldProgress2, 0.9) * 155; // 尾骨反折贴入
}
const bone3AngleDeg = (bone2AngleDeg + 4 * t) + fold2 * 0.88;
const bone3Rad = (bone3AngleDeg * Math.PI) / 180;
const p3 = {
x: p2.x + side * CFG.l3 * Math.cos(bone3Rad),
y: p2.y + CFG.l3 * Math.sin(bone3Rad)
};
// 5. 中央拉索穿行路径:从滑块 -> 伞顶导轮 -> 铰链1背侧 -> 铰链2背侧 -> 尾骨固定锚点
const cableSlack = (1 - t) * 12;
const cablePath = `M ${pRunnerJoint.x} ${pRunnerJoint.y}
Q ${CFG.cx + side * 16} ${(CFG.topY + runnerY) * 0.48}, ${CFG.cx + side * 14} ${CFG.topY - 4}
Q ${(p0.x + p1.x)*0.5 + side * cableSlack*0.5} ${(p0.y + p1.y)*0.5 - cableSlack*0.3}, ${p1.x} ${p1.y - 3}
Q ${(p1.x + p2.x)*0.5} ${(p1.y + p2.y)*0.5}, ${p2.x} ${p2.y - 2}
L ${p3.x} ${p3.y}`;
return {
p0, p1, p2, p3, pStrutAttach, pRunnerJoint, runnerY, fold1, fold2, cablePath
};
}
/**
* 将计算好的物理坐标渲染到 SVG 视图与 HUD
*/
function renderFrame(t) {
const right = calculateKinematics(t, 1);
const left = calculateKinematics(t, -1);
// 1. 更新滑块套筒位置
els.runner.setAttribute('transform', `translate(${CFG.cx}, ${right.runnerY})`);
// 2. 更新右侧骨架连杆
els.rStrut.setAttribute('x1', right.pRunnerJoint.x);
els.rStrut.setAttribute('y1', right.pRunnerJoint.y);
els.rStrut.setAttribute('x2', right.pStrutAttach.x);
els.rStrut.setAttribute('y2', right.pStrutAttach.y);
els.rBone1.setAttribute('x1', right.p0.x);
els.rBone1.setAttribute('y1', right.p0.y);
els.rBone1.setAttribute('x2', right.p1.x);
els.rBone1.setAttribute('y2', right.p1.y);
els.rBone2.setAttribute('x1', right.p1.x);
els.rBone2.setAttribute('y1', right.p1.y);
els.rBone2.setAttribute('x2', right.p2.x);
els.rBone2.setAttribute('y2', right.p2.y);
els.rBone3.setAttribute('x1', right.p2.x);
els.rBone3.setAttribute('y1', right.p2.y);
els.rBone3.setAttribute('x2', right.p3.x);
els.rBone3.setAttribute('y2', right.p3.y);
els.rJointH1.setAttribute('transform', `translate(${right.p1.x}, ${right.p1.y})`);
els.rJointH2.setAttribute('transform', `translate(${right.p2.x}, ${right.p2.y})`);
els.rTip.setAttribute('cx', right.p3.x);
els.rTip.setAttribute('cy', right.p3.y);
els.rCable.setAttribute('d', right.cablePath);
// 3. 更新左侧骨架连杆 (与右侧精确镜像对称)
els.lStrut.setAttribute('x1', left.pRunnerJoint.x);
els.lStrut.setAttribute('y1', left.pRunnerJoint.y);
els.lStrut.setAttribute('x2', left.pStrutAttach.x);
els.lStrut.setAttribute('y2', left.pStrutAttach.y);
els.lBone1.setAttribute('x1', left.p0.x);
els.lBone1.setAttribute('y1', left.p0.y);
els.lBone1.setAttribute('x2', left.p1.x);
els.lBone1.setAttribute('y2', left.p1.y);
els.lBone2.setAttribute('x1', left.p1.x);
els.lBone2.setAttribute('y1', left.p1.y);
els.lBone2.setAttribute('x2', left.p2.x);
els.lBone2.setAttribute('y2', left.p2.y);
els.lBone3.setAttribute('x1', left.p2.x);
els.lBone3.setAttribute('y1', left.p2.y);
els.lBone3.setAttribute('x2', left.p3.x);
els.lBone3.setAttribute('y2', left.p3.y);
els.lJointH1.setAttribute('transform', `translate(${left.p1.x}, ${left.p1.y})`);
els.lJointH2.setAttribute('transform', `translate(${left.p2.x}, ${left.p2.y})`);
els.lTip.setAttribute('cx', left.p3.x);
els.lTip.setAttribute('cy', left.p3.y);
els.lCable.setAttribute('d', left.cablePath);
// 4. 更新伞面布面曲线
const rightCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(right.p0.x + right.p1.x)*0.5} ${(right.p0.y + right.p1.y)*0.5 - 6*t}, ${right.p1.x} ${right.p1.y}
Q ${(right.p1.x + right.p2.x)*0.5} ${(right.p1.y + right.p2.y)*0.5 - 4*t}, ${right.p2.x} ${right.p2.y}
Q ${(right.p2.x + right.p3.x)*0.5} ${(right.p2.y + right.p3.y)*0.5 - 2*t}, ${right.p3.x} ${right.p3.y}
L ${right.p3.x - 4} ${right.p3.y + 4}
Z`;
els.canopyRight.setAttribute('d', rightCanopyD);
const leftCanopyD = `M ${CFG.cx} ${CFG.topY}
Q ${(left.p0.x + left.p1.x)*0.5} ${(left.p0.y + left.p1.y)*0.5 - 6*t}, ${left.p1.x} ${left.p1.y}
Q ${(left.p1.x + left.p2.x)*0.5} ${(left.p1.y + left.p2.y)*0.5 - 4*t}, ${left.p2.x} ${left.p2.y}
Q ${(left.p2.x + left.p3.x)*0.5} ${(left.p2.y + left.p3.y)*0.5 - 2*t}, ${left.p3.x} ${left.p3.y}
L ${left.p3.x + 4} ${left.p3.y + 4}
Z`;
els.canopyLeft.setAttribute('d', leftCanopyD);
// 5. 动态更新包络线透明度 (收伞时显现规整柱状约束)
const envOpacity = Math.max(0, (0.3 - t) * 2.5);
els.envL.style.opacity = envOpacity;
els.envR.style.opacity = envOpacity;
// 6. 更新画中画 (PIP) 铰链特写旋转角
const pipAngle = right.fold1 * 0.9;
els.pipRotArm.setAttribute('transform', `rotate(${-pipAngle})`);
// 扭簧线圈形变
const coilScale = 1 + (1 - t) * 0.25;
els.pipSpringCoil.setAttribute('transform', `scale(${coilScale})`);
// 拉索在特写中的松弛/张紧
if (t > 0.7) {
els.pipHingeMode.textContent = '锁定受拉 300N';
els.pipHingeMode.style.color = '#38bdf8';
els.pipCableLine.setAttribute('stroke', '#38bdf8');
els.pipCableLine.setAttribute('stroke-width', '2.5');
} else {
els.pipHingeMode.textContent = '扭簧释能内折';
els.pipHingeMode.style.color = '#f59e0b';
els.pipCableLine.setAttribute('stroke', 'rgba(56,189,248,0.3)');
els.pipCableLine.setAttribute('stroke-width', '1.2');
}
// 7. 更新 HUD 遥测参数读数与状态徽标
const cableTension = (Math.max(0, (t - 0.2) / 0.8) * 300).toFixed(1);
const springTorque = ((1 - t) * 0.02).toFixed(3);
const alignQuality = (100 - Math.abs(t - 0.5) * 12).toFixed(0);
els.cableTensionVal.textContent = `${cableTension} N`;
els.springTorqueVal.textContent = `${(0.02 * (1 - t * 0.5)).toFixed(3)} N·m`;
if (t < 0.15) {
els.badge.textContent = '自动贴杆聚拢';
els.badge.style.color = '#10b981';
els.alignmentVal.textContent = '100 % (极佳)';
els.rCable.setAttribute('stroke-opacity', '0.25');
els.lCable.setAttribute('stroke-opacity', '0.25');
} else if (t < 0.8) {
els.badge.textContent = '扭簧顺序内折中';
els.badge.style.color = '#f59e0b';
els.alignmentVal.textContent = '自寻优对齐...';
els.rCable.setAttribute('stroke-opacity', '0.6');
els.lCable.setAttribute('stroke-opacity', '0.6');
} else {
els.badge.textContent = '刚性限位锁定';
els.badge.style.color = '#38bdf8';
els.alignmentVal.textContent = '刚性支撑';
els.rCable.setAttribute('stroke-opacity', '1');
els.lCable.setAttribute('stroke-opacity', '1');
}
// 更新滑块状态
els.slider.value = t;
els.sliderPct.textContent = `状态 (${Math.round(t * 100)}%)`;
}
/**
* 主动画循环 - 自动往复展示
*/
function tick(timestamp) {
if (!state.lastTimestamp) state.lastTimestamp = timestamp;
const dt = timestamp - state.lastTimestamp;
state.lastTimestamp = timestamp;
if (state.isPlaying) {
state.progress += state.direction * state.speed;
// 边界平滑停留换向
if (state.progress <= 0) {
state.progress = 0;
state.direction = 0; // 停顿片刻
setTimeout(() => {
if (state.isPlaying && state.progress <= 0.05) state.direction = 1;
}, 1100);
} else if (state.progress >= 1) {
state.progress = 1;
state.direction = 0;
setTimeout(() => {
if (state.isPlaying && state.progress >= 0.95) state.direction = -1;
}, 1400);
}
renderFrame(state.progress);
}
requestAnimationFrame(tick);
}
// 交互事件绑定
function initControls() {
// 拖动条实时手动探究
els.slider.addEventListener('input', (e) => {
state.isPlaying = false;
updatePlayBtnIcon();
state.progress = parseFloat(e.target.value);
renderFrame(state.progress);
});
// 播放/暂停控制
els.playBtn.addEventListener('click', () => {
state.isPlaying = !state.isPlaying;
if (state.isPlaying && state.direction === 0) {
state.direction = state.progress > 0.5 ? -1 : 1;
}
updatePlayBtnIcon();
});
// 键盘快捷键 (空格播放/暂停)
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
els.playBtn.click();
}
});
}
function updatePlayBtnIcon() {
if (state.isPlaying) {
els.playIcon.innerHTML = `
<rect x="6" y="4" width="4" height="16" rx="1"/>
<rect x="14" y="4" width="4" height="16" rx="1"/>
`;
} else {
els.playIcon.innerHTML = `
<polygon points="6,4 20,12 6,20"/>
`;
}
}
// 页面加载完成后自启动运行
window.addEventListener('DOMContentLoaded', () => {
initControls();
renderFrame(1.0);
// 触发自动开机动画
requestAnimationFrame(tick);
});
</script>
</body>
</html>
积分规则:每轮对话扣10分
等待动画代码生成...
