Camera2D — the side-on orthographic camera of Ashcross, and the clock the
staging slice's moments run on.
The screen is 1080x1440: PORTRAIT, taller than it is wide. The frame covers
`NB.CAMERA.orthoHeight` world units vertically and only 12 across, so this
camera stays close and lets the tall axis carry the shot. It never rotates
and never leaves the z = NB.CAMERA.z plane; it looks straight down -Z at the
flat city.
In PLAY it tracks whatever `NB.state.doctor` says, with a deadzone the doctor
moves inside freely and a lead in the direction of travel, and it publishes
where it ended up back into `NB.state.cameraX/cameraY` — which is what the
backdrop reads for its parallax and what everything else reads to decide
whether a thing is on screen.
On top of the follow it carries the four things a staged moment needs:
:panTo(x, y, seconds, ease) a TIMED move along an eased curve — the
opening crane, the push onto a patient, the
walk up to the gate. `:isPanning()` says when
it has arrived.
:zoomTo(height, seconds) a TIMED change of `orthoHeight`. Smaller is
closer: 16 is the playing frame, 10 is a
push, 22 is the district opening out.
:setDrift(vx, vy) a slow constant creep added while the shot is
driven by hand. A held frame that drifts a
tenth of a unit a second is alive; one that
is nailed down is a still.
:kick(amount) the shake, for a bell or a hard landing.
`:driveTo` still exists as the damped (untimed) drive, and `:release()` hands
the shot back to the doctor from any of them.
It also TICKS THE STAGING SLICE. `game.stage` installs `NB.ext.stageTick`
and this component calls it once a frame, in edit mode as well as in play,
so the opening can be rehearsed without entering play and locking the VFS
for the other slices. The call is guarded: a staging module mid-edit can
never take the camera down with it.
Usage:
rig.component.add("Camera", {
projection = "orthographic",
behavior = asset.resolve("game.Camera2D", "component"),
})
easeLinear(t: number) → number return t end
Easing
| arg | type | description |
|---|
| t | number | |
easeInQuad(t: number) → number return t
| arg | type | description |
|---|
| t | number | |
easeOutQuad(t: number) → number return 1
| arg | type | description |
|---|
| t | number | |
easeInOutQuad(t: number) → number
| arg | type | description |
|---|
| t | number | |
easeOutCubic(t: number) → number
| arg | type | description |
|---|
| t | number | |
easeInOutCubic(t: number) → number
| arg | type | description |
|---|
| t | number | |
easeInOutSine(t: number) → number
| arg | type | description |
|---|
| t | number | |
easeOutExpo(t: number) → number
| arg | type | description |
|---|
| t | number | |
applyLens(h: number) → void
| arg | type | description |
|---|
| h | number | |
targetPose( ) → void
The follow
pose(dt: number) → void
| arg | type | description |
|---|
| dt | number | |
callStage(which: string, dt: number) → void
The staging tick
`game.stage` installs `NB.ext.stageTick`. It is called here rather than from
the module because a module has no frame of its own, and it is called before
the pose so a beat that starts a pan this frame is answered this frame.
| arg | type | description |
|---|
| which | string | |
| dt | number | |
tickStage(dt: number) → void
`stageTick` runs BEFORE the pose, so a cue that starts a pan this frame is
answered this frame. `stagePostTick` runs AFTER it, which is where the
staging slice puts its bands, veil and vignette — anything held in front of
the camera has to be placed against the pose it will actually be drawn with,
or it lags the shake and the fast part of a crane by a frame.
| arg | type | description |
|---|
| dt | number | |
tickStagePost(dt: number) → void
| arg | type | description |
|---|
| dt | number | |
snapTo(x: number, y: number) → void
Snap the shot to a world point with no easing — for a cut rather than a move.
| arg | type | description |
|---|
| x | number | World x to sit at. |
| y | number | World y to sit at. |
examples
entity(id).component.get("Camera2D"):snapTo(40, 12)setBounds(minX: number, maxX: number, minY: number, maxY: number) → void
Limit where the shot may travel, so it never leaves the district.
| arg | type | description |
|---|
| minX | number | Left edge of the shot's travel, in world units. |
| maxX | number | Right edge. |
| minY | number | Bottom edge. |
| maxY | number | Top edge. |
examples
entity(id).component.get("Camera2D"):setBounds(-4, 220, 0, 90)driveTo(x: number, y: number) → void
Take the shot away from the doctor and drive it by hand, easing in at the follow's own rate.
| arg | type | description |
|---|
| x | number | World x to drive toward. |
| y | number | World y to drive toward. |
examples
entity(id).component.get("Camera2D"):driveTo(12, 30)panTo(x: number, y: number, seconds: number, ease: string?) → void
Move the shot to a point over a stated number of seconds along an eased curve — the opening crane, the push onto a patient, the walk up to the gate. Ask :isPanning() when it has arrived.
| arg | type | description |
|---|
| x | number | World x to arrive at. |
| y | number | World y to arrive at. |
| seconds | number | How long the move takes. 0 arrives immediately. |
| ease | string? | Curve name — linear, inQuad, outQuad, inOutQuad, outCubic, inOutCubic, inOutSine, outExpo. Defaults to inOutQuad. |
examples
entity(id).component.get("Camera2D"):panTo(6, 12, 7.5, "inOutSine")zoomTo(height: number, seconds: number, ease: string?) → void
Change how much world the frame covers, over a stated number of seconds. Smaller is closer — 16 is the playing frame, 10 a push onto a face, 22 the district opening out.
| arg | type | description |
|---|
| height | number | World units the frame covers vertically at the end of the move. |
| seconds | number | How long the change takes. 0 applies it at once. |
| ease | string? | Curve name, as :panTo. Defaults to inOutQuad. |
examples
entity(id).component.get("Camera2D"):zoomTo(10.5, 1.4, "outCubic")setDrift(vx: number, vy: number) → void
Add a slow constant creep to a driven shot, in world units per second. A held frame that drifts is alive; one that is nailed down is a still. Cleared by :release().
| arg | type | description |
|---|
| vx | number | Horizontal creep, world units per second. |
| vy | number | Vertical creep, world units per second. |
examples
entity(id).component.get("Camera2D"):setDrift(0, 0.22)isPanning( ) → boolean
True while a :panTo is still travelling.
examples
if not cam:isPanning() then nextBeat() end
pose( ) → void
Where the shot is right now, in world units. The published NB.state.cameraX/Y with the shake taken back out.
examples
local x, y = cam:pose()
release( ) → void
Hand the shot back to the doctor, cancelling any pan and any drift.
examples
entity(id).component.get("Camera2D"):release()kick(amount: number) → void
Kick the shot — a landing, a bell, a collapsing floor.
| arg | type | description |
|---|
| amount | number | How far the shot jumps, in world units. |
examples
entity(id).component.get("Camera2D"):kick(0.35)onFollowChanged(_newFollow: any, _oldFollow: any) → void
Called by the Camera when its `follow` slot changes. This camera does
not track an entity — it tracks `NB.state.doctor`, which the traverse
slice writes — so binding a follow target only re-poses the shot. The
Camera raises if its behavior has no such method, which is why it is here.
| arg | type | description |
|---|
| _newFollow | any | The entity the camera now follows. |
| _oldFollow | any | The entity it followed before. |
examples
-- called by Camera.onPropertyChanged, not usually by hand
editorUpdate(dt: ?) → void
Edit mode runs no gameplay, but somebody authoring against this scene still
has to be looking at the game's frame rather than at wherever the rig was
parked — and a staged moment has to be rehearsable without entering play,
which locks the VFS for every other slice.