@@ -36,6 +36,60 @@ interface TimelineToolbarProps {
3636 onSplitElement ?: ( element : TimelineElement , splitTime : number ) => void ;
3737}
3838
39+ interface KeyframeToggleState {
40+ state : "active" | "inactive" | "none" ;
41+ isMotionPath : boolean ;
42+ pathEndpoint : boolean ;
43+ willExtend : boolean ;
44+ }
45+
46+ const NO_KEYFRAME_TOGGLE : KeyframeToggleState = {
47+ state : "none" ,
48+ isMotionPath : false ,
49+ pathEndpoint : false ,
50+ willExtend : false ,
51+ } ;
52+
53+ function isMotionPathEndpoint ( animation : GsapAnimation | undefined , percentage : number ) : boolean {
54+ if ( ! animation ?. keyframes ) return false ;
55+ const keyframes = animation . keyframes . keyframes ;
56+ return (
57+ Math . abs ( ( keyframes [ 0 ] ?. percentage ?? - Infinity ) - percentage ) <= 1 ||
58+ Math . abs ( ( keyframes . at ( - 1 ) ?. percentage ?? Infinity ) - percentage ) <= 1
59+ ) ;
60+ }
61+
62+ function resolveKeyframeToggleState (
63+ session : DomEditSessionSlice | undefined ,
64+ currentTime : number ,
65+ ) : KeyframeToggleState {
66+ if ( ! session ?. domEditSelection ) return NO_KEYFRAME_TOGGLE ;
67+ const arcAnimation = session . selectedGsapAnimations . find (
68+ ( animation ) => animation . arcPath && animation . keyframes ,
69+ ) ;
70+ const animation =
71+ arcAnimation ??
72+ session . selectedGsapAnimations . find ( ( candidate ) => candidate . keyframes && ! candidate . arcPath ) ;
73+ if ( ! animation ?. keyframes ) return NO_KEYFRAME_TOGGLE ;
74+
75+ const isMotionPath = Boolean ( arcAnimation ) ;
76+ if ( ! isPlayheadWithinTween ( animation , currentTime ) ) {
77+ return { state : "inactive" , isMotionPath, pathEndpoint : false , willExtend : true } ;
78+ }
79+
80+ const percentage = computeElementPercentage ( currentTime , session . domEditSelection , animation ) ;
81+ const pathEndpoint = isMotionPathEndpoint ( arcAnimation , percentage ) ;
82+ const active = animation . keyframes . keyframes . some (
83+ ( keyframe ) => Math . abs ( keyframe . percentage - percentage ) <= 1 ,
84+ ) ;
85+ return {
86+ state : pathEndpoint ? "none" : active ? "active" : "inactive" ,
87+ isMotionPath,
88+ pathEndpoint,
89+ willExtend : false ,
90+ } ;
91+ }
92+
3993function useKeyframeToggle ( session ?: DomEditSessionSlice ) {
4094 const currentTime = usePlayerStore ( ( s ) => s . currentTime ) ;
4195 const sessionRef = useRef ( session ) ;
@@ -45,31 +99,12 @@ function useKeyframeToggle(session?: DomEditSessionSlice) {
4599 sessionRef as React . RefObject < EnableKeyframesSession | undefined > ,
46100 ) ;
47101
48- if ( ! session ) return { state : "none" as const , onToggle : undefined } ;
49-
50- const sel = session . domEditSelection ;
51- const anims = session . selectedGsapAnimations ;
52- const kfAnim = anims . find ( ( a ) => a . keyframes ) ;
53-
54- let state : "active" | "inactive" | "none" = "none" ;
55- // Outside the tween, clicking extends the animation to the playhead rather than
56- // toggling a (clamped) edge keyframe — so the button stays an "add" affordance.
57- let willExtend = false ;
58- if ( kfAnim ?. keyframes && sel ) {
59- if ( ! isPlayheadWithinTween ( kfAnim , currentTime ) ) {
60- state = "inactive" ;
61- willExtend = true ;
62- } else {
63- // Tween-relative percentage (not the clip range) so the button state matches
64- // where the keyframe would actually land.
65- const pct = computeElementPercentage ( currentTime , sel , kfAnim ) ;
66- state = kfAnim . keyframes . keyframes . some ( ( k ) => Math . abs ( k . percentage - pct ) <= 1 )
67- ? "active"
68- : "inactive" ;
69- }
70- }
102+ const toggleState = resolveKeyframeToggleState ( session , currentTime ) ;
71103
72- return { state, willExtend, onToggle : sel ? onToggle : undefined } ;
104+ return {
105+ ...toggleState ,
106+ onToggle : session ?. domEditSelection && ! toggleState . pathEndpoint ? onToggle : undefined ,
107+ } ;
73108}
74109
75110// fallow-ignore-next-line complexity
@@ -91,6 +126,8 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
91126 const displayedTimelineZoomPercent = getTimelineZoomPercent ( zoomMode , manualZoomPercent ) ;
92127 const {
93128 state : keyframeState ,
129+ isMotionPath : keyframeIsMotionPath ,
130+ pathEndpoint : keyframePathEndpoint ,
94131 willExtend : keyframeWillExtend ,
95132 onToggle : onToggleKeyframe ,
96133 } = useKeyframeToggle ( domEditSession ) ;
@@ -180,25 +217,41 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
180217 // toolbar layout never shifts.
181218 < Tooltip
182219 label = {
183- ! onToggleKeyframe
184- ? "Select an animated element to add keyframes "
185- : keyframeState === "active"
186- ? "Remove keyframe at playhead (K) "
187- : keyframeState === "inactive"
220+ keyframePathEndpoint
221+ ? "Motion path endpoints cannot be removed "
222+ : ! onToggleKeyframe
223+ ? "Select an animated element to add keyframes "
224+ : keyframeIsMotionPath
188225 ? keyframeWillExtend
189- ? "Add keyframe at playhead, extends animation (K)"
190- : "Add keyframe at playhead (K)"
191- : "Add keyframe (K)"
226+ ? "Extend motion path to playhead (K)"
227+ : keyframeState === "active"
228+ ? "Remove waypoint from motion path (K)"
229+ : "Add waypoint to motion path (K)"
230+ : keyframeState === "active"
231+ ? "Remove keyframe at playhead (K)"
232+ : keyframeState === "inactive"
233+ ? keyframeWillExtend
234+ ? "Add keyframe at playhead, extends animation (K)"
235+ : "Add keyframe at playhead (K)"
236+ : "Add keyframe (K)"
192237 }
193238 >
194239 < button
195240 type = "button"
196241 disabled = { ! onToggleKeyframe }
197242 onClick = { onToggleKeyframe }
198243 aria-label = {
199- keyframeState === "active"
200- ? "Remove keyframe at playhead"
201- : "Add keyframe at playhead"
244+ keyframePathEndpoint
245+ ? "Motion path endpoint"
246+ : keyframeIsMotionPath
247+ ? keyframeState === "active"
248+ ? "Remove motion path waypoint"
249+ : keyframeWillExtend
250+ ? "Extend motion path to playhead"
251+ : "Add motion path waypoint"
252+ : keyframeState === "active"
253+ ? "Remove keyframe at playhead"
254+ : "Add keyframe at playhead"
202255 }
203256 className = {
204257 ! onToggleKeyframe
0 commit comments