@hyperframes/studio 0.6.52 → 0.6.54
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/index-CKJCBFsG.js +138 -0
- package/dist/assets/index-ZdgB8MFr.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/components/StudioFeedbackBar.tsx +208 -0
- package/src/components/StudioPreviewArea.tsx +97 -92
- package/src/components/StudioRightPanel.tsx +18 -0
- package/src/components/editor/AnimationCard.tsx +325 -0
- package/src/components/editor/EaseCurveSection.tsx +213 -0
- package/src/components/editor/GsapAnimationSection.tsx +112 -0
- package/src/components/editor/PropertyPanel.tsx +48 -18
- package/src/components/editor/domEditingTypes.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +130 -0
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/manualEdits.test.ts +101 -0
- package/src/components/editor/manualEdits.ts +22 -9
- package/src/components/editor/manualEditsDom.ts +22 -21
- package/src/components/editor/manualOffsetDrag.test.ts +35 -22
- package/src/components/editor/manualOffsetDrag.ts +1 -7
- package/src/components/editor/propertyPanelPrimitives.tsx +6 -1
- package/src/contexts/DomEditContext.tsx +27 -0
- package/src/hooks/useDomEditSession.ts +98 -2
- package/src/hooks/useDomSelection.ts +8 -0
- package/src/hooks/useGsapScriptCommits.ts +303 -0
- package/src/hooks/useGsapTweenCache.ts +80 -0
- package/src/hooks/usePreviewPersistence.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +142 -0
- package/src/player/hooks/useTimelinePlayer.ts +2 -1
- package/src/telemetry/events.ts +32 -0
- package/dist/assets/index-Bvy50smZ.js +0 -138
- package/dist/assets/index-SKRp8mGz.css +0 -1
|
@@ -279,3 +279,145 @@ describe("useTimelinePlayer seek keepPlaying option (#834)", () => {
|
|
|
279
279
|
expectStorePlaybackState(root, { isPlaying: true, currentTime: 0 });
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
|
+
|
|
283
|
+
describe("useTimelinePlayer RAF loop wrap-around", () => {
|
|
284
|
+
type SeekCall = { time: number; options?: { keepPlaying?: boolean } };
|
|
285
|
+
|
|
286
|
+
function attachInstrumentedAdapter(api: ReturnType<typeof useTimelinePlayer>, duration = 30) {
|
|
287
|
+
const iframe = document.createElement("iframe");
|
|
288
|
+
let currentTime = 0;
|
|
289
|
+
let playing = false;
|
|
290
|
+
const seekCalls: SeekCall[] = [];
|
|
291
|
+
const adapter = {
|
|
292
|
+
play: vi.fn(() => {
|
|
293
|
+
playing = true;
|
|
294
|
+
}),
|
|
295
|
+
pause: vi.fn(() => {
|
|
296
|
+
playing = false;
|
|
297
|
+
}),
|
|
298
|
+
seek: vi.fn((time: number, options?: { keepPlaying?: boolean }) => {
|
|
299
|
+
currentTime = time;
|
|
300
|
+
seekCalls.push({ time, options });
|
|
301
|
+
}),
|
|
302
|
+
getTime: () => currentTime,
|
|
303
|
+
getDuration: () => duration,
|
|
304
|
+
isPlaying: () => playing,
|
|
305
|
+
setTime: (t: number) => {
|
|
306
|
+
currentTime = t;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
Object.defineProperty(iframe, "contentWindow", {
|
|
310
|
+
value: {
|
|
311
|
+
__player: adapter,
|
|
312
|
+
postMessage: () => {},
|
|
313
|
+
scrollTo: () => {},
|
|
314
|
+
addEventListener: () => {},
|
|
315
|
+
removeEventListener: () => {},
|
|
316
|
+
},
|
|
317
|
+
configurable: true,
|
|
318
|
+
});
|
|
319
|
+
Object.defineProperty(iframe, "contentDocument", {
|
|
320
|
+
value: document.implementation.createHTMLDocument("preview"),
|
|
321
|
+
configurable: true,
|
|
322
|
+
});
|
|
323
|
+
act(() => {
|
|
324
|
+
api.iframeRef.current = iframe;
|
|
325
|
+
api.onIframeLoad();
|
|
326
|
+
});
|
|
327
|
+
return { adapter, seekCalls };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function installRafCapture(): {
|
|
331
|
+
flushOne: () => boolean;
|
|
332
|
+
restore: () => void;
|
|
333
|
+
} {
|
|
334
|
+
const callbacks: FrameRequestCallback[] = [];
|
|
335
|
+
const originalRAF = globalThis.requestAnimationFrame;
|
|
336
|
+
const originalCancel = globalThis.cancelAnimationFrame;
|
|
337
|
+
globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
|
|
338
|
+
callbacks.push(cb);
|
|
339
|
+
return callbacks.length;
|
|
340
|
+
}) as typeof requestAnimationFrame;
|
|
341
|
+
globalThis.cancelAnimationFrame = (() => {}) as typeof cancelAnimationFrame;
|
|
342
|
+
return {
|
|
343
|
+
flushOne: () => {
|
|
344
|
+
const next = callbacks.shift();
|
|
345
|
+
if (!next) return false;
|
|
346
|
+
next(performance.now());
|
|
347
|
+
return true;
|
|
348
|
+
},
|
|
349
|
+
restore: () => {
|
|
350
|
+
globalThis.requestAnimationFrame = originalRAF;
|
|
351
|
+
globalThis.cancelAnimationFrame = originalCancel;
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
it("passes { keepPlaying: true } when forward playback wraps around loopEnd", () => {
|
|
357
|
+
const raf = installRafCapture();
|
|
358
|
+
try {
|
|
359
|
+
const { api, root } = renderTimelinePlayerHarness();
|
|
360
|
+
const { adapter, seekCalls } = attachInstrumentedAdapter(api);
|
|
361
|
+
|
|
362
|
+
act(() => {
|
|
363
|
+
usePlayerStore.getState().setInPoint(2);
|
|
364
|
+
usePlayerStore.getState().setOutPoint(5);
|
|
365
|
+
});
|
|
366
|
+
expect(usePlayerStore.getState().loopEnabled).toBe(true);
|
|
367
|
+
|
|
368
|
+
act(() => {
|
|
369
|
+
api.play();
|
|
370
|
+
});
|
|
371
|
+
adapter.seek.mockClear();
|
|
372
|
+
seekCalls.length = 0;
|
|
373
|
+
|
|
374
|
+
adapter.setTime(6); // past outPoint=5
|
|
375
|
+
act(() => {
|
|
376
|
+
raf.flushOne();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const wrapSeek = seekCalls.find((call) => call.time === 2);
|
|
380
|
+
expect(wrapSeek).toBeDefined();
|
|
381
|
+
expect(wrapSeek?.options).toEqual({ keepPlaying: true });
|
|
382
|
+
expect(adapter.play).toHaveBeenCalled();
|
|
383
|
+
expect(usePlayerStore.getState().isPlaying).toBe(true);
|
|
384
|
+
|
|
385
|
+
unmountWithAct(root);
|
|
386
|
+
} finally {
|
|
387
|
+
raf.restore();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("does not seek and pauses cleanly when forward playback reaches the end without loop", () => {
|
|
392
|
+
const raf = installRafCapture();
|
|
393
|
+
try {
|
|
394
|
+
const { api, root } = renderTimelinePlayerHarness();
|
|
395
|
+
const { adapter, seekCalls } = attachInstrumentedAdapter(api);
|
|
396
|
+
|
|
397
|
+
act(() => {
|
|
398
|
+
usePlayerStore.getState().setLoopEnabled(false);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
act(() => {
|
|
402
|
+
api.play();
|
|
403
|
+
});
|
|
404
|
+
adapter.seek.mockClear();
|
|
405
|
+
seekCalls.length = 0;
|
|
406
|
+
adapter.play.mockClear();
|
|
407
|
+
adapter.pause.mockClear();
|
|
408
|
+
|
|
409
|
+
adapter.setTime(adapter.getDuration() + 1); // past end
|
|
410
|
+
act(() => {
|
|
411
|
+
raf.flushOne();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(seekCalls).toHaveLength(0);
|
|
415
|
+
expect(adapter.pause).toHaveBeenCalled();
|
|
416
|
+
expect(usePlayerStore.getState().isPlaying).toBe(false);
|
|
417
|
+
|
|
418
|
+
unmountWithAct(root);
|
|
419
|
+
} finally {
|
|
420
|
+
raf.restore();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -229,7 +229,8 @@ export function useTimelinePlayer() {
|
|
|
229
229
|
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
|
230
230
|
if (time >= loopEnd) {
|
|
231
231
|
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
|
232
|
-
adapter
|
|
232
|
+
// keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
|
|
233
|
+
adapter.seek(loopStart, { keepPlaying: true });
|
|
233
234
|
liveTime.notify(loopStart);
|
|
234
235
|
adapter.play();
|
|
235
236
|
setIsPlaying(true);
|
package/src/telemetry/events.ts
CHANGED
|
@@ -25,3 +25,35 @@ export function trackStudioRenderStart(props: {
|
|
|
25
25
|
composition: props.composition,
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
function getBrowserDoctorSummary(): string {
|
|
30
|
+
try {
|
|
31
|
+
const nav = navigator as Navigator & {
|
|
32
|
+
deviceMemory?: number;
|
|
33
|
+
connection?: { effectiveType?: string };
|
|
34
|
+
userAgentData?: { platform?: string };
|
|
35
|
+
};
|
|
36
|
+
const platform = nav.userAgentData?.platform ?? navigator.platform ?? "unknown";
|
|
37
|
+
const parts = [
|
|
38
|
+
`ua=${platform}`,
|
|
39
|
+
`screen=${screen.width}x${screen.height}@${devicePixelRatio}x`,
|
|
40
|
+
`lang=${navigator.language}`,
|
|
41
|
+
];
|
|
42
|
+
if (nav.deviceMemory) parts.push(`mem=${nav.deviceMemory}GB`);
|
|
43
|
+
if (nav.connection?.effectiveType) parts.push(`net=${nav.connection.effectiveType}`);
|
|
44
|
+
if (navigator.hardwareConcurrency) parts.push(`cpu=${navigator.hardwareConcurrency}cores`);
|
|
45
|
+
return parts.join(" ");
|
|
46
|
+
} catch {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function trackStudioFeedback(props: { rating: number; comment?: string }): void {
|
|
52
|
+
trackEvent("survey sent", {
|
|
53
|
+
$survey_id: "studio_experience",
|
|
54
|
+
$survey_response: props.rating,
|
|
55
|
+
...(props.comment ? { $survey_response_2: props.comment } : {}),
|
|
56
|
+
doctor_summary: getBrowserDoctorSummary(),
|
|
57
|
+
source: "studio",
|
|
58
|
+
});
|
|
59
|
+
}
|