Files
hyperframes/packages/studio/src/components/editor/ArcPathControls.tsx
T
Miguel Ángel a468550f82 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).
2026-06-09 18:30:23 -04:00

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>
);
});