@hyperframes/studio 0.6.97 → 0.6.98

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 (120) hide show
  1. package/dist/assets/hyperframes-player-DgsMQSvV.js +418 -0
  2. package/dist/assets/index-B62bDCQv.css +1 -0
  3. package/dist/assets/index-Ce3pBm_I.js +252 -0
  4. package/dist/assets/{index-HveJ0MuV.js → index-D-ET9M0b.js} +1 -1
  5. package/dist/assets/index-D-bS9Dxx.js +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +7 -5
  8. package/src/App.tsx +182 -177
  9. package/src/captions/store.ts +11 -11
  10. package/src/components/StudioHeader.tsx +4 -4
  11. package/src/components/StudioLeftSidebar.tsx +2 -2
  12. package/src/components/StudioPreviewArea.tsx +225 -183
  13. package/src/components/StudioRightPanel.tsx +3 -3
  14. package/src/components/TimelineToolbar.tsx +25 -0
  15. package/src/components/editor/DomEditOverlay.tsx +2 -5
  16. package/src/components/editor/EaseCurveSection.tsx +2 -3
  17. package/src/components/editor/GestureTrailOverlay.tsx +4 -3
  18. package/src/components/editor/LayersPanel.tsx +3 -9
  19. package/src/components/editor/PropertyPanel.tsx +20 -61
  20. package/src/components/editor/colorValue.ts +3 -1
  21. package/src/components/editor/domEditOverlayGestures.ts +54 -1
  22. package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
  23. package/src/components/editor/gradientValue.ts +3 -3
  24. package/src/components/editor/keyframeMove.test.ts +101 -0
  25. package/src/components/editor/keyframeMove.ts +151 -0
  26. package/src/components/editor/manualEditsDom.ts +0 -12
  27. package/src/components/editor/propertyPanelHelpers.ts +10 -38
  28. package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
  29. package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
  30. package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
  31. package/src/components/editor/studioMotionOps.test.ts +1 -1
  32. package/src/components/editor/studioMotionOps.ts +2 -1
  33. package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
  34. package/src/components/nle/NLELayout.tsx +1 -24
  35. package/src/components/sidebar/BlocksTab.tsx +2 -2
  36. package/src/contexts/DomEditContext.tsx +134 -31
  37. package/src/contexts/StudioContext.tsx +90 -40
  38. package/src/contexts/TimelineEditContext.tsx +47 -0
  39. package/src/hooks/domEditCommitTypes.ts +14 -0
  40. package/src/hooks/gsapDragCommit.ts +9 -24
  41. package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
  42. package/src/hooks/gsapKeyframeCommit.ts +5 -15
  43. package/src/hooks/gsapRuntimeBridge.ts +18 -52
  44. package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
  45. package/src/hooks/gsapRuntimeReaders.ts +19 -26
  46. package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
  47. package/src/hooks/gsapScriptCommitTypes.ts +58 -0
  48. package/src/hooks/gsapShared.ts +157 -0
  49. package/src/hooks/timelineEditingHelpers.ts +63 -2
  50. package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
  51. package/src/hooks/useAppHotkeys.ts +299 -377
  52. package/src/hooks/useConsoleErrorCapture.ts +33 -5
  53. package/src/hooks/useDomEditCommits.ts +35 -293
  54. package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
  55. package/src/hooks/useDomEditSession.ts +78 -249
  56. package/src/hooks/useDomEditTextCommits.ts +1 -1
  57. package/src/hooks/useDomEditWiring.ts +255 -0
  58. package/src/hooks/useDomGeometryCommits.ts +181 -0
  59. package/src/hooks/useDomSelection.ts +10 -27
  60. package/src/hooks/useEditorSave.ts +82 -0
  61. package/src/hooks/useElementLifecycleOps.ts +177 -0
  62. package/src/hooks/useEnableKeyframes.ts +10 -15
  63. package/src/hooks/useFileManager.ts +32 -114
  64. package/src/hooks/useFileTree.ts +80 -0
  65. package/src/hooks/useGestureCommit.ts +7 -5
  66. package/src/hooks/useGestureRecording.ts +1 -1
  67. package/src/hooks/useGsapAnimationOps.ts +122 -0
  68. package/src/hooks/useGsapArcPathOps.ts +61 -0
  69. package/src/hooks/useGsapAwareEditing.ts +242 -0
  70. package/src/hooks/useGsapKeyframeOps.ts +167 -0
  71. package/src/hooks/useGsapPropertyDebounce.ts +135 -0
  72. package/src/hooks/useGsapScriptCommits.ts +58 -570
  73. package/src/hooks/useGsapSelectionHandlers.ts +22 -9
  74. package/src/hooks/useGsapTweenCache.ts +35 -29
  75. package/src/hooks/useLintModal.ts +7 -0
  76. package/src/hooks/useMusicBeatAnalysis.ts +152 -0
  77. package/src/hooks/useRazorSplit.ts +1 -1
  78. package/src/hooks/useRenderClipContent.ts +46 -21
  79. package/src/hooks/useTimelineEditing.ts +48 -4
  80. package/src/player/components/AudioWaveform.tsx +29 -4
  81. package/src/player/components/BeatStrip.tsx +166 -0
  82. package/src/player/components/Timeline.tsx +39 -18
  83. package/src/player/components/TimelineCanvas.tsx +52 -12
  84. package/src/player/components/TimelineClipDiamonds.tsx +130 -20
  85. package/src/player/components/TimelinePropertyRows.tsx +8 -2
  86. package/src/player/components/TimelineRuler.tsx +36 -2
  87. package/src/player/components/timelineEditing.ts +30 -5
  88. package/src/player/components/useTimelineClipDrag.ts +155 -4
  89. package/src/player/components/useTimelinePlayhead.ts +30 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +47 -45
  91. package/src/player/lib/mediaProbe.ts +46 -3
  92. package/src/player/lib/playbackScrub.ts +16 -0
  93. package/src/player/lib/timelineDOM.ts +10 -2
  94. package/src/player/lib/timelineIframeHelpers.ts +89 -0
  95. package/src/player/store/playerStore.ts +92 -33
  96. package/src/utils/beatEditActions.ts +109 -0
  97. package/src/utils/beatEditing.ts +136 -0
  98. package/src/utils/clipboardPayload.ts +3 -2
  99. package/src/utils/compositionPatterns.ts +2 -0
  100. package/src/utils/keyframeSelection.test.ts +45 -0
  101. package/src/utils/keyframeSelection.ts +29 -0
  102. package/src/utils/rounding.ts +9 -0
  103. package/src/utils/studioHelpers.ts +5 -2
  104. package/src/utils/studioUrlState.ts +2 -1
  105. package/src/utils/timelineAssetDrop.ts +6 -5
  106. package/src/utils/timelineInspector.ts +15 -100
  107. package/dist/assets/hyperframes-player-Daj5djxa.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-Cfye9xzo.js +0 -251
  110. package/src/components/editor/DopesheetStrip.tsx +0 -141
  111. package/src/components/editor/StaggerControls.tsx +0 -61
  112. package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
  113. package/src/components/editor/TimelineLayerPanel.tsx +0 -15
  114. package/src/components/nle/TimelineEditorNotice.tsx +0 -133
  115. package/src/hooks/gsapRuntimePreview.ts +0 -19
  116. package/src/player/components/timelineUtils.ts +0 -211
  117. package/src/utils/audioBeatDetection.ts +0 -58
  118. package/src/utils/keyframeSnapping.test.ts +0 -74
  119. package/src/utils/keyframeSnapping.ts +0 -63
  120. package/src/utils/timelineInspector.test.ts +0 -79
@@ -1,6 +1,7 @@
1
- import type { TimelineElement } from "../player";
1
+ import type { TimelineElement } from "../player/store/playerStore";
2
2
  import type { DomEditSelection } from "../components/editor/domEditing";
3
3
  import type { TimelineAssetKind } from "./timelineAssetDrop";
4
+ import { roundToCenti } from "./rounding";
4
5
 
5
6
  export interface EditingFile {
6
7
  path: string;
@@ -171,6 +172,8 @@ export function clampNumber(value: number, min: number, max: number): number {
171
172
  return Math.min(Math.max(value, min), max);
172
173
  }
173
174
 
175
+ export { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
176
+
174
177
  export function collectHtmlIds(source: string): string[] {
175
178
  return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? "");
176
179
  }
@@ -205,7 +208,7 @@ export async function resolveDroppedAssetDuration(
205
208
  const raw = Number(media.duration);
206
209
  finalize(
207
210
  Number.isFinite(raw) && raw > 0
208
- ? Math.round(raw * 100) / 100
211
+ ? roundToCenti(raw)
209
212
  : DEFAULT_TIMELINE_ASSET_DURATION[kind],
210
213
  );
211
214
  },
@@ -1,6 +1,7 @@
1
1
  import type { RightPanelTab } from "./studioHelpers";
2
2
  import { buildProjectHash, parseProjectHashRoute } from "./projectRouting";
3
3
  import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
4
+ import { roundTo3 } from "./rounding";
4
5
 
5
6
  export interface StudioUrlSelectionState {
6
7
  sourceFile?: string;
@@ -111,7 +112,7 @@ export function buildStudioHash(projectId: string, state: StudioUrlState): strin
111
112
  params.set("v", "1");
112
113
  if (state.activeCompPath) params.set("comp", state.activeCompPath);
113
114
  if (state.currentTime != null && Number.isFinite(state.currentTime)) {
114
- params.set("t", String(Math.max(0, Math.round(state.currentTime * 1000) / 1000)));
115
+ params.set("t", String(Math.max(0, roundTo3(state.currentTime))));
115
116
  }
116
117
  if (state.rightPanelTab) params.set("tab", state.rightPanelTab);
117
118
  if (state.rightCollapsed != null) params.set("rc", state.rightCollapsed ? "1" : "0");
@@ -1,4 +1,6 @@
1
1
  import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes";
2
+ import { roundToCenti } from "./rounding";
3
+ import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
2
4
 
3
5
  export const TIMELINE_ASSET_MIME = "application/x-hyperframes-asset";
4
6
  export const TIMELINE_BLOCK_MIME = "application/x-hyperframes-block";
@@ -51,13 +53,13 @@ export function buildTimelineFileDropPlacements(
51
53
  durations: number[],
52
54
  occupiedClips: Array<{ start: number; duration: number; track: number }> = [],
53
55
  ): Array<{ start: number; track: number }> {
54
- let nextStart = Math.round(Math.max(0, placement.start) * 100) / 100;
56
+ let nextStart = roundToCenti(Math.max(0, placement.start));
55
57
  const sequenceStart = nextStart;
56
58
  const resolvedDurations = durations.map((duration) =>
57
59
  Number.isFinite(duration) && duration > 0 ? duration : FALLBACK_TIMELINE_FILE_DROP_DURATION,
58
60
  );
59
61
  const sequenceEnd = resolvedDurations.reduce(
60
- (end, duration) => Math.round((end + duration) * 100) / 100,
62
+ (end, duration) => roundToCenti(end + duration),
61
63
  sequenceStart,
62
64
  );
63
65
  const overlapsDropTrack = occupiedClips.some((clip) => {
@@ -72,7 +74,7 @@ export function buildTimelineFileDropPlacements(
72
74
 
73
75
  return resolvedDurations.map((duration) => {
74
76
  const start = nextStart;
75
- nextStart = Math.round((nextStart + duration) * 100) / 100;
77
+ nextStart = roundToCenti(nextStart + duration);
76
78
  return { start, track };
77
79
  });
78
80
  }
@@ -120,8 +122,7 @@ export function buildTimelineAssetInsertHtml(input: {
120
122
  }
121
123
 
122
124
  export function insertTimelineAssetIntoSource(source: string, assetHtml: string): string {
123
- const rootOpenTag = /<[^>]*data-composition-id="[^"]+"[^>]*>/i;
124
- const match = rootOpenTag.exec(source);
125
+ const match = COMPOSITION_ROOT_OPEN_TAG_RE.exec(source);
125
126
  if (!match || match.index == null) {
126
127
  throw new Error("No composition root found in target source");
127
128
  }
@@ -1,18 +1,10 @@
1
1
  import type { TimelineElement } from "../player";
2
2
 
3
- const TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS = 0.08;
4
-
5
3
  const AUDIO_TIMELINE_TAGS = new Set(["audio", "music", "sfx", "sound", "narration"]);
6
4
  const AUDIO_SOURCE_EXT_RE = /\.(aac|flac|m4a|mp3|ogg|opus|wav)(?:[?#].*)?$/i;
5
+ const MUSIC_ID_RE = /\b(music|bgm|soundtrack|background[-_]?music)\b/i;
7
6
 
8
- export function getTimelineElementKey(
9
- element: Pick<TimelineElement, "id" | "key"> | null | undefined,
10
- ): string | null {
11
- if (!element) return null;
12
- return element.key ?? element.id;
13
- }
14
-
15
- export function isAudioTimelineElement(
7
+ function isAudioTimelineElement(
16
8
  element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
17
9
  ): boolean {
18
10
  if (!element) return false;
@@ -21,96 +13,19 @@ export function isAudioTimelineElement(
21
13
  return Boolean(element.src && AUDIO_SOURCE_EXT_RE.test(element.src));
22
14
  }
23
15
 
24
- export function canInspectTimelineElement(
25
- element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
26
- ): boolean {
27
- return !isAudioTimelineElement(element);
28
- }
29
-
30
- export function shouldShowTimelineInspectorBounds(
31
- currentTime: number,
32
- element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
33
- epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
34
- ): boolean {
35
- if (!element) return false;
36
- if (!Number.isFinite(currentTime)) return false;
37
- if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
38
- const start = Math.max(0, element.start);
39
- const end = Math.max(start, start + Math.max(0, element.duration));
40
- const epsilon = Math.max(0, epsilonSeconds);
41
- return Math.abs(currentTime - start) <= epsilon || Math.abs(currentTime - end) <= epsilon;
42
- }
43
-
44
- export function isTimelineElementActiveAtTime(
45
- currentTime: number,
46
- element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
47
- epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
16
+ /** True for the music track: an audio element with data-timeline-role="music",
17
+ * or when no role is set an id matching the music regex. Voiceover/other
18
+ * audio (explicit non-music role) is excluded. */
19
+ export function isMusicTrack(
20
+ element:
21
+ | Pick<TimelineElement, "tag" | "src" | "id" | "domId" | "timelineRole">
22
+ | null
23
+ | undefined,
48
24
  ): boolean {
49
25
  if (!element) return false;
50
- if (!Number.isFinite(currentTime)) return false;
51
- if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
52
- const start = Math.max(0, element.start);
53
- const end = Math.max(start, start + Math.max(0, element.duration));
54
- const epsilon = Math.max(0, epsilonSeconds);
55
- return currentTime >= start - epsilon && currentTime <= end + epsilon;
56
- }
57
-
58
- export interface TimelineLayerVisibility {
59
- visible: boolean;
60
- compositeOpacity: number;
61
- hasBox: boolean;
62
- inViewport: boolean;
63
- }
64
-
65
- export function getTimelineLayerVisibilityInPreview(
66
- element: HTMLElement,
67
- options: { minCompositeOpacity?: number } = {},
68
- ): TimelineLayerVisibility {
69
- const hidden: TimelineLayerVisibility = {
70
- visible: false,
71
- compositeOpacity: 0,
72
- hasBox: false,
73
- inViewport: false,
74
- };
75
- if (!element.isConnected) return hidden;
76
- const doc = element.ownerDocument;
77
- const win = doc.defaultView;
78
- if (!win) return hidden;
79
-
80
- const minCompositeOpacity = options.minCompositeOpacity ?? 0.01;
81
- let compositeOpacity = 1;
82
- let current: HTMLElement | null = element;
83
- while (current && current !== doc.body && current !== doc.documentElement) {
84
- const style = win.getComputedStyle(current);
85
- if (style.display === "none" || style.visibility === "hidden") {
86
- return { ...hidden, compositeOpacity };
87
- }
88
- compositeOpacity *= Number.parseFloat(style.opacity || "1");
89
- if (compositeOpacity <= minCompositeOpacity) {
90
- return { ...hidden, compositeOpacity };
91
- }
92
- current = current.parentElement;
93
- }
94
-
95
- const rect = element.getBoundingClientRect();
96
- const hasBox = rect.width > 0.5 && rect.height > 0.5;
97
- if (!hasBox) return { visible: false, compositeOpacity, hasBox, inViewport: false };
98
-
99
- const viewportWidth = win.innerWidth || doc.documentElement.clientWidth;
100
- const viewportHeight = win.innerHeight || doc.documentElement.clientHeight;
101
- const inViewport =
102
- rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
103
- return {
104
- visible: inViewport,
105
- compositeOpacity,
106
- hasBox,
107
- inViewport,
108
- };
109
- }
110
-
111
- export function isTimelineLayerVisibleInPreview(
112
- element: HTMLElement,
113
- options: { minCompositeOpacity?: number } = {},
114
- ): boolean {
115
- return getTimelineLayerVisibilityInPreview(element, options).visible;
26
+ if (!isAudioTimelineElement(element)) return false;
27
+ if (element.timelineRole === "music") return true;
28
+ if (element.timelineRole && element.timelineRole !== "music") return false;
29
+ const id = element.domId ?? element.id ?? "";
30
+ return MUSIC_ID_RE.test(id);
116
31
  }