feat(frontend): eased camera fly-to on scene transitions (reduced-motion aware)
This commit is contained in:
@@ -9,7 +9,12 @@
|
|||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { type CameraState, fitCamera } from "@/lib/constellation/camera";
|
import {
|
||||||
|
type CameraState,
|
||||||
|
easeInOutCubic,
|
||||||
|
fitCamera,
|
||||||
|
flyTo,
|
||||||
|
} from "@/lib/constellation/camera";
|
||||||
import type { ConstellationGraph } from "@/lib/constellation/graph";
|
import type { ConstellationGraph } from "@/lib/constellation/graph";
|
||||||
import { computeLayout, type LayoutNode } from "@/lib/constellation/layout";
|
import { computeLayout, type LayoutNode } from "@/lib/constellation/layout";
|
||||||
import { readPalette, type StagePalette } from "@/lib/constellation/palette";
|
import { readPalette, type StagePalette } from "@/lib/constellation/palette";
|
||||||
@@ -59,6 +64,14 @@ export function ConstellationStage({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Deterministic layout whenever graph or viewport changes.
|
// Deterministic layout whenever graph or viewport changes.
|
||||||
|
// On scene change we FLY the camera to the new fit instead of snapping.
|
||||||
|
const prevSceneRef = useRef("");
|
||||||
|
const animRef = useRef<{
|
||||||
|
from: CameraState;
|
||||||
|
to: CameraState;
|
||||||
|
t0: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (size.w < 50 || size.h < 50) return;
|
if (size.w < 50 || size.h < 50) return;
|
||||||
const layout = computeLayout(graph.nodes, graph.edges, {
|
const layout = computeLayout(graph.nodes, graph.edges, {
|
||||||
@@ -68,7 +81,19 @@ export function ConstellationStage({
|
|||||||
reduced,
|
reduced,
|
||||||
});
|
});
|
||||||
layoutRef.current = layout;
|
layoutRef.current = layout;
|
||||||
camRef.current = fitCamera(layout, size.w, size.h);
|
const target = fitCamera(layout, size.w, size.h);
|
||||||
|
const first = graph.nodes[0]?.id ?? "";
|
||||||
|
const sceneKey = `${graph.nodes.length}:${first}`;
|
||||||
|
if (!reduced && prevSceneRef.current && prevSceneRef.current !== sceneKey) {
|
||||||
|
animRef.current = {
|
||||||
|
from: { ...camRef.current },
|
||||||
|
to: target,
|
||||||
|
t0: performance.now(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
camRef.current = target;
|
||||||
|
}
|
||||||
|
prevSceneRef.current = sceneKey;
|
||||||
}, [graph, size.w, size.h, seed, reduced]);
|
}, [graph, size.w, size.h, seed, reduced]);
|
||||||
|
|
||||||
// Build + run the renderer.
|
// Build + run the renderer.
|
||||||
@@ -310,6 +335,19 @@ export function ConstellationStage({
|
|||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
raf = requestAnimationFrame(frame);
|
raf = requestAnimationFrame(frame);
|
||||||
if (hidden) return;
|
if (hidden) return;
|
||||||
|
|
||||||
|
// Camera fly-to (scene transitions): eased interpolation over ~900ms.
|
||||||
|
const anim = animRef.current;
|
||||||
|
if (anim) {
|
||||||
|
const raw = (now - anim.t0) / 900;
|
||||||
|
if (raw >= 1) {
|
||||||
|
camRef.current = anim.to;
|
||||||
|
animRef.current = null;
|
||||||
|
} else if (raw > 0) {
|
||||||
|
camRef.current = flyTo(anim.from, anim.to, raw, easeInOutCubic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applyCamera();
|
applyCamera();
|
||||||
|
|
||||||
// Gentle breathing glow (visual only — hit positions stay baked).
|
// Gentle breathing glow (visual only — hit positions stay baked).
|
||||||
|
|||||||
Reference in New Issue
Block a user