独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仿生柔性鳞片 · 蛇形机器人单向摩擦推进原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #070b14;
--fg: #d0d8e8;
--muted: #4a5568;
--accent: #4cc9f0;
--grip: #f59e0b;
--slide: #0d9488;
--propulsion: #10b981;
--friction: #ef4444;
--card: #0f1520;
--border: #1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
background:var(--bg);
color:var(--fg);
font-family:'Rajdhani',sans-serif;
min-height:100vh;
display:flex;
flex-direction:column;
overflow:hidden;
user-select:none;
}
header{
padding:14px 28px 6px;
display:flex;
align-items:baseline;
gap:18px;
flex-shrink:0;
}
header h1{
font-size:1.35rem;
font-weight:700;
letter-spacing:.04em;
color:#e8ecf4;
}
header h1 span{color:var(--grip)}
header p{
font-size:.82rem;
color:var(--muted);
font-family:'JetBrains Mono',monospace;
font-weight:300;
}
main{
flex:1;
display:flex;
justify-content:center;
align-items:center;
padding:0 16px;
min-height:0;
}
canvas{
width:100%;
max-width:1400px;
border-radius:8px;
background:#0a0f1c;
}
footer{
padding:10px 28px 16px;
display:flex;
gap:28px;
flex-wrap:wrap;
align-items:center;
justify-content:center;
flex-shrink:0;
}
.ctrl{
display:flex;
align-items:center;
gap:8px;
font-family:'JetBrains Mono',monospace;
font-size:.72rem;
color:var(--muted);
}
.ctrl label{white-space:nowrap;min-width:70px}
.ctrl input[type=range]{
-webkit-appearance:none;
appearance:none;
width:110px;height:4px;
background:var(--border);
border-radius:2px;
outline:none;
cursor:pointer;
}
.ctrl input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;
width:14px;height:14px;
border-radius:50%;
background:var(--accent);
border:2px solid var(--bg);
cursor:pointer;
}
.ctrl .val{
min-width:38px;
text-align:right;
color:var(--accent);
font-weight:500;
}
.legend{
display:flex;gap:16px;
font-family:'JetBrains Mono',monospace;
font-size:.68rem;
color:var(--muted);
align-items:center;
}
.legend i{
display:inline-block;
width:10px;height:10px;
border-radius:2px;
margin-right:4px;
vertical-align:middle;
}
.legend .grip-i{background:var(--grip)}
.legend .slide-i{background:var(--slide)}
.legend .prop-i{background:var(--propulsion)}
@media(max-width:700px){
header{flex-direction:column;gap:4px}
footer{gap:12px}
.ctrl input[type=range]{width:80px}
}
</style>
</head>
<body>
<header>
<h1>仿生柔性鳞片 · <span>单向摩擦推进</span></h1>
<p>IFR: 正弦波输入 → 物理结构自锁 → 前进位移 | 零算法开销</p>
</header>
<main>
<canvas id="c"></canvas>
</main>
<footer>
<div class="ctrl">
<label>波动频率</label>
<input type="range" id="rFreq" min="0.2" max="1.6" step="0.05" value="0.65">
<span class="val" id="vFreq">0.65</span>
</div>
<div class="ctrl">
<label>波动幅度</label>
<input type="range" id="rAmp" min="5" max="35" step="1" value="20">
<span class="val" id="vAmp">20°</span>
</div>
<div class="ctrl">
<label>鳞片后掠角</label>
<input type="range" id="rScale" min="15" max="50" step="1" value="30">
<span class="val" id="vScale">30°</span>
</div>
<div class="ctrl">
<label>显示力矢量</label>
<input type="range" id="rArrow" min="0" max="1" step="1" value="1">
<span class="val" id="vArrow">ON</span>
</div>
<div class="legend">
<span><i class="grip-i"></i>锚定抓地</span>
<span><i class="slide-i"></i>顺滑滑行</span>
<span><i class="prop-i"></i>推进合力</span>
</div>
</footer>
<script>
/* ===== 仿生蛇单向摩擦推进原理动画 ===== */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
/* ---------- 配置 ---------- */
const CFG = {
numSeg: 14,
segLen: 38,
bodyW: 18,
waveAmp: 20, // 度
waveFreq: 0.65, // Hz
waveK: 0.62, // 空间波数 (rad/segment)
scaleAngle: 30, // 鳞片后掠角 度
scaleLen: 15,
scalesPerSeg: 3,
showArrows: true,
};
/* ---------- 状态 ---------- */
let time = 0;
let lastTS = 0;
let worldX = 0;
const prevJoints = [];
/* ---------- 控件 ---------- */
const rFreq = document.getElementById('rFreq');
const rAmp = document.getElementById('rAmp');
const rScale= document.getElementById('rScale');
const rArrow= document.getElementById('rArrow');
const vFreq = document.getElementById('vFreq');
const vAmp = document.getElementById('vAmp');
const vScale= document.getElementById('vScale');
const vArrow= document.getElementById('vArrow');
rFreq.oninput = ()=>{ CFG.waveFreq=+rFreq.value; vFreq.textContent=rFreq.value; };
rAmp.oninput = ()=>{ CFG.waveAmp =+rAmp.value; vAmp.textContent=rAmp.value+'°'; };
rScale.oninput = ()=>{ CFG.scaleAngle=+rScale.value; vScale.textContent=rScale.value+'°'; };
rArrow.oninput = ()=>{ CFG.showArrows=+rArrow.value===1; vArrow.textContent=CFG.showArrows?'ON':'OFF'; };
/* ---------- 尺寸 ---------- */
let W, H, cx, groundY;
function resize(){
const rect = canvas.parentElement.getBoundingClientRect();
W = Math.round(rect.width - 32);
H = Math.round(Math.min(rect.height, W * 0.52));
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
cx = W / 2;
groundY = H * 0.62;
}
window.addEventListener('resize', resize);
resize();
/* ---------- 关节计算 ---------- */
function computeJoints(t){
const joints = [];
let x = 0, y = 0, ang = 0;
for(let i = 0; i <= CFG.numSeg; i++){
joints.push({x, y, ang, idx:i});
if(i < CFG.numSeg){
const ja = CFG.waveAmp * Math.sin(2*Math.PI*CFG.waveFreq*t - CFG.waveK*i);
ang += ja * Math.PI/180;
x += CFG.segLen * Math.cos(ang);
y += CFG.segLen * Math.sin(ang);
}
}
return joints;
}
/* ---------- 鳞片状态计算 ---------- */
function segState(segIdx, t){
// 角速度 = dθ/dt 方向决定推/滑
const phase = 2*Math.PI*CFG.waveFreq*t - CFG.waveK*segIdx;
const dTheta = CFG.waveAmp * 2*Math.PI*CFG.waveFreq * Math.cos(phase);
// 正值 → 关节张开 → 节段向后推地 → 锚定
// 负值 → 关节收拢 → 节段向前滑行 → 顺滑
const grip = dTheta > 0 ? Math.min(dTheta / 40, 1) : 0;
const slide = dTheta < 0 ? Math.min(-dTheta / 40, 1) : 0;
return {grip, slide, phase};
}
/* ---------- 绘制辅助 ---------- */
function roundRect(x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.lineTo(x+w-r,y);
ctx.quadraticCurveTo(x+w,y,x+w,y+r);
ctx.lineTo(x+w,y+h-r);
ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
ctx.lineTo(x+r,y+h);
ctx.quadraticCurveTo(x,y+h,x,y+h-r);
ctx.lineTo(x,y+r);
ctx.quadraticCurveTo(x,y,x+r,y);
ctx.closePath();
}
function arrow(x1,y1,x2,y2,color,lw){
const dx=x2-x1, dy=y2-y1;
const len=Math.sqrt(dx*dx+dy*dy);
if(len<2)return;
const ux=dx/len, uy=dy/len;
const hl=Math.min(10,len*0.35);
ctx.save();
ctx.strokeStyle=color;
ctx.fillStyle=color;
ctx.lineWidth=lw||2;
ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2-ux*hl*0.5, y2-uy*hl*0.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x2,y2);
ctx.lineTo(x2-ux*hl-uy*hl*0.4, y2-uy*hl+ux*hl*0.4);
ctx.lineTo(x2-ux*hl+uy*hl*0.4, y2-uy*hl-ux*hl*0.4);
ctx.closePath();
ctx.fill();
ctx.restore();
}
/* ---------- 背景 ---------- */
function drawBG(){
// 渐变背景
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0a0f1c');
g.addColorStop(0.5,'#0d1222');
g.addColorStop(1,'#0a0e18');
ctx.fillStyle=g;
ctx.fillRect(0,0,W,H);
// 点阵网格
ctx.fillStyle='rgba(60,80,120,0.08)';
const sp=28;
const ox=(worldX*0.05)%sp;
for(let x=-sp+ox;x<W+sp;x+=sp){
for(let y=sp;y<H;y+=sp){
ctx.fillRect(x-0.7,y-0.7,1.4,1.4);
}
}
}
/* ---------- 地面 ---------- */
function drawGround(){
// 地面区域
const gg = ctx.createLinearGradient(0,groundY,0,H);
gg.addColorStop(0,'#1a1510');
gg.addColorStop(0.08,'#15110c');
gg.addColorStop(1,'#0c0a07');
ctx.fillStyle=gg;
ctx.fillRect(0,groundY,W,H-groundY);
// 地面线
ctx.strokeStyle='#3a2f20';
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(0,groundY);
ctx.lineTo(W,groundY);
ctx.stroke();
// 滚动纹理
ctx.strokeStyle='rgba(80,65,40,0.15)';
ctx.lineWidth=1;
const sp=40;
const off=(worldX*0.8)%sp;
for(let x=-sp+off;x<W+sp;x+=sp){
ctx.beginPath();
ctx.moveTo(x,groundY+4);
ctx.lineTo(x-6,groundY+12);
ctx.stroke();
}
// 位移刻度尺
ctx.fillStyle='rgba(76,201,240,0.25)';
ctx.font='500 9px "JetBrains Mono"';
ctx.textAlign='center';
const rulerY=groundY+22;
const majorSp=100;
const minorSp=20;
const startM=Math.floor((worldX-W/2)/minorSp)*minorSp;
for(let wx=startM;wx<worldX+W;wx+=minorSp){
const sx=wx-worldX+cx;
if(sx<-20||sx>W+20)continue;
const isMajor=(wx%majorSp===0);
ctx.fillStyle=isMajor?'rgba(76,201,240,0.4)':'rgba(76,201,240,0.12)';
ctx.fillRect(sx-0.5, groundY+2, 1, isMajor?8:4);
if(isMajor){
ctx.fillStyle='rgba(76,201,240,0.35)';
ctx.fillText((wx/10).toFixed(0), sx, rulerY);
}
}
}
/* ---------- 蛇身 ---------- */
function drawSnake(joints){
if(joints.length<2)return;
// 身体中心线偏移到 groundY 上方
const bodyCenterY = groundY - CFG.bodyW*0.5 - 4;
ctx.save();
// 整体平移:蛇中心对齐画布中心
const headX = joints[0].x;
const tailX = joints[CFG.numSeg].x;
const snakeMidX = (headX+tailX)/2;
const offsetX = cx - snakeMidX;
ctx.translate(offsetX, bodyCenterY);
// 绘制身体轮廓(上下偏移)
// 上轮廓
ctx.beginPath();
for(let i=0;i<=CFG.numSeg;i++){
const j=joints[i];
const nx=-Math.sin(j.ang);
const ny= Math.cos(j.ang);
const px=j.x+nx*CFG.bodyW*0.5;
const py=j.y+ny*CFG.bodyW*0.5;
if(i===0)ctx.moveTo(px,py);
else ctx.lineTo(px,py);
}
// 下轮廓(反向)
for(let i=CFG.numSeg;i>=0;i--){
const j=joints[i];
const nx=-Math.sin(j.ang);
const ny= Math.cos(j.ang);
const px=j.x-nx*CFG.bodyW*0.5;
const py=j.y-ny*CFG.bodyW*0.5;
ctx.lineTo(px,py);
}
ctx.closePath();
// 身体渐变填充
const bg=ctx.createLinearGradient(0,-CFG.bodyW,0,CFG.bodyW);
bg.addColorStop(0,'#6b7a8e');
bg.addColorStop(0.35,'#8a9aad');
bg.addColorStop(0.65,'#5a6878');
bg.addColorStop(1,'#3e4a58');
ctx.fillStyle=bg;
ctx.fill();
ctx.strokeStyle='#2a3444';
ctx.lineWidth=1;
ctx.stroke();
// 绘制节段分界线
for(let i=1;i<CFG.numSeg;i++){
const j=joints[i];
const nx=-Math.sin(j.ang);
const ny= Math.cos(j.ang);
ctx.beginPath();
ctx.moveTo(j.x+nx*CFG.bodyW*0.45, j.y+ny*CFG.bodyW*0.45);
ctx.lineTo(j.x-nx*CFG.bodyW*0.45, j.y-ny*CFG.bodyW*0.45);
ctx.strokeStyle='rgba(30,41,59,0.5)';
ctx.lineWidth=0.8;
ctx.stroke();
}
// 绘制鳞片
for(let i=0;i<CFG.numSeg;i++){
const j1=joints[i], j2=joints[i+1];
const segAng=Math.atan2(j2.y-j1.y, j2.x-j1.x);
const midX=(j1.x+j2.x)/2;
const midY=(j1.y+j2.y)/2;
const state=segState(i, time);
// 下法线方向(指向地面)
const bnx=-Math.sin(segAng);
const bny= Math.cos(segAng);
for(let s=0;s<CFG.scalesPerSeg;s++){
const t_param=(s+0.5)/CFG.scalesPerSeg;
const sx=j1.x+(j2.x-j1.x)*t_param;
const sy=j1.y+(j2.y-j1.y)*t_param;
// 鳞片底部附着点
const attachX=sx-bnx*CFG.bodyW*0.48;
const attachY=sy-bny*CFG.bodyW*0.48;
// 鳞片角度:基于状态
const baseAngle=segAng + Math.PI/2; // 指向下方
let sAng;
if(state.grip>0){
// 锚定:鳞片向外张开
sAng=baseAngle + (CFG.scaleAngle*Math.PI/180)*state.grip*0.6;
} else {
// 滑行:鳞片顺向折叠
sAng=baseAngle - (CFG.scaleAngle*Math.PI/180)*0.7 + segAng*0.3*state.slide;
}
const tipX=attachX+Math.cos(sAng)*CFG.scaleLen;
const tipY=attachY+Math.sin(sAng)*CFG.scaleLen;
// 鳞片宽度
const perpAng=sAng+Math.PI/2;
const sw=2.5;
ctx.beginPath();
ctx.moveTo(attachX+Math.cos(perpAng)*sw, attachY+Math.sin(perpAng)*sw);
ctx.lineTo(tipX, tipY);
ctx.lineTo(attachX-Math.cos(perpAng)*sw, attachY-Math.sin(perpAng)*sw);
ctx.closePath();
if(state.grip>0){
const alpha=0.3+state.grip*0.7;
ctx.fillStyle=`rgba(245,158,11,${alpha})`;
ctx.fill();
// 发光
if(state.grip>0.4){
ctx.shadowColor='#f59e0b';
ctx.shadowBlur=6*state.grip;
ctx.fill();
ctx.shadowBlur=0;
}
} else {
const alpha=0.15+state.slide*0.15;
ctx.fillStyle=`rgba(13,148,136,${alpha})`;
ctx.fill();
}
ctx.strokeStyle='rgba(200,210,225,0.1)';
ctx.lineWidth=0.5;
ctx.stroke();
}
}
// 力矢量
if(CFG.showArrows){
for(let i=0;i<CFG.numSeg;i++){
const j1=joints[i], j2=joints[i+1];
const state=segState(i, time);
const segAng=Math.atan2(j2.y-j1.y, j2.x-j1.x);
const midX=(j1.x+j2.x)/2;
const midY=(j1.y+j2.y)/2;
if(state.grip>0.3){
// 锚定段:向后摩擦力(红色)+ 向前推进力(绿色)
const bnx=-Math.sin(segAng);
const bny= Math.cos(segAng);
const baseX=midX-bnx*CFG.bodyW*0.7;
const baseY=midY-bny*CFG.bodyW*0.7;
const fLen=18*state.grip;
// 摩擦力(向后,红色)
arrow(baseX, baseY,
baseX-Math.cos(segAng)*fLen,
baseY-Math.sin(segAng)*fLen,
`rgba(239,68,68,${0.4+state.grip*0.5})`, 1.8);
// 推进反力(向前,绿色)
arrow(baseX, baseY,
baseX+Math.cos(segAng)*fLen*0.85,
baseY+Math.sin(segAng)*fLen*0.85,
`rgba(16,185,129,${0.3+state.grip*0.6})`, 2.2);
}
}
}
// 蛇头
const hj=joints[0];
ctx.save();
ctx.translate(hj.x, hj.y);
ctx.rotate(hj.ang);
// 头部形状
ctx.beginPath();
ctx.moveTo(CFG.bodyW*0.5, 0);
ctx.quadraticCurveTo(CFG.bodyW*0.15, -CFG.bodyW*0.55, -CFG.bodyW*0.3, -CFG.bodyW*0.42);
ctx.lineTo(-CFG.bodyW*0.3, CFG.bodyW*0.42);
ctx.quadraticCurveTo(CFG.bodyW*0.15, CFG.bodyW*0.55, CFG.bodyW*0.5, 0);
ctx.closePath();
const headG=ctx.createRadialGradient(0,0,2,0,0,CFG.bodyW*0.5);
headG.addColorStop(0,'#a0b0c4');
headG.addColorStop(1,'#5a6a7e');
ctx.fillStyle=headG;
ctx.fill();
ctx.strokeStyle='#3a4a5a';
ctx.lineWidth=0.8;
ctx.stroke();
// 眼睛
ctx.fillStyle='#ef4444';
ctx.beginPath();ctx.arc(CFG.bodyW*0.12,-CFG.bodyW*0.18,2.2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(CFG.bodyW*0.12, CFG.bodyW*0.18,2.2,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#1a1a2e';
ctx.beginPath();ctx.arc(CFG.bodyW*0.14,-CFG.bodyW*0.18,1,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(CFG.bodyW*0.14, CFG.bodyW*0.18,1,0,Math.PI*2);ctx.fill();
ctx.restore();
// 蛇尾
const tj=joints[CFG.numSeg];
ctx.save();
ctx.translate(tj.x, tj.y);
ctx.rotate(tj.ang);
ctx.beginPath();
ctx.moveTo(0,-CFG.bodyW*0.35);
ctx.lineTo(CFG.bodyW*0.6,0);
ctx.lineTo(0,CFG.bodyW*0.35);
ctx.closePath();
ctx.fillStyle='#4a5a6a';
ctx.fill();
ctx.restore();
ctx.restore(); // 整体平移
}
/* ---------- 波形信号面板 ---------- */
function drawSignalPanel(){
const pw=180, ph=60;
const px=W-pw-18, py=14;
// 面板背景
ctx.fillStyle='rgba(10,15,28,0.85)';
roundRect(px,py,pw,ph,6);
ctx.fill();
ctx.strokeStyle='rgba(76,201,240,0.2)';
ctx.lineWidth=1;
ctx.stroke();
// 标签
ctx.fillStyle='rgba(76,201,240,0.6)';
ctx.font='500 9px "JetBrains Mono"';
ctx.textAlign='left';
ctx.fillText('INPUT SIGNAL', px+8, py+12);
// 正弦波
ctx.beginPath();
ctx.strokeStyle='#10b981';
ctx.lineWidth=1.5;
const wX=px+8, wY=py+36, wW=pw-16, wH=18;
for(let i=0;i<=wW;i++){
const t_local=i/wW*4*Math.PI;
const val=Math.sin(t_local - 2*Math.PI*CFG.waveFreq*time*2);
const y=wY-val*wH*0.4;
if(i===0)ctx.moveTo(wX+i,y);else ctx.lineTo(wX+i,y);
}
ctx.stroke();
// 扫描线
const scanX=wX+((time*CFG.waveFreq*80)%wW);
ctx.strokeStyle='rgba(16,185,129,0.5)';
ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(scanX,wY-wH);
ctx.lineTo(scanX,wY+wH);
ctx.stroke();
}
/* ---------- 鳞片机理详图 ---------- */
function drawDetailInset(){
const iw=200, ih=140;
const ix=16, iy=H-ih-12;
// 背景
ctx.fillStyle='rgba(10,15,28,0.9)';
roundRect(ix,iy,iw,ih,8);
ctx.fill();
ctx.strokeStyle='rgba(76,201,240,0.15)';
ctx.lineWidth=1;
ctx.stroke();
// 标题
ctx.fillStyle='rgba(76,201,240,0.6)';
ctx.font='500 9px "JetBrains Mono"';
ctx.textAlign='left';
ctx.fillText('SCALE MECHANISM', ix+8, iy+14);
const cy=iy+50;
const segW=70;
// === 锚定状态 ===
const ax=ix+35;
// 蛇身截面
ctx.fillStyle='#5a6878';
roundRect(ax-25,cy-8,50,16,3);
ctx.fill();
ctx.strokeStyle='#3a4a5a';
ctx.lineWidth=0.8;
ctx.stroke();
// 鳞片张开
const gripPhase=(Math.sin(time*3)*0.5+0.5);
const sAng1=Math.PI/2 + CFG.scaleAngle*Math.PI/180*0.6*(0.6+gripPhase*0.4);
const sLen=22;
ctx.beginPath();
ctx.moveTo(ax-4, cy+8);
ctx.lineTo(ax-4+Math.cos(sAng1)*sLen, cy+8+Math.sin(sAng1)*sLen);
ctx.lineTo(ax+4+Math.cos(sAng1)*sLen, cy+8+Math.sin(sAng1)*sLen);
ctx.lineTo(ax+4, cy+8);
ctx.closePath();
ctx.fillStyle=`rgba(245,158,11,${0.5+gripPhase*0.5})`;
ctx.fill();
if(gripPhase>0.3){
ctx.shadowColor='#f59e0b';
ctx.shadowBlur=5;
ctx.fill();
ctx.shadowBlur=0;
}
// 地面线
ctx.strokeStyle='#3a2f20';
ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(ax-30,cy+22);
ctx.lineTo(ax+30,cy+22);
ctx.stroke();
// 锚定标记
ctx.fillStyle='rgba(245,158,11,0.7)';
ctx.font='700 8px "Rajdhani"';
ctx.textAlign='center';
ctx.fillText('ANCHOR',ax,cy+35);
// 摩擦力箭头
arrow(ax-5,cy+20,ax-20,cy+20,`rgba(239,68,68,${0.5+gripPhase*0.3})`,1.5);
// 推力箭头
arrow(ax+5,cy+14,ax+22,cy+14,`rgba(16,185,129,${0.5+gripPhase*0.3})`,1.5);
// === 滑行状态 ===
const bx=ix+145;
// 蛇身截面
ctx.fillStyle='#5a6878';
roundRect(bx-25,cy-8,50,16,3);
ctx.fill();
ctx.strokeStyle='#3a4a5a';
ctx.lineWidth=0.8;
ctx.stroke();
// 鳞片折叠
const slidePhase=(Math.cos(time*3)*0.5+0.5);
const sAng2=Math.PI/2 - CFG.scaleAngle*Math.PI/180*0.5 - slidePhase*0.3;
ctx.beginPath();
ctx.moveTo(bx-3, cy+8);
ctx.lineTo(bx-3+Math.cos(sAng2)*sLen*0.8, cy+8+Math.sin(sAng2)*sLen*0.8);
ctx.lineTo(bx+3+Math.cos(sAng2)*sLen*0.8, cy+8+Math.sin(sAng2)*sLen*0.8);
ctx.lineTo(bx+3, cy+8);
ctx.closePath();
ctx.fillStyle=`rgba(13,148,136,${0.2+slidePhase*0.15})`;
ctx.fill();
// 地面线
ctx.strokeStyle='#3a2f20';
ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(bx-30,cy+22);
ctx.lineTo(bx+30,cy+22);
ctx.stroke();
// 滑行标记
ctx.fillStyle='rgba(13,148,136,0.7)';
ctx.font='700 8px "Rajdhani"';
ctx.textAlign='center';
ctx.fillText('GLIDE',bx,cy+35);
// 滑行方向箭头
arrow(bx-5,cy+14,bx+18,cy+14,`rgba(13,148,136,${0.3+slidePhase*0.2})`,1.3);
// 底部说明
ctx.fillStyle='rgba(200,210,225,0.3)';
ctx.font='300 8px "JetBrains Mono"';
ctx.textAlign='center';
ctx.fillText('← push: scale opens & grips slide: scale folds →',ix+iw/2,iy+ih-8);
}
/* ---------- IFR 原理标注 ---------- */
function drawAnnotations(joints){
const headX=cx; // 蛇头在画布中心附近
// 前进方向大箭头
const arrY=groundY - CFG.bodyW - 30;
const arrLen=50;
ctx.save();
ctx.globalAlpha=0.25+Math.sin(time*4)*0.1;
arrow(headX-arrLen, arrY, headX+arrLen, arrY, '#4cc9f0', 2.5);
ctx.restore();
ctx.fillStyle='rgba(76,201,240,0.4)';
ctx.font='500 10px "JetBrains Mono"';
ctx.textAlign='center';
ctx.fillText('FORWARD', headX, arrY-8);
// 波传播方向
const waveArrY=groundY + 50;
ctx.save();
ctx.globalAlpha=0.2;
arrow(cx+60, waveArrY, cx-60, waveArrY, '#f59e0b', 1.5);
ctx.restore();
ctx.fillStyle='rgba(245,158,11,0.3)';
ctx.font='400 9px "JetBrains Mono"';
ctx.textAlign='center';
ctx.fillText('WAVE PROPAGATION ←', cx, waveArrY-6);
}
/* ---------- 位移追踪线 ---------- */
let displHistory=[];
const maxDisplHist=200;
function drawDisplacementTrace(){
// 记录蛇头位移
const joints=computeJoints(time);
const headWorldX=worldX;
displHistory.push(headWorldX);
if(displHistory.length>maxDisplHist)displHistory.shift();
// 在画布底部绘制位移-时间曲线
const graphH=30;
const graphY=H-graphH-8;
const graphW=W*0.4;
const graphX=W*0.55;
ctx.fillStyle='rgba(10,15,28,0.6)';
roundRect(graphX,graphY-5,graphW,graphH+10,4);
ctx.fill();
ctx.fillStyle='rgba(76,201,240,0.35)';
ctx.font='500 8px "JetBrains Mono"';
ctx.textAlign='left';
ctx.fillText('DISPLACEMENT',graphX+6,graphY+4);
if(displHistory.length>2){
const minD=displHistory[0];
const maxD=displHistory[displHistory.length-1];
const range=Math.max(maxD-minD,1);
ctx.beginPath();
ctx.strokeStyle='rgba(16,185,129,0.6)';
ctx.lineWidth=1.5;
for(let i=0;i<displHistory.length;i++){
const x=graphX+6+(i/(maxDisplHist-1))*(graphW-12);
const y=graphY+graphH-2-((displHistory[i]-minD)/range)*(graphH-10);
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
ctx.stroke();
}
}
/* ---------- 主动画循环 ---------- */
function animate(ts){
if(!lastTS)lastTS=ts;
const dt=Math.min((ts-lastTS)/1000, 0.05);
lastTS=ts;
// 前进速度与波参数关联
const fwdSpeed=CFG.waveAmp*CFG.waveFreq*0.32*CFG.segLen/18;
worldX+=fwdSpeed*60*dt;
time+=dt;
// 计算关节
const joints=computeJoints(time);
// 清空画布
ctx.clearRect(0,0,W,H);
// 绘制各层
drawBG();
drawGround();
drawAnnotations(joints);
drawSnake(joints);
drawSignalPanel();
drawDetailInset();
drawDisplacementTrace();
requestAnimationFrame(animate);
}
/* ---------- 启动 ---------- */
document.addEventListener('DOMContentLoaded', ()=>{
resize();
requestAnimationFrame(animate);
});
/* 重开即播:处理 pageshow 事件(含 bfcache 恢复) */
window.addEventListener('pageshow', ()=>{
lastTS=0;
time=0;
worldX=0;
displHistory=[];
});
</script>
</body>
</html>
实现说明:
核心原理可视化:蛇身由 14 节刚性连杆通过关节链式串联,关节角度服从行波方程
θ_i(t) = A·sin(2πft - ki)。鳞片状态(锚定/滑行)由关节角速度方向自动判定——角速度为正时鳞片张开抓地(琥珀色高亮+辉光),角速度为负时鳞片折叠滑行(青色淡化),无需任何软件闭环控制。IFR 体现:画面直接展示"正弦波信号输入 → 物理结构自锁 → 前进位移输出"的完整链路。右上角信号面板显示简单正弦输入,右下角位移曲线显示匀速前进输出——中间零算法开销,由鳞片的单向力学特性自动完成摩擦不对称转换。
视觉引导:
- 鳞片辉光:锚定态鳞片以琥珀色发光标记,滑行态以暗青色淡化,对比鲜明
- 力矢量:锚定段同时显示红色摩擦力(向后)和绿色推进力(向前),直观展示力学转化
- 波形传播:底部标注波传播方向(向后)与蛇前进方向(向前)的对比
交互控制:四个滑块分别控制波动频率、幅度、鳞片后掠角和力矢量显示开关,调节任一参数即可实时观察蛇身步态和推进效率的变化。
细节插图:左下角鳞片机理详图以截面视角动态演示"锚定→鳞片张开抓地"与"滑行→鳞片折叠顺滑"两种状态的交替过程。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
