/** * A pixel perfect camera draws on the game view's pixel grid: one step per device pixel a unit gets, * the world's origin on a whole pixel, an unturned box by its corner. Fit stays exact. */ import { describe, expect, it } from 'vitest'; import { SHADER_RESOLUTION_KEY } from '../../src/core/properties/cameraShader'; import { cameraPicture } from '../../src/viewport/cameraPicture'; import { gridSteps, snapPlacement } from '../../src/viewport/pixelGrid'; import { cameraView, overlayView, worldToScreen } from '../../src/viewport/view'; const turned = (deg: number) => { const r = (deg * Math.PI) / 180; return { a: Math.cos(r), b: Math.sin(r), c: -Math.sin(r), d: Math.cos(r) }; }; /** Whether `v` device pixels is a whole one, within float noise. */ const whole = (v: number) => Math.abs(v - Math.round(v)) < 1e-9; describe('snapPlacement', () => { const plain = { a: 1, b: 0, c: 0, d: 1, tx: 10.3, ty: -4.6 }; it('puts the corner of an odd box on the grid, so its centre lies between', () => { const m = snapPlacement(plain, 15, 9, 1); expect(m.tx - 7.5).toBe(3); expect(m.ty - 4.5).toBe(-9); }); it('counts the corner from the pivot, as a text anchored left is placed', () => { const m = snapPlacement(plain, 15, 9, 1, { x: 0, y: 0.5 }); expect(m.tx).toBe(10); }); it('snaps to fifths at five steps per unit, and a turned box by its pivot', () => { expect(snapPlacement({ ...plain, tx: 10.33 }, 15, 9, 5).tx - 7.5).toBeCloseTo(2.8, 12); const m = snapPlacement({ ...plain, ...turned(30) }, 15, 9, 1); expect([m.tx, m.ty]).toEqual([10, -5]); }); it('leaves the placement as it is without a grid', () => { expect(snapPlacement(plain, 15, 9, null)).toBe(plain); }); }); describe('cameraView through a pixel perfect camera', () => { const camera = (props: Record) => ({ x: 10.37, y: -3.81, props: { width: 481, height: 271, zoom: 1, pixelPerfect: true, ...props }, }); it('has one step per device pixel of a unit, and the world origin on a whole one', () => { // Game scale, device pixels per CSS pixel, camera zoom, steps. for (const [mode, ratio, zoom, steps] of [ [1, 1, 1, 1], [5, 1, 1, 5], [2, 1.25, 1, 2], [1, 1, 2, 2], ] as const) { const zoomed = camera({ zoom }); const view = cameraView(zoomed, 2600, 1500, mode, ratio); expect(view.grid).toBe(steps); for (const n of [0, 1, -7, 333]) { const p = worldToScreen(view, n / steps, n / steps); expect(whole(p.x * ratio) && whole(p.y * ratio)).toBe(true); } expect(Math.abs(view.x - zoomed.x)).toBeLessThanOrEqual(0.5 / (view.scale * ratio) + 1e-12); } }); it('stays exact at fit and with the switch off', () => { const fit = cameraView(camera({}), 1000, 700, 'fit'); expect(fit.grid).toBeUndefined(); expect([fit.x, fit.y]).toEqual([10.37, -3.81]); const off = cameraView(camera({ pixelPerfect: false }), 1000, 700, 1); expect(off.grid).toBeUndefined(); expect(off.x).toBe(10.37); }); it('snaps an overlay only at a fixed scale of the main picture', () => { const main = cameraView(camera({}), 1000, 700, 1); const overlay = camera({ width: 241, height: 135 }); expect(overlayView(overlay, main, { w: 481, h: 271 }, 1, false).grid).toBeUndefined(); const fixed = overlayView(overlay, main, { w: 481, h: 271 }, 1, true); expect(fixed.grid).toBe(gridSteps(fixed.scale)); expect(whole(worldToScreen(fixed, 0, 0).x)).toBe(true); }); }); describe('a pixel perfect camera wearing a shader', () => { const at = (props: Record) => ({ x: 10.37, y: -3.81, props: { width: 481, height: 271, zoom: 1, ...props }, }); it('takes its picture with the world origin on a whole pixel of it', () => { const p = cameraPicture(at({}), 5, true); expect(p.grid).toBe(5); expect(whole(p.toPicture.tx) && whole(p.toPicture.ty)).toBe(true); expect(p.centre.x).not.toBe(10.37); }); it('draws its objects on the picture grid, one step per camera pixel at camera resolution', () => { expect(cameraPicture(at({ [SHADER_RESOLUTION_KEY]: 'camera' }), 5, true).grid).toBe(1); expect(cameraPicture(at({ [SHADER_RESOLUTION_KEY]: 'camera', zoom: 2 }), 10, true).grid).toBe(2); }); it('is taken from the camera itself without the grid', () => { const p = cameraPicture(at({}), 5); expect(p.grid).toBeNull(); expect(p.centre).toEqual({ x: 10.37, y: -3.81 }); }); });