mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
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>
60 lines
2.3 KiB
TypeScript
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,
|
|
};
|
|
}
|