AxisViewCamera
A drafting-table camera: a top view, a side view, and a swing between them.
What it does
The game has three dimensions and the mouse moves in two. Each view looks straight down one axis, so a drag in it moves things along exactly two world axes. The top view looks down −Y with +X to the right and −Z up the screen. The side view looks down −Z with +X to the right and +Y up. Both keep +X on the right, so between them every axis is in reach, and the swing is a quarter turn about X with nothing sliding sideways.
V swaps views, 1 picks the top view and 2 the side view.
The swing follows the arc around the field, not the straight line between the two resting spots. That line passes close to the centre, and the field would balloon halfway through. The camera turns its offset and its facing together and blends the distance, so the field stays framed the whole way round. Reverse mid-swing and the new swing starts from where the camera is.
It is a perspective camera with a narrow lens. Each view backs off until the near face of the play volume fills the window, checked against both its height and its width. The fit is redone every frame from the canvas's CSS size, so a resize never crops the field.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add AxisViewCamera
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
SpaceGolf's field is 72 by 32 by 32, centred on the origin. KeyboardInput turns key presses into the events the view keys arrive on.
Props
view(string, default'top') — the view to start in,'top'or'side'.center(vec3, default[0, 0, 0]) — centre of the play volume. The camera aims here.size(vec3, default[64, 28, 28]) — full size of the volume on x, y and z.fov(number, default0.6) — vertical field of view in radians.margin(number, default1.06) — room around the volume. 1.06 leaves a 6% border; below 1 counts as 1.transition(number, default0.8) — seconds a swing takes. 0 cuts straight to the new view.toggleKey(string, default'KeyV') — key code that swaps views.topKey(string, default'Digit1') — key code for the top view.sideKey(string, default'Digit2') — key code for the side view.near(number, default0.5) — near clip plane.far(number, default4000) — far clip plane.shake(number, default0.6) — how hard a shake kicks the view. At full trauma it moves up to 0.9 world units per unit ofshake. 0 ignores shakes.
Events
Emits:
camera.active.changed{ entityId, handle }once, on the first frame. A pointer tool learns which camera to cast rays from here.axisview.view.changed{ entityId, view, previous }when a swing starts, and once on the first frame so a HUD can light the starting view.
Listens:
keyboard.keydown{ code }for the three view keys.axisview.view.set{ view }andaxisview.view.toggledo the same from a HUD. An unknown view is ignored.cameraShake.impulse{ impulse }adds trauma, which bleeds off at 1.8 per second.
Dependencies
None. The view keys arrive as keyboard.keydown, so something in the scene has to emit them.
Notes
- One System drives one camera, made for the first entity that carries the component. Keep it the only camera component in the scene.
- It runs before
PointerDrag(priority 85 against 75), so rays come from this frame's pose. It keeps running while the world is paused. - A shake moves the camera in the screen plane and never turns it. A tilted view would shift the black holes under the pointer.
fov,nearandfarreach the camera once, when it is made. The fit readsfovevery frame, so afovchanged at runtime moves the camera without changing the lens.- With no canvas, in a headless test, the fit assumes 16:9.
























