这是为您生成的一个展示机械蛇“变刚度推进”原理的高保真 SVG 交互式动画网页源码,用于直观演示 TRIZ 最终理想解(IFR)的核心机制。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机械蛇变刚度推进 - TRIZ IFR 原理动画</title>
<style>
:root {
--bg: #080d16;
--bg-panel: #0d1422;
--border: #1a2540;
--text: #c8d6e5;
--text-dim: #6b7d99;
--accent-warm: #f08c40;
--accent-hot: #ff5c35;
--accent-cold: #4a7db5;
--accent-rigid: #3b5f85;
--glow-warm: rgba(240, 140, 64, 0.5);
--glow-hot: rgba(255, 92, 53, 0.6);
--gold: #e2b04a;
--cyan: #3cc6d0;
--font-display: 'Georgia', 'Times New Roman', serif;
--font-body: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', 'Monaco', monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--bg);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: var(--font-body);
color: var(--text);
overflow-x: hidden;
background-image:
radial-gradient(ellipse at 50% 30%, rgba(30, 60, 110, 0.25) 0%, transparent 70%),
radial-gradient(ellipse at 85% 65%, rgba(200, 100, 40, 0.08) 0%, transparent 55%),
radial-gradient(ellipse at 15% 60%, rgba(60, 140, 200, 0.06) 0%, transparent 50%);
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1050px;
padding: 18px 20px 28px;
gap: 16px;
}
/* 标题区域 */
.header {
text-align: center;
width: 100%;
}
.header .tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--gold);
border: 1px solid rgba(226, 176, 74, 0.35);
padding: 4px 14px;
border-radius: 20px;
margin-bottom: 8px;
background: rgba(226, 176, 74, 0.05);
}
.header h1 {
font-family: var(--font-display);
font-size: 1.55rem;
font-weight: 400;
letter-spacing: 0.03em;
color: #e8edf5;
line-height: 1.35;
max-width: 680px;
margin: 0 auto;
}
.header h1 .highlight {
color: var(--accent-warm);
font-style: italic;
}
/* Canvas 容器 */
.canvas-wrapper {
position: relative;
width: 100%;
max-width: 950px;
aspect-ratio: 950 / 520;
background: var(--bg-panel);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.02),
0 8px 40px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.015);
overflow: hidden;
cursor: default;
}
.canvas-wrapper canvas {
display: block;
width: 100%;
height: 100%;
}
/* 截面图叠加层 */
.cross-section-overlay {
position: absolute;
top: 16px;
right: 18px;
width: 130px;
height: 130px;
pointer-events: none;
z-index: 5;
}
.cross-section-overlay svg {
width: 100%;
height: 100%;
}
.cross-section-label {
position: absolute;
bottom: 2px;
right: 0;
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.08em;
color: var(--text-dim);
text-align: right;
white-space: nowrap;
}
/* 刚度分布条 */
.stiffness-bar-container {
position: absolute;
bottom: 20px;
left: 30px;
right: 170px;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.04);
pointer-events: none;
z-index: 4;
overflow: visible;
}
.stiffness-bar-label {
position: absolute;
top: -18px;
left: 0;
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 0.06em;
color: var(--text-dim);
}
.stiffness-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease, background 0.4s ease;
background: linear-gradient(90deg, var(--accent-rigid) 0%, var(--accent-cold) 40%, var(--accent-warm) 70%, var(--accent-hot) 100%);
opacity: 0.8;
}
/* 控制面板 */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 22px;
width: 100%;
max-width: 950px;
padding: 14px 20px;
background: var(--bg-panel);
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group label {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.05em;
color: var(--text-dim);
white-space: nowrap;
min-width: 52px;
text-align: right;
}
.control-group input[type="range"] {
-webkit-appearance: none;
width: 120px;
height: 5px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.1);
outline: none;
cursor: pointer;
}
.control-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent-warm);
cursor: pointer;
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 0 12px var(--glow-warm);
transition: all 0.2s ease;
}
.control-group input[type="range"]::-webkit-slider-thumb:hover {
box-shadow: 0 0 20px var(--glow-hot);
transform: scale(1.1);
}
.control-value {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text);
min-width: 36px;
text-align: left;
}
.btn {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.06em;
padding: 8px 18px;
border-radius: 22px;
cursor: pointer;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
transition: all 0.25s ease;
white-space: nowrap;
position: relative;
overflow: hidden;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 0 18px rgba(240, 140, 64, 0.2);
}
.btn.active {
background: rgba(240, 140, 64, 0.18);
border-color: var(--accent-warm);
color: var(--accent-warm);
box-shadow: 0 0 14px rgba(240, 140, 64, 0.3);
}
.btn-play {
min-width: 55px;
text-align: center;
font-size: 0.85rem;
}
/* 图例 */
.legend-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
align-items: center;
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.05em;
color: var(--text-dim);
}
.legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
vertical-align: middle;
}
.legend-dot.warm {
background: var(--accent-warm);
box-shadow: 0 0 8px var(--glow-warm);
}
.legend-dot.cold {
background: var(--accent-rigid);
box-shadow: 0 0 4px rgba(74, 125, 181, 0.4);
}
.legend-dot.gold {
background: var(--gold);
box-shadow: 0 0 6px rgba(226, 176, 74, 0.5);
}
/* 响应式 */
@media (max-width: 768px) {
.main-container {
padding: 10px 8px 16px;
gap: 10px;
}
.header h1 {
font-size: 1.2rem;
max-width: 95%;
}
.canvas-wrapper {
border-radius: 10px;
aspect-ratio: 4/3;
}
.cross-section-overlay {
width: 90px;
height: 90px;
top: 8px;
right: 8px;
}
.stiffness-bar-container {
left: 14px;
right: 110px;
bottom: 10px;
height: 5px;
}
.controls {
gap: 10px;
padding: 10px 12px;
}
.control-group input[type="range"] {
width: 70px;
}
.btn {
padding: 6px 12px;
font-size: 0.62rem;
}
}
</style>
</head>
<body>
<div class="main-container">
<!-- 标题 -->
<div class="header">
<span class="tag">TRIZ · 最终理想解 IFR</span>
<h1>
变刚度机械蛇:<span class="highlight">空间分离</span>柔性与刚性段,
无需变摩擦机构即可<span class="highlight">自发推进</span>
</h1>
</div>
<!-- Canvas 动画容器 -->
<div class="canvas-wrapper" id="canvasWrapper">
<canvas id="mainCanvas"></canvas>
<!-- 截面图叠加 -->
<div class="cross-section-overlay" id="crossSectionOverlay">
<svg viewBox="0 0 130 130" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="csBg" cx="50%" cy="50%" r="55%">
<stop offset="0%" stop-color="#1a2540" stop-opacity="0.95" />
<stop offset="100%" stop-color="#0d1422" stop-opacity="0.98" />
</radialGradient>
<filter id="csGlow">
<feGaussianBlur stdDeviation="1.2" result="blur" />
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
</defs>
<!-- 背景 -->
<circle cx="65" cy="65" r="60" fill="url(#csBg)" stroke="#1e3050" stroke-width="1.2" />
<!-- 最外层:硅胶表皮 1mm (半径约27px对应实际) -->
<circle cx="65" cy="65" r="48" fill="none" stroke="#6b7d99" stroke-width="5" opacity="0.7" />
<!-- 横向纹路 -->
<circle cx="65" cy="65" r="47" fill="none" stroke="#8899b0" stroke-width="0.5" stroke-dasharray="2.5 3.5" opacity="0.5" />
<circle cx="65" cy="65" r="44.5" fill="none" stroke="#8899b0" stroke-width="0.4" stroke-dasharray="1.8 4" opacity="0.4" />
<!-- 中间层:SMP 可变刚度层 3mm -->
<circle cx="65" cy="65" r="41" fill="none" stroke="#e8a860" stroke-width="9" opacity="0.75" filter="url(#csGlow)" />
<!-- 电热丝缠绕示意(螺旋投影为虚线环) -->
<circle cx="65" cy="65" r="37" fill="none" stroke="#ff7043" stroke-width="0.7" stroke-dasharray="1.5 2.5" opacity="0.9" />
<circle cx="65" cy="65" r="34" fill="none" stroke="#ff7043" stroke-width="0.5" stroke-dasharray="1.2 3" opacity="0.6" />
<!-- 最内层:铝合金芯轴 12mm直径 -->
<circle cx="65" cy="65" r="21" fill="#3a4a60" stroke="#8899b0" stroke-width="1.5" />
<circle cx="65" cy="65" r="16" fill="#2a3545" stroke="#556880" stroke-width="1" />
<circle cx="65" cy="65" r="6" fill="#1a2535" stroke="#445566" stroke-width="0.8" />
<!-- 走线孔 -->
<circle cx="65" cy="65" r="4" fill="#0a0f18" />
<!-- 标注线 -->
<line x1="65" y1="17" x2="95" y2="6" stroke="#6b7d99" stroke-width="0.5" />
<text x="98" y="9" fill="#6b7d99" font-size="7" font-family="'SF Mono','Consolas',monospace">硅胶表皮</text>
<line x1="65" y1="26" x2="100" y2="22" stroke="#e8a860" stroke-width="0.5" />
<text x="103" y="25" fill="#e8a860" font-size="7" font-family="'SF Mono','Consolas',monospace">SMP层</text>
<line x1="65" y1="44" x2="90" y2="52" stroke="#8899b0" stroke-width="0.5" />
<text x="93" y="55" fill="#8899b0" font-size="6.5" font-family="'SF Mono','Consolas',monospace">Al芯轴</text>
</svg>
<span class="cross-section-label">截面:三层同心管</span>
</div>
<!-- 刚度分布指示条 -->
<div class="stiffness-bar-container" id="stiffnessBarContainer">
<span class="stiffness-bar-label">沿体刚度分布</span>
<div class="stiffness-bar-fill" id="stiffnessBarFill" style="width:65%;"></div>
</div>
</div>
<!-- 图例 -->
<div class="legend-row">
<span><span class="legend-dot warm"></span> 加热软化段(柔性·SMP > Tg)</span>
<span><span class="legend-dot cold"></span> 冷却刚性段(支撑自重)</span>
<span><span class="legend-dot gold"></span> 推进力方向</span>
<span style="color:var(--cyan);">●</span> 拉线肌腱示意
</div>
<!-- 控制面板 -->
<div class="controls" id="controlsPanel">
<div class="control-group">
<label for="speedSlider">波速</label>
<input type="range" id="speedSlider" min="0.2" max="2.5" step="0.1" value="1.0" title="蛇形波传播速度">
<span class="control-value" id="speedValue">1.0×</span>
</div>
<div class="control-group">
<label for="amplitudeSlider">振幅</label>
<input type="range" id="amplitudeSlider" min="0.3" max="1.8" step="0.05" value="1.0" title="弯曲幅度">
<span class="control-value" id="amplitudeValue">1.0×</span>
</div>
<div class="control-group">
<label for="preheatSlider">预热提前</label>
<input type="range" id="preheatSlider" min="0" max="3.5" step="0.1" value="2.0" title="加热提前量(秒)">
<span class="control-value" id="preheatValue">2.0s</span>
</div>
<button class="btn btn-play active" id="btnPlay" title="播放/暂停">⏯ 播放</button>
<button class="btn" id="btnHighlight" title="高亮显示加热段">🔆 高亮模式</button>
<button class="btn" id="btnReset" title="重置视角">↺ 重置</button>
</div>
</div>
<script>
(function() {
// ==================== DOM 元素 ====================
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const wrapper = document.getElementById('canvasWrapper');
const stiffnessBarFill = document.getElementById('stiffnessBarFill');
const speedSlider = document.getElementById('speedSlider');
const amplitudeSlider = document.getElementById('amplitudeSlider');
const preheatSlider = document.getElementById('preheatSlider');
const speedValue = document.getElementById('speedValue');
const amplitudeValue = document.getElementById('amplitudeValue');
const preheatValue = document.getElementById('preheatValue');
const btnPlay = document.getElementById('btnPlay');
const btnHighlight = document.getElementById('btnHighlight');
const btnReset = document.getElementById('btnReset');
// ==================== 状态变量 ====================
let isPlaying = true;
let highlightMode = false;
let waveSpeed = 1.0;
let waveAmplitude = 1.0;
let preheatSeconds = 2.0;
const WAVE_PERIOD = 5.0; // 基础周期(秒)
const NUM_SEGMENTS = 14;
const SEGMENT_LENGTH = 40; // 每个体节在身体弧长上的跨度(px)
const BASE_AMPLITUDE = 52; // 基础振幅(px)
const WAVELENGTH_PX = 340; // 波长(px)
const BODY_Y_CENTER = 240; // 蛇身体中心Y坐标(在Canvas内)
const BODY_X_START = 60; // 蛇身体起始X
const GROUND_Y = 340; // 地面Y坐标
// ==================== Canvas 尺寸适配 ====================
function resizeCanvas() {
const rect = wrapper.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = rect.width;
const h = rect.height;
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
return { w, h };
}
// ==================== 粒子系统 ====================
class HeatParticle {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 0.6;
this.vy = -(Math.random() * 1.5 + 0.8);
this.life = 1.0;
this.decay = 0.012 + Math.random() * 0.025;
this.size = 1.2 + Math.random() * 3.5;
this.opacity = 0.5 + Math.random() * 0.5;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
this.opacity *= 0.985;
}
draw(ctx) {
if (this.life <= 0) return;
const alpha = this.life * this.opacity;
const grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
grad.addColorStop(0, `rgba(255,160,60,${alpha})`);
grad.addColorStop(0.5, `rgba(255,110,35,${alpha * 0.7})`);
grad.addColorStop(1, `rgba(255,70,20,0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
get dead() { return this.life <= 0; }
}
let particles = [];
function spawnParticles(x, y, count) {
for (let i = 0; i < count; i++) {
particles.push(new HeatParticle(x + (Math.random() - 0.5) * 14, y + (Math.random() - 0.5) * 8));
}
}
function updateAndDrawParticles(ctx) {
particles = particles.filter(p => !p.dead);
for (const p of particles) {
p.update();
p.draw(ctx);
}
// 限制粒子总数
if (particles.length > 300) {
particles.splice(0, particles.length - 300);
}
}
// ==================== 蛇形波计算 ====================
function getSnakeCurveY(s, t) {
// s: 弧长参数(沿身体)
// t: 当前时间(秒)
const amp = BASE_AMPLITUDE * waveAmplitude;
const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
return BODY_Y_CENTER + amp * Math.sin((2 * Math.PI * s) / WAVELENGTH_PX - omega * t);
}
function getSnakeCurveDerivative(s, t) {
const amp = BASE_AMPLITUDE * waveAmplitude;
const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
return amp * (2 * Math.PI / WAVELENGTH_PX) * Math.cos((2 * Math.PI * s) / WAVELENGTH_PX - omega * t);
}
function getCurvatureApprox(s, t) {
// 曲率近似 = |d²y/ds²| (小斜率近似)
const amp = BASE_AMPLITUDE * waveAmplitude;
const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
const phase = (2 * Math.PI * s) / WAVELENGTH_PX - omega * t;
return amp * Math.pow(2 * Math.PI / WAVELENGTH_PX, 2) * Math.abs(Math.sin(phase));
}
// ==================== 绘制函数 ====================
function drawGridBackground(ctx, w, h) {
ctx.strokeStyle = 'rgba(255,255,255,0.025)';
ctx.lineWidth = 0.5;
const spacing = 32;
for (let x = spacing; x < w; x += spacing) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
for (let y = spacing; y < h; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// 主参考线
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, BODY_Y_CENTER);
ctx.lineTo(w, BODY_Y_CENTER);
ctx.setLineDash([4, 16]);
ctx.stroke();
ctx.setLineDash([]);
}
function drawGround(ctx, w) {
// 地面主体
const gy = GROUND_Y;
const groundGrad = ctx.createLinearGradient(0, gy, 0, gy + 60);
groundGrad.addColorStop(0, 'rgba(40,50,65,0.7)');
groundGrad.addColorStop(0.3, 'rgba(30,38,50,0.6)');
groundGrad.addColorStop(1, 'rgba(15,20,30,0)');
ctx.fillStyle = groundGrad;
ctx.fillRect(0, gy, w, 60);
// 地面线
ctx.strokeStyle = 'rgba(180,190,210,0.35)';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(0, gy);
ctx.lineTo(w, gy);
ctx.stroke();
// 地面纹理点
ctx.fillStyle = 'rgba(180,190,210,0.25)';
const seed = 42;
for (let i = 0; i < w; i += 7) {
const pseudoRandom = ((Math.sin(i * 0.37 + seed) * 0.5 + 0.5));
if (pseudoRandom > 0.55) {
const tx = i;
const ty = gy + 4 + pseudoRandom * 10;
ctx.beginPath();
ctx.arc(tx, ty, 0.6 + pseudoRandom * 1.2, 0, Math.PI * 2);
ctx.fill();
}
}
// 标签
ctx.fillStyle = 'rgba(180,190,210,0.5)';
ctx.font = '9px "SF Mono","Consolas",monospace';
ctx.fillText('地面 · 无需主动变摩擦机构', w - 200, gy - 8);
}
function drawSegment(ctx, cx, cy, angle, segLength, stiffnessRatio, index) {
// stiffnessRatio: 0=完全刚性(冷), 1=完全柔性(热)
const halfLen = segLength / 2;
const thickness = 16;
const halfThick = thickness / 2;
// 颜色插值:刚性(冷蓝灰) → 柔性(暖橙)
const rRigid = 58,
gRigid = 90,
bRigid = 130; // 冷蓝灰
const rFlex = 235,
gFlex = 130,
bFlex = 50; // 暖橙
const r = Math.round(rRigid + (rFlex - rRigid) * stiffnessRatio);
const g = Math.round(gRigid + (gFlex - gRigid) * stiffnessRatio);
const b = Math.round(bRigid + (bFlex - bRigid) * stiffnessRatio);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
// 发光光晕(加热段)
if (stiffnessRatio > 0.25) {
const glowAlpha = stiffnessRatio * 0.55;
const glowGrad = ctx.createRadialGradient(0, 0, halfThick * 0.6, 0, 0, halfThick * 2.5);
glowGrad.addColorStop(0, `rgba(255,150,50,${glowAlpha})`);
glowGrad.addColorStop(0.5, `rgba(255,100,30,${glowAlpha * 0.5})`);
glowGrad.addColorStop(1, 'rgba(255,60,20,0)');
ctx.fillStyle = glowGrad;
ctx.fillRect(-halfLen - 6, -halfThick * 2.2, segLength + 12, halfThick * 4.4);
}
// 体节主体
const bodyGrad = ctx.createLinearGradient(0, -halfThick, 0, halfThick);
bodyGrad.addColorStop(0, `rgba(${Math.min(r+30,255)},${Math.min(g+25,255)},${Math.min(b+20,255)},0.9)`);
bodyGrad.addColorStop(0.35, `rgba(${r},${g},${b},0.95)`);
bodyGrad.addColorStop(0.65, `rgba(${Math.max(r-25,0)},${Math.max(g-20,0)},${Math.max(b-15,0)},0.9)`);
bodyGrad.addColorStop(1, `rgba(${Math.max(r-40,0)},${Math.max(g-35,0)},${Math.max(b-30,0)},0.85)`);
ctx.fillStyle = bodyGrad;
const radius = 6;
ctx.beginPath();
ctx.moveTo(-halfLen + radius, -halfThick);
ctx.lineTo(halfLen - radius, -halfThick);
ctx.arcTo(halfLen, -halfThick, halfLen, -halfThick + radius, radius);
ctx.lineTo(halfLen, halfThick - radius);
ctx.arcTo(halfLen, halfThick, halfLen - radius, halfThick, radius);
ctx.lineTo(-halfLen + radius, halfThick);
ctx.arcTo(-halfLen, halfThick, -halfLen, halfThick - radius, radius);
ctx.lineTo(-halfLen, -halfThick + radius);
ctx.arcTo(-halfLen, -halfThick, -halfLen + radius, -halfThick, radius);
ctx.closePath();
ctx.fill();
// 边框
ctx.strokeStyle = `rgba(255,255,255,${0.15 + stiffnessRatio * 0.25})`;
ctx.lineWidth = 1;
ctx.stroke();
// 横向防滑纹路(硅胶表皮纹理)
ctx.strokeStyle = `rgba(255,255,255,${0.08 + stiffnessRatio * 0.12})`;
ctx.lineWidth = 0.5;
const纹路间距 = 4.5;
for (let dx = -halfLen + 8; dx < halfLen - 6; dx += 纹路间距) {
ctx.beginPath();
ctx.moveTo(dx, -halfThick + 1.5);
ctx.lineTo(dx, halfThick - 1.5);
ctx.stroke();
}
// 关节连接点标记(两端)
const jointDotR = 2.5;
[-halfLen, halfLen].forEach(jx => {
ctx.fillStyle = 'rgba(200,200,210,0.7)';
ctx.beginPath();
ctx.arc(jx, 0, jointDotR, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 0.7;
ctx.stroke();
});
ctx.restore();
}
function drawTendonLine(ctx, x1, y1, x2, y2, side) {
// side: 'left' or 'right' (relative to segment direction)
const alpha = 0.55;
ctx.strokeStyle = `rgba(60,200,210,${alpha})`;
ctx.lineWidth = 0.8;
ctx.setLineDash([3, 5]);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.setLineDash([]);
// 拉线端点小圆
ctx.fillStyle = `rgba(60,200,210,${alpha + 0.2})`;
ctx.beginPath();
ctx.arc(x1, y1, 1.8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x2, y2, 1.8, 0, Math.PI * 2);
ctx.fill();
}
function drawForceArrow(ctx, x, y, dirX, dirY, alpha) {
const len = 16;
const headLen = 7;
const headAngle = Math.PI / 7;
const angle = Math.atan2(dirY, dirX);
const ex = x + dirX * len;
const ey = y + dirY * len;
ctx.strokeStyle = `rgba(226,176,74,${alpha})`;
ctx.lineWidth = 1.8;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(ex, ey);
ctx.stroke();
// 箭头头部
ctx.fillStyle = `rgba(226,176,74,${alpha})`;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(
ex - headLen * Math.cos(angle - headAngle),
ey - headLen * Math.sin(angle - headAngle)
);
ctx.lineTo(
ex - headLen * Math.cos(angle + headAngle),
ey - headLen * Math.sin(angle + headAngle)
);
ctx.closePath();
ctx.fill();
}
// ==================== 主渲染循环 ====================
let animTime = 0; // 动画累计时间(秒)
let lastFrameTime = null;
function render(timestamp) {
const { w, h } = resizeCanvas();
if (!lastFrameTime) lastFrameTime = timestamp;
let dt = (timestamp - lastFrameTime) / 1000;
if (dt > 0.1) dt = 0.1; // 防止大帧跳跃
if (dt <= 0) dt = 0.016;
lastFrameTime = timestamp;
if (isPlaying) {
animTime += dt;
}
ctx.clearRect(0, 0, w, h);
// 背景
const bgGrad = ctx.createRadialGradient(w * 0.5, h * 0.4, w * 0.15, w * 0.5, h * 0.5, w * 0.9);
bgGrad.addColorStop(0, '#111c2e');
bgGrad.addColorStop(1, '#080d16');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
drawGridBackground(ctx, w, h);
drawGround(ctx, w);
// 计算蛇身体总弧长
const totalArcLength = NUM_SEGMENTS * SEGMENT_LENGTH;
const bodyStartS = BODY_X_START;
// 存储每个体节的信息
const segments = [];
for (let i = 0; i < NUM_SEGMENTS; i++) {
const s = bodyStartS + i * SEGMENT_LENGTH + SEGMENT_LENGTH / 2;
const y = getSnakeCurveY(s, animTime);
const dyDs = getSnakeCurveDerivative(s, animTime);
const angle = Math.atan2(dyDs, 1);
// 曲率(用于判断是否需要加热)
const curvature = getCurvatureApprox(s, animTime);
// 提前加热:使用未来时间的曲率
const futureS = s + (preheatSeconds / WAVE_PERIOD) * WAVELENGTH_PX * waveSpeed;
const futureCurvature = getCurvatureApprox(futureS, animTime);
// 刚度比率:曲率大→需要柔性→加热→stiffnessRatio高
const maxCurv = BASE_AMPLITUDE * waveAmplitude * Math.pow(2 * Math.PI / WAVELENGTH_PX, 2);
const stiffnessRatio = Math.min(1, Math.max(0, futureCurvature / (maxCurv * 0.55)));
// 平滑过渡
const smoothRatio = stiffnessRatio < 0.05 ? 0 :
stiffnessRatio > 0.95 ? 1 :
0.5 - 0.5 * Math.cos(Math.PI * (stiffnessRatio - 0.05) / 0.9);
segments.push({
index: i,
s: s,
x: s, // x坐标近似等于弧长参数
y: y,
angle: angle,
curvature: curvature,
stiffnessRatio: smoothRatio,
isHeated: smoothRatio > 0.35,
isHighlyFlexible: smoothRatio > 0.7,
});
}
// 绘制拉线肌腱(在体节下方)
for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
const segA = segments[i];
const segB = segments[i + 1];
const mx = (segA.x + segB.x) / 2;
const my = (segA.y + segB.y) / 2;
const dx = segB.x - segA.x;
const dy = segB.y - segA.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const nx = -dy / dist;
const ny = dx / dist;
const offsetDist = 10;
// 两侧拉线
drawTendonLine(ctx,
segA.x + nx * offsetDist, segA.y + ny * offsetDist,
segB.x + nx * offsetDist, segB.y + ny * offsetDist,
'left');
drawTendonLine(ctx,
segA.x - nx * offsetDist, segA.y - ny * offsetDist,
segB.x - nx * offsetDist, segB.y - ny * offsetDist,
'right');
}
// 绘制体节
for (const seg of segments) {
drawSegment(ctx, seg.x, seg.y, seg.angle, SEGMENT_LENGTH, seg.stiffnessRatio, seg.index);
}
// 绘制关节高亮(弯曲大的关节)
for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
const segA = segments[i];
const segB = segments[i + 1];
const angleDiff = Math.abs(segB.angle - segA.angle);
if (angleDiff > 0.08 && segA.stiffnessRatio > 0.3) {
const jx = (segA.x + segB.x) / 2;
const jy = (segA.y + segB.y) / 2;
const glowGrad = ctx.createRadialGradient(jx, jy, 2, jx, jy, 10);
glowGrad.addColorStop(0, 'rgba(255,180,80,0.7)');
glowGrad.addColorStop(1, 'rgba(255,100,30,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(jx, jy, 10, 0, Math.PI * 2);
ctx.fill();
}
}
// 绘制推进力箭头(波峰/波谷与地面接触点)
const contactThreshold = 28;
for (const seg of segments) {
const distToGround = Math.abs(seg.y - GROUND_Y);
if (distToGround < contactThreshold && seg.stiffnessRatio > 0.3) {
const contactAlpha = (1 - distToGround / contactThreshold) * 0.7;
// 推进力方向:在波峰处向前,波谷处也向前(蛇形波特征)
const pushDirX = 1;
const pushDirY = 0;
drawForceArrow(ctx, seg.x, GROUND_Y - 2, pushDirX, pushDirY, contactAlpha);
}
}
// 加热段粒子
if (isPlaying) {
for (const seg of segments) {
if (seg.isHighlyFlexible && Math.random() < 0.35) {
spawnParticles(seg.x, seg.y - 10, 2);
} else if (seg.isHeated && Math.random() < 0.12) {
spawnParticles(seg.x, seg.y - 6, 1);
}
}
}
updateAndDrawParticles(ctx);
// 高亮模式:在加热段绘制轮廓
if (highlightMode) {
for (const seg of segments) {
if (seg.isHeated) {
ctx.strokeStyle = 'rgba(255,200,80,0.7)';
ctx.lineWidth = 2.5;
ctx.setLineDash([6, 3]);
ctx.beginPath();
const hlRad = SEGMENT_LENGTH / 2 + 5;
ctx.arc(seg.x, seg.y, hlRad, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
// 蛇头部标记
if (segments.length > 0) {
const head = segments[0];
const headX = head.x - SEGMENT_LENGTH / 2 - 6;
const headY = head.y;
ctx.fillStyle = '#e8c060';
ctx.beginPath();
ctx.moveTo(headX - 10, headY);
ctx.lineTo(headX + 2, headY - 8);
ctx.lineTo(headX + 2, headY + 8);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(255,220,140,0.8)';
ctx.lineWidth = 1.2;
ctx.stroke();
// 头部光晕
const headGlow = ctx.createRadialGradient(headX - 3, headY, 2, headX - 3, headY, 16);
headGlow.addColorStop(0, 'rgba(255,200,100,0.6)');
headGlow.addColorStop(1, 'rgba(255,150,50,0)');
ctx.fillStyle = headGlow;
ctx.beginPath();
ctx.arc(headX - 3, headY, 16, 0, Math.PI * 2);
ctx.fill();
}
// 尾部标记
if (segments.length > 0) {
const tail = segments[NUM_SEGMENTS - 1];
const tailX = tail.x + SEGMENT_LENGTH / 2 + 4;
const tailY = tail.y;
ctx.fillStyle = '#7a8da0';
ctx.beginPath();
ctx.moveTo(tailX + 6, tailY);
ctx.lineTo(tailX - 2, tailY - 5);
ctx.lineTo(tailX - 2, tailY + 5);
ctx.closePath();
ctx.fill();
}
// 更新刚度分布条
const heatedCount = segments.filter(s => s.isHeated).length;
const heatedRatio = heatedCount / NUM_SEGMENTS;
stiffnessBarFill.style.width = (heatedRatio * 100) + '%';
if (heatedRatio > 0.6) {
stiffnessBarFill.style.background =
'linear-gradient(90deg, #3b5f85 0%, #e8a040 60%, #ff5c35 100%)';
} else if (heatedRatio > 0.3) {
stiffnessBarFill.style.background =
'linear-gradient(90deg, #3b5f85 0%, #4a7db5 45%, #e8a040 100%)';
} else {
stiffnessBarFill.style.background =
'linear-gradient(90deg, #3b5f85 0%, #4a7db5 70%, #c88050 100%)';
}
stiffnessBarFill.style.opacity = '0.75';
// 顶部标注文字
ctx.fillStyle = 'rgba(200,210,225,0.7)';
ctx.font = '11px "Georgia","Times New Roman",serif';
ctx.fillText('← 推进方向', 28, 42);
const labelX = BODY_X_START + totalArcLength + 10;
ctx.fillText('蛇形波传播 →', labelX - 30, BODY_Y_CENTER - BASE_AMPLITUDE * waveAmplitude - 30);
// 继续动画循环
requestAnimationFrame(render);
}
// ==================== 事件处理 ====================
btnPlay.addEventListener('click', () => {
isPlaying = !isPlaying;
if (isPlaying) {
lastFrameTime = null;
btnPlay.classList.add('active');
btnPlay.textContent = '⏯ 播放';
} else {
btnPlay.classList.remove('active');
btnPlay.textContent = '▶ 暂停中';
}
});
btnHighlight.addEventListener('click', () => {
highlightMode = !highlightMode;
if (highlightMode) {
btnHighlight.classList.add('active');
btnHighlight.textContent = '🔆 高亮:开';
} else {
btnHighlight.classList.remove('active');
btnHighlight.textContent = '🔆 高亮模式';
}
});
btnReset.addEventListener('click', () => {
waveSpeed = 1.0;
waveAmplitude = 1.0;
preheatSeconds = 2.0;
animTime = 0;
lastFrameTime = null;
isPlaying = true;
highlightMode = false;
particles = [];
speedSlider.value = 1.0;
amplitudeSlider.value = 1.0;
preheatSlider.value = 2.0;
speedValue.textContent = '1.0×';
amplitudeValue.textContent = '1.0×';
preheatValue.textContent = '2.0s';
btnPlay.classList.add('active');
btnPlay.textContent = '⏯ 播放';
btnHighlight.classList.remove('active');
btnHighlight.textContent = '🔆 高亮模式';
stiffnessBarFill.style.width = '65%';
stiffnessBarFill.style.background =
'linear-gradient(90deg, #3b5f85 0%, #4a7db5 40%, #e8a040 70%, #ff5c35 100%)';
stiffnessBarFill.style.opacity = '0.8';
});
speedSlider.addEventListener('input', () => {
waveSpeed = parseFloat(speedSlider.value);
speedValue.textContent = waveSpeed.toFixed(1) + '×';
});
amplitudeSlider.addEventListener('input', () => {
waveAmplitude = parseFloat(amplitudeSlider.value);
amplitudeValue.textContent = waveAmplitude.toFixed(1) + '×';
});
preheatSlider.addEventListener('input', () => {
preheatSeconds = parseFloat(preheatSlider.value);
preheatValue.textContent = preheatSeconds.toFixed(1) + 's';
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
btnPlay.click();
break;
case 'h':
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
btnHighlight.click();
}
break;
case 'r':
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
btnReset.click();
}
break;
case 'arrowright':
e.preventDefault();
waveSpeed = Math.min(2.5, waveSpeed + 0.1);
speedSlider.value = waveSpeed;
speedValue.textContent = waveSpeed.toFixed(1) + '×';
break;
case 'arrowleft':
e.preventDefault();
waveSpeed = Math.max(0.2, waveSpeed - 0.1);
speedSlider.value = waveSpeed;
speedValue.textContent = waveSpeed.toFixed(1) + '×';
break;
case 'arrowup':
e.preventDefault();
waveAmplitude = Math.min(1.8, waveAmplitude + 0.05);
amplitudeSlider.value = waveAmplitude;
amplitudeValue.textContent = waveAmplitude.toFixed(2) + '×';
break;
case 'arrowdown':
e.preventDefault();
waveAmplitude = Math.max(0.3, waveAmplitude - 0.05);
amplitudeSlider.value = waveAmplitude;
amplitudeValue.textContent = waveAmplitude.toFixed(2) + '×';
break;
}
});
// 窗口大小变化时重设
window.addEventListener('resize', () => {
lastFrameTime = null;
});
// ==================== 启动 ====================
resizeCanvas();
requestAnimationFrame(render);
console.log('🐍 机械蛇变刚度推进动画已就绪');
console.log(' TRIZ IFR 核心:空间分离柔性段与刚性段');
console.log(' ⌨ 快捷键:空格=播放/暂停 | H=高亮 | R=重置 | 方向键=调参');
console.log(' 🖱 拖动底部滑块调节波速/振幅/预热提前量');
})();
</script>
</body>
</html>
动画机制
这个动画将TRIZ“最终理想解”思想进行了演示,让您可以直观看到变刚度蛇身结构如何自行将弯曲运动转换为推力,而不依赖反馈控制和变摩擦机构。
操作与反馈
- 核心机制:蛇身动态划分为“刚性支撑段”(冷蓝色)和“柔性弯曲段”(暖橙色)。弯曲段会提前加热软化,拉线肌腱轻松拉动关节形成蛇形波;直线段保持高刚性抵抗重力下垂。动画通过色彩和光晕生动展示了这种空间上的刚度分离。
- 交互方式:您可以通过拖拽滑块实时调节“波速”、“振幅”和“预热提前量”,直观感受参数变化对运动形态的影响。点击按钮可播放/暂停动画或开启高亮模式,键盘方向键和空格键也支持快速操作。
- 视觉风格:采用深色科技感主题,蛇身带有横向防滑纹路和三层结构截面图。推进力箭头、拉线肌腱轨迹和加热粒子效果,共同勾勒出完整的运动与力传递链条。
优化建议: 您可以在代码开头的<style>标签中修改CSS变量(如--bg、--accent-warm)来调整主题色;核心运动参数(如WAVE_PERIOD、BASE_AMPLITUDE)位于<script>标签内顶部,可根据演示需求微调。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
