# lib-core.camera 2D camera state + begin/finish render wrapper, with free-pan (keys/edge/drag), bounds-clamping, and screen-to-world unproject. Wraps `engine.render.camera_2d_begin/end` with current target + zoom. **Version:** 0.3.0 **Lib-ID:** lib-core.camera **Requires:** lib-core.input v>=0.4.0 **Tags:** camera, viewport, transform, pan, bounds ## Topology ```mermaid graph LR this["lib-core.camera"] lib_core_input["lib-core.input"] this --> lib_core_input engine["engine.*"] this --> engine ``` ## API ### `camera.set_target(x, y)` **Syntax:** `camera.set_target(x: number, y: number) -> void` **Example:** ```lua camera.set_target(256, 256) -- world-coords for screen-center ``` **Description:** Sets the world-coords the camera centers on. Clamps to current bounds (if set). ### `camera.set_zoom(z)` **Syntax:** `camera.set_zoom(z: number) -> void` **Example:** ```lua camera.set_zoom(1.0) ``` **Description:** Sets zoom factor. Must be a positive number. ### `camera.target()` **Syntax:** `camera.target() -> number, number` **Example:** ```lua local x, y = camera.target() ``` **Description:** Returns current target world-coords. ### `camera.zoom()` **Syntax:** `camera.zoom() -> number` **Example:** ```lua local z = camera.zoom() ``` **Description:** Returns current zoom factor. ### `camera.begin()` **Syntax:** `camera.begin() -> void` **Example:** ```lua camera.begin() -- draw world here camera.finish() ``` **Description:** Pushes the camera transform onto the render stack. Must be called inside a render hook. ### `camera.finish()` **Syntax:** `camera.finish() -> void` **Description:** Pops the camera transform. Named `finish` because `end` is a Lua keyword. ### `camera.bind_pan_keys(left, right, up, down)` **Syntax:** `camera.bind_pan_keys(left: string, right: string, up: string, down: string) -> void` **Example:** ```lua input.bind("cam_left", { "a" }) input.bind("cam_right", { "d" }) input.bind("cam_up", { "w" }) input.bind("cam_down", { "s" }) camera.bind_pan_keys("cam_left", "cam_right", "cam_up", "cam_down") ``` **Description:** Binds 4 input-actions for keyboard pan. Per-frame summation happens in `update(dt)`. ### `camera.bind_pan_drag(action)` **Syntax:** `camera.bind_pan_drag(action: string) -> void` **Example:** ```lua input.bind("cam_drag", { "mouse_middle" }) camera.bind_pan_drag("cam_drag") ``` **Description:** Binds an input-action that, while held, pans by mouse-delta (zoom-scaled). ### `camera.enable_pan_edge()` **Syntax:** `camera.enable_pan_edge() -> void` **Description:** Enables edge-scroll panning: cursor near a window-edge pans the camera at `pan_speed`. Threshold via `set_edge_threshold`. ### `camera.set_pan_speed(n)` **Syntax:** `camera.set_pan_speed(n: number) -> void` **Description:** Sets pan-speed in world-units per second for keys + edge-scroll. ### `camera.pan_speed()` **Syntax:** `camera.pan_speed() -> number` **Description:** Returns current pan-speed. ### `camera.set_edge_threshold(n)` **Syntax:** `camera.set_edge_threshold(n: number) -> void` **Description:** Sets edge-scroll threshold in screen-pixels. ### `camera.edge_threshold()` **Syntax:** `camera.edge_threshold() -> number` **Description:** Returns current edge-scroll threshold. ### `camera.set_pan_invert(b)` **Syntax:** `camera.set_pan_invert(b: bool) -> void` **Description:** Inverts drag-pan direction. False = "drag world" feel, true = "drag camera" feel. ### `camera.pan_invert()` **Syntax:** `camera.pan_invert() -> bool` **Description:** Returns current drag-invert setting. ### `camera.set_pan_enabled(source, b)` **Syntax:** `camera.set_pan_enabled(source: string, b: bool) -> void` **Example:** ```lua camera.set_pan_enabled("edge", false) -- disable edge-scroll only ``` **Description:** Per-source toggle. `source` is one of `"keys"`, `"edge"`, `"drag"`. All sources default to enabled. ### `camera.pan_enabled(source)` **Syntax:** `camera.pan_enabled(source: string) -> bool` **Description:** Returns current toggle-state for the given source. ### `camera.set_bounds(xmin, ymin, xmax, ymax)` **Syntax:** `camera.set_bounds(xmin: number, ymin: number, xmax: number, ymax: number) -> void` **Example:** ```lua camera.set_bounds(0, 0, 4096, 4096) ``` **Description:** Sets world-space bounds. Once set, `set_target` and `update` clamp to keep the target inside. `xmin < xmax` and `ymin < ymax` required. ### `camera.clear_bounds()` **Syntax:** `camera.clear_bounds() -> void` **Description:** Removes any active bounds; camera becomes free-roaming. ### `camera.bounds()` **Syntax:** `camera.bounds() -> number, number, number, number | nil` **Description:** Returns `xmin, ymin, xmax, ymax` if bounds active, else `nil`. ### `camera.update(dt)` **Syntax:** `camera.update(dt: number) -> void` **Example:** ```lua function update(ctx, dt) camera.update(dt) end ``` **Description:** Per-frame pan summation: applies keys + edge + drag contributions (each gated by `pan_enabled[source]`), then clamps to bounds. ### `camera.screen_to_world(sx, sy)` **Syntax:** `camera.screen_to_world(sx: number, sy: number) -> number, number` **Example:** ```lua local mx, my = engine.input.get_mouse_pos() local wx, wy = camera.screen_to_world(mx, my) ``` **Description:** Inverse of the Raylib 2D camera transform (origin = window-center, no rotation). Useful for picking, world-space cursors. v0.3.0+. ## Conventions - Y-down-positive per ADR-0031. - Pan-sources are additive: keys + edge + drag contributions sum each frame. - Bounds-clamping is always-on once set; use `clear_bounds` to disable. - Drag-pan is zoom-scaled so on-screen mouse-distance maps to consistent world-distance. ## Consumer pattern ```lua local input = require("lib-core.input") local camera = require("lib-core.camera") camera.set_target(256, 256) camera.set_zoom(1.0) camera.set_pan_speed(400) camera.set_bounds(0, 0, 4096, 4096) input.bind("cam_left", { "a" }) input.bind("cam_right", { "d" }) input.bind("cam_up", { "w" }) input.bind("cam_down", { "s" }) input.bind("cam_drag", { "mouse_middle" }) camera.bind_pan_keys("cam_left", "cam_right", "cam_up", "cam_down") camera.bind_pan_drag("cam_drag") camera.enable_pan_edge() function update(ctx, dt) camera.update(dt) end function render(ctx) engine.render.clear_color(0, 0, 0) camera.begin() -- draw world here camera.finish() end ``` ## CHANGELOG ### v0.3.0 (P.3.5) - Added `screen_to_world(sx, sy)` — unproject screen-coord to world-coord. ### v0.2.0 - Pan-API (keys / edge / drag, additive, per-source toggle). - Bounds-clamping (always-on once set; `clear_bounds` to disable). - Drag-invert + zoom-scaling. - `update(dt)` for per-frame pan-summation. ### v0.1.0 (P.0) - Initial release: target/zoom + begin/finish. ## References - Spec v0.3.0 (P.3.5): `meta/docs/superpowers/specs/2026-05-13-p3-5-camera-free-pan-design.md` - Spec v0.1.0 (P.0): `meta/docs/superpowers/specs/2026-05-09-p0-lib-camera-design.md` - Architecture: `meta/docs/architecture/render-pipeline.md`, `meta/docs/architecture/map-topology.md` §7.3 - ADR-0001 (engine knows verbs, libs bring nouns) - ADR-0031 (pixel-convention: Y-down-positive) - ADR-0038 (API-Doc-Convention)