diff --git a/apps/dashboard/src/lib/graph/cinema/director.ts b/apps/dashboard/src/lib/graph/cinema/director.ts index 6252cbc..0891169 100644 --- a/apps/dashboard/src/lib/graph/cinema/director.ts +++ b/apps/dashboard/src/lib/graph/cinema/director.ts @@ -168,6 +168,11 @@ export class CinemaDirector { else if (shot.move === 'pull_back') standoff *= 1.5; else if (shot.move === 'crane') standoff *= 1.8; } + // In centered (WebGPU storm) mode the subject is pinned to the origin and + // the sandbox clamps the camera to a far band. Keep the directed standoff + // INSIDE that band so the camera never fights the clamp (which read as an + // off-center jump) — variety here comes from angle + orbit, not distance. + if (this.opts.centerOnOrigin) standoff = Math.max(31, Math.min(43, standoff)); return out.copy(nodePos).addScaledVector(_tmpDir, standoff); } diff --git a/apps/dashboard/src/lib/graph/cinema/sandbox.ts b/apps/dashboard/src/lib/graph/cinema/sandbox.ts index eec67d4..8c30087 100644 --- a/apps/dashboard/src/lib/graph/cinema/sandbox.ts +++ b/apps/dashboard/src/lib/graph/cinema/sandbox.ts @@ -16,8 +16,11 @@ import type { SemanticRole, SemanticComputeStorm } from './storm'; // The storm lives at the world origin, permanently. The camera always looks here // and is clamped to a safe distance band so the subject can never leave frame. const ORIGIN = new THREE.Vector3(0, 0, 0); -const MIN_CAM_DIST = 18; -const MAX_CAM_DIST = 46; +// Keep the camera in a narrow, fairly FAR band so the contained storm always +// sits comfortably small and centered in frame (a closer camera makes the cloud +// fill — and spill past — the edges once the bloom halo is added). +const MIN_CAM_DIST = 30; +const MAX_CAM_DIST = 44; export function isWebGPUSupported(): boolean { return typeof navigator !== 'undefined' && 'gpu' in navigator; @@ -185,11 +188,14 @@ export class CinemaSandbox { } this.camera.lookAt(ORIGIN); - // Size the containment sphere to the camera's FOV at the origin so the - // storm always fully fits the frame with margin. + // Size the containment sphere to the camera's VERTICAL FOV at the origin + // (the limiting dimension on a landscape frame). The 0.40 factor leaves + // generous room for the additive BLOOM HALO — each particle's glow spreads + // well beyond its geometric position, so the visible cloud is much larger + // than the radius; an aggressive margin is what actually stops the clip. const dist = this.camera.position.length(); const vfov = (this.camera.fov * Math.PI) / 180; - const fitRadius = Math.tan(vfov / 2) * dist * 0.55; + const fitRadius = Math.tan(vfov / 2) * dist * 0.4; this.storm.setContainRadius(fitRadius); await this.storm.update(deltaSeconds); diff --git a/apps/dashboard/src/lib/graph/cinema/storm.ts b/apps/dashboard/src/lib/graph/cinema/storm.ts index 08bae39..dfc7a08 100644 --- a/apps/dashboard/src/lib/graph/cinema/storm.ts +++ b/apps/dashboard/src/lib/graph/cinema/storm.ts @@ -110,7 +110,9 @@ export class SemanticComputeStorm { this.renderer = renderer; this.scene = scene; this.count = opts.count ?? 150_000; - const spawn = opts.spawnRadius ?? 15; + // Spawn inside the contained zone so particles don't start outside the + // shell and get yanked inward asymmetrically (which read as off-center). + const spawn = opts.spawnRadius ?? 8; const positions = new Float32Array(this.count * 3); const velocities = new Float32Array(this.count * 3);