Files
hyperframes/packages/studio/src/components/editor/propertyPanelFlatTimingDerivation.ts
T
Vance IngallsandClaude Sonnet 5 b155ed46b6 fix(studio): reconcile keyframe-seek timing basis between flat Layout and Motion groups
Whole-plan coherence review (Plan 3a Layout + Plan 3b Motion) found that
Layout's keyframe gutter and Motion's Timing row independently derived an
element's start/duration and disagreed whenever an element had animations
but no explicit data-duration: Motion correctly inferred the range from the
element's GSAP tweens, while Layout's keyframe gutter fell back to a naive
`duration ?? 1`, so clicking a keyframe percentage in Layout could seek to a
different absolute time than what Motion's Timing row displayed.

Extract deriveElementTiming (propertyPanelFlatTimingDerivation.ts) as the
single shared basis both paths now consume: FlatTimingRow (Motion) and
PropertyPanelFlat's own elStart/elDuration (Layout's keyframe gutter and 3D
Transform block). PropertyPanelFlat now recomputes this basis itself from
its own element/gsapAnimations props instead of trusting the parent's naive
value, so PropertyPanel.tsx (and its legacy non-flat panel) is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:50:43 -07:00

60 lines
2.3 KiB
TypeScript

import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { DomEditSelection } from "./domEditingTypes";
/**
* The single source of truth for an element's clip start/duration in the flat
* inspector. Both the Motion group's Timing row (`FlatTimingRow`) and the
* Layout group's keyframe gutter (fed via `elStart`/`elDuration` from
* `PropertyPanel.tsx` through `PropertyPanelFlat.tsx`) must derive this the
* same way — otherwise a keyframe-percentage seek in Layout lands on a
* different absolute time than the range Motion displays for the same
* element (found by the Plan 3a+3b whole-plan coherence review).
*
* Precedence: an explicit `data-duration` (or `data-hf-authored-duration`)
* wins outright. Only when neither is present do we infer the range from the
* element's own GSAP tweens (earliest tween start → latest tween end).
*
* Scoped to the FLAT inspector only — the legacy (non-flat) panel keeps its
* own, unrelated `elStart`/`elDuration ?? 1` computation in `PropertyPanel.tsx`
* untouched.
*/
export interface ElementTiming {
start: number;
duration: number;
/** True when duration/start came from `deriveTimingFromAnimations`, not an authored attribute. */
inferred: boolean;
}
function deriveTimingFromAnimations(
animations: GsapAnimation[],
): { start: number; duration: number } | null {
let lo = Infinity;
let hi = -Infinity;
for (const a of animations) {
const s = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
const d = a.duration ?? 0;
lo = Math.min(lo, s);
hi = Math.max(hi, s + d);
}
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) return null;
return { start: lo, duration: hi - lo };
}
export function deriveElementTiming(
element: Pick<DomEditSelection, "dataAttributes">,
animations: GsapAnimation[] = [],
): ElementTiming {
const explicitStart = Number.parseFloat(element.dataAttributes.start ?? "0") || 0;
const explicitDuration =
Number.parseFloat(
element.dataAttributes.duration ?? element.dataAttributes["hf-authored-duration"] ?? "0",
) || 0;
const derived = explicitDuration > 0 ? null : deriveTimingFromAnimations(animations);
return {
start: derived ? derived.start : explicitStart,
duration: derived ? derived.duration : explicitDuration,
inferred: derived !== null,
};
}