红军战士 - 动画调试工具 v3
by TurboKoala83744 lines33.9 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>红军战士 - 动画调试工具 v3</title>
<style>
body { margin: 0; background: #0a0a12; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; touch-action: none; }
canvas { display: block; cursor: grab; }
canvas:active { cursor: grabbing; }
.topbar { position: absolute; top: 0; left: 0; right: 0; height: 40px; background: rgba(0,0,0,0.85); border-bottom: 1px solid #333; display: flex; align-items: center; padding: 0 16px; z-index: 20; }
.topbar .title { color: #C41E3A; font-weight: bold; letter-spacing: 3px; font-size: 14px; }
.topbar .hint { color: #666; font-size: 11px; margin-left: 20px; }
.panel { position: absolute; top: 50px; right: 10px; width: 250px; max-height: calc(100vh - 70px); overflow-y: auto; background: rgba(10,10,18,0.92); border: 1px solid #333; border-radius: 8px; padding: 10px; z-index: 20; }
.panel h3 { color: #C41E3A; font-size: 12px; letter-spacing: 2px; margin: 8px 0 6px 0; border-bottom: 1px solid #333; padding-bottom: 4px; }
.panel h3:first-child { margin-top: 0; }
.btn { display: inline-block; background: #1a1a2e; border: 1px solid #444; color: #ccc; padding: 5px 10px; margin: 3px; border-radius: 4px; font-size: 11px; cursor: pointer; transition: all 0.15s; user-select: none; }
.btn:hover { background: #2a2a3e; border-color: #666; }
.btn.active { background: #C41E3A; border-color: #C41E3A; color: #fff; font-weight: bold; }
.row { display: flex; align-items: center; margin: 4px 0; font-size: 11px; color: #aaa; }
.row label { flex: 1; }
.row input[type=range] { width: 100px; }
.row .val { width: 40px; text-align: right; color: #fff; font-family: monospace; }
.state-display { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); color: #C41E3A; font-size: 16px; letter-spacing: 4px; font-weight: bold; background: rgba(0,0,0,0.7); padding: 6px 20px; border-radius: 4px; z-index: 20; text-shadow: 0 0 10px rgba(196,30,58,0.8); }
</style>
</head>
<body>
<div class="topbar">
<span class="title">★ 红军战士 · 动画调试工具 v3 ★</span>
<span class="hint">左键平移 | 右键旋转 | 滚轮缩放</span>
</div>
<div class="panel">
<h3>▸ 动画状态</h3>
<div>
<div class="btn" data-state="idle">待机</div>
<div class="btn" data-state="walk">行走</div>
<div class="btn" data-state="run">奔跑</div>
<div class="btn" data-state="fire">开火(含瞄准)</div>
<div class="btn" data-state="bolt">拉栓</div>
<div class="btn" data-state="hit">受击</div>
<div class="btn" data-state="swing">挥砍(大刀)</div>
<div class="btn" data-state="dadao_idle">持刀待机</div>
</div>
<h3>▸ 动画参数</h3>
<div class="row"><label>步频</label><input type="range" id="walkSpeed" min="1" max="10" step="0.1" value="4"><span class="val" id="walkSpeedVal">4.0</span></div>
<div class="row"><label>腿摆幅</label><input type="range" id="legSwing" min="0" max="1" step="0.01" value="0.45"><span class="val" id="legSwingVal">0.45</span></div>
<div class="row"><label>臂摆幅</label><input type="range" id="armSwing" min="0" max="1" step="0.01" value="0.45"><span class="val" id="armSwingVal">0.45</span></div>
<div class="row"><label>身体起伏</label><input type="range" id="bodyBob" min="0" max="15" step="0.1" value="3"><span class="val" id="bodyBobVal">3.0</span></div>
<div class="row"><label>前倾角</label><input type="range" id="lean" min="-0.5" max="0.5" step="0.01" value="0.05"><span class="val" id="leanVal">0.05</span></div>
<h3>▸ 大刀位置调试</h3>
<div class="row"><label>手X偏移</label><input type="range" id="handX" min="-100" max="100" step="1" value="50"><span class="val" id="handXVal">50</span></div>
<div class="row"><label>手Y偏移</label><input type="range" id="handY" min="-250" max="-50" step="1" value="-100"><span class="val" id="handYVal">-100</span></div>
<div class="row"><label>刀身角度</label><input type="range" id="dadaoBaseAngle" min="-1.5" max="1.5" step="0.05" value="-0.3"><span class="val" id="dadaoBaseAngleVal">-0.30</span></div>
<h3>▸ 播放控制</h3>
<div class="row"><label>播放速度</label><input type="range" id="playSpeed" min="0.1" max="3" step="0.1" value="1"><span class="val" id="playSpeedVal">1.0</span></div>
<div>
<div class="btn" id="btnPause">⏸ 暂停</div>
<div class="btn" id="btnReset">↺ 重置</div>
</div>
<h3>▸ 摄像机</h3>
<div class="row"><label>缩放</label><input type="range" id="camZoom" min="0.3" max="3" step="0.05" value="1.2"><span class="val" id="camZoomVal">1.20</span></div>
<div class="row"><label>旋转</label><input type="range" id="camRot" min="-180" max="180" step="1" value="0"><span class="val" id="camRotVal">0</span></div>
<div>
<div class="btn" id="btnResetCam">重置视角</div>
</div>
<h3>▸ 辅助显示</h3>
<div>
<div class="btn active" id="btnGrid">网格</div>
<div class="btn" id="btnAnchor">锚点</div>
<div class="btn" id="btnShadow">阴影</div>
</div>
</div>
<div class="state-display" id="stateDisplay">待机</div>
<canvas id="game"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W = window.innerWidth, H = window.innerHeight;
canvas.width = W; canvas.height = H;
window.addEventListener('resize', () => {
W = window.innerWidth; H = window.innerHeight;
canvas.width = W; canvas.height = H;
});
// ============================================================
// 配色
// ============================================================
const C = {
uniform:'#5A6B7C', uniformDark:'#3D4A57', uniformLite:'#6E8090',
collar:'#C41E3A', skin:'#E8B88A', skinDark:'#C99A6E',
hat:'#4A5A6B', hatDark:'#37444F', bind:'#8A8A8A',
straw:'#9A7A4A', strawDark:'#7A5A2A', belt:'#3A2A1A',
};
const D = {
blade:'#8A8A8A', bladeDark:'#5A5A5A', bladeLite:'#B0B0B0', bladeEdge:'#C8C8C8',
bloodGroove:'#6A6A6A', back:'#4A4A4A', guard:'#B8963A', guardDark:'#8A6A1A',
handle:'#3A2A1A', wrap:'#5A6B7C', wrapDark:'#3D4A57', ring:'#8A8A8A',
redCloth:'#C41E3A',
};
const S = { headW:52, headH:52, torsoW:84, torsoH:76, armW:36, armH:76, legW:44, legH:84 };
// ============================================================
// 摄像机
// ============================================================
const camera = {
x: W / 2, y: H / 2, zoom: 1.2, rotation: 0,
isDragging: false, dragButton: 0, lastX: 0, lastY: 0,
};
// ============================================================
// 动画状态
// ============================================================
let currentState = 'idle';
let stateStartTime = performance.now();
let time = 0;
let playing = true;
let playSpeed = 1.0;
const params = {
walkSpeed: 4.0, legSwing: 0.45, armSwing: 0.45, bodyBob: 3.0, lean: 0.05,
};
const dadaoParams = {
handX: 50, handY: -100, baseAngle: -0.3
};
const show = { grid: true, anchor: false, shadow: true };
const gunState = { flashTimer: 0, boltOffset: 0, recoilX: 0, recoilY: 0, recoilRot: 0 };
function lerp(a, b, t) { return (1 - t) * a + t * b; }
function easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function roundRect(ctx, 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 drawHead(ctx, x, y, rotation = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { headW, headH } = S; const headX = -headW/2; const headY = -headH;
ctx.fillStyle = C.skin; roundRect(ctx, headX, headY, headW, headH, 6); ctx.fill();
ctx.fillStyle = C.skinDark; ctx.fillRect(headX, headY, 6, headH);
ctx.fillStyle = '#2A1A0A'; ctx.fillRect(headX + 12, headY + 22, 7, 5); ctx.fillRect(headX + headW - 19, headY + 22, 7, 5);
ctx.strokeStyle = '#2A1A0A'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(headX + 10, headY + 18); ctx.lineTo(headX + 20, headY + 20); ctx.stroke();
ctx.beginPath(); ctx.moveTo(headX + headW - 10, headY + 18); ctx.lineTo(headX + headW - 20, headY + 20); ctx.stroke();
ctx.strokeStyle = '#8A5A3A'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(headX + 20, headY + 40); ctx.lineTo(headX + headW - 20, headY + 40); ctx.stroke();
const hatW = headW + 14; const hatH = 22; const hatX = -hatW/2; const hatY = headY - hatH + 4;
ctx.fillStyle = C.hat; ctx.beginPath();
ctx.moveTo(hatX + 10, hatY); ctx.lineTo(hatX + hatW - 10, hatY);
ctx.lineTo(hatX + hatW, hatY + 8); ctx.lineTo(hatX + hatW - 4, hatY + hatH);
ctx.lineTo(hatX + 4, hatY + hatH); ctx.lineTo(hatX, hatY + 8);
ctx.closePath(); ctx.fill();
ctx.fillStyle = C.hatDark; ctx.fillRect(hatX + 2, hatY + hatH - 4, hatW - 4, 4);
const starCY = hatY + 9, starR = 7;
ctx.fillStyle = C.collar; ctx.beginPath();
for (let i = 0; i < 5; i++) {
const a = (i * 72 - 90) * Math.PI / 180;
const x1 = starR * Math.cos(a), y1 = starCY + starR * Math.sin(a);
const a2 = ((i + 1) * 72 - 90) * Math.PI / 180;
const ir = starR * 0.4;
const x2 = ir * Math.cos(a2 - Math.PI/5), y2 = starCY + ir * Math.sin(a2 - Math.PI/5);
if (i === 0) ctx.moveTo(x1, y1); else ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
}
ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#8A0A1A'; ctx.lineWidth = 1; ctx.stroke();
ctx.restore();
}
function drawTorso(ctx, x, y, rotation = 0) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { torsoW, torsoH } = S; const torsoX = -torsoW/2; const torsoY = -torsoH;
ctx.fillStyle = C.uniform; roundRect(ctx, torsoX, torsoY, torsoW, torsoH, 4); ctx.fill();
ctx.fillStyle = C.uniformDark; ctx.fillRect(torsoX, torsoY, 8, torsoH);
ctx.fillStyle = C.uniformLite; ctx.fillRect(torsoX + torsoW - 6, torsoY, 6, torsoH);
ctx.fillStyle = C.collar; ctx.fillRect(torsoX + 12, torsoY + 2, 14, 8); ctx.fillRect(torsoX + torsoW - 26, torsoY + 2, 14, 8);
ctx.strokeStyle = C.uniformDark; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(0, torsoY + 12); ctx.lineTo(0, torsoY + torsoH - 4); ctx.stroke();
const beltY = torsoY + torsoH * 0.55;
ctx.fillStyle = C.belt; ctx.fillRect(torsoX, beltY, torsoW, 7);
ctx.fillStyle = '#B8963A'; ctx.fillRect(-5, beltY + 1, 10, 5);
ctx.restore();
}
function drawArm(ctx, x, y, rotation, side, scaleY = 1) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.scale(1, scaleY);
const { armW, armH } = S; const armX = -armW/2;
ctx.fillStyle = C.uniform; roundRect(ctx, armX, 0, armW, armH - 12, 3); ctx.fill();
if (side === 'left') { ctx.fillStyle = C.uniformDark; ctx.fillRect(armX, 0, 5, armH - 12); }
else { ctx.fillStyle = C.uniformLite; ctx.fillRect(armX + armW - 5, 0, 5, armH - 12); }
ctx.fillStyle = C.skin; roundRect(ctx, armX + 4, armH - 14, armW - 8, 14, 3); ctx.fill();
ctx.restore();
}
function drawLeg(ctx, x, y, rotation) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
const { legW, legH } = S; const legX = -legW/2;
ctx.fillStyle = C.uniform; roundRect(ctx, legX, 0, legW, legH * 0.55, 3); ctx.fill();
ctx.fillStyle = C.uniformDark; ctx.fillRect(legX, 0, 6, legH * 0.55);
const bindY = legH * 0.5; const bindH = legH * 0.42;
ctx.fillStyle = C.bind; roundRect(ctx, legX - 1, bindY, legW + 2, bindH, 2); ctx.fill();
ctx.strokeStyle = '#6A6A6A'; ctx.lineWidth = 1.2;
for (let i = 0; i < 5; i++) {
const yy = bindY + 4 + i * (bindH - 8) / 4;
ctx.beginPath(); ctx.moveTo(legX - 1, yy); ctx.lineTo(legX + legW + 1, yy); ctx.stroke();
}
const shoeY = bindY + bindH; const shoeH = legH * 0.08;
ctx.fillStyle = C.straw; roundRect(ctx, legX - 3, shoeY, legW + 6, shoeH, 3); ctx.fill();
ctx.strokeStyle = C.strawDark; ctx.lineWidth = 1;
for (let i = 0; i < 3; i++) {
ctx.beginPath(); ctx.moveTo(legX - 3, shoeY + 2 + i * 2); ctx.lineTo(legX + legW + 3, shoeY + 2 + i * 2); ctx.stroke();
}
ctx.restore();
}
// ============================================================
// 步枪
// ============================================================
function drawHanyangRifle(ctx) {
ctx.fillStyle = '#5E3A20'; ctx.beginPath();
ctx.moveTo(-30, -6); ctx.lineTo(-160, -6); ctx.lineTo(-165, 20);
ctx.lineTo(-130, 25); ctx.lineTo(-30, 15); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#3A2210'; ctx.fillRect(-160, 18, 130, 7);
ctx.fillStyle = '#4A2E15'; ctx.fillRect(-160, -6, 130, 3);
ctx.fillStyle = '#2A2A2A'; ctx.beginPath();
ctx.moveTo(-160, -6); ctx.lineTo(-165, 18); ctx.lineTo(-160, 20); ctx.lineTo(-155, 2); ctx.closePath(); ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.beginPath(); ctx.arc(-70, 18, 12, 0, Math.PI, true); ctx.fill();
ctx.fillStyle = '#5E3A20'; ctx.fillRect(-30, -6, 215, 16);
ctx.fillStyle = '#8A5A3A'; ctx.fillRect(-30, -6, 215, 3);
ctx.fillStyle = '#3A2210'; ctx.fillRect(-30, 7, 215, 3);
ctx.fillStyle = '#4A2E15'; ctx.fillRect(-30, -18, 195, 12);
ctx.fillStyle = '#1A1A1A'; ctx.fillRect(-30, -6, 215, 2);
[60, 150].forEach(x => {
ctx.fillStyle = '#333333'; ctx.fillRect(x, -20, 8, 32);
ctx.fillStyle = '#666666'; ctx.fillRect(x, -20, 2, 32);
});
ctx.fillStyle = '#5A5A5A'; ctx.fillRect(185, -5, 30, 10);
ctx.fillStyle = '#9A9A9A'; ctx.fillRect(185, -5, 30, 2);
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(210, -7, 5, 14);
ctx.fillStyle = '#555555'; ctx.fillRect(200, -12, 6, 5);
ctx.fillStyle = '#888888'; ctx.beginPath();
ctx.moveTo(202, -12); ctx.lineTo(205, -12); ctx.lineTo(203.5, -20); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(-30, -12, 35, 24);
ctx.fillStyle = '#666666'; ctx.fillRect(-30, -12, 35, 2);
ctx.save(); ctx.translate(gunState.boltOffset, 0);
ctx.fillStyle = '#4A4A4A'; ctx.fillRect(-10, -24, 6, 12);
ctx.fillStyle = '#888888'; ctx.beginPath(); ctx.arc(-7, -24, 6, 0, Math.PI * 2); ctx.fill();
ctx.restore();
ctx.strokeStyle = '#555555'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(0, 12, 10, 0, Math.PI); ctx.stroke();
ctx.fillStyle = '#333333'; ctx.fillRect(-2, 8, 4, 12);
ctx.fillStyle = '#333333'; ctx.fillRect(20, -18, 12, 6);
ctx.fillStyle = '#666666'; ctx.fillRect(24, -32, 4, 14);
ctx.fillStyle = '#1A1A1A'; ctx.fillRect(25, -32, 2, 4);
ctx.fillStyle = '#222222'; ctx.beginPath(); ctx.arc(64, 10, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(-145, 25, 4, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#5A4A3A'; ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(-145, 25); ctx.quadraticCurveTo(-50, 65, 20, 40); ctx.lineTo(64, 10); ctx.stroke();
}
// ============================================================
// 大刀
// ============================================================
function drawDadao(ctx) {
const bladeStartX = 5, bladeLength = 140, bladeEndX = bladeStartX + bladeLength;
const backY0 = -5, backY1 = -6, edgeY0 = 2, edgeY1 = 5, tipY = -10;
ctx.fillStyle = D.blade; ctx.beginPath();
ctx.moveTo(bladeStartX, backY0); ctx.lineTo(bladeEndX - 15, backY1);
ctx.lineTo(bladeEndX, tipY); ctx.lineTo(bladeEndX - 5, edgeY1);
ctx.lineTo(bladeStartX, edgeY0); ctx.closePath(); ctx.fill();
ctx.fillStyle = D.bladeLite; ctx.beginPath();
ctx.moveTo(bladeStartX, backY0); ctx.lineTo(bladeEndX - 15, backY1);
ctx.lineTo(bladeEndX - 8, (backY1 + tipY) / 2); ctx.lineTo(bladeStartX, backY0 + 3);
ctx.closePath(); ctx.fill();
ctx.fillStyle = D.bladeDark; ctx.beginPath();
ctx.moveTo(bladeStartX, edgeY0); ctx.lineTo(bladeEndX - 5, edgeY1);
ctx.lineTo(bladeEndX - 12, edgeY1 - 2); ctx.lineTo(bladeStartX, edgeY0 - 2);
ctx.closePath(); ctx.fill();
ctx.strokeStyle = D.back; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(bladeStartX, backY0); ctx.lineTo(bladeEndX - 15, backY1); ctx.stroke();
ctx.strokeStyle = D.bladeEdge; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(bladeStartX, edgeY0); ctx.lineTo(bladeEndX - 5, edgeY1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(bladeEndX - 15, backY1); ctx.lineTo(bladeEndX, tipY); ctx.lineTo(bladeEndX - 5, edgeY1); ctx.stroke();
ctx.strokeStyle = D.bloodGroove; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(bladeStartX + 8, backY0 + 2); ctx.lineTo(bladeEndX - 20, backY1 + 3); ctx.stroke();
ctx.beginPath(); ctx.moveTo(bladeStartX + 8, backY0 + 4); ctx.lineTo(bladeEndX - 20, backY1 + 5); ctx.stroke();
const guardW = 14, guardH = 20, guardX = bladeStartX - guardW + 1, guardY = -guardH / 2;
ctx.fillStyle = D.guard; ctx.beginPath();
ctx.moveTo(guardX, guardY); ctx.lineTo(guardX + guardW, guardY - 2);
ctx.lineTo(guardX + guardW, guardY + guardH); ctx.lineTo(guardX, guardY + guardH + 2);
ctx.closePath(); ctx.fill();
ctx.fillStyle = D.guardDark; ctx.fillRect(guardX, guardY + guardH - 3, guardW, 3);
ctx.fillStyle = '#D4B04A'; ctx.fillRect(guardX + 1, guardY + 1, 2, guardH - 2);
const handleStartX = guardX - 3, handleLength = 70, handleEndX = handleStartX - handleLength;
const handleW = 10, handleY = -handleW / 2;
ctx.fillStyle = D.handle; ctx.fillRect(handleEndX, handleY, handleLength, handleW);
ctx.fillStyle = '#5A3A2A'; ctx.fillRect(handleEndX, handleY, handleLength, 2);
ctx.strokeStyle = D.wrap; ctx.lineWidth = 2;
for (let i = 0; i < 10; i++) {
const wx = handleEndX + 5 + i * 6;
ctx.beginPath(); ctx.moveTo(wx, handleY - 1); ctx.lineTo(wx + 3, handleY + handleW + 1); ctx.stroke();
}
ctx.strokeStyle = D.wrapDark; ctx.lineWidth = 0.8;
for (let i = 0; i < 10; i++) {
const wx = handleEndX + 5 + i * 6;
ctx.beginPath(); ctx.moveTo(wx + 2, handleY - 1); ctx.lineTo(wx + 5, handleY + handleW + 1); ctx.stroke();
}
const ringX = handleEndX - 3, ringY = 0, ringR = 7;
ctx.strokeStyle = D.ring; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(ringX, ringY, ringR, 0, Math.PI * 2); ctx.stroke();
ctx.strokeStyle = '#B0B0B0'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(ringX - 1, ringY - 1, ringR - 1, 0, Math.PI * 2); ctx.stroke();
ctx.strokeStyle = D.redCloth; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(ringX - ringR, ringY - 2);
ctx.quadraticCurveTo(ringX - ringR - 10, ringY - 10, ringX - ringR - 18, ringY - 5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ringX - ringR, ringY + 2);
ctx.quadraticCurveTo(ringX - ringR - 8, ringY + 9, ringX - ringR - 15, ringY + 5); ctx.stroke();
ctx.fillStyle = D.bladeDark; ctx.fillRect(bladeStartX - 2, backY0 - 2, 4, edgeY0 - backY0 + 4);
}
// ============================================================
// 动画参数计算
// ============================================================
function computeAnim() {
const phase = time * params.walkSpeed;
const anim = {
bob: 0, lean: params.lean, bodyRotation: 0, headRot: 0,
leftLegRot: 0, rightLegRot: 0,
leftArmRot: 0, rightArmRot: 0,
aiming: false,
armSwingMod: 0, bodySwingMod: 0, dadaoRot: 0,
};
const elapsed = (performance.now() - stateStartTime) / 1000;
switch (currentState) {
case 'idle':
anim.leftArmRot = -0.3;
anim.rightArmRot = 0.2;
anim.bob = Math.sin(time * 2) * 1.5;
break;
case 'dadao_idle':
// 持刀待机
anim.leftArmRot = -1.2;
anim.rightArmRot = -0.9;
anim.bob = Math.sin(time * 2) * 1.5;
anim.dadaoRot = 0;
break;
case 'walk':
anim.leftLegRot = Math.sin(phase) * params.legSwing;
anim.rightLegRot = Math.sin(phase - Math.PI * 0.6) * params.legSwing;
anim.leftArmRot = -0.7 - Math.sin(phase) * params.armSwing;
anim.rightArmRot = 0.2 + Math.sin(phase - Math.PI * 0.6) * params.armSwing;
anim.bob = Math.abs(Math.sin(phase)) * params.bodyBob;
break;
case 'run':
anim.leftLegRot = Math.sin(phase) * (params.legSwing * 1.4);
anim.rightLegRot = Math.sin(phase - Math.PI * 0.65) * (params.legSwing * 1.4);
anim.leftArmRot = -0.9 - Math.sin(phase) * (params.armSwing * 1.6);
anim.rightArmRot = 0.4 + Math.sin(phase - Math.PI * 0.65) * (params.armSwing * 1.6);
anim.bob = Math.abs(Math.sin(phase)) * (params.bodyBob * 1.7);
anim.lean = params.lean + 0.15;
break;
// ★ 合并:瞄准 + 开火 + 复位
case 'fire': {
const t = Math.min(1, elapsed / 1.0); // 整个动作 1 秒
// 0 - 0.3s:抬枪瞄准
// 0.3 - 0.5s:开火
// 0.5 - 1.0s:收枪复位
let aimP, recoil = 0;
if (t < 0.3) {
aimP = t / 0.3; // 0 -> 1
} else if (t < 0.5) {
aimP = 1;
const k = (t - 0.3) / 0.2;
recoil = Math.sin(k * Math.PI); // 后坐力峰值
} else {
aimP = 1 - (t - 0.5) / 0.5; // 1 -> 0
}
anim.leftLegRot = lerp(0, 0.05, aimP);
anim.rightLegRot = lerp(0, -0.05, aimP);
anim.leftArmRot = lerp(-0.3, -1.1, aimP) + recoil * 0.15;
anim.rightArmRot = lerp(0.2, -0.45, aimP) + recoil * 0.2;
anim.bodyRotation = lerp(0, -0.2, aimP) + recoil * 0.05;
anim.lean = params.lean + lerp(0, 0.05, aimP) - recoil * 0.08;
anim.aiming = aimP > 0.5;
break;
}
case 'bolt': {
const t = elapsed % 1.5;
let boltOffset = 0;
if (t < 0.3) boltOffset = t / 0.3 * 10;
else if (t < 0.6) boltOffset = 10;
else if (t < 0.9) boltOffset = 10 * (1 - (t - 0.6) / 0.3);
gunState.boltOffset = boltOffset;
anim.leftLegRot = 0.05; anim.rightLegRot = -0.05;
anim.leftArmRot = -1.1;
anim.rightArmRot = -0.45 - (boltOffset > 5 ? 0.3 : 0);
anim.bodyRotation = -0.2;
anim.aiming = true;
break;
}
case 'hit': {
const t = Math.min(1, elapsed / 0.8);
let leanMod, headMod, recoilX;
if (t < 0.2) { const k = t / 0.2; leanMod = lerp(0, -0.4, k); headMod = lerp(0, 0.35, k); recoilX = lerp(0, -20, k); }
else if (t < 0.5) { const k = (t - 0.2) / 0.3; leanMod = lerp(-0.4, -0.2, easeInOut(k)); headMod = lerp(0.35, 0.1, easeInOut(k)); recoilX = lerp(-20, -8, easeInOut(k)); }
else { const k = (t - 0.5) / 0.5; leanMod = lerp(-0.2, 0, easeOut(k)); headMod = lerp(0.1, 0, easeOut(k)); recoilX = lerp(-8, 0, easeOut(k)); }
anim.leftArmRot = -0.3 + leanMod * 0.5;
anim.rightArmRot = 0.2 + leanMod * 0.3;
anim.headRot = headMod;
anim.lean = params.lean + leanMod;
anim.bodyRecoilX = recoilX;
break;
}
case 'swing': {
// 大刀挥砍
const swingP = Math.min(1, elapsed / 0.6);
if (swingP < 0.2) {
const k = swingP / 0.2;
anim.armSwingMod = lerp(0, -0.8, easeOut(k));
anim.dadaoRot = lerp(0, -1.5, easeOut(k));
} else if (swingP < 0.45) {
const k = (swingP - 0.2) / 0.25;
anim.armSwingMod = lerp(-0.8, 0.4, easeInOut(k));
anim.dadaoRot = lerp(-1.5, 0.9, easeInOut(k));
} else {
const k = (swingP - 0.45) / 0.55;
anim.armSwingMod = lerp(0.4, 0, easeOut(k));
anim.dadaoRot = lerp(0.9, 0, easeOut(k));
}
anim.leftArmRot = -1.2 + anim.armSwingMod * 0.3;
anim.rightArmRot = -0.9 + anim.armSwingMod * 0.3;
anim.leftLegRot = 0.1; anim.rightLegRot = -0.1;
break;
}
}
return anim;
}
// ============================================================
// 绘制人物
// ============================================================
function drawSoldier(ctx, cx, groundY, scale, anim) {
const shoulderY = -S.torsoH;
const headY = shoulderY;
const legSpacing = S.torsoW / 4 - 6;
const armSpacing = S.torsoW / 2 + S.armW / 2 - 2;
const isMelee = (currentState === 'swing' || currentState === 'dadao_idle');
const bodyRot = (anim.bodyRotation || 0) + (anim.bodySwingMod || 0);
const bodyRecoilX = anim.bodyRecoilX || 0;
// 身体
ctx.save();
ctx.translate(cx + bodyRecoilX, groundY + anim.bob);
ctx.scale(scale, scale);
ctx.rotate(anim.lean + bodyRot);
drawLeg(ctx, -legSpacing, 0, anim.leftLegRot);
drawLeg(ctx, legSpacing, 0, anim.rightLegRot);
drawTorso(ctx, 0, 0, 0);
drawArm(ctx, armSpacing, shoulderY, anim.rightArmRot, 'right', 0.85);
drawHead(ctx, 0, headY, anim.headRot);
ctx.restore();
// 武器
if (isMelee) {
// ★ 大刀:绕手部旋转,而不是绕护手旋转
const handX = cx + bodyRecoilX + dadaoParams.handX * scale;
const handY = groundY + anim.bob + dadaoParams.handY * scale;
const dadaoAngle = dadaoParams.baseAngle + (anim.dadaoRot || 0);
ctx.save();
ctx.translate(handX, handY);
ctx.scale(scale, scale);
ctx.rotate(dadaoAngle);
// 关键修正:把手握持的刀柄位置(大约在 x=-50)移到原点
// 这样旋转轴心就是手,而不是护手
ctx.translate(50, 0);
drawDadao(ctx);
ctx.restore();
} else {
// 步枪
const handLY = S.armH - 6;
const armC = Math.cos(anim.leftArmRot); const armS = Math.sin(anim.leftArmRot);
const localX = -armSpacing - handLY * armS;
const localY = shoulderY + handLY * armC;
const bodyC = Math.cos(bodyRot); const bodyS = Math.sin(bodyRot);
const rotX = localX * bodyC - localY * bodyS;
const rotY = localX * bodyS + localY * bodyC;
const handX = cx + (bodyRecoilX + rotX) * scale;
const handY = groundY + anim.bob + rotY * scale;
const aimP = anim.aiming ? 1 : 0;
const gunOffsetX = lerp(0, 15, aimP);
const gunOffsetY = lerp(0, -18, aimP);
ctx.save();
ctx.translate(handX + gunOffsetX, handY + gunOffsetY);
ctx.scale(scale, scale);
ctx.rotate(lerp(-0.05, 0, aimP));
ctx.save();
ctx.translate(gunState.recoilX, -gunState.recoilY);
ctx.rotate(gunState.recoilRot);
drawHanyangRifle(ctx);
ctx.restore();
if (gunState.flashTimer > 0) {
const fa = gunState.flashTimer / 0.06;
ctx.save(); ctx.globalAlpha = fa; ctx.translate(200, -5);
ctx.fillStyle = '#FFFF00';
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(30,-15); ctx.lineTo(15,0); ctx.lineTo(35,15); ctx.lineTo(0,5); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#FFAA00'; ctx.beginPath(); ctx.arc(5,0,15,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(5,0,8,0,Math.PI*2); ctx.fill();
ctx.restore();
}
ctx.restore();
}
// 前手(画在武器上层)
ctx.save();
ctx.translate(cx + bodyRecoilX, groundY + anim.bob);
ctx.scale(scale, scale);
ctx.rotate(anim.lean + bodyRot);
drawArm(ctx, -armSpacing, shoulderY, anim.leftArmRot, 'left');
ctx.restore();
// 锚点显示
if (show.anchor) {
ctx.fillStyle = 'rgba(255, 50, 50, 0.9)';
ctx.beginPath(); ctx.arc(cx - armSpacing * scale, groundY + shoulderY * scale, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(cx + armSpacing * scale, groundY + shoulderY * scale, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(cx - legSpacing * scale, groundY, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(cx + legSpacing * scale, groundY, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(cx, groundY + headY * scale, 4, 0, Math.PI * 2); ctx.fill();
if (isMelee) {
ctx.fillStyle = 'rgba(255, 200, 50, 0.9)';
ctx.beginPath();
ctx.arc(cx + dadaoParams.handX * scale, groundY + dadaoParams.handY * scale, 6, 0, Math.PI * 2);
ctx.fill();
}
}
}
// ============================================================
// 主循环
// ============================================================
let lastTime = performance.now();
function animate(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 0.1) return;
if (playing) time += dt * playSpeed;
// 火器特效(仅在 fire 状态的时间窗口内触发)
if (currentState === 'fire') {
const elapsed = (performance.now() - stateStartTime) / 1000;
if (elapsed > 0.3 && elapsed < 0.5) {
// 开火窗口
const t = (elapsed - 0.3) / 0.2;
gunState.flashTimer = t < 0.3 ? 0.06 * (1 - t / 0.3) : 0;
gunState.recoilX = -15 * Math.sin(t * Math.PI);
gunState.recoilY = 8 * Math.sin(t * Math.PI);
gunState.recoilRot = 0.08 * Math.sin(t * Math.PI);
} else {
gunState.flashTimer = 0;
gunState.recoilX = 0; gunState.recoilY = 0; gunState.recoilRot = 0;
}
} else {
gunState.flashTimer = 0;
gunState.recoilX = 0; gunState.recoilY = 0; gunState.recoilRot = 0;
}
if (currentState !== 'bolt') gunState.boltOffset = 0;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0F1A0F'; ctx.fillRect(0, 0, W, H);
if (show.grid) {
const gridSize = 80;
ctx.strokeStyle = 'rgba(74, 90, 74, 0.12)'; ctx.lineWidth = 1;
for (let x = 0; x < W; x += gridSize) {
for (let y = 0; y < H; y += gridSize) {
ctx.strokeRect(x, y, gridSize, gridSize);
}
}
}
ctx.save();
ctx.translate(camera.x, camera.y);
ctx.rotate(camera.rotation);
ctx.scale(camera.zoom, camera.zoom);
ctx.translate(-camera.x, -camera.y);
const groundY = H / 2 + 100;
const cx = W / 2;
const scale = 1.8;
if (show.shadow) {
const anim = computeAnim();
const shadowScale = 1 - anim.bob * 0.01;
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.beginPath();
ctx.ellipse(cx, groundY + 5, 75 * shadowScale, 12 * shadowScale, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.strokeStyle = 'rgba(74, 90, 74, 0.4)'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();
const anim = computeAnim();
drawSoldier(ctx, cx, groundY, scale, anim);
ctx.restore();
requestAnimationFrame(animate);
}
// ============================================================
// 交互事件
// ============================================================
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('mousedown', (e) => {
camera.isDragging = true; camera.dragButton = e.button;
camera.lastX = e.clientX; camera.lastY = e.clientY;
});
canvas.addEventListener('mousemove', (e) => {
if (!camera.isDragging) return;
const dx = e.clientX - camera.lastX;
const dy = e.clientY - camera.lastY;
if (camera.dragButton === 0) { camera.x += dx; camera.y += dy; }
else if (camera.dragButton === 2) { camera.rotation += dx * 0.005; }
camera.lastX = e.clientX; camera.lastY = e.clientY;
});
canvas.addEventListener('mouseup', () => { camera.isDragging = false; });
canvas.addEventListener('mouseleave', () => { camera.isDragging = false; });
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
camera.zoom *= e.deltaY > 0 ? 0.9 : 1.1;
camera.zoom = Math.max(0.2, Math.min(5.0, camera.zoom));
document.getElementById('camZoom').value = camera.zoom;
document.getElementById('camZoomVal').textContent = camera.zoom.toFixed(2);
}, { passive: false });
document.querySelectorAll('[data-state]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-state]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentState = btn.dataset.state;
stateStartTime = performance.now();
time = 0;
const names = { idle:'待机', walk:'行走', run:'奔跑', fire:'开火(含瞄准)', bolt:'拉栓', hit:'受击', swing:'挥砍', dadao_idle:'持刀待机' };
document.getElementById('stateDisplay').textContent = names[currentState];
});
});
document.querySelector('[data-state="idle"]').classList.add('active');
function bindSlider(id, key) {
const el = document.getElementById(id);
const valEl = document.getElementById(id + 'Val');
el.addEventListener('input', () => {
params[key] = parseFloat(el.value);
valEl.textContent = params[key].toFixed(2);
});
}
bindSlider('walkSpeed', 'walkSpeed');
bindSlider('legSwing', 'legSwing');
bindSlider('armSwing', 'armSwing');
bindSlider('bodyBob', 'bodyBob');
bindSlider('lean', 'lean');
document.getElementById('handX').addEventListener('input', (e) => {
dadaoParams.handX = parseFloat(e.target.value);
document.getElementById('handXVal').textContent = dadaoParams.handX.toFixed(0);
});
document.getElementById('handY').addEventListener('input', (e) => {
dadaoParams.handY = parseFloat(e.target.value);
document.getElementById('handYVal').textContent = dadaoParams.handY.toFixed(0);
});
document.getElementById('dadaoBaseAngle').addEventListener('input', (e) => {
dadaoParams.baseAngle = parseFloat(e.target.value);
document.getElementById('dadaoBaseAngleVal').textContent = dadaoParams.baseAngle.toFixed(2);
});
document.getElementById('playSpeed').addEventListener('input', (e) => {
playSpeed = parseFloat(e.target.value);
document.getElementById('playSpeedVal').textContent = playSpeed.toFixed(1);
});
document.getElementById('btnPause').addEventListener('click', () => {
playing = !playing;
document.getElementById('btnPause').textContent = playing ? '⏸ 暂停' : '▶ 继续';
});
document.getElementById('btnReset').addEventListener('click', () => {
time = 0; stateStartTime = performance.now(); playing = true;
document.getElementById('btnPause').textContent = '⏸ 暂停';
});
document.getElementById('camZoom').addEventListener('input', (e) => {
camera.zoom = parseFloat(e.target.value);
document.getElementById('camZoomVal').textContent = camera.zoom.toFixed(2);
});
document.getElementById('camRot').addEventListener('input', (e) => {
camera.rotation = parseFloat(e.target.value) * Math.PI / 180;
document.getElementById('camRotVal').textContent = e.target.value;
});
document.getElementById('btnResetCam').addEventListener('click', () => {
camera.x = W / 2; camera.y = H / 2;
camera.zoom = 1.2; camera.rotation = 0;
document.getElementById('camZoom').value = 1.2;
document.getElementById('camZoomVal').textContent = '1.20';
document.getElementById('camRot').value = 0;
document.getElementById('camRotVal').textContent = '0';
});
document.getElementById('btnGrid').addEventListener('click', (e) => {
show.grid = !show.grid; e.target.classList.toggle('active');
});
document.getElementById('btnAnchor').addEventListener('click', (e) => {
show.anchor = !show.anchor; e.target.classList.toggle('active');
});
document.getElementById('btnShadow').addEventListener('click', (e) => {
show.shadow = !show.shadow; e.target.classList.toggle('active');
});
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 红军战士 - 动画调试工具 v3
Creator: TurboKoala83
Libraries: none
Complexity: complex (744 lines, 33.9 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: v3-turbokoala83" to link back to the original. Then publish at arcadelab.ai/publish.