update: add runtine camera keyframe
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import gsap from "gsap";
|
||||
import * as THREE from "three";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import type {
|
||||
CinematicDefinition,
|
||||
CinematicManifest,
|
||||
} from "@/types/cinematics/cinematics";
|
||||
import { logger } from "@/utils/core/logger";
|
||||
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
|
||||
|
||||
export function GameCinematics(): null {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
|
||||
const playedCinematicsRef = useRef(new Set<string>());
|
||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void loadCinematicManifest()
|
||||
.then((loadedManifest) => {
|
||||
if (mounted) setManifest(loadedManifest);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
logger.error("GameCinematics", "Failed to load cinematic manifest", {
|
||||
error: error instanceof Error ? error : String(error),
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
stopActiveCinematic(timelineRef);
|
||||
useGameStore.getState().setCinematicPlaying(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!manifest) return;
|
||||
|
||||
const elapsedTime = clock.getElapsedTime();
|
||||
|
||||
manifest.cinematics.forEach((cinematic) => {
|
||||
if (cinematic.timecode === undefined) return;
|
||||
if (cinematic.timecode > elapsedTime) return;
|
||||
if (playedCinematicsRef.current.has(cinematic.id)) return;
|
||||
|
||||
playedCinematicsRef.current.add(cinematic.id);
|
||||
playCinematic(camera, cinematic, timelineRef);
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function stopActiveCinematic(
|
||||
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
|
||||
): void {
|
||||
timelineRef.current?.kill();
|
||||
timelineRef.current = null;
|
||||
}
|
||||
|
||||
function playCinematic(
|
||||
camera: THREE.Camera,
|
||||
cinematic: CinematicDefinition,
|
||||
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
|
||||
): void {
|
||||
const firstKeyframe = cinematic.cameraKeyframes[0];
|
||||
if (!firstKeyframe) return;
|
||||
|
||||
document.exitPointerLock();
|
||||
timelineRef.current?.kill();
|
||||
useGameStore.getState().setCinematicPlaying(true);
|
||||
|
||||
const target = new THREE.Vector3(...firstKeyframe.target);
|
||||
camera.position.set(...firstKeyframe.position);
|
||||
camera.lookAt(target);
|
||||
|
||||
const timeline = gsap.timeline({
|
||||
onUpdate: () => camera.lookAt(target),
|
||||
onComplete: () => {
|
||||
timelineRef.current = null;
|
||||
useGameStore.getState().setCinematicPlaying(false);
|
||||
},
|
||||
});
|
||||
|
||||
cinematic.cameraKeyframes.slice(1).forEach((keyframe, index) => {
|
||||
const previousKeyframe = cinematic.cameraKeyframes[index];
|
||||
if (!previousKeyframe) return;
|
||||
|
||||
const duration = keyframe.time - previousKeyframe.time;
|
||||
timeline.to(
|
||||
camera.position,
|
||||
{
|
||||
x: keyframe.position[0],
|
||||
y: keyframe.position[1],
|
||||
z: keyframe.position[2],
|
||||
duration,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
previousKeyframe.time,
|
||||
);
|
||||
timeline.to(
|
||||
target,
|
||||
{
|
||||
x: keyframe.target[0],
|
||||
y: keyframe.target[1],
|
||||
z: keyframe.target[2],
|
||||
duration,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
previousKeyframe.time,
|
||||
);
|
||||
});
|
||||
|
||||
timelineRef.current = timeline;
|
||||
}
|
||||
Reference in New Issue
Block a user