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 && (
-
- )}
-
-
- {/* 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 && (
+
)}
);