camera.test.js 3.17 KB
import { describe, it, expect } from 'vitest'
import { createCamera } from '../src/camera.js'

const vp = () => ({ w: 1920, h: 1080 })

describe('camera 镜头数学', () => {
  it('cut 瞬变状态(验收 6)', () => {
    const cam = createCamera({ getViewport: vp })
    cam.applyCommand({ type: 'cut', target: { x: 100, y: 50 }, zoom: 2 })
    expect(cam.cam).toEqual({ x: 100, y: 50, zoom: 2 })
  })

  it('pan 在 duration 内单调逼近目标,中点介于起止(验收 6)', () => {
    const cam = createCamera({ getViewport: vp })
    cam.applyCommand({ type: 'cut', target: { x: 0, y: 0 }, zoom: 1 })
    cam.applyCommand({ type: 'pan', target: { x: 1000, y: 0 }, duration: 1000, easing: 'linear' })
    cam.update(500)
    expect(cam.cam.x).toBeGreaterThan(0)
    expect(cam.cam.x).toBeLessThan(1000)
    expect(cam.cam.x).toBeCloseTo(500, 6) // linear 中点
    cam.update(500)
    expect(cam.cam.x).toBeCloseTo(1000, 6)
  })

  it('zoom 补间逼近目标(验收 6)', () => {
    const cam = createCamera({ getViewport: vp })
    cam.applyCommand({ type: 'cut', zoom: 1 })
    cam.applyCommand({ type: 'zoom', zoom: 3, duration: 1000, easing: 'linear' })
    cam.update(1000)
    expect(cam.cam.zoom).toBeCloseTo(3, 6)
  })

  it('follow 阻尼跟随帧率无关:不同 dt 切分到同总时长结果一致(验收 6)', () => {
    const lookup = () => ({ x: 1000, y: 0 })
    const makeFollow = () => {
      const c = createCamera({ getViewport: vp })
      c.applyCommand({ type: 'cut', target: { x: 0, y: 0 }, zoom: 1 })
      c.applyCommand({ type: 'follow', follow: 'hero' }, lookup)
      return c
    }
    const cA = makeFollow()
    for (let i = 0; i < 100; i++) cA.update(10) // 100×10ms
    const cB = makeFollow()
    for (let i = 0; i < 20; i++) cB.update(50) // 20×50ms(同 1000ms 总时长)
    expect(cA.cam.x).toBeCloseTo(cB.cam.x, 0) // 帧率无关,容差内一致
  })

  it('shake 随时间衰减、duration 后归零(验收 6)', () => {
    let seq = [0.9, 0.9, 0.9, 0.9, 0.9, 0.9]
    let i = 0
    const cam = createCamera({ getViewport: vp, random: () => seq[i++ % seq.length] })
    cam.applyCommand({ type: 'shake', magnitude: 20, duration: 100 })
    cam.update(10)
    const s1 = Math.abs(cam.getRenderState().shakeX)
    cam.update(50)
    const s2 = Math.abs(cam.getRenderState().shakeX)
    expect(s2).toBeLessThan(s1) // 衰减
    cam.update(100) // 过 duration
    const rs = cam.getRenderState()
    expect(rs.shakeX).toBe(0)
    expect(rs.shakeY).toBe(0)
  })

  it('reset 归零跟随/补间/抖动', () => {
    const cam = createCamera({ getViewport: vp })
    cam.applyCommand({ type: 'pan', target: { x: 500, y: 0 }, duration: 1000 })
    cam.reset({ x: 7, y: 8 })
    expect(cam.tween).toBeNull()
    expect(cam.cam).toEqual({ x: 7, y: 8, zoom: 1 })
  })

  it('getRenderState 只产状态,含视口尺寸(验收 6:只产状态)', () => {
    const cam = createCamera({ getViewport: vp })
    cam.applyCommand({ type: 'cut', target: { x: 3, y: 4 }, zoom: 1.5 })
    cam.update(16)
    const rs = cam.getRenderState()
    expect(rs).toMatchObject({ x: 3, y: 4, zoom: 1.5, viewW: 1920, viewH: 1080 })
    expect(typeof rs.shakeX).toBe('number')
  })
})