import { Fragment, useRef } from "react"; import { KEYFRAME_DRAG_THRESHOLD_PX } from "../../components/editor/keyframeDrag"; import { MiniCurveSvg } from "../../components/editor/EaseCurveSection"; import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity"; import { keyframeTimeLabel, type TimelineDiamondKeyframe } from "./timelineDiamondTypes"; import { timelineEaseFocusId } from "./timelineNavigationIdentity"; /** One diamond's geometry within its row, as computed by the lane. */ export interface TimelineDiamondMarker { keyframe: TimelineDiamondKeyframe; centerX: number; hitWidth: number; visualSize: number; } /** * The line between each pair of adjacent diamonds, plus the ease control that * sits at the segment's midpoint. Split out of TimelineClipDiamonds only to keep * that file inside the repo's file-size cap; it has no state of its own beyond * the press-position guard below. */ export function TimelineDiamondConnectors({ markers, centerY, elementId, clipStart, clipDuration, rovingTargetId, baseColor, baseOpacity, groupAware, globalEase, keyframeTarget, onSelectSegment, }: { markers: readonly TimelineDiamondMarker[]; centerY: number; elementId: string; clipStart: number; clipDuration: number; /** Focus id of the one timeline control currently in the tab order. */ rovingTargetId: string | null; baseColor: string; baseOpacity: number; groupAware: boolean; globalEase: string; keyframeTarget: (keyframe: TimelineDiamondKeyframe) => TimelineKeyframeTarget; onSelectSegment?: (target: TimelineKeyframeTarget) => void; }) { return ( <> {markers.map((marker, i) => { const previous = markers[i - 1]; if (!previous) return null; const kf = marker.keyframe; const x1 = previous.centerX; const x2 = marker.centerX; if (x2 - x1 < 1) return null; const connectorLeft = x1 + previous.visualSize / 2; const connectorWidth = x2 - x1 - previous.visualSize / 2 - marker.visualSize / 2; const target = keyframeTarget(kf); return (
{onSelectSegment && showsEaseControl(kf) && ( = 24} onSelectSegment={onSelectSegment} /> )} ); })} ); } /** * The ease control targets one segment, so it needs the keyframe's own * animationId. A merged keyframe now shows one too: the editor edits every * colliding tween together, so one button standing for several curves is * honest. Only a keyframe with no source animation id (runtime-scanned) is left * out, because there is no tween to target. */ function showsEaseControl(kf: TimelineDiamondKeyframe): boolean { return kf.animationId !== undefined; } /** * The ease button centred on one connector segment, plus the transparent * wrapper that positions it. Split out of the connector map so that map stays a * geometry loop and this keeps the press guard, hit-target sizing and click * filtering together. */ function SegmentEaseControl({ left, width, centerY, ease, target, focusId, rovingTargetId, afterLabel, roomForFullTarget, onSelectSegment, }: { left: number; width: number; centerY: number; ease: string; target: TimelineKeyframeTarget; focusId: string; /** Focus id of the one timeline control currently in the tab order. */ rovingTargetId: string | null; /** Time label of the keyframe this segment starts at, for the accessible name. */ afterLabel: string; roomForFullTarget: boolean; onSelectSegment: (target: TimelineKeyframeTarget) => void; }) { // The ease button sits dead centre of its segment, which on a two-keyframe clip // is the centre of the clip bar, the natural place to grab a clip and drag it. // Swallowing pointerdown there made that grab a no-op. Instead the press falls // through to the clip (so the drag starts normally) and the button keeps only // the click, which we drop if the pointer actually travelled. const pressXRef = useRef(null); return (
); }