mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 09:28:08 +00:00
* 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).
132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
});
|