mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30: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:
@@ -90,6 +90,29 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
}: DomEditOverlayProps) {
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const boxRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const selectionShapeStyles = (() => {
|
||||
const fallback = {
|
||||
borderRadius: 4 as string | number,
|
||||
clipPath: undefined as string | undefined,
|
||||
};
|
||||
if (!selection?.element) return fallback;
|
||||
try {
|
||||
const tag = selection.element.tagName.toLowerCase();
|
||||
if (tag === "svg" || tag === "img" || tag === "video" || tag === "canvas") return fallback;
|
||||
const win = selection.element.ownerDocument.defaultView;
|
||||
if (!win) return fallback;
|
||||
const cs = win.getComputedStyle(selection.element);
|
||||
const br = cs.borderRadius;
|
||||
const cp = cs.clipPath;
|
||||
return {
|
||||
borderRadius: br && br !== "0px" ? br : 4,
|
||||
clipPath: cp && cp !== "none" ? cp : undefined,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
})();
|
||||
const gestureRef = useRef<GestureState | null>(null);
|
||||
const groupGestureRef = useRef<GroupGestureState | null>(null);
|
||||
const blockedMoveRef = useRef<BlockedMoveState | null>(null);
|
||||
@@ -134,6 +157,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
groupOverlayItems,
|
||||
groupOverlayItemsRef,
|
||||
setGroupOverlayItems,
|
||||
childRects,
|
||||
} = useDomEditOverlayRects({
|
||||
iframeRef,
|
||||
overlayRef,
|
||||
@@ -228,6 +252,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
groupOverlayItems.every((item) => item.selection.capabilities.canApplyManualOffset);
|
||||
|
||||
const handleOverlayMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!allowCanvasMovement) return;
|
||||
if (suppressNextOverlayMouseDownRef.current) {
|
||||
suppressNextOverlayMouseDownRef.current = false;
|
||||
suppressNextBoxMouseDownRef.current = false;
|
||||
@@ -288,6 +313,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
};
|
||||
|
||||
const handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!allowCanvasMovement) return;
|
||||
if (gestureRef.current || groupGestureRef.current) return;
|
||||
if (suppressNextBoxClickRef.current) {
|
||||
suppressNextBoxClickRef.current = false;
|
||||
@@ -320,20 +346,37 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
onPointerUp={gestures.onPointerUp}
|
||||
onPointerCancel={() => gestures.clearPointerState(selectionRef)}
|
||||
>
|
||||
{hoverSelection && hoverRect && (
|
||||
{hoverSelection && hoverRect && compRect.width > 0 && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-dom-edit-hover-box="true"
|
||||
className="pointer-events-none absolute rounded-xl border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
||||
style={{
|
||||
left: hoverRect.left,
|
||||
top: hoverRect.top,
|
||||
width: hoverRect.width,
|
||||
height: hoverRect.height,
|
||||
}}
|
||||
className="pointer-events-none absolute border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
||||
style={(() => {
|
||||
let br: string | number = 4;
|
||||
let cp: string | undefined;
|
||||
try {
|
||||
const el = hoverSelection.element;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag !== "svg" && tag !== "img" && tag !== "video" && tag !== "canvas") {
|
||||
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
||||
if (cs?.borderRadius && cs.borderRadius !== "0px") br = cs.borderRadius;
|
||||
if (cs?.clipPath && cs.clipPath !== "none") cp = cs.clipPath;
|
||||
}
|
||||
} catch {
|
||||
/* cross-origin guard */
|
||||
}
|
||||
return {
|
||||
left: hoverRect.left,
|
||||
top: hoverRect.top,
|
||||
width: hoverRect.width,
|
||||
height: hoverRect.height,
|
||||
borderRadius: br,
|
||||
clipPath: cp,
|
||||
};
|
||||
})()}
|
||||
/>
|
||||
)}
|
||||
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && (
|
||||
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && compRect.width > 0 && (
|
||||
<>
|
||||
{groupOverlayItems.map((item) => (
|
||||
<div
|
||||
@@ -367,7 +410,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!hasGroupSelection && selection && overlayRect && (
|
||||
{!hasGroupSelection && selection && overlayRect && compRect.width > 0 && (
|
||||
<>
|
||||
{allowCanvasMovement && selection.capabilities.canApplyManualRotation && (
|
||||
<div
|
||||
@@ -398,12 +441,14 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
key={selectionKey}
|
||||
ref={boxRef}
|
||||
data-dom-edit-selection-box="true"
|
||||
className="pointer-events-auto absolute rounded-xl border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
||||
className={`pointer-events-auto absolute ${selectionShapeStyles.clipPath ? "shadow-[inset_0_0_0_2px_rgba(60,230,172,0.6)]" : "border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"} bg-studio-accent/5`}
|
||||
style={{
|
||||
left: overlayRect.left,
|
||||
top: overlayRect.top,
|
||||
width: overlayRect.width,
|
||||
height: overlayRect.height,
|
||||
borderRadius: selectionShapeStyles.borderRadius,
|
||||
clipPath: selectionShapeStyles.clipPath,
|
||||
cursor:
|
||||
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
||||
? "move"
|
||||
@@ -441,6 +486,20 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{childRects.length > 0 &&
|
||||
compRect.width > 0 &&
|
||||
childRects.map((cr, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="pointer-events-none absolute border border-dashed border-white/20 rounded-sm"
|
||||
style={{
|
||||
left: cr.left,
|
||||
top: cr.top,
|
||||
width: cr.width,
|
||||
height: cr.height,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<GridOverlay
|
||||
visible={gridVisible}
|
||||
spacing={gridSpacing}
|
||||
|
||||
Reference in New Issue
Block a user