mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor(studio): split the diamond row's data contract into its own file
TimelineClipDiamonds.tsx had grown past the 600-line studio file gate, and its keyframe/props types were declared a second time next to the ones the lanes already share. The types, the render constants, and the keyframe identity helper move to timelineDiamondTypes.ts; the rendering stays put.
This commit is contained in:
@@ -8,123 +8,18 @@ import {
|
|||||||
import { MiniCurveSvg } from "../../components/editor/EaseCurveSection";
|
import { MiniCurveSvg } from "../../components/editor/EaseCurveSection";
|
||||||
import { clipToTweenPercentage } from "../../components/editor/KeyframeNavigation";
|
import { clipToTweenPercentage } from "../../components/editor/KeyframeNavigation";
|
||||||
import { LANE_H } from "./timelineLayout";
|
import { LANE_H } from "./timelineLayout";
|
||||||
|
import { timelineKeyframeSelectionKey } from "./timelineKeyframeIdentity";
|
||||||
import {
|
import {
|
||||||
timelineKeyframeSelectionKey,
|
DIAMOND_RATIO,
|
||||||
type TimelineKeyframeTarget,
|
KF_MAX_PCT,
|
||||||
} from "./timelineKeyframeIdentity";
|
KF_MIN_PCT,
|
||||||
export interface TimelineDiamondKeyframe {
|
keyframeTarget,
|
||||||
percentage: number;
|
type DragState,
|
||||||
/** Tween-relative percentage (the retime mutation keys on this, not clip %). */
|
type TimelineClipDiamondsProps,
|
||||||
tweenPercentage?: number;
|
type TimelineDiamondLaneProps,
|
||||||
propertyGroup?: string;
|
} from "./timelineDiamondTypes";
|
||||||
animationId?: string;
|
|
||||||
properties: Record<string, number | string>;
|
|
||||||
ease?: string;
|
|
||||||
/** Set when 2+ source animations collide at this percentage (a single inline
|
|
||||||
* ease button can't target one): the collapsed row hides the button here. */
|
|
||||||
easeAmbiguous?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeyframeCacheEntry {
|
export type { TimelineDiamondKeyframe } from "./timelineDiamondTypes";
|
||||||
format: string;
|
|
||||||
keyframes: TimelineDiamondKeyframe[];
|
|
||||||
ease?: string;
|
|
||||||
easeEach?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TimelineClipDiamondsProps {
|
|
||||||
keyframesData: KeyframeCacheEntry;
|
|
||||||
clipWidthPx: number;
|
|
||||||
clipHeightPx: number;
|
|
||||||
/** Beat-dot strip is shown on this track → shrink diamonds + drop them into
|
|
||||||
* the bottom half so they clear the strip at the top. */
|
|
||||||
beatsActive?: boolean;
|
|
||||||
accentColor: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
currentPercentage: number;
|
|
||||||
elementId: string;
|
|
||||||
selectedKeyframes: ReadonlySet<string>;
|
|
||||||
onClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
||||||
onShiftClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
||||||
onContextMenuKeyframe?: (
|
|
||||||
e: React.MouseEvent,
|
|
||||||
elementId: string,
|
|
||||||
keyframe: TimelineKeyframeTarget,
|
|
||||||
) => void;
|
|
||||||
/** Drag-to-retime: move a keyframe to a new time, preserving its value + ease.
|
|
||||||
* `keyframe` identifies the dragged keyframe (clip-relative percentage plus
|
|
||||||
* whatever animation identity the row carries); `toClipPercentage` is the
|
|
||||||
* neighbour-clamped drop position, also clip-relative. The handler decides
|
|
||||||
* move (within the tween) vs resize (past its boundary). */
|
|
||||||
onMoveKeyframe?: (
|
|
||||||
elementId: string,
|
|
||||||
keyframe: TimelineKeyframeTarget,
|
|
||||||
toClipPercentage: number,
|
|
||||||
) => Promise<boolean>;
|
|
||||||
/** Open the segment ease editor for the hovered mid-point button — available on
|
|
||||||
* the inline clip row too, not just the expanded lanes. */
|
|
||||||
onSelectSegment?: (elementId: string, target: TimelineKeyframeTarget) => void;
|
|
||||||
/** Set while resolving a diamond press so the ancestor clip's onClick (which
|
|
||||||
* toggles selection off when already selected) ignores the native "click"
|
|
||||||
* the browser auto-synthesizes after this button's pointerdown+pointerup. */
|
|
||||||
suppressClickRef?: React.RefObject<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TimelineDiamondLaneProps extends Omit<
|
|
||||||
TimelineClipDiamondsProps,
|
|
||||||
| "onClickKeyframe"
|
|
||||||
| "onShiftClickKeyframe"
|
|
||||||
| "onContextMenuKeyframe"
|
|
||||||
| "onMoveKeyframe"
|
|
||||||
| "onSelectSegment"
|
|
||||||
> {
|
|
||||||
groupAware?: boolean;
|
|
||||||
globalEase?: string;
|
|
||||||
onSelectSegment?: (target: TimelineKeyframeTarget) => void;
|
|
||||||
onClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
|
||||||
onShiftClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
|
||||||
onContextMenuKeyframe?: (e: React.MouseEvent, target: TimelineKeyframeTarget) => void;
|
|
||||||
onMoveKeyframe?: (target: TimelineKeyframeTarget, toClipPercentage: number) => Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DIAMOND_RATIO = 0.8;
|
|
||||||
// Percentage tolerance for rendering keyframes near clip boundaries. Keyframes
|
|
||||||
// slightly outside [0, 100] (from rounding or stale cache during the async
|
|
||||||
// persist → reload cycle) are still rendered (the clip is overflow-visible) at
|
|
||||||
// their true position rather than hidden.
|
|
||||||
const KF_MIN_PCT = -5;
|
|
||||||
const KF_MAX_PCT = 105;
|
|
||||||
|
|
||||||
type DragState = {
|
|
||||||
kfKey: string;
|
|
||||||
startX: number;
|
|
||||||
fromClipPct: number;
|
|
||||||
moved: boolean;
|
|
||||||
/** Latest pointer x, flushed to the preview once per frame. */
|
|
||||||
lastX: number;
|
|
||||||
/** Index in the sorted row, needed by the neighbour clamp off the render path. */
|
|
||||||
index: number;
|
|
||||||
/** Escape was pressed: the drag is dead, and the pointerup that follows is
|
|
||||||
* swallowed rather than falling through to the click branch. */
|
|
||||||
cancelled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The full identity of a diamond, used by every callback and by the selection
|
|
||||||
* key. Collapsed clip rows and expanded property lanes read the same cache, so
|
|
||||||
* they must hash a shared keyframe to the same key: dropping the group here for
|
|
||||||
* the collapsed row would leave a diamond selected in one view and unselected in
|
|
||||||
* the other, and would strip the animation id the retime/delete mutations use to
|
|
||||||
* pick between two animations that collide at one percentage.
|
|
||||||
*/
|
|
||||||
function keyframeTarget(keyframe: TimelineDiamondKeyframe): TimelineKeyframeTarget {
|
|
||||||
return {
|
|
||||||
percentage: keyframe.percentage,
|
|
||||||
tweenPercentage: keyframe.tweenPercentage,
|
|
||||||
propertyGroup: keyframe.propertyGroup,
|
|
||||||
animationId: keyframe.animationId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TimelineDiamondLane = memo(function TimelineDiamondLane({
|
export const TimelineDiamondLane = memo(function TimelineDiamondLane({
|
||||||
keyframesData,
|
keyframesData,
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* The diamond row's data + prop contract, split out of TimelineClipDiamonds so
|
||||||
|
* that file stays under the 600-line studio gate. Types and the shared
|
||||||
|
* keyframe-identity helper live here; the rendering lives there.
|
||||||
|
*/
|
||||||
|
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
||||||
|
|
||||||
|
export interface TimelineDiamondKeyframe {
|
||||||
|
percentage: number;
|
||||||
|
/** Tween-relative percentage (the retime mutation keys on this, not clip %). */
|
||||||
|
tweenPercentage?: number;
|
||||||
|
propertyGroup?: string;
|
||||||
|
animationId?: string;
|
||||||
|
properties: Record<string, number | string>;
|
||||||
|
ease?: string;
|
||||||
|
/** Set when 2+ source animations collide at this percentage (a single inline
|
||||||
|
* ease button can't target one): the collapsed row hides the button here. */
|
||||||
|
easeAmbiguous?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyframeCacheEntry {
|
||||||
|
format: string;
|
||||||
|
keyframes: TimelineDiamondKeyframe[];
|
||||||
|
ease?: string;
|
||||||
|
easeEach?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineClipDiamondsProps {
|
||||||
|
keyframesData: KeyframeCacheEntry;
|
||||||
|
clipWidthPx: number;
|
||||||
|
clipHeightPx: number;
|
||||||
|
/** Beat-dot strip is shown on this track → shrink diamonds + drop them into
|
||||||
|
* the bottom half so they clear the strip at the top. */
|
||||||
|
beatsActive?: boolean;
|
||||||
|
accentColor: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
currentPercentage: number;
|
||||||
|
elementId: string;
|
||||||
|
selectedKeyframes: ReadonlySet<string>;
|
||||||
|
onClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||||
|
onShiftClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||||
|
onContextMenuKeyframe?: (
|
||||||
|
e: React.MouseEvent,
|
||||||
|
elementId: string,
|
||||||
|
keyframe: TimelineKeyframeTarget,
|
||||||
|
) => void;
|
||||||
|
/** Drag-to-retime: move a keyframe to a new time, preserving its value + ease.
|
||||||
|
* `keyframe` identifies the dragged keyframe (clip-relative percentage plus
|
||||||
|
* whatever animation identity the row carries); `toClipPercentage` is the
|
||||||
|
* neighbour-clamped drop position, also clip-relative. The handler decides
|
||||||
|
* move (within the tween) vs resize (past its boundary). */
|
||||||
|
onMoveKeyframe?: (
|
||||||
|
elementId: string,
|
||||||
|
keyframe: TimelineKeyframeTarget,
|
||||||
|
toClipPercentage: number,
|
||||||
|
) => Promise<boolean>;
|
||||||
|
/** Open the segment ease editor for the hovered mid-point button — available on
|
||||||
|
* the inline clip row too, not just the expanded lanes. */
|
||||||
|
onSelectSegment?: (elementId: string, target: TimelineKeyframeTarget) => void;
|
||||||
|
/** Set while resolving a diamond press so the ancestor clip's onClick (which
|
||||||
|
* toggles selection off when already selected) ignores the native "click"
|
||||||
|
* the browser auto-synthesizes after this button's pointerdown+pointerup. */
|
||||||
|
suppressClickRef?: React.RefObject<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineDiamondLaneProps extends Omit<
|
||||||
|
TimelineClipDiamondsProps,
|
||||||
|
| "onClickKeyframe"
|
||||||
|
| "onShiftClickKeyframe"
|
||||||
|
| "onContextMenuKeyframe"
|
||||||
|
| "onMoveKeyframe"
|
||||||
|
| "onSelectSegment"
|
||||||
|
> {
|
||||||
|
groupAware?: boolean;
|
||||||
|
globalEase?: string;
|
||||||
|
onSelectSegment?: (target: TimelineKeyframeTarget) => void;
|
||||||
|
onClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
||||||
|
onShiftClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
||||||
|
onContextMenuKeyframe?: (e: React.MouseEvent, target: TimelineKeyframeTarget) => void;
|
||||||
|
onMoveKeyframe?: (target: TimelineKeyframeTarget, toClipPercentage: number) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DIAMOND_RATIO = 0.8;
|
||||||
|
// Percentage tolerance for rendering keyframes near clip boundaries. Keyframes
|
||||||
|
// slightly outside [0, 100] (from rounding or stale cache during the async
|
||||||
|
// persist → reload cycle) are still rendered (the clip is overflow-visible) at
|
||||||
|
// their true position rather than hidden.
|
||||||
|
export const KF_MIN_PCT = -5;
|
||||||
|
export const KF_MAX_PCT = 105;
|
||||||
|
|
||||||
|
export type DragState = {
|
||||||
|
kfKey: string;
|
||||||
|
startX: number;
|
||||||
|
fromClipPct: number;
|
||||||
|
moved: boolean;
|
||||||
|
/** Latest pointer x, flushed to the preview once per frame. */
|
||||||
|
lastX: number;
|
||||||
|
/** Index in the sorted row, needed by the neighbour clamp off the render path. */
|
||||||
|
index: number;
|
||||||
|
/** Escape was pressed: the drag is dead, and the pointerup that follows is
|
||||||
|
* swallowed rather than falling through to the click branch. */
|
||||||
|
cancelled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full identity of a diamond, used by every callback and by the selection
|
||||||
|
* key. Collapsed clip rows and expanded property lanes read the same cache, so
|
||||||
|
* they must hash a shared keyframe to the same key: dropping the group here for
|
||||||
|
* the collapsed row would leave a diamond selected in one view and unselected in
|
||||||
|
* the other, and would strip the animation id the retime/delete mutations use to
|
||||||
|
* pick between two animations that collide at one percentage.
|
||||||
|
*/
|
||||||
|
export function keyframeTarget(keyframe: TimelineDiamondKeyframe): TimelineKeyframeTarget {
|
||||||
|
return {
|
||||||
|
percentage: keyframe.percentage,
|
||||||
|
tweenPercentage: keyframe.tweenPercentage,
|
||||||
|
propertyGroup: keyframe.propertyGroup,
|
||||||
|
animationId: keyframe.animationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user