mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +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,131 @@
|
||||
import { memo, useCallback } from "react";
|
||||
import type { ArcPathConfig, ArcPathSegment } from "@hyperframes/core/gsap-parser";
|
||||
import { SliderControl } from "./propertyPanelPrimitives";
|
||||
import { LABEL } from "./propertyPanelHelpers";
|
||||
import { P } from "./panelTokens";
|
||||
|
||||
interface ArcPathControlsProps {
|
||||
arcPath: ArcPathConfig;
|
||||
segmentCount: number;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onUpdateSegment: (index: number, update: Partial<ArcPathSegment>) => void;
|
||||
onToggleAutoRotate: (autoRotate: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ArcPathControls = memo(function ArcPathControls({
|
||||
arcPath,
|
||||
segmentCount,
|
||||
onToggle,
|
||||
onUpdateSegment,
|
||||
onToggleAutoRotate,
|
||||
disabled,
|
||||
}: ArcPathControlsProps) {
|
||||
const handleToggle = useCallback(() => {
|
||||
onToggle(!arcPath.enabled);
|
||||
}, [arcPath.enabled, onToggle]);
|
||||
|
||||
const handleAutoRotate = useCallback(() => {
|
||||
onToggleAutoRotate(!arcPath.autoRotate);
|
||||
}, [arcPath.autoRotate, onToggleAutoRotate]);
|
||||
|
||||
if (segmentCount < 1) {
|
||||
return (
|
||||
<div className="rounded-md border border-neutral-800 bg-neutral-900/50 px-3 py-2">
|
||||
<p className="text-[11px] text-neutral-500">
|
||||
Add at least 2 position keyframes to enable arc motion.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={LABEL}>Arc Motion</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled}
|
||||
className="relative rounded-full transition-all duration-150"
|
||||
style={{ width: 28, height: 16, background: arcPath.enabled ? P.accent : P.borderInput }}
|
||||
title={arcPath.enabled ? "Disable arc motion" : "Enable arc motion"}
|
||||
>
|
||||
<span
|
||||
className="absolute top-[2px] left-0 rounded-full transition-transform duration-150"
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
background: arcPath.enabled ? P.white : P.textMuted,
|
||||
transform: arcPath.enabled ? "translateX(14px)" : "translateX(2px)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{arcPath.enabled && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={LABEL}>Auto-Rotate</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAutoRotate}
|
||||
disabled={disabled}
|
||||
className="relative rounded-full transition-all duration-150"
|
||||
style={{
|
||||
width: 28,
|
||||
height: 16,
|
||||
background: arcPath.autoRotate ? P.accent : "#27272A",
|
||||
}}
|
||||
title={
|
||||
arcPath.autoRotate
|
||||
? "Disable auto-rotate along path"
|
||||
: "Rotate element to follow path tangent"
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="absolute top-[2px] left-0 rounded-full transition-transform duration-150"
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
background: arcPath.autoRotate ? P.white : P.textMuted,
|
||||
transform: arcPath.autoRotate ? "translateX(14px)" : "translateX(2px)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{arcPath.segments.map((seg, i) => (
|
||||
<div key={i} className="grid min-w-0 gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={LABEL}>
|
||||
{segmentCount === 1 ? "Curviness" : `Segment ${i + 1}`}
|
||||
</span>
|
||||
{seg.cp1 && seg.cp2 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateSegment(i, { cp1: undefined, cp2: undefined })}
|
||||
className="text-[9px] font-medium text-neutral-500 transition-colors hover:text-neutral-300"
|
||||
title="Reset to auto-generated control points"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<SliderControl
|
||||
value={seg.curviness}
|
||||
min={0}
|
||||
max={3}
|
||||
step={0.1}
|
||||
disabled={disabled}
|
||||
displayValue={seg.curviness.toFixed(1)}
|
||||
formatDisplayValue={(v) => v.toFixed(1)}
|
||||
onCommit={(v) => onUpdateSegment(i, { curviness: v })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user