@ -42,6 +42,8 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
const updateNodeData = useWorkflowStore ( ( state ) = > state . updateNodeData ) ;
const updateNodeData = useWorkflowStore ( ( state ) = > state . updateNodeData ) ;
const regenerateNode = useWorkflowStore ( ( state ) = > state . regenerateNode ) ;
const regenerateNode = useWorkflowStore ( ( state ) = > state . regenerateNode ) ;
const isRunning = useWorkflowStore ( ( state ) = > state . isRunning ) ;
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 [ activeTab , setActiveTab ] = useState < "editor" | "video" > ( "editor" ) ;
const [ showPresets , setShowPresets ] = useState ( false ) ;
const [ showPresets , setShowPresets ] = useState ( false ) ;
@ -137,6 +139,25 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
regenerateNode ( id ) ;
regenerateNode ( id ) ;
} , [ id , regenerateNode ] ) ;
} , [ 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 ;
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
// Memoize the preset thumbnail SVGs
const presetThumbnails = useMemo ( ( ) = > {
const presetThumbnails = useMemo ( ( ) = > {
return ALL_EASING_NAMES . map ( ( name ) = > ( {
return ALL_EASING_NAMES . map ( ( name ) = > ( {
@ -146,25 +167,72 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
} ) ) ;
} ) ) ;
} , [ ] ) ;
} , [ ] ) ;
// Shared handles rendered in ALL states
// Shared handles rendered in ALL states (4 handles with labels)
const renderHandles = ( ) = > (
const renderHandles = ( ) = > (
< >
< >
{ /* Video In (target, left, 35%) */ }
< Handle
< Handle
type = "target"
type = "target"
position = { Position . Left }
position = { Position . Left }
id = "video"
id = "video"
data - handletype = "video"
data - handletype = "video"
isConnectable = { true }
isConnectable = { true }
style = { { top : "50 %" } }
style = { { top : "3 5%" } }
/ >
/ >
< div
className = "absolute text-[10px] font-medium whitespace-nowrap pointer-events-none text-right"
style = { { right : "calc(100% + 8px)" , top : "calc(35% - 7px)" , color : "rgb(168, 85, 247)" } }
>
Video In
< / div >
{ /* Video Out (source, right, 35%) */ }
< Handle
< Handle
type = "source"
type = "source"
position = { Position . Right }
position = { Position . Right }
id = "video"
id = "video"
data - handletype = "video"
data - handletype = "video"
isConnectable = { true }
isConnectable = { true }
style = { { top : "50%" } }
style = { { top : "3 5%" } }
/ >
/ >
< div
className = "absolute text-[10px] font-medium whitespace-nowrap pointer-events-none"
style = { { left : "calc(100% + 8px)" , top : "calc(35% - 7px)" , color : "rgb(168, 85, 247)" } }
>
Video Out
< / div >
{ /* Ease In (target, left, 75%) */ }
< Handle
type = "target"
position = { Position . Left }
id = "easeCurve"
data - handletype = "easeCurve"
isConnectable = { true }
style = { { top : "75%" , background : "rgb(190, 242, 100)" } }
/ >
< div
className = "absolute text-[10px] font-medium whitespace-nowrap pointer-events-none text-right"
style = { { right : "calc(100% + 8px)" , top : "calc(75% - 7px)" , color : "rgb(190, 242, 100)" } }
>
Ease In
< / div >
{ /* Ease Out (source, right, 75%) */ }
< Handle
type = "source"
position = { Position . Right }
id = "easeCurve"
data - handletype = "easeCurve"
isConnectable = { true }
style = { { top : "75%" , background : "rgb(190, 242, 100)" } }
/ >
< div
className = "absolute text-[10px] font-medium whitespace-nowrap pointer-events-none"
style = { { left : "calc(100% + 8px)" , top : "calc(75% - 7px)" , color : "rgb(190, 242, 100)" } }
>
Ease Out
< / div >
< / >
< / >
) ;
) ;
@ -278,14 +346,17 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
{ /* Tab content */ }
{ /* Tab content */ }
{ activeTab === "editor" && (
{ activeTab === "editor" && (
< div className = "flex-1 flex flex-col min-h-0 gap-2" >
< div className = "flex-1 flex flex-col min-h-0 gap-2 relative" >
{ /* Editor controls - dimmed when inherited */ }
< div className = { isInherited ? "pointer-events-none opacity-40" : "" } >
{ /* Bezier curve editor - fills available width */ }
{ /* Bezier curve editor - fills available width */ }
< div className = "flex-1 min-h-0 px-2" >
< div className = "flex-1 min-h-0 px-2" >
< CubicBezierEditor
< CubicBezierEditor
value = { nodeData . bezierHandles }
value = { nodeData . bezierHandles }
onChange = { handleBezierChange }
onChange = { handleBezierChange }
onCommit = { handleBezierCommit }
onCommit = { handleBezierCommit }
disabled = { nodeData . status === "loading" }
disabled = { nodeData . status === "loading" || isInherited }
easingCurve = { editorEasingCurve }
/ >
/ >
< / div >
< / div >
@ -315,8 +386,9 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
{ /* Presets button */ }
{ /* Presets button */ }
< div className = "relative ml-auto" ref = { presetsRef } >
< div className = "relative ml-auto" ref = { presetsRef } >
< button
< button
className = "nodrag nopan px-2 py-1 bg-neutral-800 hover:bg-neutral-700 border border-neutral-600 rounded text-xs text-neutral-300 transition-colors flex items-center gap-1"
className = "nodrag nopan px-2 py-1 bg-neutral-800 hover:bg-neutral-700 border border-neutral-600 rounded text-xs text-neutral-300 transition-colors flex items-center gap-1 disabled:opacity-40 disabled:pointer-events-none "
onClick = { ( ) = > setShowPresets ( ! showPresets ) }
onClick = { ( ) = > setShowPresets ( ! showPresets ) }
disabled = { isInherited }
>
>
{ /* Curve icon */ }
{ /* Curve icon */ }
< svg className = "w-3 h-3" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" >
< svg className = "w-3 h-3" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" >
@ -420,12 +492,27 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
< button
< button
className = "nodrag nopan px-3 py-1.5 bg-lime-300/15 hover:bg-lime-300/25 border border-lime-300/30 rounded text-xs text-lime-300 font-medium transition-colors disabled:opacity-40 disabled:pointer-events-none"
className = "nodrag nopan px-3 py-1.5 bg-lime-300/15 hover:bg-lime-300/25 border border-lime-300/30 rounded text-xs text-lime-300 font-medium transition-colors disabled:opacity-40 disabled:pointer-events-none"
onClick = { handleRun }
onClick = { handleRun }
disabled = { isRunning || nodeData . status === "loading" }
disabled = { isRunning || nodeData . status === "loading" || isInherited }
>
>
Apply
Apply
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
{ /* Inheritance overlay */ }
{ isInherited && (
< div className = "absolute inset-0 flex flex-col items-center justify-center bg-neutral-900/80 backdrop-blur-sm rounded z-10" >
< p className = "text-sm text-neutral-200 font-medium" > Settings inherited from parent node < / p >
< p className = "text-[11px] text-neutral-400 mt-1" > Break connection to edit manually < / p >
< button
className = "mt-3 px-3 py-1.5 bg-white/10 hover:bg-white/20 border border-white/20 rounded text-xs text-neutral-200 transition-colors"
onClick = { handleBreakInheritance }
>
Control manually
< / button >
< / div >
) }
< / div >
) }
) }
{ activeTab === "video" && (
{ activeTab === "video" && (