Axel Meyer 26012aa9b6 chore: add Tier-1 LICENSE file
Adds the proprietary all-rights-reserved license that applies to this
library. The text is identical to the engine LICENSE — this lib is
Tier-1 official platform content, distributed under the same terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:04:17 +02:00
2026-05-16 18:04:17 +02:00

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

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:

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:

camera.set_zoom(1.0)

Description: Sets zoom factor. Must be a positive number.

camera.target()

Syntax: camera.target() -> number, number

Example:

local x, y = camera.target()

Description: Returns current target world-coords.

camera.zoom()

Syntax: camera.zoom() -> number

Example:

local z = camera.zoom()

Description: Returns current zoom factor.

camera.begin()

Syntax: camera.begin() -> void

Example:

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:

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:

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:

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:

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:

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:

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

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)
Description
P.0.lib.camera � 2D camera state + begin/finish wrapper
Readme 39 KiB
Languages
Lua 94.8%
Shell 5.2%