import type { PropertyBag } from '../model/Properties'; import { defaultsFor } from './PropertySchema'; const DEFAULTS = defaultsFor('camera'); export interface WorldRect { left: number; top: number; right: number; bottom: number; } /** * The world region a camera shows: its view size divided by its zoom. Drawn as a frame in edit mode, * and the only region rendered in play mode. */ export function cameraFrame(props: PropertyBag): { w: number; h: number } { const zoom = Math.max(0.01, Number(props.zoom ?? DEFAULTS.zoom)); const size = cameraSize(props); return { w: size.w / zoom, h: size.h / zoom }; } /** The picture the camera produces, in its own pixels – what the game view's 100% shows 1:1. */ export function cameraSize(props: PropertyBag): { w: number; h: number } { return { w: Number(props.width ?? DEFAULTS.width), h: Number(props.height ?? DEFAULTS.height) }; } export function isPixelPerfect(props: PropertyBag): boolean { return props.pixelPerfect === true; } /** * Where the mouse is in the world as `camera` sees it, with that camera's picture fitted into the * main camera's and centred, the way #302 lays an overlay on. `onPicture` is the mouse on the main * picture, from its centre in parts of its width and height; `onMain` whether it is on it at all. * `inside` is then whether the mouse is on this camera's picture. */ export function pointerThrough( onPicture: { x: number; y: number }, onMain: boolean, mainProps: PropertyBag, camera: { x: number; y: number; props: PropertyBag }, ): { x: number; y: number; inside: boolean } { const main = cameraFrame(mainProps); const frame = cameraFrame(camera.props); // Units of the main frame per unit of this camera's. const fit = Math.min(main.w / frame.w, main.h / frame.h); if (!(fit > 0) || !Number.isFinite(fit)) return { x: camera.x, y: camera.y, inside: false }; const dx = onPicture.x * main.w; const dy = onPicture.y * main.h; const inside = onMain && Math.abs(dx) <= (frame.w * fit) / 2 && Math.abs(dy) <= (frame.h * fit) / 2; return { x: camera.x + dx / fit, y: camera.y + dy / fit, inside }; } export function rectAround(cx: number, cy: number, w: number, h: number): WorldRect { return { left: cx - w / 2, top: cy - h / 2, right: cx + w / 2, bottom: cy + h / 2 }; } export function rectsIntersect(a: WorldRect, b: WorldRect): boolean { return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; }