Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 05cb76a4-7…

Camera2D

The only camera in Nine Bells. Orthographic, side-on, never rotating, never leaving the `NB.CAMERA.z` plane; it looks straight down -Z at a flat city on a 1080x1440 portrait screen, which covers sixteen world units vertically and only twelve across.

byclaude-code @ aura·posted 2mo ago
What it does

Camera2D — the camera of Ashcross

The only camera in Nine Bells. Orthographic, side-on, never rotating, never leaving the NB.CAMERA.z plane; it looks straight down -Z at a flat city on a 1080x1440 portrait screen, which covers sixteen world units vertically and only twelve across.

In play

It follows NB.state.doctor — not an entity, because the traverse slice runs its own 2D physics and writes the doctor's position into the blackboard. There is a deadzone the doctor moves inside without the camera answering (narrow horizontally, because there is almost no horizontal room to give away), a lead in the direction of travel scaled by run speed, and a softer vertical time constant so a landing reads as weight. It publishes where it ended up into NB.state.cameraX / cameraY, which is what the backdrop reads for its scripted parallax and what every slice reads to decide whether a thing is on screen.

When a moment takes it

CallWhat it does
:panTo(x, y, seconds, ease)a TIMED move along an eased curve; :isPanning() says when it has arrived
:zoomTo(height, seconds, ease)a TIMED change of lens. 16 is the playing frame, 10 a push onto a face, 22 the district opening out
:setDrift(vx, vy)a slow creep added to a driven shot — a held frame that drifts is alive, one that is nailed down is a still
:kick(amount)the shake: a bell, a hard landing, a floor giving way
:snapTo(x, y)a cut. Take the override too (:driveTo) or the follow walks it straight back
:driveTo(x, y)the untimed, damped drive
:release()hand the shot back to the doctor, cancelling any pan and drift
:setBounds(minX, maxX, minY, maxY)the city slice's limits on the FOLLOW. A driven shot ignores them: a staged moment may look above the roofline
:pose()where the shot is, with the shake taken back out

Ease names: linear inQuad outQuad inOutQuad outCubic inOutCubic inOutSine outExpo.

It also carries the staging clock

game.stage installs NB.ext.stageTick and NB.ext.stagePostTick; this component calls them once a frame, in edit mode as well as in play, so a moment can be rehearsed without entering play and locking the VFS for the other slices. The pre-tick runs before the pose, so a cue that starts a pan this frame is answered this frame; the post-tick runs after it, which is where anything held in front of the lens is placed so it never lags the shake. Both calls are 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"),
})

Owned by the staging slice. See /zero/source/design/spec.md.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

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

argtypedescription
tnumber

easeInQuad(t: number) → number return t

argtypedescription
tnumber

easeOutQuad(t: number) → number return 1

argtypedescription
tnumber

easeInOutQuad(t: number) → number

argtypedescription
tnumber

easeOutCubic(t: number) → number

argtypedescription
tnumber

easeInOutCubic(t: number) → number

argtypedescription
tnumber

easeInOutSine(t: number) → number

argtypedescription
tnumber

easeOutExpo(t: number) → number

argtypedescription
tnumber

camComponent( ) → any

applyLens(h: number) → void

argtypedescription
hnumber

targetPose( ) → void

The follow

pose(dt: number) → void

argtypedescription
dtnumber

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.

argtypedescription
whichstring
dtnumber

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.

argtypedescription
dtnumber

tickStagePost(dt: number) → void

argtypedescription
dtnumber

snapTo(x: number, y: number) → void

Snap the shot to a world point with no easing — for a cut rather than a move.

argtypedescription
xnumberWorld x to sit at.
ynumberWorld 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.

argtypedescription
minXnumberLeft edge of the shot's travel, in world units.
maxXnumberRight edge.
minYnumberBottom edge.
maxYnumberTop 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.

argtypedescription
xnumberWorld x to drive toward.
ynumberWorld 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.

argtypedescription
xnumberWorld x to arrive at.
ynumberWorld y to arrive at.
secondsnumberHow long the move takes. 0 arrives immediately.
easestring?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.

argtypedescription
heightnumberWorld units the frame covers vertically at the end of the move.
secondsnumberHow long the change takes. 0 applies it at once.
easestring?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().

argtypedescription
vxnumberHorizontal creep, world units per second.
vynumberVertical 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.

argtypedescription
amountnumberHow 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.

argtypedescription
_newFollowanyThe entity the camera now follows.
_oldFollowanyThe entity it followed before.

examples

-- called by Camera.onPropertyChanged, not usually by hand

awake( ) → void

start( ) → void

update(dt: ?) → void

argtypedescription
dt?

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.

argtypedescription
dt?
⌬ Metadata
descriptionThe orthographic side-on camera of Nine Bells: a deadzone-and-lead follow on a portrait 3:4 frame, extended with timed eased pans, a timed lens push, a drift for held shots, screen shake and district bounds. It also carries the staging slice's per-frame clock, before and after the pose, so a cinematic moment can be rehearsed in edit mode without entering play.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

7items
▣
module · born here
❒asset
# nb <!-- zero:scaffolded-readme --> <!-- Replace the line below with what this module provides and who consumes it, then delete both comment lines. --> TODO: describe this module.
▲ 0↑ born
backing path · game/Camera2D.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.