汉阳造步枪 - 开火动画
by TurboKoala83388 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: [] // 烟雾粒子列表
};
// =====================================================================
// 枪械绘制函数(精简版,确保视觉紧凑)
// =====================================================================
function drawRifle(ctx) {
// 1. 枪托与木质主体
ctx.fillStyle = '#5E3A20';
ctx.beginPath();
ctx.moveTo(-30, -14); ctx.lineTo(-210, -14);
ctx.lineTo(-220, 10); ctx.lineTo(-150, 18);
ctx.lineTo(-30, 15); ctx.lineTo(240, 15); ctx.lineTo(240, -14);
ctx.closePath(); ctx.fill();
// 木质立体感
ctx.fillStyle = '#3A2210'; ctx.fillRect(-210, 12, 450, 6);
ctx.fillStyle = '#8A5A3A'; ctx.fillRect(-210, -14, 450, 2);
// 金属托底板
ctx.fillStyle = '#2A2A2A';
ctx.beginPath();
ctx.moveTo(-210, -14); ctx.lineTo(-220, 10);
ctx.lineTo(-215, 12); ctx.lineTo(-205, -12);
ctx.closePath(); ctx.fill();
// 握把凹陷
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath(); ctx.arc(-90, 15, 12, 0, Math.PI, true); ctx.fill();
// 2. 上下护木缝隙
ctx.fillStyle = '#1A1A1A';
ctx.fillRect(-30, -14, 270, 8); // 枪管槽
// 3. 外露枪管
ctx.fillStyle = '#5A5A5A'; ctx.fillRect(240, -14, 60, 8);
ctx.fillStyle = '#9A9A9A'; ctx.fillRect(240, -14, 60, 2);
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(240, -8, 60, 2);
// 4. 金属枪箍
[60, 150, 235].forEach(x => {
ctx.fillStyle = '#333333'; ctx.fillRect(x, -28, 8, 44);
ctx.fillStyle = '#666666'; ctx.fillRect(x, -28, 2, 44);
});
// 5. 枪口帽、准星、刺刀座
const muzzleX = 300;
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(muzzleX - 6, -16, 6, 12); // 枪口帽
ctx.fillStyle = '#555555'; ctx.fillRect(muzzleX - 18, -22, 8, 8); // 准星底座
ctx.fillStyle = '#888888'; // 准星尖
ctx.beginPath(); ctx.moveTo(muzzleX - 16, -22); ctx.lineTo(muzzleX - 12, -22); ctx.lineTo(muzzleX - 14, -30); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#333333'; ctx.fillRect(muzzleX - 22, 8, 10, 8); // 刺刀座
ctx.fillStyle = '#555555'; ctx.fillRect(muzzleX - 19, 14, 4, 8);
// 6. 机匣与枪栓
ctx.fillStyle = '#2A2A2A'; ctx.fillRect(-30, -18, 40, 36);
ctx.fillStyle = '#666666'; ctx.fillRect(-30, -18, 40, 2);
ctx.fillStyle = '#4A4A4A'; ctx.fillRect(-10, -28, 8, 12); // 拉柄
ctx.fillStyle = '#888888'; ctx.beginPath(); ctx.arc(-6, -28, 6, 0, Math.PI * 2); ctx.fill(); // 球头
// 7. 扳机
ctx.strokeStyle = '#555555'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(0, 18, 10, 0, Math.PI); ctx.stroke();
ctx.fillStyle = '#333333'; ctx.fillRect(-2, 14, 4, 12);
// 8. 表尺
ctx.fillStyle = '#333333'; ctx.fillRect(20, -28, 12, 8);
ctx.fillStyle = '#666666'; ctx.fillRect(24, -42, 4, 14);
// 9. 枪背带
ctx.fillStyle = '#222222';
ctx.beginPath(); ctx.arc(64, 15, 4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(-190, 12, 4, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#5A4A3A'; ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(-190, 12); ctx.quadraticCurveTo(-60, 65, 20, 45); ctx.lineTo(64, 15); ctx.stroke();
}
// =====================================================================
// 开火特效处理
// =====================================================================
function triggerFire() {
if (fireState.isFiring) return; // 防止连发
fireState.isFiring = true;
fireState.fireTime = performance.now();
fireState.flashTimer = 0.06; // 火光持续60毫秒
// 镜头震动
camera.shakeX = (Math.random() - 0.5) * 12;
camera.shakeY = (Math.random() - 0.5) * 12;
// 1. 抛壳:从枪栓位置 (-6, -28) 抛出
fireState.shells.push({
x: -6, y: -28,
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
});
// 2. 烟雾粒子:从枪口 (300, -10) 喷出
for (let i = 0; i < 5; i++) {
fireState.smokeParticles.push({
x: 300 + (Math.random() - 0.5) * 10,
y: -10 + (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;
// 后坐力曲线:快速后退,然后缓慢恢复(缓出)
if (elapsed < 150) {
// 0 - 150ms: 后坐力加大
const t = elapsed / 150;
fireState.recoilX = -15 * Math.sin(t * Math.PI); // 向后缩15px
fireState.recoilY = 8 * Math.sin(t * Math.PI); // 向上抬8px(y轴向下为正,所以是负偏移)
fireState.recoilRot = 0.08 * Math.sin(t * Math.PI); // 枪口上扬
} else {
// 150ms之后恢复
const t = Math.min((elapsed - 150) / 200, 1); // 200ms内恢复
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) { // 地面大致在y=40左右
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(300, -10); // 枪口位置
// 绘制一个不规则的爆炸火光
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';
// 使用方形的R6风格烟雾
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 - 80, canvas.height / 2);
// 应用后坐力
ctx.translate(fireState.recoilX, -fireState.recoilY);
ctx.rotate(fireState.recoilRot);
drawRifle(ctx);
ctx.restore();
// 绘制开火特效(在摄像机坐标系下绘制,因为特效坐标是相对的)
ctx.save();
ctx.translate(canvas.width / 2 - 80, 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 });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
triggerFire();
}
});
// 启动
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 汉阳造步枪 - 开火动画
Creator: TurboKoala83
Libraries: none
Complexity: complex (388 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-mup502gn" to link back to the original. Then publish at arcadelab.ai/publish.