@hyperframes/studio 0.7.75 → 0.7.77
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-BrfzYqX3.js → hyperframes-player-CEggaaxR.js} +1 -1
- package/dist/assets/{index-BvGKYVrH.js → index-B1INQNmj.js} +1 -1
- package/dist/assets/{index-CjGZkNxS.js → index-B1Tjjdse.js} +1 -1
- package/dist/assets/index-Bp4jAYZG.js +428 -0
- package/dist/assets/index-CBAfprvx.css +1 -0
- package/dist/index.d.ts +64 -17
- package/dist/index.html +2 -2
- package/dist/index.js +18040 -16293
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +16 -17
- package/src/components/EditorShell.tsx +3 -0
- package/src/components/TimelineToolbar.test.tsx +48 -3
- package/src/components/TimelineToolbar.tsx +89 -36
- package/src/components/editor/AnimationCard.test.tsx +18 -75
- package/src/components/editor/AnimationCard.tsx +26 -2
- package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
- package/src/components/editor/GsapAnimationSection.tsx +18 -157
- package/src/components/editor/KeyframeEaseList.tsx +5 -1
- package/src/components/editor/KeyframeNavigation.tsx +46 -14
- package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
- package/src/components/editor/MotionPathOverlay.tsx +32 -10
- package/src/components/editor/PropertyPanel.tsx +15 -5
- package/src/components/editor/PropertyPanelFlat.tsx +48 -23
- package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
- package/src/components/editor/keyframeRetime.test.ts +25 -22
- package/src/components/editor/keyframeRetime.ts +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
- package/src/components/nle/PreviewOverlays.tsx +3 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
- package/src/contexts/TimelineEditContext.tsx +1 -1
- package/src/hooks/deleteSelectedKeyframes.ts +24 -8
- package/src/hooks/gsapDragCommit.ts +2 -2
- package/src/hooks/gsapDragPositionCommit.ts +84 -5
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
- package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
- package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
- package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
- package/src/hooks/gsapScriptCommitTypes.ts +2 -0
- package/src/hooks/gsapShared.test.ts +109 -1
- package/src/hooks/gsapShared.ts +142 -17
- package/src/hooks/gsapTweenSynth.test.ts +30 -1
- package/src/hooks/gsapTweenSynth.ts +61 -14
- package/src/hooks/keyframeCacheAstLoad.ts +181 -0
- package/src/hooks/timelineEditingHelpers.test.ts +80 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -2
- package/src/hooks/useDomEditWiring.ts +2 -2
- package/src/hooks/useDomSelection.ts +4 -0
- package/src/hooks/useEnableKeyframes.test.ts +122 -0
- package/src/hooks/useEnableKeyframes.ts +116 -56
- package/src/hooks/useGestureCommit.ts +2 -2
- package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
- package/src/hooks/useGsapKeyframeOps.ts +30 -13
- package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
- package/src/hooks/useGsapScriptCommits.ts +2 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
- package/src/hooks/useGsapSelectionHandlers.ts +90 -65
- package/src/hooks/useGsapTweenCache.test.ts +16 -0
- package/src/hooks/useGsapTweenCache.ts +64 -178
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
- package/src/hooks/useStudioContextValue.test.ts +93 -0
- package/src/hooks/useStudioContextValue.ts +17 -3
- package/src/hooks/useStudioTestHooks.ts +63 -0
- package/src/hooks/useTimelineEditing.test.tsx +187 -22
- package/src/hooks/useTimelineEditing.ts +43 -32
- package/src/hooks/useTimelineEditingTypes.ts +2 -0
- package/src/hooks/useTimelineGroupEditing.ts +106 -82
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
- package/src/player/components/LayerDisclosureRow.tsx +76 -0
- package/src/player/components/Timeline.test.ts +420 -90
- package/src/player/components/Timeline.tsx +80 -78
- package/src/player/components/TimelineCanvas.tsx +33 -21
- package/src/player/components/TimelineClip.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
- package/src/player/components/TimelineClipDiamonds.tsx +320 -117
- package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
- package/src/player/components/TimelineLanes.tsx +153 -79
- package/src/player/components/TimelineOverlays.tsx +4 -18
- package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
- package/src/player/components/TimelinePropertyLanes.tsx +191 -0
- package/src/player/components/TimelineRazorInteraction.tsx +60 -0
- package/src/player/components/TimelineRuler.tsx +11 -21
- package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
- package/src/player/components/TimelineTrackHeader.tsx +363 -0
- package/src/player/components/TrackClipCount.tsx +17 -0
- package/src/player/components/timelineCallbacks.ts +21 -6
- package/src/player/components/timelineClipDragPreview.test.ts +50 -3
- package/src/player/components/timelineClipDragPreview.ts +33 -15
- package/src/player/components/timelineCollision.ts +4 -13
- package/src/player/components/timelineDiamondTypes.ts +120 -0
- package/src/player/components/timelineDragDrop.ts +8 -3
- package/src/player/components/timelineEditing.test.ts +19 -27
- package/src/player/components/timelineEditing.ts +7 -8
- package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
- package/src/player/components/timelineKeyframeIdentity.ts +66 -0
- package/src/player/components/timelineLayout.test.ts +162 -9
- package/src/player/components/timelineLayout.ts +198 -38
- package/src/player/components/timelineMarquee.test.ts +76 -21
- package/src/player/components/timelineMarquee.ts +17 -14
- package/src/player/components/trackHeaderLaneState.ts +121 -0
- package/src/player/components/trackHeaderLaneValues.ts +139 -0
- package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
- package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
- package/src/player/components/useTimelineClipDrag.ts +4 -1
- package/src/player/components/useTimelineGeometry.ts +4 -1
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
- package/src/player/components/useTimelinePlayhead.ts +21 -12
- package/src/player/components/useTimelineRangeSelection.ts +35 -14
- package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
- package/src/player/components/useTimelineTrackLayout.ts +162 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
- package/src/player/lib/timelineElementHelpers.ts +26 -11
- package/src/player/store/keyframeSlice.ts +122 -0
- package/src/player/store/playerStore.test.ts +25 -0
- package/src/player/store/playerStore.ts +7 -43
- package/src/telemetry/events.test.ts +9 -3
- package/src/telemetry/events.ts +16 -7
- package/src/utils/globalTimeCompiler.test.ts +10 -0
- package/src/utils/globalTimeCompiler.ts +3 -3
- package/dist/assets/index-B37rWXo4.css +0 -1
- package/dist/assets/index-DzDajONI.js +0 -428
- package/src/player/components/TimelineDragGhost.tsx +0 -59
- package/src/utils/keyframeSelection.test.ts +0 -45
- package/src/utils/keyframeSelection.ts +0 -29
|
@@ -19,8 +19,11 @@ import {
|
|
|
19
19
|
shouldAutoScrollTimeline,
|
|
20
20
|
} from "./Timeline";
|
|
21
21
|
import {
|
|
22
|
+
CLIP_Y,
|
|
22
23
|
FIT_ZOOM_HEADROOM,
|
|
23
24
|
GUTTER,
|
|
25
|
+
LABEL_COL_W,
|
|
26
|
+
LANE_H,
|
|
24
27
|
MIN_TIMELINE_EXTENT_S,
|
|
25
28
|
PLAYHEAD_HEAD_W,
|
|
26
29
|
RULER_H,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
TRACKS_LEFT_PAD,
|
|
29
32
|
getTimelineDisplayContentWidth,
|
|
30
33
|
getTimelineFitPps,
|
|
34
|
+
getTimelineLaneTop,
|
|
31
35
|
} from "./timelineLayout";
|
|
32
36
|
import { formatTime } from "../lib/time";
|
|
33
37
|
import { usePlayerStore } from "../store/playerStore";
|
|
@@ -40,52 +44,216 @@ afterEach(() => {
|
|
|
40
44
|
usePlayerStore.getState().reset();
|
|
41
45
|
});
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
function getHorizontalGeometry(host: HTMLElement, clipId: string, tickLabel: string) {
|
|
48
|
+
const clip = host.querySelector<HTMLElement>(`[data-el-id="${clipId}"]`);
|
|
49
|
+
if (!clip) throw new Error(`Missing timeline clip ${clipId}`);
|
|
50
|
+
const trackContent = clip.parentElement;
|
|
51
|
+
if (!trackContent) throw new Error(`Missing content row for ${clipId}`);
|
|
52
|
+
const trackHeader = trackContent.previousElementSibling;
|
|
53
|
+
if (!(trackHeader instanceof HTMLElement)) throw new Error(`Missing track header for ${clipId}`);
|
|
54
|
+
const rulerTickLabel = Array.from(host.querySelectorAll("span")).find(
|
|
55
|
+
(node) => node.textContent === tickLabel,
|
|
56
|
+
);
|
|
57
|
+
const rulerTick = rulerTickLabel?.parentElement;
|
|
58
|
+
if (!rulerTick) throw new Error(`Missing ruler tick ${tickLabel}`);
|
|
59
|
+
const ruler = rulerTick.parentElement;
|
|
60
|
+
if (!ruler) throw new Error("Missing timeline ruler");
|
|
61
|
+
const rulerOrigin = ruler.previousElementSibling;
|
|
62
|
+
if (!(rulerOrigin instanceof HTMLElement)) throw new Error("Missing timeline ruler origin");
|
|
63
|
+
const playhead = Array.from(host.querySelectorAll<HTMLElement>("div")).find(
|
|
64
|
+
(node) => node.style.zIndex === "100",
|
|
65
|
+
);
|
|
66
|
+
if (!playhead) throw new Error("Missing timeline playhead");
|
|
67
|
+
return { clip, trackHeader, rulerTick, rulerOrigin, playhead };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderTimelineGeometry(clipId: string) {
|
|
71
|
+
const host = document.createElement("div");
|
|
72
|
+
document.body.append(host);
|
|
73
|
+
const root = createRoot(host);
|
|
74
|
+
act(() => {
|
|
75
|
+
root.render(React.createElement(Timeline));
|
|
76
|
+
});
|
|
77
|
+
return { host, root, ...getHorizontalGeometry(host, clipId, "00:10") };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createSizedTimelineHost(width: number): HTMLDivElement {
|
|
81
|
+
const host = document.createElement("div");
|
|
82
|
+
document.body.append(host);
|
|
83
|
+
Object.defineProperty(host, "clientWidth", { configurable: true, value: width });
|
|
84
|
+
return host;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expectTrackExpansion(
|
|
88
|
+
row: HTMLElement | null | undefined,
|
|
89
|
+
expandedClipIds: string[],
|
|
90
|
+
height: number,
|
|
91
|
+
) {
|
|
92
|
+
expect(usePlayerStore.getState().expandedClipIds).toEqual(new Set(expandedClipIds));
|
|
93
|
+
expect(row?.style.height).toBe(`${height}px`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderBasicTimeline() {
|
|
97
|
+
const host = createSizedTimelineHost(640);
|
|
98
|
+
usePlayerStore.setState({
|
|
99
|
+
duration: 4,
|
|
100
|
+
timelineReady: true,
|
|
101
|
+
elements: [{ id: "clip-1", tag: "div", start: 0, duration: 2, track: 0 }],
|
|
102
|
+
});
|
|
103
|
+
const root = createRoot(host);
|
|
104
|
+
act(() => {
|
|
105
|
+
root.render(React.createElement(Timeline));
|
|
106
|
+
});
|
|
107
|
+
return { host, root };
|
|
108
|
+
}
|
|
52
109
|
|
|
110
|
+
describe("Timeline provider boundary", () => {
|
|
111
|
+
it("keeps all-collapsed horizontal positions at the gutter plus the pre-t=0 pad", () => {
|
|
53
112
|
usePlayerStore.setState({
|
|
54
|
-
duration:
|
|
113
|
+
duration: 11,
|
|
55
114
|
timelineReady: true,
|
|
56
|
-
|
|
115
|
+
currentTime: 10,
|
|
116
|
+
zoomMode: "manual",
|
|
117
|
+
manualZoomPercent: 100,
|
|
118
|
+
elements: [{ id: "clip-1", tag: "div", start: 10, duration: 1, track: 0 }],
|
|
57
119
|
});
|
|
58
120
|
|
|
59
|
-
const root =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
const { root, clip, trackHeader, rulerTick, rulerOrigin, playhead } =
|
|
122
|
+
renderTimelineGeometry("clip-1");
|
|
123
|
+
|
|
124
|
+
expect(trackHeader.style.width).toBe(`${GUTTER + TRACKS_LEFT_PAD}px`);
|
|
125
|
+
expect(clip.style.left).toBe("1000px");
|
|
126
|
+
expect(clip.style.height).toBe("");
|
|
127
|
+
expect(clip.style.bottom).toBe(`${CLIP_Y}px`);
|
|
128
|
+
expect(rulerOrigin.style.width).toBe(`${GUTTER + TRACKS_LEFT_PAD}px`);
|
|
129
|
+
expect(rulerTick.style.left).toBe("999.5px");
|
|
130
|
+
expect(playhead.style.left).toBe(`${GUTTER + TRACKS_LEFT_PAD + 1000 - PLAYHEAD_HEAD_W / 2}px`);
|
|
131
|
+
expect(playhead.style.width).toBe(`${PLAYHEAD_HEAD_W}px`);
|
|
132
|
+
expect(
|
|
133
|
+
resolveTimelineAssetDrop(
|
|
134
|
+
{
|
|
135
|
+
rectLeft: 100,
|
|
136
|
+
rectTop: 0,
|
|
137
|
+
scrollLeft: 0,
|
|
138
|
+
scrollTop: 0,
|
|
139
|
+
contentOrigin: GUTTER,
|
|
140
|
+
pixelsPerSecond: 100,
|
|
141
|
+
duration: 60,
|
|
142
|
+
trackOrder: [0],
|
|
143
|
+
},
|
|
144
|
+
1132,
|
|
145
|
+
100,
|
|
146
|
+
).start,
|
|
147
|
+
).toBe(10);
|
|
148
|
+
expect(getTimelineFitPps(640, 11, GUTTER)).toBe(10.1);
|
|
66
149
|
|
|
67
150
|
act(() => root.unmount());
|
|
68
151
|
});
|
|
69
152
|
|
|
70
|
-
|
|
71
|
-
it("renders the gutter without legacy icons or hue dots", () => {
|
|
72
|
-
const host = document.createElement("div");
|
|
73
|
-
document.body.append(host);
|
|
74
|
-
Object.defineProperty(host, "clientWidth", {
|
|
75
|
-
configurable: true,
|
|
76
|
-
value: 640,
|
|
77
|
-
});
|
|
78
|
-
|
|
153
|
+
it("reserves the label column and keeps expanded keyframes aligned with ruler time", () => {
|
|
79
154
|
usePlayerStore.setState({
|
|
80
|
-
duration:
|
|
155
|
+
duration: 20,
|
|
81
156
|
timelineReady: true,
|
|
82
|
-
|
|
157
|
+
currentTime: 10,
|
|
158
|
+
zoomMode: "manual",
|
|
159
|
+
manualZoomPercent: 100,
|
|
160
|
+
selectedElementId: "clip-1",
|
|
161
|
+
expandedClipIds: new Set(["clip-1"]),
|
|
162
|
+
elements: [
|
|
163
|
+
{ id: "clip-1", label: "Hero card", tag: "div", start: 0, duration: 20, track: 0 },
|
|
164
|
+
{ id: "clip-2", label: "Outro", tag: "div", start: 2, duration: 1, track: 1 },
|
|
165
|
+
],
|
|
166
|
+
gsapAnimations: new Map([
|
|
167
|
+
[
|
|
168
|
+
"clip-1",
|
|
169
|
+
[
|
|
170
|
+
{
|
|
171
|
+
id: "position-tween",
|
|
172
|
+
targetSelector: "#clip-1",
|
|
173
|
+
method: "to",
|
|
174
|
+
position: 0,
|
|
175
|
+
duration: 20,
|
|
176
|
+
properties: {},
|
|
177
|
+
propertyGroup: "position",
|
|
178
|
+
keyframes: {
|
|
179
|
+
format: "percentage",
|
|
180
|
+
keyframes: [{ percentage: 50, properties: { x: 100 } }],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
],
|
|
185
|
+
]),
|
|
83
186
|
});
|
|
84
187
|
|
|
85
|
-
const root =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
188
|
+
const { host, root, clip, trackHeader, rulerTick, rulerOrigin, playhead } =
|
|
189
|
+
renderTimelineGeometry("clip-1");
|
|
190
|
+
const { trackHeader: collapsedHeader } = getHorizontalGeometry(host, "clip-2", "00:10");
|
|
191
|
+
const diamond = host.querySelector<HTMLElement>(
|
|
192
|
+
'[data-keyframe-group="position"][data-keyframe-percentage="50"]',
|
|
193
|
+
);
|
|
194
|
+
if (!diamond) throw new Error("Missing expanded position keyframe");
|
|
195
|
+
const propertyLane = diamond.closest<HTMLElement>("[data-timeline-property-lane]");
|
|
196
|
+
if (!propertyLane) throw new Error("Missing flat position property lane");
|
|
197
|
+
const headerLane = trackHeader.querySelector<HTMLElement>('[data-property-group="position"]');
|
|
198
|
+
if (!headerLane) throw new Error("Missing position property header");
|
|
199
|
+
// Absolute x rebuilds from the content origin (the ruler-origin spacer),
|
|
200
|
+
// which now insets a GUTTER past the LABEL_COL_W label column so a 0%
|
|
201
|
+
// diamond has room to its left. The content row reaches that same origin via
|
|
202
|
+
// header (LABEL_COL_W) + its gutter margin, so ruler tick and diamond still
|
|
203
|
+
// coincide on the shared time x.
|
|
204
|
+
const contentOrigin = Number.parseFloat(rulerOrigin.style.width);
|
|
205
|
+
const rulerX = contentOrigin + Number.parseFloat(rulerTick.style.left) + 0.5;
|
|
206
|
+
const diamondX =
|
|
207
|
+
contentOrigin +
|
|
208
|
+
Number.parseFloat(propertyLane.style.left) +
|
|
209
|
+
Number.parseFloat(diamond.style.left) +
|
|
210
|
+
Number.parseFloat(diamond.style.width) / 2;
|
|
211
|
+
|
|
212
|
+
expect(clip.contains(propertyLane)).toBe(false);
|
|
213
|
+
expect(clip.style.height).toBe(`${TRACK_H - 2 * CLIP_Y}px`);
|
|
214
|
+
expect(clip.style.bottom).toBe("");
|
|
215
|
+
expect(propertyLane.style.top).toBe(`${getTimelineLaneTop(0)}px`);
|
|
216
|
+
expect(propertyLane.style.top).toBe(headerLane.style.top);
|
|
217
|
+
expect(propertyLane.style.background).toBe("");
|
|
218
|
+
expect(propertyLane.style.border).toBe("");
|
|
219
|
+
expect(propertyLane.style.borderRadius).toBe("");
|
|
220
|
+
expect(trackHeader.style.width).toBe(`${LABEL_COL_W}px`);
|
|
221
|
+
expect(rulerOrigin.style.width).toBe(`${LABEL_COL_W + GUTTER}px`);
|
|
222
|
+
expect(playhead.style.left).toBe(`${LABEL_COL_W + GUTTER + 1000 - PLAYHEAD_HEAD_W / 2}px`);
|
|
223
|
+
expect(diamondX).toBe(rulerX);
|
|
224
|
+
expect(rulerX).toBe(LABEL_COL_W + GUTTER + 1000);
|
|
225
|
+
expect(collapsedHeader.textContent).toContain("Outro");
|
|
226
|
+
expect(getTimelineFitPps(640, 20, LABEL_COL_W + GUTTER)).toBeCloseTo(
|
|
227
|
+
(640 - (LABEL_COL_W + GUTTER) - 2) / MIN_TIMELINE_EXTENT_S,
|
|
228
|
+
);
|
|
229
|
+
expect(
|
|
230
|
+
resolveTimelineAssetDrop(
|
|
231
|
+
{
|
|
232
|
+
rectLeft: 100,
|
|
233
|
+
rectTop: 0,
|
|
234
|
+
scrollLeft: 0,
|
|
235
|
+
scrollTop: 0,
|
|
236
|
+
contentOrigin: LABEL_COL_W + GUTTER,
|
|
237
|
+
pixelsPerSecond: 100,
|
|
238
|
+
duration: 60,
|
|
239
|
+
trackOrder: [0],
|
|
240
|
+
},
|
|
241
|
+
100 + LABEL_COL_W + GUTTER + 1000,
|
|
242
|
+
100,
|
|
243
|
+
).start,
|
|
244
|
+
).toBe(10);
|
|
245
|
+
|
|
246
|
+
act(() => root.unmount());
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("renders the public Timeline export without TimelineEditProvider", () => {
|
|
250
|
+
const { root } = renderBasicTimeline();
|
|
251
|
+
|
|
252
|
+
act(() => root.unmount());
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("renders the gutter without legacy icons or hue dots", () => {
|
|
256
|
+
const { host, root } = renderBasicTimeline();
|
|
89
257
|
|
|
90
258
|
const hueDot = Array.from(host.querySelectorAll("div")).find(
|
|
91
259
|
(node) =>
|
|
@@ -99,14 +267,8 @@ describe("Timeline provider boundary", () => {
|
|
|
99
267
|
act(() => root.unmount());
|
|
100
268
|
});
|
|
101
269
|
|
|
102
|
-
// fallow-ignore-next-line code-duplication
|
|
103
270
|
it("requests persisted track visibility from the gutter without seeking", () => {
|
|
104
|
-
const host =
|
|
105
|
-
document.body.append(host);
|
|
106
|
-
Object.defineProperty(host, "clientWidth", {
|
|
107
|
-
configurable: true,
|
|
108
|
-
value: 640,
|
|
109
|
-
});
|
|
271
|
+
const host = createSizedTimelineHost(640);
|
|
110
272
|
|
|
111
273
|
usePlayerStore.setState({
|
|
112
274
|
duration: 4,
|
|
@@ -153,8 +315,8 @@ describe("Timeline provider boundary", () => {
|
|
|
153
315
|
});
|
|
154
316
|
|
|
155
317
|
const row = button.parentElement?.parentElement;
|
|
156
|
-
// Row children: [sticky
|
|
157
|
-
const trackContent = row?.children.item(
|
|
318
|
+
// Row children: [TimelineTrackHeader (sticky column), time-mapped content].
|
|
319
|
+
const trackContent = row?.children.item(1);
|
|
158
320
|
expect(onToggleTrackHidden).toHaveBeenCalledWith(0, false);
|
|
159
321
|
expect(trackContent).toBeInstanceOf(HTMLElement);
|
|
160
322
|
if (!(trackContent instanceof HTMLElement)) {
|
|
@@ -165,14 +327,49 @@ describe("Timeline provider boundary", () => {
|
|
|
165
327
|
act(() => root.unmount());
|
|
166
328
|
});
|
|
167
329
|
|
|
168
|
-
it("
|
|
169
|
-
const host =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
330
|
+
it("splits all tracks once when shift-clicking the timeline with the razor", () => {
|
|
331
|
+
const host = createSizedTimelineHost(640);
|
|
332
|
+
usePlayerStore.setState({
|
|
333
|
+
activeTool: "razor",
|
|
334
|
+
duration: 4,
|
|
335
|
+
timelineReady: true,
|
|
336
|
+
elements: [{ id: "clip-1", tag: "div", start: 0, duration: 2, track: 0 }],
|
|
174
337
|
});
|
|
175
338
|
|
|
339
|
+
const onRazorSplitAll = vi.fn();
|
|
340
|
+
const root = createRoot(host);
|
|
341
|
+
act(() => {
|
|
342
|
+
root.render(
|
|
343
|
+
React.createElement(
|
|
344
|
+
TimelineEditProvider,
|
|
345
|
+
{ value: { onRazorSplitAll } },
|
|
346
|
+
React.createElement(Timeline),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const viewport = host.querySelector('[aria-label="Timeline"]')?.firstElementChild;
|
|
352
|
+
expect(viewport).toBeInstanceOf(HTMLElement);
|
|
353
|
+
act(() => {
|
|
354
|
+
viewport?.dispatchEvent(
|
|
355
|
+
new MouseEvent("pointerdown", {
|
|
356
|
+
bubbles: true,
|
|
357
|
+
cancelable: true,
|
|
358
|
+
button: 0,
|
|
359
|
+
clientX: 240,
|
|
360
|
+
shiftKey: true,
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(onRazorSplitAll).toHaveBeenCalledTimes(1);
|
|
366
|
+
expect(onRazorSplitAll).toHaveBeenCalledWith(expect.any(Number));
|
|
367
|
+
act(() => root.unmount());
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("opens the keyframe context menu without seeking to that keyframe", () => {
|
|
371
|
+
const host = createSizedTimelineHost(720);
|
|
372
|
+
|
|
176
373
|
usePlayerStore.setState({
|
|
177
374
|
duration: 4,
|
|
178
375
|
timelineReady: true,
|
|
@@ -215,14 +412,101 @@ describe("Timeline provider boundary", () => {
|
|
|
215
412
|
act(() => root.unmount());
|
|
216
413
|
});
|
|
217
414
|
|
|
218
|
-
it("
|
|
219
|
-
const host =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
415
|
+
it("shows a disclosure only for grouped keyframes and toggles the track height", () => {
|
|
416
|
+
const host = createSizedTimelineHost(720);
|
|
417
|
+
|
|
418
|
+
usePlayerStore.setState({
|
|
419
|
+
duration: 4,
|
|
420
|
+
timelineReady: true,
|
|
421
|
+
elements: [
|
|
422
|
+
{ id: "clip-1", tag: "div", start: 0, duration: 2, track: 0 },
|
|
423
|
+
{ id: "clip-2", tag: "div", start: 2, duration: 2, track: 1 },
|
|
424
|
+
],
|
|
425
|
+
keyframeCache: new Map([
|
|
426
|
+
[
|
|
427
|
+
"clip-1",
|
|
428
|
+
{
|
|
429
|
+
format: "percentage",
|
|
430
|
+
keyframes: [
|
|
431
|
+
{ percentage: 0, properties: { x: 0 }, propertyGroup: "position" },
|
|
432
|
+
{ percentage: 50, properties: { x: 100 }, propertyGroup: "position" },
|
|
433
|
+
{ percentage: 100, properties: { opacity: 0 }, propertyGroup: "visual" },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
]),
|
|
438
|
+
gsapAnimations: new Map([
|
|
439
|
+
[
|
|
440
|
+
"clip-1",
|
|
441
|
+
[
|
|
442
|
+
{
|
|
443
|
+
id: "clip-1-position",
|
|
444
|
+
targetSelector: "#clip-1",
|
|
445
|
+
method: "to",
|
|
446
|
+
position: 0,
|
|
447
|
+
duration: 2,
|
|
448
|
+
properties: {},
|
|
449
|
+
propertyGroup: "position",
|
|
450
|
+
keyframes: {
|
|
451
|
+
format: "percentage",
|
|
452
|
+
keyframes: [
|
|
453
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
454
|
+
{ percentage: 50, properties: { x: 100 } },
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: "clip-1-visual",
|
|
460
|
+
targetSelector: "#clip-1",
|
|
461
|
+
method: "to",
|
|
462
|
+
position: 0,
|
|
463
|
+
duration: 2,
|
|
464
|
+
properties: {},
|
|
465
|
+
propertyGroup: "visual",
|
|
466
|
+
keyframes: {
|
|
467
|
+
format: "percentage",
|
|
468
|
+
keyframes: [{ percentage: 100, properties: { opacity: 0 } }],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
],
|
|
473
|
+
]),
|
|
224
474
|
});
|
|
225
475
|
|
|
476
|
+
const root = createRoot(host);
|
|
477
|
+
act(() => {
|
|
478
|
+
root.render(React.createElement(Timeline));
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Keyframed clip-1 is expanded by default (AE/Figma default); its disclosure
|
|
482
|
+
// lives in the left column. clip-2 has no keyframes so it never shows one.
|
|
483
|
+
const collapseButton = host.querySelector<HTMLButtonElement>(
|
|
484
|
+
'button[aria-label="Collapse clip-1 keyframes"]',
|
|
485
|
+
);
|
|
486
|
+
expect(collapseButton).not.toBeNull();
|
|
487
|
+
expect(host.querySelector('button[aria-label="Expand clip-2 keyframes"]')).toBeNull();
|
|
488
|
+
expect(host.querySelector('button[aria-label="Collapse clip-2 keyframes"]')).toBeNull();
|
|
489
|
+
|
|
490
|
+
const clip = host.querySelector<HTMLElement>('[data-el-id="clip-1"]');
|
|
491
|
+
const row = clip?.parentElement?.parentElement;
|
|
492
|
+
expectTrackExpansion(row, ["clip-1"], TRACK_H + 2 * LANE_H);
|
|
493
|
+
|
|
494
|
+
// Collapsing sticks (does not bounce back open via auto-expand).
|
|
495
|
+
act(() => collapseButton?.click());
|
|
496
|
+
expectTrackExpansion(row, [], TRACK_H);
|
|
497
|
+
|
|
498
|
+
const expandButton = host.querySelector<HTMLButtonElement>(
|
|
499
|
+
'button[aria-label="Expand clip-1 keyframes"]',
|
|
500
|
+
);
|
|
501
|
+
expect(expandButton).not.toBeNull();
|
|
502
|
+
act(() => expandButton?.click());
|
|
503
|
+
expectTrackExpansion(row, ["clip-1"], TRACK_H + 2 * LANE_H);
|
|
504
|
+
act(() => root.unmount());
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("marks every clip in selectedElementIds as selected", () => {
|
|
508
|
+
const host = createSizedTimelineHost(720);
|
|
509
|
+
|
|
226
510
|
usePlayerStore.setState({
|
|
227
511
|
duration: 6,
|
|
228
512
|
timelineReady: true,
|
|
@@ -461,23 +745,29 @@ describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
|
461
745
|
it("computes fit pps against the 60s floor for short compositions", () => {
|
|
462
746
|
// A 10s comp maps 60s onto the viewport → the comp takes ~1/6 of the width.
|
|
463
747
|
// (10 * 1.2 = 12s of headroom-padded content is still under the 60s floor.)
|
|
464
|
-
const pps = getTimelineFitPps(viewport, 10);
|
|
465
|
-
expect(pps).toBeCloseTo((viewport - GUTTER
|
|
466
|
-
expect(10 * pps).toBeCloseTo((viewport - GUTTER
|
|
748
|
+
const pps = getTimelineFitPps(viewport, 10, GUTTER + TRACKS_LEFT_PAD);
|
|
749
|
+
expect(pps).toBeCloseTo((viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / MIN_TIMELINE_EXTENT_S);
|
|
750
|
+
expect(10 * pps).toBeCloseTo((viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / 6);
|
|
467
751
|
});
|
|
468
752
|
|
|
469
753
|
it("fits duration * FIT_ZOOM_HEADROOM (not the bare duration) for long compositions", () => {
|
|
470
|
-
expect(getTimelineFitPps(viewport, 60)).toBeCloseTo(
|
|
471
|
-
(viewport - GUTTER
|
|
754
|
+
expect(getTimelineFitPps(viewport, 60, GUTTER + TRACKS_LEFT_PAD)).toBeCloseTo(
|
|
755
|
+
(viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
756
|
+
);
|
|
757
|
+
expect(getTimelineFitPps(viewport, 120, GUTTER + TRACKS_LEFT_PAD)).toBeCloseTo(
|
|
758
|
+
(viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
472
759
|
);
|
|
473
|
-
|
|
474
|
-
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("subtracts the expanded keyframe label column before fitting headroom", () => {
|
|
763
|
+
expect(getTimelineFitPps(viewport, 120, LABEL_COL_W)).toBeCloseTo(
|
|
764
|
+
(viewport - LABEL_COL_W - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
475
765
|
);
|
|
476
766
|
});
|
|
477
767
|
|
|
478
768
|
it("leaves CapCut-style trailing headroom: the comp ends at 1/1.2 of the usable width", () => {
|
|
479
|
-
const usable = viewport - GUTTER
|
|
480
|
-
const pps = getTimelineFitPps(viewport, 120);
|
|
769
|
+
const usable = viewport - (GUTTER + TRACKS_LEFT_PAD) - 2;
|
|
770
|
+
const pps = getTimelineFitPps(viewport, 120, GUTTER + TRACKS_LEFT_PAD);
|
|
481
771
|
// Composition content occupies usable/1.2 px; the remaining ~17% is empty
|
|
482
772
|
// droppable ruler/lane surface past the end.
|
|
483
773
|
expect(120 * pps).toBeCloseTo(usable / FIT_ZOOM_HEADROOM);
|
|
@@ -485,17 +775,17 @@ describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
|
485
775
|
});
|
|
486
776
|
|
|
487
777
|
it("falls back to 100 pps before the viewport is measured", () => {
|
|
488
|
-
expect(getTimelineFitPps(0, 10)).toBe(100);
|
|
489
|
-
expect(getTimelineFitPps(GUTTER
|
|
490
|
-
expect(getTimelineFitPps(Number.NaN, 10)).toBe(100);
|
|
778
|
+
expect(getTimelineFitPps(0, 10, GUTTER)).toBe(100);
|
|
779
|
+
expect(getTimelineFitPps(GUTTER, 10, GUTTER)).toBe(100);
|
|
780
|
+
expect(getTimelineFitPps(Number.NaN, 10, GUTTER)).toBe(100);
|
|
491
781
|
});
|
|
492
782
|
|
|
493
783
|
it("uses the floor for zero/invalid durations", () => {
|
|
494
|
-
expect(getTimelineFitPps(viewport, 0)).toBeCloseTo(
|
|
495
|
-
(viewport - GUTTER
|
|
784
|
+
expect(getTimelineFitPps(viewport, 0, GUTTER + TRACKS_LEFT_PAD)).toBeCloseTo(
|
|
785
|
+
(viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / MIN_TIMELINE_EXTENT_S,
|
|
496
786
|
);
|
|
497
|
-
expect(getTimelineFitPps(viewport, Number.NaN)).toBeCloseTo(
|
|
498
|
-
(viewport - GUTTER
|
|
787
|
+
expect(getTimelineFitPps(viewport, Number.NaN, GUTTER + TRACKS_LEFT_PAD)).toBeCloseTo(
|
|
788
|
+
(viewport - (GUTTER + TRACKS_LEFT_PAD) - 2) / MIN_TIMELINE_EXTENT_S,
|
|
499
789
|
);
|
|
500
790
|
});
|
|
501
791
|
});
|
|
@@ -504,14 +794,24 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
504
794
|
it("always spans at least MIN_TIMELINE_EXTENT_S seconds of content", () => {
|
|
505
795
|
// 10s of content at 20 pps = 200px; the floor keeps 60s (1200px) rendered.
|
|
506
796
|
expect(
|
|
507
|
-
getTimelineDisplayContentWidth({
|
|
797
|
+
getTimelineDisplayContentWidth({
|
|
798
|
+
trackContentWidth: 200,
|
|
799
|
+
viewportWidth: 400,
|
|
800
|
+
contentOrigin: GUTTER,
|
|
801
|
+
pps: 20,
|
|
802
|
+
}),
|
|
508
803
|
).toBe(MIN_TIMELINE_EXTENT_S * 20);
|
|
509
804
|
});
|
|
510
805
|
|
|
511
806
|
it("still fills the viewport when that is larger than the 60s floor", () => {
|
|
512
807
|
expect(
|
|
513
|
-
getTimelineDisplayContentWidth({
|
|
514
|
-
|
|
808
|
+
getTimelineDisplayContentWidth({
|
|
809
|
+
trackContentWidth: 200,
|
|
810
|
+
viewportWidth: 2000,
|
|
811
|
+
contentOrigin: GUTTER + TRACKS_LEFT_PAD,
|
|
812
|
+
pps: 5,
|
|
813
|
+
}),
|
|
814
|
+
).toBe(2000 - (GUTTER + TRACKS_LEFT_PAD) - 2);
|
|
515
815
|
});
|
|
516
816
|
|
|
517
817
|
it("tracks a drag ghost past every other bound (drag-to-extend)", () => {
|
|
@@ -519,6 +819,7 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
519
819
|
getTimelineDisplayContentWidth({
|
|
520
820
|
trackContentWidth: 500,
|
|
521
821
|
viewportWidth: 400,
|
|
822
|
+
contentOrigin: GUTTER,
|
|
522
823
|
pps: 5,
|
|
523
824
|
dragGhostEndPx: 5000,
|
|
524
825
|
}),
|
|
@@ -530,6 +831,7 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
530
831
|
getTimelineDisplayContentWidth({
|
|
531
832
|
trackContentWidth: 500,
|
|
532
833
|
viewportWidth: 400,
|
|
834
|
+
contentOrigin: GUTTER,
|
|
533
835
|
pps: 5,
|
|
534
836
|
resizeGhostEndPx: 4200,
|
|
535
837
|
}),
|
|
@@ -538,7 +840,12 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
538
840
|
|
|
539
841
|
it("keeps long content authoritative", () => {
|
|
540
842
|
expect(
|
|
541
|
-
getTimelineDisplayContentWidth({
|
|
843
|
+
getTimelineDisplayContentWidth({
|
|
844
|
+
trackContentWidth: 9000,
|
|
845
|
+
viewportWidth: 400,
|
|
846
|
+
contentOrigin: GUTTER,
|
|
847
|
+
pps: 50,
|
|
848
|
+
}),
|
|
542
849
|
).toBe(9000);
|
|
543
850
|
});
|
|
544
851
|
});
|
|
@@ -565,7 +872,7 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
565
872
|
getTimelineScrollLeftForZoomAnchor({
|
|
566
873
|
pointerX: 300,
|
|
567
874
|
currentScrollLeft: 200,
|
|
568
|
-
|
|
875
|
+
contentOrigin: GUTTER,
|
|
569
876
|
currentPixelsPerSecond: 10,
|
|
570
877
|
nextPixelsPerSecond: 20,
|
|
571
878
|
duration: 120,
|
|
@@ -578,7 +885,7 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
578
885
|
getTimelineScrollLeftForZoomAnchor({
|
|
579
886
|
pointerX: 300,
|
|
580
887
|
currentScrollLeft: 0,
|
|
581
|
-
|
|
888
|
+
contentOrigin: GUTTER,
|
|
582
889
|
currentPixelsPerSecond: 20,
|
|
583
890
|
nextPixelsPerSecond: 5,
|
|
584
891
|
duration: 120,
|
|
@@ -591,7 +898,7 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
591
898
|
getTimelineScrollLeftForZoomAnchor({
|
|
592
899
|
pointerX: 300,
|
|
593
900
|
currentScrollLeft: 120,
|
|
594
|
-
|
|
901
|
+
contentOrigin: GUTTER,
|
|
595
902
|
currentPixelsPerSecond: 0,
|
|
596
903
|
nextPixelsPerSecond: 20,
|
|
597
904
|
duration: 120,
|
|
@@ -601,38 +908,59 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
601
908
|
});
|
|
602
909
|
|
|
603
910
|
describe("getTimelinePlayheadLeft", () => {
|
|
604
|
-
it("offsets the wrapper by half the head width so the line CENTER =
|
|
911
|
+
it("offsets the wrapper by half the head width so the line CENTER = contentOrigin + t*pps", () => {
|
|
605
912
|
// Wrapper left + PLAYHEAD_HEAD_W/2 (where the 1px line is centered) must
|
|
606
|
-
// equal
|
|
607
|
-
|
|
913
|
+
// equal contentOrigin + t*pps at any zoom, for both the padded default
|
|
914
|
+
// origin and the plain gutter origin.
|
|
915
|
+
expect(getTimelinePlayheadLeft(4, 20, GUTTER + TRACKS_LEFT_PAD) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
608
916
|
GUTTER + TRACKS_LEFT_PAD + 4 * 20,
|
|
609
917
|
);
|
|
610
|
-
expect(getTimelinePlayheadLeft(10, 7.5) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
918
|
+
expect(getTimelinePlayheadLeft(10, 7.5, GUTTER + TRACKS_LEFT_PAD) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
611
919
|
GUTTER + TRACKS_LEFT_PAD + 75,
|
|
612
920
|
);
|
|
921
|
+
expect(getTimelinePlayheadLeft(4, 20, GUTTER) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER + 4 * 20);
|
|
922
|
+
expect(getTimelinePlayheadLeft(10, 7.5, GUTTER) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER + 75);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("uses the expanded keyframe label column as the playhead origin", () => {
|
|
926
|
+
expect(getTimelinePlayheadLeft(4, 20, LABEL_COL_W) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
927
|
+
LABEL_COL_W + 4 * 20,
|
|
928
|
+
);
|
|
613
929
|
});
|
|
614
930
|
|
|
615
931
|
it("centers the line exactly on the left pad's end (the 00:00 tick) at t = 0", () => {
|
|
616
|
-
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
932
|
+
expect(getTimelinePlayheadLeft(0, 20, GUTTER + TRACKS_LEFT_PAD) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
933
|
+
GUTTER + TRACKS_LEFT_PAD,
|
|
934
|
+
);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("centers the line exactly on the gutter (the 00:00 tick) at t = 0", () => {
|
|
938
|
+
expect(getTimelinePlayheadLeft(0, 20, GUTTER) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER);
|
|
617
939
|
});
|
|
618
940
|
|
|
619
941
|
it("guards invalid input", () => {
|
|
620
|
-
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
942
|
+
expect(getTimelinePlayheadLeft(Number.NaN, 20, GUTTER + TRACKS_LEFT_PAD)).toBe(
|
|
621
943
|
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
622
944
|
);
|
|
623
|
-
expect(getTimelinePlayheadLeft(4, Number.NaN)).toBe(
|
|
945
|
+
expect(getTimelinePlayheadLeft(4, Number.NaN, GUTTER + TRACKS_LEFT_PAD)).toBe(
|
|
624
946
|
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
625
947
|
);
|
|
948
|
+
expect(getTimelinePlayheadLeft(Number.NaN, 20, GUTTER)).toBe(GUTTER - PLAYHEAD_HEAD_W / 2);
|
|
949
|
+
expect(getTimelinePlayheadLeft(4, Number.NaN, LABEL_COL_W)).toBe(
|
|
950
|
+
LABEL_COL_W - PLAYHEAD_HEAD_W / 2,
|
|
951
|
+
);
|
|
626
952
|
});
|
|
627
953
|
});
|
|
628
954
|
|
|
629
955
|
describe("getTimelineCanvasHeight", () => {
|
|
630
956
|
it("includes bottom scroll buffer below the last track", () => {
|
|
631
|
-
expect(getTimelineCanvasHeight(
|
|
957
|
+
expect(getTimelineCanvasHeight([TRACK_H, TRACK_H, TRACK_H])).toBeGreaterThan(
|
|
958
|
+
RULER_H + 3 * TRACK_H,
|
|
959
|
+
);
|
|
632
960
|
});
|
|
633
961
|
|
|
634
962
|
it("still keeps ruler space when there are no tracks", () => {
|
|
635
|
-
expect(getTimelineCanvasHeight(
|
|
963
|
+
expect(getTimelineCanvasHeight([])).toBeGreaterThan(24);
|
|
636
964
|
});
|
|
637
965
|
});
|
|
638
966
|
|
|
@@ -686,14 +1014,15 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
686
1014
|
rectTop: 200,
|
|
687
1015
|
scrollLeft: 0,
|
|
688
1016
|
scrollTop: 0,
|
|
1017
|
+
contentOrigin: GUTTER,
|
|
689
1018
|
pixelsPerSecond: 100,
|
|
690
1019
|
duration: 10,
|
|
691
1020
|
trackHeight: 72,
|
|
692
1021
|
trackOrder: [0, 3, 7],
|
|
693
1022
|
},
|
|
694
|
-
|
|
695
|
-
// clientY
|
|
696
|
-
//
|
|
1023
|
+
432, // rectLeft(100) + GUTTER(32) + 3s*100pps (contentOrigin = GUTTER)
|
|
1024
|
+
// clientY: rectTop(200) + RULER_H(24) + TRACKS_TOP_PAD(72) + TRACK_H(48)
|
|
1025
|
+
// + TRACK_H/2(24) = 368 → row 1 → track 3.
|
|
697
1026
|
368,
|
|
698
1027
|
),
|
|
699
1028
|
).toEqual({ start: 3, track: 3 });
|
|
@@ -707,12 +1036,13 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
707
1036
|
rectTop: 200,
|
|
708
1037
|
scrollLeft: 0,
|
|
709
1038
|
scrollTop: 0,
|
|
1039
|
+
contentOrigin: GUTTER,
|
|
710
1040
|
pixelsPerSecond: 100,
|
|
711
1041
|
duration: 10,
|
|
712
1042
|
trackHeight: 72,
|
|
713
1043
|
trackOrder: [0, 3, 7],
|
|
714
1044
|
},
|
|
715
|
-
250 +
|
|
1045
|
+
250, // rectLeft(100) + GUTTER(32) + 1.18s*100pps (contentOrigin = GUTTER)
|
|
716
1046
|
600,
|
|
717
1047
|
),
|
|
718
1048
|
).toEqual({ start: 1.18, track: 8 });
|