three-kit

v0.1.0·changelog·edit on GitHub

@zkmake/three-cameras

A dev panel for the cameras in a three.js scene. It lists every camera: the ones in the scene graph, and the one the renderer draws with even when it isn’t in the scene (R3F’s default camera, most vanilla apps’ main camera). It marks which cameras are drawing right now and how often, reads each one’s position and projection live, and draws a camera’s frustum in the scene.

Pick a camera and you control it: edit its position, rotation and lens by typing or scrubbing, look through it in place of the app’s main view, copy its setup as code, and save views to fly back to. Animate cameras on a keyframe timeline: key a camera, retime and ease its keys, and play the move back in the running scene, shaping them in a graph editor and an ease curve editor, with the motion path drawn in the scene.

It’s one panel, anchored across the bottom of the screen: the camera list as the timeline’s left sidebar, each camera’s row beside its key lane. Collapsed, it’s a small widget with the picked camera (its kind, name, and whether it’s live or looked through); click it to expand.

bun add -d @zkmake/three-cameras   # or npm i -D / yarn add -D / pnpm add -D

Live demo: three-kit.pages.dev/three-cameras, with a vanilla three and a React Three Fiber take on the same set: an orbit view, a dolly camera circling the set and drawing a picture-in-picture inset, an overhead orthographic camera and a security camera.

Vanilla three

import { mountCameraPanel } from "@zkmake/three-cameras/ui";

const panel = mountCameraPanel({ scene, renderer });
// later: panel.dispose()

React Three Fiber

import { CameraPanel } from "@zkmake/three-cameras/react";

<Canvas>
  <CameraPanel />
</Canvas>;

It reads the scene and renderer from the canvas, so the canvas’s camera shows as live, and asks for a frame after a frustum toggles, so frameloop="demand" works.

What each row shows

Part What it is
icon The kind: perspective, orthographic, array or cube. Green while the camera draws.
live · 60 fps It drew a frame in the last half second, and how many in the last second
second line fov, near–far and aspect (perspective), or zoom and near–far (orthographic); “not in scene”
look through Show the main view through it (see below)
frustum Draw its view volume in the scene. It follows the camera, and hides while you look through it

Rows are named by the cameras option, else the camera’s name, else its kind (perspective camera 2). Name your cameras and the panel reads better.

Controlling a camera

Pick a row (click its name) and it opens into the camera’s controls:

Look through swaps a camera into the app’s main view: the view drawing the most of the screen, so a picture-in-picture inset or a render target keeps its own camera. The camera is fitted to that view’s aspect for the frame and put back after, and the app’s camera is left where it was. A banner shows what you’re looking through, with a way back.

The timeline

The timeline animates cameras with keyframes. Its left column is the camera list: each camera’s row sits beside its lane, and a row opened into its controls stretches its lane to match.

Shaping a move

Motion paths in the scene

A camera that moves shows its path in the scene by itself, while it moves (cameras in the scene; your own view camera is left out):

How it finds cameras

dispose() puts render back, unless something wrapped it after the panel did, in which case the panel’s wrapper stays in place and just passes through.

Options

Option Default What it does
scene required Where to look for cameras, and where frustum helpers go
renderer none Watched for the cameras it draws with; without it nothing is “live”
cameras [] { name, camera } to list whether or not the scene holds them yet
invalidate none Ask for frames after an edit, a toggle, or during a move (render-on-demand loops)
store localStorage, by storageKey Where saved views live: { load(), save(views) }
trackStore localStorage, by storageKey Where keyframe tracks live: { load(), save({ duration, tracks }) }
timeline true The timeline, with the camera list as its sidebar, across the bottom; false for the list alone
storageKey "three-cameras" localStorage prefix for the dock and compact state; null for none
defaultPlacement { edge: "right", align: "start" } Where the list alone docks on a first visit (with the timeline it spans the bottom)
theme "system" "dark", "light" or "system"
compact false Start compact, showing the brand row and the count
container document.body Where to mount

The engine

CameraLab is the panel’s engine, with no DOM, for scripting or a panel of your own:

import { CameraLab, writeChannel } from "@zkmake/three-cameras";
import { Vector3 } from "three";
import { createCameraPanel } from "@zkmake/three-cameras/ui";

const lab = new CameraLab({ scene, renderer });

lab.entries(); // [{ id, kind, camera, inScene, live, fps, helper, viewing, views, … }]
lab.details("dolly"); // { position, rotation, local: { position, rotation }, projection }
lab.set("dolly", { fov: 30, position: [0, 2, 8] });
lab.lookThrough("dolly"); // null to go back
lab.setHelper("dolly", true);

const home = lab.saveView("dolly", "home");
lab.goToView("dolly", home.id, { duration: 800 });

lab.addKey("dolly", { time: 0 }); // the camera as it is now
lab.addKey("dolly", { time: 4, ease: "ease-out" });
lab.play(); // pause(), seek(2), stop(), setLoop(false), setDuration(12)

const [first] = lab.keys("dolly");
lab.updateKey("dolly", first.id, { bezier: [0.3, 1.4, 0.6, 1] }); // overshoot
lab.updateKey("dolly", first.id, { pose: writeChannel(first.pose, "fov", 30) });
lab.setTrail("dolly", true); // or false, or "auto" (the default)
lab.bakeMotion("dolly"); // its recorded move as keys
lab.moveKeyTo("dolly", home.id, new Vector3(0, 3, 8)); // a key to a world point

tab.append(createCameraPanel(lab).element); // the bare panel, for a host with its own tabs

The dev-panel frame

It sits in three-meter’s mountDevPanel, so it compacts and dims like the three-meter HUD and the three-textures panel, with its brand label on top. With the timeline it’s anchored across the bottom (no drag grip); the list alone drags and snaps to an edge like the others.

License

MIT