红军战士 - 行走动画
by TurboKoala83451 lines13.6 KB
<!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); }
.controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #aaa; font-size: 12px; letter-spacing: 2px; }
</style>
</head>
<body>
<div class="info">★ 中国工农红军 · 湘江战役后 · 行军动画 ★</div>
<div class="controls">速度: 1.0x | 点击画布可暂停/继续</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',
};
// ========== 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();
}
// =====================================================================
// 【部件 5】汉阳造步枪
// =====================================================================
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();
}
// =====================================================================
// 【部件 6】道具
// =====================================================================
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,
};
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);
// 绘制顺序(从后到前)
drawLeg(ctx, -legSpacing, hipLY, a.leftLegRot, 'left');
drawLeg(ctx, legSpacing, hipLY, a.rightLegRot, '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);
const s = Math.sin(armRot);
const hx = -handLY * s;
const hy = handLY * c;
return {
x: centerX + (armPivotLX + hx) * scale,
y: groundY + bob + (armPivotLY + hy) * 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;
let paused = false;
// 行走参数(可调)
const WALK = {
speed: 5.0, // 步频(越大走得越快)
legSwing: 0.55, // 腿摆动幅度(弧度)
armSwing: 0.45, // 右臂摆动幅度
armCarry: 0.15, // 左臂持枪侧的微摆幅度
bodyBob: 4.0, // 身体起伏高度
headBob: 0.04, // 头部摆动幅度
torsoSway: 0.06, // 躯干左右摆动
};
function animate() {
if (!paused) 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;
// 手臂:和同侧腿反向(左腿后 → 左臂前)
// 右臂自由摆动,左臂因为持枪只微摆
const rightArmRot = -rightLegRot * WALK.armSwing;
const leftArmRot = -0.35 + (-leftLegRot) * WALK.armCarry;
// 身体上下起伏(每步一次,所以用 abs)
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,
};
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();
}
// 地面阴影(跟着 bob 稍微缩放,增强起伏感)
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);
}
// 点击暂停
canvas.addEventListener('click', () => { paused = !paused; });
animate();
</script>
</body>
</html>Game Source: 红军战士 - 行走动画
Creator: TurboKoala83
Libraries: none
Complexity: complex (451 lines, 13.6 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-mup08qso" to link back to the original. Then publish at arcadelab.ai/publish.