mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +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,103 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { usePlayerStore } from "../player/store/playerStore";
|
||||
|
||||
interface KeyframeKeyboardOptions {
|
||||
enabled: boolean;
|
||||
onAddKeyframe?: () => void;
|
||||
onDeleteKeyframe?: () => void;
|
||||
onPrevKeyframe?: () => void;
|
||||
onNextKeyframe?: () => void;
|
||||
onToggleHold?: () => void;
|
||||
onToggleExpand?: () => void;
|
||||
onNudgeKeyframe?: (direction: -1 | 1, large: boolean) => void;
|
||||
}
|
||||
|
||||
function isTextInput(el: Element | null): boolean {
|
||||
if (!el) return false;
|
||||
const tag = el.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
||||
return (el as HTMLElement).isContentEditable === true;
|
||||
}
|
||||
|
||||
export function useKeyframeKeyboard({
|
||||
enabled,
|
||||
onAddKeyframe,
|
||||
onDeleteKeyframe,
|
||||
onPrevKeyframe,
|
||||
onNextKeyframe,
|
||||
onToggleHold,
|
||||
onToggleExpand,
|
||||
onNudgeKeyframe,
|
||||
}: KeyframeKeyboardOptions): void {
|
||||
const handler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!enabled) return;
|
||||
if (isTextInput(document.activeElement)) return;
|
||||
|
||||
const hasSelectedKeyframes = usePlayerStore.getState().selectedKeyframes.size > 0;
|
||||
|
||||
switch (e.key.toLowerCase()) {
|
||||
case "k":
|
||||
if (!e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
onAddKeyframe?.();
|
||||
}
|
||||
break;
|
||||
case "delete":
|
||||
case "backspace":
|
||||
if (hasSelectedKeyframes) {
|
||||
e.preventDefault();
|
||||
onDeleteKeyframe?.();
|
||||
}
|
||||
break;
|
||||
case "j":
|
||||
if (!e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) onNextKeyframe?.();
|
||||
else onPrevKeyframe?.();
|
||||
}
|
||||
break;
|
||||
case "h":
|
||||
if (!e.metaKey && !e.ctrlKey && hasSelectedKeyframes) {
|
||||
e.preventDefault();
|
||||
onToggleHold?.();
|
||||
}
|
||||
break;
|
||||
case "u":
|
||||
if (!e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
onToggleExpand?.();
|
||||
}
|
||||
break;
|
||||
case "arrowleft":
|
||||
if (hasSelectedKeyframes && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
onNudgeKeyframe?.(-1, e.shiftKey);
|
||||
}
|
||||
break;
|
||||
case "arrowright":
|
||||
if (hasSelectedKeyframes && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
onNudgeKeyframe?.(1, e.shiftKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
enabled,
|
||||
onAddKeyframe,
|
||||
onDeleteKeyframe,
|
||||
onPrevKeyframe,
|
||||
onNextKeyframe,
|
||||
onToggleHold,
|
||||
onToggleExpand,
|
||||
onNudgeKeyframe,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [enabled, handler]);
|
||||
}
|
||||
Reference in New Issue
Block a user