🎮ArcadeLab

红军战士 - 纯人物 R6 行走动画

by TurboKoala83
416 lines12.2 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>红军战士 - 纯人物 R6 行走动画</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);
  }
  .tips {
    position: absolute;
    bottom: 20px;
    color: #888;
    font-size: 12px;
  }
</style>
</head>
<body>
<div class="info">★ 中国工农红军 · 行走动画测试 (无武器) ★</div>
<div class="tips">点击画面暂停/继续</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', belt:       '#3A2A1A',
};

// ========== Roblox R6 尺寸 ==========
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();
}

// =====================================================================
// 【部件 1】头部(轴心:脖子底部)
// =====================================================================
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;
  const 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;
  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);

  // 帽子左侧阴影
  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();
}

// =====================================================================
// 【部件 2】躯干(轴心:髋部底部,向上绘制)
// =====================================================================
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();
}

// =====================================================================
// 【部件 3】手臂(轴心:肩膀顶部,向下绘制)
// =====================================================================
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();
}

// =====================================================================
// 【部件 4】腿(轴心:髋部顶部,向下绘制)
// =====================================================================
function drawLeg(ctx, x, y, rotation = 0, side = 'left') {
  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 drawSoldier(ctx, centerX, groundY, scale = 1, anim = {}) {
  const a = {
    bob:         anim.bob         || 0,
    leftLegRot:  anim.leftLegRot  || 0,
    rightLegRot: anim.rightLegRot || 0,
    leftArmRot:  anim.leftArmRot  || 0,
    rightArmRot: anim.rightArmRot || 0,
    headRot:     anim.headRot     || 0,
  };

  // 坐标基准线:
  // y = 0 代表髋部(两条腿的顶端)
  // y = -S.torsoH 代表肩膀(两条手臂的顶端)
  const shoulderY = -S.torsoH;
  const neckY     = shoulderY - S.neckH;

  const legSpacing = S.torsoW / 2 + 2; // 两条腿的间距
  const armSpacing = S.torsoW / 2 + S.armW / 2 - 2; // 两条手臂的间距

  ctx.save();
  // 整体平移到地面中心点,并应用呼吸起伏
  ctx.translate(centerX, groundY + a.bob);
  ctx.scale(scale, scale);

  // ===== 绘制顺序(从后到前)=====

  // 1. 左腿(轴心在髋部 y=0 处,向左偏移)
  drawLeg(ctx, -legSpacing, 0, a.leftLegRot, 'left');
  
  // 2. 右腿
  drawLeg(ctx,  legSpacing, 0, a.rightLegRot, 'right');

  // 3. 躯干(轴心在髋部 y=0 处)
  drawTorso(ctx, 0, 0, 0);

  // 4. 右臂(轴心在肩膀 shoulderY 处,向右偏移)
  drawArm(ctx, armSpacing, shoulderY, a.rightArmRot, 'right');

  // 5. 头部(轴心在脖子 neckY 处)
  drawHead(ctx, 0, neckY, a.headRot);

  // 6. 左臂(轴心在肩膀 shoulderY 处,向左偏移)
  drawArm(ctx, -armSpacing, shoulderY, a.leftArmRot, 'left');

  ctx.restore();
}

// =====================================================================
// 主循环 —— 纯粹的行走动画
// =====================================================================
let time = 0;
let paused = false;

const WALK = {
  speed:      4.0,    // 步行速度(周期频率)
  legSwing:   0.55,   // 腿部前后摆动的最大角度(弧度制,约 30度)
  armSwing:   0.45,   // 手臂前后摆动的最大角度
  bodyBob:    3.5,    // 身体上下起伏的高度
  headBob:    0.03    // 头部轻微晃动
};

function animate() {
  if (!paused) time += 0.02;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // ===== 计算行走相位 =====
  // 相位不断增加,正弦函数驱动摆动
  const phase = time * WALK.speed;

  // 腿部角度计算(相差 180 度,即先迈左腿再迈右腿)
  const leftLegRot  = Math.sin(phase) * WALK.legSwing;
  const rightLegRot = -Math.sin(phase) * WALK.legSwing;

  // 手臂角度计算(和腿部反向摆动,左腿前迈时,左臂后摆,右臂前摆)
  const leftArmRot  = -Math.sin(phase) * WALK.armSwing;
  const rightArmRot = Math.sin(phase) * WALK.armSwing;

  // 身体起伏:每迈一步身体上下起伏一次(使用绝对值使频率翻倍)
  // 当双腿并拢时(phase = 0, PI),身体处于最低点;当腿分开时,身体处于最高点
  const bob = Math.abs(Math.sin(phase)) * WALK.bodyBob;

  // 头部晃动
  const headRot = Math.sin(phase * 2) * WALK.headBob;

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

  const groundY = canvas.height - 100; // 留出地面空间
  const cx = canvas.width / 2;
  const scale = 1.8; // 整体缩放比例

  // ===== 绘制环境 =====
  // 静态地面
  ctx.fillStyle = '#152a1f';
  ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);

  // 地面上的参照线
  ctx.strokeStyle = '#1f3d2d';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(0, groundY + 2);
  ctx.lineTo(canvas.width, groundY + 2);
  ctx.stroke();

  // 人物阴影(跟随起伏稍微变化)
  const shadowScale = 1 - bob * 0.015;
  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();

  // ===== 绘制人物 =====
  drawSoldier(ctx, cx, groundY, scale, anim);

  requestAnimationFrame(animate);
}

// 点击暂停
canvas.addEventListener('click', () => { paused = !paused; });

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

Game Source: 红军战士 - 纯人物 R6 行走动画

Creator: TurboKoala83

Libraries: none

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