Skip to content

相机

平移、缩放、聚焦。观察世界的窗口。

目标

相机必须做到:

  • 🖱️ 操作直觉(拖拽平移、滚轮缩放)
  • 🎯 支持聚焦(居中显示 NPC 或地点)
  • 🔍 支持 0.5x 到 3x 缩放
  • 📱 兼容触摸设备(双指缩放、拖拽平移)

实现

一个简单的 Camera 类,封装 PixiJS Container 的变换:

ts
class Camera {
  x = 0;
  y = 0;
  zoom = 1;
  target: Container;

  constructor(target: Container) {
    this.target = target;
  }

  pan(dx: number, dy: number) {
    this.x += dx;
    this.y += dy;
    this.apply();
  }

  setZoom(z: number, focusX?: number, focusY?: number) {
    const oldZoom = this.zoom;
    this.zoom = clamp(z, 0.5, 3);

    if (focusX !== undefined && focusY !== undefined) {
      this.x = focusX - (focusX - this.x) * (this.zoom / oldZoom);
      this.y = focusY - (focusY - this.y) * (this.zoom / oldZoom);
    }

    this.apply();
  }

  focusOn(wx: number, wy: number, viewportWidth: number, viewportHeight: number) {
    this.x = wx * TILE_SIZE - viewportWidth / 2;
    this.y = wy * TILE_SIZE - viewportHeight / 2;
    this.apply();
  }

  private apply() {
    this.target.x = -this.x * this.zoom;
    this.target.y = -this.y * this.zoom;
    this.target.scale.set(this.zoom);
  }
}

输入处理

鼠标 / 触摸

ts
let isDragging = false;
let lastX = 0, lastY = 0;

stage.on('pointerdown', (event) => {
  isDragging = true;
  lastX = event.global.x;
  lastY = event.global.y;
});

stage.on('pointermove', (event) => {
  if (!isDragging) return;
  camera.pan(lastX - event.global.x, lastY - event.global.y);
  lastX = event.global.x;
  lastY = event.global.y;
});

stage.on('pointerup', () => isDragging = false);

window.addEventListener('wheel', (event) => {
  event.preventDefault();
  const delta = -event.deltaY * 0.001;
  camera.setZoom(camera.zoom + delta, event.clientX, event.clientY);
});

触摸(双指缩放)

ts
let touchStartDist = 0;
let touchStartZoom = 1;

stage.on('touchstart', (event) => {
  if (event.touches.length === 2) {
    const [a, b] = event.touches;
    touchStartDist = distance(a, b);
    touchStartZoom = camera.zoom;
  }
});

stage.on('touchmove', (event) => {
  if (event.touches.length === 2) {
    const [a, b] = event.touches;
    const dist = distance(a, b);
    camera.setZoom(touchStartZoom * (dist / touchStartDist));
  }
});

平滑过渡

"聚焦 NPC" 操作时,相机会平滑动画过渡:

ts
async function smoothFocusOn(camera: Camera, wx: number, wy: number, duration = 600) {
  const startX = camera.x;
  const startY = camera.y;
  const startZoom = camera.zoom;
  const targetZoom = 2;
  const targetX = wx * TILE_SIZE - viewport.width / 2;
  const targetY = wy * TILE_SIZE - viewport.height / 2;

  return new Promise<void>(resolve => {
    const start = performance.now();
    const tick = (now: number) => {
      const t = Math.min((now - start) / duration, 1);
      const eased = easeInOutCubic(t);

      camera.x = startX + (targetX - startX) * eased;
      camera.y = startY + (targetY - startY) * eased;
      camera.zoom = startZoom + (targetZoom - startZoom) * eased;
      camera.apply();

      if (t < 1) requestAnimationFrame(tick);
      else resolve();
    };
    requestAnimationFrame(tick);
  });
}

小地图

屏幕角落的全局概览,显示所有建筑和 NPC 位置。点击小地图可移动主相机。


相机状态

状态显示内容
overview全镇缩略图
focus-npc居中某个 NPC,缩放 2x
focus-location居中某栋建筑,缩放 2x

键盘快捷键

按键操作
方向键平移
+ / -放大 / 缩小
0重置为概览
F聚焦选中的 NPC
Esc取消选中

性能

相机不会触发重新渲染——只变换已有精灵的坐标。平移/缩放操作在常规速度下零 FPS 开销。

Released under the MIT License.