mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
refactor(studio): split the track header's lane math out of its JSX
The header file owned value sampling, readout formatting, lane-state resolution and the JSX at once, so a formatting change and a layout change edited the same file. Sampling and formatting now live in trackHeaderLaneValues, lane-state resolution in trackHeaderLaneState, and resolveLaneHeaderState returns only the four fields its caller reads. Also shows the track's clip count next to the track identity, which the header promised but never rendered.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Resolves what a property lane's header row shows at the current playhead:
|
||||
* which animation owns the lane right now, where the playhead sits inside it,
|
||||
* the sampled values, and the add/remove keyframe target. Pure state, no JSX, so
|
||||
* the header component only renders what this returns.
|
||||
*/
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import {
|
||||
clipToTweenPercentage,
|
||||
getKeyframeNavigationState,
|
||||
} from "../../components/editor/KeyframeNavigation";
|
||||
import {
|
||||
absoluteToPercentageForAnimation,
|
||||
isTimeWithinTween,
|
||||
resolveTweenDuration,
|
||||
resolveTweenStart,
|
||||
} from "../../utils/globalTimeCompiler";
|
||||
import type { TimelinePropertyGroupKeyframeToggle } from "./timelineCallbacks";
|
||||
import { getTimelinePropertyLanes } from "./TimelinePropertyLanes";
|
||||
import { groupLabel, valuesAt, type LaneValues } from "./trackHeaderLaneValues";
|
||||
|
||||
export type TimelinePropertyLane = ReturnType<typeof getTimelinePropertyLanes>[number];
|
||||
export type KeyframeNavigationState = ReturnType<
|
||||
typeof getKeyframeNavigationState<TimelinePropertyLane["keyframes"][number]>
|
||||
>;
|
||||
|
||||
function findNearestLaneKeyframe(lane: TimelinePropertyLane, clipPercentage: number) {
|
||||
return lane.keyframes.reduce<(typeof lane.keyframes)[number] | null>(
|
||||
(nearest, keyframe) =>
|
||||
!nearest ||
|
||||
Math.abs(keyframe.percentage - clipPercentage) < Math.abs(nearest.percentage - clipPercentage)
|
||||
? keyframe
|
||||
: nearest,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
function findAnimationAtTime(animations: TimelinePropertyLane["animations"], currentTime: number) {
|
||||
return animations.find((candidate) => {
|
||||
const start = resolveTweenStart(candidate);
|
||||
return start !== null && isTimeWithinTween(currentTime, start, resolveTweenDuration(candidate));
|
||||
});
|
||||
}
|
||||
|
||||
function resolveLaneAnimation(
|
||||
lane: TimelinePropertyLane,
|
||||
navigation: KeyframeNavigationState,
|
||||
nearestKeyframe: TimelinePropertyLane["keyframes"][number] | null,
|
||||
animationAtPlayhead: GsapAnimation | undefined,
|
||||
) {
|
||||
const animationId = navigation.currentKeyframe?.animationId ?? nearestKeyframe?.animationId;
|
||||
return animationAtPlayhead ?? lane.animations.find((candidate) => candidate.id === animationId);
|
||||
}
|
||||
|
||||
function resolveLaneTweenPercentage(
|
||||
navigation: KeyframeNavigationState,
|
||||
animation: GsapAnimation | undefined,
|
||||
animationKeyframes: TimelinePropertyLane["keyframes"],
|
||||
currentTime: number,
|
||||
clipPercentage: number,
|
||||
) {
|
||||
return (
|
||||
navigation.currentKeyframe?.tweenPercentage ??
|
||||
(animation ? absoluteToPercentageForAnimation(currentTime, animation) : null) ??
|
||||
clipToTweenPercentage(animationKeyframes, clipPercentage)
|
||||
);
|
||||
}
|
||||
|
||||
function createLaneToggleTarget(
|
||||
animation: GsapAnimation | undefined,
|
||||
lane: TimelinePropertyLane,
|
||||
tweenPercentage: number,
|
||||
values: LaneValues,
|
||||
navigation: KeyframeNavigationState,
|
||||
): TimelinePropertyGroupKeyframeToggle | null {
|
||||
return animation
|
||||
? {
|
||||
animationId: animation.id,
|
||||
propertyGroup: lane.group,
|
||||
tweenPercentage,
|
||||
properties: values,
|
||||
remove: navigation.currentKeyframe !== null,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
export interface LaneHeaderState {
|
||||
navigation: KeyframeNavigationState;
|
||||
values: LaneValues;
|
||||
label: string;
|
||||
toggleTarget: TimelinePropertyGroupKeyframeToggle | null;
|
||||
}
|
||||
|
||||
export function resolveLaneHeaderState(
|
||||
lane: TimelinePropertyLane,
|
||||
currentTime: number,
|
||||
clipPercentage: number,
|
||||
): LaneHeaderState {
|
||||
const navigation = getKeyframeNavigationState(lane.keyframes, clipPercentage);
|
||||
const nearestKeyframe = findNearestLaneKeyframe(lane, clipPercentage);
|
||||
const animationAtPlayhead = findAnimationAtTime(lane.animations, currentTime);
|
||||
const animation = resolveLaneAnimation(lane, navigation, nearestKeyframe, animationAtPlayhead);
|
||||
const animationKeyframes = lane.keyframes.filter(
|
||||
(keyframe) => keyframe.animationId === animation?.id,
|
||||
);
|
||||
const tweenPercentage = resolveLaneTweenPercentage(
|
||||
navigation,
|
||||
animation,
|
||||
animationKeyframes,
|
||||
currentTime,
|
||||
clipPercentage,
|
||||
);
|
||||
const values = animation ? valuesAt(animation, lane.group, tweenPercentage) : {};
|
||||
|
||||
return {
|
||||
navigation,
|
||||
values,
|
||||
label: groupLabel(lane.group, values),
|
||||
toggleTarget: createLaneToggleTarget(animation, lane, tweenPercentage, values, navigation),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user