diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx index da2f28bb..90a88477 100644 --- a/src/components/nodes/ControlPanel.tsx +++ b/src/components/nodes/ControlPanel.tsx @@ -874,10 +874,10 @@ function EaseCurveControls({ node }: { node: Node }) { {showPresets && typeof document !== 'undefined' && createPortal(
diff --git a/src/components/nodes/EaseCurveNode.tsx b/src/components/nodes/EaseCurveNode.tsx index e6f9e435..a39897de 100644 --- a/src/components/nodes/EaseCurveNode.tsx +++ b/src/components/nodes/EaseCurveNode.tsx @@ -1,80 +1,25 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react"; +import React, { useCallback, useEffect, useMemo } from "react"; +import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore } from "@/store/workflowStore"; import { EaseCurveNodeData } from "@/types"; import { checkEncoderSupport } from "@/hooks/useStitchVideos"; import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl"; -import { CubicBezierEditor } from "@/components/CubicBezierEditor"; -import { - EASING_PRESETS, - getPresetBezier, - getEasingBezier, -} from "@/lib/easing-presets"; -import { getAllEasingNames, getEasingFunction } from "@/lib/easing-functions"; type EaseCurveNodeType = Node; -// Generate SVG polyline points by sampling an easing function -function generateEasingPolyline( - easingName: string, - width: number, - height: number, - samples: number = 20 -): string { - const fn = getEasingFunction(easingName); - return Array.from({ length: samples + 1 }, (_, i) => { - const t = i / samples; - const y = fn(t); - return `${(t * width).toFixed(1)},${((1 - y) * height).toFixed(1)}`; - }).join(" "); -} - -// Categorize easing names into presets (with Bezier handles) and others (polyline only) -const ALL_EASING_NAMES = getAllEasingNames(); -const PRESET_NAMES = new Set(EASING_PRESETS); +const VIDEO_HEIGHT = 320; export function EaseCurveNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); - const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const isRunning = useWorkflowStore((state) => state.isRunning); const edges = useWorkflowStore((state) => state.edges); const removeEdge = useWorkflowStore((state) => state.removeEdge); - const { setNodes } = useReactFlow(); const videoBlobUrl = useVideoBlobUrl(nodeData.outputVideo ?? null); - const [activeTab, setActiveTab] = useState<"editor" | "video">("editor"); - const [showPresets, setShowPresets] = useState(false); - const presetsButtonRef = useRef(null); - const presetsPopoverRef = useRef(null); - const [presetsPosition, setPresetsPosition] = useState<{ top: number; left: number } | null>(null); - - // Auto-resize node height when switching tabs - const EDITOR_HEIGHT = 480; - const VIDEO_HEIGHT = 320; - const switchTab = useCallback((tab: "editor" | "video") => { - setActiveTab(tab); - const height = tab === "editor" ? EDITOR_HEIGHT : VIDEO_HEIGHT; - setNodes((nodes) => - nodes.map((n) => - n.id === id ? { ...n, style: { ...n.style, height } } : n - ) - ); - }, [id, setNodes]); - - // Auto-switch to Video tab when processing completes - const prevOutputRef = useRef(nodeData.outputVideo); - useEffect(() => { - if (!prevOutputRef.current && nodeData.outputVideo) { - switchTab("video"); - } - prevOutputRef.current = nodeData.outputVideo; - }, [nodeData.outputVideo, switchTab]); - // Check encoder support on mount useEffect(() => { if (nodeData.encoderSupported === null) { @@ -84,123 +29,10 @@ export function EaseCurveNode({ id, data, selected }: NodeProps { - if (!showPresets || !presetsButtonRef.current) { - setPresetsPosition(null); - return; - } - - const updatePosition = () => { - if (presetsButtonRef.current) { - const rect = presetsButtonRef.current.getBoundingClientRect(); - setPresetsPosition({ - top: rect.top, - left: rect.right, - }); - } - }; - - updatePosition(); - - let animationId: number; - const trackPosition = () => { - updatePosition(); - animationId = requestAnimationFrame(trackPosition); - }; - animationId = requestAnimationFrame(trackPosition); - - return () => cancelAnimationFrame(animationId); - }, [showPresets]); - - // Close presets popover on click outside or Escape - useEffect(() => { - if (!showPresets) return; - - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as HTMLElement; - if ( - presetsButtonRef.current && !presetsButtonRef.current.contains(target) && - presetsPopoverRef.current && !presetsPopoverRef.current.contains(target) - ) { - setShowPresets(false); - } - }; - - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") { - setShowPresets(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - document.addEventListener("keydown", handleEscape); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("keydown", handleEscape); - }; - }, [showPresets]); - - const handleBezierChange = useCallback( - (value: [number, number, number, number]) => { - updateNodeData(id, { bezierHandles: value, easingPreset: null }); - }, - [id, updateNodeData] - ); - - const handleBezierCommit = useCallback( - (value: [number, number, number, number]) => { - updateNodeData(id, { bezierHandles: value, easingPreset: null }); - }, - [id, updateNodeData] - ); - - const handleSelectPreset = useCallback( - (name: string) => { - updateNodeData(id, { - easingPreset: name, - bezierHandles: getPresetBezier(name), - }); - setShowPresets(false); - }, - [id, updateNodeData] - ); - - const handleSelectEasing = useCallback( - (name: string) => { - updateNodeData(id, { - easingPreset: name, - bezierHandles: getEasingBezier(name), - }); - setShowPresets(false); - }, - [id, updateNodeData] - ); - - const handleDurationChange = useCallback( - (e: React.ChangeEvent) => { - const val = parseFloat(e.target.value); - updateNodeData(id, { outputDuration: isNaN(val) ? 1.5 : Math.max(0.1, Math.min(30, val)) }); - }, - [id, updateNodeData] - ); - - const handleRun = useCallback(() => { - regenerateNode(id); - }, [id, regenerateNode]); - // Check if this node has an incoming easeCurve connection (inheritance) const inheritedEdge = useMemo(() => { return edges.find((e) => e.target === id && e.targetHandle === "easeCurve") || null; }, [edges, id]); - const isInherited = inheritedEdge !== null; - - // Default to video tab when inherited - useEffect(() => { - if (isInherited) { - switchTab("video"); - } - }, [isInherited, switchTab]); const handleBreakInheritance = useCallback(() => { if (inheritedEdge) { @@ -209,21 +41,6 @@ export function EaseCurveNode({ id, data, selected }: NodeProps { - if (!nodeData.easingPreset) return undefined; - return generateEasingPolyline(nodeData.easingPreset, 100, 100, 50); - }, [nodeData.easingPreset]); - - // Memoize the preset thumbnail SVGs - const presetThumbnails = useMemo(() => { - return ALL_EASING_NAMES.map((name) => ({ - name, - polyline: generateEasingPolyline(name, 36, 36), - isPreset: PRESET_NAMES.has(name as any), - })); - }, []); - // Shared handles rendered in ALL states (4 handles with labels) const renderHandles = () => ( <> @@ -299,8 +116,9 @@ export function EaseCurveNode({ id, data, selected }: NodeProps {renderHandles()}
@@ -329,8 +147,9 @@ export function EaseCurveNode({ id, data, selected }: NodeProps {renderHandles()}
@@ -350,6 +169,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps {renderHandles()} -
- {/* Tab bar */} -
+ {/* Video preview (full-bleed) */} + {nodeData.outputVideo ? ( +
+
+ ) : ( +
+ Run workflow to apply ease curve +
+ )} - {/* Tab content */} - {activeTab === "editor" && ( -
- {/* Editor controls - dimmed when inherited */} -
- {/* Bezier curve editor - fixed size, centered */} -
-
- -
-
- - {/* Preset label */} - {nodeData.easingPreset && ( -
- - {nodeData.easingPreset} - -
- )} - - {/* Controls row: Duration + Presets button */} -
- - - sec - - {/* Presets button */} -
- -
- -
- - {/* Apply button */} -
- -
-
- - {/* Inheritance overlay */} - {isInherited && ( -
-

Settings inherited from parent node

-

Break connection to edit manually

- -
- )} -
- )} - - {activeTab === "video" && ( -
- {nodeData.outputVideo ? ( -
-
- ) : ( -
- Run workflow to apply ease curve -
- )} -
- )} - - {/* Processing overlay */} - {nodeData.status === "loading" && ( -
- - - - - Processing... {Math.round(nodeData.progress)}% -
- )} - - {/* Error display */} - {nodeData.status === "error" && nodeData.error && ( -
-

{nodeData.error}

-
- )} -
- - {/* Presets popover - rendered via portal to avoid clipping */} - {showPresets && presetsPosition && createPortal( -
- {/* Preset Bezier thumbnails (top section) */} -
-
- Bezier Presets -
-
- {EASING_PRESETS.map((name) => { - const thumb = presetThumbnails.find((t) => t.name === name); - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} -
-
+ {/* Processing overlay */} + {nodeData.status === "loading" && ( +
+ + + + + Processing... {Math.round(nodeData.progress)}% +
+ )} - {/* All easing functions (scrollable grid) */} -
-
- All Easing Functions -
-
- {presetThumbnails.map(({ name, polyline }) => { - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} -
-
-
, - document.body + {/* Error display */} + {nodeData.status === "error" && nodeData.error && ( +
+

{nodeData.error}

+
)} );