@hyperframes/studio 0.6.90 → 0.6.92

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 (58) hide show
  1. package/dist/assets/{index-DSLrl2tB.js → index-CDy8BuGq.js} +24 -24
  2. package/dist/assets/index-CmRIkCwI.js +251 -0
  3. package/dist/assets/index-rm9tn9nH.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +2 -0
  7. package/src/components/StudioPreviewArea.tsx +54 -13
  8. package/src/components/TimelineToolbar.tsx +52 -35
  9. package/src/components/editor/DomEditOverlay.tsx +79 -0
  10. package/src/components/editor/PropertyPanel.tsx +19 -10
  11. package/src/components/editor/gsapAnimatesProperty.ts +30 -0
  12. package/src/components/editor/manualEditingAvailability.test.ts +12 -0
  13. package/src/components/editor/manualEditingAvailability.ts +16 -0
  14. package/src/components/editor/manualEditsDom.ts +25 -5
  15. package/src/components/editor/manualEditsDomPatches.test.ts +1 -0
  16. package/src/components/editor/manualEditsDomPatches.ts +17 -1
  17. package/src/components/editor/manualEditsSnapshot.ts +16 -0
  18. package/src/components/editor/propertyPanel3dTransform.tsx +19 -4
  19. package/src/components/editor/useOffScreenIndicators.ts +197 -0
  20. package/src/components/nle/NLELayout.tsx +22 -32
  21. package/src/components/nle/TimelineEditorNotice.tsx +2 -25
  22. package/src/contexts/DomEditContext.tsx +4 -0
  23. package/src/hooks/gsapDragCommit.ts +119 -43
  24. package/src/hooks/gsapKeyframeCacheHelpers.ts +9 -4
  25. package/src/hooks/gsapRuntimeBridge.ts +266 -41
  26. package/src/hooks/gsapRuntimeReaders.ts +16 -2
  27. package/src/hooks/useAnimatedPropertyCommit.ts +11 -5
  28. package/src/hooks/useAppHotkeys.ts +48 -1
  29. package/src/hooks/useContextMenuDismiss.ts +29 -0
  30. package/src/hooks/useDomEditCommits.ts +7 -1
  31. package/src/hooks/useDomEditSession.ts +20 -4
  32. package/src/hooks/useEnableKeyframes.ts +3 -1
  33. package/src/hooks/useGestureCommit.ts +99 -13
  34. package/src/hooks/useGestureRecording.ts +18 -2
  35. package/src/hooks/useGsapScriptCommits.ts +24 -3
  36. package/src/hooks/useGsapSelectionHandlers.ts +19 -3
  37. package/src/hooks/useGsapTweenCache.ts +30 -10
  38. package/src/hooks/useRazorSplit.ts +298 -0
  39. package/src/hooks/useTimelineEditing.ts +15 -98
  40. package/src/player/components/ClipContextMenu.tsx +14 -25
  41. package/src/player/components/KeyframeDiamondContextMenu.tsx +16 -112
  42. package/src/player/components/PlayheadIndicator.tsx +43 -0
  43. package/src/player/components/Timeline.tsx +45 -38
  44. package/src/player/components/TimelineCanvas.tsx +29 -22
  45. package/src/player/components/TimelineClipDiamonds.tsx +3 -1
  46. package/src/player/components/timelineCallbacks.ts +44 -0
  47. package/src/player/components/timelineDragDrop.ts +2 -14
  48. package/src/player/components/useTimelineZoom.ts +18 -0
  49. package/src/player/store/playerStore.ts +20 -0
  50. package/src/utils/globalTimeCompiler.test.ts +2 -2
  51. package/src/utils/globalTimeCompiler.ts +2 -1
  52. package/src/utils/gsapSoftReload.test.ts +16 -0
  53. package/src/utils/gsapSoftReload.ts +43 -8
  54. package/src/utils/rdpSimplify.ts +3 -2
  55. package/src/utils/timelineElementSplit.test.ts +50 -0
  56. package/src/utils/timelineElementSplit.ts +32 -0
  57. package/dist/assets/index-BKuDHMYl.js +0 -146
  58. package/dist/assets/index-D2NkPomd.css +0 -1
@@ -11,8 +11,6 @@ import {
11
11
  resolveTweenStart,
12
12
  resolveTweenDuration,
13
13
  } from "../utils/globalTimeCompiler";
14
- import { readAllAnimatedProperties } from "./gsapRuntimeReaders";
15
-
16
14
  export interface GsapDragCommitCallbacks {
17
15
  commitMutation: (
18
16
  selection: DomEditSelection,
@@ -25,6 +23,7 @@ export interface GsapDragCommitCallbacks {
25
23
  beforeReload?: () => void;
26
24
  },
27
25
  ) => Promise<void>;
26
+ fetchAnimations?: () => Promise<GsapAnimation[]>;
28
27
  }
29
28
 
30
29
  // ── Percentage computation ─────────────────────────────────────────────────
@@ -114,7 +113,6 @@ async function extendTweenAndAddKeyframe(
114
113
  const newStart = Math.min(targetTime, tweenStart);
115
114
  const newEnd = Math.max(targetTime, tweenEnd);
116
115
  const newDuration = Math.max(0.01, newEnd - newStart);
117
-
118
116
  const existingKfs = anim.keyframes?.keyframes ?? [];
119
117
  const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
120
118
  [];
@@ -126,20 +124,15 @@ async function extendTweenAndAddKeyframe(
126
124
 
127
125
  const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
128
126
  remappedKfs.push({ percentage: targetPct, properties });
129
- remappedKfs.sort((a, b) => a.percentage - b.percentage);
130
127
 
131
- await callbacks.commitMutation(
132
- selection,
133
- { type: "delete", animationId: anim.id },
134
- { label: "Extend tween range", skipReload: true },
135
- );
128
+ remappedKfs.sort((a, b) => a.percentage - b.percentage);
136
129
 
137
- const selector = anim.targetSelector;
138
130
  await callbacks.commitMutation(
139
131
  selection,
140
132
  {
141
- type: "add-with-keyframes",
142
- targetSelector: selector,
133
+ type: "replace-with-keyframes",
134
+ animationId: anim.id,
135
+ targetSelector: anim.targetSelector,
143
136
  position: Math.round(newStart * 1000) / 1000,
144
137
  duration: Math.round(newDuration * 1000) / 1000,
145
138
  keyframes: remappedKfs,
@@ -156,8 +149,8 @@ async function commitKeyframedPosition(
156
149
  callbacks: GsapDragCommitCallbacks,
157
150
  beforeReload?: () => void,
158
151
  ): Promise<void> {
159
- const pct = computeCurrentPercentage(selection, anim);
160
-
152
+ const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
153
+ const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
161
154
  await callbacks.commitMutation(
162
155
  selection,
163
156
  {
@@ -168,6 +161,7 @@ async function commitKeyframedPosition(
168
161
  },
169
162
  { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
170
163
  );
164
+ if (activeKeyframePct != null) setActiveKeyframePct(null);
171
165
  }
172
166
 
173
167
  /**
@@ -182,10 +176,11 @@ async function commitFlatViaKeyframes(
182
176
  callbacks: GsapDragCommitCallbacks,
183
177
  beforeReload?: () => void,
184
178
  ): Promise<void> {
179
+ const coalesceKey = `gsap:convert-drag:${anim.id}`;
185
180
  await callbacks.commitMutation(
186
181
  selection,
187
182
  { type: "convert-to-keyframes", animationId: anim.id },
188
- { label: "Convert to keyframes for drag", skipReload: true },
183
+ { label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
189
184
  );
190
185
 
191
186
  const pct = computeCurrentPercentage(selection, anim);
@@ -198,7 +193,7 @@ async function commitFlatViaKeyframes(
198
193
  percentage: pct,
199
194
  properties,
200
195
  },
201
- { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
196
+ { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
202
197
  );
203
198
  }
204
199
 
@@ -243,19 +238,20 @@ export async function commitGsapPositionFromDrag(
243
238
  el.removeAttribute("data-hf-drag-initial-offset-y");
244
239
  };
245
240
 
241
+ const ct = usePlayerStore.getState().currentTime;
246
242
  if (anim.keyframes) {
247
243
  const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
248
244
  const effectiveAnim = newId ? { ...anim, id: newId } : anim;
249
- const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
245
+ const dragProps: Record<string, number> = { x: newX, y: newY };
250
246
 
251
- const ct = usePlayerStore.getState().currentTime;
252
247
  const ts = resolveTweenStart(effectiveAnim);
253
248
  const td = resolveTweenDuration(effectiveAnim);
254
- if (ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01)) {
249
+ const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
250
+ if (outsideRange) {
255
251
  await extendTweenAndAddKeyframe(
256
252
  selection,
257
253
  effectiveAnim,
258
- { ...runtimeProps, x: newX, y: newY },
254
+ dragProps,
259
255
  ct,
260
256
  ts,
261
257
  td,
@@ -263,32 +259,112 @@ export async function commitGsapPositionFromDrag(
263
259
  restoreOffset,
264
260
  );
265
261
  } else {
266
- await commitKeyframedPosition(
262
+ await commitKeyframedPosition(selection, effectiveAnim, dragProps, callbacks, restoreOffset);
263
+ }
264
+ } else if (anim.method === "from" || anim.method === "fromTo") {
265
+ const ct = usePlayerStore.getState().currentTime;
266
+ const ts = resolveTweenStart(anim);
267
+ const td = resolveTweenDuration(anim);
268
+ const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
269
+ const dragProps: Record<string, number> = { x: newX, y: newY };
270
+
271
+ if (outsideRange && ts !== null) {
272
+ // Split the original from() tween into property groups first.
273
+ await callbacks.commitMutation(
267
274
  selection,
268
- effectiveAnim,
269
- { ...runtimeProps, x: newX, y: newY },
270
- callbacks,
271
- restoreOffset,
275
+ { type: "split-into-property-groups", animationId: anim.id },
276
+ { label: "Split from() for drag", skipReload: true },
277
+ );
278
+
279
+ const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
280
+ const existingPosAnim = allAnims.find(
281
+ (a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
282
+ );
283
+
284
+ if (existingPosAnim?.keyframes) {
285
+ // Extend the existing position tween
286
+ const posTs = resolveTweenStart(existingPosAnim);
287
+ const posTd = resolveTweenDuration(existingPosAnim);
288
+ if (posTs !== null) {
289
+ await extendTweenAndAddKeyframe(
290
+ selection,
291
+ existingPosAnim,
292
+ { x: newX, y: newY },
293
+ ct,
294
+ posTs,
295
+ posTd,
296
+ callbacks,
297
+ restoreOffset,
298
+ );
299
+ return;
300
+ }
301
+ }
302
+
303
+ // No existing position tween — create one
304
+ const newStart = Math.min(ct, ts);
305
+ const newEnd = Math.max(ct, ts + td);
306
+ const newDuration = Math.max(0.01, newEnd - newStart);
307
+ const dragBefore = ct < ts;
308
+ const origStartPct = Math.round(((ts - newStart) / newDuration) * 1000) / 10;
309
+ const origEndPct = Math.round(((ts + td - newStart) / newDuration) * 1000) / 10;
310
+
311
+ const keyframes: Array<{ percentage: number; properties: Record<string, number | string> }> =
312
+ [];
313
+ if (dragBefore) {
314
+ keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
315
+ if (origStartPct > 0.5 && origStartPct < 99.5) {
316
+ keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
317
+ }
318
+ keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
319
+ } else {
320
+ keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
321
+ if (origEndPct > 0.5 && origEndPct < 99.5) {
322
+ keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
323
+ }
324
+ keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
325
+ }
326
+ keyframes.sort((a, b) => a.percentage - b.percentage);
327
+
328
+ await callbacks.commitMutation(
329
+ selection,
330
+ {
331
+ type: "add-with-keyframes",
332
+ targetSelector: anim.targetSelector,
333
+ position: Math.round(newStart * 1000) / 1000,
334
+ duration: Math.round(newDuration * 1000) / 1000,
335
+ keyframes,
336
+ },
337
+ { label: "Move layer (from extended)", softReload: true, beforeReload: restoreOffset },
338
+ );
339
+ } else {
340
+ // Inside tween range: convert then add keyframe at current time
341
+ const coalesceKey = `gsap:convert-drag:${anim.id}`;
342
+ await callbacks.commitMutation(
343
+ selection,
344
+ {
345
+ type: "convert-to-keyframes",
346
+ animationId: anim.id,
347
+ },
348
+ { label: "Convert from() for drag", skipReload: true, coalesceKey },
349
+ );
350
+ const pct = computeCurrentPercentage(selection, anim);
351
+ await callbacks.commitMutation(
352
+ selection,
353
+ {
354
+ type: "add-keyframe",
355
+ animationId: anim.id,
356
+ percentage: pct,
357
+ properties: dragProps,
358
+ },
359
+ {
360
+ label: `Move layer (keyframe ${pct}%)`,
361
+ softReload: true,
362
+ beforeReload: restoreOffset,
363
+ coalesceKey,
364
+ },
272
365
  );
273
366
  }
274
- } else if (anim.method === "from" || anim.method === "fromTo") {
275
- await callbacks.commitMutation(
276
- selection,
277
- {
278
- type: "convert-to-keyframes",
279
- animationId: anim.id,
280
- resolvedFromValues: { x: newX, y: newY },
281
- },
282
- { label: "Move layer (keyframe rest)", softReload: true, beforeReload: restoreOffset },
283
- );
284
367
  } else {
285
- const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
286
- await commitFlatViaKeyframes(
287
- selection,
288
- anim,
289
- { ...runtimeProps, x: newX, y: newY },
290
- callbacks,
291
- restoreOffset,
292
- );
368
+ await commitFlatViaKeyframes(selection, anim, { x: newX, y: newY }, callbacks, restoreOffset);
293
369
  }
294
370
  }
@@ -21,18 +21,23 @@ export function updateKeyframeCacheFromParsed(
21
21
 
22
22
  // Convert tween-relative percentages to clip-relative so diamonds
23
23
  // render at the correct position within the timeline clip.
24
- const tweenPos = typeof anim.position === "number" ? anim.position : 0;
24
+ const tweenPos = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
25
25
  const tweenDur = anim.duration ?? 1;
26
26
  const timelineEl = elements.find(
27
27
  (el) => el.domId === id || (el.key ?? el.id) === `${targetPath}#${id}`,
28
28
  );
29
29
  const elStart = timelineEl?.start ?? 0;
30
- const elDuration = timelineEl?.duration ?? 4;
30
+ const elDuration = timelineEl?.duration ?? 1;
31
31
  const clipKeyframes = anim.keyframes.keyframes.map((kf) => {
32
32
  const absTime = tweenPos + (kf.percentage / 100) * tweenDur;
33
33
  const clipPct =
34
34
  elDuration > 0 ? Math.round(((absTime - elStart) / elDuration) * 1000) / 10 : kf.percentage;
35
- return { ...kf, percentage: clipPct };
35
+ return {
36
+ ...kf,
37
+ percentage: clipPct,
38
+ tweenPercentage: kf.percentage,
39
+ propertyGroup: anim.propertyGroup,
40
+ };
36
41
  });
37
42
 
38
43
  const existing = merged.get(id);
@@ -66,7 +71,7 @@ export function updateKeyframeCacheFromParsed(
66
71
  }
67
72
  }
68
73
 
69
- export function buildCacheKey(sourceFile: string, elementId: string): string {
74
+ function buildCacheKey(sourceFile: string, elementId: string): string {
70
75
  return `${sourceFile}#${elementId}`;
71
76
  }
72
77