mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* feat(studio): runtime hooks — global time compiler + keyframe runtime Add the runtime bridge layer: global time compilation (tween % → clip %), soft reload after mutations, runtime keyframe preview, and keyframe commit helper. * feat(studio): runtime hooks — global time compiler + keyframe runtime Add the runtime bridge layer: global time compilation (tween % → clip %), soft reload after mutations, runtime keyframe preview, and keyframe commit helper. * feat(studio): keyframe cache + commit hooks Add hooks for keyframe cache population (tween → clip-relative %), mutation dispatch, keyframe snapping, and audio beat detection. * feat(studio): timeline UI — dopesheet diamonds + keyboard nav Add dopesheet strip with diamond keyframe indicators, timeline property rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate (STUDIO_KEYFRAMES_ENABLED defaults to false). * feat(studio): design panel — arc controls + ease curve + stagger Add arc path controls (curviness slider, auto-rotate), motion path SVG overlay, ease curve visualization, stagger controls, and expanded animation card. Includes border-radius editor dependency from #1217. * feat(studio): gesture recording core Add gesture recording engine with RAF sampling, modifier key property mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity), Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay. * fix(studio): keyframe drag + recording bug bash 21 fixes: capture GSAP base at drag start, translate:none before gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording seek, _auto flag for 100% keyframes, overlay flash fix, block edits during recording. * feat(studio): keyframe integration wiring + docs Wire App.tsx recording orchestration, TimelineToolbar K/R buttons, PropertyPanel per-property diamonds, shortcuts panel, toast notifications, and keyframes guide documentation. All gated on STUDIO_KEYFRAMES_ENABLED (default false).
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
|
||||
export function absoluteToPercentage(
|
||||
time: number,
|
||||
tweenStart: number,
|
||||
tweenDuration: number,
|
||||
): number {
|
||||
if (tweenDuration <= 0) return 0;
|
||||
const raw = ((time - tweenStart) / tweenDuration) * 100;
|
||||
return Math.max(0, Math.min(100, Math.round(raw * 10) / 10));
|
||||
}
|
||||
|
||||
export function percentageToAbsolute(
|
||||
pct: number,
|
||||
tweenStart: number,
|
||||
tweenDuration: number,
|
||||
): number {
|
||||
return tweenStart + (pct / 100) * tweenDuration;
|
||||
}
|
||||
|
||||
export function isTimeWithinTween(
|
||||
time: number,
|
||||
tweenStart: number,
|
||||
tweenDuration: number,
|
||||
): boolean {
|
||||
return time >= tweenStart && time <= tweenStart + tweenDuration;
|
||||
}
|
||||
|
||||
export function resolveTweenStart(animation: GsapAnimation): number | null {
|
||||
if (typeof animation.position === "number") return animation.position;
|
||||
const parsed = Number.parseFloat(animation.position as string);
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveTweenDuration(animation: GsapAnimation): number {
|
||||
return animation.duration ?? 1;
|
||||
}
|
||||
|
||||
export function findTweenAtTime(
|
||||
time: number,
|
||||
animations: GsapAnimation[],
|
||||
selector: string,
|
||||
): GsapAnimation | null {
|
||||
for (const anim of animations) {
|
||||
if (!matchesSelector(anim.targetSelector, selector)) continue;
|
||||
const start = resolveTweenStart(anim);
|
||||
if (start === null) continue;
|
||||
const duration = resolveTweenDuration(anim);
|
||||
if (isTimeWithinTween(time, start, duration)) return anim;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function absoluteToPercentageForAnimation(
|
||||
time: number,
|
||||
animation: GsapAnimation,
|
||||
): number | null {
|
||||
const start = resolveTweenStart(animation);
|
||||
if (start === null) return null;
|
||||
const duration = resolveTweenDuration(animation);
|
||||
return absoluteToPercentage(time, start, duration);
|
||||
}
|
||||
|
||||
export function percentageToAbsoluteForAnimation(
|
||||
pct: number,
|
||||
animation: GsapAnimation,
|
||||
): number | null {
|
||||
const start = resolveTweenStart(animation);
|
||||
if (start === null) return null;
|
||||
const duration = resolveTweenDuration(animation);
|
||||
return percentageToAbsolute(pct, start, duration);
|
||||
}
|
||||
|
||||
function matchesSelector(tweenSelector: string, querySelector: string): boolean {
|
||||
return tweenSelector.split(",").some((part) => part.trim() === querySelector);
|
||||
}
|
||||
Reference in New Issue
Block a user