A camera is a value, not a host-owned resource. Build the camera you want
for the current frame and pass it to Draw.with_camera!, which applies it
for the duration of a nested scope.
Every constructor and builder here is total: it answers with a Camera2D
rather than a Try. A camera has exactly two invariants -- every transform
field is finite, and the zoom is non-zero so screen_to_world can invert it
-- and the only inputs that can break them are inputs no caller means to
pass. Rather than make every app carry an unreachable Err branch for
those, they are sanitized on the way in. A target or offset component
that is not finite becomes 0, as does a rotation that is not finite; a
zoom that is zero or not finite becomes fallback_zoom, keeping its sign
so a mirrored camera stays mirrored.
Nothing else is touched. Sanitizing only ever rewrites a value a validating
API would have refused outright, so a camera built from ordinary numbers
comes back with its fields unchanged -- negative, axis-mirroring zooms
included.
The zoom substituted for a zoom that cannot be used, meaning zero (which
has no inverse) or a non-finite one (which has no meaning).
Deliberately tiny rather than 1: a camera that had to be rescued
collapses the world to a dot at its offset, which reads on screen as the
mistake it is, instead of quietly looking plausible.
Total: non-finite target, offset, and rotation components become
0, and a zoom of zero or one that is not finite becomes
fallback_zoom with the same sign. Every usable setting is kept exactly.
Immutable camera value accepted by drawing and coordinate transforms.
Camera.Camera2D and Draw.CameraMode on the platform are this same
type, re-exported.
Its representation is opaque so non-finite transform fields, or a zero
zoom that has no inverse, cannot bypass the sanitizing every constructor
applies. The module header states what is rewritten and what is left
alone.
Return a copy rotated new_rotation degrees clockwise.
Total: a new_rotation that is not finite becomes 0. Any finite
angle is kept exactly, including one outside 0 to 360 -- degrees wrap
through the sine and cosine, so there is no range to clamp to.
Total: a new_zoom of zero has no inverse and one that is not finite
has no meaning, so either becomes fallback_zoom with the same sign.
Every other zoom is kept exactly, so a negative zoom still mirrors
both axes.