starraid/demo/tentacle-sweep.html

137 lines
7.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>泰伦触手扫击 · 动作设计 Demo v2</title>
<style>
html,body{margin:0;background:#05070d;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:monospace}
canvas{border-radius:8px;box-shadow:0 0 40px #0a2a1a}
#tip{color:#5a7a6a;font-size:12px;margin-top:10px}
</style>
</head>
<body>
<canvas id="cv" width="600" height="330"></canvas>
<div id="tip">右侧触手:前摇向右后拉蓄力(发光+聚能粒子)→ 从右下向左下 60° 扇形挥鞭(残影轨迹)→ 顿挫收回 · 判定=根部圆心扇形</div>
<script>
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
const W=600,H=330;
const ROOT={x:352,y:56},ROOT2={x:248,y:56};
const SEG=7,BASE_LEN=148;
// 时间线待机0.9s → 前摇0.83s → 扫击0.47s → 顿挫0.28s → 收回0.67s
const T_IDLE=55,T_WIND=105,T_STRIKE=133,T_HOLD=150,T_REC=190,T_LOOP=T_REC;
const A_MAX=0.73; // 前摇后拉角(+42°扫击起点
const A_END=-0.58; // 扫击终点(-33°覆盖 ±30° = 60° 扇形
const rnd=(a,b)=>a+Math.random()*(b-a);
function circ(x,y,r,c){ctx.fillStyle=c;ctx.beginPath();ctx.arc(x,y,Math.max(0.1,r),0,Math.PI*2);ctx.fill();}
function segDist2(px,py,ax,ay,bx,by){ // 点到线段距离平方(触手=连续碰撞体,无节段间隙)
const dx=bx-ax,dy=by-ay,L2=dx*dx+dy*dy||1;
const tt=Math.max(0,Math.min(1,((px-ax)*dx+(py-ay)*dy)/L2));
const cx=ax+dx*tt,cy=ay+dy*tt;
return (px-cx)*(px-cx)+(py-cy)*(py-cy);
}
// —— 动作采样输出扫掠角弧度0=正下方,右正左负)、伸缩、发光 ——
function sample(tt){
tt=((tt%T_LOOP)+T_LOOP)%T_LOOP;
const idle=Math.sin(tt*0.06)*0.07;
if(tt<T_IDLE)return{ang:idle,stretch:1,glow:0,ph:0};
if(tt<T_WIND){const f=(tt-T_IDLE)/(T_WIND-T_IDLE),e=f*f; // 前摇:向右后拉过伸,发光渐强+绷紧微颤
return{ang:idle*(1-f)+A_MAX*e+Math.sin(tt*0.9)*0.012*e,stretch:1+e*0.15,glow:f,ph:1};}
if(tt<T_STRIKE){const f=(tt-T_WIND)/(T_STRIKE-T_WIND),e=f*f*f; // 扫击:加速挥出(从右下到左下)
return{ang:A_MAX-e*(A_MAX-A_END),stretch:1.15+e*0.45,glow:1,ph:2};}
if(tt<T_HOLD){const f=(tt-T_STRIKE)/(T_HOLD-T_STRIKE); // 顿挫:末端微颤衰减
return{ang:A_END+Math.sin(tt*1.2)*0.05*(1-f),stretch:1.6,glow:1,ph:3};}
const f=(tt-T_HOLD)/(T_REC-T_HOLD),e=1-(1-f)*(1-f); // 收回
return{ang:A_END+(idle-A_END)*e,stretch:1.6-0.6*e,glow:1-f,ph:4};
}
function segPos(i,tt){ // 节段 i0=根SEG-1=尖),逐节延迟形成鞭弧
const s=sample(tt-i*2.2),f=i/(SEG-1),L=BASE_LEN*s.stretch*f;
return{x:ROOT.x+Math.sin(s.ang)*L,y:ROOT.y+Math.cos(s.ang)*L,s};
}
let t=0,trail=[],parts=[];
let ships=[
{x:421,y:211,ang:"+24°",dead:false,hitT:0}, // 扫击前段命中
{x:293,y:247,ang:"-17°",dead:false,hitT:0}, // 扫击后段命中
{x:529,y:169,ang:"扇外",dead:false,hitT:0} // 扇形外安全
];
const PHN=["待机 IDLE","前摇 WINDUP · 后拉蓄力","扫击 STRIKE!","顿挫 HIT-STOP","收回 RECOVER"];
const PHC=["#5a7a6a","#ffd23d","#ff5566","#e8dcc0","#43e8ff"];
function ship(s,col,label){
if(s.dead)return;
ctx.fillStyle=col;ctx.beginPath();
ctx.moveTo(s.x,s.y-6);ctx.lineTo(s.x-5,s.y+5);ctx.lineTo(s.x+5,s.y+5);ctx.closePath();ctx.fill();
circ(s.x,s.y-1,1.5,"#fff");
ctx.font="8px monospace";ctx.fillStyle="#3a4a5a";ctx.textAlign="center";ctx.fillText(label,s.x,s.y+14);
}
function draw(){
t++;
ctx.fillStyle="#070a14";ctx.fillRect(0,0,W,H);
for(let i=0;i<40;i++)circ((i*137)%W,(i*89)%H,0.7,"#141d33");
const cur=sample(t);
// —— 母舰头部示意 ——
circ(300,20,58,"#241a30");circ(300,14,46,"#3d2f4a");
ctx.fillStyle="#1a1426";ctx.fillRect(244,40,112,20);
circ(ROOT2.x,ROOT2.y,9,"#241a30");circ(ROOT.x,ROOT.y,9,"#241a30");
// —— 左侧待机触手(对比) ——
for(let i=SEG-1;i>=0;i--){const f=i/(SEG-1);
circ(ROOT2.x+Math.sin(t*0.06+1.7)*10*f*f,ROOT2.y+BASE_LEN*f,7.5-f*4.5,i%2?"#7a4a6a":"#5a3a5a");}
// —— 触手节点 ——
const pts=[];for(let i=0;i<SEG;i++)pts.push(segPos(i,t));
// —— 残影轨迹(扫击期记录,扇形弧带) ——
if(cur.ph===2){trail.push(pts.map(p=>({x:p.x,y:p.y})));if(trail.length>10)trail.shift();}
else if(trail.length)trail.shift();
trail.forEach((h,hi)=>{const a=(hi+1)/trail.length*0.2;
ctx.globalAlpha=a;for(let i=1;i<SEG;i++){const f=i/(SEG-1);circ(h[i].x,h[i].y,(7.5-f*4.5)*0.85,"#a8ff5c");}ctx.globalAlpha=1;});
// —— 前摇聚能粒子(被触手吸入) ——
if(cur.ph===1&&t%3===0){const tp=pts[4];
parts.push({x:tp.x+rnd(-55,55),y:tp.y+rnd(-35,55),sx:tp.x,sy:tp.y,vx:0,vy:0,life:18,suck:1});}
// —— 扫击触手本体 ——
for(let i=SEG-1;i>=0;i--){const p=pts[i],f=i/(SEG-1),r=7.5-f*4.5;
if(p.s.glow>0.15){ctx.globalAlpha=p.s.glow*0.28;circ(p.x,p.y,r+3.5,"#a8ff5c");ctx.globalAlpha=1;}
circ(p.x,p.y,r,i%2?"#7a4a6a":"#5a3a5a");}
// —— 骨白刀尖(朝向跟随触手末端方向) ——
const p1=pts[SEG-2],p2=pts[SEG-1],ta=Math.atan2(p2.y-p1.y,p2.x-p1.x);
ctx.save();ctx.translate(p2.x,p2.y);ctx.rotate(ta-Math.PI/2);
ctx.fillStyle="#e8dcc0";ctx.beginPath();
ctx.moveTo(-4,-2);ctx.lineTo(4,-2);ctx.lineTo(0,10);ctx.closePath();ctx.fill();
ctx.restore();
// —— 扫击洒落酸液 ——
if(cur.ph===2&&t%2===0)parts.push({x:pts[5].x,y:pts[5].y,vx:rnd(-0.6,0.6),vy:rnd(0.5,1.5),life:26});
// —— 触手即碰撞体:扫击+顿挫期间(尖端实际扫完全程)整条触手与飞船碰撞 ——
if(cur.ph===2||cur.ph===3)for(const s of ships){if(s.dead)continue;
for(let i=1;i<SEG;i++){const f=i/(SEG-1),sr=(7.5-f*4.5)+5;
if(segDist2(s.x,s.y,pts[i-1].x,pts[i-1].y,pts[i].x,pts[i].y)<sr*sr){s.dead=true;s.hitT=45;
for(let j=0;j<12;j++)parts.push({x:s.x,y:s.y,vx:rnd(-1.6,1.6),vy:rnd(-1.6,1.6),life:22,red:1});
break;}}}
if(t%T_LOOP===1)for(const s of ships){s.dead=false;s.hitT=0;}
// —— 粒子 ——
for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.life--;
if(p.suck){p.vx+=(p.sx-p.x)*0.006;p.vy+=(p.sy-p.y)*0.006;}
p.x+=p.vx;p.y+=p.vy;
if(p.life<=0){parts.splice(i,1);continue;}
ctx.globalAlpha=Math.min(1,p.life/16);
circ(p.x,p.y,p.red?2:1.6,p.red?"#ff5566":"#a8ff5c");ctx.globalAlpha=1;}
// —— 演示飞船 ——
for(const s of ships){ship(s,s.ang==="扇外"?"#3a5a6a":"#43e8ff",s.ang);
if(s.hitT>0){s.hitT--;ctx.globalAlpha=Math.min(1,s.hitT/20);
ctx.font="bold 12px monospace";ctx.fillStyle="#ff5566";ctx.textAlign="center";
ctx.fillText("HIT!",s.x,s.y-14);ctx.globalAlpha=1;}}
// —— 阶段标签 ——
ctx.font="bold 13px monospace";ctx.fillStyle=PHC[cur.ph];ctx.textAlign="left";
ctx.fillText(PHN[cur.ph],16,26);
ctx.font="10px monospace";ctx.fillStyle="#3a4a5a";
ctx.fillText("frame "+(t%T_LOOP)+" / "+T_LOOP,16,42);
// —— 底部时间轴 ——
const ax=16,aw=W-32,ay=H-18;
const seg3=[[0,T_IDLE,"#1a2433"],[T_IDLE,T_WIND,"#8a7420"],[T_WIND,T_STRIKE,"#a03040"],[T_STRIKE,T_REC,"#2a6a80"]];
for(const[a0,a1,c]of seg3){ctx.fillStyle=c;ctx.fillRect(ax+a0/T_LOOP*aw,ay,(a1-a0)/T_LOOP*aw,8);}
ctx.fillStyle="#fff";ctx.fillRect(ax+(t%T_LOOP)/T_LOOP*aw-1,ay-2,2,12);
ctx.fillStyle="#3a4a5a";ctx.font="9px monospace";
ctx.fillText("待机",ax+8,ay+7);ctx.fillText("前摇0.83s",ax+T_IDLE/T_LOOP*aw+6,ay+7);
ctx.fillText("扫击0.47s",ax+T_WIND/T_LOOP*aw+4,ay+7);ctx.fillText("顿挫+收回",ax+T_STRIKE/T_LOOP*aw+6,ay+7);
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>