@gwenjs/camera-core
v0.1.2
Published
ECS camera system for Gwen. Provides the components, `CameraSystem` orchestrator, and `CameraCorePlugin` shared by `@gwenjs/camera2d` and `@gwenjs/camera3d`.
Readme
@gwenjs/camera-core
ECS camera system for Gwen. Provides the components, CameraSystem orchestrator, and CameraCorePlugin shared by @gwenjs/camera2d and @gwenjs/camera3d.
Note — you normally do not install this package directly. Use
@gwenjs/camera2dor@gwenjs/camera3dinstead. They installCameraCorePluginautomatically.
Installation
npm install @gwenjs/camera-coreQuick start
Since camera-core is a low-level primitive, the canonical way to use it is via a
gwen.config.ts with CameraCorePlugin and a setup system:
// gwen.config.ts
import { defineConfig } from "@gwenjs/app";
import { defineSystem, onUpdate } from "@gwenjs/core/system";
import { useEngine } from "@gwenjs/core";
import { useViewportManager } from "@gwenjs/renderer-core";
import { CameraCorePlugin, Camera, cameraViewportMap } from "@gwenjs/camera-core";
const CameraSetupSystem = defineSystem("CameraSetupSystem", () => {
const engine = useEngine();
const viewports = useViewportManager();
// Register a full-screen viewport (normalized [0–1])
viewports.set("main", { x: 0, y: 0, width: 1, height: 1 });
// Create the camera entity
const camId = engine.createEntity();
engine.addComponent(camId, Camera, {
active: 1,
priority: 0,
projectionType: 0, // 0 = orthographic, 1 = perspective
x: 0,
y: 0,
z: 0,
rotX: 0,
rotY: 0,
rotZ: 0,
zoom: 1,
fov: Math.PI / 3,
near: -1000,
far: 1000,
});
cameraViewportMap.set(camId, "main");
});
export default defineConfig({
plugins: [CameraCorePlugin(), CameraSetupSystem],
});ECS components
| Component | Purpose |
| -------------- | --------------------------------------------------------------------------------------- |
| Camera | Core camera state — position, rotation, projection, active flag, priority |
| FollowTarget | Lerps the camera toward another entity's position each frame |
| CameraBounds | Clamps the camera position to a bounding box after movement |
| CameraShake | Trauma-based screen shake — offsets the rendered position without moving Camera.x/y/z |
| CameraPath | ECS bookmark for path-following state (index + progress in current segment) |
Camera fields
{
active: 0 | 1, // 0 = inactive, 1 = active
priority: number, // higher priority wins the viewport slot
projectionType: 0 | 1, // 0 = orthographic, 1 = perspective
x, y, z: number, // world position
rotX, rotY, rotZ: number, // euler rotation (radians)
zoom: number, // orthographic zoom
fov: number, // perspective field-of-view (radians)
near, far: number, // clipping planes
}FollowTarget fields
{
entityId: number, // target entity (u32 cast of EntityId)
lerp: number, // interpolation factor per frame [0–1]
offsetX, offsetY, offsetZ: number,
}CameraBounds fields
{ minX, minY, minZ, maxX, maxY, maxZ: number }CameraShake fields
{
trauma: number, // current trauma [0–1], add to it to trigger shake
decay: number, // trauma lost per second
maxX: number, // max horizontal offset in world units
maxY: number, // max vertical offset in world units
}Side-car stores
cameraViewportMap and cameraPathStore are module-level Maps that live alongside the ECS components because strings and complex objects cannot be stored in SoA buffers.
import { cameraViewportMap, cameraPathStore } from "@gwenjs/camera-core";
import type { CameraPathData } from "@gwenjs/camera-core";
// Assign a camera to a viewport
cameraViewportMap.set(camId, "main");
// Start a path
const pathData: CameraPathData = {
waypoints: [
{ position: { x: 200, y: 0, z: 0 }, duration: 1.5, easing: "easeInOut" },
{ position: { x: 200, y: 300, z: 0 }, duration: 1.0 },
],
opts: { loop: false, onComplete: () => console.log("done") },
elapsed: 0,
};
engine.addComponent(camId, CameraPath, { index: 0, progress: 0 });
cameraPathStore.set(camId, pathData);Engine hooks
CameraSystem emits these hooks each frame via engine.hooks:
| Hook | Payload | When |
| ------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
| camera:activate | { viewportId: string, entityId: EntityId } | First time a camera becomes active on a viewport |
| camera:deactivate | { viewportId: string } | The active camera is deactivated with no replacement |
| camera:switch | { viewportId: string, from: EntityId, to: EntityId } | Active camera changes from one entity to another |
engine.hooks.hook("camera:activate", ({ viewportId, entityId }) => {
console.log(`camera ${entityId} is now active on ${viewportId}`);
});viewport:* hooks (viewport:add, viewport:resize, viewport:remove) are declared in @gwenjs/renderer-core.
CameraSystem pipeline (per frame)
CameraManager.clearFrame()— stale states are discarded- For each entity in
useQuery([Camera])withactive = 1:- Apply
FollowTargetlerp toward the target entity — or advanceCameraPathwaypoints - Clamp to
CameraBounds - Compute
CameraShakeoffset (does not modifyCamera.x/y/z) - Push
CameraStatetoCameraManager
- Apply
- Detect semantic changes per viewport and emit
camera:activate / deactivate / switch
Multi-camera / priority
Multiple cameras can target the same viewport. The one with the highest Camera.priority wins. On equal priority, the last entity to push its state wins.
Building a custom camera handle
If camera2d/camera3d don't fit your needs, you can build your own on top of camera-core:
import { CameraCorePlugin, Camera, cameraViewportMap } from "@gwenjs/camera-core";
import { useCameraManager } from "@gwenjs/renderer-core";
import { defineSystem, onUpdate } from "@gwenjs/core/system";
await engine.use(CameraCorePlugin());
// Your system reads CameraManager after CameraSystem runs
const MyRenderSystem = defineSystem("MyRenderSystem", () => {
const cameras = useCameraManager();
onUpdate(() => {
const state = cameras.get("main");
if (state) {
const { x, y, z } = state.worldTransform.position;
// apply to your renderer
}
});
});