@hyperframes/studio 0.8.93 → 0.8.94
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.
- package/dist/assets/{hyperframes-player-CkV9KK6A.js → hyperframes-player-Bkm-3G56.js} +1 -1
- package/dist/assets/{index-BlLFipOk.js → index-BUn5H_np.js} +1 -1
- package/dist/assets/{index-CYk0NwSv.js → index-BupvpoiM.js} +175 -175
- package/dist/assets/{index-V3fzEfOD.js → index-n2ZRiO44.js} +1 -1
- package/dist/index.d.ts +62 -46
- package/dist/index.html +1 -1
- package/dist/index.js +142 -98
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +17 -0
- package/src/components/TimelineToolbar.tsx +3 -1
- package/src/components/editor/ConnectedDomEditOverlay.test.tsx +2 -0
- package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -1
- package/src/components/editor/DomEditCropHandles.test.tsx +87 -1
- package/src/components/editor/DomEditCropHandles.tsx +64 -62
- package/src/components/editor/DomEditOverlay.test.ts +39 -0
- package/src/components/editor/DomEditOverlay.tsx +6 -1
- package/src/components/editor/DomEditSelectionChrome.test.tsx +114 -1
- package/src/components/editor/DomEditSelectionChrome.tsx +17 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +19 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +1 -1
- package/src/components/editor/SnapToolbar.tsx +1 -1
- package/src/components/editor/domEditOverlayGeometry.ts +2 -0
- package/src/components/nle/PreviewOverlays.tsx +3 -3
- package/src/hooks/gsapResizeIntercept.test.ts +40 -0
- package/src/hooks/gsapResizeIntercept.ts +8 -4
- package/src/index.ts +4 -0
- package/src/player/components/PlayerControls.tsx +2 -15
- package/src/player/components/ShortcutsPanel.test.tsx +33 -1
- package/src/player/components/ShortcutsPanel.tsx +26 -0
- package/src/player/components/Timeline.trackPadding.test.tsx +157 -0
- package/src/player/components/TimelineCanvas.tsx +16 -19
- package/src/player/components/TimelineTypes.ts +2 -0
- package/src/player/components/timelineLayout.test.ts +47 -0
- package/src/player/components/timelineLayout.ts +22 -15
- package/src/player/components/useTimelineProviderState.tsx +2 -0
- package/src/player/components/useTimelineTrackLayout.ts +11 -2
- package/src/player/components/useTimelineVirtualRows.test.tsx +32 -0
- package/src/player/components/useTimelineVirtualRows.ts +2 -2
|
@@ -247,6 +247,53 @@ describe("track-area breathing pad y-math", () => {
|
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
describe("host track padding", () => {
|
|
251
|
+
const geometry = createTimelineRowGeometry([0, 1, 2], baseRows(3), { top: 0, bottom: TRACK_H });
|
|
252
|
+
// Row 1 with no top pad, but still row 0 if any reader keeps the default pad.
|
|
253
|
+
const yInRow1 = RULER_H + TRACK_H + 10;
|
|
254
|
+
|
|
255
|
+
it("moves the first row up and sizes the canvas from the host pads", () => {
|
|
256
|
+
expect(geometry.getRowTop(0)).toBe(RULER_H);
|
|
257
|
+
expect(getTimelineRowTop(1, geometry.rowHeights)).toBe(RULER_H + TRACK_H);
|
|
258
|
+
expect(getTimelineCanvasHeight(geometry.rowHeights)).toBe(RULER_H + 4 * TRACK_H);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("maps a drag pointer y to the row under it", () => {
|
|
262
|
+
expect(Math.floor(geometry.getRowFromY(yInRow1))).toBe(1);
|
|
263
|
+
expect(Math.floor(getTimelineRowFromY(yInRow1, geometry.rowHeights))).toBe(1);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("drops onto the row under the pointer, and a new track in the bottom pad", () => {
|
|
267
|
+
const drop = {
|
|
268
|
+
rectLeft: 0,
|
|
269
|
+
rectTop: 0,
|
|
270
|
+
scrollLeft: 0,
|
|
271
|
+
scrollTop: 0,
|
|
272
|
+
contentOrigin: GUTTER,
|
|
273
|
+
pixelsPerSecond: 100,
|
|
274
|
+
rowHeights: geometry.rowHeights,
|
|
275
|
+
trackOrder: [0, 1, 2],
|
|
276
|
+
};
|
|
277
|
+
expect(resolveTimelineAssetDrop(drop, GUTTER, yInRow1).track).toBe(1);
|
|
278
|
+
const inBottomPad = RULER_H + 3 * TRACK_H + TRACK_H / 2;
|
|
279
|
+
expect(resolveTimelineAssetDrop(drop, GUTTER, inBottomPad).track).toBe(3);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("clamps a negative pad to 0 and replaces a non-finite one with the default", () => {
|
|
283
|
+
const odd = createTimelineRowGeometry([0], baseRows(1), { top: -20, bottom: Number.NaN });
|
|
284
|
+
expect(odd.padding).toEqual({ top: 0, bottom: TRACKS_BOTTOM_PAD });
|
|
285
|
+
expect(odd.getRowTop(0)).toBe(RULER_H);
|
|
286
|
+
expect(odd.canvasHeight).toBe(RULER_H + TRACK_H + TRACKS_BOTTOM_PAD);
|
|
287
|
+
const infinite = createTimelineRowGeometry([0], baseRows(1), { top: Infinity });
|
|
288
|
+
expect(infinite.padding.top).toBe(TRACKS_TOP_PAD);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("keeps the default pads when a host passes none", () => {
|
|
292
|
+
const defaults = createTimelineRowGeometry([0], baseRows(1));
|
|
293
|
+
expect(defaults.padding).toEqual({ top: TRACKS_TOP_PAD, bottom: TRACKS_BOTTOM_PAD });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
250
297
|
describe("getTimelineScrubTime", () => {
|
|
251
298
|
const at = (clientX: number, duration = 10) =>
|
|
252
299
|
getTimelineScrubTime({
|
|
@@ -55,9 +55,8 @@ export function getTimelineLaneTop(laneIndex: number): number {
|
|
|
55
55
|
return TRACK_H + Math.max(0, Math.trunc(laneIndex)) * LANE_H;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* offset; a track row's top is always ruler + top pad + cumulative row heights.
|
|
58
|
+
* Default breathing room INSIDE the scroll area (CapCut-style). A host overrides
|
|
59
|
+
* it per Timeline; rows always read TimelineRowGeometry.padding, never these.
|
|
61
60
|
*
|
|
62
61
|
* - TRACKS_TOP_PAD: empty space between the (sticky) ruler and the first track
|
|
63
62
|
* (~half a track height) so the first clip isn't jammed under the ruler.
|
|
@@ -66,6 +65,10 @@ export function getTimelineLaneTop(laneIndex: number): number {
|
|
|
66
65
|
*/
|
|
67
66
|
export const TRACKS_TOP_PAD = 50;
|
|
68
67
|
export const TRACKS_BOTTOM_PAD = Math.round(TRACK_H * 1.5);
|
|
68
|
+
export interface TimelineTrackPadding {
|
|
69
|
+
top?: number;
|
|
70
|
+
bottom?: number;
|
|
71
|
+
}
|
|
69
72
|
/**
|
|
70
73
|
* Breathing room LEFT of t=0 (CapCut-style), inside the scroll content — the
|
|
71
74
|
* horizontal sibling of TRACKS_TOP_PAD: empty lane surface between the sticky
|
|
@@ -115,12 +118,17 @@ function validRowHeight(height: number | undefined): number {
|
|
|
115
118
|
return height;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function validPad(pad: number | undefined, fallback: number): number {
|
|
122
|
+
return pad !== undefined && Number.isFinite(pad) ? Math.max(0, pad) : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
export interface TimelineRowGeometry {
|
|
119
126
|
readonly rowKeys: readonly number[];
|
|
120
127
|
readonly rowHeights: readonly number[];
|
|
121
128
|
/** Cumulative row boundaries, including the final bottom boundary. */
|
|
122
129
|
readonly rowOffsets: readonly number[];
|
|
123
130
|
readonly rowsHeight: number;
|
|
131
|
+
readonly padding: Readonly<Required<TimelineTrackPadding>>;
|
|
124
132
|
readonly canvasHeight: number;
|
|
125
133
|
getRowIndex(rowKey: number): number;
|
|
126
134
|
getRowHeight(row: number): number;
|
|
@@ -141,7 +149,10 @@ const EMPTY_ROW_HEIGHTS: readonly number[] = Object.freeze([]);
|
|
|
141
149
|
export function createTimelineRowGeometry(
|
|
142
150
|
rowKeys: readonly number[],
|
|
143
151
|
rowHeights: readonly number[],
|
|
152
|
+
padding: TimelineTrackPadding = {},
|
|
144
153
|
): TimelineRowGeometry {
|
|
154
|
+
const topPad = validPad(padding.top, TRACKS_TOP_PAD);
|
|
155
|
+
const bottomPad = validPad(padding.bottom, TRACKS_BOTTOM_PAD);
|
|
145
156
|
const heights = Object.freeze(rowHeights.map(validRowHeight));
|
|
146
157
|
const keys = Object.freeze(
|
|
147
158
|
heights.map((_, row) => {
|
|
@@ -165,7 +176,7 @@ export function createTimelineRowGeometry(
|
|
|
165
176
|
return (offsets[wholeRow] ?? 0) + (row - wholeRow) * getRowHeight(wholeRow);
|
|
166
177
|
};
|
|
167
178
|
const getRowFromY = (contentY: number) => {
|
|
168
|
-
const y = contentY - RULER_H -
|
|
179
|
+
const y = contentY - RULER_H - topPad;
|
|
169
180
|
if (heights.length === 0) return y / TRACK_H;
|
|
170
181
|
if (y < 0) return y / getRowHeight(0);
|
|
171
182
|
const rowsHeight = offsets[heights.length] ?? 0;
|
|
@@ -188,10 +199,11 @@ export function createTimelineRowGeometry(
|
|
|
188
199
|
rowHeights: heights,
|
|
189
200
|
rowOffsets: offsets,
|
|
190
201
|
rowsHeight: offsets.at(-1) ?? 0,
|
|
191
|
-
|
|
202
|
+
padding: Object.freeze({ top: topPad, bottom: bottomPad }),
|
|
203
|
+
canvasHeight: RULER_H + topPad + (offsets.at(-1) ?? 0) + bottomPad,
|
|
192
204
|
getRowIndex: (rowKey) => rowIndexByKey.get(rowKey) ?? -1,
|
|
193
205
|
getRowHeight,
|
|
194
|
-
getRowTop: (row) => RULER_H +
|
|
206
|
+
getRowTop: (row) => RULER_H + topPad + getRowOffset(row),
|
|
195
207
|
getRowFromY,
|
|
196
208
|
getRowPositionFromY: (contentY) => {
|
|
197
209
|
const rowFloat = getRowFromY(contentY);
|
|
@@ -204,7 +216,7 @@ export function createTimelineRowGeometry(
|
|
|
204
216
|
return frozenGeometry;
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
/** Compatibility accessor
|
|
219
|
+
/** Compatibility accessor: a geometry's own rowHeights resolves back to it, pads included. */
|
|
208
220
|
export function getTimelineRowGeometry(rowHeights: readonly number[]): TimelineRowGeometry {
|
|
209
221
|
const cached = rowGeometryCache.get(rowHeights);
|
|
210
222
|
if (cached) return cached;
|
|
@@ -228,10 +240,6 @@ export function getTimelineRowHeight(
|
|
|
228
240
|
return validRowHeight(rowHeights[row]);
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
function getTimelineRowOffset(row: number, rowHeights: readonly number[]): number {
|
|
232
|
-
return getTimelineRowGeometry(rowHeights).getRowTop(row) - RULER_H - TRACKS_TOP_PAD;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
243
|
/**
|
|
236
244
|
* The y (content-space) of the top edge of track ROW index `row` (0 = first
|
|
237
245
|
* displayed lane). The single source of truth for row->y: the ruler height plus
|
|
@@ -243,7 +251,7 @@ export function getTimelineRowTop(
|
|
|
243
251
|
row: number,
|
|
244
252
|
rowHeights: readonly number[] = EMPTY_ROW_HEIGHTS,
|
|
245
253
|
): number {
|
|
246
|
-
return
|
|
254
|
+
return getTimelineRowGeometry(rowHeights).getRowTop(row);
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
/**
|
|
@@ -484,9 +492,8 @@ export function getTimelinePlaybackFollowScrollLeft(input: {
|
|
|
484
492
|
}
|
|
485
493
|
|
|
486
494
|
export function getTimelineCanvasHeight(rowHeights: readonly number[]): number {
|
|
487
|
-
// RULER_H + top pad + lanes + bottom pad
|
|
488
|
-
//
|
|
489
|
-
// below the last lane is real scrollable surface, not a hidden buffer.
|
|
495
|
+
// RULER_H + top pad + lanes + bottom pad, read from the geometry's padding.
|
|
496
|
+
// The drag-into-void space below the last lane is real scrollable surface.
|
|
490
497
|
return getTimelineRowGeometry(rowHeights).canvasHeight;
|
|
491
498
|
}
|
|
492
499
|
|
|
@@ -65,6 +65,7 @@ export function useTimelineProviderState({
|
|
|
65
65
|
theme: themeOverrides,
|
|
66
66
|
showAudioEffects = true,
|
|
67
67
|
showKeyframes = true,
|
|
68
|
+
trackPadding,
|
|
68
69
|
sessionEpoch = 0,
|
|
69
70
|
previewIframeRef,
|
|
70
71
|
onZIndexReorder,
|
|
@@ -143,6 +144,7 @@ export function useTimelineProviderState({
|
|
|
143
144
|
gsapAnimations,
|
|
144
145
|
selectedElementId,
|
|
145
146
|
selectedElementIds,
|
|
147
|
+
trackPadding,
|
|
146
148
|
);
|
|
147
149
|
const timelineElementsRef = useRef(timelineElements);
|
|
148
150
|
timelineElementsRef.current = timelineElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
TRACK_H,
|
|
11
11
|
createTimelineRowGeometry,
|
|
12
12
|
type TimelineRowGeometry,
|
|
13
|
+
type TimelineTrackPadding,
|
|
13
14
|
trackHeights,
|
|
14
15
|
type TimelineTrackHeightClip,
|
|
15
16
|
} from "./timelineLayout";
|
|
@@ -164,7 +165,10 @@ function useTimelineRowHeights(
|
|
|
164
165
|
selectedElementId: string | null,
|
|
165
166
|
selectedElementIds: ReadonlySet<string>,
|
|
166
167
|
groups: readonly TimelineTrackGroupInfo[],
|
|
168
|
+
trackPadding: TimelineTrackPadding | undefined,
|
|
167
169
|
) {
|
|
170
|
+
const padTop = trackPadding?.top;
|
|
171
|
+
const padBottom = trackPadding?.bottom;
|
|
168
172
|
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
169
173
|
const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
|
|
170
174
|
const { laneCounts, rowGeometry } = useMemo(() => {
|
|
@@ -207,9 +211,12 @@ function useTimelineRowHeights(
|
|
|
207
211
|
rowGeometry: createTimelineRowGeometry(
|
|
208
212
|
tracks.map(([track]) => track),
|
|
209
213
|
rowHeights,
|
|
214
|
+
{ top: padTop, bottom: padBottom },
|
|
210
215
|
),
|
|
211
216
|
};
|
|
212
217
|
}, [
|
|
218
|
+
padTop,
|
|
219
|
+
padBottom,
|
|
213
220
|
expandedClipIds,
|
|
214
221
|
expandedLaneOwnerIds,
|
|
215
222
|
gsapAnimations,
|
|
@@ -233,6 +240,7 @@ export function useTimelineTrackLayout(
|
|
|
233
240
|
gsapAnimations: Map<string, GsapAnimation[]>,
|
|
234
241
|
selectedElementId: string | null,
|
|
235
242
|
selectedElementIds: ReadonlySet<string>,
|
|
243
|
+
trackPadding?: TimelineTrackPadding,
|
|
236
244
|
) {
|
|
237
245
|
const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
|
|
238
246
|
useTimelineTrackDerivations(expandedElements);
|
|
@@ -244,6 +252,7 @@ export function useTimelineTrackLayout(
|
|
|
244
252
|
selectedElementId,
|
|
245
253
|
selectedElementIds,
|
|
246
254
|
groups,
|
|
255
|
+
trackPadding,
|
|
247
256
|
);
|
|
248
257
|
|
|
249
258
|
return {
|
|
@@ -305,8 +314,8 @@ export function useTimelineDisplayLayout(
|
|
|
305
314
|
const displayTrackOrder = useDisplayTrackOrder(draggedClip, trackOrder);
|
|
306
315
|
const displayRowHeights = useDisplayRowHeights(displayTrackOrder, rowGeometry);
|
|
307
316
|
const displayRowGeometry = useMemo(
|
|
308
|
-
() => createTimelineRowGeometry(displayTrackOrder, displayRowHeights),
|
|
309
|
-
[displayTrackOrder, displayRowHeights],
|
|
317
|
+
() => createTimelineRowGeometry(displayTrackOrder, displayRowHeights, rowGeometry.padding),
|
|
318
|
+
[displayTrackOrder, displayRowHeights, rowGeometry.padding],
|
|
310
319
|
);
|
|
311
320
|
return {
|
|
312
321
|
displayTrackOrder,
|
|
@@ -171,4 +171,36 @@ describe("useTimelineVirtualRows", () => {
|
|
|
171
171
|
|
|
172
172
|
act(() => root.unmount());
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
it("offsets the mounted range by the geometry's own top pad", () => {
|
|
176
|
+
const keys = Array.from({ length: 1_000 }, (_, index) => index);
|
|
177
|
+
const topPad = 200 * 48;
|
|
178
|
+
const geometry = createTimelineRowGeometry(
|
|
179
|
+
keys,
|
|
180
|
+
keys.map(() => 48),
|
|
181
|
+
{ top: topPad },
|
|
182
|
+
);
|
|
183
|
+
const scroll = createScrollElement(RULER_H + topPad + 300 * 48);
|
|
184
|
+
const scrollRef = { current: scroll };
|
|
185
|
+
let rows: ReturnType<typeof useTimelineVirtualRows> = [];
|
|
186
|
+
|
|
187
|
+
function Probe() {
|
|
188
|
+
rows = useTimelineVirtualRows({
|
|
189
|
+
enabled: true,
|
|
190
|
+
scrollRef,
|
|
191
|
+
viewport: viewport(scroll),
|
|
192
|
+
rowGeometry: geometry,
|
|
193
|
+
sessionEpoch: 1,
|
|
194
|
+
pinnedRowKeys: [],
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const root = createRoot(document.createElement("div"));
|
|
200
|
+
act(() => root.render(React.createElement(Probe)));
|
|
201
|
+
act(() => {});
|
|
202
|
+
expect(rows.some((row) => row.index === 300)).toBe(true);
|
|
203
|
+
expect(rows.some((row) => row.index === 500)).toBe(false);
|
|
204
|
+
act(() => root.unmount());
|
|
205
|
+
});
|
|
174
206
|
});
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, type RefObject } from "react";
|
|
|
2
2
|
import { defaultRangeExtractor, useVirtualizer, type Range } from "@tanstack/react-virtual";
|
|
3
3
|
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
|
|
4
4
|
import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
|
|
5
|
-
import { RULER_H,
|
|
5
|
+
import { RULER_H, type TimelineRowGeometry } from "./timelineLayout";
|
|
6
6
|
|
|
7
7
|
export interface TimelineVirtualRow {
|
|
8
8
|
readonly index: number;
|
|
@@ -84,7 +84,7 @@ export function useTimelineVirtualRows({
|
|
|
84
84
|
getItemKey,
|
|
85
85
|
overscan: TIMELINE_VIEWPORT_BUDGETS.rowOverscanPerSide,
|
|
86
86
|
rangeExtractor,
|
|
87
|
-
scrollMargin: RULER_H +
|
|
87
|
+
scrollMargin: RULER_H + rowGeometry.padding.top,
|
|
88
88
|
initialRect: { width: viewport.clientWidth, height: viewport.clientHeight },
|
|
89
89
|
initialOffset,
|
|
90
90
|
});
|