summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/components/graph-editor/use-controller.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/timeline/components/graph-editor/use-controller.ts')
-rw-r--r--apps/web/src/timeline/components/graph-editor/use-controller.ts178
1 files changed, 178 insertions, 0 deletions
diff --git a/apps/web/src/timeline/components/graph-editor/use-controller.ts b/apps/web/src/timeline/components/graph-editor/use-controller.ts
new file mode 100644
index 00000000..ea091485
--- /dev/null
+++ b/apps/web/src/timeline/components/graph-editor/use-controller.ts
@@ -0,0 +1,178 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useEditor } from "@/editor/use-editor";
+import { registerCanceller } from "@/editor/cancel-interaction";
+import type { NormalizedCubicBezier } from "@/animation/types";
+import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
+import {
+ applyGraphEditorCurvePreview,
+ buildGraphEditorCurvePatches,
+ resolveGraphEditorSelectionState,
+ type GraphEditorSelectionState,
+} from "./session";
+
+export function useGraphEditorController() {
+ const editor = useEditor();
+ const renderTracks = useEditor(
+ (currentEditor) =>
+ currentEditor.timeline.getPreviewTracks() ??
+ currentEditor.scenes.getActiveScene().tracks,
+ );
+ const { selectedKeyframes } = useKeyframeSelection();
+ const [open, setOpen] = useState(false);
+ const [activeComponentKey, setActiveComponentKey] = useState<string | null>(
+ null,
+ );
+ const hasPreviewRef = useRef(false);
+
+ const state = useMemo<GraphEditorSelectionState>(
+ () =>
+ resolveGraphEditorSelectionState({
+ tracks: renderTracks,
+ selectedKeyframes,
+ preferredComponentKey: activeComponentKey,
+ }),
+ [activeComponentKey, renderTracks, selectedKeyframes],
+ );
+
+ const stateKey =
+ state.status === "ready"
+ ? `${state.trackId}:${state.elementId}:${state.activeComponentKey}:${state.segments
+ .map(
+ (segment) =>
+ `${segment.propertyPath}:${segment.keyframeId}:${segment.context.componentKey}`,
+ )
+ .join("|")}`
+ : `${state.status}:${state.reason}:${state.activeComponentKey ?? "none"}`;
+ const previousStateKeyRef = useRef(stateKey);
+
+ const discardPreview = useCallback(() => {
+ if (!hasPreviewRef.current) {
+ return;
+ }
+
+ editor.timeline.discardPreview();
+ hasPreviewRef.current = false;
+ }, [editor]);
+
+ useEffect(() => {
+ if (hasPreviewRef.current && previousStateKeyRef.current !== stateKey) {
+ discardPreview();
+ }
+
+ previousStateKeyRef.current = stateKey;
+ }, [discardPreview, stateKey]);
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+
+ return registerCanceller({
+ fn: () => {
+ discardPreview();
+ setOpen(false);
+ },
+ });
+ }, [discardPreview, open]);
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ if (!nextOpen) {
+ discardPreview();
+ }
+
+ setOpen(nextOpen);
+ },
+ [discardPreview],
+ );
+
+ const handleActiveComponentKeyChange = useCallback(
+ (nextComponentKey: string) => {
+ discardPreview();
+ setActiveComponentKey(nextComponentKey);
+ },
+ [discardPreview],
+ );
+
+ const handlePreviewValue = useCallback(
+ (nextValue: NormalizedCubicBezier) => {
+ if (state.status !== "ready") {
+ return;
+ }
+
+ const nextAnimations = state.segments.reduce(
+ (animations, segment) =>
+ segment.allContexts.reduce(
+ (nextAnimationsForSegment, context) =>
+ applyGraphEditorCurvePreview({
+ animations: nextAnimationsForSegment,
+ context,
+ cubicBezier: nextValue,
+ referenceSpanValue: segment.referenceSpanValue,
+ }),
+ animations,
+ ),
+ state.element.animations,
+ );
+ editor.timeline.previewElements({
+ updates: [
+ {
+ trackId: state.trackId,
+ elementId: state.elementId,
+ updates: { animations: nextAnimations },
+ },
+ ],
+ });
+ hasPreviewRef.current = true;
+ },
+ [editor, state],
+ );
+
+ const handleCommitValue = useCallback(
+ (nextValue: NormalizedCubicBezier) => {
+ if (state.status !== "ready") {
+ return;
+ }
+
+ editor.timeline.updateKeyframeCurves({
+ keyframes: state.segments.flatMap((segment) => {
+ const patches = buildGraphEditorCurvePatches({
+ context: segment.context,
+ cubicBezier: nextValue,
+ referenceSpanValue: segment.referenceSpanValue,
+ });
+ if (!patches) {
+ return [];
+ }
+
+ return segment.allContexts.flatMap((context) =>
+ patches.map(({ keyframeId, patch }) => ({
+ trackId: state.trackId,
+ elementId: state.elementId,
+ propertyPath: segment.propertyPath,
+ componentKey: context.componentKey,
+ keyframeId,
+ patch,
+ })),
+ );
+ }),
+ });
+ hasPreviewRef.current = false;
+ },
+ [editor, state],
+ );
+
+ return {
+ open,
+ onOpenChange: handleOpenChange,
+ canOpen: state.status === "ready",
+ tooltip: state.status === "ready" ? "Open graph editor" : state.message,
+ state,
+ onActiveComponentKeyChange: handleActiveComponentKeyChange,
+ onPreviewValue: handlePreviewValue,
+ onCommitValue: handleCommitValue,
+ onCancelPreview: discardPreview,
+ };
+}