@hyperframes/studio 0.8.139 → 0.8.141
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-5Vx2AiUz.js +473 -0
- package/dist/assets/{index-DsEkE5_G.js → index-BAnh7cSQ.js} +1 -1
- package/dist/assets/{index-Br1Whc0f.js → index-C0thd4eM.js} +204 -204
- package/dist/assets/{index-BmIPrb8I.js → index-P8fgj_Xa.js} +1 -1
- package/dist/{chunk-E422JDF5.js → chunk-H2DUOXTF.js} +2 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.html +1 -1
- package/dist/index.js +6518 -5813
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +79 -1
- package/src/components/TimelineToolbar.tsx +14 -29
- package/src/components/editor/DomEditOverlay.tsx +2 -2
- package/src/components/editor/PropertyPanel.tsx +1 -0
- package/src/components/editor/PropertyPanelFlat.tsx +1 -0
- package/src/components/editor/manualEdits.ts +1 -1
- package/src/components/editor/manualEditsParsing.ts +14 -2
- package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
- package/src/components/editor/propertyPanelFill.tsx +36 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
- package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
- package/src/components/nle/NLEPreview.test.ts +60 -1
- package/src/components/nle/NLEPreview.tsx +9 -2
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +43 -0
- package/src/components/sidebar/CompositionsTab.tsx +17 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
- package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
- package/src/hooks/useMusicBeatAnalysis.ts +2 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
- package/src/hooks/useSdkSession.ts +4 -1
- package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
- package/src/hooks/useThumbnailStripSize.ts +23 -1
- package/src/index.ts +1 -0
- package/src/player/components/AudioWaveform.test.tsx +153 -1
- package/src/player/components/AudioWaveform.tsx +93 -37
- package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
- package/src/player/components/Timeline.test.ts +3 -1
- package/src/player/components/TimelineCanvas.tsx +4 -0
- package/src/player/components/TimelineClip.tsx +20 -6
- package/src/player/components/TimelineClipFades.test.tsx +11 -0
- package/src/player/components/TimelineClipFades.tsx +4 -4
- package/src/player/components/TimelineClipJoins.tsx +23 -18
- package/src/player/components/TimelineGestureOverlay.tsx +30 -27
- package/src/player/components/TimelineGroupRow.tsx +1 -0
- package/src/player/components/TimelineLaneClip.tsx +127 -0
- package/src/player/components/TimelineLanes.test.tsx +51 -6
- package/src/player/components/TimelineLanes.tsx +166 -178
- package/src/player/components/TimelineRuler.tsx +2 -0
- package/src/player/components/TimelineTimeLayer.tsx +25 -0
- package/src/player/components/VideoThumbnail.test.tsx +204 -12
- package/src/player/components/VideoThumbnail.tsx +37 -9
- package/src/player/components/thumbnailUtils.test.ts +17 -3
- package/src/player/components/thumbnailUtils.ts +17 -1
- package/src/player/components/timelineLayout.test.ts +3 -3
- package/src/player/components/timelineMotion.test.ts +27 -0
- package/src/player/components/timelineMotion.ts +62 -0
- package/src/player/components/timelineZoomInput.test.ts +438 -0
- package/src/player/components/timelineZoomInput.ts +456 -0
- package/src/player/components/useTimelineGeometry.ts +0 -3
- package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
- package/src/player/components/useTimelinePlayhead.ts +53 -59
- package/src/player/components/useTimelineProviderState.tsx +16 -21
- package/src/player/components/useTimelineZoom.ts +7 -12
- package/src/player/lib/runtimeProtocol.ts +6 -0
- package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
- package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
- package/dist/assets/hyperframes-player-CckOoCJ8.js +0 -473
- /package/dist/{chunk-E422JDF5.js.map → chunk-H2DUOXTF.js.map} +0 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { TIMELINE_VIEWPORT_BUDGETS, type TimelineViewportBudgets } from "./timelineViewportBudgets";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
createThumbnailKey,
|
|
4
|
+
type ThumbnailLoadedResult,
|
|
5
|
+
type ThumbnailValue,
|
|
6
|
+
} from "./thumbnailScheduler";
|
|
3
7
|
|
|
4
8
|
export interface VideoThumbnailDecodeRequest {
|
|
5
9
|
source: string;
|
|
10
|
+
contentVersion?: string;
|
|
6
11
|
sourceStart?: number;
|
|
7
12
|
sourceRangeDuration?: number;
|
|
8
13
|
frameCount: number;
|
|
9
14
|
fit?: "contain" | "cover";
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
const LAST_FRAME_INSET_S = 0.001;
|
|
18
|
+
|
|
12
19
|
export function videoThumbnailTimestamps(
|
|
13
20
|
start: number,
|
|
14
21
|
duration: number,
|
|
@@ -18,10 +25,63 @@ export function videoThumbnailTimestamps(
|
|
|
18
25
|
const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
|
|
19
26
|
const count = Math.max(1, Number.isFinite(frameCount) ? Math.floor(frameCount) : 1);
|
|
20
27
|
if (count === 1) return [safeStart + safeDuration / 2];
|
|
21
|
-
|
|
28
|
+
const edges = Array.from(
|
|
22
29
|
{ length: count },
|
|
23
|
-
(_, index) => safeStart + (safeDuration * index) /
|
|
30
|
+
(_, index) => safeStart + (safeDuration * index) / count,
|
|
24
31
|
);
|
|
32
|
+
return [...edges, safeStart + Math.max(0, safeDuration - LAST_FRAME_INSET_S)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SharedFrame {
|
|
36
|
+
url: string;
|
|
37
|
+
decodedAt: number;
|
|
38
|
+
users: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface HeldFrame {
|
|
42
|
+
key: string;
|
|
43
|
+
frame: SharedFrame;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface FrameWindow {
|
|
47
|
+
sourceStart: number;
|
|
48
|
+
lead: number;
|
|
49
|
+
end: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sharedFrames = new Map<string, SharedFrame[]>();
|
|
53
|
+
const sourceInfos = new Map<string, { version: string | undefined; info: SourceInfo }>();
|
|
54
|
+
|
|
55
|
+
const accepts = (window: FrameWindow, time: number, decodedAt: number) =>
|
|
56
|
+
decodedAt >= window.sourceStart && time - decodedAt <= (time >= window.end ? 0 : window.lead);
|
|
57
|
+
|
|
58
|
+
function takeSharedFrame(key: string, time: number, window: FrameWindow): SharedFrame | undefined {
|
|
59
|
+
const frame = sharedFrames.get(key)?.find((shared) => accepts(window, time, shared.decodedAt));
|
|
60
|
+
if (frame) frame.users += 1;
|
|
61
|
+
return frame;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shareFrame(key: string, decodedAt: number, url: string): SharedFrame {
|
|
65
|
+
const frames = sharedFrames.get(key) ?? [];
|
|
66
|
+
const existing = frames.find((shared) => shared.decodedAt === decodedAt);
|
|
67
|
+
if (existing) {
|
|
68
|
+
existing.users += 1;
|
|
69
|
+
URL.revokeObjectURL(url);
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
const frame = { url, decodedAt, users: 1 };
|
|
73
|
+
sharedFrames.set(key, [...frames, frame]);
|
|
74
|
+
return frame;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function releaseSharedFrames(held: HeldFrame[]): void {
|
|
78
|
+
for (const { key, frame } of held.splice(0)) {
|
|
79
|
+
if (--frame.users > 0) continue;
|
|
80
|
+
const rest = sharedFrames.get(key)?.filter((shared) => shared !== frame) ?? [];
|
|
81
|
+
if (rest.length > 0) sharedFrames.set(key, rest);
|
|
82
|
+
else sharedFrames.delete(key);
|
|
83
|
+
URL.revokeObjectURL(frame.url);
|
|
84
|
+
}
|
|
25
85
|
}
|
|
26
86
|
|
|
27
87
|
async function canvasToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promise<Blob> {
|
|
@@ -38,7 +98,8 @@ async function canvasToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promis
|
|
|
38
98
|
}
|
|
39
99
|
|
|
40
100
|
interface DecodedResources {
|
|
41
|
-
urls: string[];
|
|
101
|
+
urls: (string | undefined)[];
|
|
102
|
+
held: HeldFrame[];
|
|
42
103
|
canvases: Set<HTMLCanvasElement | OffscreenCanvas>;
|
|
43
104
|
}
|
|
44
105
|
|
|
@@ -48,12 +109,18 @@ interface ThumbnailCanvasSink {
|
|
|
48
109
|
): AsyncIterable<{ canvas: HTMLCanvasElement | OffscreenCanvas } | null>;
|
|
49
110
|
}
|
|
50
111
|
|
|
112
|
+
interface SourceInfo {
|
|
113
|
+
aspect: number;
|
|
114
|
+
metadataDuration: number | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
51
117
|
function throwIfAborted(signal: AbortSignal): void {
|
|
52
118
|
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
53
119
|
}
|
|
54
120
|
|
|
55
121
|
function releaseDecodedResources(resources: DecodedResources): void {
|
|
56
|
-
|
|
122
|
+
resources.urls.length = 0;
|
|
123
|
+
releaseSharedFrames(resources.held);
|
|
57
124
|
for (const canvas of resources.canvases) {
|
|
58
125
|
canvas.width = 0;
|
|
59
126
|
canvas.height = 0;
|
|
@@ -75,19 +142,71 @@ function targetDimensions(
|
|
|
75
142
|
};
|
|
76
143
|
}
|
|
77
144
|
|
|
145
|
+
function stripTimes(
|
|
146
|
+
request: VideoThumbnailDecodeRequest,
|
|
147
|
+
info: SourceInfo,
|
|
148
|
+
budgets: Readonly<TimelineViewportBudgets>,
|
|
149
|
+
) {
|
|
150
|
+
const requestedStart = Math.max(0, request.sourceStart ?? 0);
|
|
151
|
+
const sourceDuration = Math.max(
|
|
152
|
+
0,
|
|
153
|
+
info.metadataDuration ?? requestedStart + (request.sourceRangeDuration ?? 0),
|
|
154
|
+
);
|
|
155
|
+
const sourceStart = Math.min(requestedStart, sourceDuration);
|
|
156
|
+
const requestedDuration =
|
|
157
|
+
request.sourceRangeDuration ?? Math.max(0, sourceDuration - sourceStart);
|
|
158
|
+
const duration = Math.min(
|
|
159
|
+
Math.max(0, requestedDuration),
|
|
160
|
+
Math.max(0, sourceDuration - sourceStart),
|
|
161
|
+
);
|
|
162
|
+
const timestamps = videoThumbnailTimestamps(
|
|
163
|
+
sourceStart,
|
|
164
|
+
duration,
|
|
165
|
+
Math.min(request.frameCount, budgets.richPreviewFrameCount),
|
|
166
|
+
);
|
|
167
|
+
const slices = Math.max(1, timestamps.length - 1);
|
|
168
|
+
const window: FrameWindow = {
|
|
169
|
+
sourceStart,
|
|
170
|
+
lead: duration / Math.max(2, slices) / 2,
|
|
171
|
+
end: timestamps.length > 1 ? timestamps.at(-1)! : Infinity,
|
|
172
|
+
};
|
|
173
|
+
return { window, timestamps };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function takeDecodedFrames(
|
|
177
|
+
{ timestamps, window }: ReturnType<typeof stripTimes>,
|
|
178
|
+
keyOf: (time: number) => string,
|
|
179
|
+
resources: DecodedResources,
|
|
180
|
+
): number[] {
|
|
181
|
+
const missing: number[] = [];
|
|
182
|
+
timestamps.forEach((time, slot) => {
|
|
183
|
+
const key = keyOf(time);
|
|
184
|
+
const frame = takeSharedFrame(key, time, window);
|
|
185
|
+
if (!frame) return void missing.push(slot);
|
|
186
|
+
resources.urls[slot] = frame.url;
|
|
187
|
+
resources.held.push({ key, frame });
|
|
188
|
+
});
|
|
189
|
+
return missing;
|
|
190
|
+
}
|
|
191
|
+
|
|
78
192
|
async function decodeFrames(
|
|
79
193
|
sink: ThumbnailCanvasSink,
|
|
80
|
-
|
|
194
|
+
times: AsyncIterable<number>,
|
|
195
|
+
slots: { slot: number; key: string; decodedAt?: number }[],
|
|
81
196
|
signal: AbortSignal,
|
|
82
197
|
resources: DecodedResources,
|
|
83
198
|
): Promise<void> {
|
|
84
|
-
|
|
199
|
+
let next = 0;
|
|
200
|
+
for await (const wrapped of sink.canvasesAtTimestamps(times)) {
|
|
85
201
|
throwIfAborted(signal);
|
|
86
|
-
|
|
202
|
+
const target = slots[next++];
|
|
203
|
+
if (!wrapped || target?.decodedAt === undefined) continue;
|
|
87
204
|
resources.canvases.add(wrapped.canvas);
|
|
88
205
|
const blob = await canvasToBlob(wrapped.canvas);
|
|
89
206
|
throwIfAborted(signal);
|
|
90
|
-
|
|
207
|
+
const frame = shareFrame(target.key, target.decodedAt, URL.createObjectURL(blob));
|
|
208
|
+
resources.urls[target.slot] = frame.url;
|
|
209
|
+
resources.held.push({ key: target.key, frame });
|
|
91
210
|
}
|
|
92
211
|
throwIfAborted(signal);
|
|
93
212
|
}
|
|
@@ -95,18 +214,25 @@ async function decodeFrames(
|
|
|
95
214
|
function loadedResult(
|
|
96
215
|
resources: DecodedResources,
|
|
97
216
|
aspect: number,
|
|
98
|
-
|
|
99
|
-
|
|
217
|
+
budgets: Readonly<TimelineViewportBudgets>,
|
|
218
|
+
frameCount = resources.urls.length,
|
|
100
219
|
): ThumbnailLoadedResult {
|
|
101
|
-
const
|
|
220
|
+
const decoded = resources.urls.filter((url): url is string => url !== undefined);
|
|
221
|
+
const firstUrl = decoded[0];
|
|
102
222
|
if (!firstUrl) throw new Error("Video source returned no thumbnail frames");
|
|
223
|
+
let nearest = firstUrl;
|
|
224
|
+
const urls = Array.from(
|
|
225
|
+
{ length: frameCount },
|
|
226
|
+
(_, slot) => (nearest = resources.urls[slot] ?? nearest),
|
|
227
|
+
);
|
|
228
|
+
const { width, height } = targetDimensions(aspect, budgets);
|
|
103
229
|
const value: ThumbnailValue =
|
|
104
|
-
|
|
230
|
+
urls.length === 1
|
|
105
231
|
? { kind: "image", url: firstUrl, aspect }
|
|
106
|
-
: { kind: "filmstrip", urls
|
|
232
|
+
: { kind: "filmstrip", urls, aspect };
|
|
107
233
|
return {
|
|
108
234
|
value,
|
|
109
|
-
weight: width * height * 4 *
|
|
235
|
+
weight: width * height * 4 * decoded.length,
|
|
110
236
|
dispose: () => releaseDecodedResources(resources),
|
|
111
237
|
};
|
|
112
238
|
}
|
|
@@ -116,6 +242,34 @@ export async function decodeVideoThumbnail(
|
|
|
116
242
|
request: VideoThumbnailDecodeRequest,
|
|
117
243
|
signal: AbortSignal,
|
|
118
244
|
budgets: Readonly<TimelineViewportBudgets> = TIMELINE_VIEWPORT_BUDGETS,
|
|
245
|
+
): Promise<ThumbnailLoadedResult> {
|
|
246
|
+
const fit = request.fit ?? "cover";
|
|
247
|
+
const sourceKey = createThumbnailKey({ source: request.source, version: request.contentVersion });
|
|
248
|
+
const keyOf = (time: number) => `${sourceKey}\u0000${fit}\u0000${time}`;
|
|
249
|
+
const resources: DecodedResources = { urls: [], held: [], canvases: new Set() };
|
|
250
|
+
const entry = sourceInfos.get(request.source);
|
|
251
|
+
const known = entry?.version === request.contentVersion ? entry?.info : undefined;
|
|
252
|
+
try {
|
|
253
|
+
if (known) {
|
|
254
|
+
if (takeDecodedFrames(stripTimes(request, known, budgets), keyOf, resources).length === 0) {
|
|
255
|
+
throwIfAborted(signal);
|
|
256
|
+
return loadedResult(resources, known.aspect, budgets);
|
|
257
|
+
}
|
|
258
|
+
releaseDecodedResources(resources);
|
|
259
|
+
}
|
|
260
|
+
return await decodeMissingFrames(request, signal, budgets, keyOf, resources);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
releaseDecodedResources(resources);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function decodeMissingFrames(
|
|
268
|
+
request: VideoThumbnailDecodeRequest,
|
|
269
|
+
signal: AbortSignal,
|
|
270
|
+
budgets: Readonly<TimelineViewportBudgets>,
|
|
271
|
+
keyOf: (time: number) => string,
|
|
272
|
+
resources: DecodedResources,
|
|
119
273
|
): Promise<ThumbnailLoadedResult> {
|
|
120
274
|
const mediabunny = await import("mediabunny");
|
|
121
275
|
throwIfAborted(signal);
|
|
@@ -124,7 +278,6 @@ export async function decodeVideoThumbnail(
|
|
|
124
278
|
source: new mediabunny.UrlSource(request.source),
|
|
125
279
|
formats: mediabunny.ALL_FORMATS,
|
|
126
280
|
});
|
|
127
|
-
const resources: DecodedResources = { urls: [], canvases: new Set() };
|
|
128
281
|
try {
|
|
129
282
|
const track = await input.getPrimaryVideoTrack();
|
|
130
283
|
throwIfAborted(signal);
|
|
@@ -139,46 +292,36 @@ export async function decodeVideoThumbnail(
|
|
|
139
292
|
}
|
|
140
293
|
const metadataDuration = await track.getDurationFromMetadata({ skipLiveWait: true });
|
|
141
294
|
throwIfAborted(signal);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
async function* decodeTimesAtNearbyKeyframes() {
|
|
162
|
-
for (const time of timestamps) {
|
|
163
|
-
const key = await keys.getKeyPacket(time, { metadataOnly: true });
|
|
164
|
-
if (signal.aborted) return;
|
|
165
|
-
const near = key && key.timestamp >= sourceStart && time - key.timestamp <= maxKeyframeLead;
|
|
166
|
-
yield near ? key.timestamp : time;
|
|
295
|
+
const info: SourceInfo = { aspect: displayWidth / displayHeight, metadataDuration };
|
|
296
|
+
sourceInfos.set(request.source, { version: request.contentVersion, info });
|
|
297
|
+
const strip = stripTimes(request, info, budgets);
|
|
298
|
+
const { timestamps, window } = strip;
|
|
299
|
+
const slots: { slot: number; key: string; decodedAt?: number }[] = takeDecodedFrames(
|
|
300
|
+
strip,
|
|
301
|
+
keyOf,
|
|
302
|
+
resources,
|
|
303
|
+
).map((slot) => ({ slot, key: keyOf(timestamps[slot]!) }));
|
|
304
|
+
if (slots.length > 0) {
|
|
305
|
+
const keys = new mediabunny.EncodedPacketSink(track);
|
|
306
|
+
async function* decodeTimesAtNearbyKeyframes() {
|
|
307
|
+
for (const target of slots) {
|
|
308
|
+
const time = timestamps[target.slot]!;
|
|
309
|
+
const key = await keys.getKeyPacket(time, { metadataOnly: true });
|
|
310
|
+
if (signal.aborted) return;
|
|
311
|
+
target.decodedAt = key && accepts(window, time, key.timestamp) ? key.timestamp : time;
|
|
312
|
+
yield target.decodedAt;
|
|
313
|
+
}
|
|
167
314
|
}
|
|
315
|
+
const target = targetDimensions(info.aspect, budgets);
|
|
316
|
+
const sink = new mediabunny.CanvasSink(track, {
|
|
317
|
+
width: target.width,
|
|
318
|
+
height: target.height,
|
|
319
|
+
fit: request.fit ?? "cover",
|
|
320
|
+
poolSize: 1,
|
|
321
|
+
});
|
|
322
|
+
await decodeFrames(sink, decodeTimesAtNearbyKeyframes(), slots, signal, resources);
|
|
168
323
|
}
|
|
169
|
-
|
|
170
|
-
const target = targetDimensions(aspect, budgets);
|
|
171
|
-
const sink = new mediabunny.CanvasSink(track, {
|
|
172
|
-
width: target.width,
|
|
173
|
-
height: target.height,
|
|
174
|
-
fit: request.fit ?? "cover",
|
|
175
|
-
poolSize: 1,
|
|
176
|
-
});
|
|
177
|
-
await decodeFrames(sink, decodeTimesAtNearbyKeyframes(), signal, resources);
|
|
178
|
-
return loadedResult(resources, aspect, target.width, target.height);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
releaseDecodedResources(resources);
|
|
181
|
-
throw error;
|
|
324
|
+
return loadedResult(resources, info.aspect, budgets, timestamps.length);
|
|
182
325
|
} finally {
|
|
183
326
|
input.dispose();
|
|
184
327
|
}
|
|
@@ -9,7 +9,7 @@ describe("timeline viewport budgets", () => {
|
|
|
9
9
|
expect(TIMELINE_VIEWPORT_BUDGETS).toMatchObject({
|
|
10
10
|
directScrollSafetyPx: 8_000_000,
|
|
11
11
|
rowOverscanPerSide: 2,
|
|
12
|
-
timeOverscanViewportRatio: 0.
|
|
12
|
+
timeOverscanViewportRatio: 0.5,
|
|
13
13
|
maxMountedRows: 64,
|
|
14
14
|
maxMountedClipRoots: 512,
|
|
15
15
|
maxMountedClipRootsPerRow: 128,
|
|
@@ -61,7 +61,7 @@ export const MAX_VISIBLE_THUMBNAIL_FRAMES = Math.ceil(3840 / (66 * (16 / 9))); /
|
|
|
61
61
|
export const TIMELINE_VIEWPORT_BUDGETS: Readonly<TimelineViewportBudgets> = Object.freeze({
|
|
62
62
|
directScrollSafetyPx: 8_000_000,
|
|
63
63
|
rowOverscanPerSide: 2,
|
|
64
|
-
timeOverscanViewportRatio: 0.
|
|
64
|
+
timeOverscanViewportRatio: 0.5,
|
|
65
65
|
maxMountedRows: 64,
|
|
66
66
|
maxMountedClipRoots: 512,
|
|
67
67
|
maxMountedClipRootsPerRow: 128,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
// Real Chrome: the box GSAP and a stylesheet translate produce together is what no DOM emulation computes.
|
|
2
2
|
import { mkdtempSync, readFileSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import
|
|
6
|
+
import type { Browser } from "puppeteer-core";
|
|
8
7
|
import { build, type Plugin } from "esbuild";
|
|
9
8
|
import { afterAll, beforeAll, expect, it, vi } from "vitest";
|
|
10
|
-
import {
|
|
9
|
+
import { launchTestChrome, showWithRuntime } from "../../tests/chromeTestUtils";
|
|
11
10
|
import { writeFixture } from "../../tests/e2e/edit-accuracy/grid.mjs";
|
|
12
11
|
|
|
13
|
-
const require = createRequire(import.meta.url);
|
|
14
12
|
const CROP = "inset(0px 40px 0px 0px)";
|
|
15
13
|
// Real Chrome on a loaded Windows runner exceeds Vitest's 5s test and 10s hook defaults.
|
|
16
14
|
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
|
|
@@ -44,9 +42,7 @@ async function bundle(file: string, name: string): Promise<string> {
|
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
beforeAll(async () => {
|
|
47
|
-
|
|
48
|
-
if (!executablePath) throw new Error("no Chrome found: set HYPERFRAMES_BROWSER_PATH");
|
|
49
|
-
browser = await puppeteer.launch({ executablePath, headless: true, args: ["--no-sandbox"] });
|
|
45
|
+
browser = await launchTestChrome();
|
|
50
46
|
undoBundle = await bundle("./gsapUndoRestore.ts", "hfUndo");
|
|
51
47
|
softReloadBundle = await bundle("./gsapSoftReload.ts", "hfSoftReload");
|
|
52
48
|
});
|
|
@@ -62,16 +58,7 @@ async function openCase(spec: Spec, moduleBundle: string) {
|
|
|
62
58
|
const restored = readFileSync(join(dir, "index.html"), "utf8");
|
|
63
59
|
const page = await browser.newPage();
|
|
64
60
|
await page.setViewport({ width: 1920, height: 1080 });
|
|
65
|
-
await page
|
|
66
|
-
// Only the fixture's GSAP CDN script is served; anything else is refused, never fetched.
|
|
67
|
-
page.on("request", (request) =>
|
|
68
|
-
request.url().endsWith("/gsap.min.js")
|
|
69
|
-
? request.respond({ body: readFileSync(require.resolve("gsap/dist/gsap.min.js"), "utf8") })
|
|
70
|
-
: request.abort("blockedbyclient"),
|
|
71
|
-
);
|
|
72
|
-
await page.setContent(restored, { waitUntil: "load" });
|
|
73
|
-
await page.evaluate(readFileSync(require.resolve("@hyperframes/core/runtime"), "utf8"));
|
|
74
|
-
await page.waitForFunction(() => "__player" in window);
|
|
61
|
+
await showWithRuntime(page, restored);
|
|
75
62
|
await page.evaluate(moduleBundle);
|
|
76
63
|
await page.evaluate(
|
|
77
64
|
(time) => (window as unknown as { __player: { seek(t: number): void } }).__player.seek(time),
|