/** * The picture a camera's shader is laid over (#299), as numbers: how many pixels it gets, which * transform takes the world into it, and what a pixel of it counts as. Pure, so the arithmetic is * tested without a GPU. */ import type { Mat } from '../core/model/matrix'; import type { PropertyBag } from '../core/model/Properties'; import { shaderResolutionOf } from '../core/properties/cameraShader'; import { cameraFrame, cameraSize } from '../core/properties/camera'; import { gridSteps, lookFrom } from './pixelGrid'; /** The longest side the picture may have, a game tab on a large screen included. */ export const CAMERA_MAX_PIXELS = 4096; export interface CameraPicture { /** The picture's size in pixels: the camera's raster, or the frame as large as it stands on screen. */ pixels: { w: number; h: number }; /** World units to pixels of the picture: the frame's top left corner is 0,0. */ toPicture: Mat; /** The frame in world units, centred on `centre`: where the shaded picture is drawn. */ frame: { w: number; h: number }; /** The camera's position, or on a pixel grid the point the picture is taken from. */ centre: { x: number; y: number }; /** On a pixel grid the steps per unit the objects in the picture are drawn on, else null. */ grid: number | null; /** * Camera pixels one pixel of the picture covers, per axis: what the shader counts a pixel as in * either resolution (the user's choice), so a Blur of 4 is 4 camera pixels at any game scale. */ pixelScale: { x: number; y: number }; /** Whether the picture is scaled up without smoothing: the camera's raster, for pixel art. */ blocky: boolean; } /** * `devicePixelsPerUnit` is what one world unit covers on screen, in device pixels. `onGrid`: the view * is a pixel perfect camera's, so the world's origin lands on a whole pixel of the picture and the * objects on the picture's own grid – one step per pixel of it, whichever resolution it has. */ export function cameraPicture( camera: { x: number; y: number; props: PropertyBag }, devicePixelsPerUnit: number, onGrid = false, ): CameraPicture { const frame = cameraFrame(camera.props); const size = cameraSize(camera.props); const blocky = shaderResolutionOf(camera.props) === 'camera'; const pixels = blocky ? { w: pixelsAlong(size.w), h: pixelsAlong(size.h) } : { w: pixelsAlong(frame.w * devicePixelsPerUnit), h: pixelsAlong(frame.h * devicePixelsPerUnit) }; const a = pixels.w / frame.w; const d = pixels.h / frame.h; const x = onGrid ? lookFrom(camera.x, pixels.w / 2, a) : camera.x; const y = onGrid ? lookFrom(camera.y, pixels.h / 2, d) : camera.y; return { pixels, toPicture: { a, b: 0, c: 0, d, tx: pixels.w / 2 - x * a, ty: pixels.h / 2 - y * d }, frame, centre: { x, y }, grid: onGrid ? gridSteps(a) : null, pixelScale: { x: size.w / pixels.w, y: size.h / pixels.h }, blocky, }; } function pixelsAlong(length: number): number { return Math.min(CAMERA_MAX_PIXELS, Math.max(1, Math.round(Number.isFinite(length) ? length : 1))); }