@hyperframes/studio 0.6.45 → 0.6.47

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/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-CaRE7VOD.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DpbZouXZ.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-SKRp8mGz.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.45",
3
+ "version": "0.6.47",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "@codemirror/view": "6.40.0",
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "mediabunny": "^1.45.3",
34
- "@hyperframes/core": "0.6.45",
35
- "@hyperframes/player": "0.6.45"
34
+ "@hyperframes/core": "0.6.47",
35
+ "@hyperframes/player": "0.6.47"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/react": "19",
@@ -46,7 +46,7 @@
46
46
  "vite": "^6.4.2",
47
47
  "vitest": "^3.2.4",
48
48
  "zustand": "^5.0.0",
49
- "@hyperframes/producer": "0.6.45"
49
+ "@hyperframes/producer": "0.6.47"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "19",
@@ -92,9 +92,16 @@ export function toOverlayRect(
92
92
  const root =
93
93
  doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement ?? null;
94
94
  const rootRect = root?.getBoundingClientRect();
95
- const rootWidth = rootRect?.width;
96
- const rootHeight = rootRect?.height;
97
- if (!rootWidth || !rootHeight) return null;
95
+ // Use the composition's declared dimensions (data-width/data-height) for scale
96
+ // calculation instead of rootRect.width/height. When GSAP applies transforms
97
+ // (scale, translate) to the root element, rootRect dimensions change but the
98
+ // composition's canonical size stays the same. Using rootRect causes overlay
99
+ // misalignment during animated playback.
100
+ const declaredWidth = readPositiveDimension(root?.getAttribute("data-width") ?? null);
101
+ const declaredHeight = readPositiveDimension(root?.getAttribute("data-height") ?? null);
102
+ const rootWidth = declaredWidth ?? rootRect?.width;
103
+ const rootHeight = declaredHeight ?? rootRect?.height;
104
+ if (!rootWidth || !rootHeight || !rootRect) return null;
98
105
 
99
106
  const elementRect = element.getBoundingClientRect();
100
107
  const rootScaleX = iframeRect.width / rootWidth;
@@ -111,8 +118,8 @@ export function toOverlayRect(
111
118
  });
112
119
 
113
120
  return {
114
- left: iframeRect.left - overlayRect.left + (elementRect.left - rootRect.left) * rootScaleX,
115
- top: iframeRect.top - overlayRect.top + (elementRect.top - rootRect.top) * rootScaleY,
121
+ left: iframeRect.left - overlayRect.left + elementRect.left * rootScaleX,
122
+ top: iframeRect.top - overlayRect.top + elementRect.top * rootScaleY,
116
123
  width: elementRect.width * rootScaleX,
117
124
  height: elementRect.height * rootScaleY,
118
125
  editScaleX: editScale.scaleX,
@@ -196,6 +196,29 @@ describe("createStaticSeekPlaybackAdapter", () => {
196
196
  expect(adapter.getDuration()).toBe(82);
197
197
  });
198
198
 
199
+ it("clamps time at the duration boundary during RAF tick", () => {
200
+ const clock = createManualAnimationClock();
201
+ const renderedTimes: number[] = [];
202
+ const adapter = createStaticSeekPlaybackAdapter(
203
+ {
204
+ getTime: () => 0,
205
+ renderSeek: (time: number) => {
206
+ renderedTimes.push(time);
207
+ },
208
+ },
209
+ 2,
210
+ clock,
211
+ );
212
+
213
+ adapter.seek(0);
214
+ adapter.play();
215
+ clock.step(3_000);
216
+
217
+ expect(adapter.getTime()).toBe(2);
218
+ expect(adapter.isPlaying()).toBe(false);
219
+ expect(renderedTimes).toEqual([0, 2]);
220
+ });
221
+
199
222
  it("pauses old adapter before replacing with new duration", () => {
200
223
  const clock = createManualAnimationClock();
201
224
  const adapter = createStaticSeekPlaybackAdapter(
@@ -226,11 +226,12 @@ export function useTimelinePlayer() {
226
226
  const tick = () => {
227
227
  const adapter = getAdapter();
228
228
  if (adapter) {
229
- const time = adapter.getTime();
229
+ const rawTime = adapter.getTime();
230
230
  const dur = adapter.getDuration();
231
+ const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
231
232
  liveTime.notify(time); // direct DOM updates, no React re-render
232
233
  const { inPoint, outPoint } = usePlayerStore.getState();
233
- const rawLoopEnd = outPoint !== null ? outPoint : dur;
234
+ const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
234
235
  const rawLoopStart = inPoint !== null ? inPoint : 0;
235
236
  const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
236
237
  const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
@@ -258,7 +259,6 @@ export function useTimelinePlayer() {
258
259
  const stopRAFLoop = useCallback(() => {
259
260
  cancelAnimationFrame(rafRef.current);
260
261
  }, []);
261
-
262
262
  const applyPlaybackRate = useCallback((rate: number) => {
263
263
  const iframe = iframeRef.current;
264
264
  if (!iframe) return;
@@ -280,7 +280,6 @@ export function useTimelinePlayer() {
280
280
  console.warn("[useTimelinePlayer] Could not set playback rate (cross-origin)", err);
281
281
  }
282
282
  }, []);
283
-
284
283
  const applyPreviewAudioState = useCallback((playbackRateOverride?: number) => {
285
284
  const { audioMuted, playbackRate } = usePlayerStore.getState();
286
285
  const effectivePlaybackRate = playbackRateOverride ?? playbackRate;
@@ -289,7 +288,6 @@ export function useTimelinePlayer() {
289
288
  shouldMutePreviewAudio(audioMuted, effectivePlaybackRate),
290
289
  );
291
290
  }, []);
292
-
293
291
  const play = useCallback(() => {
294
292
  stopRAFLoop();
295
293
  stopReverseLoop();
@@ -313,7 +311,6 @@ export function useTimelinePlayer() {
313
311
  stopRAFLoop,
314
312
  stopReverseLoop,
315
313
  ]);
316
-
317
314
  const playBackward = useCallback(
318
315
  (rate: number) => {
319
316
  stopRAFLoop();
@@ -334,7 +331,7 @@ export function useTimelinePlayer() {
334
331
  const elapsed = ((now - startedAt) / 1000) * speed;
335
332
  let nextTime = startTime - elapsed;
336
333
  const { inPoint, outPoint } = usePlayerStore.getState();
337
- const rawLoopEnd = outPoint !== null ? outPoint : duration;
334
+ const rawLoopEnd = outPoint !== null ? Math.min(outPoint, duration) : duration;
338
335
  const rawLoopStart = inPoint !== null ? inPoint : 0;
339
336
  const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : duration;
340
337
  const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
@@ -373,7 +370,6 @@ export function useTimelinePlayer() {
373
370
  stopReverseLoop,
374
371
  ],
375
372
  );
376
-
377
373
  const pause = useCallback(() => {
378
374
  stopReverseLoop();
379
375
  const adapter = getAdapter();
@@ -385,7 +381,6 @@ export function useTimelinePlayer() {
385
381
  shuttleSpeedIndexRef.current = 0;
386
382
  stopRAFLoop();
387
383
  }, [getAdapter, setCurrentTime, setIsPlaying, stopRAFLoop, stopReverseLoop]);
388
-
389
384
  const seek = useCallback(
390
385
  (time: number, options?: { keepPlaying?: boolean }) => {
391
386
  // Reverse shuttle is always stopped: the RAF reverse tick can't survive
@@ -431,7 +426,6 @@ export function useTimelinePlayer() {
431
426
  }
432
427
  });
433
428
  }, [seek]);
434
-
435
429
  const { playbackKeyDownRef, playbackKeyUpRef, attachIframeShortcutListeners, togglePlay } =
436
430
  usePlaybackKeyboard({
437
431
  iframeRef,
@@ -460,7 +454,6 @@ export function useTimelinePlayer() {
460
454
  attachIframeShortcutListeners,
461
455
  applyPreviewAudioState,
462
456
  });
463
-
464
457
  const saveSeekPosition = useCallback(() => {
465
458
  const adapter = getAdapter();
466
459
  pendingSeekRef.current = adapter
@@ -471,19 +464,15 @@ export function useTimelinePlayer() {
471
464
  stopReverseLoop();
472
465
  setIsPlaying(false);
473
466
  }, [getAdapter, stopRAFLoop, setIsPlaying, stopReverseLoop]);
474
-
475
467
  const refreshPlayer = useCallback(() => {
476
468
  const iframe = iframeRef.current;
477
469
  if (!iframe) return;
478
-
479
470
  saveSeekPosition();
480
-
481
471
  const src = iframe.src;
482
472
  const url = new URL(src, window.location.origin);
483
473
  url.searchParams.set("_t", String(Date.now()));
484
474
  iframe.src = url.toString();
485
475
  }, [saveSeekPosition]);
486
-
487
476
  const getAdapterRef = useRef(getAdapter);
488
477
  getAdapterRef.current = getAdapter;
489
478