fix(studio): retime flat tween keyframe diamonds

This commit is contained in:
ukimsanov
2026-07-20 18:56:14 -07:00
parent e4a30f627f
commit f25a136927
4 changed files with 183 additions and 32 deletions
@@ -56,6 +56,29 @@ const round3 = (n: number) => Math.round(n * 1000) / 1000;
const round1 = (n: number) => Math.round(n * 10) / 10; // 0.1% precision
const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
/** Resolve timing for a flat tween's synthesized start/end diamond. */
function resolveFlatTweenBoundaryRetime(opts: {
keyframeCount: number;
draggedTweenPct: number;
tweenStart: number;
tweenEnd: number;
dropAbsTime: number;
}): KeyframeRetimeResult | null {
const { keyframeCount, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
if (keyframeCount > 0 || (draggedTweenPct !== 0 && draggedTweenPct !== 100)) return null;
const draggedTime = draggedTweenPct === 0 ? tweenStart : tweenEnd;
if (Math.abs(dropAbsTime - draggedTime) <= EPSILON_TIME) return { kind: "noop" };
const newStart = draggedTweenPct === 0 ? dropAbsTime : tweenStart;
const newEnd = draggedTweenPct === 100 ? dropAbsTime : tweenEnd;
if (newEnd <= newStart + EPSILON_TIME) return { kind: "noop" };
return {
kind: "resize",
position: round3(newStart),
duration: round3(newEnd - newStart),
pctRemap: [],
};
}
/**
* Decide move vs resize for a dragged keyframe and, for resize, return the new
* tween window + remapped keyframes.
@@ -76,15 +99,30 @@ export function resolveKeyframeRetime(opts: {
if (tweenDuration <= 0) return { kind: "noop" };
const tweenEnd = tweenStart + tweenDuration;
// Within the tween window → plain move (re-key the tween-%). This branch never
// touches the keyframes array, so it still works for synthesized flat tweens.
// A flat tween's synthesized diamonds are its real start/end boundaries —
// there is no authored keyframe node to re-key. Moving either boundary must
// therefore resize the tween window while keeping the opposite endpoint at
// its absolute time. Returning `resize` with an empty remap lets the caller
// update the flat tween's position/duration instead of sending a keyframe
// mutation that would silently no-op.
const flatBoundary = resolveFlatTweenBoundaryRetime({
keyframeCount: keyframes.length,
draggedTweenPct,
tweenStart,
tweenEnd,
dropAbsTime,
});
if (flatBoundary) return flatBoundary;
// Within the tween window → plain move (re-key the tween-%).
if (dropAbsTime >= tweenStart - EPSILON_TIME && dropAbsTime <= tweenEnd + EPSILON_TIME) {
const toTweenPct = clamp(((dropAbsTime - tweenStart) / tweenDuration) * 100, 0, 100);
if (Math.abs(toTweenPct - draggedTweenPct) < NOOP_EPSILON_PCT) return { kind: "noop" };
return { kind: "move", toTweenPct };
}
// Boundary resize needs the real keyframes to remap; a flat tween has none here.
// Boundary resize needs the real keyframes to remap. Flat start/end boundaries
// were handled above.
if (keyframes.length === 0) return { kind: "noop" };
const newStart = Math.min(dropAbsTime, tweenStart);