add: cinematic preview

This commit is contained in:
Tom Boullay
2026-05-11 12:53:18 +02:00
parent 9f5c105c1b
commit 7a439d0879
5 changed files with 147 additions and 5 deletions
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Plus, RefreshCw, Save, Trash2 } from "lucide-react";
import { Play, Plus, RefreshCw, Save, Trash2 } from "lucide-react";
import type {
CinematicCameraKeyframe,
CinematicDefinition,
@@ -124,7 +124,13 @@ function updateVector(
return nextVector;
}
export function EditorCinematicManifestPanel(): React.JSX.Element {
interface EditorCinematicManifestPanelProps {
onPreviewCinematic?: ((cinematic: CinematicDefinition) => void) | undefined;
}
export function EditorCinematicManifestPanel({
onPreviewCinematic,
}: EditorCinematicManifestPanelProps): React.JSX.Element {
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
const [selectedCinematicId, setSelectedCinematicId] = useState("");
const [status, setStatus] = useState("Chargement des cinematics...");
@@ -431,6 +437,16 @@ export function EditorCinematicManifestPanel(): React.JSX.Element {
)}
</div>
<button
className="editor-cinematic-manifest-preview"
type="button"
disabled={errors.length > 0 || !onPreviewCinematic}
onClick={() => onPreviewCinematic?.(selectedCinematic)}
>
<Play size={14} aria-hidden="true" />
Preview cinematic
</button>
<button
className="editor-cinematic-manifest-delete"
type="button"
+4 -1
View File
@@ -15,6 +15,7 @@ import {
import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel";
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
import { EditorSrtPanel } from "@/components/editor/EditorSrtPanel";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode } from "@/types/editor/editor";
interface EditorControlsProps {
@@ -31,6 +32,7 @@ interface EditorControlsProps {
onExportJson: () => void;
onSaveToServer?: (() => void | Promise<void>) | undefined;
onPlayerMode?: (() => void) | undefined;
onPreviewCinematic?: ((cinematic: CinematicDefinition) => void) | undefined;
isPlayerMode?: boolean;
}
@@ -62,6 +64,7 @@ export function EditorControls({
onExportJson,
onSaveToServer,
onPlayerMode,
onPreviewCinematic,
isPlayerMode,
}: EditorControlsProps): React.JSX.Element {
const viewModeLabel = isPlayerMode ? "View locked" : "Lock view";
@@ -240,7 +243,7 @@ export function EditorControls({
</div>
</section>
<EditorCinematicManifestPanel />
<EditorCinematicManifestPanel onPreviewCinematic={onPreviewCinematic} />
<EditorDialogueManifestPanel />
<EditorSrtPanel />
</aside>
+96 -2
View File
@@ -1,9 +1,18 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { OrbitControls } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import gsap from "gsap";
import * as THREE from "three";
import { EditorMap } from "@/components/editor/scene/EditorMap";
import { FlyController } from "@/controls/editor/FlyController";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
export interface EditorCinematicPreviewRequest {
id: string;
cinematic: CinematicDefinition;
}
interface EditorSceneProps {
sceneData: SceneData;
selectedNodeIndex: number | null;
@@ -18,6 +27,8 @@ interface EditorSceneProps {
onUndo: () => void;
onRedo: () => void;
isPlayerMode?: boolean;
cinematicPreviewRequest?: EditorCinematicPreviewRequest | null;
onCinematicPreviewComplete?: (() => void) | undefined;
}
export function EditorScene({
@@ -34,7 +45,11 @@ export function EditorScene({
onUndo,
onRedo,
isPlayerMode = false,
cinematicPreviewRequest = null,
onCinematicPreviewComplete,
}: EditorSceneProps): React.JSX.Element {
const isCinematicPreviewing = cinematicPreviewRequest !== null;
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) {
@@ -74,10 +89,16 @@ export function EditorScene({
return (
<>
<EditorCinematicPreviewPlayer
request={cinematicPreviewRequest}
onComplete={onCinematicPreviewComplete}
/>
{isPlayerMode ? (
<FlyController disabled={false} />
<FlyController disabled={isCinematicPreviewing} />
) : (
<OrbitControls
enabled={!isCinematicPreviewing}
enableDamping
dampingFactor={0.05}
mouseButtons={{
@@ -106,3 +127,76 @@ export function EditorScene({
</>
);
}
interface EditorCinematicPreviewPlayerProps {
request: EditorCinematicPreviewRequest | null;
onComplete?: (() => void) | undefined;
}
function EditorCinematicPreviewPlayer({
request,
onComplete,
}: EditorCinematicPreviewPlayerProps): null {
const camera = useThree((state) => state.camera);
const timelineRef = useRef<gsap.core.Timeline | null>(null);
useEffect(() => {
timelineRef.current?.kill();
timelineRef.current = null;
if (!request) return undefined;
const firstKeyframe = request.cinematic.cameraKeyframes[0];
if (!firstKeyframe) return undefined;
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;
onComplete?.();
},
});
request.cinematic.cameraKeyframes.slice(1).forEach((keyframe, index) => {
const previousKeyframe = request.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;
return () => {
timeline.kill();
if (timelineRef.current === timeline) timelineRef.current = null;
};
}, [camera, onComplete, request]);
return null;
}