@zkmake/three-cameras 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zubin Khavarian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @zkmake/three-cameras
2
+
3
+ A dev panel for the cameras in a three.js scene. It lists every camera: the ones in the scene
4
+ graph, and the one the renderer draws with even when it isn't in the scene (R3F's default camera,
5
+ most vanilla apps' main camera). It marks which cameras are drawing right now and how often,
6
+ reads each one's position and projection live, and draws a camera's frustum in the scene.
7
+
8
+ Pick a camera and you control it: edit its position, rotation and lens by typing or scrubbing, look
9
+ through it in place of the app's main view, copy its setup as code, and save views to fly back to.
10
+ Keyframed camera moves with curve and graph editors are next.
11
+
12
+ ```sh
13
+ bun add -d @zkmake/three-cameras # or npm i -D / yarn add -D / pnpm add -D
14
+ ```
15
+
16
+ Live demo: [three-kit.pages.dev/three-cameras](https://three-kit.pages.dev/three-cameras/), with a
17
+ vanilla three and a React Three Fiber take on the same set: an orbit view, a dolly camera circling
18
+ the set and drawing a picture-in-picture inset, an overhead orthographic camera and a security
19
+ camera.
20
+
21
+ ## Vanilla three
22
+
23
+ ```ts
24
+ import { mountCameraPanel } from "@zkmake/three-cameras/ui";
25
+
26
+ const panel = mountCameraPanel({ scene, renderer });
27
+ // later: panel.dispose()
28
+ ```
29
+
30
+ ## React Three Fiber
31
+
32
+ ```tsx
33
+ import { CameraPanel } from "@zkmake/three-cameras/react";
34
+
35
+ <Canvas>
36
+ <CameraPanel />
37
+ </Canvas>;
38
+ ```
39
+
40
+ It reads the scene and renderer from the canvas, so the canvas's camera shows as live, and asks for
41
+ a frame after a frustum toggles, so `frameloop="demand"` works.
42
+
43
+ ## What each row shows
44
+
45
+ | Part | What it is |
46
+ | ------------- | --------------------------------------------------------------------------------------------- |
47
+ | icon | The kind: perspective, orthographic, array or cube. Green while the camera draws. |
48
+ | live · 60 fps | It drew a frame in the last half second, and how many in the last second |
49
+ | second line | fov, near–far and aspect (perspective), or zoom and near–far (orthographic); "not in scene" |
50
+ | look through | Show the main view through it (see below) |
51
+ | frustum | Draw its view volume in the scene. It follows the camera, and hides while you look through it |
52
+
53
+ Rows are named by the `cameras` option, else the camera's `name`, else its kind (`perspective camera
54
+ 2`). Name your cameras and the panel reads better.
55
+
56
+ ## Controlling a camera
57
+
58
+ Pick a row (click its name) and it opens into the camera's controls:
59
+
60
+ - **Position and rotation**, its own (relative to its parent), rotation in degrees, and **lens**: fov,
61
+ near, far and zoom. Type a value, or drag a field's label to scrub it: Shift for fine steps, Alt
62
+ for coarse. An app that sets the camera every frame wins on its next frame.
63
+ - **Copy as code**: three.js that sets the camera up as it is now, named after the camera.
64
+ - **Save view**: keeps where it is and how it projects. Click a saved view to fly back to it (eased
65
+ over 0.8 s); the docked panel keeps them in localStorage, by camera name.
66
+ - A readout of what isn't edited here: world position, aspect, frame rate, where it sits.
67
+
68
+ **Look through** swaps a camera into the app's main view: the view drawing the most of the screen,
69
+ so a picture-in-picture inset or a render target keeps its own camera. The camera is fitted to that
70
+ view's aspect for the frame and put back after, and the app's camera is left where it was. A banner
71
+ shows what you're looking through, with a way back.
72
+
73
+ ## How it finds cameras
74
+
75
+ - **The scene graph:** anything with `isCamera` under `scene`. A `CubeCamera` is one row; its six face
76
+ cameras aren't listed.
77
+ - **The renderer:** the lab wraps `renderer.render` on the instance and notes every camera it's
78
+ handed. A camera the scene doesn't hold gets a row on its first frame and stays while it keeps
79
+ drawing (a few seconds after it stops). Post-processing that calls `renderer.render` counts too.
80
+
81
+ `dispose()` puts `render` back, unless something wrapped it after the panel did, in which case the
82
+ panel's wrapper stays in place and just passes through.
83
+
84
+ ## Options
85
+
86
+ | Option | Default | What it does |
87
+ | ------------------ | ----------------------------------- | --------------------------------------------------------------------------------- |
88
+ | `scene` | required | Where to look for cameras, and where frustum helpers go |
89
+ | `renderer` | none | Watched for the cameras it draws with; without it nothing is "live" |
90
+ | `cameras` | `[]` | `{ name, camera }` to list whether or not the scene holds them yet |
91
+ | `invalidate` | none | Ask for frames after an edit, a toggle, or during a move (render-on-demand loops) |
92
+ | `store` | localStorage, by `storageKey` | Where saved views live: `{ load(), save(views) }` |
93
+ | `storageKey` | `"three-cameras"` | localStorage prefix for the dock and compact state; `null` for none |
94
+ | `defaultPlacement` | `{ edge: "right", align: "start" }` | Where it docks on a first visit |
95
+ | `theme` | `"system"` | `"dark"`, `"light"` or `"system"` |
96
+ | `compact` | `false` | Start compact, showing the brand row and the count |
97
+ | `container` | `document.body` | Where to mount |
98
+
99
+ ## The engine
100
+
101
+ `CameraLab` is the panel's engine, with no DOM, for scripting or a panel of your own:
102
+
103
+ ```ts
104
+ import { CameraLab } from "@zkmake/three-cameras";
105
+ import { createCameraPanel } from "@zkmake/three-cameras/ui";
106
+
107
+ const lab = new CameraLab({ scene, renderer });
108
+
109
+ lab.entries(); // [{ id, kind, camera, inScene, live, fps, helper, viewing, views, … }]
110
+ lab.details("dolly"); // { position, rotation, local: { position, rotation }, projection }
111
+ lab.set("dolly", { fov: 30, position: [0, 2, 8] });
112
+ lab.lookThrough("dolly"); // null to go back
113
+ lab.setHelper("dolly", true);
114
+
115
+ const home = lab.saveView("dolly", "home");
116
+ lab.goToView("dolly", home.id, { duration: 800 });
117
+
118
+ tab.append(createCameraPanel(lab).element); // the bare panel, for a host with its own tabs
119
+ ```
120
+
121
+ ## The dev-panel frame
122
+
123
+ It docks in three-meter's `mountDevPanel`, so it drags, snaps to an edge, compacts and dims exactly
124
+ like the three-meter HUD and the three-textures panel, with its brand label on top.
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,2 @@
1
+ import { C as pathOf, S as kindOf, _ as CameraKind, a as CameraLabOptions, b as isCamera, c as RendererLike, d as Pose, f as applyPose, g as poseToCode, h as easeInOut, i as CameraLab, l as SavedView, m as capturePose, n as CameraEntry, o as CameraPatch, p as blendPoses, r as CameraInfo, s as Projection, t as CameraDetails, u as ViewStore, v as SceneCamera, x as isCubeCamera, y as findCameras } from "./lab-AaEFBp_R.js";
2
+ export { type CameraDetails, type CameraEntry, type CameraInfo, type CameraKind, CameraLab, type CameraLabOptions, type CameraPatch, type Pose, type Projection, type RendererLike, type SavedView, type SceneCamera, type ViewStore, applyPose, blendPoses, capturePose, easeInOut, findCameras, isCamera, isCubeCamera, kindOf, pathOf, poseToCode };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as easeInOut, c as isCamera, d as pathOf, i as capturePose, l as isCubeCamera, n as applyPose, o as poseToCode, r as blendPoses, s as findCameras, t as CameraLab, u as kindOf } from "./lab-D4PnOVwG.js";
2
+ export { CameraLab, applyPose, blendPoses, capturePose, easeInOut, findCameras, isCamera, isCubeCamera, kindOf, pathOf, poseToCode };
@@ -0,0 +1,206 @@
1
+ import { Camera, Object3D } from "three";
2
+ //#region src/core/discover.d.ts
3
+ type CameraKind = "perspective" | "orthographic" | "array" | "cube" | "other";
4
+ /** A camera, or a `CubeCamera`, which is an `Object3D` holding six. */
5
+ type SceneCamera = Camera | Object3D;
6
+ declare const isCubeCamera: (object: Object3D) => boolean;
7
+ declare const isCamera: (object: Object3D) => object is Camera;
8
+ declare const kindOf: (camera: SceneCamera) => CameraKind;
9
+ /** Every camera under `root`, in scene order, `root` included. */
10
+ declare const findCameras: (root: Object3D) => SceneCamera[];
11
+ /** Where it sits: its ancestors' names (or types) from the root down, `scene › rig › cam`. */
12
+ declare const pathOf: (object: Object3D) => string[];
13
+ //#endregion
14
+ //#region src/core/pose.d.ts
15
+ type Pose = {
16
+ position: [number, number, number];
17
+ /** x, y, z, w. */
18
+ quaternion: [number, number, number, number];
19
+ near: number;
20
+ far: number;
21
+ zoom: number;
22
+ /** Perspective cameras only. */
23
+ fov?: number;
24
+ };
25
+ declare const capturePose: (camera: Camera) => Pose;
26
+ declare const applyPose: (camera: Camera, pose: Pose) => void;
27
+ /** The pose `t` of the way from `a` to `b`: positions and lenses straight, rotation by slerp. */
28
+ declare const blendPoses: (a: Pose, b: Pose, t: number) => Pose;
29
+ /** Slow in, slow out: a camera move that starts and settles gently. */
30
+ declare const easeInOut: (t: number) => number;
31
+ /** Three.js to recreate the pose on a camera called `name`. */
32
+ declare const poseToCode: (pose: Pose, name?: string) => string;
33
+ //#endregion
34
+ //#region src/core/lab.d.ts
35
+ type CameraInfo = {
36
+ name: string;
37
+ camera: SceneCamera;
38
+ };
39
+ /** The part of a renderer the lab needs: `render(scene, camera)`. */
40
+ type RendererLike = {
41
+ render: (scene: Object3D, camera: Camera, ...rest: never[]) => unknown;
42
+ };
43
+ type CameraLabOptions = {
44
+ /** Where to look for cameras, and where frustum helpers go. */
45
+ scene: Object3D;
46
+ /** A three renderer: its `render` is watched, so cameras outside the scene show up and live ones are marked. */
47
+ renderer?: unknown;
48
+ /** Cameras to list whether or not the scene holds them yet, with names. */
49
+ cameras?: readonly CameraInfo[];
50
+ /** Ask for a frame, for render-on-demand loops (R3F `frameloop="demand"`). */
51
+ invalidate?: () => void;
52
+ /** Where saved views live. Default: this page only. */
53
+ store?: ViewStore;
54
+ };
55
+ /** A pose saved on a camera, to go back to. */
56
+ type SavedView = {
57
+ id: string;
58
+ name: string;
59
+ pose: Pose;
60
+ };
61
+ /** Saved views by camera name. `mountCameraPanel` keeps them in localStorage. */
62
+ type ViewStore = {
63
+ load(): Record<string, SavedView[]>;
64
+ save(views: Record<string, SavedView[]>): void;
65
+ };
66
+ /** Values to set on a camera. Position and rotation are its own (local); rotation in degrees. */
67
+ type CameraPatch = {
68
+ position?: readonly [number, number, number];
69
+ rotation?: readonly [number, number, number];
70
+ fov?: number;
71
+ near?: number;
72
+ far?: number;
73
+ zoom?: number;
74
+ };
75
+ type CameraEntry = {
76
+ id: string;
77
+ kind: CameraKind;
78
+ camera: SceneCamera;
79
+ /** Under the lab's scene. A camera only the renderer knows about isn't. */
80
+ inScene: boolean;
81
+ /** Drew a frame in the last half second. */
82
+ live: boolean;
83
+ /** Frames drawn with it in the last second. */
84
+ fps: number;
85
+ /** Its frustum helper is showing. */
86
+ helper: boolean;
87
+ /** A frustum helper can draw it: perspective and orthographic cameras. */
88
+ canHelp: boolean;
89
+ /** Its ancestors, root first. */
90
+ path: string[];
91
+ /** Perspective or orthographic: its lens can be edited, looked through and saved. */
92
+ projects: boolean;
93
+ /** The main view shows through it now. */
94
+ viewing: boolean;
95
+ /** The app's camera that it's standing in for, while another is looked through. */
96
+ standingIn: boolean;
97
+ /** Saved views on it. */
98
+ views: number;
99
+ };
100
+ type Projection = {
101
+ near: number;
102
+ far: number;
103
+ zoom: number;
104
+ /** Perspective: vertical field of view in degrees, and aspect. */
105
+ fov?: number;
106
+ aspect?: number;
107
+ /** Orthographic: the frustum's sides. */
108
+ left?: number;
109
+ right?: number;
110
+ top?: number;
111
+ bottom?: number;
112
+ };
113
+ type CameraDetails = {
114
+ /** World position. */
115
+ position: [number, number, number];
116
+ /** World rotation, XYZ Euler in degrees. */
117
+ rotation: [number, number, number];
118
+ /** Its own position and rotation (degrees), relative to its parent: what `set` changes. */
119
+ local: {
120
+ position: [number, number, number];
121
+ rotation: [number, number, number];
122
+ };
123
+ projection: Projection | null;
124
+ };
125
+ declare class CameraLab {
126
+ private readonly options;
127
+ private readonly records;
128
+ private readonly byId;
129
+ private readonly listeners;
130
+ private readonly tweens;
131
+ private readonly store;
132
+ private saved;
133
+ private restoreRender;
134
+ /** Look-through: the camera shown, and the app camera it stands in for. */
135
+ private looking;
136
+ private standIn;
137
+ /** The frame being drawn: what to put back after it. */
138
+ private fitted;
139
+ private readonly viewport;
140
+ private notifyQueued;
141
+ private disposed;
142
+ constructor(options: CameraLabOptions);
143
+ /** Whether the lab sees frames: look-through and animated moves need a renderer. */
144
+ get watching(): boolean;
145
+ subscribe(listener: () => void): () => void;
146
+ /** Every camera, in scene order; ones only the renderer knows about after. */
147
+ entries(): CameraEntry[];
148
+ entry(id: string): CameraEntry | null;
149
+ /** Where it is and how it projects, read now. */
150
+ details(id: string): CameraDetails | null;
151
+ /**
152
+ * Change a camera: its own position or rotation, or its lens. The projection updates, and a
153
+ * render-on-demand loop gets a frame. An app that sets the camera every frame wins on the next.
154
+ */
155
+ set(id: string, patch: CameraPatch): void;
156
+ /**
157
+ * Show the main view through `id`, or back through the app's own camera with `null`. The camera
158
+ * it stands in for is the app's main view: the one drawing the most of the screen (a
159
+ * picture-in-picture inset or a render target loses to it), most often on a tie.
160
+ */
161
+ lookThrough(id: string | null): void;
162
+ /** Save where the camera is and how it projects, as a view to go back to. */
163
+ saveView(id: string, name?: string): SavedView;
164
+ views(id: string): SavedView[];
165
+ deleteView(id: string, viewId: string): void;
166
+ /**
167
+ * Move the camera to a saved view: eased over `duration` ms, stepped once per frame (so it needs
168
+ * a renderer; without one, or with `duration: 0`, it lands at once).
169
+ */
170
+ goToView(id: string, viewId: string, { duration }?: {
171
+ duration?: number;
172
+ }): void;
173
+ /**
174
+ * Look at the scene again: new cameras get entries, ones that left go (a camera outside the
175
+ * scene stays while it's still rendering, and a few seconds after).
176
+ */
177
+ refresh(): void;
178
+ /** Show or hide a camera's frustum. The helper sits in the lab's scene and follows the camera. */
179
+ setHelper(id: string, on: boolean): void;
180
+ dispose(): void;
181
+ private watch;
182
+ /** Before a frame: count it, step moves, and pick the camera it's drawn with. */
183
+ private beforeFrame;
184
+ private afterFrame;
185
+ /** How much of the screen this frame covers, if the renderer says. */
186
+ private measure;
187
+ private count;
188
+ /** Moves to saved views, one step per frame. */
189
+ private step;
190
+ /** A camera the renderer was handed that belongs to one listed, like a CubeCamera's faces. */
191
+ private findOwner;
192
+ private canHelp;
193
+ private projects;
194
+ private add;
195
+ private remove;
196
+ private dropHelper;
197
+ private uniqueName;
198
+ private inScene;
199
+ private entryOf;
200
+ private require;
201
+ private notifySoon;
202
+ private notify;
203
+ }
204
+ //#endregion
205
+ export { pathOf as C, kindOf as S, CameraKind as _, CameraLabOptions as a, isCamera as b, RendererLike as c, Pose as d, applyPose as f, poseToCode as g, easeInOut as h, CameraLab as i, SavedView as l, capturePose as m, CameraEntry as n, CameraPatch as o, blendPoses as p, CameraInfo as r, Projection as s, CameraDetails as t, ViewStore as u, SceneCamera as v, isCubeCamera as x, findCameras as y };
206
+ //# sourceMappingURL=lab-AaEFBp_R.d.ts.map