🎮ArcadeLab

汉阳造步枪 - 开火动画

by TurboKoala83
388 lines12.2 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); 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.