Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
1dfe644178
  1. 101
      src/components/nodes/EaseCurveNode.tsx

101
src/components/nodes/EaseCurveNode.tsx

@ -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: "35%" }}
/> />
<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: "35%" }}
/> />
<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" && (

Loading…
Cancel
Save