独立渲染引擎就绪引擎就绪
<!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=Chakra+Petch:wght@300;500;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#060a14;--fg:#d0d8e8;--muted:#3e4d65;--accent:#f97316;--card:#0b1018;--border:#1a2540;--grip:#f97316;--slide:#06b6d4;--thrust:#eab308;--body-c:#5a7090}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:1.5rem;letter-spacing:.08em;color:var(--fg);margin:18px 0 6px;text-align:center}
.sub{font-size:.72rem;color:var(--muted);text-align:center;margin-bottom:10px;letter-spacing:.03em;max-width:700px;line-height:1.5}
#wrap{width:96vw;max-width:1280px;aspect-ratio:16/9;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--card);box-shadow:0 0 50px rgba(249,115,22,.04),0 0 100px rgba(6,182,212,.03)}
#main-svg{width:100%;height:100%;display:block}
.ctrls{display:flex;gap:20px;flex-wrap:wrap;justify-content:center;margin:14px 0 6px;padding:0 16px;max-width:1280px}
.cg{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:130px}
.cg label{font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
.cg input[type=range]{width:110px;accent-color:var(--accent);cursor:pointer}
.cg .v{font-size:.8rem;color:var(--accent);font-weight:500}
.drow{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-bottom:14px}
.di{font-size:.72rem;color:var(--muted);display:flex;align-items:center;gap:5px}
.di .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.di .vl{color:var(--fg);font-weight:500}
</style>
</head>
<body>
<h1>机械蛇 · 单向阻力鳞片推进原理</h1>
<p class="sub">各向异性摩擦将双向弯曲单向转化为前进推力 — TRIZ 最终理想解 (IFR) 演示</p>
<div id="wrap"><svg id="main-svg" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg"></svg></div>
<div class="ctrls">
<div class="cg"><label>波速 ω</label><input type="range" id="c-spd" min="0.5" max="4" step="0.1" value="2.0"><span class="v" id="v-spd">2.0</span></div>
<div class="cg"><label>振幅 A</label><input type="range" id="c-amp" min="15" max="65" step="5" value="45"><span class="v" id="v-amp">45</span></div>
<div class="cg"><label>短节数 N</label><input type="range" id="c-seg" min="8" max="15" step="1" value="12"><span class="v" id="v-seg">12</span></div>
<div class="cg"><label>摩擦比 μ</label><input type="range" id="c-fr" min="2" max="6" step="0.5" value="3"><span class="v" id="v-fr">3.0</span></div>
</div>
<div class="drow">
<div class="di"><span class="dot" style="background:var(--grip)"></span>抓地: <span class="vl" id="d-grip">0</span>节</div>
<div class="di"><span class="dot" style="background:var(--slide)"></span>滑行: <span class="vl" id="d-slide">0</span>节</div>
<div class="di"><span class="dot" style="background:var(--thrust)"></span>推进: <span class="vl" id="d-spd">0</span>mm/s</div>
<div class="di">行程: <span class="vl" id="d-dist">0</span>mm</div>
</div>
<script>
(function(){
/* ====== 配置 ====== */
const CFG={numSeg:12,segLen:46,segW:26,amp:45,waveLen:280,omega:2.0,fwdSpd:22,friction:3,cx:580,cy:260};
let groundOffset=0,totalDist=0;
/* ====== SVG 工具 ====== */
const NS="http://www.w3.org/2000/svg";
const svg=document.getElementById("main-svg");
function el(tag,attrs,parent){
const e=document.createElementNS(NS,tag);
for(const k in attrs)e.setAttribute(k,attrs[k]);
if(parent)parent.appendChild(e);
return e;
}
/* ====== Defs ====== */
const defs=el("defs",{},svg);
// 发光滤镜-抓地
const fGrip=el("filter",{id:"fg",x:"-60%",y:"-60%",width:"220%",height:"220%"},defs);
el("feGaussianBlur",{stdDeviation:"5",result:"b"},fGrip);
const mGrip=el("feMerge",{},fGrip);el("feMergeNode",{in:"b"},mGrip);el("feMergeNode",{in:"SourceGraphic"},mGrip);
// 发光滤镜-推力
const fThrust=el("filter",{id:"ft",x:"-60%",y:"-60%",width:"220%",height:"220%"},defs);
el("feGaussianBlur",{stdDeviation:"3",result:"b"},fThrust);
const mThrust=el("feMerge",{},fThrust);el("feMergeNode",{in:"b"},mThrust);el("feMergeNode",{in:"SourceGraphic"},mThrust);
// 阴影
const fShadow=el("filter",{id:"fs",x:"-20%",y:"-20%",width:"140%",height:"160%"},defs);
el("feDropShadow",{dx:"0",dy:"4",stdDeviation:"6","flood-color":"#000000","flood-opacity":"0.5"},fShadow);
// 渐变-身体
const gBody=el("linearGradient",{id:"gb",x1:"0",y1:"0",x2:"0",y2:"1"},defs);
el("stop",{offset:"0%","stop-color":"#7a90aa"},gBody);el("stop",{offset:"100%","stop-color":"#3e5470"},gBody);
// 渐变-地面
const gGnd=el("linearGradient",{id:"gg",x1:"0",y1:"0",x2:"0",y2:"1"},defs);
el("stop",{offset:"0%","stop-color":"#141e30"},gGnd);el("stop",{offset:"100%","stop-color":"#0a0f18"},gGnd);
// 箭头标记
const mkT=el("marker",{id:"at",viewBox:"0 0 10 6",refX:"10",refY:"3",markerWidth:"8",markerHeight:"5",orient:"auto"},defs);
el("path",{d:"M0,0 L10,3 L0,6 Z",fill:"#eab308"},mkT);
const mkF=el("marker",{id:"af",viewBox:"0 0 10 6",refX:"10",refY:"3",markerWidth:"7",markerHeight:"4",orient:"auto"},defs);
el("path",{d:"M0,0 L10,3 L0,6 Z",fill:"#f97316"},mkF);
/* ====== 静态背景 ====== */
el("rect",{width:1280,height:720,fill:"#0b1018"},svg);
// 网格
const gridG=el("g",{opacity:"0.08"},svg);
for(let x=0;x<=1280;x+=40)el("line",{x1:x,y1:0,x2:x,y2:720,stroke:"#4080c0","stroke-width":"0.5"},gridG);
for(let y=0;y<=720;y+=40)el("line",{x1:0,y1:y,x2:1280,y2:y,stroke:"#4080c0","stroke-width":"0.5"},gridG);
// 地面
el("rect",{x:0,y:385,width:1280,height:335,fill:"url(#gg)",opacity:"0.6"},svg);
el("line",{x1:0,y1:385,x2:1280,y2:385,stroke:"#1e3050","stroke-width":"1.5"},svg);
/* ====== 动态层 ====== */
const gMarkers=el("g",{},svg); // 地面滚动标记
const gShadow=el("g",{opacity:"0.25",filter:"url(#fs)"},svg);
const gBody=el("g",{},svg); // 蛇身
const gScales=el("g",{},svg); // 鳞片
const gArrows=el("g",{},svg); // 推力箭头
const gParts=el("g",{},svg); // 抓地粒子
const gPhase=el("g",{transform:"translate(70,460)"},svg); // 相位条
const gDetail=el("g",{transform:"translate(850,500)"},svg); // 截面详图
const gIFR=el("g",{transform:"translate(40,500)"},svg); // IFR说明
/* ====== 预创建元素 ====== */
// 地面标记线
const markers=[];
for(let i=0;i<40;i++){
const m=el("line",{y1:387,y2:405,stroke:"#2a3a58","stroke-width":"1.5"},gMarkers);
markers.push(m);
}
// 身体路径
const bodyPath=el("path",{fill:"url(#gb)",stroke:"#3a506e","stroke-width":"1.5",d:""},gBody);
const shadowPath=el("path",{fill:"#000000",d:"",opacity:"0.35"},gShadow);
// 身体脊线
const spinePath=el("path",{fill:"none",stroke:"#8aa0ba","stroke-width":"1","stroke-dasharray":"4 3",d:"",opacity:"0.4"},gBody);
// 鳞片元素(最多15段×每段4个鳞片×2侧)
const MAX_SEG=15, SCALES_PER_SEG=4;
const scaleEls=[];
for(let i=0;i<MAX_SEG;i++){
const row=[];
for(let j=0;j<SCALES_PER_SEG;j++){
const s=el("polygon",{points:"0,0 -5,4 0,3 5,4",fill:"#06b6d4",opacity:"0.8"},gScales);
row.push(s);
}
scaleEls.push(row);
}
// 关节圆
const jointEls=[];
for(let i=0;i<MAX_SEG;i++){
const j=el("circle",{r:"4",fill:"#2a3a58",stroke:"#4a6080","stroke-width":"1"},gBody);
jointEls.push(j);
}
// 头部标记
const headEl=el("ellipse",{rx:"14",ry:"10",fill:"#5a7090",stroke:"#8aa0ba","stroke-width":"1.5"},gBody);
const headEye1=el("circle",{r:"2.5",fill:"#e0e8f0"},gBody);
const headEye2=el("circle",{r:"2.5",fill:"#e0e8f0"},gBody);
// 推力箭头
const arrowEls=[];
for(let i=0;i<MAX_SEG;i++){
const a=el("line",{x1:"0",y1:"0",x2:"0",y2:"0",stroke:"#eab308","stroke-width":"2.5","marker-end":"url(#at)",opacity:"0",filter:"url(#ft)"},gArrows);
arrowEls.push(a);
}
// 抓地粒子
const particles=[];
for(let i=0;i<60;i++){
const p=el("circle",{r:"1.5",fill:"#f97316",opacity:"0"},gParts);
particles.push({el:p,life:0,x:0,y:0,vx:0,vy:0});
}
/* ====== 相位图 ====== */
el("rect",{x:0,y:0,width:1100,height:50,rx:"6",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1"},gPhase);
el("text",{x:"8",y:"15","font-size":"10",fill:"#4a5e78","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gPhase).textContent="鳞片状态相位图 ▸ 波形传播方向";
const phaseBars=[];
for(let i=0;i<MAX_SEG;i++){
const b=el("rect",{x:0,y:20,width:0,height:22,rx:"3",fill:"#06b6d4"},gPhase);
const t=el("text",{y:"35","font-size":"8",fill:"#c0d0e0","text-anchor":"middle","font-family":"'IBM Plex Mono'"},gPhase);
t.textContent=""+(i+1);
phaseBars.push({bar:b,label:t});
}
/* ====== 截面详图 ====== */
el("rect",{x:0,y:0,width:390,height:200,rx:"8",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1.5"},gDetail);
el("text",{x:"12",y:"18","font-size":"11",fill:"#5a7090","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gDetail).textContent="鳞片截面原理 (侧视)";
// 地面线
el("line",{x1:30,y1:160,x2:360,y2:160,stroke:"#2a3a58","stroke-width":"2"},gDetail);
el("text",{x:45,y:178,"font-size":"9",fill:"#3e5068","font-family":"'IBM Plex Mono'"},gDetail).textContent="地面";
// 段体
const detailSeg=el("rect",{x:100,y:105,width:160,height:35,rx:"4",fill:"#4a5e78",stroke:"#6a8098","stroke-width":"1"},gDetail);
// 鳞片组(2个状态)
const detailScaleGrip=el("g",{},gDetail);
const detailScaleSlide=el("g",{opacity:"0"},gDetail);
// 抓地鳞片
const scG1=el("polygon",{points:"120,140 108,155 120,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG2=el("polygon",{points:150,140 138,155 150,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG3=el("polygon",{points:180,140 168,155 180,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
const scG4=el("polygon",{points:210,140 198,155 210,148",fill:"#f97316",stroke:"#c2590a","stroke-width":"0.8"},detailScaleGrip);
// 抓地标注
el("text",{x:260,y:132,"font-size":"10",fill:"#f97316","font-family":"'Chakra Petch',sans-serif","font-weight":"700"},detailScaleGrip).textContent="GRIP 抓地";
el("text",{x:260,y:148,"font-size":"8",fill:"#8a6040","font-family":"'IBM Plex Mono'"},detailScaleGrip).textContent="鳞片刚硬卡地";
el("text",{x:260,y:160,"font-size":"8",fill:"#8a6040","font-family":"'IBM Plex Mono'"},detailScaleGrip).textContent="→ 高摩擦防后退";
// 抓地力箭头
const gripForceArr=el("line",{x1:140,y1:155,x2:140,y2:172,stroke:"#f97316","stroke-width":"2","marker-end":"url(#af)"},detailScaleGrip);
// 滑行鳞片
const scS1=el("polygon",{points:"120,138 115,143 120,140 125,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS2=el("polygon",{points:"150,138 145,143 150,140 155,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS3=el("polygon",{points:"180,138 175,143 180,140 185,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
const scS4=el("polygon",{points:"210,138 205,143 210,140 215,143",fill:"#06b6d4",stroke:"#0891a2","stroke-width":"0.8"},detailScaleSlide);
// 滑行标注
el("text",{x:260,y:132,"font-size":"10",fill:"#06b6d4","font-family":"'Chakra Petch',sans-serif","font-weight":"700"},detailScaleSlide).textContent="SLIDE 滑行";
el("text",{x:260,y:148,"font-size":"8",fill:"#2a6a7a","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="鳞片顺势倒伏";
el("text",{x:260,y:160,"font-size":"8",fill:"#2a6a7a","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="→ 低摩擦易前滑";
// 运动方向箭头
const slideDirArr=el("line",{x1:105,y1:95,x2:75,y2:95,stroke:"#06b6d4","stroke-width":"1.5","marker-end":"url(#af)","stroke-dasharray":"4 2"},detailScaleSlide);
el("text",{x:80,y:88,"font-size":"8",fill:"#06b6d4","font-family":"'IBM Plex Mono'"},detailScaleSlide).textContent="前滑方向";
/* ====== IFR说明框 ====== */
el("rect",{x:0,y:0,width:380,height:200,rx:"8",fill:"#080e1a",stroke:"#1a2540","stroke-width":"1.5"},gIFR);
el("text",{x:"12",y:"18","font-size":"11",fill:"#5a7090","font-family":"'Chakra Petch',sans-serif","font-weight":"500"},gIFR).textContent="IFR 最终理想解核心";
const ifrLines=[
{t:"矛盾:弯曲运动双向,推力需单向",y:40,c:"#8a9ab0"},
{t:"理想解:鳞片 = 零成本单向阀",y:60,c:"#f97316"},
{t:"",y:78,c:""},
{t:"▸ 复用资源:地面摩擦力",y:95,c:"#eab308"},
{t:" (原本是阻力 → 转为推力来源)",y:112,c:"#8a7a40"},
{t:"▸ 极简增量:仅改变表面纹理方向",y:130,c:"#eab308"},
{t:" (不增加主动机构或能源消耗)",y:147,c:"#8a7a40"},
{t:"▸ 自消除矛盾:后退时高阻/前滑时低阻",y:165,c:"#06b6d4"},
{t:" (同一结构在不同运动方向自动切换)",y:182,c:"#2a6a7a"},
];
ifrLines.forEach(l=>{
if(l.t)el("text",{x:"16",y:l.y,"font-size":l.c==="#f97316"||l.c==="#eab308"?"10":"9",fill:l.c,"font-family":"'IBM Plex Mono'","font-weight":l.c==="#f97316"?"700":"400"},gIFR).textContent=l.t;
});
/* ====== 波形方向标注 ====== */
const waveLabel=el("text",{x:CFG.cx+80,y:CFG.cy-80,"font-size":"11",fill:"#4a6080","font-family":"'Chakra Petch',sans-serif","font-weight":"500",opacity:"0.7"},svg);
waveLabel.textContent="▸ 波形传播方向 头 → 尾";
/* ====== 状态计算 ====== */
function computeSnake(t){
const N=CFG.numSeg, L=CFG.segLen, A=CFG.amp, wl=CFG.waveLen, w=CFG.omega;
const segs=[];
const totalLen=N*L;
for(let i=0;i<N;i++){
const s=i*L;
const phase=(2*Math.PI*s/wl)-w*t;
const x=CFG.cx-s;
const y=CFG.cy+A*Math.sin(phase);
const dyds=A*(2*Math.PI/wl)*Math.cos(phase);
const angle=Math.atan2(dyds,-1);
const dydt=-A*w*Math.cos(phase);
const yPos=A*Math.sin(phase);
const product=yPos*dydt; // >0 推离中心=抓地, <0 回归中心=滑行
const maxProd=A*A*w*0.5;
const gripI=Math.max(0,product/maxProd);
const slideI=Math.max(0,-product/maxProd);
segs.push({x,y,angle,phase,gripI,slideI,isGrip:gripI>0.25});
}
return segs;
}
/* ====== 身体轮廓 ====== */
function buildOutline(segs){
const hw=CFG.segW/2;
const pts=segs.length;
// 增加首尾锥形
const left=[],right=[];
for(let i=0;i<pts;i++){
const s=segs[i];
const nx=-Math.sin(s.angle),ny=Math.cos(s.angle);
const taper=i<2?(0.6+0.2*i):i>pts-3?(0.6+0.2*(pts-1-i)):1;
const w=hw*taper;
left.push({x:s.x+nx*w,y:s.y+ny*w});
right.push({x:s.x-nx*w,y:s.y-ny*w});
}
// 头部尖端
const h=segs[0];
const hx=h.x+Math.cos(h.angle)*18,hy=h.y+Math.sin(h.angle)*18;
// 尾部尖端
const tl=segs[pts-1];
const tx=tl.x-Math.cos(tl.angle)*14,ty=tl.y-Math.sin(tl.angle)*14;
let d=`M${tx} ${ty} `;
for(let i=pts-1;i>=0;i--)d+=`L${left[i].x} ${left[i].y} `;
d+=`L${hx} ${hy} `;
for(let i=0;i<pts;i++)d+=`L${right[i].x} ${right[i].y} `;
d+="Z";
return d;
}
/* ====== 更新函数 ====== */
let prevTime=0, particleIdx=0;
function update(ts){
const t=ts/1000;
const dt=Math.min(t-prevTime,0.05);
prevTime=t;
const N=CFG.numSeg;
const segs=computeSnake(t);
// 推进距离
const fwdInc=CFG.fwdSpd*(CFG.amp/45)*(CFG.omega/2)*dt;
totalDist+=fwdInc;
groundOffset+=fwdInc*2;
// 地面标记
const spacing=60;
for(let i=0;i<markers.length;i++){
const rawX=((i*spacing-groundOffset)%1280+1280)%1280;
markers[i].setAttribute("x1",rawX);
markers[i].setAttribute("x2",rawX);
// 颜色根据蛇下方的位置变化
markers[i].setAttribute("stroke","#2a3a58");
}
// 身体轮廓
const outlineD=buildOutline(segs);
bodyPath.setAttribute("d",outlineD);
// 阴影(向下偏移)
const shadowSegs=segs.map(s=>({...s,y:s.y+12}));
shadowPath.setAttribute("d",buildOutline(shadowSegs));
// 脊线
let spineD=`M${segs[0].x} ${segs[0].y}`;
for(let i=1;i<N;i++)spineD+=` L${segs[i].x} ${segs[i].y}`;
spinePath.setAttribute("d",spineD);
// 关节
for(let i=0;i<MAX_SEG;i++){
if(i<N){
jointEls[i].setAttribute("cx",segs[i].x);
jointEls[i].setAttribute("cy",segs[i].y);
jointEls[i].setAttribute("opacity","1");
}else{
jointEls[i].setAttribute("opacity","0");
}
}
// 头部
headEl.setAttribute("cx",segs[0].x+Math.cos(segs[0].angle)*8);
headEl.setAttribute("cy",segs[0].y+Math.sin(segs[0].angle)*8);
headEl.setAttribute("transform",`rotate(${segs[0].angle*180/Math.PI},${segs[0].x+Math.cos(segs[0].angle)*8},${segs[0].y+Math.sin(segs[0].angle)*8})`);
const ex1=segs[0].x+Math.cos(segs[0].angle)*14-Math.sin(segs[0].angle)*5;
const ey1=segs[0].y+Math.sin(segs[0].angle)*14+Math.cos(segs[0].angle)*5;
const ex2=segs[0].x+Math.cos(segs[0].angle)*14+Math.sin(segs[0].angle)*5;
const ey2=segs[0].y+Math.sin(segs[0].angle)*14-Math.cos(segs[0].angle)*5;
headEye1.setAttribute("cx",ex1);headEye1.setAttribute("cy",ey1);
headEye2.setAttribute("cx",ex2);headEye2.setAttribute("cy",ey2);
// 鳞片
for(let i=0;i<MAX_SEG;i++){
for(let j=0;j<SCALES_PER_SEG;j++){
const s=scaleEls[i][j];
if(i>=N){s.setAttribute("opacity","0");continue;}
const seg=segs[i];
// 在段体底部排列鳞片(沿段体方向等间距)
const along=-0.3+0.2*j; // 沿体轴的偏移比例
const sx=seg.x+Math.cos(seg.angle)*along*CFG.segLen;
const sy=seg.y+Math.sin(seg.angle)*along*CFG.segLen;
// 法向偏移(身体下方)
const bnx=Math.sin(seg.angle),bny=-Math.cos(seg.angle);
const offY=8;
const px=sx+bnx*offY;
const py=sy+bny*offY;
const rot=seg.angle*180/Math.PI;
// 颜色
const gI=seg.gripI,sI=seg.slideI;
let fill,op;
if(gI>0.25){
const r=Math.round(6+249*gI),g=Math.round(182-67*gI),b=Math.round(212-190*gI);
fill=`rgb(${r},${g},${b})`;
op=0.5+0.5*gI;
}else if(sI>0.15){
fill="#06b6d4";
op=0.4+0.4*sI;
}else{
fill="#2a4a5a";op=0.3;
}
s.setAttribute("transform",`translate(${px},${py}) rotate(${rot})`);
s.setAttribute("fill",fill);
s.setAttribute("opacity",op);
}
}
// 推力箭头
let gripCount=0,slideCount=0;
for(let i=0;i<MAX_SEG;i++){
if(i>=N){arrowEls[i].setAttribute("opacity","0");continue;}
const seg=segs[i];
if(seg.isGrip){
gripCount++;
const len=20+30*seg.gripI;
const ax=seg.x+Math.cos(seg.angle)*len;
const ay=seg.y+Math.sin(seg.angle)*len;
arrowEls[i].setAttribute("x1",seg.x);
arrowEls[i].setAttribute("y1",seg.y);
arrowEls[i].setAttribute("x2",ax);
arrowEls[i].setAttribute("y2",ay);
arrowEls[i].setAttribute("opacity",""+(0.4+0.6*seg.gripI));
// 抓地粒子
if(seg.gripI>0.6&&Math.random()<0.3){
const pi=particleIdx%particles.length;
particles[pi].x=seg.x+Math.sin(seg.angle)*(5+Math.random()*10);
particles[pi].y=seg.y-Math.cos(seg.angle)*(5+Math.random()*10);
particles[pi].vx=(Math.random()-0.5)*15;
particles[pi].vy=-Math.random()*10-5;
particles[pi].life=1;
particleIdx++;
}
}else{
slideCount++;
arrowEls[i].setAttribute("opacity","0");
}
}
// 粒子更新
for(const p of particles){
if(p.life>0){
p.life-=dt*2;
p.x+=p.vx*dt;
p.y+=p.vy*dt;
p.vy+=30*dt;
p.el.setAttribute("cx",p.x);
p.el.setAttribute("cy",p.y);
p.el.setAttribute("opacity",""+Math.max(0,p.life*0.8));
p.el.setAttribute("r",""+(1+p.life));
}else{
p.el.setAttribute("opacity","0");
}
}
// 相位图
const barW=Math.min(80,1000/N-4);
const barGap=4;
const totalW=N*(barW+barGap);
const startX=(1100-totalW)/2;
for(let i=0;i<MAX_SEG;i++){
if(i<N){
const seg=segs[i];
const bx=startX+i*(barW+barGap);
phaseBars[i].bar.setAttribute("x",bx);
phaseBars[i].bar.setAttribute("width",barW);
phaseBars[i].bar.setAttribute("y","20");
phaseBars[i].bar.setAttribute("height","22");
// 颜色
if(seg.isGrip){
phaseBars[i].bar.setAttribute("fill","#f97316");
phaseBars[i].bar.setAttribute("opacity",""+(0.5+0.5*seg.gripI));
}else{
phaseBars[i].bar.setAttribute("fill","#06b6d4");
phaseBars[i].bar.setAttribute("opacity",""+(0.3+0.5*seg.slideI));
}
phaseBars[i].label.setAttribute("x",bx+barW/2);
phaseBars[i].label.setAttribute("opacity","1");
}else{
phaseBars[i].bar.setAttribute("width","0");
phaseBars[i].label.setAttribute("opacity","0");
}
}
// 截面详图 - 切换抓地/滑行状态
const detailPhase=Math.sin(t*CFG.omega);
if(detailPhase>0.1){
detailScaleGrip.setAttribute("opacity","1");
detailScaleSlide.setAttribute("opacity","0");
}else if(detailPhase<-0.1){
detailScaleGrip.setAttribute("opacity","0");
detailScaleSlide.setAttribute("opacity","1");
}else{
detailScaleGrip.setAttribute("opacity","0.5");
detailScaleSlide.setAttribute("opacity","0.5");
}
// 数据显示
document.getElementById("d-grip").textContent=gripCount;
document.getElementById("d-slide").textContent=slideCount;
document.getElementById("d-spd").textContent=Math.round(fwdInc/dt*10||0);
document.getElementById("d-dist").textContent=Math.round(totalDist);
requestAnimationFrame(update);
}
/* ====== 控件绑定 ====== */
function bindCtrl(id,valId,fn){
const inp=document.getElementById(id);
const val=document.getElementById(valId);
inp.addEventListener("input",()=>{val.textContent=inp.value;fn(parseFloat(inp.value));});
}
bindCtrl("c-spd","v-spd",v=>{CFG.omega=v;});
bindCtrl("c-amp","v-amp",v=>{CFG.amp=v;});
bindCtrl("c-seg","v-seg",v=>{CFG.numSeg=Math.round(v);});
bindCtrl("c-fr","v-fr",v=>{CFG.friction=v;});
/* ====== 启动 ====== */
requestAnimationFrame(update);
})();
</script>
</body>
</html>
实现说明:
核心物理建模:蛇身由 N 个刚性短节通过正弦行波串联而成。每节的位置由
y = A·sin(2πs/λ − ωt)决定,通过y × dy/dt的符号判断鳞片状态——乘积为正表示推离中心线(抓地发力),为负表示回归中心线(滑行复位),由此实现各向异性摩擦的可视化。IFR 理想解聚焦:动画直接展示理想工作状态——鳞片在侧推时自动刚硬卡地(橙色高亮+推力箭头),前滑时自动倒伏(青色低阻),无需额外主动机构。左下角 IFR 框用文字明确标注"零成本单向阀"的核心创新:复用地面摩擦力资源、仅改变表面纹理方向即消除矛盾。
视觉引导:
- 颜色编码:橙色 = 抓地高摩擦,青色 = 滑行低摩擦,金色箭头 = 推力方向
- 粒子特效:抓地瞬间溅出橙色粒子,强化"力传递到地面"的直觉
- 相位图:底部条形图实时显示每节鳞片状态,波形传播一目了然
- 截面详图:右下角侧视动画在抓地/滑行两种状态间切换,展示鳞片倾角变化的物理机制
交互控制:四个滑块(波速 ω、振幅 A、短节数 N、摩擦比 μ)允许用户实时调节参数,观察推进效果的变化——例如降低摩擦比后抓地力减弱、减小振幅后推力下降等。
自动播放:
requestAnimationFrame在页面加载后立即启动动画循环,无需任何手动触发。
<!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 href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;500;700;900&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root {
--bg: #060a12;
--surface: #0c1220;
--card: #0f1829;
--border: #1a2a42;
--fg: #c8d6e5;
--muted: #5a6e84;
--accent: #00e5c7;
--accent-dim: rgba(0,229,199,0.15);
--amber: #ff9f1c;
--amber-dim: rgba(255,159,28,0.15);
--red: #ff4757;
--green: #2ed573;
--green-dim: rgba(46,213,115,0.2);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
background: var(--bg);
color: var(--fg);
font-family: 'IBM Plex Mono', monospace;
min-height: 100vh;
overflow-x: hidden;
}
/* 噪点纹理叠加 */
body::before {
content: '';
position: fixed; inset: 0;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 9999;
}
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: 100vh;
}
header {
display: flex;
align-items: baseline;
gap: 16px;
flex-wrap: wrap;
}
header h1 {
font-family: 'Outfit', sans-serif;
font-weight: 900;
font-size: 28px;
letter-spacing: -0.5px;
color: var(--accent);
line-height: 1.1;
}
header .subtitle {
font-size: 13px;
color: var(--muted);
font-weight: 400;
}
.ifrs-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--amber-dim);
border: 1px solid rgba(255,159,28,0.3);
color: var(--amber);
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
}
.main-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
flex: 1;
}
@media (max-width: 960px) {
.main-layout {
grid-template-columns: 1fr;
}
}
.canvas-wrapper {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
min-height: 480px;
}
.canvas-wrapper canvas {
display: block;
width: 100%;
height: 100%;
}
/* 左上角图例 */
.legend {
position: absolute;
top: 12px; left: 12px;
display: flex;
flex-direction: column;
gap: 6px;
z-index: 10;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--muted);
}
.legend-dot {
width: 10px; height: 10px;
border-radius: 2px;
flex-shrink: 0;
}
/* 侧面板 */
.side-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
}
.panel-card h3 {
font-family: 'Outfit', sans-serif;
font-weight: 700;
font-size: 14px;
color: var(--fg);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.panel-card h3 i {
color: var(--accent);
font-size: 13px;
}
.detail-canvas-wrapper {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16/10;
}
.detail-canvas-wrapper canvas {
display: block;
width: 100%;
height: 100%;
}
.detail-label {
text-align: center;
font-size: 10px;
color: var(--muted);
margin-top: 8px;
letter-spacing: 0.5px;
}
/* 滑块控件 */
.control-group {
margin-bottom: 14px;
}
.control-group:last-child { margin-bottom: 0; }
.control-label {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 6px;
}
.control-label span:first-child {
font-size: 12px;
color: var(--fg);
}
.control-label .value {
font-size: 12px;
color: var(--accent);
font-weight: 600;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: var(--border);
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px; height: 16px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 8px rgba(0,229,199,0.4);
}
/* 指标 */
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.metric-item {
background: var(--surface);
border-radius: 8px;
padding: 10px;
text-align: center;
}
.metric-item .metric-value {
font-family: 'Outfit', sans-serif;
font-weight: 700;
font-size: 22px;
color: var(--accent);
line-height: 1.2;
}
.metric-item .metric-label {
font-size: 10px;
color: var(--muted);
margin-top: 2px;
letter-spacing: 0.3px;
}
.metric-item.amber .metric-value { color: var(--amber); }
.metric-item.green .metric-value { color: var(--green); }
/* IFR 注释条 */
.ifr-insight {
background: linear-gradient(135deg, var(--amber-dim), var(--accent-dim));
border: 1px solid rgba(255,159,28,0.2);
border-radius: 10px;
padding: 14px 16px;
font-size: 12px;
line-height: 1.6;
color: var(--fg);
}
.ifr-insight strong {
color: var(--amber);
}
.ifr-insight .highlight {
color: var(--accent);
font-weight: 600;
}
/* 底部说明 */
.footer-note {
text-align: center;
font-size: 11px;
color: var(--muted);
padding: 8px 0;
letter-spacing: 0.3px;
}
/* 动画 - 脉冲指示 */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(0,229,199,0.2); }
50% { box-shadow: 0 0 12px 2px rgba(0,229,199,0.3); }
}
.pulse-border {
animation: pulse-glow 2s ease-in-out infinite;
}
/* 减弱动画偏好 */
@media (prefers-reduced-motion: reduce) {
.pulse-border { animation: none; }
}
</style>
</head>
<body>
<div class="app-container">
<header>
<h1>单向阻力鳞片底盘</h1>
<span class="subtitle">机械蛇各向异性摩擦推进原理</span>
<span class="ifrs-badge"><i class="fas fa-bolt"></i> IFRS — 理想最终解</span>
</header>
<div class="main-layout">
<!-- 主画布 -->
<div class="canvas-wrapper" id="canvasWrapper">
<canvas id="mainCanvas"></canvas>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div>鳞片抓地(高摩擦)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>鳞片倒伏(低摩擦)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div>推进力方向</div>
</div>
</div>
<!-- 侧面板 -->
<div class="side-panel">
<!-- 鳞片细节 -->
<div class="panel-card pulse-border">
<h3><i class="fas fa-microscope"></i>鳞片机构剖面</h3>
<div class="detail-canvas-wrapper">
<canvas id="detailCanvas"></canvas>
</div>
<div class="detail-label">侧视图 · 鳞片棘爪状态切换</div>
</div>
<!-- 控制面板 -->
<div class="panel-card">
<h3><i class="fas fa-sliders-h"></i>参数控制</h3>
<div class="control-group">
<div class="control-label"><span>波幅</span><span class="value" id="ampVal">0.35 rad</span></div>
<input type="range" id="ampSlider" min="10" max="55" value="35" step="1">
</div>
<div class="control-group">
<div class="control-label"><span>波速</span><span class="value" id="speedVal">1.8 rad/s</span></div>
<input type="range" id="speedSlider" min="5" max="35" value="18" step="1">
</div>
<div class="control-group">
<div class="control-label"><span>鳞片摩擦比</span><span class="value" id="fricVal">3.0 : 1</span></div>
<input type="range" id="fricSlider" min="15" max="60" value="30" step="1">
</div>
</div>
<!-- 实时指标 -->
<div class="panel-card">
<h3><i class="fas fa-tachometer-alt"></i>实时指标</h3>
<div class="metrics-grid">
<div class="metric-item">
<div class="metric-value" id="metricSpeed">0</div>
<div class="metric-label">前进速度 mm/s</div>
</div>
<div class="metric-item amber">
<div class="metric-value" id="metricDist">0</div>
<div class="metric-label">总行程 mm</div>
</div>
<div class="metric-item green">
<div class="metric-value" id="metricGrip">0</div>
<div class="metric-label">当前抓地段</div>
</div>
<div class="metric-item">
<div class="metric-value" id="metricWave">0</div>
<div class="metric-label">波形位置</div>
</div>
</div>
</div>
<!-- IFR 核心洞察 -->
<div class="ifr-insight">
<strong>最终理想解:</strong>蛇体弯曲运动<em>已存在</em>,鳞片仅以<strong>零能耗被动几何</strong>将侧向力<strong>单向转化</strong>为前向推力——<span class="highlight">不增加主动机构,矛盾自解</span>。
</div>
</div>
</div>
<div class="footer-note">各向异性摩擦鳞片 · 仿生腹鳞力学 · 被动式单向推进</div>
</div>
<script>
// ==============================
// 全局参数
// ==============================
const SEG_COUNT = 12;
const SEG_LEN = 46;
const SEG_WID = 24;
const SCALES_PER_SEG = 4;
const GROUND_OFFSET = 32; // 蛇身中心到地面的距离
let amplitude = 0.35;
let waveSpeed = 1.8;
let waveNumber = 0.72;
let frictionRatio = 3.0;
let time = 0;
let headX = 0;
let headY = 0;
let totalDist = 0;
let segments = [];
let lastTs = 0;
// 画布引用
let mc, mctx, dc, dctx;
// ==============================
// 初始化
// ==============================
function init() {
mc = document.getElementById('mainCanvas');
mctx = mc.getContext('2d');
dc = document.getElementById('detailCanvas');
dctx = dc.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 滑块绑定
document.getElementById('ampSlider').addEventListener('input', e => {
amplitude = parseInt(e.target.value) / 100;
document.getElementById('ampVal').textContent = amplitude.toFixed(2) + ' rad';
});
document.getElementById('speedSlider').addEventListener('input', e => {
waveSpeed = parseInt(e.target.value) / 10;
document.getElementById('speedVal').textContent = waveSpeed.toFixed(1) + ' rad/s';
});
document.getElementById('fricSlider').addEventListener('input', e => {
frictionRatio = parseInt(e.target.value) / 10;
document.getElementById('fricVal').textContent = frictionRatio.toFixed(1) + ' : 1';
});
requestAnimationFrame(loop);
}
function resizeCanvas() {
const wrapper = document.getElementById('canvasWrapper');
const dpr = window.devicePixelRatio || 1;
mc.width = wrapper.clientWidth * dpr;
mc.height = wrapper.clientHeight * dpr;
mctx.setTransform(dpr, 0, 0, dpr, 0, 0);
mc.style.width = wrapper.clientWidth + 'px';
mc.style.height = wrapper.clientHeight + 'px';
headY = wrapper.clientHeight / 2;
if (headX === 0) headX = wrapper.clientWidth * 0.3;
const dw = dc.parentElement;
dc.width = dw.clientWidth * dpr;
dc.height = dw.clientHeight * dpr;
dctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// ==============================
// 蛇体计算
// ==============================
function computeSnake() {
segments = [];
let cumAngle = 0;
for (let i = 0; i < SEG_COUNT; i++) {
const relAngle = amplitude * Math.sin(waveNumber * i - waveSpeed * time);
cumAngle += relAngle;
let x, y;
if (i === 0) {
x = headX;
y = headY;
} else {
const prev = segments[i - 1];
x = prev.x - SEG_LEN * Math.cos(prev.angle);
y = prev.y - SEG_LEN * Math.sin(prev.angle);
}
// 抓地因子:角速度越大 → 越在主动摆动 → 鳞片抓地
const angVel = Math.abs(amplitude * waveSpeed * Math.cos(waveNumber * i - waveSpeed * time));
const maxAngVel = Math.max(amplitude * waveSpeed, 0.001);
const gripFactor = Math.pow(angVel / maxAngVel, 0.7);
segments.push({ x, y, angle: cumAngle, relAngle, gripFactor });
}
}
// ==============================
// 主循环
// ==============================
function loop(ts) {
if (!lastTs) lastTs = ts;
const dt = Math.min((ts - lastTs) / 1000, 0.05);
lastTs = ts;
// 更新状态
time += dt;
const fwdSpeed = 55 * amplitude * waveSpeed;
totalDist += fwdSpeed * dt;
headX += fwdSpeed * dt;
computeSnake();
renderMain();
renderDetail();
updateMetrics(fwdSpeed);
requestAnimationFrame(loop);
}
// ==============================
// 主画布渲染
// ==============================
function renderMain() {
const W = mc.width / (window.devicePixelRatio || 1);
const H = mc.height / (window.devicePixelRatio || 1);
const ctx = mctx;
const camX = headX - W * 0.35;
// 背景
ctx.fillStyle = '#060a12';
ctx.fillRect(0, 0, W, H);
// 网格
drawGrid(ctx, W, H, camX);
// 地面
const groundY = headY + GROUND_OFFSET;
drawGround(ctx, W, H, camX, groundY);
ctx.save();
ctx.translate(-camX, 0);
// 绘制蛇体(从尾到头,头在最上层)
for (let i = SEG_COUNT - 1; i >= 0; i--) {
drawSegment(ctx, segments[i], i);
}
// 绘制头部
drawHead(ctx, segments[0]);
// 推进力箭头
drawForceArrows(ctx, camX, W);
// 波形传播指示
drawWaveIndicator(ctx, camX, W);
ctx.restore();
// 前进距离标尺
drawRuler(ctx, W, H, camX);
}
function drawGrid(ctx, W, H, camX) {
ctx.strokeStyle = 'rgba(26,42,66,0.4)';
ctx.lineWidth = 0.5;
const step = 60;
const offX = camX % step;
for (let x = -offX; x < W; x += step) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += step) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
}
function drawGround(ctx, W, H, camX, groundY) {
// 地面填充
const grd = ctx.createLinearGradient(0, groundY, 0, groundY + 80);
grd.addColorStop(0, '#0e1628');
grd.addColorStop(1, '#060a12');
ctx.fillStyle = grd;
ctx.fillRect(0, groundY, W, H - groundY);
// 地面线
ctx.strokeStyle = '#1e3050';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();
// 距离刻度
ctx.fillStyle = '#2a3a5a';
ctx.font = '9px "IBM Plex Mono"';
ctx.textAlign = 'center';
const step = 80;
const offX = camX % step;
for (let x = -offX; x < W; x += step) {
const worldX = Math.round((camX + x) / 10) * 10;
ctx.fillRect(x, groundY - 4, 1, 8);
ctx.fillText(worldX + '', x, groundY + 18);
}
// 抓地痕迹
for (let i = 0; i < SEG_COUNT; i++) {
const seg = segments[i];
if (seg.gripFactor > 0.6) {
const gx = seg.x;
const gy = groundY;
ctx.fillStyle = `rgba(255,159,28,${seg.gripFactor * 0.25})`;
for (let s = 0; s < SCALES_PER_SEG; s++) {
const sx = gx - SEG_LEN * 0.4 + (SEG_LEN * 0.8 / SCALES_PER_SEG) * (s + 0.5);
ctx.fillRect(sx - 2, gy, 4, 3);
}
}
}
}
function drawSegment(ctx, seg, idx) {
ctx.save();
ctx.translate(seg.x, seg.y);
ctx.rotate(seg.angle);
const w = SEG_LEN;
const h = SEG_WID;
const gf = seg.gripFactor;
// 阴影
ctx.shadowColor = gf > 0.5 ? `rgba(255,159,28,${gf * 0.3})` : 'rgba(0,229,199,0.08)';
ctx.shadowBlur = gf > 0.5 ? 14 : 6;
ctx.shadowOffsetY = 4;
// 段体填充
const bodyGrd = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
bodyGrd.addColorStop(0, '#162a3a');
bodyGrd.addColorStop(0.5, '#0e1e2e');
bodyGrd.addColorStop(1, '#0a1622');
ctx.fillStyle = bodyGrd;
roundRect(ctx, -w / 2, -h / 2, w, h, 5);
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
// 段体描边
const borderColor = gf > 0.5
? `rgba(255,159,28,${0.3 + gf * 0.5})`
: `rgba(0,229,199,${0.2 + (1 - gf) * 0.2})`;
ctx.strokeStyle = borderColor;
ctx.lineWidth = 1.5;
roundRect(ctx, -w / 2, -h / 2, w, h, 5);
ctx.stroke();
// 鳞片(底部)
drawScales(ctx, w, h, gf);
// 关节圆点
ctx.beginPath();
ctx.arc(-w / 2, 0, 3.5, 0, Math.PI * 2);
ctx.fillStyle = gf > 0.5 ? '#ff9f1c' : '#1a3a4a';
ctx.fill();
ctx.strokeStyle = gf > 0.5 ? '#ff9f1c' : '#00e5c7';
ctx.lineWidth = 1;
ctx.stroke();
// 段号
ctx.fillStyle = 'rgba(200,214,229,0.25)';
ctx.font = '8px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(idx, 0, 0);
ctx.restore();
}
function drawScales(ctx, w, h, gripFactor) {
const scaleW = 7;
const scaleH = 5;
const spacing = (w * 0.7) / SCALES_PER_SEG;
const startX = -w * 0.35;
for (let s = 0; s < SCALES_PER_SEG; s++) {
const sx = startX + spacing * (s + 0.5);
const sy = h / 2;
ctx.save();
ctx.translate(sx, sy);
if (gripFactor > 0.5) {
// 抓地状态:鳞片竖起
const tilt = -0.3 - gripFactor * 0.4; // 向后倾斜
ctx.rotate(tilt);
// 鳞片体
const sGrd = ctx.createLinearGradient(0, 0, 0, scaleH);
sGrd.addColorStop(0, `rgba(255,159,28,${0.6 + gripFactor * 0.4})`);
sGrd.addColorStop(1, `rgba(255,71,87,${0.3 + gripFactor * 0.3})`);
ctx.fillStyle = sGrd;
ctx.beginPath();
ctx.moveTo(-scaleW / 2, 0);
ctx.lineTo(0, scaleH);
ctx.lineTo(scaleW / 2, 0);
ctx.closePath();
ctx.fill();
// 发光
ctx.shadowColor = `rgba(255,159,28,${gripFactor * 0.5})`;
ctx.shadowBlur = 6;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
} else {
// 倒伏状态:鳞片平贴
const flatFactor = 1 - gripFactor;
ctx.fillStyle = `rgba(46,213,115,${0.2 + flatFactor * 0.25})`;
ctx.beginPath();
ctx.moveTo(-scaleW / 2, 0);
ctx.lineTo(-scaleW / 2 - 1, scaleH * 0.3);
ctx.lineTo(scaleW / 2 + 1, scaleH * 0.3);
ctx.lineTo(scaleW / 2, 0);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
}
function drawHead(ctx, seg) {
ctx.save();
ctx.translate(seg.x, seg.y);
ctx.rotate(seg.angle);
// 头部形状(略尖)
ctx.beginPath();
ctx.moveTo(SEG_LEN / 2 + 14, 0);
ctx.lineTo(SEG_LEN / 2, -SEG_WID / 2 - 2);
ctx.lineTo(SEG_LEN / 2 - 6, -SEG_WID / 2);
ctx.lineTo(SEG_LEN / 2 - 6, SEG_WID / 2);
ctx.lineTo(SEG_LEN / 2, SEG_WID / 2 + 2);
ctx.closePath();
const headGrd = ctx.createLinearGradient(0, -SEG_WID / 2, 0, SEG_WID / 2);
headGrd.addColorStop(0, '#1a4050');
headGrd.addColorStop(1, '#0e2838');
ctx.fillStyle = headGrd;
ctx.fill();
ctx.strokeStyle = 'rgba(0,229,199,0.6)';
ctx.lineWidth = 1.5;
ctx.stroke();
// 眼睛
ctx.beginPath();
ctx.arc(SEG_LEN / 2 + 4, -6, 3, 0, Math.PI * 2);
ctx.fillStyle = '#00e5c7';
ctx.fill();
ctx.beginPath();
ctx.arc(SEG_LEN / 2 + 4, 6, 3, 0, Math.PI * 2);
ctx.fillStyle = '#00e5c7';
ctx.fill();
// 眼睛高光
ctx.beginPath();
ctx.arc(SEG_LEN / 2 + 5, -7, 1.2, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.beginPath();
ctx.arc(SEG_LEN / 2 + 5, 5, 1.2, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.restore();
}
function drawForceArrows(ctx, camX, viewW) {
// 在抓地段显示推进力箭头
for (let i = 0; i < SEG_COUNT; i++) {
const seg = segments[i];
if (seg.gripFactor < 0.6) continue;
if (seg.x < camX - 50 || seg.x > camX + viewW + 50) continue;
const arrowLen = 20 + seg.gripFactor * 25;
const ax = seg.x + Math.cos(seg.angle) * (SEG_LEN / 2 + 10);
const ay = seg.y + Math.sin(seg.angle) * (SEG_LEN / 2 + 10);
// 推进力箭头(沿蛇身方向前进方向)
ctx.save();
ctx.translate(ax, ay);
ctx.rotate(seg.angle);
ctx.strokeStyle = `rgba(0,229,199,${seg.gripFactor * 0.8})`;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(arrowLen, 0);
ctx.stroke();
// 箭头头部
ctx.fillStyle = `rgba(0,229,199,${seg.gripFactor * 0.8})`;
ctx.beginPath();
ctx.moveTo(arrowLen + 6, 0);
ctx.lineTo(arrowLen - 3, -4);
ctx.lineTo(arrowLen - 3, 4);
ctx.closePath();
ctx.fill();
// 力值标签
const forceVal = (seg.gripFactor * frictionRatio * 0.8).toFixed(1);
ctx.fillStyle = `rgba(0,229,199,${seg.gripFactor * 0.7})`;
ctx.font = '9px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText('F=' + forceVal + 'N', arrowLen / 2, -8);
ctx.restore();
}
}
function drawWaveIndicator(ctx, camX, viewW) {
// 波形传播方向指示(沿蛇身上方)
if (segments.length < 2) return;
const headSeg = segments[0];
const tailSeg = segments[SEG_COUNT - 1];
const midX = (headSeg.x + tailSeg.x) / 2;
const topY = Math.min(...segments.map(s => s.y)) - 50;
if (midX < camX - 100 || midX > camX + viewW + 100) return;
// 波形传播箭头(从尾到头表示波向后传,但实际是蛇向前)
const wavePhase = (time * waveSpeed) % (Math.PI * 2);
const pulseX = headSeg.x - ((wavePhase / (Math.PI * 2)) * (headSeg.x - tailSeg.x));
ctx.save();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(255,159,28,0.35)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(headSeg.x, topY);
ctx.lineTo(tailSeg.x, topY);
ctx.stroke();
ctx.setLineDash([]);
// 脉冲点
ctx.beginPath();
ctx.arc(pulseX, topY, 5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,159,28,0.8)';
ctx.fill();
// 标签
ctx.fillStyle = 'rgba(255,159,28,0.7)';
ctx.font = '10px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText('← 波形传播方向', midX, topY - 10);
ctx.restore();
}
function drawRuler(ctx, W, H, camX) {
const y = H - 20;
ctx.fillStyle = 'rgba(200,214,229,0.3)';
ctx.font = '10px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText('前进距离: ' + (totalDist / 10).toFixed(1) + ' cm', W / 2, y);
}
// ==============================
// 辅助:圆角矩形
// ==============================
function roundRect(ctx, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
}
// ==============================
// 鳞片细节剖面渲染
// ==============================
function renderDetail() {
const W = dc.width / (window.devicePixelRatio || 1);
const H = dc.height / (window.devicePixelRatio || 1);
const ctx = dctx;
// 取第2段的抓地因子作为示例
const refGrip = segments.length > 2 ? segments[2].gripFactor : 0;
ctx.fillStyle = '#080c14';
ctx.fillRect(0, 0, W, H);
const cx = W / 2;
const groundY = H * 0.65;
// 地面
ctx.fillStyle = '#0e1628';
ctx.fillRect(0, groundY, W, H - groundY);
ctx.strokeStyle = '#1e3050';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();
// 地面纹理
ctx.fillStyle = '#162240';
for (let i = 0; i < 12; i++) {
const bx = 10 + i * (W - 20) / 11;
ctx.fillRect(bx, groundY + 2, 8, 3);
}
// 蛇身截面(底部弧形)
const bodyW = W * 0.5;
const bodyH = 28;
const bodyX = cx - bodyW / 2;
const bodyY = groundY - bodyH - 8;
const bodyGrd = ctx.createLinearGradient(0, bodyY, 0, bodyY + bodyH);
bodyGrd.addColorStop(0, '#1a3a4a');
bodyGrd.addColorStop(1, '#0d2030');
ctx.fillStyle = bodyGrd;
roundRect(ctx, bodyX, bodyY, bodyW, bodyH, 6);
ctx.fill();
ctx.strokeStyle = refGrip > 0.5
? `rgba(255,159,28,${0.4 + refGrip * 0.4})`
: 'rgba(0,229,199,0.3)';
ctx.lineWidth = 1.5;
roundRect(ctx, bodyX, bodyY, bodyW, bodyH, 6);
ctx.stroke();
// 标签:蛇身
ctx.fillStyle = 'rgba(200,214,229,0.5)';
ctx.font = '9px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText('短节截面', cx, bodyY + bodyH / 2 + 3);
// 鳞片(3个)
const scalePositions = [cx - bodyW * 0.25, cx, cx + bodyW * 0.25];
scalePositions.forEach((sx, si) => {
ctx.save();
ctx.translate(sx, bodyY + bodyH);
if (refGrip > 0.5) {
// 抓地:鳞片竖起,向后倾斜
const tiltAngle = -0.4 - refGrip * 0.5;
ctx.rotate(tiltAngle);
const sLen = 18;
const sWid = 6;
// 鳞片体
const sGrd = ctx.createLinearGradient(0, 0, 0, sLen);
sGrd.addColorStop(0, `rgba(255,159,28,${0.7 + refGrip * 0.3})`);
sGrd.addColorStop(1, `rgba(255,71,87,${0.4 + refGrip * 0.3})`);
ctx.fillStyle = sGrd;
ctx.beginPath();
ctx.moveTo(-sWid / 2, 0);
ctx.lineTo(0, sLen);
ctx.lineTo(sWid / 2, 0);
ctx.closePath();
ctx.fill();
ctx.shadowColor = `rgba(255,159,28,${refGrip * 0.5})`;
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// 棘爪尖端标记
ctx.beginPath();
ctx.arc(0, sLen, 2, 0, Math.PI * 2);
ctx.fillStyle = '#ff4757';
ctx.fill();
// 与地面的接触力
if (si === 1) {
ctx.rotate(-tiltAngle); // 恢复旋转以画水平箭头
// 摩擦力(水平,向前)
ctx.strokeStyle = 'rgba(0,229,199,0.8)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, groundY - bodyY - bodyH + 8);
ctx.lineTo(30, groundY - bodyY - bodyH + 8);
ctx.stroke();
ctx.fillStyle = 'rgba(0,229,199,0.8)';
ctx.beginPath();
ctx.moveTo(36, groundY - bodyY - bodyH + 8);
ctx.lineTo(28, groundY - bodyY - bodyH + 4);
ctx.lineTo(28, groundY - bodyY - bodyH + 12);
ctx.closePath();
ctx.fill();
// 法向力(竖直向上)
ctx.strokeStyle = 'rgba(255,159,28,0.6)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -20);
ctx.stroke();
ctx.fillStyle = 'rgba(255,159,28,0.6)';
ctx.beginPath();
ctx.moveTo(0, -24);
ctx.lineTo(-3, -17);
ctx.lineTo(3, -17);
ctx.closePath();
ctx.fill();
}
} else {
// 倒伏:鳞片平贴蛇身底部
const flatLen = 14;
const flatH = 3;
ctx.fillStyle = `rgba(46,213,115,${0.3 + (1 - refGrip) * 0.3})`;
ctx.fillRect(-flatLen / 2, 0, flatLen, flatH);
// 滑动方向小箭头
if (si === 1) {
ctx.fillStyle = `rgba(46,213,115,0.6)`;
ctx.font = '8px "IBM Plex Mono"';
ctx.textAlign = 'left';
ctx.fillText('→ 滑行', flatLen / 2 + 4, flatH + 8);
}
}
ctx.restore();
});
// 状态标签
const stateLabel = refGrip > 0.5 ? '抓地模式' : '滑行模式';
const stateColor = refGrip > 0.5 ? '#ff9f1c' : '#2ed573';
ctx.fillStyle = stateColor;
ctx.font = 'bold 11px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText(stateLabel, cx, 16);
// 摩擦系数
if (refGrip > 0.5) {
ctx.fillStyle = 'rgba(255,159,28,0.7)';
ctx.font = '9px "IBM Plex Mono"';
ctx.fillText('μ = ' + (frictionRatio * 0.5).toFixed(1) + '(高摩擦)', cx, groundY + 20);
} else {
ctx.fillStyle = 'rgba(46,213,115,0.6)';
ctx.font = '9px "IBM Plex Mono"';
ctx.fillText('μ = ' + (frictionRatio * 0.5 / frictionRatio).toFixed(2) + '(低摩擦)', cx, groundY + 20);
}
// 力分解标注
if (refGrip > 0.5) {
ctx.fillStyle = 'rgba(0,229,199,0.6)';
ctx.font = '8px "IBM Plex Mono"';
ctx.textAlign = 'left';
ctx.fillText('推进力 F∥', cx + 38, groundY - bodyY - bodyH + 10);
ctx.fillStyle = 'rgba(255,159,28,0.5)';
ctx.fillText('法向力 F⊥', 6, bodyY - 6);
}
// IFR标注
ctx.fillStyle = 'rgba(200,214,229,0.3)';
ctx.font = '8px "IBM Plex Mono"';
ctx.textAlign = 'center';
ctx.fillText('被动几何 · 零能耗 · 各向异性', cx, H - 6);
}
// ==============================
// 指标更新
// ==============================
function updateMetrics(fwdSpeed) {
document.getElementById('metricSpeed').textContent = (fwdSpeed * 1.2).toFixed(0);
document.getElementById('metricDist').textContent = (totalDist / 10).toFixed(1);
const gripCount = segments.filter(s => s.gripFactor > 0.6).length;
document.getElementById('metricGrip').textContent = gripCount + ' / ' + SEG_COUNT;
const wavePos = ((time * waveSpeed) % (Math.PI * 2) / (Math.PI * 2) * 100).toFixed(0);
document.getElementById('metricWave').textContent = wavePos + '%';
}
// ==============================
// 启动
// ==============================
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
这个动画完整实现了机械蛇"单向阻力鳞片底盘"的推进原理演示,核心设计思路如下:
动画核心机制:
- 12 段刚性短节通过铰链串联,正弦波从头部向尾部传播
- 每段底部的仿生棘爪鳞片根据角速度自动判断抓地/滑行状态——角速度大时鳞片竖起(琥珀色高亮,高摩擦),角速度小时鳞片倒伏(绿色,低摩擦)
- 抓地段显示推进力矢量箭头和力值,地面留有抓地痕迹
IFR 理想解视觉引导:
- 侧面板的鳞片剖面图实时展示"被动几何·零能耗"的核心创新——鳞片无需额外动力,仅靠各向异性摩擦将侧向摆动单向转化为前向推力
- 力分解箭头(推进力 F∥ 与法向力 F⊥)清晰展示转化机理
- 抓地/滑行双色系统(琥珀 vs 翠绿)+ 段体描边辉光变化,引导用户关注矛盾破解的关键动作
交互控制: 三个滑块可实时调节波幅、波速和摩擦比,观察参数变化对推进效果的影响;四项实时指标监控速度、行程、抓地段数和波形位置。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
