@hyperframes/studio 0.8.56 → 0.8.58
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-zGyOcwwh.js +464 -0
- package/dist/assets/{index-DFlILHJe.js → index-BcE2OSVA.js} +1 -1
- package/dist/assets/{index-ZmBtVrEm.css → index-DKJpHYYd.css} +1 -1
- package/dist/assets/{index-CxR7F2YQ.js → index-ZJaRCDYV.js} +1 -1
- package/dist/assets/{index-BlkJ_Axe.js → index-d8ferjRc.js} +194 -194
- package/dist/index.d.ts +124 -9
- package/dist/index.html +2 -2
- package/dist/index.js +15686 -14882
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -0
- package/src/components/EditorShell.tsx +3 -1
- package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
- package/src/components/ProjectUnreachableBanner.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +0 -15
- package/src/components/editor/GridOverlay.tsx +12 -23
- package/src/components/editor/MotionPathOverlay.test.tsx +65 -0
- package/src/components/editor/MotionPathOverlay.tsx +3 -1
- package/src/components/editor/PreviewGuides.test.tsx +6 -1
- package/src/components/editor/PreviewGuides.tsx +8 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
- package/src/components/editor/SnapToolbar.test.tsx +25 -12
- package/src/components/editor/SnapToolbar.tsx +30 -59
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
- package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
- package/src/components/editor/usePreviewCompositionRect.ts +12 -12
- package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
- package/src/components/editor/useVolumeAutomation.ts +57 -16
- package/src/components/nle/NLEContext.tsx +48 -6
- package/src/components/nle/NLEPreview.test.ts +67 -1
- package/src/components/nle/NLEPreview.tsx +88 -44
- package/src/components/nle/PreviewOverlays.tsx +4 -16
- package/src/components/nle/PreviewPane.tsx +17 -2
- package/src/components/nle/previewReadOnly.test.tsx +1 -0
- package/src/components/nle/useCompositionStack.ts +1 -1
- package/src/hooks/useCompositionDimensions.test.tsx +37 -0
- package/src/hooks/useCompositionDimensions.ts +17 -1
- package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
- package/src/hooks/useDomEditAttributeCommits.ts +14 -3
- package/src/hooks/useFileTree.test.tsx +56 -0
- package/src/hooks/useFileTree.ts +8 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +108 -0
- package/src/hooks/useSdkSession.ts +77 -14
- package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
- package/src/hooks/useServerConnection.ts +81 -8
- package/src/index.ts +21 -1
- package/src/player/components/LayerDisclosureRow.tsx +3 -3
- package/src/player/components/Player.test.ts +175 -13
- package/src/player/components/Player.tsx +58 -2
- package/src/player/components/ShortcutsPanel.test.tsx +4 -10
- package/src/player/components/Timeline.tsx +23 -572
- package/src/player/components/TimelineCanvas.tsx +9 -30
- package/src/player/components/TimelineGroupHeader.tsx +4 -6
- package/src/player/components/TimelineOverlays.test.ts +67 -32
- package/src/player/components/TimelineOverlays.tsx +46 -107
- package/src/player/components/TimelineProvider.tsx +191 -0
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +10 -0
- package/src/player/components/TimelineTrackPlainHeader.tsx +4 -7
- package/src/player/components/testRootHarness.ts +18 -0
- package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
- package/src/player/components/timelineProviderStateBuilders.ts +83 -0
- package/src/player/components/useTimelineClipContextMenu.ts +1 -1
- package/src/player/components/useTimelineOverlaysState.ts +100 -0
- package/src/player/components/useTimelineProviderState.tsx +583 -0
- package/src/player/hooks/playbackAdapterResolution.test.ts +41 -0
- package/src/player/hooks/playbackAdapterResolution.ts +98 -0
- package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
- package/src/player/hooks/usePlayerHandle.test.tsx +152 -0
- package/src/player/hooks/usePlayerHandle.ts +94 -0
- package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
- package/src/player/hooks/useShadowPreviewReload.ts +183 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
- package/src/player/hooks/useTimelinePlayer.ts +65 -99
- package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -88
- package/src/player/index.ts +7 -0
- package/src/telemetry/events.test.ts +18 -0
- package/src/telemetry/events.ts +10 -0
- package/src/telemetry/system.test.ts +49 -0
- package/src/telemetry/system.ts +6 -0
- package/src/telemetry/tabId.test.ts +43 -0
- package/src/telemetry/tabId.ts +44 -0
- package/src/utils/previewCompositionSize.ts +26 -0
- package/src/utils/reloadDebug.ts +2 -4
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioTelemetry.test.ts +14 -0
- package/src/utils/studioTelemetry.ts +4 -0
- package/dist/assets/hyperframes-player-k8LzMtXX.js +0 -464
|
@@ -54,26 +54,39 @@ afterEach(() => {
|
|
|
54
54
|
document.body.innerHTML = "";
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
type PlayerProps = Parameters<typeof Player>[0];
|
|
58
|
+
|
|
59
|
+
async function mountPlayer(props: Partial<PlayerProps> = {}) {
|
|
58
60
|
const host = document.createElement("div");
|
|
59
61
|
document.body.append(host);
|
|
60
62
|
root = createRoot(host);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const render = () =>
|
|
64
|
+
act(async () => {
|
|
65
|
+
root?.render(
|
|
66
|
+
createElement(Player, {
|
|
67
|
+
directUrl: "/api/projects/demo/preview",
|
|
68
|
+
onLoad: vi.fn(),
|
|
69
|
+
suppressLoadingOverlay: true,
|
|
70
|
+
...props,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
});
|
|
75
|
+
await render();
|
|
71
76
|
|
|
72
77
|
const player = host.querySelector<TestHyperframesPlayer>("hyperframes-player");
|
|
73
78
|
if (!player) throw new Error("player did not mount");
|
|
74
|
-
|
|
79
|
+
const rerender = (next: Partial<PlayerProps>) => {
|
|
80
|
+
Object.assign(props, next);
|
|
81
|
+
return render();
|
|
82
|
+
};
|
|
83
|
+
return { host, player, rerender };
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
const twoFrames = () => act(async () => void (await new Promise((r) => setTimeout(r, 80))));
|
|
87
|
+
|
|
88
|
+
const flushEffects = () => act(async () => await Promise.resolve());
|
|
89
|
+
|
|
77
90
|
function createAudioIframe() {
|
|
78
91
|
const iframe = document.createElement("iframe");
|
|
79
92
|
document.body.appendChild(iframe);
|
|
@@ -123,6 +136,7 @@ describe("preview errors", () => {
|
|
|
123
136
|
"player:click",
|
|
124
137
|
"player:shadertransitionstate",
|
|
125
138
|
"player:ready",
|
|
139
|
+
"player:painted",
|
|
126
140
|
"player:error",
|
|
127
141
|
]) {
|
|
128
142
|
expect(lifecycleLog.indexOf(listener)).toBeGreaterThan(-1);
|
|
@@ -131,7 +145,10 @@ describe("preview errors", () => {
|
|
|
131
145
|
});
|
|
132
146
|
|
|
133
147
|
it("retries a failed preview with a fresh player URL", async () => {
|
|
134
|
-
const
|
|
148
|
+
const onPainted = vi.fn();
|
|
149
|
+
const { host, player } = await mountPlayer({ onPainted });
|
|
150
|
+
|
|
151
|
+
act(() => void player.dispatchEvent(new Event("painted")));
|
|
135
152
|
|
|
136
153
|
act(() => {
|
|
137
154
|
player.dispatchEvent(
|
|
@@ -152,6 +169,151 @@ describe("preview errors", () => {
|
|
|
152
169
|
const retryUrl = new URL(player.getAttribute("src") ?? "", window.location.origin);
|
|
153
170
|
expect(retryUrl.searchParams.get("_hfStudioRetry")).toBe("1");
|
|
154
171
|
expect(host.querySelector('[data-testid="composition-preview-error"]')).toBeNull();
|
|
172
|
+
|
|
173
|
+
act(() => void player.dispatchEvent(new Event("painted")));
|
|
174
|
+
expect(onPainted.mock.calls.map(([details]) => details.loadId)).toEqual([1, 2]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("callbacks after the player is already mounted", () => {
|
|
179
|
+
it("runs the latest onLoad on a later load, not the one captured at mount", async () => {
|
|
180
|
+
const first = vi.fn();
|
|
181
|
+
const second = vi.fn();
|
|
182
|
+
const { player, rerender } = await mountPlayer({ onLoad: first });
|
|
183
|
+
await rerender({ onLoad: second });
|
|
184
|
+
|
|
185
|
+
act(
|
|
186
|
+
() => void (player as TestHyperframesPlayer).iframeElement.dispatchEvent(new Event("load")),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect(first).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("reports the preview error cause", async () => {
|
|
194
|
+
const onPreviewError = vi.fn();
|
|
195
|
+
const { player } = await mountPlayer({ onPreviewError });
|
|
196
|
+
act(() => {
|
|
197
|
+
player.dispatchEvent(new CustomEvent("error", { detail: { message: "boom" } }));
|
|
198
|
+
});
|
|
199
|
+
expect(onPreviewError).toHaveBeenCalledWith("boom");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("ready to show", () => {
|
|
204
|
+
const loadAndReady = (player: TestHyperframesPlayer) =>
|
|
205
|
+
act(() => {
|
|
206
|
+
player.iframeElement.dispatchEvent(new Event("load"));
|
|
207
|
+
player.dispatchEvent(new Event("ready"));
|
|
208
|
+
});
|
|
209
|
+
const shaderState = (player: TestHyperframesPlayer, loading: boolean) =>
|
|
210
|
+
act(() => {
|
|
211
|
+
player.dispatchEvent(
|
|
212
|
+
new CustomEvent("shadertransitionstate", {
|
|
213
|
+
detail: { state: { loading, ready: !loading } },
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const painted = (player: TestHyperframesPlayer) =>
|
|
219
|
+
act(() => void player.dispatchEvent(new Event("painted")));
|
|
220
|
+
|
|
221
|
+
it("waits two animation frames before notifying that the preview can show", async () => {
|
|
222
|
+
const callbacks: FrameRequestCallback[] = [];
|
|
223
|
+
const requestAnimationFrame = vi
|
|
224
|
+
.spyOn(window, "requestAnimationFrame")
|
|
225
|
+
.mockImplementation((callback) => {
|
|
226
|
+
callbacks.push(callback);
|
|
227
|
+
return callbacks.length;
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
const onReadyToShowChange = vi.fn();
|
|
231
|
+
const { player } = await mountPlayer({ onReadyToShowChange });
|
|
232
|
+
const el = player as TestHyperframesPlayer;
|
|
233
|
+
|
|
234
|
+
loadAndReady(el);
|
|
235
|
+
painted(el);
|
|
236
|
+
await flushEffects();
|
|
237
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
238
|
+
expect(callbacks).toHaveLength(1);
|
|
239
|
+
|
|
240
|
+
act(() => callbacks.shift()?.(0));
|
|
241
|
+
await flushEffects();
|
|
242
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
243
|
+
expect(callbacks).toHaveLength(1);
|
|
244
|
+
|
|
245
|
+
act(() => callbacks.shift()?.(16));
|
|
246
|
+
await flushEffects();
|
|
247
|
+
expect(onReadyToShowChange).toHaveBeenLastCalledWith(true);
|
|
248
|
+
} finally {
|
|
249
|
+
requestAnimationFrame.mockRestore();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("promotes only once the player reports painted, not at ready or assetsready", async () => {
|
|
254
|
+
const onReadyToShowChange = vi.fn();
|
|
255
|
+
const { player } = await mountPlayer({ onReadyToShowChange });
|
|
256
|
+
const el = player as TestHyperframesPlayer;
|
|
257
|
+
|
|
258
|
+
loadAndReady(el);
|
|
259
|
+
await twoFrames();
|
|
260
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
261
|
+
|
|
262
|
+
act(() => void el.dispatchEvent(new Event("assetsready")));
|
|
263
|
+
await twoFrames();
|
|
264
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
265
|
+
|
|
266
|
+
painted(el);
|
|
267
|
+
await twoFrames();
|
|
268
|
+
expect(onReadyToShowChange).toHaveBeenLastCalledWith(true);
|
|
269
|
+
|
|
270
|
+
act(() => void el.iframeElement.dispatchEvent(new Event("load")));
|
|
271
|
+
expect(onReadyToShowChange).toHaveBeenLastCalledWith(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("reports the document start time with the painted iframe", async () => {
|
|
275
|
+
const onPainted = vi.fn();
|
|
276
|
+
const now = vi.spyOn(performance, "now").mockReturnValue(100);
|
|
277
|
+
const { player } = await mountPlayer({ onPainted });
|
|
278
|
+
|
|
279
|
+
painted(player as TestHyperframesPlayer);
|
|
280
|
+
|
|
281
|
+
expect(onPainted).toHaveBeenCalledWith({
|
|
282
|
+
iframe: (player as TestHyperframesPlayer).iframeElement,
|
|
283
|
+
startedAt: 100,
|
|
284
|
+
loadId: 1,
|
|
285
|
+
});
|
|
286
|
+
now.mockRestore();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("holds while the shader transition loader is up and fires once it clears", async () => {
|
|
290
|
+
const onReadyToShowChange = vi.fn();
|
|
291
|
+
const { player } = await mountPlayer({ onReadyToShowChange });
|
|
292
|
+
const el = player as TestHyperframesPlayer;
|
|
293
|
+
|
|
294
|
+
shaderState(el, true);
|
|
295
|
+
loadAndReady(el);
|
|
296
|
+
shaderState(el, true);
|
|
297
|
+
await twoFrames();
|
|
298
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
299
|
+
|
|
300
|
+
shaderState(el, false);
|
|
301
|
+
painted(el);
|
|
302
|
+
await twoFrames();
|
|
303
|
+
expect(onReadyToShowChange).toHaveBeenLastCalledWith(true);
|
|
304
|
+
|
|
305
|
+
shaderState(el, true);
|
|
306
|
+
expect(onReadyToShowChange).toHaveBeenLastCalledWith(false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("does not fire for a document that failed to load", async () => {
|
|
310
|
+
const onReadyToShowChange = vi.fn();
|
|
311
|
+
const { player } = await mountPlayer({ onReadyToShowChange });
|
|
312
|
+
act(() => {
|
|
313
|
+
player.dispatchEvent(new CustomEvent("error", { detail: { message: "boom" } }));
|
|
314
|
+
});
|
|
315
|
+
await twoFrames();
|
|
316
|
+
expect(onReadyToShowChange).not.toHaveBeenCalledWith(true);
|
|
155
317
|
});
|
|
156
318
|
});
|
|
157
319
|
|
|
@@ -26,7 +26,11 @@ interface PlayerProps {
|
|
|
26
26
|
projectId?: string;
|
|
27
27
|
directUrl?: string;
|
|
28
28
|
onLoad: () => void;
|
|
29
|
+
/** Fires once the loaded document is painted and every loader (shader, assets) has cleared. */
|
|
30
|
+
onReadyToShowChange?: (ready: boolean) => void;
|
|
31
|
+
onPreviewError?: (message: string) => void;
|
|
29
32
|
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
33
|
+
onPainted?: (details: { iframe: HTMLIFrameElement; startedAt: number; loadId: number }) => void;
|
|
30
34
|
portrait?: boolean;
|
|
31
35
|
style?: React.CSSProperties;
|
|
32
36
|
suppressLoadingOverlay?: boolean;
|
|
@@ -134,7 +138,10 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
134
138
|
projectId,
|
|
135
139
|
directUrl,
|
|
136
140
|
onLoad,
|
|
141
|
+
onReadyToShowChange,
|
|
142
|
+
onPreviewError,
|
|
137
143
|
onCompositionLoadingChange,
|
|
144
|
+
onPainted,
|
|
138
145
|
portrait,
|
|
139
146
|
style,
|
|
140
147
|
suppressLoadingOverlay,
|
|
@@ -146,13 +153,25 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
146
153
|
const assetPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
147
154
|
const assetFadeRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
148
155
|
const retryPreviewRef = useRef<(() => void) | null>(null);
|
|
156
|
+
const loadStartedAtRef = useRef(0);
|
|
157
|
+
const loadIdRef = useRef(0);
|
|
149
158
|
const retryCountRef = useRef(0);
|
|
159
|
+
// Read at call time: this element outlives its first props (a shadow preview is
|
|
160
|
+
// promoted in place), so mount-time closures would go stale.
|
|
161
|
+
const onLoadRef = useRef(onLoad);
|
|
162
|
+
onLoadRef.current = onLoad;
|
|
163
|
+
const onReadyToShowChangeRef = useRef(onReadyToShowChange);
|
|
164
|
+
onReadyToShowChangeRef.current = onReadyToShowChange;
|
|
165
|
+
const onPreviewErrorRef = useRef(onPreviewError);
|
|
166
|
+
onPreviewErrorRef.current = onPreviewError;
|
|
167
|
+
const [loaded, setLoaded] = useState(false);
|
|
150
168
|
const [assetsLoading, setAssetsLoading] = useState(false);
|
|
151
169
|
const [assetOverlayVisible, setAssetOverlayVisible] = useState(false);
|
|
152
170
|
const [assetOverlayFading, setAssetOverlayFading] = useState(false);
|
|
153
171
|
const [assetWaitLong, setAssetWaitLong] = useState(false);
|
|
154
172
|
const [shaderTransitionLoading, setShaderTransitionLoading] = useState(false);
|
|
155
173
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
174
|
+
const [painted, setPainted] = useState(false);
|
|
156
175
|
const [compositionOverlayDeferred, setCompositionOverlayDeferred] = useState(true);
|
|
157
176
|
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
158
177
|
|
|
@@ -194,6 +213,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
194
213
|
retryUrl.searchParams.set("_hfStudioRetry", String(retryCountRef.current));
|
|
195
214
|
setPreviewError(null);
|
|
196
215
|
setCompositionLoading(true);
|
|
216
|
+
loadStartedAtRef.current = performance.now();
|
|
217
|
+
loadIdRef.current += 1;
|
|
197
218
|
player.setAttribute("src", retryUrl.pathname + retryUrl.search);
|
|
198
219
|
};
|
|
199
220
|
retryPreviewRef.current = retryPreview;
|
|
@@ -207,15 +228,23 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
207
228
|
setPreviewError(null);
|
|
208
229
|
setCompositionLoading(false);
|
|
209
230
|
};
|
|
231
|
+
const handlePainted = () => {
|
|
232
|
+
setPainted(true);
|
|
233
|
+
onPainted?.({ iframe, startedAt: loadStartedAtRef.current, loadId: loadIdRef.current });
|
|
234
|
+
};
|
|
210
235
|
const handleError = (event: Event) => {
|
|
211
|
-
|
|
236
|
+
const message = readPreviewErrorMessage(event);
|
|
237
|
+
onPreviewErrorRef.current?.(message);
|
|
238
|
+
setPreviewError(message);
|
|
212
239
|
setCompositionLoading(false);
|
|
213
240
|
};
|
|
214
241
|
const handleLoad = () => {
|
|
215
242
|
loadCountRef.current++;
|
|
243
|
+
setLoaded(true);
|
|
216
244
|
setPreviewError(null);
|
|
217
245
|
setShaderTransitionLoading(false);
|
|
218
246
|
setCompositionLoading(true);
|
|
247
|
+
setPainted(false);
|
|
219
248
|
// Reveal animation on reload (hot-reload, composition switch)
|
|
220
249
|
if (loadCountRef.current > 1) {
|
|
221
250
|
container.classList.remove("preview-revealing");
|
|
@@ -224,7 +253,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
224
253
|
const onEnd = () => container.classList.remove("preview-revealing");
|
|
225
254
|
container.addEventListener("animationend", onEnd, { once: true });
|
|
226
255
|
}
|
|
227
|
-
|
|
256
|
+
onLoadRef.current();
|
|
228
257
|
|
|
229
258
|
// Show a loading overlay until every `<video>`/`<audio>` and Lottie
|
|
230
259
|
// asset is ready. Without this users can click play before audio has
|
|
@@ -274,6 +303,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
274
303
|
player.addEventListener("click", preventToggle, { capture: true });
|
|
275
304
|
player.addEventListener("shadertransitionstate", handleShaderTransitionState);
|
|
276
305
|
player.addEventListener("ready", handleReady);
|
|
306
|
+
player.addEventListener("painted", handlePainted);
|
|
277
307
|
player.addEventListener("error", handleError);
|
|
278
308
|
|
|
279
309
|
// Bridge the inner iframe to the forwarded ref for useTimelinePlayer.
|
|
@@ -291,6 +321,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
291
321
|
player.style.height = "100%";
|
|
292
322
|
player.style.display = "block";
|
|
293
323
|
player.style.background = "transparent";
|
|
324
|
+
loadStartedAtRef.current = performance.now();
|
|
325
|
+
loadIdRef.current += 1;
|
|
294
326
|
player.setAttribute("src", src);
|
|
295
327
|
container.appendChild(player);
|
|
296
328
|
|
|
@@ -313,6 +345,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
313
345
|
player.removeEventListener("click", preventToggle, { capture: true });
|
|
314
346
|
player.removeEventListener("shadertransitionstate", handleShaderTransitionState);
|
|
315
347
|
player.removeEventListener("ready", handleReady);
|
|
348
|
+
player.removeEventListener("painted", handlePainted);
|
|
316
349
|
player.removeEventListener("error", handleError);
|
|
317
350
|
if (assetPollRef.current) clearInterval(assetPollRef.current);
|
|
318
351
|
assetPollRef.current = null;
|
|
@@ -394,6 +427,29 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
394
427
|
setAssetsLoading(false);
|
|
395
428
|
};
|
|
396
429
|
|
|
430
|
+
const readyToShow =
|
|
431
|
+
loaded &&
|
|
432
|
+
painted &&
|
|
433
|
+
!compositionLoading &&
|
|
434
|
+
!shaderTransitionLoading &&
|
|
435
|
+
!assetsLoading &&
|
|
436
|
+
!previewError;
|
|
437
|
+
// `painted` means the player's own loader has finished fading; two frames of grace on top.
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (!readyToShow) {
|
|
440
|
+
onReadyToShowChangeRef.current?.(false);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
let second = 0;
|
|
444
|
+
const first = requestAnimationFrame(() => {
|
|
445
|
+
second = requestAnimationFrame(() => onReadyToShowChangeRef.current?.(true));
|
|
446
|
+
});
|
|
447
|
+
return () => {
|
|
448
|
+
cancelAnimationFrame(first);
|
|
449
|
+
cancelAnimationFrame(second);
|
|
450
|
+
};
|
|
451
|
+
}, [readyToShow]);
|
|
452
|
+
|
|
397
453
|
const showCompositionOverlay =
|
|
398
454
|
!suppressLoadingOverlay &&
|
|
399
455
|
!compositionOverlayDeferred &&
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import React, { act, Profiler } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
5
|
import { ShortcutsPanel } from "./ShortcutsPanel";
|
|
6
|
+
import { createHappyDomRootHarness } from "./testRootHarness";
|
|
7
7
|
|
|
8
8
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
for (const root of roots.splice(0)) act(() => root.unmount());
|
|
14
|
-
document.body.innerHTML = "";
|
|
15
|
-
});
|
|
10
|
+
const { mount } = createHappyDomRootHarness();
|
|
16
11
|
|
|
17
12
|
function renderPanel(onRender = vi.fn()) {
|
|
18
13
|
const host = document.createElement("div");
|
|
19
14
|
document.body.append(host);
|
|
20
|
-
const root =
|
|
21
|
-
roots.push(root);
|
|
15
|
+
const root = mount(host);
|
|
22
16
|
act(() => {
|
|
23
17
|
root.render(
|
|
24
18
|
<Profiler id="shortcuts-panel" onRender={onRender}>
|