🎮ArcadeLab

红军战士 - 修正行走动画

by TurboKoala83
341 lines12.9 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>红军战士 - 修正行走动画</title>
<style>
  body { margin: 0; background: #1a1a2e; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; }
  canvas { background: linear-gradient(180deg, #16213e 0%, #0f3460 100%); border-radius: 12px; box-shadow: 0 0 40px rgba(196, 30, 58, 0.3); }
  .info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: #c41e3a; font-size: 14px; letter-spacing: 4px; text-shadow: 0 0 10px rgba(196, 30, 58, 0.8); }
</style>
</head>
<body>
<div class="info">★ 中国工农红军 · 湘江战役后 · 行军动画修正 ★</div>
<canvas id="game" width="700" height="600"></canvas>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

const C = {
  uniform:'#5A6B7C', uniformDark:'#3D4A57', uniformLite:'#6E8090',
  collar:'#C41E3A', skin:'#E8B88A', skinDark:'#C99A6E',
  hat:'#4A5A6B', hatDark:'#37444F', bind:'#8A8A8A',
  straw:'#9A7A4A', strawDark:'#7A5A2A', wood:'#5A3A1A',
  metal:'#4A4A4A', belt:'#3A2A1A', strap:'#8B7355',
};

const S = {
  headW:52, headH:52, neckH:6,
  torsoW:84, torsoH:76,
  armW:36, armH:76,
  legW:44, legH:84,
};

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, neckH } = S;
  const headX = -headW / 2, headY = -headH - neckH;
  ctx.fillStyle = C.skinDark; ctx.fillRect(-8, -neckH, 16, neckH);
  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, hatH = 22;
  const hatX = -hatW / 2, 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);
  ctx.fillStyle = C.hatDark;
  ctx.beginPath();
  ctx.moveTo(hatX, hatY + 8); ctx.lineTo(hatX + 8, hatY + 2);
  ctx.lineTo(hatX + 8, hatY + hatH); ctx.lineTo(hatX + 2, hatY + hatH);
  ctx.closePath(); ctx.fill();
  const starCY = hatY + 9, starR = 7;
  ctx.fillStyle = C.collar; ctx.beginPath();
  for (let i = 0; i < 5; i++) {
    const angle = (i * 72 - 90) * Math.PI / 180;
    const x1 = starR * Math.cos(angle), y1 = starCY + starR * Math.sin(angle);
    const angle2 = ((i + 1) * 72 - 90) * Math.PI / 180;
    const innerR = starR * 0.4;
    const x2 = innerR * Math.cos(angle2 - Math.PI / 5), y2 = starCY + innerR * Math.sin(angle2 - 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, 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 = 0, side = 'left') {
  ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
  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 drawLegFixed(ctx, x, y, legRot = 0, kneeRot = 0, side = 'left') {
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(legRot); // 整条腿绕髋部旋转

  const { legW, legH } = S;
  const legX = -legW / 2;
  const kneeY = legH * 0.5; // 膝盖位置:腿长的中点

  // ===== 大腿(从髋部到膝盖)=====
  ctx.fillStyle = C.uniform;
  roundRect(ctx, legX, 0, legW, kneeY, 3); ctx.fill();
  ctx.fillStyle = C.uniformDark;
  ctx.fillRect(legX, 0, 6, kneeY);

  // ===== 小腿(从膝盖到脚底,以膝盖为轴心旋转)=====
  ctx.save();
  ctx.translate(0, kneeY);       // 移到膝盖位置
  ctx.rotate(kneeRot);           // 小腿绕膝盖旋转

  const calfY = 0;
  const calfH = legH * 0.5;

  // 绑腿
  const bindY = calfY;
  const bindH = calfH * 0.84;
  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 = calfH * 0.16;
  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(); // 结束小腿变换
  ctx.restore(); // 结束整条腿变换
}

// =====================================================================
// 步枪(不变)
// =====================================================================
function drawRifle(ctx, x, y, rotation = 0) {
  ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);
  ctx.fillStyle = C.wood; roundRect(ctx, -10, -8, 24, 50, 4); ctx.fill();
  ctx.fillStyle = C.metal; ctx.fillRect(-4, -70, 8, 70);
  ctx.fillStyle = '#7A7A7A'; ctx.fillRect(-4, -70, 3, 70);
  ctx.fillStyle = '#8A8A8A'; ctx.fillRect(-1.5, -95, 3, 25);
  ctx.strokeStyle = C.metal; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(2, 18, 5, 0, Math.PI); ctx.stroke();
  ctx.restore();
}

// =====================================================================
// 道具(不变)
// =====================================================================
function drawGear(ctx, waistX, waistY) {
  ctx.save(); ctx.translate(waistX, waistY);
  ctx.fillStyle = '#A89878'; roundRect(ctx, 15, -15, 20, 16, 3); ctx.fill();
  ctx.strokeStyle = '#7A6858'; ctx.lineWidth = 1; ctx.stroke();
  ctx.fillStyle = '#4A5A3A'; roundRect(ctx, -35, -10, 14, 18, 4); ctx.fill();
  ctx.fillStyle = '#2A3A1A'; ctx.fillRect(-30, -14, 4, 5);
  ctx.restore();
}

// =====================================================================
// 装配
// =====================================================================
function drawSoldier(ctx, centerX, groundY, scale = 1, anim = {}) {
  const a = {
    bob: anim.bob || 0,
    headRot: anim.headRot || 0,
    torsoRot: anim.torsoRot || 0,
    leftArmRot: anim.leftArmRot || 0,
    rightArmRot: anim.rightArmRot || 0,
    leftLegRot: anim.leftLegRot || 0,
    rightLegRot: anim.rightLegRot || 0,
    leftKneeRot: anim.leftKneeRot || 0,
    rightKneeRot: anim.rightKneeRot || 0,
  };

  const hipLY = -S.legH;
  const shoulderLY = -S.legH - S.torsoH;
  const neckLY = shoulderLY;
  const legSpacing = S.torsoW / 4;
  const armSpacing = S.torsoW / 2 + S.armW / 2 - 2;

  ctx.save();
  ctx.translate(centerX, groundY + a.bob);
  ctx.scale(scale, scale);

  // 绘制顺序
  drawLegFixed(ctx, -legSpacing, hipLY, a.leftLegRot, a.leftKneeRot, 'left');
  drawLegFixed(ctx, legSpacing, hipLY, a.rightLegRot, a.rightKneeRot, 'right');
  drawTorso(ctx, 0, hipLY, a.torsoRot);
  drawArm(ctx, armSpacing, shoulderLY, a.rightArmRot, 'right');
  drawHead(ctx, 0, neckLY, a.headRot);
  drawArm(ctx, -armSpacing, shoulderLY, a.leftArmRot, 'left');

  ctx.restore();

  // 锚点
  const bob = a.bob;
  function handWorld(armPivotLX, armPivotLY, armRot) {
    const handLY = S.armH - 6;
    const c = Math.cos(armRot), s = Math.sin(armRot);
    return {
      x: centerX + (armPivotLX - handLY * s) * scale,
      y: groundY + bob + (armPivotLY + handLY * c) * scale
    };
  }
  const leftHand = handWorld(-armSpacing, shoulderLY, a.leftArmRot);
  const rightHand = handWorld(armSpacing, shoulderLY, a.rightArmRot);
  const waist = { x: centerX, y: groundY + bob + (hipLY - 30) * scale };
  return { leftHand, rightHand, waist };
}

// =====================================================================
// 主循环
// =====================================================================
let time = 0;
const WALK = {
  speed: 5.0,
  legSwing: 0.45,     // 减小摆幅,避免腿叉太开
  kneeBend: 0.6,      // 膝盖弯曲幅度
  armSwing: 0.35,
  armCarry: 0.12,
  bodyBob: 3.5,
  headBob: 0.03,
  torsoSway: 0.04,
};

function animate() {
  time += 0.02;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const phase = time * WALK.speed;

  // 腿
  const leftLegRot  = Math.sin(phase) * WALK.legSwing;
  const rightLegRot = -Math.sin(phase) * WALK.legSwing;

  // 膝盖弯曲:腿向前摆时膝盖弯曲(脚抬起),腿向后蹬时膝盖伸直
  // 用 sin(phase) 的负半周期来驱动膝盖
  const leftKneeRot  = Math.max(0, -Math.sin(phase)) * WALK.kneeBend;
  const rightKneeRot = Math.max(0, Math.sin(phase)) * WALK.kneeBend;

  // 手臂
  const rightArmRot = -rightLegRot * WALK.armSwing;
  const leftArmRot  = -0.35 + (-leftLegRot) * WALK.armCarry;

  const bob = Math.abs(Math.sin(phase)) * WALK.bodyBob;
  const headRot = Math.sin(phase * 2) * WALK.headBob;
  const torsoRot = Math.sin(phase) * WALK.torsoSway;

  const anim = {
    bob, headRot, torsoRot,
    leftArmRot, rightArmRot,
    leftLegRot, rightLegRot,
    leftKneeRot, rightKneeRot,
  };

  const groundY = canvas.height - 80;
  const cx = canvas.width / 2;
  const scale = 1.8;

  // 地面
  ctx.fillStyle = '#1a3a2a';
  ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);
  const scrollOffset = (phase * 30) % 60;
  ctx.strokeStyle = '#2a5a3a'; ctx.lineWidth = 2;
  for (let x = -scrollOffset; x < canvas.width + 60; x += 60) {
    ctx.beginPath(); ctx.moveTo(x, groundY + 15); ctx.lineTo(x + 10, groundY + 5); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(x + 30, groundY + 25); ctx.lineTo(x + 38, groundY + 12); ctx.stroke();
  }

  // 阴影
  const shadowScale = 1 - bob * 0.02;
  ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
  ctx.beginPath();
  ctx.ellipse(cx, groundY + 5, 70 * shadowScale, 12 * shadowScale, 0, 0, Math.PI * 2);
  ctx.fill();

  const anchors = drawSoldier(ctx, cx, groundY, scale, anim);
  drawGear(ctx, anchors.waist.x, anchors.waist.y + 5);
  drawRifle(ctx, anchors.leftHand.x, anchors.leftHand.y - 10 * scale, leftArmRot - 0.3);

  requestAnimationFrame(animate);
}

animate();
</script>
</body>
</html>

Game Source: 红军战士 - 修正行走动画

Creator: TurboKoala83

Libraries: none

Complexity: complex (341 lines, 12.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: game-turbokoala83-mup0cbow" to link back to the original. Then publish at arcadelab.ai/publish.