@hyperframes/studio 0.6.72 → 0.6.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/assets/index-BcJO6Ej5.js +140 -0
  2. package/dist/assets/index-C2gBZ2km.css +1 -0
  3. package/dist/index.html +2 -2
  4. package/package.json +4 -4
  5. package/src/App.tsx +30 -24
  6. package/src/components/StudioPreviewArea.tsx +101 -26
  7. package/src/components/StudioRightPanel.tsx +3 -0
  8. package/src/components/StudioToast.tsx +18 -0
  9. package/src/components/TimelineToolbar.tsx +230 -4
  10. package/src/components/editor/AnimationCard.tsx +68 -4
  11. package/src/components/editor/DomEditOverlay.tsx +70 -1
  12. package/src/components/editor/GridOverlay.tsx +50 -0
  13. package/src/components/editor/KeyframeDiamond.tsx +49 -0
  14. package/src/components/editor/KeyframeNavigation.tsx +139 -0
  15. package/src/components/editor/PropertyPanel.tsx +293 -140
  16. package/src/components/editor/SnapGuideOverlay.tsx +166 -0
  17. package/src/components/editor/SnapToolbar.tsx +163 -0
  18. package/src/components/editor/SpringEaseEditor.tsx +256 -0
  19. package/src/components/editor/domEditOverlayGestures.ts +7 -0
  20. package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
  21. package/src/components/editor/gsapAnimationConstants.ts +42 -0
  22. package/src/components/editor/gsapAnimationHelpers.ts +2 -1
  23. package/src/components/editor/manualEditingAvailability.ts +6 -0
  24. package/src/components/editor/manualEditsDom.ts +56 -2
  25. package/src/components/editor/manualOffsetDrag.ts +19 -3
  26. package/src/components/editor/propertyPanelHelpers.ts +90 -0
  27. package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
  28. package/src/components/editor/snapEngine.test.ts +657 -0
  29. package/src/components/editor/snapEngine.ts +575 -0
  30. package/src/components/editor/snapTargetCollection.ts +147 -0
  31. package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
  32. package/src/components/nle/NLELayout.tsx +18 -0
  33. package/src/contexts/DomEditContext.tsx +24 -0
  34. package/src/hooks/gsapRuntimeBridge.ts +585 -0
  35. package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
  36. package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
  37. package/src/hooks/useAppHotkeys.ts +63 -1
  38. package/src/hooks/useDomEditCommits.ts +39 -4
  39. package/src/hooks/useDomEditSession.ts +177 -63
  40. package/src/hooks/useGsapScriptCommits.ts +144 -7
  41. package/src/hooks/useGsapSelectionHandlers.ts +202 -0
  42. package/src/hooks/useGsapTweenCache.ts +174 -3
  43. package/src/hooks/useTimelineEditing.ts +93 -0
  44. package/src/icons/SystemIcons.tsx +2 -0
  45. package/src/player/components/ClipContextMenu.tsx +99 -0
  46. package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
  47. package/src/player/components/Timeline.test.ts +2 -1
  48. package/src/player/components/Timeline.tsx +108 -68
  49. package/src/player/components/TimelineCanvas.tsx +47 -1
  50. package/src/player/components/TimelineClip.tsx +8 -3
  51. package/src/player/components/TimelineClipDiamonds.tsx +174 -0
  52. package/src/player/components/timelineDragDrop.ts +103 -0
  53. package/src/player/components/timelineLayout.ts +1 -1
  54. package/src/player/store/playerStore.ts +42 -0
  55. package/src/utils/editHistory.ts +1 -1
  56. package/src/utils/optimisticUpdate.test.ts +53 -0
  57. package/src/utils/optimisticUpdate.ts +18 -0
  58. package/src/utils/studioUiPreferences.ts +17 -0
  59. package/dist/assets/index-CrxThtSJ.css +0 -1
  60. package/dist/assets/index-CveQve6o.js +0 -140
@@ -1,3 +1,4 @@
1
+ // fallow-ignore-file code-duplication
1
2
  /**
2
3
  * Gesture handling for DomEditOverlay.
3
4
  * Owns: onPointerMove, onPointerUp, clearPointerState.
@@ -23,7 +24,12 @@ import {
23
24
  restoreStudioPathOffset,
24
25
  restoreStudioRotation,
25
26
  } from "./manualEdits";
26
- import { type GroupOverlayItem, type OverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
27
+ import {
28
+ type GroupOverlayItem,
29
+ type OverlayRect,
30
+ resolveDomEditGroupOverlayRect,
31
+ toOverlayRect,
32
+ } from "./domEditOverlayGeometry";
27
33
  import {
28
34
  BLOCKED_MOVE_THRESHOLD_PX,
29
35
  type BlockedMoveState,
@@ -39,6 +45,13 @@ import {
39
45
  startGesture as _startGesture,
40
46
  startGroupDrag as _startGroupDrag,
41
47
  } from "./domEditOverlayStartGesture";
48
+ import {
49
+ resolveSnapAdjustment,
50
+ resolveResizeSnapAdjustment,
51
+ resolveEquidistanceGuides,
52
+ SNAP_THRESHOLD_PX,
53
+ } from "./snapEngine";
54
+ import type { SnapGuidesState } from "./SnapGuideOverlay";
42
55
 
43
56
  // Refs are stable across renders; values are read via .current.
44
57
  export type UseDomEditOverlayGesturesOptions = {
@@ -79,6 +92,7 @@ export type UseDomEditOverlayGesturesOptions = {
79
92
  e: React.MouseEvent<HTMLDivElement>,
80
93
  o?: { preferClipAncestor?: boolean },
81
94
  ) => void;
95
+ snapGuidesRef: RefObject<SnapGuidesState | null>;
82
96
  };
83
97
 
84
98
  export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
@@ -111,6 +125,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
111
125
  options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
112
126
  ) => _startGesture(kind, e, opts, options);
113
127
 
128
+ // fallow-ignore-next-line complexity
114
129
  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
115
130
  const g = opts.gestureRef.current;
116
131
  const groupG = opts.groupGestureRef.current;
@@ -133,8 +148,48 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
133
148
  }
134
149
 
135
150
  if (groupG) {
136
- const dx = e.clientX - groupG.startX;
137
- const dy = e.clientY - groupG.startY;
151
+ let dx = e.clientX - groupG.startX;
152
+ let dy = e.clientY - groupG.startY;
153
+
154
+ const sc = groupG.snapContext;
155
+ if (sc?.snapEnabled && sc.targets.length > 0) {
156
+ const groupBounds = resolveDomEditGroupOverlayRect(
157
+ groupG.originItems.map((item) => item.rect),
158
+ );
159
+ if (groupBounds) {
160
+ const allTargets = sc.compositionTarget
161
+ ? [...sc.targets, sc.compositionTarget]
162
+ : sc.targets;
163
+ const snap = resolveSnapAdjustment({
164
+ movingRect: groupBounds,
165
+ proposedDx: dx,
166
+ proposedDy: dy,
167
+ targets: allTargets,
168
+ gridEdges: sc.gridEdges ?? undefined,
169
+ threshold: SNAP_THRESHOLD_PX,
170
+ disabled: e.altKey,
171
+ });
172
+ dx = snap.dx;
173
+ dy = snap.dy;
174
+ const movedRect = {
175
+ left: groupBounds.left + dx,
176
+ top: groupBounds.top + dy,
177
+ width: groupBounds.width,
178
+ height: groupBounds.height,
179
+ };
180
+ const spacingGuides = e.altKey
181
+ ? []
182
+ : resolveEquidistanceGuides({
183
+ movingRect: movedRect,
184
+ targets: allTargets,
185
+ threshold: SNAP_THRESHOLD_PX,
186
+ });
187
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
188
+ }
189
+ }
190
+ groupG.lastSnappedDx = dx;
191
+ groupG.lastSnappedDy = dy;
192
+
138
193
  setDraftGroupOverlayItems(
139
194
  groupG.originItems.map((item) => ({
140
195
  ...item,
@@ -146,8 +201,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
146
201
  }
147
202
 
148
203
  if (!g || !sel) return;
149
- const dx = e.clientX - g.startX;
150
- const dy = e.clientY - g.startY;
204
+ let dx = e.clientX - g.startX;
205
+ let dy = e.clientY - g.startY;
151
206
 
152
207
  if (g.kind === "rotate") {
153
208
  applyStudioRotationDraft(
@@ -167,6 +222,46 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
167
222
  }
168
223
 
169
224
  if (g.kind === "drag") {
225
+ const sc = g.snapContext;
226
+ if (sc?.snapEnabled && sc.targets.length > 0) {
227
+ const movingRect = {
228
+ left: g.originLeft,
229
+ top: g.originTop,
230
+ width: g.originWidth,
231
+ height: g.originHeight,
232
+ };
233
+ const allTargets = sc.compositionTarget
234
+ ? [...sc.targets, sc.compositionTarget]
235
+ : sc.targets;
236
+ const snap = resolveSnapAdjustment({
237
+ movingRect,
238
+ proposedDx: dx,
239
+ proposedDy: dy,
240
+ targets: allTargets,
241
+ gridEdges: sc.gridEdges ?? undefined,
242
+ threshold: SNAP_THRESHOLD_PX,
243
+ disabled: e.altKey,
244
+ });
245
+ dx = snap.dx;
246
+ dy = snap.dy;
247
+ const movedRect = {
248
+ left: movingRect.left + dx,
249
+ top: movingRect.top + dy,
250
+ width: movingRect.width,
251
+ height: movingRect.height,
252
+ };
253
+ const spacingGuides = e.altKey
254
+ ? []
255
+ : resolveEquidistanceGuides({
256
+ movingRect: movedRect,
257
+ targets: allTargets,
258
+ threshold: SNAP_THRESHOLD_PX,
259
+ });
260
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
261
+ }
262
+ g.lastSnappedDx = dx;
263
+ g.lastSnappedDy = dy;
264
+
170
265
  const nextBoxLeft = g.originLeft + dx;
171
266
  const nextBoxTop = g.originTop + dy;
172
267
  setDraftOverlayRect({
@@ -184,6 +279,32 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
184
279
  if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
185
280
  } else {
186
281
  if (!box) return;
282
+
283
+ const sc = g.snapContext;
284
+ if (sc?.snapEnabled && sc.targets.length > 0) {
285
+ const movingRect = {
286
+ left: g.originLeft,
287
+ top: g.originTop,
288
+ width: g.originWidth,
289
+ height: g.originHeight,
290
+ };
291
+ const allTargets = sc.compositionTarget
292
+ ? [...sc.targets, sc.compositionTarget]
293
+ : sc.targets;
294
+ const snap = resolveResizeSnapAdjustment({
295
+ movingRect,
296
+ proposedDx: dx,
297
+ proposedDy: dy,
298
+ targets: allTargets,
299
+ gridEdges: sc.gridEdges ?? undefined,
300
+ threshold: SNAP_THRESHOLD_PX,
301
+ disabled: e.altKey,
302
+ });
303
+ dx = snap.dx;
304
+ dy = snap.dy;
305
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides: [] };
306
+ }
307
+
187
308
  const nextSize = resolveDomEditResizeGesture({
188
309
  originWidth: g.originWidth,
189
310
  originHeight: g.originHeight,
@@ -223,7 +344,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
223
344
  }
224
345
  };
225
346
 
347
+ // fallow-ignore-next-line complexity
226
348
  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
349
+ opts.snapGuidesRef.current = null;
227
350
  const g = opts.gestureRef.current;
228
351
  const groupG = opts.groupGestureRef.current;
229
352
  const sel = g?.selection ?? opts.selectionRef.current;
@@ -233,13 +356,15 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
233
356
  if (groupG) {
234
357
  opts.groupGestureRef.current = null;
235
358
  opts.rafPausedRef.current = false;
236
- const dx = e.clientX - groupG.startX;
237
- const dy = e.clientY - groupG.startY;
238
- if (Math.hypot(dx, dy) < BLOCKED_MOVE_THRESHOLD_PX) {
359
+ const rawDx = e.clientX - groupG.startX;
360
+ const rawDy = e.clientY - groupG.startY;
361
+ if (Math.hypot(rawDx, rawDy) < BLOCKED_MOVE_THRESHOLD_PX) {
239
362
  restoreGroupPathOffsets(groupG);
240
363
  opts.suppressNextBoxClickRef.current = true;
241
364
  return;
242
365
  }
366
+ const dx = groupG.lastSnappedDx ?? rawDx;
367
+ const dy = groupG.lastSnappedDy ?? rawDy;
243
368
  setDraftGroupOverlayItems(
244
369
  groupG.originItems.map((item) => ({
245
370
  ...item,
@@ -327,8 +452,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
327
452
  })
328
453
  .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
329
454
  } else if (g.kind === "drag") {
330
- const dx = e.clientX - g.startX;
331
- const dy = e.clientY - g.startY;
455
+ const dx = g.lastSnappedDx ?? e.clientX - g.startX;
456
+ const dy = g.lastSnappedDy ?? e.clientY - g.startY;
332
457
  if (!g.pathOffsetMember) return;
333
458
  const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
334
459
  const nextBoxLeft = g.originLeft + dx;
@@ -372,7 +497,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
372
497
  }
373
498
  };
374
499
 
500
+ // fallow-ignore-next-line complexity
375
501
  const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
502
+ opts.snapGuidesRef.current = null;
376
503
  const groupG = opts.groupGestureRef.current;
377
504
  if (groupG) restoreGroupPathOffsets(groupG);
378
505
  const g = opts.gestureRef.current;
@@ -69,7 +69,13 @@ interface NLELayoutProps {
69
69
  updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
70
70
  ) => Promise<void> | void;
71
71
  onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
72
+ onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
72
73
  onSelectTimelineElement?: (element: TimelineElement | null) => void;
74
+ onDeleteKeyframe?: (elementId: string, percentage: number) => void;
75
+ onDeleteAllKeyframes?: (elementId: string) => void;
76
+ onChangeKeyframeEase?: (elementId: string, percentage: number, ease: string) => void;
77
+ onMoveKeyframe?: (element: TimelineElement, oldPct: number, newPct: number) => void;
78
+ onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
73
79
  /** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
74
80
  onCompIdToSrcChange?: (map: Map<string, string>) => void;
75
81
  /** Whether the timeline panel is visible (default: true) */
@@ -117,7 +123,13 @@ export const NLELayout = memo(function NLELayout({
117
123
  onMoveElement,
118
124
  onResizeElement,
119
125
  onBlockedEditAttempt,
126
+ onSplitElement,
120
127
  onSelectTimelineElement,
128
+ onDeleteKeyframe,
129
+ onDeleteAllKeyframes,
130
+ onChangeKeyframeEase,
131
+ onMoveKeyframe,
132
+ onToggleKeyframeAtPlayhead,
121
133
  onCompIdToSrcChange,
122
134
  timelineVisible,
123
135
  onToggleTimeline,
@@ -447,7 +459,13 @@ export const NLELayout = memo(function NLELayout({
447
459
  onMoveElement={onMoveElement}
448
460
  onResizeElement={onResizeElement}
449
461
  onBlockedEditAttempt={onBlockedEditAttempt}
462
+ onSplitElement={onSplitElement}
450
463
  onSelectElement={onSelectTimelineElement}
464
+ onDeleteKeyframe={onDeleteKeyframe}
465
+ onDeleteAllKeyframes={onDeleteAllKeyframes}
466
+ onChangeKeyframeEase={onChangeKeyframeEase}
467
+ onMoveKeyframe={onMoveKeyframe}
468
+ onToggleKeyframeAtPlayhead={onToggleKeyframeAtPlayhead}
451
469
  />
452
470
  </div>
453
471
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
@@ -65,6 +65,14 @@ export function DomEditProvider({
65
65
  handleGsapUpdateFromProperty,
66
66
  handleGsapAddFromProperty,
67
67
  handleGsapRemoveFromProperty,
68
+ handleGsapAddKeyframe,
69
+ handleGsapRemoveKeyframe,
70
+ handleGsapConvertToKeyframes,
71
+ handleGsapRemoveAllKeyframes,
72
+ handleResetSelectedElementKeyframes,
73
+ commitAnimatedProperty,
74
+ invalidateGsapCache,
75
+ previewIframeRef,
68
76
  },
69
77
  children,
70
78
  }: {
@@ -125,6 +133,14 @@ export function DomEditProvider({
125
133
  handleGsapUpdateFromProperty,
126
134
  handleGsapAddFromProperty,
127
135
  handleGsapRemoveFromProperty,
136
+ handleGsapAddKeyframe,
137
+ handleGsapRemoveKeyframe,
138
+ handleGsapConvertToKeyframes,
139
+ handleGsapRemoveAllKeyframes,
140
+ handleResetSelectedElementKeyframes,
141
+ commitAnimatedProperty,
142
+ invalidateGsapCache,
143
+ previewIframeRef,
128
144
  }),
129
145
  [
130
146
  domEditSelection,
@@ -179,6 +195,14 @@ export function DomEditProvider({
179
195
  handleGsapUpdateFromProperty,
180
196
  handleGsapAddFromProperty,
181
197
  handleGsapRemoveFromProperty,
198
+ handleGsapAddKeyframe,
199
+ handleGsapRemoveKeyframe,
200
+ handleGsapConvertToKeyframes,
201
+ handleGsapRemoveAllKeyframes,
202
+ handleResetSelectedElementKeyframes,
203
+ commitAnimatedProperty,
204
+ invalidateGsapCache,
205
+ previewIframeRef,
182
206
  ],
183
207
  );
184
208
  return <DomEditContext value={stable}>{children}</DomEditContext>;