汉阳造步枪 - 瘦身精准版(无刺刀) + 开火动画
by TurboKoala83461 lines12.2 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); cursor: crosshair; }
.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); pointer-events: none; }
.controls { position: absolute; bottom: 20px; color: #888; font-size: 12px; letter-spacing: 2px; text-align: center; width: 100%; pointer-events: none; }
</style>
</head>
<body>
<div class="info">★ 汉阳造步枪 · 瘦身精准版(无刺刀) ★</div>
<div class="controls">点击画面 / 按空格键 开火 | 左键拖拽平移 | 右键拖拽旋转 | 滚轮缩放 | 双击重置视角</div>
<canvas id="game" width="900" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// ========== 摄像机系统 ==========
const camera = {
x: canvas.width / 2,
y: canvas.height / 2,
zoom: 1.0,
rotation: 0,
isDragging: false,
dragButton: 0,
lastX: 0,
lastY: 0,
shakeX: 0, // 开火镜头震动
shakeY: 0
};
// ========== 开火动画状态 ==========
let lastTime = performance.now();
let fireState = {
isFiring: false,
fireTime: 0,
recoilX: 0, // 后坐力水平偏移
recoilY: 0, // 后坐力垂直偏移
recoilRot: 0, // 后坐力旋转
flashTimer: 0, // 火光持续时间
shells: [], // 抛壳列表
smokeParticles: [] // 烟雾粒子列表
};
// =====================================================================
// 【原汁原味】汉阳造步枪 - 瘦身精准版(无刺刀)
// 原点 (0,0) 在【扳机位置】,向右为枪口方向
// =====================================================================
function drawHanyangRifle(ctx) {
// 1. 枪托后部(木质)
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();
// 2. 下护木
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);
// 3. 上护木
ctx.fillStyle = '#4A2E15';
ctx.fillRect(-30, -18, 195, 12);
ctx.fillStyle = '#1A1A1A';
ctx.fillRect(-30, -6, 215, 2);
// 4. 金属枪箍
const clampPositions = [60, 150];
clampPositions.forEach(x => {
ctx.fillStyle = '#333333';
ctx.fillRect(x, -20, 8, 32);
ctx.fillStyle = '#666666';
ctx.fillRect(x, -20, 2, 32);
});
// 5. 外露枪管
const barrelStart = 185;
const barrelLength = 30;
ctx.fillStyle = '#5A5A5A';
ctx.fillRect(barrelStart, -5, barrelLength, 10);
ctx.fillStyle = '#9A9A9A';
ctx.fillRect(barrelStart, -5, barrelLength, 2);
// 6. 枪口帽与准星
const muzzleX = barrelStart + barrelLength;
ctx.fillStyle = '#2A2A2A';
ctx.fillRect(muzzleX - 5, -7, 5, 14);
ctx.fillStyle = '#555555';
ctx.fillRect(muzzleX - 15, -12, 6, 5);
ctx.fillStyle = '#888888';
ctx.beginPath();
ctx.moveTo(muzzleX - 13, -12);
ctx.lineTo(muzzleX - 10, -12);
ctx.lineTo(muzzleX - 11.5, -20);
ctx.closePath();
ctx.fill();
// 7. 机匣、枪栓与扳机
ctx.fillStyle = '#2A2A2A';
ctx.fillRect(-30, -12, 35, 24);
ctx.fillStyle = '#666666';
ctx.fillRect(-30, -12, 35, 2);
// 枪栓拉柄
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.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);
// 8. 表尺照门
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);
// 9. 枪背带
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 triggerFire() {
if (fireState.isFiring) return;
fireState.isFiring = true;
fireState.fireTime = performance.now();
fireState.flashTimer = 0.06;
// 镜头震动
camera.shakeX = (Math.random() - 0.5) * 12;
camera.shakeY = (Math.random() - 0.5) * 12;
// 弹壳抛出:从枪栓位置 (-7, -24) 抛出
fireState.shells.push({
x: -7, y: -24,
vx: 2 + Math.random() * 3,
vy: -6 - Math.random() * 4,
rotation: Math.random() * Math.PI,
vRot: (Math.random() - 0.5) * 0.4,
bounce: 0,
life: 1.0
});
// 烟雾生成:从枪口中心 (200, -5) 喷出
for (let i = 0; i < 5; i++) {
fireState.smokeParticles.push({
x: 200 + (Math.random() - 0.5) * 10,
y: -5 + (Math.random() - 0.5) * 10,
vx: 2 + Math.random() * 4,
vy: -1 - Math.random() * 3,
size: 5 + Math.random() * 10,
alpha: 0.8,
life: 1.0,
decay: 0.02 + Math.random() * 0.03
});
}
}
function updateFire(dt) {
if (!fireState.isFiring) return;
const now = performance.now();
const elapsed = now - fireState.fireTime;
// 后坐力:0-150ms 猛地向后,150-350ms 平滑恢复
if (elapsed < 150) {
const t = elapsed / 150;
fireState.recoilX = -15 * Math.sin(t * Math.PI);
fireState.recoilY = 8 * Math.sin(t * Math.PI);
fireState.recoilRot = 0.08 * Math.sin(t * Math.PI);
} else {
const t = Math.min((elapsed - 150) / 200, 1);
const easeOut = 1 - Math.pow(1 - t, 3);
fireState.recoilX = -15 * (1 - easeOut);
fireState.recoilY = 8 * (1 - easeOut);
fireState.recoilRot = 0.08 * (1 - easeOut);
}
if (fireState.flashTimer > 0) {
fireState.flashTimer -= dt;
}
// 物理更新:弹壳
for (let i = fireState.shells.length - 1; i >= 0; i--) {
const s = fireState.shells[i];
s.vy += 0.4;
s.x += s.vx;
s.y += s.vy;
s.rotation += s.vRot;
if (s.y > 40 && s.vy > 0) {
s.y = 40;
s.vy *= -0.4;
s.vx *= 0.7;
s.vRot *= -0.6;
s.bounce++;
}
if (s.bounce > 2 || s.y > 100) {
fireState.shells.splice(i, 1);
}
}
// 物理更新:烟雾
for (let i = fireState.smokeParticles.length - 1; i >= 0; i--) {
const p = fireState.smokeParticles[i];
p.x += p.vx;
p.y += p.vy;
p.vy -= 0.1;
p.vx *= 0.96;
p.size += 0.5;
p.alpha -= p.decay;
p.life -= p.decay;
if (p.alpha <= 0 || p.life <= 0) {
fireState.smokeParticles.splice(i, 1);
}
}
if (elapsed > 500 && fireState.flashTimer <= 0 && fireState.shells.length === 0 && fireState.smokeParticles.length === 0) {
fireState.isFiring = false;
fireState.recoilX = 0; fireState.recoilY = 0; fireState.recoilRot = 0;
}
}
function drawFireEffects(ctx) {
// 1. 枪口火光
if (fireState.flashTimer > 0) {
const flashAlpha = fireState.flashTimer / 0.06;
ctx.save();
ctx.globalAlpha = flashAlpha;
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();
}
// 2. 烟雾
fireState.smokeParticles.forEach(p => {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.fillStyle = '#AAAAAA';
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
ctx.restore();
});
// 3. 弹壳
fireState.shells.forEach(s => {
ctx.save();
ctx.translate(s.x, s.y);
ctx.rotate(s.rotation);
ctx.fillStyle = '#D4AF37';
ctx.fillRect(-4, -2, 8, 4);
ctx.fillStyle = '#B8963A';
ctx.fillRect(-4, -2, 2, 4);
ctx.restore();
});
}
// =====================================================================
// 主循环 + 摄像机变换
// =====================================================================
function animate(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 背景网格
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
for (let i = 0; i < canvas.width; i += 60) {
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); ctx.stroke();
}
for (let i = 0; i < canvas.height; i += 60) {
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke();
}
updateFire(dt);
// ===== 应用摄像机变换 =====
ctx.save();
// 加入镜头震动偏移
ctx.translate(camera.x + camera.shakeX, camera.y + camera.shakeY);
ctx.rotate(camera.rotation);
ctx.scale(camera.zoom, camera.zoom);
ctx.translate(-camera.x, -camera.y);
// 绘制枪械(带后坐力)
ctx.save();
ctx.translate(canvas.width / 2 - 70, canvas.height / 2);
// 后坐力平移和旋转
ctx.translate(fireState.recoilX, -fireState.recoilY);
ctx.rotate(fireState.recoilRot);
drawHanyangRifle(ctx);
ctx.restore();
// 绘制开火特效(特效坐标跟随枪械原点)
ctx.save();
ctx.translate(canvas.width / 2 - 70, canvas.height / 2);
drawFireEffects(ctx);
ctx.restore();
ctx.restore();
// UI 信息
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = '12px sans-serif';
ctx.fillText(`点击或空格开火 | 缩放: ${camera.zoom.toFixed(2)}x | 旋转: ${(camera.rotation * 180 / Math.PI).toFixed(0)}°`, 20, 30);
requestAnimationFrame(animate);
}
// =====================================================================
// 交互事件
// =====================================================================
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0) { // 左键开火
triggerFire();
}
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));
}, { passive: false });
canvas.addEventListener('dblclick', () => {
camera.x = canvas.width / 2;
camera.y = canvas.height / 2;
camera.zoom = 1.0;
camera.rotation = 0;
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
triggerFire();
}
});
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 汉阳造步枪 - 瘦身精准版(无刺刀) + 开火动画
Creator: TurboKoala83
Libraries: none
Complexity: complex (461 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: game-turbokoala83-mup549lz" to link back to the original. Then publish at arcadelab.ai/publish.