From 1dfe6441788649ebf5664f77122f961a7962fece Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 4 Feb 2026 16:02:36 +1300 Subject: [PATCH] feat(quick-004): add easeCurve handles, labels, inheritance overlay to EaseCurveNode - Replace 2 video handles with 4 labeled handles (Video In/Out at 35%, Ease In/Out at 75%) - Add lime-300 color for easeCurve handles matching node accent - Compute inheritance state from edges (useMemo) - Add inheritance overlay on Editor tab: semi-transparent dark bg with informational text - Add "Control manually" button that removes easeCurve edge and clears inheritedFrom - Disable CubicBezierEditor, Presets button, and Apply button when inherited - Video tab unaffected by inheritance state Co-Authored-By: Claude Opus 4.5 --- src/components/nodes/EaseCurveNode.tsx | 355 +++++++++++++++---------- 1 file changed, 221 insertions(+), 134 deletions(-) diff --git a/src/components/nodes/EaseCurveNode.tsx b/src/components/nodes/EaseCurveNode.tsx index ae8e6a0c..19d11a0b 100644 --- a/src/components/nodes/EaseCurveNode.tsx +++ b/src/components/nodes/EaseCurveNode.tsx @@ -42,6 +42,8 @@ export function EaseCurveNode({ id, data, selected }: NodeProps 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 [activeTab, setActiveTab] = useState<"editor" | "video">("editor"); const [showPresets, setShowPresets] = useState(false); @@ -137,6 +139,25 @@ export function EaseCurveNode({ id, data, selected }: NodeProps { + return edges.find((e) => e.target === id && e.targetHandle === "easeCurve") || null; + }, [edges, id]); + const isInherited = inheritedEdge !== null; + + const handleBreakInheritance = useCallback(() => { + if (inheritedEdge) { + removeEdge(inheritedEdge.id); + updateNodeData(id, { inheritedFrom: null }); + } + }, [inheritedEdge, removeEdge, id, updateNodeData]); + + // Compute easing curve polyline for the editor overlay (higher sample count than thumbnails) + const editorEasingCurve = useMemo(() => { + 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) => ({ @@ -146,25 +167,72 @@ export function EaseCurveNode({ id, data, selected }: NodeProps ( <> + {/* Video In (target, left, 35%) */} +
+ Video In +
+ + {/* Video Out (source, right, 35%) */} +
+ Video Out +
+ + {/* Ease In (target, left, 75%) */} + +
+ Ease In +
+ + {/* Ease Out (source, right, 75%) */} + +
+ Ease Out +
); @@ -278,153 +346,172 @@ export function EaseCurveNode({ id, data, selected }: NodeProps - {/* Bezier curve editor - fills available width */} -
- -
- - {/* Preset label */} - {nodeData.easingPreset && ( -
- - {nodeData.easingPreset} - +
+ {/* Editor controls - dimmed when inherited */} +
+ {/* Bezier curve editor - fills available width */} +
+
- )} - - {/* Controls row: Duration + Presets button */} -
- - - sec - - {/* Presets button */} -
- - {/* Presets popover */} - {showPresets && ( -
- {/* Preset Bezier thumbnails (top section) */} -
-
- Bezier Presets + {/* Preset label */} + {nodeData.easingPreset && ( +
+ + {nodeData.easingPreset} + +
+ )} + + {/* Controls row: Duration + Presets button */} +
+ + + sec + + {/* Presets button */} +
+ + + {/* Presets popover */} + {showPresets && ( +
+ {/* 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 ( + + ); + })} +
-
- {EASING_PRESETS.map((name) => { - const thumb = presetThumbnails.find((t) => t.name === name); - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} + + + {name.replace(/^ease/, "")} + + + ); + })} +
+ )} +
+
- {/* All easing functions (scrollable grid) */} -
-
- All Easing Functions -
-
- {presetThumbnails.map(({ name, polyline }) => { - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} -
-
-
- )} + {/* Apply button */} +
+
- {/* Apply button */} -
- -
+ {/* Inheritance overlay */} + {isInherited && ( +
+

Settings inherited from parent node

+

Break connection to edit manually

+ +
+ )}
)}