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 (