11import { useState } from "react" ;
22import { Eye , EyeSlash } from "@phosphor-icons/react" ;
3- import {
4- classifyPropertyGroup ,
5- type GsapAnimation ,
6- type PropertyGroupName ,
7- } from "@hyperframes/core/gsap-parser" ;
8- import {
9- clipToTweenPercentage ,
10- getKeyframeNavigationState ,
11- } from "../../components/editor/KeyframeNavigation" ;
3+ import type { GsapAnimation , PropertyGroupName } from "@hyperframes/core/gsap-parser" ;
124import { Music } from "../../icons/SystemIcons" ;
13- import {
14- absoluteToPercentageForAnimation ,
15- isTimeWithinTween ,
16- resolveTweenDuration ,
17- resolveTweenStart ,
18- } from "../../utils/globalTimeCompiler" ;
195import type { TimelineElement } from "../store/playerStore" ;
20- import type {
21- TimelineEditCallbacks ,
22- TimelinePropertyGroupKeyframeToggle ,
23- } from "./timelineCallbacks" ;
6+ import type { TimelineEditCallbacks } from "./timelineCallbacks" ;
247import { getTimelinePropertyLanes } from "./TimelinePropertyLanes" ;
258import { LayerDisclosureRow } from "./LayerDisclosureRow" ;
9+ import { TrackClipCount } from "./TrackClipCount" ;
2610import { LABEL_COL_W , LANE_H , getTimelineLaneTop } from "./timelineLayout" ;
2711import type { TimelineTheme } from "./timelineTheme" ;
12+ import {
13+ resolveLaneHeaderState ,
14+ type KeyframeNavigationState ,
15+ type TimelinePropertyLane ,
16+ } from "./trackHeaderLaneState" ;
17+ import { valueReadout } from "./trackHeaderLaneValues" ;
2818
2919interface TimelineTrackHeaderProps {
3020 trackNumber : number ;
@@ -33,6 +23,8 @@ interface TimelineTrackHeaderProps {
3323 /** The track's active keyframe clip (selected, else primary) — the one whose
3424 * disclosure + property rows this header shows, whether expanded or not. */
3525 keyframeClip : TimelineElement | null ;
26+ /** Clips on this track, so the header can say how many the row holds. */
27+ clipCount : number ;
3628 isExpanded : boolean ;
3729 animations : readonly GsapAnimation [ ] ;
3830 currentTime : number ;
@@ -47,104 +39,6 @@ interface TimelineTrackHeaderProps {
4739 onSeek ?: ( time : number ) => void ;
4840}
4941
50- function roundValue ( value : number ) : string {
51- return String ( Math . round ( value * 100 ) / 100 ) ;
52- }
53-
54- function propertyValueAt (
55- animation : GsapAnimation ,
56- property : string ,
57- tweenPercentage : number ,
58- ) : number | string | undefined {
59- const keyframes = animation . keyframes ?. keyframes ?? [ ] ;
60- const values = keyframes
61- . filter ( ( keyframe ) => property in keyframe . properties )
62- . map ( ( keyframe ) => ( {
63- percentage : keyframe . percentage ,
64- value : keyframe . properties [ property ] ,
65- } ) ) ;
66- const before = values . filter ( ( value ) => value . percentage <= tweenPercentage ) . at ( - 1 ) ;
67- const after = values . find ( ( value ) => value . percentage >= tweenPercentage ) ;
68- if ( ! before ) return after ?. value ;
69- if ( ! after ) return before . value ;
70- if (
71- typeof before . value !== "number" ||
72- typeof after . value !== "number" ||
73- before . percentage === after . percentage
74- ) {
75- return before . value ;
76- }
77- const progress = ( tweenPercentage - before . percentage ) / ( after . percentage - before . percentage ) ;
78- return before . value + ( after . value - before . value ) * progress ;
79- }
80-
81- function valuesAt (
82- animation : GsapAnimation ,
83- group : PropertyGroupName ,
84- tweenPercentage : number ,
85- ) : Record < string , number | string > {
86- const propertyNames = new Set < string > ( ) ;
87- for ( const keyframe of animation . keyframes ?. keyframes ?? [ ] ) {
88- for ( const property of Object . keys ( keyframe . properties ) ) {
89- if ( classifyPropertyGroup ( property ) === group ) propertyNames . add ( property ) ;
90- }
91- }
92- const values : Record < string , number | string > = { } ;
93- for ( const property of propertyNames ) {
94- const value = propertyValueAt ( animation , property , tweenPercentage ) ;
95- if ( value !== undefined ) values [ property ] = value ;
96- }
97- return values ;
98- }
99-
100- function groupLabel ( group : PropertyGroupName , properties : Record < string , number | string > ) : string {
101- if ( group === "visual" && ( "opacity" in properties || "autoAlpha" in properties ) ) {
102- return "Opacity" ;
103- }
104- if ( group !== "other" ) return `${ group [ 0 ] ?. toUpperCase ( ) ?? "" } ${ group . slice ( 1 ) } ` ;
105- const property = Object . keys ( properties ) [ 0 ] ;
106- return property ? `${ property [ 0 ] ?. toUpperCase ( ) ?? "" } ${ property . slice ( 1 ) } ` : "Other" ;
107- }
108-
109- type LaneValues = Record < string , number | string > ;
110-
111- function defaultValueReadout ( values : LaneValues ) : string {
112- return Object . values ( values )
113- . map ( ( value ) => ( typeof value === "number" ? roundValue ( value ) : value ) )
114- . join ( ", " ) ;
115- }
116-
117- function positionValueReadout ( values : LaneValues ) : string | null {
118- const x = values . x ;
119- const y = values . y ;
120- return typeof x === "number" && typeof y === "number"
121- ? `${ roundValue ( x ) } , ${ roundValue ( y ) } `
122- : null ;
123- }
124-
125- function rotationValueReadout ( values : LaneValues ) : string | null {
126- return typeof values . rotation === "number" ? `${ roundValue ( values . rotation ) } °` : null ;
127- }
128-
129- function visualValueReadout ( values : LaneValues ) : string | null {
130- const opacity = values . opacity ?? values . autoAlpha ;
131- return typeof opacity === "number"
132- ? `${ roundValue ( Math . abs ( opacity ) <= 1 ? opacity * 100 : opacity ) } %`
133- : null ;
134- }
135-
136- const GROUP_VALUE_READOUTS : Partial <
137- Record < PropertyGroupName , ( values : LaneValues ) => string | null >
138- > = {
139- position : positionValueReadout ,
140- rotation : rotationValueReadout ,
141- visual : visualValueReadout ,
142- } ;
143-
144- function valueReadout ( group : PropertyGroupName , values : Record < string , number | string > ) : string {
145- return GROUP_VALUE_READOUTS [ group ] ?.( values ) ?? defaultValueReadout ( values ) ;
146- }
147-
14842function VisibilityButton ( {
14943 hidden,
15044 trackNumber,
@@ -184,13 +78,19 @@ function VisibilityButton({
18478function LegacyTrackHeader ( {
18579 trackNumber,
18680 trackLabel,
81+ clipCount,
18782 showTrackLabel,
18883 isTrackHidden,
18984 isAudioTrack,
19085 onToggleTrackHidden,
19186} : Pick <
19287 TimelineTrackHeaderProps ,
193- "trackNumber" | "trackLabel" | "isTrackHidden" | "isAudioTrack" | "onToggleTrackHidden"
88+ | "trackNumber"
89+ | "trackLabel"
90+ | "clipCount"
91+ | "isTrackHidden"
92+ | "isAudioTrack"
93+ | "onToggleTrackHidden"
19494> & { showTrackLabel : boolean } ) {
19595 return (
19696 < >
@@ -202,6 +102,7 @@ function LegacyTrackHeader({
202102 { trackLabel }
203103 </ span >
204104 ) }
105+ { showTrackLabel && < TrackClipCount clipCount = { clipCount } /> }
205106 < VisibilityButton
206107 hidden = { isTrackHidden }
207108 trackNumber = { trackNumber }
@@ -212,115 +113,6 @@ function LegacyTrackHeader({
212113 ) ;
213114}
214115
215- type TimelinePropertyLane = ReturnType < typeof getTimelinePropertyLanes > [ number ] ;
216- type KeyframeNavigationState = ReturnType <
217- typeof getKeyframeNavigationState < TimelinePropertyLane [ "keyframes" ] [ number ] >
218- > ;
219-
220- function findNearestLaneKeyframe ( lane : TimelinePropertyLane , clipPercentage : number ) {
221- return lane . keyframes . reduce < ( typeof lane . keyframes ) [ number ] | null > (
222- ( nearest , keyframe ) =>
223- ! nearest ||
224- Math . abs ( keyframe . percentage - clipPercentage ) < Math . abs ( nearest . percentage - clipPercentage )
225- ? keyframe
226- : nearest ,
227- null ,
228- ) ;
229- }
230-
231- function findAnimationAtTime ( animations : TimelinePropertyLane [ "animations" ] , currentTime : number ) {
232- return animations . find ( ( candidate ) => {
233- const start = resolveTweenStart ( candidate ) ;
234- return start !== null && isTimeWithinTween ( currentTime , start , resolveTweenDuration ( candidate ) ) ;
235- } ) ;
236- }
237-
238- function resolveLaneAnimation (
239- lane : TimelinePropertyLane ,
240- navigation : KeyframeNavigationState ,
241- nearestKeyframe : TimelinePropertyLane [ "keyframes" ] [ number ] | null ,
242- animationAtPlayhead : GsapAnimation | undefined ,
243- ) {
244- const animationId = navigation . currentKeyframe ?. animationId ?? nearestKeyframe ?. animationId ;
245- return animationAtPlayhead ?? lane . animations . find ( ( candidate ) => candidate . id === animationId ) ;
246- }
247-
248- function resolveLaneTweenPercentage (
249- navigation : KeyframeNavigationState ,
250- animation : GsapAnimation | undefined ,
251- animationKeyframes : TimelinePropertyLane [ "keyframes" ] ,
252- currentTime : number ,
253- clipPercentage : number ,
254- ) {
255- return (
256- navigation . currentKeyframe ?. tweenPercentage ??
257- ( animation ? absoluteToPercentageForAnimation ( currentTime , animation ) : null ) ??
258- clipToTweenPercentage ( animationKeyframes , clipPercentage )
259- ) ;
260- }
261-
262- function valuesForLaneAnimation (
263- animation : GsapAnimation | undefined ,
264- lane : TimelinePropertyLane ,
265- tweenPercentage : number ,
266- ) {
267- return animation ? valuesAt ( animation , lane . group , tweenPercentage ) : { } ;
268- }
269-
270- function createLaneToggleTarget (
271- animation : GsapAnimation | undefined ,
272- lane : TimelinePropertyLane ,
273- tweenPercentage : number ,
274- values : LaneValues ,
275- navigation : KeyframeNavigationState ,
276- ) : TimelinePropertyGroupKeyframeToggle | null {
277- return animation
278- ? {
279- animationId : animation . id ,
280- propertyGroup : lane . group ,
281- tweenPercentage,
282- properties : values ,
283- remove : navigation . currentKeyframe !== null ,
284- }
285- : null ;
286- }
287-
288- function resolveLaneHeaderState (
289- lane : TimelinePropertyLane ,
290- currentTime : number ,
291- clipPercentage : number ,
292- ) {
293- const navigation = getKeyframeNavigationState ( lane . keyframes , clipPercentage ) ;
294- const nearestKeyframe = findNearestLaneKeyframe ( lane , clipPercentage ) ;
295- const animationAtPlayhead = findAnimationAtTime ( lane . animations , currentTime ) ;
296- const animation = resolveLaneAnimation ( lane , navigation , nearestKeyframe , animationAtPlayhead ) ;
297- const animationKeyframes = lane . keyframes . filter (
298- ( keyframe ) => keyframe . animationId === animation ?. id ,
299- ) ;
300- const tweenPercentage = resolveLaneTweenPercentage (
301- navigation ,
302- animation ,
303- animationKeyframes ,
304- currentTime ,
305- clipPercentage ,
306- ) ;
307- const values = valuesForLaneAnimation ( animation , lane , tweenPercentage ) ;
308- const label = groupLabel ( lane . group , values ) ;
309- const toggleTarget = createLaneToggleTarget ( animation , lane , tweenPercentage , values , navigation ) ;
310-
311- return {
312- navigation,
313- nearestKeyframe,
314- animationAtPlayhead,
315- animation,
316- animationKeyframes,
317- tweenPercentage,
318- values,
319- label,
320- toggleTarget,
321- } ;
322- }
323-
324116// Figma layout: prev-keyframe ‹, the add/remove toggle (children), next ›.
325117function PropertyGroupNavigation ( {
326118 navigation,
@@ -484,6 +276,7 @@ export function TimelineTrackHeader({
484276 trackLabel,
485277 contentOrigin,
486278 keyframeClip,
279+ clipCount,
487280 isExpanded,
488281 animations,
489282 currentTime,
@@ -528,6 +321,7 @@ export function TimelineTrackHeader({
528321 < LegacyTrackHeader
529322 trackNumber = { trackNumber }
530323 trackLabel = { trackLabel }
324+ clipCount = { clipCount }
531325 showTrackLabel = { showTrackLabel }
532326 isTrackHidden = { isTrackHidden }
533327 isAudioTrack = { isAudioTrack }
@@ -537,6 +331,7 @@ export function TimelineTrackHeader({
537331 < >
538332 < LayerDisclosureRow
539333 keyframeClip = { keyframeClip }
334+ clipCount = { clipCount }
540335 isExpanded = { isExpanded }
541336 gutterBackground = { theme . gutterBackground }
542337 onToggleClipExpanded = { onToggleClipExpanded }
0 commit comments