PinballScrollCamera
A camera for a pinball table too tall to fit on one screen.
What it does
Alien Crush and Devil's Crush had tables two or three screens tall. The camera showed one screen at a time and slid to the next when the ball got there. This does the same.
It cuts the table into screens equal pieces, with screen 0 at the bottom where the flippers are. Pinball Tower is 78 deep in 3 screens, so each screen is 26: screen 0 runs from z 13 to 39, screen 1 from -13 to 13, screen 2 from -39 to -13.
It fits the camera distance to one screen, not the whole table. With a 0.9 radian field of view, 26 of table needs the camera 13 / tan(0.45) = 26.9 back, times marginFactor for room round the edge.
The camera keeps its screen until the ball is more than hysteresis past the edge. With 1.5, a ball must reach z 11.5 to leave screen 0 for screen 1, and z 14.5 to come back. Without that gap, a ball resting on the line, say on a pair of flippers, flips the view every time it wobbles.
It glides to the new screen at a rate that does not depend on the frame rate. One 1/30 second frame and two 1/60 second frames land on the same spot.
When the ball is parked off the table (below y -5, between balls or after the game) the camera stays where it is.
It sits beside an ArcCamera and moves it by writing its targetOffset z and its distance. It says pinballScrollCamera.screenChanged when the screen changes, so a HUD can show which screen you are on.
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 PinballScrollCamera
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.
The ArcCamera must look at the middle of the table (restTarget on the playfield) with follow off.
Props
followTag(string, default"ball"). Tag of the entity to watch.mode("screens"or"follow", default"screens"). One screen at a time, or keep the ball near the middle.screens(number, default3). How many screens tall the table is.hysteresis(number, default1.5). How far past a screen's edge the ball must go to change screen.ease(number, default5). How fast the camera catches up, per second.lead(number, default2). Follow mode: look this far up the table from the ball.aim(number, default0). Screens mode: look this far down the table (+Z) from each screen's middle. A tilted camera sees more table above the middle of the picture than below it, so a little aim keeps the bottom of each screen in view.fov(number, default0.9). Vertical field of view, radians.marginFactor(number, default1.15). Room round one screen when fitting the distance.lockControls(boolean, defaulttrue). Turn off the mouse orbit.
Events
Emits:
pinballScrollCamera.screenChanged,{ screen, count }. The camera moved to another screen.
Listens to:
pinballTable.resized. The table's depth.
Dependencies
ArcCamera. The camera it moves.PinballTable. The table's depth, if the resize event was missed.MeshPrimitive. Where the ball is.
Notes
- In follow mode the view stops at each end of the table, so it never shows past the top or bottom edge.
- With no
PinballTableat all, it takes the depth as 33.2.



























