@hyperframes/studio 0.8.65 → 0.8.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.65",
3
+ "version": "0.8.66",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,11 +51,11 @@
51
51
  "dockview-react": "8.3.1",
52
52
  "gsap": "^3.13.0",
53
53
  "mediabunny": "^1.45.3",
54
- "@hyperframes/player": "0.8.65",
55
- "@hyperframes/sdk": "0.8.65",
56
- "@hyperframes/parsers": "0.8.65",
57
- "@hyperframes/core": "0.8.65",
58
- "@hyperframes/studio-server": "0.8.65"
54
+ "@hyperframes/core": "0.8.66",
55
+ "@hyperframes/player": "0.8.66",
56
+ "@hyperframes/parsers": "0.8.66",
57
+ "@hyperframes/sdk": "0.8.66",
58
+ "@hyperframes/studio-server": "0.8.66"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@tailwindcss/vite": "^4.3.3",
@@ -71,7 +71,7 @@
71
71
  "vite": "^6.4.2",
72
72
  "vitest": "^4.1.11",
73
73
  "zustand": "^5.0.0",
74
- "@hyperframes/producer": "0.8.65"
74
+ "@hyperframes/producer": "0.8.66"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "react": "19",
@@ -474,6 +474,106 @@ describe("studio manual edits", () => {
474
474
  expect(frames).toHaveLength(0);
475
475
  });
476
476
 
477
+ it("does not reapply every frame while the player is paused over unpaused scene timelines", () => {
478
+ const window = new Window();
479
+ const frames: FrameRequestCallback[] = [];
480
+ let playing = false;
481
+ const previewWindow = window as unknown as Parameters<
482
+ typeof installStudioManualEditSeekReapply
483
+ >[0] & {
484
+ __player: Record<string, unknown>;
485
+ __timelines: Record<string, Record<string, unknown>>;
486
+ requestAnimationFrame: (callback: FrameRequestCallback) => number;
487
+ };
488
+ previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
489
+ frames.push(callback);
490
+ return frames.length;
491
+ };
492
+ previewWindow.__player = {
493
+ play: () => {
494
+ playing = true;
495
+ },
496
+ isPlaying: () => playing,
497
+ };
498
+ // A GSAP child of a paused master: not paused itself, inactive, time left.
499
+ const master = { paused: () => true };
500
+ previewWindow.__timelines = {
501
+ scene0: {
502
+ parent: master,
503
+ play: () => {},
504
+ paused: () => false,
505
+ isActive: () => false,
506
+ time: () => 0,
507
+ duration: () => 5,
508
+ },
509
+ };
510
+
511
+ expect(installStudioManualEditSeekReapply(previewWindow, () => {})).toBe(true);
512
+ expect(frames).toHaveLength(0);
513
+
514
+ (previewWindow.__player.play as () => void)();
515
+ expect(frames).toHaveLength(1);
516
+ playing = false;
517
+ frames.shift()?.(16);
518
+ expect(frames).toHaveLength(0);
519
+ });
520
+
521
+ it("tolerates a timeline whose parent chain loops", () => {
522
+ const window = new Window();
523
+ const previewWindow = window as unknown as Parameters<
524
+ typeof installStudioManualEditSeekReapply
525
+ >[0] & {
526
+ __timelines: Record<string, Record<string, unknown>>;
527
+ requestAnimationFrame: (callback: FrameRequestCallback) => number;
528
+ };
529
+ previewWindow.requestAnimationFrame = () => 1;
530
+ const loop: Record<string, unknown> = { paused: () => false };
531
+ loop.parent = loop;
532
+ previewWindow.__timelines = {
533
+ scene0: {
534
+ parent: loop,
535
+ play: () => {},
536
+ paused: () => false,
537
+ time: () => 0,
538
+ duration: () => 5,
539
+ },
540
+ };
541
+
542
+ expect(() => installStudioManualEditSeekReapply(previewWindow, () => {})).not.toThrow();
543
+ });
544
+
545
+ it("keeps reapplying while Studio drives a timeline directly and the player reads idle", () => {
546
+ const window = new Window();
547
+ const frames: FrameRequestCallback[] = [];
548
+ let paused = true;
549
+ const previewWindow = window as unknown as Parameters<
550
+ typeof installStudioManualEditSeekReapply
551
+ >[0] & {
552
+ __player: Record<string, unknown>;
553
+ __timeline: Record<string, unknown>;
554
+ requestAnimationFrame: (callback: FrameRequestCallback) => number;
555
+ };
556
+ previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
557
+ frames.push(callback);
558
+ return frames.length;
559
+ };
560
+ previewWindow.__player = { play: () => {}, isPlaying: () => false };
561
+ previewWindow.__timeline = {
562
+ play: () => {
563
+ paused = false;
564
+ },
565
+ paused: () => paused,
566
+ isActive: () => !paused,
567
+ time: () => 1,
568
+ duration: () => 5,
569
+ };
570
+
571
+ expect(installStudioManualEditSeekReapply(previewWindow, () => {})).toBe(true);
572
+ (previewWindow.__timeline.play as () => void)();
573
+ frames.shift()?.(16);
574
+ expect(frames).toHaveLength(1);
575
+ });
576
+
477
577
  it("stops playback reapply after an unpaused timeline has completed", () => {
478
578
  const window = new Window();
479
579
  const frames: FrameRequestCallback[] = [];
@@ -120,6 +120,21 @@ function hasRemainingTimelineTime(owner: Record<string, unknown>): boolean {
120
120
  return time < duration;
121
121
  }
122
122
 
123
+ function hasPausedAncestor(owner: Record<string, unknown>): boolean {
124
+ let ancestor = owner.parent;
125
+ for (let depth = 0; depth < 32 && ancestor && typeof ancestor === "object"; depth++) {
126
+ const node = ancestor as Record<string, unknown>;
127
+ const paused = node.paused;
128
+ try {
129
+ if (typeof paused === "function" && paused.call(node)) return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ ancestor = node.parent;
134
+ }
135
+ return false;
136
+ }
137
+
123
138
  function isTimelinePlaying(owner: Record<string, unknown> | undefined): boolean {
124
139
  if (!owner) return false;
125
140
  const isPlaying = owner.isPlaying;
@@ -148,6 +163,8 @@ function isTimelinePlaying(owner: Record<string, unknown> | undefined): boolean
148
163
  }
149
164
  }
150
165
 
166
+ // A GSAP child of a paused timeline reads unpaused with time left; only its ancestors know.
167
+ if (hasPausedAncestor(owner)) return false;
151
168
  return hasRemainingTimelineTime(owner);
152
169
  }
153
170