137 lines
7.5 KiB
HTML
137 lines
7.5 KiB
HTML
<!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){ // 节段 i(0=根,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>
|